Skip to content

Commit

Permalink
Integration test for QR code verification (#3439)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
richvdh authored and toger5 committed Jun 7, 2023
1 parent e1ce16a commit a21736c
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 3 deletions.
137 changes: 136 additions & 1 deletion spec/integ/crypto/verification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import fetchMock from "fetch-mock-jest";
import { MockResponse } from "fetch-mock";

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 { VerificationBase } from "../../../src/crypto/verification/Base";
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
import { SyncResponder } from "../../test-utils/SyncResponder";
import {
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
SIGNED_CROSS_SIGNING_KEYS_DATA,
SIGNED_TEST_DEVICE_DATA,
TEST_DEVICE_ID,
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
Expand All @@ -40,6 +42,34 @@ import {
// to ensure that we don't end up with dangling timeouts.
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.
*
Expand Down Expand Up @@ -208,6 +238,107 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
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 {
ev.sender ??= TEST_USER_ID;
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
Expand Down Expand Up @@ -253,3 +384,7 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
//console.info(`Test MAC: input:'${input}, info: '${info}' -> '${mac}`);
return mac;
}

function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
}
90 changes: 88 additions & 2 deletions spec/test-utils/test-data/generate-test-data.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
# any 32-byte string can be an ed25519 private key.
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:
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
Expand All @@ -57,10 +61,17 @@ def main() -> None:
"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
)

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(
f"""\
/* Test data for cryptography tests
Expand All @@ -69,6 +80,7 @@ def main() -> None:
*/
import {{ IDeviceKeys }} from "../../../src/@types/crypto";
import {{ IDownloadKeyResult }} from "../../../src";
/* eslint-disable comma-dangle */
Expand All @@ -80,8 +92,82 @@ def main() -> None:
/** Signed device data, suitable for returning from a `/keys/query` call */
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:
Expand Down
51 changes: 51 additions & 0 deletions spec/test-utils/test-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { IDeviceKeys } from "../../../src/@types/crypto";
import { IDownloadKeyResult } from "../../../src";

/* eslint-disable comma-dangle */

Expand Down Expand Up @@ -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"
}
}
}
}
};

0 comments on commit a21736c

Please sign in to comment.