1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Integration test for bootstrapCrossSigning (#3355)

* Stub implementation of bootstrapCrossSigning

* Integration test for `bootstrapCrossSigning`
This commit is contained in:
Richard van der Hoff
2023-05-12 17:19:18 +01:00
committed by GitHub
parent 63abd00ca7
commit 7ff44d4a50
6 changed files with 163 additions and 5 deletions

View File

@ -0,0 +1,117 @@
/*
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 "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
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
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
const TEST_USER_ID = "@alice:localhost";
const TEST_DEVICE_ID = "xzcvb";
/**
* Integration tests for cross-signing 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))("cross-signing (%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;
let aliceClient: MatrixClient;
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: TEST_DEVICE_ID,
});
await initCrypto(aliceClient);
});
afterEach(async () => {
await aliceClient.stopClient();
fetchMock.mockReset();
});
describe("bootstrapCrossSigning (before initialsync completes)", () => {
oldBackendOnly("publishes keys if none were yet published", async () => {
// have account_data requests return an empty object
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
// ... and one to upload the cross-signing keys (with UIA)
fetchMock.post(
{ url: "path:/_matrix/client/unstable/keys/device_signing/upload", name: "upload-keys" },
{},
);
// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
await makeRequest(authDict);
};
// now bootstrap cross signing, and check it resolves successfully
await aliceClient.bootstrapCrossSigning({
authUploadDeviceSigningKeys: uiaCallback,
});
// check the cross-signing keys upload
expect(fetchMock.called("upload-keys")).toBeTruthy();
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
const keysBody = JSON.parse(keysOpts!.body as string);
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
// there should be a key of each type
// master key is signed by the device
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
// ssk and usk are signed by the master key
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
// check the publish call
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
const body = JSON.parse(sigsOpts!.body as string);
// there should be a signature for our device, by our self-signing key.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
);
});
});
});

View File

@ -2758,6 +2758,10 @@ describe("MatrixClient", function () {
expect(() => client.isCrossSigningReady()).toThrow("End-to-end encryption disabled"); expect(() => client.isCrossSigningReady()).toThrow("End-to-end encryption disabled");
}); });
it("bootstrapCrossSigning", () => {
expect(() => client.bootstrapCrossSigning({})).toThrow("End-to-end encryption disabled");
});
it("isSecretStorageReady", () => { it("isSecretStorageReady", () => {
expect(() => client.isSecretStorageReady()).toThrow("End-to-end encryption disabled"); expect(() => client.isSecretStorageReady()).toThrow("End-to-end encryption disabled");
}); });
@ -2769,6 +2773,7 @@ describe("MatrixClient", function () {
beforeEach(() => { beforeEach(() => {
mockCryptoBackend = { mockCryptoBackend = {
isCrossSigningReady: jest.fn(), isCrossSigningReady: jest.fn(),
bootstrapCrossSigning: jest.fn(),
isSecretStorageReady: jest.fn(), isSecretStorageReady: jest.fn(),
stop: jest.fn().mockResolvedValue(undefined), stop: jest.fn().mockResolvedValue(undefined),
} as unknown as Mocked<CryptoBackend>; } as unknown as Mocked<CryptoBackend>;
@ -2782,6 +2787,14 @@ describe("MatrixClient", function () {
expect(mockCryptoBackend.isCrossSigningReady).toHaveBeenCalledTimes(1); expect(mockCryptoBackend.isCrossSigningReady).toHaveBeenCalledTimes(1);
}); });
it("bootstrapCrossSigning", async () => {
const testOpts = {};
mockCryptoBackend.bootstrapCrossSigning.mockResolvedValue(undefined);
await client.bootstrapCrossSigning(testOpts);
expect(mockCryptoBackend.bootstrapCrossSigning).toHaveBeenCalledTimes(1);
expect(mockCryptoBackend.bootstrapCrossSigning).toHaveBeenCalledWith(testOpts);
});
it("isSecretStorageReady", async () => { it("isSecretStorageReady", async () => {
client["cryptoBackend"] = mockCryptoBackend; client["cryptoBackend"] = mockCryptoBackend;
const testResult = "test"; const testResult = "test";

View File

@ -98,6 +98,11 @@ describe("RustCrypto", () => {
await expect(rustCrypto.isCrossSigningReady()).resolves.toBe(false); await expect(rustCrypto.isCrossSigningReady()).resolves.toBe(false);
}); });
it("bootstrapCrossSigning", async () => {
const rustCrypto = await makeTestRustCrypto();
await rustCrypto.bootstrapCrossSigning({});
});
it("isSecretStorageReady", async () => { it("isSecretStorageReady", async () => {
const rustCrypto = await makeTestRustCrypto(); const rustCrypto = await makeTestRustCrypto();
await expect(rustCrypto.isSecretStorageReady()).resolves.toBe(false); await expect(rustCrypto.isSecretStorageReady()).resolves.toBe(false);

View File

@ -2747,15 +2747,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* *
* This function: * This function:
* - creates new cross-signing keys if they are not found locally cached nor in * - creates new cross-signing keys if they are not found locally cached nor in
* secret storage (if it has been setup) * secret storage (if it has been set up)
* *
* The cross-signing API is currently UNSTABLE and may change without notice. * @deprecated Prefer {@link CryptoApi.bootstrapCrossSigning | `CryptoApi.bootstrapCrossSigning`}.
*/ */
public bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise<void> { public bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise<void> {
if (!this.crypto) { if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
} }
return this.crypto.bootstrapCrossSigning(opts); return this.cryptoBackend.bootstrapCrossSigning(opts);
} }
/** /**

View File

@ -137,6 +137,22 @@ export interface CryptoApi {
*/ */
isCrossSigningReady(): Promise<boolean>; isCrossSigningReady(): Promise<boolean>;
/**
* Bootstrap cross-signing by creating keys if needed.
*
* If everything is already set up, then no changes are made, so this is safe to run to ensure
* cross-signing is ready for use.
*
* This function:
* - creates new cross-signing keys if they are not found locally cached nor in
* secret storage (if it has been set up)
* - publishes the public keys to the server if they are not already published
* - stores the private keys in secret storage if secret storage is set up.
*
* @param opts - options object
*/
bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise<void>;
/** /**
* Checks whether secret storage: * Checks whether secret storage:
* - is enabled on this account * - is enabled on this account

View File

@ -30,7 +30,7 @@ import { RoomEncryptor } from "./RoomEncryptor";
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
import { KeyClaimManager } from "./KeyClaimManager"; import { KeyClaimManager } from "./KeyClaimManager";
import { MapWithDefault } from "../utils"; import { MapWithDefault } from "../utils";
import { DeviceVerificationStatus } from "../crypto-api"; import { BootstrapCrossSigningOpts, DeviceVerificationStatus } from "../crypto-api";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter"; import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client"; import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
import { Device, DeviceMap } from "../models/device"; import { Device, DeviceMap } from "../models/device";
@ -324,6 +324,13 @@ export class RustCrypto implements CryptoBackend {
return false; return false;
} }
/**
* Implementation of {@link CryptoApi#boostrapCrossSigning}
*/
public async bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise<void> {
logger.log("Cross-signing ready");
}
/** /**
* Implementation of {@link CryptoApi#isSecretStorageReady} * Implementation of {@link CryptoApi#isSecretStorageReady}
*/ */