From d90ae11e2be9912784c7a90b9665ee1f1bd840a7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:45:26 +0000 Subject: [PATCH] Expose new method `CryptoApi.crossSignDevice` (#3930) --- spec/integ/crypto/cross-signing.spec.ts | 52 +++++++++++++++++++++++++ src/crypto-api.ts | 17 +++++++- src/crypto/index.ts | 9 +++++ src/rust-crypto/rust-crypto.ts | 21 ++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/spec/integ/crypto/cross-signing.spec.ts b/spec/integ/crypto/cross-signing.spec.ts index 04ade62c186..c31b7d4ad9f 100644 --- a/spec/integ/crypto/cross-signing.spec.ts +++ b/spec/integ/crypto/cross-signing.spec.ts @@ -31,8 +31,10 @@ import { SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64, SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64, SIGNED_CROSS_SIGNING_KEYS_DATA, + SIGNED_TEST_DEVICE_DATA, USER_CROSS_SIGNING_PRIVATE_KEY_BASE64, } from "../../test-utils/test-data"; +import * as testData from "../../test-utils/test-data"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; afterEach(() => { @@ -97,6 +99,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s /** an object which intercepts `/keys/upload` requests on the test homeserver */ new E2EKeyReceiver(homeserverUrl); + // Silence warnings from the backup manager + fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), { + status: 404, + body: { errcode: "M_NOT_FOUND" }, + }); + await initCrypto(aliceClient); }); @@ -339,4 +347,48 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s expect(userSigningKeyId).toBe(getPubKey(crossSigningKeys.user_signing_key)); }); }); + + describe("crossSignDevice", () => { + beforeEach(async () => { + jest.useFakeTimers(); + + // make sure that there is another device which we can sign + e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA); + + // Complete initialsync, to get the outgoing requests going + mockInitialApiRequests(aliceClient.getHomeserverUrl()); + syncResponder.sendOrQueueSyncResponse({ next_batch: 1 }); + await aliceClient.startClient(); + await syncPromise(aliceClient); + + // Wait for legacy crypto to find the device + await jest.advanceTimersByTimeAsync(10); + + const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([aliceClient.getSafeUserId()]); + expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy(); + }); + + afterEach(async () => { + jest.useRealTimers(); + }); + + it("fails for an unknown device", async () => { + await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device"); + }); + + it("cross-signs the device", async () => { + mockSetupCrossSigningRequests(); + await aliceClient.getCrypto()!.bootstrapCrossSigning({}); + + fetchMock.mockClear(); + await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID); + + // check that a sig for the device was uploaded + const calls = fetchMock.calls("upload-sigs"); + expect(calls.length).toEqual(1); + const body = JSON.parse(calls[0][1]!.body as string); + const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID]; + expect(deviceSig).toHaveProperty("signatures"); + }); + }); }); diff --git a/src/crypto-api.ts b/src/crypto-api.ts index 3520ee0cbe9..babc4ad7091 100644 --- a/src/crypto-api.ts +++ b/src/crypto-api.ts @@ -162,7 +162,7 @@ export interface CryptoApi { /** * Mark the given device as locally verified. * - * Marking a devices as locally verified has much the same effect as completing the verification dance, or receiving + * Marking a device as locally verified has much the same effect as completing the verification dance, or receiving * a cross-signing signature for it. * * @param userId - owner of the device @@ -175,6 +175,21 @@ export interface CryptoApi { */ setDeviceVerified(userId: string, deviceId: string, verified?: boolean): Promise; + /** + * Cross-sign one of our own devices. + * + * This will create a signature for the device using our self-signing key, and publish that signature. + * Cross-signing a device indicates, to our other devices and to other users, that we have verified that it really + * belongs to us. + * + * Requires that cross-signing has been set up on this device (normally by calling {@link bootstrapCrossSigning}. + * + * *Note*: Do not call this unless you have verified, somehow, that the device is genuine! + * + * @param deviceId - ID of the device to be signed. + */ + crossSignDevice(deviceId: string): Promise; + /** * Checks whether cross signing: * - is enabled on this account and trusted by this device diff --git a/src/crypto/index.ts b/src/crypto/index.ts index f643491cf05..7bf98139d5a 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -2306,6 +2306,15 @@ export class Crypto extends TypedEventEmitter { + await this.setDeviceVerified(this.userId, deviceId, true); + } + /** * Update the blocked/verified state of the given device * diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 3eeaf4cbc07..f4952df7306 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -572,6 +572,27 @@ export class RustCrypto extends TypedEventEmitter { + const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice( + new RustSdkCryptoJs.UserId(this.userId), + new RustSdkCryptoJs.DeviceId(deviceId), + ); + if (!device) { + throw new Error(`Unknown device ${deviceId}`); + } + try { + const outgoingRequest: RustSdkCryptoJs.SignatureUploadRequest = await device.verify(); + await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest); + } finally { + device.free(); + } + } + /** * Implementation of {@link CryptoApi#getDeviceVerificationStatus}. */