mirror of https://github.com/vector-im/riot-web
				
				
				
			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>pull/28788/head^2
							parent
							
								
									5593872b7a
								
							
						
					
					
						commit
						8d77d6e4cc
					
				|  | @ -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 New Issue
	
	 Florian Duros
						Florian Duros