From 79d4113a6b0e3ff8bf70e62115ed42be28d0d612 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 25 Jul 2023 19:03:43 +0200 Subject: [PATCH] ElementR: Stub `CheckOwnCrossSigningTrust`, import cross signing keys and verify local device in `bootstrapCrossSigning` (#3608) --- spec/integ/crypto/cross-signing.spec.ts | 139 +++++++++++++++++- spec/test-utils/mockEndpoints.ts | 7 +- .../test-data/generate-test-data.py | 38 +++++ spec/test-utils/test-data/index.ts | 15 ++ .../rust-crypto/CrossSigningIdentity.spec.ts | 10 +- src/client.ts | 11 +- src/common-crypto/CryptoBackend.ts | 16 ++ src/rust-crypto/CrossSigningIdentity.ts | 33 ++++- src/rust-crypto/rust-crypto.ts | 33 ++++- 9 files changed, 288 insertions(+), 14 deletions(-) diff --git a/spec/integ/crypto/cross-signing.spec.ts b/spec/integ/crypto/cross-signing.spec.ts index 4043379e1..3e03461e5 100644 --- a/spec/integ/crypto/cross-signing.spec.ts +++ b/spec/integ/crypto/cross-signing.spec.ts @@ -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 { + async function bootstrapCrossSigning(authDict: AuthDict): Promise { 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((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()", () => { diff --git a/spec/test-utils/mockEndpoints.ts b/spec/test-utils/mockEndpoints.ts index bd5bb819a..386b7a4cb 100644 --- a/spec/test-utils/mockEndpoints.ts +++ b/spec/test-utils/mockEndpoints.ts @@ -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" }, {}); diff --git a/spec/test-utils/test-data/generate-test-data.py b/spec/test-utils/test-data/generate-test-data.py index 3ba7dd4a8..d48bb37d7 100755 --- a/spec/test-utils/test-data/generate-test-data.py +++ b/spec/test-utils/test-data/generate-test-data.py @@ -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 = { json.dumps(build_cross_signing_keys_data(), indent=4) diff --git a/spec/test-utils/test-data/index.ts b/spec/test-utils/test-data/index.ts index 07ff12225..c25a2f78a 100644 --- a/spec/test-utils/test-data/index.ts +++ b/spec/test-utils/test-data/index.ts @@ -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 = { "master_keys": { diff --git a/spec/unit/rust-crypto/CrossSigningIdentity.spec.ts b/spec/unit/rust-crypto/CrossSigningIdentity.spec.ts index 6187ec7ed..83d747162 100644 --- a/spec/unit/rust-crypto/CrossSigningIdentity.spec.ts +++ b/spec/unit/rust-crypto/CrossSigningIdentity.spec.ts @@ -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; + /** A mock ServerSideSecretStorage which crossSigning is connected to */ + let secretStorage: Mocked; + beforeEach(async () => { await RustSdkCryptoJs.initAsync(); @@ -44,7 +48,11 @@ describe("CrossSigningIdentity", () => { makeOutgoingRequest: jest.fn(), } as unknown as Mocked; - crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor); + secretStorage = { + get: jest.fn(), + } as unknown as Mocked; + + crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor, secretStorage, jest.fn()); }); it("should do nothing if keys are present on-device and in secret storage", async () => { diff --git a/src/client.ts b/src/client.ts index 6fc342492..fc7f2bef3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2250,7 +2250,10 @@ export class MatrixClient extends TypedEventEmitter { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.checkOwnCrossSigningTrust(opts); + return this.cryptoBackend.checkOwnCrossSigningTrust(opts); } /** diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts index c6888437d..b99410d07 100644 --- a/src/common-crypto/CryptoBackend.ts +++ b/src/common-crypto/CryptoBackend.ts @@ -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; } /** 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; +} diff --git a/src/rust-crypto/CrossSigningIdentity.ts b/src/rust-crypto/CrossSigningIdentity.ts index 2c43ed603..41f16b90e 100644 --- a/src/rust-crypto/CrossSigningIdentity.ts +++ b/src/rust-crypto/CrossSigningIdentity.ts @@ -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 diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 7e267071b..f0404d5c0 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -108,7 +108,17 @@ export class RustCrypto extends TypedEventEmitter { + 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 { + 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; };