Add cypress test for verifying a new device via SAS (#10940)
* Add WIP Sas cross-signing test * Login after bot creation * Figuring out how to make it work in ci * Wait for `r0/login` to be called before bot creation * Make waitForVerificationRequest automatically accept requests ... thereby making the `acceptVerificationRequest` helper redundant * Clean up `deviceIsCrossSigned` * combine `handleVerificationRequest` and `verifyEmojiSas` * get rid of a layer ... it adds no value * fix bad merge * minor cleanups to new test * Move `logIntoElement` to utils module * use `logIntoElement` function * Avoid intercept * Avoid `CryptoTestContext` --------- Co-authored-by: Richard van der Hoff <richard@matrix.org>
This commit is contained in:
parent
5593872b7a
commit
8d77d6e4cc
4 changed files with 161 additions and 65 deletions
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import { handleVerificationRequest, waitForVerificationRequest } from "./utils";
|
||||
import { handleVerificationRequest, logIntoElement, waitForVerificationRequest } from "./utils";
|
||||
import { CypressBot } from "../../support/bot";
|
||||
import { skipIfRustCrypto } from "../../support/util";
|
||||
|
||||
|
@ -69,7 +69,6 @@ describe("Complete security", () => {
|
|||
|
||||
// accept the verification request on the "bot" side
|
||||
cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => {
|
||||
await verificationRequest.accept();
|
||||
await handleVerificationRequest(verificationRequest);
|
||||
});
|
||||
|
||||
|
@ -83,22 +82,3 @@ describe("Complete security", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Fill in the login form in element with the given creds
|
||||
*/
|
||||
function logIntoElement(homeserverUrl: string, username: string, password: string) {
|
||||
cy.visit("/#/login");
|
||||
|
||||
// select homeserver
|
||||
cy.findByRole("button", { name: "Edit" }).click();
|
||||
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl);
|
||||
cy.findByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// wait for the dialog to go away
|
||||
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
||||
|
||||
cy.findByRole("textbox", { name: "Username" }).type(username);
|
||||
cy.findByPlaceholderText("Password").type(password);
|
||||
cy.findByRole("button", { name: "Sign in" }).click();
|
||||
}
|
||||
|
|
|
@ -19,7 +19,13 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/
|
|||
import type { CypressBot } from "../../support/bot";
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import { UserCredentials } from "../../support/login";
|
||||
import { EmojiMapping, handleVerificationRequest, waitForVerificationRequest } from "./utils";
|
||||
import {
|
||||
checkDeviceIsCrossSigned,
|
||||
EmojiMapping,
|
||||
handleVerificationRequest,
|
||||
logIntoElement,
|
||||
waitForVerificationRequest,
|
||||
} from "./utils";
|
||||
import { skipIfRustCrypto } from "../../support/util";
|
||||
|
||||
interface CryptoTestContext extends Mocha.Context {
|
||||
|
@ -104,6 +110,27 @@ function autoJoin(client: MatrixClient) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a VerificationRequest in a bot client, add cypress commands to:
|
||||
* - wait for the bot to receive a 'verify by emoji' notification
|
||||
* - check that the bot sees the same emoji as the application
|
||||
*
|
||||
* @param botVerificationRequest - a verification request in a bot client
|
||||
*/
|
||||
function doTwoWaySasVerification(botVerificationRequest: VerificationRequest): void {
|
||||
// on the bot side, wait for the emojis, confirm they match, and return them
|
||||
const emojiPromise = handleVerificationRequest(botVerificationRequest);
|
||||
|
||||
// then, check that our application shows an emoji panel with the same emojis.
|
||||
cy.wrap(emojiPromise).then((emojis: EmojiMapping[]) => {
|
||||
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
|
||||
emojis.forEach((emoji: EmojiMapping, index: number) => {
|
||||
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const verify = function (this: CryptoTestContext) {
|
||||
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
|
||||
|
||||
|
@ -112,21 +139,9 @@ const verify = function (this: CryptoTestContext) {
|
|||
cy.findByText("Bob").click();
|
||||
cy.findByRole("button", { name: "Verify" }).click();
|
||||
cy.findByRole("button", { name: "Start Verification" }).click();
|
||||
cy.wrap(bobsVerificationRequestPromise)
|
||||
.then((verificationRequest: VerificationRequest) => {
|
||||
verificationRequest.accept();
|
||||
return verificationRequest;
|
||||
})
|
||||
.as("bobsVerificationRequest");
|
||||
cy.findByRole("button", { name: "Verify by emoji" }).click();
|
||||
cy.get<VerificationRequest>("@bobsVerificationRequest").then((request: VerificationRequest) => {
|
||||
return cy.wrap(handleVerificationRequest(request)).then((emojis: EmojiMapping[]) => {
|
||||
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
|
||||
emojis.forEach((emoji: EmojiMapping, index: number) => {
|
||||
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
cy.wrap(bobsVerificationRequestPromise).then((request: VerificationRequest) => {
|
||||
doTwoWaySasVerification(request);
|
||||
});
|
||||
cy.findByRole("button", { name: "They match" }).click();
|
||||
cy.findByText("You've successfully verified Bob!").should("exist");
|
||||
|
@ -144,7 +159,11 @@ describe("Cryptography", function () {
|
|||
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
|
||||
aliceCredentials = credentials;
|
||||
});
|
||||
cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob");
|
||||
cy.getBot(homeserver, {
|
||||
displayName: "Bob",
|
||||
autoAcceptInvites: false,
|
||||
userIdPrefix: "bob_",
|
||||
}).as("bob");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -305,3 +324,67 @@ describe("Cryptography", function () {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Verify own device", () => {
|
||||
let aliceBotClient: CypressBot;
|
||||
let homeserver: HomeserverInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startHomeserver("default").then((data: HomeserverInstance) => {
|
||||
homeserver = data;
|
||||
|
||||
// Visit the login page of the app, to load the matrix sdk
|
||||
cy.visit("/#/login");
|
||||
|
||||
// wait for the page to load
|
||||
cy.window({ log: false }).should("have.property", "matrixcs");
|
||||
|
||||
// Create a new device for alice
|
||||
cy.getBot(homeserver, { bootstrapCrossSigning: true }).then((bot) => {
|
||||
aliceBotClient = bot;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopHomeserver(homeserver);
|
||||
});
|
||||
|
||||
/* Click the "Verify with another device" button, and have the bot client auto-accept it.
|
||||
*
|
||||
* Stores the incoming `VerificationRequest` on the bot client as `@verificationRequest`.
|
||||
*/
|
||||
function initiateAliceVerificationRequest() {
|
||||
// alice bot waits for verification request
|
||||
const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient);
|
||||
|
||||
// Click on "Verify with another device"
|
||||
cy.get(".mx_AuthPage").within(() => {
|
||||
cy.findByRole("button", { name: "Verify with another device" }).click();
|
||||
});
|
||||
|
||||
// alice bot responds yes to verification request from alice
|
||||
cy.wrap(promiseVerificationRequest).as("verificationRequest");
|
||||
}
|
||||
|
||||
it("with SAS", function (this: CryptoTestContext) {
|
||||
logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password);
|
||||
|
||||
// Launch the verification request between alice and the bot
|
||||
initiateAliceVerificationRequest();
|
||||
|
||||
// Handle emoji SAS verification
|
||||
cy.get(".mx_InfoDialog").within(() => {
|
||||
cy.get<VerificationRequest>("@verificationRequest").then((request: VerificationRequest) => {
|
||||
// Handle emoji request and check that emojis are matching
|
||||
doTwoWaySasVerification(request);
|
||||
});
|
||||
|
||||
cy.findByRole("button", { name: "They match" }).click();
|
||||
cy.findByRole("button", { name: "Got it" }).click();
|
||||
});
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
checkDeviceIsCrossSigned();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,15 +21,16 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/
|
|||
export type EmojiMapping = [emoji: string, name: string];
|
||||
|
||||
/**
|
||||
* wait for the given client to receive an incoming verification request
|
||||
* wait for the given client to receive an incoming verification request, and automatically accept it
|
||||
*
|
||||
* @param cli - matrix client we expect to receive a request
|
||||
*/
|
||||
export function waitForVerificationRequest(cli: MatrixClient): Promise<VerificationRequest> {
|
||||
return new Promise<VerificationRequest>((resolve) => {
|
||||
const onVerificationRequestEvent = (request: VerificationRequest) => {
|
||||
const onVerificationRequestEvent = async (request: VerificationRequest) => {
|
||||
// @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here
|
||||
cli.off("crypto.verification.request", onVerificationRequestEvent);
|
||||
await request.accept();
|
||||
resolve(request);
|
||||
};
|
||||
// @ts-ignore
|
||||
|
@ -62,3 +63,59 @@ export function handleVerificationRequest(request: VerificationRequest): Promise
|
|||
verifier.verify();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the user has published cross-signing keys, and that the user's device has been cross-signed.
|
||||
*/
|
||||
export function checkDeviceIsCrossSigned(): void {
|
||||
let userId: string;
|
||||
let myDeviceId: string;
|
||||
cy.window({ log: false })
|
||||
.then((win) => {
|
||||
// Get the userId and deviceId of the current user
|
||||
const cli = win.mxMatrixClientPeg.get();
|
||||
const accessToken = cli.getAccessToken()!;
|
||||
const homeserverUrl = cli.getHomeserverUrl();
|
||||
myDeviceId = cli.getDeviceId();
|
||||
userId = cli.getUserId();
|
||||
return cy.request({
|
||||
method: "POST",
|
||||
url: `${homeserverUrl}/_matrix/client/v3/keys/query`,
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
body: { device_keys: { [userId]: [] } },
|
||||
});
|
||||
})
|
||||
.then((res) => {
|
||||
// there should be three cross-signing keys
|
||||
expect(res.body.master_keys[userId]).to.have.property("keys");
|
||||
expect(res.body.self_signing_keys[userId]).to.have.property("keys");
|
||||
expect(res.body.user_signing_keys[userId]).to.have.property("keys");
|
||||
|
||||
// and the device should be signed by the self-signing key
|
||||
const selfSigningKeyId = Object.keys(res.body.self_signing_keys[userId].keys)[0];
|
||||
|
||||
expect(res.body.device_keys[userId][myDeviceId]).to.exist;
|
||||
|
||||
const myDeviceSignatures = res.body.device_keys[userId][myDeviceId].signatures[userId];
|
||||
expect(myDeviceSignatures[selfSigningKeyId]).to.exist;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill in the login form in element with the given creds
|
||||
*/
|
||||
export function logIntoElement(homeserverUrl: string, username: string, password: string) {
|
||||
cy.visit("/#/login");
|
||||
|
||||
// select homeserver
|
||||
cy.findByRole("button", { name: "Edit" }).click();
|
||||
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl);
|
||||
cy.findByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// wait for the dialog to go away
|
||||
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
||||
|
||||
cy.findByRole("textbox", { name: "Username" }).type(username);
|
||||
cy.findByPlaceholderText("Password").type(password);
|
||||
cy.findByRole("button", { name: "Sign in" }).click();
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
/// <reference types="cypress" />
|
||||
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import { checkDeviceIsCrossSigned } from "../crypto/utils";
|
||||
|
||||
describe("Registration", () => {
|
||||
let homeserver: HomeserverInstance;
|
||||
|
@ -95,32 +96,7 @@ describe("Registration", () => {
|
|||
);
|
||||
|
||||
// check that cross-signing keys have been uploaded.
|
||||
const myUserId = "@alice:localhost";
|
||||
let myDeviceId: string;
|
||||
cy.window({ log: false })
|
||||
.then((win) => {
|
||||
const cli = win.mxMatrixClientPeg.get();
|
||||
const accessToken = cli.getAccessToken()!;
|
||||
myDeviceId = cli.getDeviceId();
|
||||
return cy.request({
|
||||
method: "POST",
|
||||
url: `${homeserver.baseUrl}/_matrix/client/v3/keys/query`,
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
body: { device_keys: { [myUserId]: [] } },
|
||||
});
|
||||
})
|
||||
.then((res) => {
|
||||
// there should be three cross-signing keys
|
||||
expect(res.body.master_keys[myUserId]).to.have.property("keys");
|
||||
expect(res.body.self_signing_keys[myUserId]).to.have.property("keys");
|
||||
expect(res.body.user_signing_keys[myUserId]).to.have.property("keys");
|
||||
|
||||
// and the device should be signed by the self-signing key
|
||||
const selfSigningKeyId = Object.keys(res.body.self_signing_keys[myUserId].keys)[0];
|
||||
expect(res.body.device_keys[myUserId][myDeviceId]).to.exist;
|
||||
const myDeviceSignatures = res.body.device_keys[myUserId][myDeviceId].signatures[myUserId];
|
||||
expect(myDeviceSignatures[selfSigningKeyId]).to.exist;
|
||||
});
|
||||
checkDeviceIsCrossSigned();
|
||||
});
|
||||
|
||||
it("should require username to fulfil requirements and be available", () => {
|
||||
|
|
Loading…
Reference in a new issue