Skip to content

Commit f5f6100

Browse files
authored
Integration test for QR code verification (#3439)
* Integration test for QR code verification Followup to #3436: another integration test, this time using the QR code flow * Use Object.defineProperty, and restore afterwards Apparently global.crypto exists in some environments * apply ts-ignore * remove stray comment * Update spec/integ/crypto/verification.spec.ts
1 parent ecd700a commit f5f6100

File tree

3 files changed

+275
-3
lines changed

3 files changed

+275
-3
lines changed

spec/integ/crypto/verification.spec.ts

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ import fetchMock from "fetch-mock-jest";
1818
import { MockResponse } from "fetch-mock";
1919

2020
import { createClient, MatrixClient } from "../../../src";
21-
import { ShowSasCallbacks, VerifierEvent } from "../../../src/crypto-api/verification";
21+
import { ShowQrCodeCallbacks, ShowSasCallbacks, VerifierEvent } from "../../../src/crypto-api/verification";
2222
import { escapeRegExp } from "../../../src/utils";
2323
import { VerificationBase } from "../../../src/crypto/verification/Base";
2424
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
2525
import { SyncResponder } from "../../test-utils/SyncResponder";
2626
import {
27+
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
28+
SIGNED_CROSS_SIGNING_KEYS_DATA,
2729
SIGNED_TEST_DEVICE_DATA,
2830
TEST_DEVICE_ID,
2931
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
@@ -40,6 +42,34 @@ import {
4042
// to ensure that we don't end up with dangling timeouts.
4143
jest.useFakeTimers();
4244

45+
let previousCrypto: Crypto | undefined;
46+
47+
beforeAll(() => {
48+
// Stub out global.crypto
49+
previousCrypto = global["crypto"];
50+
51+
Object.defineProperty(global, "crypto", {
52+
value: {
53+
getRandomValues: function <T extends Uint8Array>(array: T): T {
54+
array.fill(0x12);
55+
return array;
56+
},
57+
},
58+
});
59+
});
60+
61+
// restore the original global.crypto
62+
afterAll(() => {
63+
if (previousCrypto === undefined) {
64+
// @ts-ignore deleting a non-optional property. It *is* optional really.
65+
delete global.crypto;
66+
} else {
67+
Object.defineProperty(global, "crypto", {
68+
value: previousCrypto,
69+
});
70+
}
71+
});
72+
4373
/**
4474
* Integration tests for verification functionality.
4575
*
@@ -208,6 +238,107 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
208238
olmSAS.free();
209239
});
210240

241+
oldBackendOnly(
242+
"Outgoing verification: can verify another device via QR code with an untrusted cross-signing key",
243+
async () => {
244+
// expect requests to download our own keys
245+
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
246+
device_keys: {
247+
[TEST_USER_ID]: {
248+
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
249+
},
250+
},
251+
...SIGNED_CROSS_SIGNING_KEYS_DATA,
252+
});
253+
254+
// QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
255+
//
256+
// Completing the initial sync will make the device list download outdated device lists (of which our own
257+
// user will be one).
258+
syncResponder.sendOrQueueSyncResponse({});
259+
// DeviceList has a sleep(5) which we need to make happen
260+
await jest.advanceTimersByTimeAsync(10);
261+
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();
262+
263+
// have alice initiate a verification. She should send a m.key.verification.request
264+
const [requestBody, request] = await Promise.all([
265+
expectSendToDeviceMessage("m.key.verification.request"),
266+
aliceClient.requestVerification(TEST_USER_ID, [TEST_DEVICE_ID]),
267+
]);
268+
const transactionId = request.channel.transactionId;
269+
270+
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
271+
expect(toDeviceMessage.methods).toContain("m.qr_code.show.v1");
272+
expect(toDeviceMessage.methods).toContain("m.qr_code.scan.v1");
273+
expect(toDeviceMessage.methods).toContain("m.reciprocate.v1");
274+
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
275+
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
276+
277+
// The dummy device replies with an m.key.verification.ready, with an indication we can scan the QR code
278+
returnToDeviceMessageFromSync({
279+
type: "m.key.verification.ready",
280+
content: {
281+
from_device: TEST_DEVICE_ID,
282+
methods: ["m.qr_code.scan.v1"],
283+
transaction_id: transactionId,
284+
},
285+
});
286+
await waitForVerificationRequestChanged(request);
287+
expect(request.phase).toEqual(Phase.Ready);
288+
289+
// we should now have QR data we can display
290+
const qrCodeData = request.qrCodeData!;
291+
expect(qrCodeData).toBeTruthy();
292+
const qrCodeBuffer = qrCodeData.getBuffer();
293+
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
294+
expect(qrCodeBuffer.subarray(0, 6).toString("latin1")).toEqual("MATRIX");
295+
expect(qrCodeBuffer.readUint8(6)).toEqual(0x02); // version
296+
expect(qrCodeBuffer.readUint8(7)).toEqual(0x02); // mode
297+
const txnIdLen = qrCodeBuffer.readUint16BE(8);
298+
expect(qrCodeBuffer.subarray(10, 10 + txnIdLen).toString("utf-8")).toEqual(transactionId);
299+
// Alice's device's public key comes next, but we have nothing to do with it here.
300+
// const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
301+
expect(qrCodeBuffer.subarray(42 + txnIdLen, 32 + 42 + txnIdLen)).toEqual(
302+
Buffer.from(MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, "base64"),
303+
);
304+
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
305+
306+
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
307+
returnToDeviceMessageFromSync({
308+
type: "m.key.verification.start",
309+
content: {
310+
from_device: TEST_DEVICE_ID,
311+
method: "m.reciprocate.v1",
312+
transaction_id: transactionId,
313+
secret: encodeUnpaddedBase64(sharedSecret),
314+
},
315+
});
316+
await waitForVerificationRequestChanged(request);
317+
expect(request.phase).toEqual(Phase.Started);
318+
expect(request.chosenMethod).toEqual("m.reciprocate.v1");
319+
320+
// there should now be a verifier
321+
const verifier: VerificationBase = request.verifier!;
322+
expect(verifier).toBeDefined();
323+
324+
// ... which we call .verify on, which emits a ShowReciprocateQr event
325+
const verificationPromise = verifier.verify();
326+
const reciprocateQRCodeCallbacks = await new Promise<ShowQrCodeCallbacks>((resolve) => {
327+
verifier.once(VerifierEvent.ShowReciprocateQr, resolve);
328+
});
329+
330+
// Alice confirms she is happy
331+
reciprocateQRCodeCallbacks.confirm();
332+
333+
// that should satisfy Alice, who should reply with a 'done'
334+
await expectSendToDeviceMessage("m.key.verification.done");
335+
336+
// ... and the whole thing should be done!
337+
await verificationPromise;
338+
expect(request.phase).toEqual(Phase.Done);
339+
},
340+
);
341+
211342
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
212343
ev.sender ??= TEST_USER_ID;
213344
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
@@ -253,3 +384,7 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
253384
//console.info(`Test MAC: input:'${input}, info: '${info}' -> '${mac}`);
254385
return mac;
255386
}
387+
388+
function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
389+
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
390+
}

spec/test-utils/test-data/generate-test-data.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
# any 32-byte string can be an ed25519 private key.
3838
TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef"
3939

40+
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"doyouspeakwhaaaaaaaaaaaaaaaaaale"
41+
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"useruseruseruseruseruseruseruser"
42+
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"selfselfselfselfselfselfselfself"
43+
4044

4145
def main() -> None:
4246
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
@@ -57,10 +61,17 @@ def main() -> None:
5761
"user_id": TEST_USER_ID,
5862
}
5963

60-
device_data["signatures"][TEST_USER_ID][ f"ed25519:{TEST_DEVICE_ID}"] = sign_json(
64+
device_data["signatures"][TEST_USER_ID][f"ed25519:{TEST_DEVICE_ID}"] = sign_json(
6165
device_data, private_key
6266
)
6367

68+
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
69+
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
70+
)
71+
b64_master_public_key = encode_base64(
72+
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
73+
)
74+
6475
print(
6576
f"""\
6677
/* Test data for cryptography tests
@@ -69,6 +80,7 @@ def main() -> None:
6980
*/
7081
7182
import {{ IDeviceKeys }} from "../../../src/@types/crypto";
83+
import {{ IDownloadKeyResult }} from "../../../src";
7284
7385
/* eslint-disable comma-dangle */
7486
@@ -80,8 +92,82 @@ def main() -> None:
8092
8193
/** Signed device data, suitable for returning from a `/keys/query` call */
8294
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)};
83-
""", end='',
95+
96+
/** base64-encoded public master cross-signing key */
97+
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
98+
99+
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
100+
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
101+
json.dumps(build_cross_signing_keys_data(), indent=4)
102+
};
103+
""",
104+
end="",
105+
)
106+
107+
108+
def build_cross_signing_keys_data() -> dict:
109+
"""Build the signed cross-signing-keys data for return from /keys/query"""
110+
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
111+
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
84112
)
113+
b64_master_public_key = encode_base64(
114+
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
115+
)
116+
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
117+
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
118+
)
119+
b64_self_signing_public_key = encode_base64(
120+
self_signing_private_key.public_key().public_bytes(
121+
Encoding.Raw, PublicFormat.Raw
122+
)
123+
)
124+
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
125+
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
126+
)
127+
b64_user_signing_public_key = encode_base64(
128+
user_signing_private_key.public_key().public_bytes(
129+
Encoding.Raw, PublicFormat.Raw
130+
)
131+
)
132+
# create without signatures initially
133+
cross_signing_keys_data = {
134+
"master_keys": {
135+
TEST_USER_ID: {
136+
"keys": {
137+
f"ed25519:{b64_master_public_key}": b64_master_public_key,
138+
},
139+
"user_id": TEST_USER_ID,
140+
"usage": ["master"],
141+
}
142+
},
143+
"self_signing_keys": {
144+
TEST_USER_ID: {
145+
"keys": {
146+
f"ed25519:{b64_self_signing_public_key}": b64_self_signing_public_key,
147+
},
148+
"user_id": TEST_USER_ID,
149+
"usage": ["self_signing"],
150+
},
151+
},
152+
"user_signing_keys": {
153+
TEST_USER_ID: {
154+
"keys": {
155+
f"ed25519:{b64_user_signing_public_key}": b64_user_signing_public_key,
156+
},
157+
"user_id": TEST_USER_ID,
158+
"usage": ["user_signing"],
159+
},
160+
},
161+
}
162+
# sign the sub-keys with the master
163+
for k in ["self_signing_keys", "user_signing_keys"]:
164+
to_sign = cross_signing_keys_data[k][TEST_USER_ID]
165+
sig = sign_json(to_sign, master_private_key)
166+
to_sign["signatures"] = {
167+
TEST_USER_ID: {f"ed25519:{b64_master_public_key}": sig}
168+
}
169+
170+
return cross_signing_keys_data
85171

86172

87173
def encode_base64(input_bytes: bytes) -> str:

spec/test-utils/test-data/index.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { IDeviceKeys } from "../../../src/@types/crypto";
7+
import { IDownloadKeyResult } from "../../../src";
78

89
/* eslint-disable comma-dangle */
910

@@ -31,3 +32,53 @@ export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
3132
}
3233
}
3334
};
35+
36+
/** base64-encoded public master cross-signing key */
37+
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY";
38+
39+
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
40+
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
41+
"master_keys": {
42+
"@alice:localhost": {
43+
"keys": {
44+
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY"
45+
},
46+
"user_id": "@alice:localhost",
47+
"usage": [
48+
"master"
49+
]
50+
}
51+
},
52+
"self_signing_keys": {
53+
"@alice:localhost": {
54+
"keys": {
55+
"ed25519:aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY": "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY"
56+
},
57+
"user_id": "@alice:localhost",
58+
"usage": [
59+
"self_signing"
60+
],
61+
"signatures": {
62+
"@alice:localhost": {
63+
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "XfhYEhZmOs8BJdb3viatILBZ/bElsHXEW28V4tIaY5CxrBR0YOym3yZHWmRmypXessHZAKOhZn3yBMXzdajyCw"
64+
}
65+
}
66+
}
67+
},
68+
"user_signing_keys": {
69+
"@alice:localhost": {
70+
"keys": {
71+
"ed25519:g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY": "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY"
72+
},
73+
"user_id": "@alice:localhost",
74+
"usage": [
75+
"user_signing"
76+
],
77+
"signatures": {
78+
"@alice:localhost": {
79+
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "6AkD1XM2H0/ebgP9oBdMKNeft7uxsrb0XN1CsjjHgeZCvCTMmv3BHlLiT/Hzy4fe8H+S1tr484dcXN/PIdnfDA"
80+
}
81+
}
82+
}
83+
}
84+
};

0 commit comments

Comments
 (0)