Skip to content

Commit e112e34

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 e112e34

File tree

3 files changed

+261
-3
lines changed

3 files changed

+261
-3
lines changed

spec/integ/crypto/verification.spec.ts

Lines changed: 122 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,17 @@ import {
4042
// to ensure that we don't end up with dangling timeouts.
4143
jest.useFakeTimers();
4244

45+
// Stub out global.crypto
46+
//
47+
// This shouldn't leak into other files, because jest gives each file a new environment.
48+
global["crypto"] = {
49+
// @ts-ignore this doesn't match the type in typescript, but that doesn't really matter
50+
getRandomValues: function <T extends Uint8Array>(array: T): T {
51+
array.fill(0x12);
52+
return array;
53+
},
54+
};
55+
4356
/**
4457
* Integration tests for verification functionality.
4558
*
@@ -208,6 +221,110 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
208221
olmSAS.free();
209222
});
210223

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

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)