Skip to content

Rearrange the verification integration tests, again #3504

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 26, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 75 additions & 89 deletions spec/integ/crypto/verification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { MockResponse } from "fetch-mock";
import fetchMock from "fetch-mock-jest";
import { IDBFactory } from "fake-indexeddb";

import { createClient, CryptoEvent, MatrixClient } from "../../../src";
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src";
import {
canAcceptVerificationRequest,
ShowQrCodeCallbacks,
Expand Down Expand Up @@ -88,6 +88,9 @@ afterAll(() => {
}
});

/** The homeserver url that we give to the test client, and where we intercept /sync, /keys, etc requests. */
const TEST_HOMESERVER_URL = "https://alice-server.com";

/**
* Integration tests for verification functionality.
*
Expand All @@ -96,59 +99,39 @@ afterAll(() => {
*/
// we test with both crypto stacks...
describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: string, initCrypto: InitCrypto) => {
// and with (1) the default verification method list, (2) a custom verification method list.
describe.each([undefined, ["m.sas.v1", "m.qr_code.show.v1", "m.reciprocate.v1"]])(
"supported methods=%s",
(methods) => {
runTests(backend, initCrypto, methods);
},
);
});

function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | undefined) {
// 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;

/** the client under test */
let aliceClient: MatrixClient;

/** an object which intercepts `/sync` requests from {@link #aliceClient} */
/** an object which intercepts `/sync` requests on the test homeserver */
let syncResponder: SyncResponder;

/** an object which intercepts `/keys/query` requests from {@link #aliceClient} */
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;

/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} */
/** an object which intercepts `/keys/upload` requests on the test homeserver */
let e2eKeyReceiver: E2EKeyReceiver;

beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;

const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: "device_under_test",
verificationMethods: methods,
});

await initCrypto(aliceClient);

e2eKeyReceiver = new E2EKeyReceiver(aliceClient.getHomeserverUrl());
e2eKeyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);

syncResponder = new SyncResponder(aliceClient.getHomeserverUrl());
mockInitialApiRequests(aliceClient.getHomeserverUrl());
await aliceClient.startClient();
mockInitialApiRequests(TEST_HOMESERVER_URL);
});

afterEach(async () => {
await aliceClient.stopClient();
if (aliceClient !== undefined) {
await aliceClient.stopClient();
}

// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
Expand All @@ -162,7 +145,10 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
});

it("can verify another device via SAS", async () => {
// test with (1) the default verification method list, (2) a custom verification method list.
const TEST_METHODS = ["m.sas.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
it.each([undefined, TEST_METHODS])("can verify via SAS (supported methods=%s)", async (methods) => {
aliceClient = await startTestClient({ verificationMethods: methods });
await waitForDeviceList();

// initially there should be no verifications in progress
Expand All @@ -176,7 +162,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId;
const transactionId = request.transactionId!;
expect(transactionId).toBeDefined();
expect(request.phase).toEqual(VerificationPhase.Requested);
expect(request.roomId).toBeUndefined();
Expand All @@ -202,32 +188,14 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
}

// The dummy device replies with an m.key.verification.ready...
returnToDeviceMessageFromSync({
type: "m.key.verification.ready",
content: {
from_device: TEST_DEVICE_ID,
methods: ["m.sas.v1"],
transaction_id: transactionId,
},
});
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);
expect(request.otherDeviceId).toEqual(TEST_DEVICE_ID);

// ... and picks a method with m.key.verification.start
returnToDeviceMessageFromSync({
type: "m.key.verification.start",
content: {
from_device: TEST_DEVICE_ID,
method: "m.sas.v1",
transaction_id: transactionId,
hashes: ["sha256"],
key_agreement_protocols: ["curve25519-hkdf-sha256"],
message_authentication_codes: ["hkdf-hmac-sha256.v2"],
// we have to include "decimal" per the spec.
short_authentication_string: ["decimal", "emoji"],
},
});
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));

// as soon as the Changed event arrives, `verifier` should be defined
const verifier = await new Promise<Verifier>((resolve) => {
function onChange() {
Expand Down Expand Up @@ -327,6 +295,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
});

it("Can make a verification request to *all* devices", async () => {
aliceClient = await startTestClient();
// we need an existing cross-signing key for this
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
await waitForDeviceList();
Expand Down Expand Up @@ -356,6 +325,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
});

oldBackendOnly("can verify another via QR code with an untrusted cross-signing key", async () => {
aliceClient = await startTestClient();
// QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
await waitForDeviceList();
Expand All @@ -366,26 +336,17 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId;
const transactionId = request.transactionId!;

const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.methods).toContain("m.qr_code.show.v1");
expect(toDeviceMessage.methods).toContain("m.reciprocate.v1");
if (methods === undefined) {
expect(toDeviceMessage.methods).toContain("m.qr_code.scan.v1");
}
expect(toDeviceMessage.methods).toContain("m.qr_code.scan.v1");
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(transactionId);

// The dummy device replies with an m.key.verification.ready, with an indication we can scan the QR code
returnToDeviceMessageFromSync({
type: "m.key.verification.ready",
content: {
from_device: TEST_DEVICE_ID,
methods: ["m.qr_code.scan.v1"],
transaction_id: transactionId,
},
});
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.qr_code.scan.v1"]));
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);

Expand Down Expand Up @@ -447,40 +408,22 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
});

it("can cancel during the SAS phase", async () => {
aliceClient = await startTestClient();
await waitForDeviceList();

// have alice initiate a verification. She should send a m.key.verification.request
const [, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId;
const transactionId = request.transactionId!;

// The dummy device replies with an m.key.verification.ready...
returnToDeviceMessageFromSync({
type: "m.key.verification.ready",
content: {
from_device: TEST_DEVICE_ID,
methods: ["m.sas.v1"],
transaction_id: transactionId,
},
});
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
await waitForVerificationRequestChanged(request);

// ... and picks a method with m.key.verification.start
returnToDeviceMessageFromSync({
type: "m.key.verification.start",
content: {
from_device: TEST_DEVICE_ID,
method: "m.sas.v1",
transaction_id: transactionId,
hashes: ["sha256"],
key_agreement_protocols: ["curve25519-hkdf-sha256"],
message_authentication_codes: ["hkdf-hmac-sha256.v2"],
// we have to include "decimal" per the spec.
short_authentication_string: ["decimal", "emoji"],
},
});
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Started);

Expand Down Expand Up @@ -514,6 +457,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
});

oldBackendOnly("Incoming verification: can accept", async () => {
aliceClient = await startTestClient();
const TRANSACTION_ID = "abcd";

// Initiate the request by sending a to-device message
Expand Down Expand Up @@ -551,6 +495,19 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
});
});

async function startTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
const client = createClient({
baseUrl: TEST_HOMESERVER_URL,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: "device_under_test",
...opts,
});
await initCrypto(client);
await client.startClient();
return client;
}

/** make sure that the client knows about the dummy device */
async function waitForDeviceList(): Promise<void> {
// Completing the initial sync will make the device list download outdated device lists (of which our own
Expand All @@ -568,7 +525,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
ev.sender ??= TEST_USER_ID;
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
}
}
});

/**
* Wait for the client under test to send a to-device message of the given type.
Expand Down Expand Up @@ -613,3 +570,32 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
}

/** build an m.key.verification.ready to-device message originating from the dummy device */
function buildReadyMessage(transactionId: string, methods: string[]): { type: string; content: object } {
return {
type: "m.key.verification.ready",
content: {
from_device: TEST_DEVICE_ID,
methods: methods,
transaction_id: transactionId,
},
};
}

/** build an m.key.verification.start to-device message suitable for the SAS flow, originating from the dummy device */
function buildSasStartMessage(transactionId: string): { type: string; content: object } {
return {
type: "m.key.verification.start",
content: {
from_device: TEST_DEVICE_ID,
method: "m.sas.v1",
transaction_id: transactionId,
hashes: ["sha256"],
key_agreement_protocols: ["curve25519-hkdf-sha256"],
message_authentication_codes: ["hkdf-hmac-sha256.v2"],
// we have to include "decimal" per the spec.
short_authentication_string: ["decimal", "emoji"],
},
};
}