Skip to content

Commit

Permalink
Expose new method CryptoApi.crossSignDevice (#3930)
Browse files Browse the repository at this point in the history
  • Loading branch information
richvdh authored Nov 29, 2023
1 parent 3f246c6 commit d90ae11
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 1 deletion.
52 changes: 52 additions & 0 deletions spec/integ/crypto/cross-signing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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");
});
});
});
17 changes: 16 additions & 1 deletion src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -175,6 +175,21 @@ export interface CryptoApi {
*/
setDeviceVerified(userId: string, deviceId: string, verified?: boolean): Promise<void>;

/**
* 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<void>;

/**
* Checks whether cross signing:
* - is enabled on this account and trusted by this device
Expand Down
9 changes: 9 additions & 0 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,15 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
await this.setDeviceVerification(userId, deviceId, verified);
}

/**
* Blindly cross-sign one of our other devices.
*
* Implementation of {@link CryptoApi#crossSignDevice}.
*/
public async crossSignDevice(deviceId: string): Promise<void> {
await this.setDeviceVerified(this.userId, deviceId, true);
}

/**
* Update the blocked/verified state of the given device
*
Expand Down
21 changes: 21 additions & 0 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,27 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
}
}

/**
* Blindly cross-sign one of our other devices.
*
* Implementation of {@link CryptoApi#crossSignDevice}.
*/
public async crossSignDevice(deviceId: string): Promise<void> {
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}.
*/
Expand Down

0 comments on commit d90ae11

Please sign in to comment.