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 type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { handleVerificationRequest, waitForVerificationRequest } from "./utils";
|
import { handleVerificationRequest, logIntoElement, waitForVerificationRequest } from "./utils";
|
||||||
import { CypressBot } from "../../support/bot";
|
import { CypressBot } from "../../support/bot";
|
||||||
import { skipIfRustCrypto } from "../../support/util";
|
import { skipIfRustCrypto } from "../../support/util";
|
||||||
|
|
||||||
|
@ -69,7 +69,6 @@ describe("Complete security", () => {
|
||||||
|
|
||||||
// accept the verification request on the "bot" side
|
// accept the verification request on the "bot" side
|
||||||
cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => {
|
cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => {
|
||||||
await verificationRequest.accept();
|
|
||||||
await handleVerificationRequest(verificationRequest);
|
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 type { CypressBot } from "../../support/bot";
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { UserCredentials } from "../../support/login";
|
import { UserCredentials } from "../../support/login";
|
||||||
import { EmojiMapping, handleVerificationRequest, waitForVerificationRequest } from "./utils";
|
import {
|
||||||
|
checkDeviceIsCrossSigned,
|
||||||
|
EmojiMapping,
|
||||||
|
handleVerificationRequest,
|
||||||
|
logIntoElement,
|
||||||
|
waitForVerificationRequest,
|
||||||
|
} from "./utils";
|
||||||
import { skipIfRustCrypto } from "../../support/util";
|
import { skipIfRustCrypto } from "../../support/util";
|
||||||
|
|
||||||
interface CryptoTestContext extends Mocha.Context {
|
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 verify = function (this: CryptoTestContext) {
|
||||||
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
|
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
|
||||||
|
|
||||||
|
@ -112,21 +139,9 @@ const verify = function (this: CryptoTestContext) {
|
||||||
cy.findByText("Bob").click();
|
cy.findByText("Bob").click();
|
||||||
cy.findByRole("button", { name: "Verify" }).click();
|
cy.findByRole("button", { name: "Verify" }).click();
|
||||||
cy.findByRole("button", { name: "Start Verification" }).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.findByRole("button", { name: "Verify by emoji" }).click();
|
||||||
cy.get<VerificationRequest>("@bobsVerificationRequest").then((request: VerificationRequest) => {
|
cy.wrap(bobsVerificationRequestPromise).then((request: VerificationRequest) => {
|
||||||
return cy.wrap(handleVerificationRequest(request)).then((emojis: EmojiMapping[]) => {
|
doTwoWaySasVerification(request);
|
||||||
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.findByRole("button", { name: "They match" }).click();
|
cy.findByRole("button", { name: "They match" }).click();
|
||||||
cy.findByText("You've successfully verified Bob!").should("exist");
|
cy.findByText("You've successfully verified Bob!").should("exist");
|
||||||
|
@ -144,7 +159,11 @@ describe("Cryptography", function () {
|
||||||
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
|
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
|
||||||
aliceCredentials = 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];
|
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
|
* @param cli - matrix client we expect to receive a request
|
||||||
*/
|
*/
|
||||||
export function waitForVerificationRequest(cli: MatrixClient): Promise<VerificationRequest> {
|
export function waitForVerificationRequest(cli: MatrixClient): Promise<VerificationRequest> {
|
||||||
return new Promise<VerificationRequest>((resolve) => {
|
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
|
// @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here
|
||||||
cli.off("crypto.verification.request", onVerificationRequestEvent);
|
cli.off("crypto.verification.request", onVerificationRequestEvent);
|
||||||
|
await request.accept();
|
||||||
resolve(request);
|
resolve(request);
|
||||||
};
|
};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -62,3 +63,59 @@ export function handleVerificationRequest(request: VerificationRequest): Promise
|
||||||
verifier.verify();
|
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" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
import { checkDeviceIsCrossSigned } from "../crypto/utils";
|
||||||
|
|
||||||
describe("Registration", () => {
|
describe("Registration", () => {
|
||||||
let homeserver: HomeserverInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
@ -95,32 +96,7 @@ describe("Registration", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// check that cross-signing keys have been uploaded.
|
// check that cross-signing keys have been uploaded.
|
||||||
const myUserId = "@alice:localhost";
|
checkDeviceIsCrossSigned();
|
||||||
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;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should require username to fulfil requirements and be available", () => {
|
it("should require username to fulfil requirements and be available", () => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue