From 858155e0efc910e5e094803bb71af78d42419780 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 2 Jun 2023 16:01:21 +0100 Subject: [PATCH] Add an integration test for verification (#3436) * Move existing crypto integ tests into a subdirectory * Factor out some common bits from `crypto.spec.ts` * Integration test for device verification * Ignore generated file in prettier --- .prettierignore | 3 + spec/integ/{ => crypto}/cross-signing.spec.ts | 4 +- spec/integ/{ => crypto}/crypto.spec.ts | 32 +-- spec/integ/{ => crypto}/megolm-backup.spec.ts | 12 +- .../integ/{ => crypto}/olm-encryption-spec.ts | 16 +- spec/integ/{ => crypto}/rust-crypto.spec.ts | 2 +- spec/integ/crypto/verification.spec.ts | 255 ++++++++++++++++++ spec/test-utils/mockEndpoints.ts | 30 +++ spec/test-utils/test-data/.gitignore | 1 + .../test-data/generate-test-data.py | 117 ++++++++ spec/test-utils/test-data/index.ts | 33 +++ 11 files changed, 470 insertions(+), 35 deletions(-) rename spec/integ/{ => crypto}/cross-signing.spec.ts (98%) rename spec/integ/{ => crypto}/crypto.spec.ts (98%) rename spec/integ/{ => crypto}/megolm-backup.spec.ts (94%) rename spec/integ/{ => crypto}/olm-encryption-spec.ts (98%) rename spec/integ/{ => crypto}/rust-crypto.spec.ts (98%) create mode 100644 spec/integ/crypto/verification.spec.ts create mode 100644 spec/test-utils/mockEndpoints.ts create mode 100644 spec/test-utils/test-data/.gitignore create mode 100755 spec/test-utils/test-data/generate-test-data.py create mode 100644 spec/test-utils/test-data/index.ts diff --git a/.prettierignore b/.prettierignore index 6d56d969f..3d2a9aded 100644 --- a/.prettierignore +++ b/.prettierignore @@ -24,3 +24,6 @@ out # This file is owned, parsed, and generated by allchange, which doesn't comply with prettier /CHANGELOG.md + +# This file is also autogenerated +/spec/test-utils/test-data/index.ts diff --git a/spec/integ/cross-signing.spec.ts b/spec/integ/crypto/cross-signing.spec.ts similarity index 98% rename from spec/integ/cross-signing.spec.ts rename to spec/integ/crypto/cross-signing.spec.ts index ba227d3f8..305bae222 100644 --- a/spec/integ/cross-signing.spec.ts +++ b/spec/integ/crypto/cross-signing.spec.ts @@ -18,8 +18,8 @@ import fetchMock from "fetch-mock-jest"; import "fake-indexeddb/auto"; import { IDBFactory } from "fake-indexeddb"; -import { CRYPTO_BACKENDS, InitCrypto } from "../test-utils/test-utils"; -import { createClient, MatrixClient, UIAuthCallback } from "../../src"; +import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils"; +import { createClient, MatrixClient, UIAuthCallback } from "../../../src"; afterEach(() => { // reset fake-indexeddb after each test, to make sure we don't leak connections diff --git a/spec/integ/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts similarity index 98% rename from spec/integ/crypto.spec.ts rename to spec/integ/crypto/crypto.spec.ts index 951e853f9..f4d7fc49f 100644 --- a/spec/integ/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -21,11 +21,11 @@ import "fake-indexeddb/auto"; import { IDBFactory } from "fake-indexeddb"; import { MockResponse, MockResponseFunction } from "fetch-mock"; -import type { IDeviceKeys } from "../../src/@types/crypto"; -import * as testUtils from "../test-utils/test-utils"; -import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../test-utils/test-utils"; -import { TestClient } from "../TestClient"; -import { logger } from "../../src/logger"; +import type { IDeviceKeys } from "../../../src/@types/crypto"; +import * as testUtils from "../../test-utils/test-utils"; +import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils"; +import { TestClient } from "../../TestClient"; +import { logger } from "../../../src/logger"; import { createClient, IClaimOTKsResult, @@ -43,13 +43,14 @@ import { Room, RoomMember, RoomStateEvent, -} from "../../src/matrix"; -import { DeviceInfo } from "../../src/crypto/deviceinfo"; -import { E2EKeyReceiver, IE2EKeyReceiver } from "../test-utils/E2EKeyReceiver"; -import { ISyncResponder, SyncResponder } from "../test-utils/SyncResponder"; -import { escapeRegExp } from "../../src/utils"; -import { downloadDeviceToJsDevice } from "../../src/rust-crypto/device-converter"; -import { flushPromises } from "../test-utils/flushPromises"; +} from "../../../src/matrix"; +import { DeviceInfo } from "../../../src/crypto/deviceinfo"; +import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; +import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder"; +import { escapeRegExp } from "../../../src/utils"; +import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter"; +import { flushPromises } from "../../test-utils/flushPromises"; +import { mockInitialApiRequests } from "../../test-utils/mockEndpoints"; const ROOM_ID = "!room:id"; @@ -419,12 +420,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, async function startClientAndAwaitFirstSync(opts: IStartClientOpts = {}): Promise { logger.log(aliceClient.getUserId() + ": starting"); - const homeserverUrl = aliceClient.getHomeserverUrl(); - fetchMock.get(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["r0.5.0"] }); - fetchMock.get(new URL("/_matrix/client/r0/pushrules/", homeserverUrl).toString(), {}); - fetchMock.post(new URL("/_matrix/client/r0/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), { - filter_id: "fid", - }); + mockInitialApiRequests(aliceClient.getHomeserverUrl()); // we let the client do a very basic initial sync, which it needs before // it will upload one-time keys. diff --git a/spec/integ/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts similarity index 94% rename from spec/integ/megolm-backup.spec.ts rename to spec/integ/crypto/megolm-backup.spec.ts index adbfb70d7..58adcbfc3 100644 --- a/spec/integ/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -16,12 +16,12 @@ limitations under the License. import { Account } from "@matrix-org/olm"; -import { logger } from "../../src/logger"; -import { decodeRecoveryKey } from "../../src/crypto/recoverykey"; -import { IKeyBackupInfo, IKeyBackupSession } from "../../src/crypto/keybackup"; -import { TestClient } from "../TestClient"; -import { IEvent } from "../../src"; -import { MatrixEvent, MatrixEventEvent } from "../../src/models/event"; +import { logger } from "../../../src/logger"; +import { decodeRecoveryKey } from "../../../src/crypto/recoverykey"; +import { IKeyBackupInfo, IKeyBackupSession } from "../../../src/crypto/keybackup"; +import { TestClient } from "../../TestClient"; +import { IEvent } from "../../../src"; +import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event"; const ROOM_ID = "!ROOM:ID"; diff --git a/spec/integ/olm-encryption-spec.ts b/spec/integ/crypto/olm-encryption-spec.ts similarity index 98% rename from spec/integ/olm-encryption-spec.ts rename to spec/integ/crypto/olm-encryption-spec.ts index 171cb3fa0..aff150adb 100644 --- a/spec/integ/olm-encryption-spec.ts +++ b/spec/integ/crypto/olm-encryption-spec.ts @@ -26,16 +26,16 @@ limitations under the License. */ // load olm before the sdk if possible -import "../olm-loader"; +import "../../olm-loader"; import type { Session } from "@matrix-org/olm"; -import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto"; -import { logger } from "../../src/logger"; -import * as testUtils from "../test-utils/test-utils"; -import { TestClient } from "../TestClient"; -import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client"; -import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix"; -import { DeviceInfo } from "../../src/crypto/deviceinfo"; +import type { IDeviceKeys, IOneTimeKey } from "../../../src/@types/crypto"; +import { logger } from "../../../src/logger"; +import * as testUtils from "../../test-utils/test-utils"; +import { TestClient } from "../../TestClient"; +import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client"; +import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../../src/matrix"; +import { DeviceInfo } from "../../../src/crypto/deviceinfo"; let aliTestClient: TestClient; const roomId = "!room:localhost"; diff --git a/spec/integ/rust-crypto.spec.ts b/spec/integ/crypto/rust-crypto.spec.ts similarity index 98% rename from spec/integ/rust-crypto.spec.ts rename to spec/integ/crypto/rust-crypto.spec.ts index e018c2102..6fa56b3d4 100644 --- a/spec/integ/rust-crypto.spec.ts +++ b/spec/integ/crypto/rust-crypto.spec.ts @@ -17,7 +17,7 @@ limitations under the License. import "fake-indexeddb/auto"; import { IDBFactory } from "fake-indexeddb"; -import { createClient } from "../../src"; +import { createClient } from "../../../src"; afterEach(() => { // reset fake-indexeddb after each test, to make sure we don't leak connections diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts new file mode 100644 index 000000000..64a43d1ce --- /dev/null +++ b/spec/integ/crypto/verification.spec.ts @@ -0,0 +1,255 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +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 { 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 { + SIGNED_TEST_DEVICE_DATA, + TEST_DEVICE_ID, + TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64, + TEST_USER_ID, +} from "../../test-utils/test-data"; +import { mockInitialApiRequests } from "../../test-utils/mockEndpoints"; +import { + Phase, + VerificationRequest, + VerificationRequestEvent, +} from "../../../src/crypto/verification/request/VerificationRequest"; + +// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations +// to ensure that we don't end up with dangling timeouts. +jest.useFakeTimers(); + +/** + * Integration tests for verification functionality. + * + * These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as + * to provide the most effective integration tests possible. + */ +describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: string, initCrypto: InitCrypto) => { + // oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the + // Rust backend. Once we have full support in the rust sdk, it will go away. + const oldBackendOnly = backend === "rust-sdk" ? test.skip : test; + + /** the client under test */ + let aliceClient: MatrixClient; + + /** an object which intercepts `/sync` requests from {@link #aliceClient} */ + let syncResponder: SyncResponder; + + beforeEach(async () => { + // anything that we don't have a specific matcher for silently returns a 404 + fetchMock.catch(404); + fetchMock.config.warnOnFallback = false; + + const homeserverUrl = "https://alice-server.com"; + aliceClient = createClient({ + baseUrl: homeserverUrl, + userId: TEST_USER_ID, + accessToken: "akjgkrgjs", + deviceId: "device_under_test", + }); + + await initCrypto(aliceClient); + }); + + afterEach(async () => { + await aliceClient.stopClient(); + fetchMock.mockReset(); + }); + + beforeEach(() => { + syncResponder = new SyncResponder(aliceClient.getHomeserverUrl()); + mockInitialApiRequests(aliceClient.getHomeserverUrl()); + aliceClient.startClient(); + }); + + oldBackendOnly("Outgoing verification: can verify another device via SAS", 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, + }, + }, + }); + + // have alice initiate a verification. She should send a m.key.verification.request + let [requestBody, request] = await Promise.all([ + expectSendToDeviceMessage("m.key.verification.request"), + aliceClient.requestVerification(TEST_USER_ID, [TEST_DEVICE_ID]), + ]); + const transactionId = request.channel.transactionId; + expect(transactionId).toBeDefined(); + expect(request.phase).toEqual(Phase.Requested); + + let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID]; + expect(toDeviceMessage.methods).toContain("m.sas.v1"); + expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId); + expect(toDeviceMessage.transaction_id).toEqual(transactionId); + + // The dummy device replies with an m.key.verification.ready... + returnToDeviceMessageFromSync({ + type: "m.key.verification.ready", + content: { + from_device: TEST_DEVICE_ID, + methods: ["m.sas.v1"], + transaction_id: transactionId, + }, + }); + await waitForVerificationRequestChanged(request); + expect(request.phase).toEqual(Phase.Ready); + + // ... and picks a method with m.key.verification.start + returnToDeviceMessageFromSync({ + type: "m.key.verification.start", + content: { + from_device: TEST_DEVICE_ID, + method: "m.sas.v1", + transaction_id: transactionId, + hashes: ["sha256"], + key_agreement_protocols: ["curve25519"], + message_authentication_codes: ["hkdf-hmac-sha256.v2"], + short_authentication_string: ["emoji"], + }, + }); + await waitForVerificationRequestChanged(request); + expect(request.phase).toEqual(Phase.Started); + expect(request.chosenMethod).toEqual("m.sas.v1"); + + // there should now be a verifier + const verifier: VerificationBase = request.verifier!; + expect(verifier).toBeDefined(); + + // start off the verification process: alice will send an `accept` + const verificationPromise = verifier.verify(); + // advance the clock, because the devicelist likes to sleep for 5ms during key downloads + jest.advanceTimersByTime(10); + + requestBody = await expectSendToDeviceMessage("m.key.verification.accept"); + toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID]; + expect(toDeviceMessage.key_agreement_protocol).toEqual("curve25519"); + expect(toDeviceMessage.short_authentication_string).toEqual(["emoji"]); + expect(toDeviceMessage.transaction_id).toEqual(transactionId); + + // The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key' + // We use the Curve25519, HMAC and HKDF implementations in libolm, for now + const olmSAS = new global.Olm.SAS(); + returnToDeviceMessageFromSync({ + type: "m.key.verification.key", + content: { + transaction_id: transactionId, + key: olmSAS.get_pubkey(), + }, + }); + + // alice responds with a 'key' ... + requestBody = await expectSendToDeviceMessage("m.key.verification.key"); + toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID]; + expect(toDeviceMessage.transaction_id).toEqual(transactionId); + const aliceDevicePubKeyBase64 = toDeviceMessage.key; + olmSAS.set_their_key(aliceDevicePubKeyBase64); + + // ... and the client is notified to show the emoji + const showSas = await new Promise((resolve) => { + verifier.once(VerifierEvent.ShowSas, resolve); + }); + + // user confirms that the emoji match, and alice sends a 'mac' + [requestBody] = await Promise.all([expectSendToDeviceMessage("m.key.verification.mac"), showSas.confirm()]); + toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID]; + expect(toDeviceMessage.transaction_id).toEqual(transactionId); + + // the dummy device also confirms that the emoji match, and sends a mac + const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${TEST_USER_ID}${aliceClient.deviceId}${transactionId}`; + returnToDeviceMessageFromSync({ + type: "m.key.verification.mac", + content: { + keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`), + transaction_id: transactionId, + mac: { + [`ed25519:${TEST_DEVICE_ID}`]: calculateMAC( + olmSAS, + TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64, + `${macInfoBase}ed25519:${TEST_DEVICE_ID}`, + ), + }, + }, + }); + + // 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); + + // we're done with the temporary keypair + olmSAS.free(); + }); + + function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void { + ev.sender ??= TEST_USER_ID; + syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } }); + } +}); + +/** + * Wait for the client under test to send a to-device message of the given type. + * + * @param msgtype - type of to-device message we expect + * @returns A Promise which resolves with the body of the HTTP request + */ +function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }> { + return new Promise((resolve) => { + fetchMock.putOnce( + new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`), + (url: string, opts: RequestInit): MockResponse => { + resolve(JSON.parse(opts.body as string)); + return {}; + }, + ); + }); +} + +/** wait for the verification request to emit a 'Change' event */ +function waitForVerificationRequestChanged(request: VerificationRequest): Promise { + return new Promise((resolve) => { + request.once(VerificationRequestEvent.Change, resolve); + }); +} + +/** Perform a MAC calculation on the given data + * + * Does an HKDR and HMAC as defined by the matrix spec (https://spec.matrix.org/v1.7/client-server-api/#mac-calculation, + * as amended by https://github.com/matrix-org/matrix-spec/issues/1553). + * + * @param olmSAS + * @param input + * @param info + */ +function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string { + const mac = olmSAS.calculate_mac_fixed_base64(input, info); + //console.info(`Test MAC: input:'${input}, info: '${info}' -> '${mac}`); + return mac; +} diff --git a/spec/test-utils/mockEndpoints.ts b/spec/test-utils/mockEndpoints.ts new file mode 100644 index 000000000..a4c162867 --- /dev/null +++ b/spec/test-utils/mockEndpoints.ts @@ -0,0 +1,30 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import fetchMock from "fetch-mock-jest"; + +/** + * Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`. + * + * @param homeserverUrl - the homeserver url for the client under test + */ +export function mockInitialApiRequests(homeserverUrl: string) { + fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["r0.5.0"] }); + fetchMock.getOnce(new URL("/_matrix/client/r0/pushrules/", homeserverUrl).toString(), {}); + fetchMock.postOnce(new URL("/_matrix/client/r0/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), { + filter_id: "fid", + }); +} diff --git a/spec/test-utils/test-data/.gitignore b/spec/test-utils/test-data/.gitignore new file mode 100644 index 000000000..91078accd --- /dev/null +++ b/spec/test-utils/test-data/.gitignore @@ -0,0 +1 @@ +/env diff --git a/spec/test-utils/test-data/generate-test-data.py b/spec/test-utils/test-data/generate-test-data.py new file mode 100755 index 000000000..f5eae004e --- /dev/null +++ b/spec/test-utils/test-data/generate-test-data.py @@ -0,0 +1,117 @@ +#!/bin/env python +# +# Copyright 2023 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This file is a Python script to generate test data for crypto tests. + +To run it: + +python -m venv env +./env/bin/pip install cryptography canonicaljson +./env/bin/python generate-test-data.py > index.ts +""" + +import base64 +import json + +from canonicaljson import encode_canonical_json +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +# input data +TEST_USER_ID = "@alice:localhost" +TEST_DEVICE_ID = "test_device" +# any 32-byte string can be an ed25519 private key. +TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef" + + +def main() -> None: + private_key = ed25519.Ed25519PrivateKey.from_private_bytes( + TEST_DEVICE_PRIVATE_KEY_BYTES + ) + b64_public_key = encode_base64( + private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + ) + + device_data = { + "algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], + "device_id": TEST_DEVICE_ID, + "keys": { + f"curve25519:{TEST_DEVICE_ID}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0", + f"ed25519:{TEST_DEVICE_ID}": b64_public_key, + }, + "signatures": {TEST_USER_ID: {}}, + "user_id": TEST_USER_ID, + } + + device_data["signatures"][TEST_USER_ID][ f"ed25519:{TEST_DEVICE_ID}"] = sign_json( + device_data, private_key + ) + + print( + f"""\ +/* Test data for cryptography tests + * + * Do not edit by hand! This file is generated by `./generate-test-data.py` + */ + +import {{ IDeviceKeys }} from "../../../src/@types/crypto"; + +/* eslint-disable comma-dangle */ + +export const TEST_USER_ID = "{TEST_USER_ID}"; +export const TEST_DEVICE_ID = "{TEST_DEVICE_ID}"; + +/** The base64-encoded public ed25519 key for this device */ +export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}"; + +/** 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='', + ) + + +def encode_base64(input_bytes: bytes) -> str: + """Encode with unpadded base64""" + output_bytes = base64.b64encode(input_bytes) + output_string = output_bytes.decode("ascii") + return output_string.rstrip("=") + + +def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str: + """ + Sign the given json object + + Returns the base64-encoded signature of signing `input` following the Matrix + JSON signature algorithm [1] + + [1]: https://spec.matrix.org/v1.7/appendices/#signing-details + """ + signatures = json_object.pop("signatures", {}) + unsigned = json_object.pop("unsigned", None) + + signature = private_key.sign(encode_canonical_json(json_object)) + signature_base64 = encode_base64(signature) + + json_object["signatures"] = signatures + if unsigned is not None: + json_object["unsigned"] = unsigned + + return signature_base64 + + +if __name__ == "__main__": + main() diff --git a/spec/test-utils/test-data/index.ts b/spec/test-utils/test-data/index.ts new file mode 100644 index 000000000..fbb9a1c2b --- /dev/null +++ b/spec/test-utils/test-data/index.ts @@ -0,0 +1,33 @@ +/* Test data for cryptography tests + * + * Do not edit by hand! This file is generated by `./generate-test-data.py` + */ + +import { IDeviceKeys } from "../../../src/@types/crypto"; + +/* eslint-disable comma-dangle */ + +export const TEST_USER_ID = "@alice:localhost"; +export const TEST_DEVICE_ID = "test_device"; + +/** The base64-encoded public ed25519 key for this device */ +export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU"; + +/** Signed device data, suitable for returning from a `/keys/query` call */ +export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "test_device", + "keys": { + "curve25519:test_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0", + "ed25519:test_device": "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU" + }, + "user_id": "@alice:localhost", + "signatures": { + "@alice:localhost": { + "ed25519:test_device": "LmQC/yAUZJmkxZ+3L0nEwvtVWOzjqQqADWBhk+C47SPaFYHeV+E291mgXaSCJVeGltX+HC49Aw7nb6ga7sw0Aw" + } + } +};