1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

ElementR: Add CryptoApi#bootstrapSecretStorage (#3483)

* Add WIP bootstrapSecretStorage

* Add new test if `createSecretStorageKey` is not set

* Remove old comments

* Add docs for `crypto-api.bootstrapSecretStorage`

* Remove default parameter for `createSecretStorageKey`

* Move `bootstrapSecretStorage` next to `isSecretStorageReady`

* Deprecate `bootstrapSecretStorage` in `MatrixClient`

* Update documentations

* Raise error if missing `keyInfo`

* Update behavior around `setupNewSecretStorage`

* Move `ICreateSecretStorageOpts` to `rust-crypto`

* Move `ICryptoCallbacks` to `rust-crypto`

* Update `bootstrapSecretStorage` documentation

* Add partial `CryptoCallbacks` documentation

* Fix typo

* Review changes

* Review changes
This commit is contained in:
Florian Duros
2023-06-20 10:40:11 +02:00
committed by GitHub
parent 8df4be0939
commit 49f11578f7
11 changed files with 365 additions and 87 deletions

View File

@@ -74,7 +74,7 @@ module.exports = {
"jest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly", "newBackendOnly"],
},
],
},

View File

@@ -51,6 +51,7 @@ import { escapeRegExp } from "../../../src/utils";
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
import { flushPromises } from "../../test-utils/flushPromises";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
const ROOM_ID = "!room:id";
@@ -402,6 +403,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
const newBackendOnly = backend !== "rust-sdk" ? test.skip : test;
const Olm = global.Olm;
@@ -2169,4 +2171,163 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
);
});
});
describe("bootstrapSecretStorage", () => {
/**
* Create a fake secret storage key
* Async because `bootstrapSecretStorage` expect an async method
*/
const createSecretStorageKey = jest.fn().mockResolvedValue({
keyInfo: {}, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
});
/**
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/:type`
* Resolved when a key is uploaded (ie in `body.content.key`)
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
*/
function awaitKeyStoredInAccountData(): Promise<string> {
return new Promise((resolve) => {
// This url is called multiple times during the secret storage bootstrap process
// When we received the newly generated key, we return it
fetchMock.put(
"express:/_matrix/client/r0/user/:userId/account_data/:type",
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
if (content.key) {
resolve(content.key);
}
return {};
},
{ overwriteRoutes: true },
);
});
}
/**
* Send in the sync response the provided `secretStorageKey` into the account_data field
* The key is set for the `m.secret_storage.default_key` and `m.secret_storage.key.${secretStorageKey}` events
* https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv3sync
* @param secretStorageKey
*/
function sendSyncResponse(secretStorageKey: string) {
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.secret_storage.default_key",
content: {
key: secretStorageKey,
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
// Needed for secretStorage.getKey or secretStorage.hasKey
{
type: `m.secret_storage.key.${secretStorageKey}`,
content: {
key: secretStorageKey,
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
],
},
});
}
beforeEach(async () => {
createSecretStorageKey.mockClear();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
});
newBackendOnly("should do no nothing if createSecretStorageKey is not set", async () => {
await aliceClient.getCrypto()!.bootstrapSecretStorage({ setupNewSecretStorage: true });
// No key was created
expect(createSecretStorageKey).toHaveBeenCalledTimes(0);
});
newBackendOnly("should create a new key", async () => {
const bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
// Wait for the key to be uploaded in the account data
const secretStorageKey = await awaitKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Finally, wait for bootstrapSecretStorage to finished
await bootstrapPromise;
const defaultKeyId = await aliceClient.secretStorage.getDefaultKeyId();
// Check that the uploaded key in stored in the secret storage
expect(await aliceClient.secretStorage.hasKey(secretStorageKey)).toBeTruthy();
// Check that the uploaded key is the default key
expect(defaultKeyId).toBe(secretStorageKey);
});
newBackendOnly(
"should do nothing if an AES key is already in the secret storage and setupNewSecretStorage is not set",
async () => {
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
// Wait for the key to be uploaded in the account data
const secretStorageKey = await awaitKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// Call again bootstrapSecretStorage
await aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
// createSecretStorageKey should be called only on the first run of bootstrapSecretStorage
expect(createSecretStorageKey).toHaveBeenCalledTimes(1);
},
);
newBackendOnly(
"should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage",
async () => {
let bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
// Wait for the key to be uploaded in the account data
let secretStorageKey = await awaitKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// Call again bootstrapSecretStorage
bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
// Wait for the key to be uploaded in the account data
secretStorageKey = await awaitKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
},
);
});
});

View File

@@ -28,7 +28,7 @@ import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
import { IEventDecryptionResult } from "../../../src/@types/crypto";
import { OutgoingRequest, OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
import { ImportRoomKeysOpts } from "../../../src/crypto-api";
import { CryptoCallbacks, ImportRoomKeysOpts } from "../../../src/crypto-api";
import * as testData from "../../test-utils/test-data";
afterEach(() => {
@@ -212,6 +212,7 @@ describe("RustCrypto", () => {
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
});
@@ -334,6 +335,7 @@ describe("RustCrypto", () => {
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
@@ -430,6 +432,7 @@ async function makeTestRustCrypto(
userId: string = TEST_USER,
deviceId: string = TEST_DEVICE_ID,
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks,
): Promise<RustCrypto> {
return await initRustCrypto(http, userId, deviceId, secretStorage);
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks);
}

View File

@@ -2223,7 +2223,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
// needed.
const RustCrypto = await import("./rust-crypto");
const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId, this.secretStorage);
const rustCrypto = await RustCrypto.initRustCrypto(
this.http,
userId,
deviceId,
this.secretStorage,
this.cryptoCallbacks,
);
this.cryptoBackend = rustCrypto;
// attach the event listeners needed by RustCrypto
@@ -2874,6 +2880,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
* algorithm is found
*
* @deprecated Use {@link CryptoApi#bootstrapSecretStorage}.
*/
public bootstrapSecretStorage(opts: ICreateSecretStorageOpts): Promise<void> {
if (!this.crypto) {

View File

@@ -18,8 +18,9 @@ import type { IMegolmSessionData } from "./@types/crypto";
import { Room } from "./models/room";
import { DeviceMap } from "./models/device";
import { UIAuthCallback } from "./interactive-auth";
import { AddSecretStorageKeyOpts } from "./secret-storage";
import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage";
import { VerificationRequest } from "./crypto-api/verification";
import { KeyBackupInfo } from "./crypto-api/keybackup";
/**
* Public interface to the cryptography parts of the js-sdk
@@ -190,6 +191,18 @@ export interface CryptoApi {
*/
isSecretStorageReady(): Promise<boolean>;
/**
* Bootstrap the secret storage by creating a new secret storage key and store it in the secret storage.
*
* - Do nothing if an AES key is already stored in the secret storage and `setupNewKeyBackup` is not set;
* - Generate a new key {@link GeneratedSecretStorageKey} with `createSecretStorageKey`.
* - Store this key in the secret storage and set it as the default key.
* - Call `cryptoCallbacks.cacheSecretStorageKey` if provided.
*
* @param opts - Options object.
*/
bootstrapSecretStorage(opts: CreateSecretStorageOpts): Promise<void>;
/**
* Get the status of our cross-signing keys.
*
@@ -377,6 +390,72 @@ export interface CrossSigningStatus {
};
}
/**
* Crypto callbacks provided by the application
*/
export interface CryptoCallbacks extends SecretStorageCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
/**
* Called by {@link CryptoApi#bootstrapSecretStorage}
* @param keyId - secret storage key id
* @param keyInfo - secret storage key info
* @param key - private key to store
*/
cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
onSecretRequested?: (
userId: string,
deviceId: string,
requestId: string,
secretName: string,
deviceTrust: DeviceVerificationStatus,
) => Promise<string | undefined>;
getDehydrationKey?: (
keyInfo: SecretStorageKeyDescription,
checkFunc: (key: Uint8Array) => void,
) => Promise<Uint8Array>;
getBackupKey?: () => Promise<Uint8Array>;
}
/**
* Parameter of {@link CryptoApi#bootstrapSecretStorage}
*/
export interface CreateSecretStorageOpts {
/**
* Function called to await a secret storage key creation flow.
* @returns Promise resolving to an object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
createSecretStorageKey?: () => Promise<GeneratedSecretStorageKey>;
/**
* The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
*/
keyBackupInfo?: KeyBackupInfo;
/**
* If true, a new key backup version will be
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
* is supplied.
*/
setupNewKeyBackup?: boolean;
/**
* Reset even if keys already exist.
*/
setupNewSecretStorage?: boolean;
/**
* Function called to get the user's
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
* containing the key, or rejects if the key cannot be obtained.
*/
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
}
/** Types of cross-signing key */
export enum CrossSigningKey {
Master = "master",
@@ -396,3 +475,4 @@ export interface GeneratedSecretStorageKey {
}
export * from "./crypto-api/verification";
export * from "./crypto-api/keybackup";

View File

@@ -0,0 +1,42 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ISigned } from "../@types/signed";
export interface Curve25519AuthData {
public_key: string;
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
export interface Aes256AuthData {
iv: string;
mac: string;
private_key_salt?: string;
private_key_iterations?: number;
}
/**
* Extra info of a recovery key
*/
export interface KeyBackupInfo {
algorithm: string;
auth_data: ISigned & (Curve25519AuthData | Aes256AuthData);
count?: number;
etag?: string;
version?: string; // number contained within
}

View File

@@ -15,13 +15,14 @@ limitations under the License.
*/
import { DeviceInfo } from "./deviceinfo";
import { IKeyBackupInfo } from "./keybackup";
import { GeneratedSecretStorageKey } from "../crypto-api";
/* re-exports for backwards compatibility. */
// CrossSigningKey is used as a value in `client.ts`, we can't export it as a type
export { CrossSigningKey } from "../crypto-api";
export type { GeneratedSecretStorageKey as IRecoveryKey } from "../crypto-api";
export type {
GeneratedSecretStorageKey as IRecoveryKey,
CreateSecretStorageOpts as ICreateSecretStorageOpts,
} from "../crypto-api";
export type {
ImportRoomKeyProgressData as IImportOpts,
@@ -67,38 +68,3 @@ export interface IEncryptedEventInfo {
*/
mismatchedSender: boolean;
}
export interface ICreateSecretStorageOpts {
/**
* Function called to await a secret storage key creation flow.
* @returns Promise resolving to an object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
createSecretStorageKey?: () => Promise<GeneratedSecretStorageKey>;
/**
* The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
*/
keyBackupInfo?: IKeyBackupInfo;
/**
* If true, a new key backup version will be
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
* is supplied.
*/
setupNewKeyBackup?: boolean;
/**
* Reset even if keys already exist.
*/
setupNewSecretStorage?: boolean;
/**
* Function called to get the user's
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
* containing the key, or rejects if the key cannot be obtained.
*/
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
}

View File

@@ -80,7 +80,6 @@ import {
AccountDataClient,
AddSecretStorageKeyOpts,
SECRET_STORAGE_ALGORITHM_V1_AES,
SecretStorageCallbacks,
SecretStorageKeyDescription,
SecretStorageKeyObject,
SecretStorageKeyTuple,
@@ -97,7 +96,10 @@ import { Device, DeviceMap } from "../models/device";
import { deviceInfoToDevice } from "./device-converter";
/* re-exports for backwards compatibility */
export type { BootstrapCrossSigningOpts as IBootstrapCrossSigningOpts } from "../crypto-api";
export type {
BootstrapCrossSigningOpts as IBootstrapCrossSigningOpts,
CryptoCallbacks as ICryptoCallbacks,
} from "../crypto-api";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -134,25 +136,6 @@ interface IInitOpts {
pickleKey?: string;
}
export interface ICryptoCallbacks extends SecretStorageCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
onSecretRequested?: (
userId: string,
deviceId: string,
requestId: string,
secretName: string,
deviceTrust: DeviceTrustLevel,
) => Promise<string | undefined>;
getDehydrationKey?: (
keyInfo: SecretStorageKeyDescription,
checkFunc: (key: Uint8Array) => void,
) => Promise<Uint8Array>;
getBackupKey?: () => Promise<Uint8Array>;
}
/* eslint-disable camelcase */
interface IRoomKey {
room_id: string;

View File

@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ISigned } from "../@types/signed";
import { IEncryptedPayload } from "./aes";
export interface Curve25519SessionData {
@@ -35,27 +34,13 @@ export interface IKeyBackupRoomSessions {
[sessionId: string]: IKeyBackupSession;
}
export interface ICurve25519AuthData {
public_key: string;
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
// Export for backward compatibility
export type {
Curve25519AuthData as ICurve25519AuthData,
Aes256AuthData as IAes256AuthData,
KeyBackupInfo as IKeyBackupInfo,
} from "../crypto-api/keybackup";
export interface IAes256AuthData {
iv: string;
mac: string;
private_key_salt?: string;
private_key_iterations?: number;
}
export interface IKeyBackupInfo {
algorithm: string;
auth_data: ISigned & (ICurve25519AuthData | IAes256AuthData);
count?: number;
etag?: string;
version?: string; // number contained within
}
/* eslint-enable camelcase */
export interface IKeyBackupPrepareOpts {

View File

@@ -21,6 +21,7 @@ import { logger } from "../logger";
import { RUST_SDK_STORE_PREFIX } from "./constants";
import { IHttpOpts, MatrixHttpApi } from "../http-api";
import { ServerSideSecretStorage } from "../secret-storage";
import { ICryptoCallbacks } from "../crypto";
/**
* Create a new `RustCrypto` implementation
@@ -30,12 +31,14 @@ import { ServerSideSecretStorage } from "../secret-storage";
* @param userId - The local user's User ID.
* @param deviceId - The local user's Device ID.
* @param secretStorage - Interface to server-side secret storage.
* @param cryptoCallbacks - Crypto callbacks provided by the application
*/
export async function initRustCrypto(
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
userId: string,
deviceId: string,
secretStorage: ServerSideSecretStorage,
cryptoCallbacks: ICryptoCallbacks,
): Promise<RustCrypto> {
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
await RustSdkCryptoJs.initAsync();
@@ -49,7 +52,7 @@ export async function initRustCrypto(
// TODO: use the pickle key for the passphrase
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass");
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage);
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage, cryptoCallbacks);
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
rustCrypto.onRoomKeysUpdated(sessions),
);

View File

@@ -39,11 +39,13 @@ import {
ImportRoomKeyProgressData,
ImportRoomKeysOpts,
VerificationRequest,
CreateSecretStorageOpts,
CryptoCallbacks,
} from "../crypto-api";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
import { Device, DeviceMap } from "../models/device";
import { AddSecretStorageKeyOpts, ServerSideSecretStorage } from "../secret-storage";
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage";
import { CrossSigningIdentity } from "./CrossSigningIdentity";
import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
import { keyFromPassphrase } from "../crypto/key_passphrase";
@@ -90,6 +92,9 @@ export class RustCrypto implements CryptoBackend {
/** Interface to server-side secret storage */
private readonly secretStorage: ServerSideSecretStorage,
/** Crypto callbacks provided by the application */
private readonly cryptoCallbacks: CryptoCallbacks,
) {
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
@@ -375,6 +380,49 @@ export class RustCrypto implements CryptoBackend {
return false;
}
/**
* Implementation of {@link CryptoApi#bootstrapSecretStorage}
*/
public async bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage,
}: CreateSecretStorageOpts = {}): Promise<void> {
// If createSecretStorageKey is not set, we stop
if (!createSecretStorageKey) return;
// See if we already have an AES secret-storage key.
const secretStorageKeyTuple = await this.secretStorage.getKey();
if (secretStorageKeyTuple) {
const [, keyInfo] = secretStorageKeyTuple;
// If an AES Key is already stored in the secret storage and setupNewSecretStorage is not set
// we don't want to create a new key
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES && !setupNewSecretStorage) {
return;
}
}
const recoveryKey = await createSecretStorageKey();
// keyInfo is required to continue
if (!recoveryKey.keyInfo) {
throw new Error("missing keyInfo field in the secret storage key created by createSecretStorageKey");
}
const secretStorageKeyObject = await this.secretStorage.addKey(
SECRET_STORAGE_ALGORITHM_V1_AES,
recoveryKey.keyInfo,
);
await this.secretStorage.setDefaultKeyId(secretStorageKeyObject.keyId);
this.cryptoCallbacks.cacheSecretStorageKey?.(
secretStorageKeyObject.keyId,
secretStorageKeyObject.keyInfo,
recoveryKey.privateKey,
);
}
/**
* Implementation of {@link CryptoApi#getCrossSigningStatus}
*/