browser_webauthn_prompts.js (14781B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 XPCOMUtils.defineLazyScriptGetter( 8 this, 9 ["FullScreen"], 10 "chrome://browser/content/browser-fullScreenAndPointerLock.js" 11 ); 12 13 const TEST_URL = "https://example.com/"; 14 var gAuthenticatorId; 15 16 /** 17 * Waits for the PopupNotifications button enable delay to expire so the 18 * Notification can be interacted with using the buttons. 19 */ 20 async function waitForPopupNotificationSecurityDelay() { 21 let notification = PopupNotifications.panel.firstChild.notification; 22 let notificationEnableDelayMS = Services.prefs.getIntPref( 23 "security.notification_enable_delay" 24 ); 25 await TestUtils.waitForCondition( 26 () => { 27 let timeSinceShown = ChromeUtils.now() - notification.timeShown; 28 return timeSinceShown > notificationEnableDelayMS; 29 }, 30 "Wait for security delay to expire", 31 500, 32 50 33 ); 34 } 35 36 add_task(async function test_setup_usbtoken() { 37 return SpecialPowers.pushPrefEnv({ 38 set: [ 39 ["security.webauth.webauthn_enable_softtoken", false], 40 ["security.webauth.webauthn_enable_usbtoken", true], 41 ], 42 }); 43 }); 44 add_task(test_register); 45 add_task(test_register_escape); 46 add_task(test_sign); 47 add_task(test_sign_escape); 48 add_task(test_tab_switching); 49 add_task(test_window_switching); 50 add_task(async function test_setup_softtoken() { 51 gAuthenticatorId = add_virtual_authenticator(); 52 return SpecialPowers.pushPrefEnv({ 53 set: [ 54 ["browser.fullscreen.autohide", true], 55 ["full-screen-api.enabled", true], 56 ["full-screen-api.allow-trusted-requests-only", false], 57 ["security.webauth.webauthn_enable_softtoken", true], 58 ["security.webauth.webauthn_enable_usbtoken", false], 59 ], 60 }); 61 }); 62 add_task(test_fullscreen_show_nav_toolbar); 63 add_task(test_no_fullscreen_dom); 64 add_task(test_register_direct_with_consent); 65 add_task(test_register_direct_without_consent); 66 add_task(test_select_sign_result); 67 68 function promiseNavToolboxStatus(aExpectedStatus) { 69 let navToolboxStatus; 70 return TestUtils.topicObserved("fullscreen-nav-toolbox", (subject, data) => { 71 navToolboxStatus = data; 72 return data == aExpectedStatus; 73 }).then(() => 74 Assert.equal( 75 navToolboxStatus, 76 aExpectedStatus, 77 "nav toolbox is " + aExpectedStatus 78 ) 79 ); 80 } 81 82 function promiseFullScreenPaint(aExpectedStatus) { 83 return TestUtils.topicObserved("fullscreen-painted"); 84 } 85 86 function triggerMainPopupCommand(popup) { 87 info("triggering main command"); 88 let notifications = popup.childNodes; 89 ok(notifications.length, "at least one notification displayed"); 90 let notification = notifications[0]; 91 info("triggering command: " + notification.getAttribute("buttonlabel")); 92 93 return EventUtils.synthesizeMouseAtCenter(notification.button, {}); 94 } 95 96 let expectNotAllowedError = expectError("NotAllowed"); 97 98 function verifyAnonymizedCertificate(aResult) { 99 return webAuthnDecodeCBORAttestation(aResult.attObj).then( 100 ({ fmt, attStmt }) => { 101 is(fmt, "none", "Is a None Attestation"); 102 is(typeof attStmt, "object", "attStmt is a map"); 103 is(Object.keys(attStmt).length, 0, "attStmt is empty"); 104 } 105 ); 106 } 107 108 async function verifyDirectCertificate(aResult) { 109 let clientDataHash = await crypto.subtle 110 .digest("SHA-256", aResult.clientDataJSON) 111 .then(digest => new Uint8Array(digest)); 112 let { fmt, attStmt, authData, authDataObj } = 113 await webAuthnDecodeCBORAttestation(aResult.attObj); 114 is(fmt, "packed", "Is a Packed Attestation"); 115 let signedData = new Uint8Array(authData.length + clientDataHash.length); 116 signedData.set(authData); 117 signedData.set(clientDataHash, authData.length); 118 let valid = await verifySignature( 119 authDataObj.publicKeyHandle, 120 signedData, 121 new Uint8Array(attStmt.sig) 122 ); 123 ok(valid, "Signature is valid."); 124 } 125 126 async function test_register() { 127 // Open a new tab. 128 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 129 130 // Request a new credential and wait for the prompt. 131 let notificationPromise = promiseNotification("webauthn-prompt-presence"); 132 let active = true; 133 let request = promiseWebAuthnMakeCredential(tab) 134 .then(arrivingHereIsBad) 135 .catch(expectNotAllowedError) 136 .then(() => (active = false)); 137 await notificationPromise; 138 139 // Cancel the request with the button. 140 ok(active, "request should still be active"); 141 PopupNotifications.panel.firstElementChild.button.click(); 142 await request; 143 144 // Close tab. 145 await BrowserTestUtils.removeTab(tab); 146 } 147 148 async function test_register_escape() { 149 // Open a new tab. 150 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 151 152 // Request a new credential and wait for the prompt. 153 let notificationPromise = promiseNotification("webauthn-prompt-presence"); 154 let active = true; 155 let request = promiseWebAuthnMakeCredential(tab) 156 .then(arrivingHereIsBad) 157 .catch(expectNotAllowedError) 158 .then(() => (active = false)); 159 await notificationPromise; 160 161 // Cancel the request by hitting escape. 162 ok(active, "request should still be active"); 163 EventUtils.synthesizeKey("KEY_Escape"); 164 await request; 165 166 // Close tab. 167 await BrowserTestUtils.removeTab(tab); 168 } 169 170 async function test_sign() { 171 // Open a new tab. 172 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 173 174 // Request a new assertion and wait for the prompt. 175 let notificationPromise = promiseNotification("webauthn-prompt-presence"); 176 let active = true; 177 let request = promiseWebAuthnGetAssertion(tab) 178 .then(arrivingHereIsBad) 179 .catch(expectNotAllowedError) 180 .then(() => (active = false)); 181 await notificationPromise; 182 183 // Cancel the request with the button. 184 ok(active, "request should still be active"); 185 PopupNotifications.panel.firstElementChild.button.click(); 186 await request; 187 188 // Close tab. 189 await BrowserTestUtils.removeTab(tab); 190 } 191 192 async function test_sign_escape() { 193 // Open a new tab. 194 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 195 196 // Request a new assertion and wait for the prompt. 197 let notificationPromise = promiseNotification("webauthn-prompt-presence"); 198 let active = true; 199 let request = promiseWebAuthnGetAssertion(tab) 200 .then(arrivingHereIsBad) 201 .catch(expectNotAllowedError) 202 .then(() => (active = false)); 203 await notificationPromise; 204 205 // Cancel the request by hitting escape. 206 ok(active, "request should still be active"); 207 EventUtils.synthesizeKey("KEY_Escape"); 208 await request; 209 210 // Close tab. 211 await BrowserTestUtils.removeTab(tab); 212 } 213 214 // Add two tabs, open WebAuthn in the first, switch, assert the prompt is 215 // not visible, switch back, assert the prompt is there and cancel it. 216 async function test_tab_switching() { 217 // Open a new tab. 218 let tab_one = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 219 220 // Request a new credential and wait for the prompt. 221 let notificationPromise = promiseNotification("webauthn-prompt-presence"); 222 let active = true; 223 let request = promiseWebAuthnMakeCredential(tab_one) 224 .then(arrivingHereIsBad) 225 .catch(expectNotAllowedError) 226 .then(() => (active = false)); 227 await notificationPromise; 228 is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); 229 230 // Open and switch to a second tab. 231 let tab_two = await BrowserTestUtils.openNewForegroundTab( 232 gBrowser, 233 "https://example.org/" 234 ); 235 236 await TestUtils.waitForCondition( 237 () => PopupNotifications.panel.state == "closed" 238 ); 239 is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); 240 241 let notificationPromise2 = promiseNotification("webauthn-prompt-presence"); 242 243 // Go back to the first tab 244 await BrowserTestUtils.removeTab(tab_two); 245 246 await notificationPromise2; 247 248 await TestUtils.waitForCondition( 249 () => PopupNotifications.panel.state == "open" 250 ); 251 is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); 252 253 // Cancel the request. 254 ok(active, "request should still be active"); 255 await triggerMainPopupCommand(PopupNotifications.panel); 256 await request; 257 ok(!active, "request should be stopped"); 258 259 // Close tab. 260 await BrowserTestUtils.removeTab(tab_one); 261 } 262 263 // Add two tabs, open WebAuthn in the first, switch, assert the prompt is 264 // not visible, switch back, assert the prompt is there and cancel it. 265 async function test_window_switching() { 266 // Open a new tab. 267 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 268 269 // Request a new credential and wait for the prompt. 270 let notificationPromise = promiseNotification("webauthn-prompt-presence"); 271 let active = true; 272 let request = promiseWebAuthnMakeCredential(tab) 273 .then(arrivingHereIsBad) 274 .catch(expectNotAllowedError) 275 .then(() => (active = false)); 276 await notificationPromise; 277 278 await TestUtils.waitForCondition( 279 () => PopupNotifications.panel.state == "open" 280 ); 281 is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); 282 283 // Open and switch to a second window 284 let new_window = await BrowserTestUtils.openNewBrowserWindow(); 285 await SimpleTest.promiseFocus(new_window); 286 287 await TestUtils.waitForCondition( 288 () => new_window.PopupNotifications.panel.state == "closed" 289 ); 290 is( 291 new_window.PopupNotifications.panel.state, 292 "closed", 293 "Doorhanger is hidden" 294 ); 295 296 // Go back to the first tab 297 await BrowserTestUtils.closeWindow(new_window); 298 await SimpleTest.promiseFocus(window); 299 300 await TestUtils.waitForCondition( 301 () => PopupNotifications.panel.state == "open" 302 ); 303 is(PopupNotifications.panel.state, "open", "Doorhanger is still visible"); 304 305 // Cancel the request. 306 ok(active, "request should still be active"); 307 await triggerMainPopupCommand(PopupNotifications.panel); 308 await request; 309 ok(!active, "request should be stopped"); 310 311 // Close tab. 312 await BrowserTestUtils.removeTab(tab); 313 } 314 315 async function test_register_direct_with_consent() { 316 // Open a new tab. 317 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 318 319 // Request a new credential with direct attestation and wait for the prompt. 320 let notificationPromise = promiseNotification( 321 "webauthn-prompt-register-direct" 322 ); 323 let request = promiseWebAuthnMakeCredential(tab, "direct"); 324 await notificationPromise; 325 326 // Click "Allow". 327 PopupNotifications.panel.firstElementChild.button.click(); 328 329 // Ensure we got "direct" attestation. 330 await request.then(verifyDirectCertificate); 331 332 // Close tab. 333 await BrowserTestUtils.removeTab(tab); 334 } 335 336 async function test_register_direct_without_consent() { 337 // Open a new tab. 338 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 339 340 // Request a new credential with direct attestation and wait for the prompt. 341 let notificationPromise = promiseNotification( 342 "webauthn-prompt-register-direct" 343 ); 344 let request = promiseWebAuthnMakeCredential(tab, "direct"); 345 await notificationPromise; 346 347 // Click "Block". 348 PopupNotifications.panel.firstElementChild.secondaryButton.click(); 349 350 // Ensure we got "none" attestation. 351 await request.then(verifyAnonymizedCertificate); 352 353 // Close tab. 354 await BrowserTestUtils.removeTab(tab); 355 } 356 357 async function test_select_sign_result() { 358 // Open a new tab. 359 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 360 361 // Make two discoverable credentials for the same RP ID so that 362 // the user has to select one to return. 363 let cred1 = await addCredential(gAuthenticatorId, "example.com"); 364 let cred2 = await addCredential(gAuthenticatorId, "example.com"); 365 366 let notificationPromise = promiseNotification( 367 "webauthn-prompt-select-sign-result" 368 ); 369 let active = true; 370 let request = promiseWebAuthnGetAssertionDiscoverable(tab) 371 .then(arrivingHereIsBad) 372 .catch(expectNotAllowedError) 373 .then(() => (active = false)); 374 375 // Ensure the selection prompt is shown 376 await notificationPromise; 377 378 ok(active, "request is active"); 379 380 // Cancel the request 381 PopupNotifications.panel.firstElementChild.button.click(); 382 await request; 383 384 await removeCredential(gAuthenticatorId, cred1); 385 await removeCredential(gAuthenticatorId, cred2); 386 await BrowserTestUtils.removeTab(tab); 387 } 388 389 async function test_fullscreen_show_nav_toolbar() { 390 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 391 392 // Start with the window fullscreen and the nav toolbox hidden 393 let fullscreenState = window.fullScreen; 394 395 let navToolboxHiddenPromise = promiseNavToolboxStatus("hidden"); 396 397 window.fullScreen = true; 398 FullScreen.hideNavToolbox(false); 399 400 await navToolboxHiddenPromise; 401 402 // Request a new credential with direct attestation. The consent prompt will 403 // keep the request active until we can verify that the nav toolbar is shown. 404 let promptPromise = promiseNotification("webauthn-prompt-register-direct"); 405 let navToolboxShownPromise = promiseNavToolboxStatus("shown"); 406 407 let active = true; 408 let requestPromise = promiseWebAuthnMakeCredential(tab, "direct").then( 409 () => (active = false) 410 ); 411 412 await Promise.all([promptPromise, navToolboxShownPromise]); 413 414 ok(active, "request is active"); 415 ok(window.fullScreen, "window is fullscreen"); 416 417 // Proceed through the consent prompt. 418 PopupNotifications.panel.firstElementChild.secondaryButton.click(); 419 await requestPromise; 420 421 window.fullScreen = fullscreenState; 422 423 // Close tab. 424 await BrowserTestUtils.removeTab(tab); 425 } 426 427 async function test_no_fullscreen_dom() { 428 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 429 430 let fullScreenPaintPromise = promiseFullScreenPaint(); 431 // Make a DOM element fullscreen 432 await ContentTask.spawn(tab.linkedBrowser, [], () => { 433 return content.document.body.requestFullscreen(); 434 }); 435 await fullScreenPaintPromise; 436 ok(!!document.fullscreenElement, "a DOM element is fullscreen"); 437 438 // Request a new credential with direct attestation. The consent prompt will 439 // keep the request active until we can verify that we've left fullscreen. 440 let promptPromise = promiseNotification("webauthn-prompt-register-direct"); 441 fullScreenPaintPromise = promiseFullScreenPaint(); 442 443 let active = true; 444 let requestPromise = promiseWebAuthnMakeCredential(tab, "direct").then( 445 () => (active = false) 446 ); 447 448 await Promise.all([promptPromise, fullScreenPaintPromise]); 449 450 ok(active, "request is active"); 451 ok(!document.fullscreenElement, "no DOM element is fullscreen"); 452 453 // Proceed through the consent prompt. 454 await waitForPopupNotificationSecurityDelay(); 455 PopupNotifications.panel.firstElementChild.secondaryButton.click(); 456 await requestPromise; 457 458 // Close tab. 459 await BrowserTestUtils.removeTab(tab); 460 }