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:
Florian Duros 2023-05-25 20:24:50 +02:00 committed by GitHub
parent 5593872b7a
commit 8d77d6e4cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 161 additions and 65 deletions

View file

@ -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();
}

View file

@ -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();
});
});

View file

@ -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();
}

View file

@ -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", () => {