You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-07 23:02:56 +03:00
Integration test for QR code verification (#3439)
* Integration test for QR code verification Followup to https://github.com/matrix-org/matrix-js-sdk/pull/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
This commit is contained in:
committed by
GitHub
parent
ecd700a36e
commit
f5f6100b1e
@@ -18,12 +18,14 @@ import fetchMock from "fetch-mock-jest";
|
|||||||
import { MockResponse } from "fetch-mock";
|
import { MockResponse } from "fetch-mock";
|
||||||
|
|
||||||
import { createClient, MatrixClient } from "../../../src";
|
import { createClient, MatrixClient } from "../../../src";
|
||||||
import { ShowSasCallbacks, VerifierEvent } from "../../../src/crypto-api/verification";
|
import { ShowQrCodeCallbacks, ShowSasCallbacks, VerifierEvent } from "../../../src/crypto-api/verification";
|
||||||
import { escapeRegExp } from "../../../src/utils";
|
import { escapeRegExp } from "../../../src/utils";
|
||||||
import { VerificationBase } from "../../../src/crypto/verification/Base";
|
import { VerificationBase } from "../../../src/crypto/verification/Base";
|
||||||
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
|
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
|
||||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||||
import {
|
import {
|
||||||
|
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
|
||||||
|
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||||
SIGNED_TEST_DEVICE_DATA,
|
SIGNED_TEST_DEVICE_DATA,
|
||||||
TEST_DEVICE_ID,
|
TEST_DEVICE_ID,
|
||||||
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||||
@@ -40,6 +42,34 @@ import {
|
|||||||
// to ensure that we don't end up with dangling timeouts.
|
// to ensure that we don't end up with dangling timeouts.
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
let previousCrypto: Crypto | undefined;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// Stub out global.crypto
|
||||||
|
previousCrypto = global["crypto"];
|
||||||
|
|
||||||
|
Object.defineProperty(global, "crypto", {
|
||||||
|
value: {
|
||||||
|
getRandomValues: function <T extends Uint8Array>(array: T): T {
|
||||||
|
array.fill(0x12);
|
||||||
|
return array;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// restore the original global.crypto
|
||||||
|
afterAll(() => {
|
||||||
|
if (previousCrypto === undefined) {
|
||||||
|
// @ts-ignore deleting a non-optional property. It *is* optional really.
|
||||||
|
delete global.crypto;
|
||||||
|
} else {
|
||||||
|
Object.defineProperty(global, "crypto", {
|
||||||
|
value: previousCrypto,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration tests for verification functionality.
|
* Integration tests for verification functionality.
|
||||||
*
|
*
|
||||||
@@ -208,6 +238,107 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
|||||||
olmSAS.free();
|
olmSAS.free();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
oldBackendOnly(
|
||||||
|
"Outgoing verification: can verify another device via QR code with an untrusted cross-signing key",
|
||||||
|
async () => {
|
||||||
|
// expect requests to download our own keys
|
||||||
|
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
|
||||||
|
device_keys: {
|
||||||
|
[TEST_USER_ID]: {
|
||||||
|
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||||
|
});
|
||||||
|
|
||||||
|
// QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
|
||||||
|
//
|
||||||
|
// Completing the initial sync will make the device list download outdated device lists (of which our own
|
||||||
|
// user will be one).
|
||||||
|
syncResponder.sendOrQueueSyncResponse({});
|
||||||
|
// DeviceList has a sleep(5) which we need to make happen
|
||||||
|
await jest.advanceTimersByTimeAsync(10);
|
||||||
|
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();
|
||||||
|
|
||||||
|
// have alice initiate a verification. She should send a m.key.verification.request
|
||||||
|
const [requestBody, request] = await Promise.all([
|
||||||
|
expectSendToDeviceMessage("m.key.verification.request"),
|
||||||
|
aliceClient.requestVerification(TEST_USER_ID, [TEST_DEVICE_ID]),
|
||||||
|
]);
|
||||||
|
const transactionId = request.channel.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.qr_code.scan.v1");
|
||||||
|
expect(toDeviceMessage.methods).toContain("m.reciprocate.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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await waitForVerificationRequestChanged(request);
|
||||||
|
expect(request.phase).toEqual(Phase.Ready);
|
||||||
|
|
||||||
|
// we should now have QR data we can display
|
||||||
|
const qrCodeData = request.qrCodeData!;
|
||||||
|
expect(qrCodeData).toBeTruthy();
|
||||||
|
const qrCodeBuffer = qrCodeData.getBuffer();
|
||||||
|
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
|
||||||
|
expect(qrCodeBuffer.subarray(0, 6).toString("latin1")).toEqual("MATRIX");
|
||||||
|
expect(qrCodeBuffer.readUint8(6)).toEqual(0x02); // version
|
||||||
|
expect(qrCodeBuffer.readUint8(7)).toEqual(0x02); // mode
|
||||||
|
const txnIdLen = qrCodeBuffer.readUint16BE(8);
|
||||||
|
expect(qrCodeBuffer.subarray(10, 10 + txnIdLen).toString("utf-8")).toEqual(transactionId);
|
||||||
|
// Alice's device's public key comes next, but we have nothing to do with it here.
|
||||||
|
// const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
|
||||||
|
expect(qrCodeBuffer.subarray(42 + txnIdLen, 32 + 42 + txnIdLen)).toEqual(
|
||||||
|
Buffer.from(MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, "base64"),
|
||||||
|
);
|
||||||
|
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
|
||||||
|
|
||||||
|
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
|
||||||
|
returnToDeviceMessageFromSync({
|
||||||
|
type: "m.key.verification.start",
|
||||||
|
content: {
|
||||||
|
from_device: TEST_DEVICE_ID,
|
||||||
|
method: "m.reciprocate.v1",
|
||||||
|
transaction_id: transactionId,
|
||||||
|
secret: encodeUnpaddedBase64(sharedSecret),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await waitForVerificationRequestChanged(request);
|
||||||
|
expect(request.phase).toEqual(Phase.Started);
|
||||||
|
expect(request.chosenMethod).toEqual("m.reciprocate.v1");
|
||||||
|
|
||||||
|
// there should now be a verifier
|
||||||
|
const verifier: VerificationBase = request.verifier!;
|
||||||
|
expect(verifier).toBeDefined();
|
||||||
|
|
||||||
|
// ... which we call .verify on, which emits a ShowReciprocateQr event
|
||||||
|
const verificationPromise = verifier.verify();
|
||||||
|
const reciprocateQRCodeCallbacks = await new Promise<ShowQrCodeCallbacks>((resolve) => {
|
||||||
|
verifier.once(VerifierEvent.ShowReciprocateQr, resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alice confirms she is happy
|
||||||
|
reciprocateQRCodeCallbacks.confirm();
|
||||||
|
|
||||||
|
// that should satisfy Alice, who should reply with a 'done'
|
||||||
|
await expectSendToDeviceMessage("m.key.verification.done");
|
||||||
|
|
||||||
|
// ... and the whole thing should be done!
|
||||||
|
await verificationPromise;
|
||||||
|
expect(request.phase).toEqual(Phase.Done);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
|
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
|
||||||
ev.sender ??= TEST_USER_ID;
|
ev.sender ??= TEST_USER_ID;
|
||||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
|
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
|
||||||
@@ -253,3 +384,7 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
|
|||||||
//console.info(`Test MAC: input:'${input}, info: '${info}' -> '${mac}`);
|
//console.info(`Test MAC: input:'${input}, info: '${info}' -> '${mac}`);
|
||||||
return mac;
|
return mac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||||
|
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
|
||||||
|
}
|
||||||
|
@@ -37,6 +37,10 @@ TEST_DEVICE_ID = "test_device"
|
|||||||
# any 32-byte string can be an ed25519 private key.
|
# any 32-byte string can be an ed25519 private key.
|
||||||
TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef"
|
TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef"
|
||||||
|
|
||||||
|
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"doyouspeakwhaaaaaaaaaaaaaaaaaale"
|
||||||
|
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"useruseruseruseruseruseruseruser"
|
||||||
|
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"selfselfselfselfselfselfselfself"
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||||
@@ -57,10 +61,17 @@ def main() -> None:
|
|||||||
"user_id": TEST_USER_ID,
|
"user_id": TEST_USER_ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
device_data["signatures"][TEST_USER_ID][ f"ed25519:{TEST_DEVICE_ID}"] = sign_json(
|
device_data["signatures"][TEST_USER_ID][f"ed25519:{TEST_DEVICE_ID}"] = sign_json(
|
||||||
device_data, private_key
|
device_data, private_key
|
||||||
)
|
)
|
||||||
|
|
||||||
|
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||||
|
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||||
|
)
|
||||||
|
b64_master_public_key = encode_base64(
|
||||||
|
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||||
|
)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"""\
|
f"""\
|
||||||
/* Test data for cryptography tests
|
/* Test data for cryptography tests
|
||||||
@@ -69,6 +80,7 @@ def main() -> None:
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {{ IDeviceKeys }} from "../../../src/@types/crypto";
|
import {{ IDeviceKeys }} from "../../../src/@types/crypto";
|
||||||
|
import {{ IDownloadKeyResult }} from "../../../src";
|
||||||
|
|
||||||
/* eslint-disable comma-dangle */
|
/* eslint-disable comma-dangle */
|
||||||
|
|
||||||
@@ -80,10 +92,84 @@ export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}";
|
|||||||
|
|
||||||
/** Signed device data, suitable for returning from a `/keys/query` call */
|
/** Signed device data, suitable for returning from a `/keys/query` call */
|
||||||
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)};
|
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)};
|
||||||
""", end='',
|
|
||||||
|
/** base64-encoded public master cross-signing key */
|
||||||
|
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
|
||||||
|
|
||||||
|
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||||
|
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||||
|
json.dumps(build_cross_signing_keys_data(), indent=4)
|
||||||
|
};
|
||||||
|
""",
|
||||||
|
end="",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_cross_signing_keys_data() -> dict:
|
||||||
|
"""Build the signed cross-signing-keys data for return from /keys/query"""
|
||||||
|
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||||
|
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||||
|
)
|
||||||
|
b64_master_public_key = encode_base64(
|
||||||
|
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||||
|
)
|
||||||
|
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||||
|
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||||
|
)
|
||||||
|
b64_self_signing_public_key = encode_base64(
|
||||||
|
self_signing_private_key.public_key().public_bytes(
|
||||||
|
Encoding.Raw, PublicFormat.Raw
|
||||||
|
)
|
||||||
|
)
|
||||||
|
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||||
|
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||||
|
)
|
||||||
|
b64_user_signing_public_key = encode_base64(
|
||||||
|
user_signing_private_key.public_key().public_bytes(
|
||||||
|
Encoding.Raw, PublicFormat.Raw
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# create without signatures initially
|
||||||
|
cross_signing_keys_data = {
|
||||||
|
"master_keys": {
|
||||||
|
TEST_USER_ID: {
|
||||||
|
"keys": {
|
||||||
|
f"ed25519:{b64_master_public_key}": b64_master_public_key,
|
||||||
|
},
|
||||||
|
"user_id": TEST_USER_ID,
|
||||||
|
"usage": ["master"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"self_signing_keys": {
|
||||||
|
TEST_USER_ID: {
|
||||||
|
"keys": {
|
||||||
|
f"ed25519:{b64_self_signing_public_key}": b64_self_signing_public_key,
|
||||||
|
},
|
||||||
|
"user_id": TEST_USER_ID,
|
||||||
|
"usage": ["self_signing"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user_signing_keys": {
|
||||||
|
TEST_USER_ID: {
|
||||||
|
"keys": {
|
||||||
|
f"ed25519:{b64_user_signing_public_key}": b64_user_signing_public_key,
|
||||||
|
},
|
||||||
|
"user_id": TEST_USER_ID,
|
||||||
|
"usage": ["user_signing"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
# sign the sub-keys with the master
|
||||||
|
for k in ["self_signing_keys", "user_signing_keys"]:
|
||||||
|
to_sign = cross_signing_keys_data[k][TEST_USER_ID]
|
||||||
|
sig = sign_json(to_sign, master_private_key)
|
||||||
|
to_sign["signatures"] = {
|
||||||
|
TEST_USER_ID: {f"ed25519:{b64_master_public_key}": sig}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cross_signing_keys_data
|
||||||
|
|
||||||
|
|
||||||
def encode_base64(input_bytes: bytes) -> str:
|
def encode_base64(input_bytes: bytes) -> str:
|
||||||
"""Encode with unpadded base64"""
|
"""Encode with unpadded base64"""
|
||||||
output_bytes = base64.b64encode(input_bytes)
|
output_bytes = base64.b64encode(input_bytes)
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IDeviceKeys } from "../../../src/@types/crypto";
|
import { IDeviceKeys } from "../../../src/@types/crypto";
|
||||||
|
import { IDownloadKeyResult } from "../../../src";
|
||||||
|
|
||||||
/* eslint-disable comma-dangle */
|
/* eslint-disable comma-dangle */
|
||||||
|
|
||||||
@@ -31,3 +32,53 @@ export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** base64-encoded public master cross-signing key */
|
||||||
|
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY";
|
||||||
|
|
||||||
|
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||||
|
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||||
|
"master_keys": {
|
||||||
|
"@alice:localhost": {
|
||||||
|
"keys": {
|
||||||
|
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY"
|
||||||
|
},
|
||||||
|
"user_id": "@alice:localhost",
|
||||||
|
"usage": [
|
||||||
|
"master"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"self_signing_keys": {
|
||||||
|
"@alice:localhost": {
|
||||||
|
"keys": {
|
||||||
|
"ed25519:aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY": "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY"
|
||||||
|
},
|
||||||
|
"user_id": "@alice:localhost",
|
||||||
|
"usage": [
|
||||||
|
"self_signing"
|
||||||
|
],
|
||||||
|
"signatures": {
|
||||||
|
"@alice:localhost": {
|
||||||
|
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "XfhYEhZmOs8BJdb3viatILBZ/bElsHXEW28V4tIaY5CxrBR0YOym3yZHWmRmypXessHZAKOhZn3yBMXzdajyCw"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user_signing_keys": {
|
||||||
|
"@alice:localhost": {
|
||||||
|
"keys": {
|
||||||
|
"ed25519:g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY": "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY"
|
||||||
|
},
|
||||||
|
"user_id": "@alice:localhost",
|
||||||
|
"usage": [
|
||||||
|
"user_signing"
|
||||||
|
],
|
||||||
|
"signatures": {
|
||||||
|
"@alice:localhost": {
|
||||||
|
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "6AkD1XM2H0/ebgP9oBdMKNeft7uxsrb0XN1CsjjHgeZCvCTMmv3BHlLiT/Hzy4fe8H+S1tr484dcXN/PIdnfDA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user