browser_fido_appid_extension.js (5502B)
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 const TEST_URL = "https://example.com/"; 8 9 let expectNotSupportedError = expectError("NotSupported"); 10 let expectNotAllowedError = expectError("NotAllowed"); 11 let expectSecurityError = expectError("Security"); 12 13 let gAppId = "https://example.com/appId"; 14 let gSameSiteAppId = "https://subdomain.example.com/appId"; 15 let gCrossOriginAppId = "https://example.org/appId"; 16 let gAuthenticatorId = add_virtual_authenticator(); 17 18 add_task(async function test_appid() { 19 // Open a new tab. 20 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 21 22 // The FIDO AppId extension can't be used for MakeCredential. 23 await promiseWebAuthnMakeCredential(tab, "none", "discouraged", { 24 appid: gAppId, 25 }) 26 .then(arrivingHereIsBad) 27 .catch(expectNotSupportedError); 28 29 // Side-load a credential with an RP ID matching the App ID. 30 let credIdB64 = await addCredential(gAuthenticatorId, gAppId); 31 let credId = base64ToBytesUrlSafe(credIdB64); 32 33 // And another for a different origin 34 let crossOriginCredIdB64 = await addCredential( 35 gAuthenticatorId, 36 gCrossOriginAppId 37 ); 38 let crossOriginCredId = base64ToBytesUrlSafe(crossOriginCredIdB64); 39 40 // The App ID extension is required 41 await promiseWebAuthnGetAssertion(tab, credId) 42 .then(arrivingHereIsBad) 43 .catch(expectNotAllowedError); 44 45 // The value in the App ID extension must match the origin. 46 await promiseWebAuthnGetAssertion(tab, crossOriginCredId, { 47 appid: gCrossOriginAppId, 48 }) 49 .then(arrivingHereIsBad) 50 .catch(expectSecurityError); 51 52 // The value in the App ID extension must match the credential's RP ID. 53 await promiseWebAuthnGetAssertion(tab, credId, { appid: gAppId + "2" }) 54 .then(arrivingHereIsBad) 55 .catch(expectNotAllowedError); 56 57 // Succeed with the right App ID. 58 let rpIdHash = await promiseWebAuthnGetAssertion(tab, credId, { 59 appid: gAppId, 60 }) 61 .then(({ authenticatorData, extensions }) => { 62 is(extensions.appid, true, "appid extension was acted upon"); 63 return authenticatorData.slice(0, 32); 64 }) 65 .then(rpIdHash => { 66 // Make sure the returned RP ID hash matches the hash of the App ID. 67 checkRpIdHash(rpIdHash, gAppId); 68 }) 69 .catch(arrivingHereIsBad); 70 71 removeCredential(gAuthenticatorId, credIdB64); 72 removeCredential(gAuthenticatorId, crossOriginCredIdB64); 73 74 // Close tab. 75 BrowserTestUtils.removeTab(tab); 76 }); 77 78 add_task(async function test_same_site_appid() { 79 // Open a new tab. 80 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 81 82 // Side-load a credential with an RP ID matching the App ID. 83 let credIdB64 = await addCredential(gAuthenticatorId, gSameSiteAppId); 84 let credId = base64ToBytesUrlSafe(credIdB64); 85 86 // A request without the AppID extension should fail. 87 await promiseWebAuthnGetAssertion(tab, credId) 88 .then(arrivingHereIsBad) 89 .catch(expectNotAllowedError); 90 91 // As should a request with a cross-origin AppID. 92 await promiseWebAuthnGetAssertion(tab, credId, { 93 appid: gCrossOriginAppId, 94 }) 95 .then(arrivingHereIsBad) 96 .catch(expectSecurityError); 97 98 // A request with the correct AppID extension should succeed. 99 let rpIdHash = await promiseWebAuthnGetAssertion(tab, credId, { 100 appid: gSameSiteAppId, 101 }) 102 .then(({ authenticatorData, extensions }) => { 103 is(extensions.appid, true, "appid extension was acted upon"); 104 return authenticatorData.slice(0, 32); 105 }) 106 .then(rpIdHash => { 107 // Make sure the returned RP ID hash matches the hash of the App ID. 108 checkRpIdHash(rpIdHash, gSameSiteAppId); 109 }) 110 .catch(arrivingHereIsBad); 111 112 removeCredential(gAuthenticatorId, credIdB64); 113 114 // Close tab. 115 BrowserTestUtils.removeTab(tab); 116 }); 117 118 add_task(async function test_appid_unused() { 119 // Open a new tab. 120 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); 121 122 let appid = "https://example.com/appId"; 123 124 let { attObj, rawId } = await promiseWebAuthnMakeCredential(tab); 125 let { authDataObj } = await webAuthnDecodeCBORAttestation(attObj); 126 127 // Make sure the RP ID hash matches what we calculate. 128 await checkRpIdHash(authDataObj.rpIdHash, "example.com"); 129 130 // Get a new assertion. 131 let { clientDataJSON, authenticatorData, signature, extensions } = 132 await promiseWebAuthnGetAssertion(tab, rawId, { appid }); 133 134 ok( 135 "appid" in extensions, 136 `appid should be populated in the extensions data, but saw: ` + 137 `${JSON.stringify(extensions)}` 138 ); 139 is(extensions.appid, false, "appid extension should indicate it was unused"); 140 141 // Check auth data. 142 let attestation = await webAuthnDecodeAuthDataArray( 143 new Uint8Array(authenticatorData) 144 ); 145 is( 146 "" + (attestation.flags & flag_TUP), 147 "" + flag_TUP, 148 "Assertion's user presence byte set correctly" 149 ); 150 151 // Verify the signature. 152 let params = await deriveAppAndChallengeParam( 153 "example.com", 154 clientDataJSON, 155 attestation 156 ); 157 let signedData = await assembleSignedData( 158 params.appParam, 159 params.attestation.flags, 160 params.attestation.counter, 161 params.challengeParam 162 ); 163 let valid = await verifySignature( 164 authDataObj.publicKeyHandle, 165 signedData, 166 signature 167 ); 168 ok(valid, "signature is valid"); 169 170 // Close tab. 171 BrowserTestUtils.removeTab(tab); 172 });