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

Rearrange the verification integration tests, again (#3504)

* Element-R: Implement `CryptoApi.getVerificationRequestsToDeviceInProgress`

* Element-R: Implement `requestOwnUserVerification`

* init aliceClient *after* the fetch interceptors

* Initialise the test client separately for each test

* Avoid running all the tests twice

Currently all of these tests are running twice, with different client
configurations. That's not really adding much value; we just need to run
specific tests that way.

* Factor out functions for building responses
This commit is contained in:
Richard van der Hoff
2023-06-26 15:44:42 +01:00
committed by GitHub
parent e8fb47fdca
commit 326a13bcfe

View File

@ -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,
@ -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.
*
@ -96,16 +99,6 @@ 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;
@ -113,13 +106,13 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
/** 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 () => {
@ -127,28 +120,18 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
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();
@ -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
@ -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();
@ -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() {
@ -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();
@ -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();
@ -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);
@ -447,6 +408,7 @@ 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
@ -454,33 +416,14 @@ 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!;
// 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);
@ -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
@ -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
@ -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.
@ -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"],
},
};
}