1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

ElementR: Stub CheckOwnCrossSigningTrust, import cross signing keys and verify local device in bootstrapCrossSigning (#3608)

This commit is contained in:
Florian Duros
2023-07-25 19:03:43 +02:00
committed by GitHub
parent 8a80886358
commit 79d4113a6b
9 changed files with 288 additions and 14 deletions

View File

@ -18,9 +18,22 @@ import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
import { createClient, IAuthDict, MatrixClient } from "../../../src";
import { mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { encryptAES } from "../../../src/crypto/aes";
import { CryptoCallbacks } from "../../../src/crypto-api";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import {
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64,
SIGNED_CROSS_SIGNING_KEYS_DATA,
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
} from "../../test-utils/test-data";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@ -39,8 +52,32 @@ const TEST_DEVICE_ID = "xzcvb";
* to provide the most effective integration tests possible.
*/
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
// newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
// backend. Once we drop support for legacy crypto, it will go away.
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
let syncResponder: ISyncResponder;
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
// Encryption key used to encrypt cross signing keys
const encryptionKey = new Uint8Array(32);
/**
* Create the {@link CryptoCallbacks}
*/
function createCryptoCallbacks(): CryptoCallbacks {
return {
getSecretStorageKey: (keys, name) => {
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
},
};
}
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
@ -52,8 +89,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
cryptoCallbacks: createCryptoCallbacks(),
});
syncResponder = new SyncResponder(homeserverUrl);
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
/** an object which intercepts `/keys/upload` requests on the test homeserver */
new E2EKeyReceiver(homeserverUrl);
await initCrypto(aliceClient);
});
@ -68,7 +111,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
* @param authDict - The parameters to as the `auth` dict in the key upload request.
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
*/
async function bootstrapCrossSigning(authDict: IAuthDict): Promise<void> {
async function bootstrapCrossSigning(authDict: AuthDict): Promise<void> {
await aliceClient.getCrypto()?.bootstrapCrossSigning({
authUploadDeviceSigningKeys: (makeRequest) => makeRequest(authDict).then(() => undefined),
});
@ -105,6 +148,94 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
);
});
newBackendOnly("get cross signing keys from secret storage and import them", async () => {
// Return public cross signing keys
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
const masterKey = await encryptAES(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.master",
);
const selfSigningKey = await encryptAES(
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.self_signing",
);
const userSigningKey = await encryptAES(
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.user_signing",
);
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.cross_signing.master",
content: {
encrypted: {
key_id: masterKey,
},
},
},
{
type: "m.cross_signing.self_signing",
content: {
encrypted: {
key_id: selfSigningKey,
},
},
},
{
type: "m.cross_signing.user_signing",
content: {
encrypted: {
key_id: userSigningKey,
},
},
},
{
type: "m.secret_storage.key.key_id",
content: {
key: "key_id",
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
],
},
});
await aliceClient.startClient();
await syncPromise(aliceClient);
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
// we expect the UserTrustStatusChanged event to be fired after the cross signing keys import
const userTrustStatusChangedPromise = new Promise<string>((resolve) =>
aliceClient.on(CryptoEvent.UserTrustStatusChanged, resolve),
);
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
// Check if the UserTrustStatusChanged event was fired
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
// Expect the signature to be uploaded
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
const body = JSON.parse(sigsOpts!.body as string);
// the device should have a signature with the public self cross signing keys.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
);
});
});
describe("getCrossSigningStatus()", () => {

View File

@ -32,13 +32,16 @@ export function mockInitialApiRequests(homeserverUrl: string) {
/**
* Mock the requests needed to set up cross signing
*
* Return `{}` for `GET _matrix/client/r0/user/:userId/account_data/:type` request
* Return 404 error for `GET _matrix/client/r0/user/:userId/account_data/:type` request
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
*/
export function mockSetupCrossSigningRequests(): void {
// have account_data requests return an empty object
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
});
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});

View File

@ -71,6 +71,29 @@ def main() -> None:
b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
b64_master_private_key = encode_base64(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
)
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
)
b64_self_signing_public_key = encode_base64(
self_signing_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
b64_self_signing_private_key = encode_base64(
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
)
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
)
b64_user_signing_public_key = encode_base64(
user_signing_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
b64_user_signing_private_key = encode_base64(
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
)
print(
f"""\
@ -96,6 +119,21 @@ export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, ind
/** base64-encoded public master cross-signing key */
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
/** base64-encoded private master cross-signing key */
export const MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}";
/** base64-encoded public self cross-signing key */
export const SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}";
/** base64-encoded private self signing cross-signing key */
export const SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}";
/** base64-encoded public user cross-signing key */
export const USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}";
/** base64-encoded private user signing cross-signing key */
export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(), indent=4)

View File

@ -36,6 +36,21 @@ export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
/** base64-encoded public master cross-signing key */
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY";
/** base64-encoded private master cross-signing key */
export const MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "ZG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU";
/** base64-encoded public self cross-signing key */
export const SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY";
/** base64-encoded private self signing cross-signing key */
export const SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "c2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY";
/** base64-encoded public user cross-signing key */
export const USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY";
/** base64-encoded private user signing cross-signing key */
export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "dXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {

View File

@ -19,6 +19,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { CrossSigningIdentity } from "../../../src/rust-crypto/CrossSigningIdentity";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
describe("CrossSigningIdentity", () => {
describe("bootstrapCrossSigning", () => {
@ -31,6 +32,9 @@ describe("CrossSigningIdentity", () => {
/** A mock OutgoingRequestProcessor which crossSigning is connected to */
let outgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;
/** A mock ServerSideSecretStorage which crossSigning is connected to */
let secretStorage: Mocked<ServerSideSecretStorage>;
beforeEach(async () => {
await RustSdkCryptoJs.initAsync();
@ -44,7 +48,11 @@ describe("CrossSigningIdentity", () => {
makeOutgoingRequest: jest.fn(),
} as unknown as Mocked<OutgoingRequestProcessor>;
crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor);
secretStorage = {
get: jest.fn(),
} as unknown as Mocked<ServerSideSecretStorage>;
crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor, secretStorage, jest.fn());
});
it("should do nothing if keys are present on-device and in secret storage", async () => {

View File

@ -2250,7 +2250,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
// re-emit the events emitted by the crypto impl
this.reEmitter.reEmit(rustCrypto, [CryptoEvent.VerificationRequestReceived]);
this.reEmitter.reEmit(rustCrypto, [
CryptoEvent.VerificationRequestReceived,
CryptoEvent.UserTrustStatusChanged,
]);
}
/**
@ -2679,12 +2682,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Check the copy of our cross-signing key that we have in the device list and
* see if we can get the private key. If so, mark it as trusted.
* @param opts - ICheckOwnCrossSigningTrustOpts object
*
* @deprecated Unneeded for the new crypto
*/
public checkOwnCrossSigningTrust(opts?: ICheckOwnCrossSigningTrustOpts): Promise<void> {
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.checkOwnCrossSigningTrust(opts);
return this.cryptoBackend.checkOwnCrossSigningTrust(opts);
}
/**

View File

@ -90,6 +90,15 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* @returns the cross signing information for the user.
*/
getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null;
/**
* Check the cross signing trust of the current user
*
* @param opts - Options object.
*
* @deprecated Unneeded for the new crypto
*/
checkOwnCrossSigningTrust(opts?: CheckOwnCrossSigningTrustOpts): Promise<void>;
}
/** The methods which crypto implementations should expose to the Sync api
@ -165,3 +174,10 @@ export interface OnSyncCompletedData {
*/
catchingUp?: boolean;
}
/**
* Options object for {@link CryptoBackend#checkOwnCrossSigningTrust}.
*/
export interface CheckOwnCrossSigningTrustOpts {
allowPrivateKeyRequests?: boolean;
}

View File

@ -15,11 +15,13 @@ limitations under the License.
*/
import { OlmMachine, CrossSigningStatus } from "@matrix-org/matrix-sdk-crypto-wasm";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { BootstrapCrossSigningOpts } from "../crypto-api";
import { logger } from "../logger";
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
import { UIAuthCallback } from "../interactive-auth";
import { ServerSideSecretStorage } from "../secret-storage";
/** Manages the cross-signing keys for our own user.
*
@ -29,6 +31,9 @@ export class CrossSigningIdentity {
public constructor(
private readonly olmMachine: OlmMachine,
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
private readonly secretStorage: ServerSideSecretStorage,
/** Called if the cross signing keys are imported from the secret storage */
private readonly onCrossSigningKeysImport: () => void,
) {}
/**
@ -41,7 +46,15 @@ export class CrossSigningIdentity {
}
const olmDeviceStatus: CrossSigningStatus = await this.olmMachine.crossSigningStatus();
const privateKeysInSecretStorage = false; // TODO
// Try to fetch cross signing keys from the secret storage
const masterKeyFromSecretStorage = await this.secretStorage.get("m.cross_signing.master");
const selfSigningKeyFromSecretStorage = await this.secretStorage.get("m.cross_signing.self_signing");
const userSigningKeyFromSecretStorage = await this.secretStorage.get("m.cross_signing.user_signing");
const privateKeysInSecretStorage = Boolean(
masterKeyFromSecretStorage && selfSigningKeyFromSecretStorage && userSigningKeyFromSecretStorage,
);
const olmDeviceHasKeys =
olmDeviceStatus.hasMaster && olmDeviceStatus.hasUserSigning && olmDeviceStatus.hasSelfSigning;
@ -67,7 +80,23 @@ export class CrossSigningIdentity {
"bootStrapCrossSigning: Cross-signing private keys not found locally, but they are available " +
"in secret storage, reading storage and caching locally",
);
throw new Error("TODO");
await this.olmMachine.importCrossSigningKeys(
masterKeyFromSecretStorage,
selfSigningKeyFromSecretStorage,
userSigningKeyFromSecretStorage,
);
// Get the current device
const device: RustSdkCryptoJs.Device = await this.olmMachine.getDevice(
this.olmMachine.userId,
this.olmMachine.deviceId,
);
// Sign the device with our cross-signing key and upload the signature
const request: RustSdkCryptoJs.SignatureUploadRequest = await device.verify();
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
this.onCrossSigningKeysImport();
}
// TODO: we might previously have bootstrapped cross-signing but not completed uploading the keys to the

View File

@ -108,7 +108,17 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
this.eventDecryptor = new EventDecryptor(olmMachine);
this.crossSigningIdentity = new CrossSigningIdentity(olmMachine, this.outgoingRequestProcessor);
// Fire if the cross signing keys are imported from the secret storage
const onCrossSigningKeysImport = (): void => {
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(this.userId));
};
this.crossSigningIdentity = new CrossSigningIdentity(
olmMachine,
this.outgoingRequestProcessor,
secretStorage,
onCrossSigningKeysImport,
);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -194,6 +204,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
return null;
}
/**
* This function is unneeded for the rust-crypto.
* The cross signing key import and the device verification are done in {@link CryptoApi#bootstrapCrossSigning}
*
* The function is stub to keep the compatibility with the old crypto.
* More information: https://github.com/vector-im/element-web/issues/25648
*
*
* Implementation of {@link CryptoBackend#checkOwnCrossSigningTrust}
*/
public async checkOwnCrossSigningTrust(): Promise<void> {
return;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// CryptoApi implementation
@ -1019,11 +1043,16 @@ class EventDecryptor {
}
}
type RustCryptoEvents = CryptoEvent.VerificationRequestReceived;
type RustCryptoEvents = CryptoEvent.VerificationRequestReceived | CryptoEvent.UserTrustStatusChanged;
type RustCryptoEventMap = {
/**
* Fires when a key verification request is received.
*/
[CryptoEvent.VerificationRequestReceived]: (request: VerificationRequest) => void;
/**
* Fires when the cross signing keys are imported during {@link CryptoApi#bootstrapCrossSigning}
*/
[CryptoEvent.UserTrustStatusChanged]: (userId: string, userTrustLevel: UserTrustLevel) => void;
};