Skip to content

Commit fb4845d

Browse files
committed
Integration test for QR code verification
Followup to #3436: another integration test, this time using the QR code flow
1 parent 858155e commit fb4845d

File tree

3 files changed

+260
-3
lines changed

3 files changed

+260
-3
lines changed

spec/integ/crypto/verification.spec.ts

Lines changed: 121 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,
@@ -71,6 +73,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
7173
});
7274

7375
await initCrypto(aliceClient);
76+
77+
// stub out global.crypto
78+
Object.defineProperty(globalThis, "crypto", {
79+
value: {
80+
getRandomValues: function <T extends Uint8Array>(array: T): T {
81+
array.fill(0x12);
82+
return array;
83+
},
84+
},
85+
});
7486
});
7587

7688
afterEach(async () => {
@@ -208,6 +220,110 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
208220
olmSAS.free();
209221
});
210222

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

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)