tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_webauthn_serialization.html (22947B)


      1 <!DOCTYPE html>
      2 <meta charset=utf-8>
      3 <head>
      4  <title>Tests W3C Web Authentication Data Types Serialization</title>
      5  <script src="/tests/SimpleTest/SimpleTest.js"></script>
      6  <script type="text/javascript" src="u2futil.js"></script>
      7  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
      8 </head>
      9 <body>
     10 
     11  <h1>Tests W3C Web Authentication Data Types Serialization</h1>
     12  <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1823782">Mozilla Bug 1823782</a>
     13 
     14  <script class="testbody" type="text/javascript">
     15    "use strict";
     16 
     17    const { Assert } = SpecialPowers.ChromeUtils.importESModule(
     18      "resource://testing-common/Assert.sys.mjs"
     19    );
     20 
     21    function arrayBufferEqualsArray(actual, expected, description) {
     22      ok(actual instanceof ArrayBuffer, `${description} (actual should be array)`);
     23      ok(expected instanceof Array, `${description} (expected should be array)`);
     24      is(actual.byteLength, expected.length, `${description} (actual and expected should have same length)`);
     25      let actualView = new Uint8Array(actual);
     26      for (let i = 0; i < actualView.length; i++) {
     27        if (actualView[i] != expected[i]) {
     28          throw new Error(`actual and expected differ in byte ${i}: ${actualView[i]} vs ${expected[i]}`);
     29        }
     30      }
     31      ok(true, description);
     32    }
     33 
     34    function isEmptyArray(arr, description) {
     35      ok(arr instanceof Array, `${description} (expecting Array)`);
     36      is(arr.length, 0, `${description} (array should be empty)`);
     37    }
     38 
     39    function stringArrayEquals(actual, expected, description) {
     40      is(actual.length, expected.length, `${description} (actual and expected should have the same length)`);
     41      for (let i = 0; i < actual.length; i++) {
     42        if (actual[i] instanceof String) {
     43          throw new Error(`actual[${i}] is not a string` + typeof actual[i]);
     44        }
     45        if (actual[i] !== expected[i]) {
     46          throw new Error(`actual and expected differ in position ${i}: ${actual[i]} vs ${expected[i]}`);
     47        }
     48      }
     49    }
     50 
     51    function shouldThrow(func, expectedError, description) {
     52      let threw = false;
     53      try {
     54        func();
     55      } catch (e) {
     56        is(e.message, expectedError);
     57        threw = true;
     58      }
     59      ok(threw, description);
     60    }
     61 
     62    add_task(function test_parseCreationOptionsFromJSON_minimal() {
     63      let creationOptionsJSON = {
     64        rp: { name: "Example" },
     65        user: { id: "-l3qNLTKJng", name: "username", displayName: "display name" },
     66        challenge: "XNJTTB3kfqk",
     67        pubKeyCredParams: [],
     68      };
     69      let creationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);
     70      is(Object.getOwnPropertyNames(creationOptions).length, 9, "creation options should have 9 properties");
     71      is(creationOptions.rp.id, undefined, "rp.id should be undefined");
     72      is(creationOptions.rp.name, "Example", "rp.name should be Example");
     73      arrayBufferEqualsArray(creationOptions.user.id, [ 250, 93, 234, 52, 180, 202, 38, 120 ], "user.id should be as expected");
     74      is(creationOptions.user.displayName, "display name", "user.displayName should be 'display name'");
     75      is(creationOptions.user.name, "username", "user.name should be username");
     76      arrayBufferEqualsArray(creationOptions.challenge, [ 92, 210, 83, 76, 29, 228, 126, 169 ], "challenge should be as expected");
     77      isEmptyArray(creationOptions.pubKeyCredParams, "pubKeyCredParams should be an empty array");
     78      is(creationOptions.timeout, undefined, "timeout should be undefined");
     79      isEmptyArray(creationOptions.excludeCredentials, "excludeCredentials should be an empty array");
     80      is(creationOptions.authenticatorSelection.authenticatorAttachment, undefined, "authenticatorSelection.authenticatorAttachment should be undefined");
     81      is(creationOptions.authenticatorSelection.residentKey, undefined, "creationOptions.authenticatorSelection.residentKey should be undefined");
     82      is(creationOptions.authenticatorSelection.requireResidentKey, false, "creationOptions.authenticatorSelection.requireResidentKey should be false");
     83      is(creationOptions.authenticatorSelection.userVerification, "preferred", "creationOptions.authenticatorSelection.userVerification should be preferred");
     84      is(creationOptions.attestation, "none", "attestation should be none");
     85      stringArrayEquals(creationOptions.hints, [], "hints should be an empty array");
     86      is(Object.getOwnPropertyNames(creationOptions.extensions).length, 0, "extensions should be an empty object");
     87    });
     88 
     89    add_task(function test_parseCreationOptionsFromJSON() {
     90      let creationOptionsJSON = {
     91        rp: { name: "Example", id: "example.com" },
     92        user: { id: "19TVpqBBOAM", name: "username2", displayName: "another display name" },
     93        challenge: "dR82FeUh5q4",
     94        pubKeyCredParams: [{ type: "public-key", alg: -7 }],
     95        timeout: 20000,
     96        excludeCredentials: [{ type: "public-key", id: "TeM2k_di7Dk", transports: [ "usb" ]}],
     97        authenticatorSelection: { authenticatorAttachment: "platform", residentKey: "required", requireResidentKey: true, userVerification: "discouraged" },
     98        hints: ["hybrid"],
     99        attestation: "indirect",
    100        attestationFormats: ["fido-u2f"],
    101        extensions: {
    102              appid: "https://www.example.com/appID",
    103              credProps: true,
    104              hmacCreateSecret: true,
    105              minPinLength: true,
    106              prf: {
    107                    eval: {
    108                          first: "Zmlyc3Q",
    109                          second: "c2Vjb25k"
    110                        },
    111                    evalByCredential: {
    112                          "19TVpqBBOAM": {
    113                                first: "Zmlyc3Q",
    114                                second: "c2Vjb25k"
    115                              }
    116                        }
    117                  }
    118            },
    119      };
    120      let creationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);
    121      is(Object.getOwnPropertyNames(creationOptions).length, 10, "creation options should have 10 properties");
    122      is(creationOptions.rp.name, "Example", "rp.name should be Example");
    123      is(creationOptions.rp.id, "example.com", "rp.id should be example.com");
    124      arrayBufferEqualsArray(creationOptions.user.id, [ 215, 212, 213, 166, 160, 65, 56, 3 ], "user.id should be as expected");
    125      is(creationOptions.user.displayName, "another display name", "user.displayName should be 'another display name'");
    126      is(creationOptions.user.name, "username2", "user.name should be username2");
    127      arrayBufferEqualsArray(creationOptions.challenge, [ 117, 31, 54, 21, 229, 33, 230, 174 ], "challenge should be as expected");
    128      is(creationOptions.pubKeyCredParams.length, 1, "pubKeyCredParams should have one element");
    129      is(creationOptions.pubKeyCredParams[0].type, "public-key", "pubKeyCredParams[0].type should be public-key");
    130      is(creationOptions.pubKeyCredParams[0].alg, -7, "pubKeyCredParams[0].alg should be -7");
    131      is(creationOptions.timeout, 20000, "timeout should be 20000");
    132      is(creationOptions.excludeCredentials.length, 1, "excludeCredentials should have one element");
    133      is(creationOptions.excludeCredentials[0].type, "public-key", "excludeCredentials[0].type should be public-key");
    134      arrayBufferEqualsArray(creationOptions.excludeCredentials[0].id, [ 77, 227, 54, 147, 247, 98, 236, 57 ], "excludeCredentials[0].id should be as expected");
    135      is(creationOptions.excludeCredentials[0].transports.length, 1, "excludeCredentials[0].transports should have one element");
    136      is(creationOptions.excludeCredentials[0].transports[0], "usb", "excludeCredentials[0].transports[0] should be usb");
    137      is(creationOptions.authenticatorSelection.authenticatorAttachment, "platform", "authenticatorSelection.authenticatorAttachment should be platform");
    138      is(creationOptions.authenticatorSelection.residentKey, "required", "creationOptions.authenticatorSelection.residentKey should be required");
    139      is(creationOptions.authenticatorSelection.requireResidentKey, true, "creationOptions.authenticatorSelection.requireResidentKey should be true");
    140      is(creationOptions.authenticatorSelection.userVerification, "discouraged", "creationOptions.authenticatorSelection.userVerification should be discouraged");
    141      stringArrayEquals(creationOptions.hints, creationOptionsJSON.hints, "creationOptions.hints should be as expected");
    142      is(creationOptions.attestation, "indirect", "attestation should be indirect");
    143      is(creationOptions.extensions.appid, "https://www.example.com/appID", "extensions.appid should be https://www.example.com/appID");
    144      is(creationOptions.extensions.credProps, true, "extensions.credProps should be true");
    145      is(creationOptions.extensions.hmacCreateSecret, true, "extensions.hmacCreateSecret should be true");
    146      is(creationOptions.extensions.minPinLength, true, "extensions.minPinLength should be true");
    147      arrayBufferEqualsArray(creationOptions.extensions.prf.eval.first, [102, 105, 114, 115, 116], "extensions.prf.eval.first should be 'first'");
    148      arrayBufferEqualsArray(creationOptions.extensions.prf.eval.second, [115, 101, 99, 111, 110, 100], "extensions.prf.eval.second should be 'second'");
    149      arrayBufferEqualsArray(creationOptions.extensions.prf.evalByCredential["19TVpqBBOAM"].first, [102, 105, 114, 115, 116], "extensions.prf.evalByCredential[\"19TVpqBBOAM\"].first should be 'first'");
    150      arrayBufferEqualsArray(creationOptions.extensions.prf.evalByCredential["19TVpqBBOAM"].second, [115, 101, 99, 111, 110, 100], "extensions.prf.evalByCredential[\"19TVpqBBOAM\"].second should be 'second'");
    151    });
    152 
    153    add_task(function test_parseCreationOptionsFromJSON_malformed() {
    154      let userIdNotBase64 = {
    155        rp: { name: "Example" },
    156        user: { id: "/not urlsafe base64+", name: "username", displayName: "display name" },
    157        challenge: "XNJTTB3kfqk",
    158        pubKeyCredParams: [],
    159      };
    160      shouldThrow(
    161        () => { PublicKeyCredential.parseCreationOptionsFromJSON(userIdNotBase64); },
    162        "PublicKeyCredential.parseCreationOptionsFromJSON: could not decode user ID as urlsafe base64",
    163        "should get encoding error if user.id is not urlsafe base64"
    164      );
    165 
    166      let challengeNotBase64 = {
    167        rp: { name: "Example" },
    168        user: { id: "-l3qNLTKJng", name: "username", displayName: "display name" },
    169        challenge: "this is not urlsafe base64!",
    170        pubKeyCredParams: [],
    171      };
    172      shouldThrow(
    173        () => { PublicKeyCredential.parseCreationOptionsFromJSON(challengeNotBase64); },
    174        "PublicKeyCredential.parseCreationOptionsFromJSON: could not decode challenge as urlsafe base64",
    175        "should get encoding error if challenge is not urlsafe base64"
    176      );
    177 
    178      let excludeCredentialsIdNotBase64 = {
    179        rp: { name: "Example", id: "example.com" },
    180        user: { id: "-l3qNLTKJng", name: "username", displayName: "display name" },
    181        challenge: "dR82FeUh5q4",
    182        pubKeyCredParams: [{ type: "public-key", alg: -7 }],
    183        timeout: 20000,
    184        excludeCredentials: [{ type: "public-key", id: "@#$%&^", transports: [ "usb" ]}],
    185        authenticatorselection: { authenticatorattachment: "platform", residentkey: "required", requireresidentkey: true, userverification: "discouraged" },
    186        hints: ["hybrid"],
    187        attestation: "indirect",
    188        attestationformats: ["fido-u2f"],
    189        extensions: { appid: "https://www.example.com/appid", hmaccreatesecret: true },
    190      };
    191      shouldThrow(
    192        () => { PublicKeyCredential.parseCreationOptionsFromJSON(excludeCredentialsIdNotBase64); },
    193        "PublicKeyCredential.parseCreationOptionsFromJSON: could not decode excluded credential ID as urlsafe base64",
    194        "should get encoding error if excludeCredentials[0].id is not urlsafe base64"
    195      );
    196    });
    197 
    198    add_task(function test_parseRequestOptionsFromJSON_minimal() {
    199      let requestOptionsJSON = {
    200        challenge: "3yW2WHD_jbU",
    201      };
    202      let requestOptions = PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJSON);
    203      is(Object.getOwnPropertyNames(requestOptions).length, 5, "request options should have 5 properties");
    204      arrayBufferEqualsArray(requestOptions.challenge, [ 223, 37, 182, 88, 112, 255, 141, 181 ], "challenge should be as expected");
    205      is(requestOptions.timeout, undefined, "timeout should be undefined");
    206      is(requestOptions.rpId, undefined, "rpId should be undefined");
    207      isEmptyArray(requestOptions.allowCredentials, "allowCredentials should be an empty array");
    208      is(requestOptions.userVerification, "preferred", "userVerification should be preferred");
    209      stringArrayEquals(requestOptions.hints, [], "hints should be an empty array");
    210      is(Object.getOwnPropertyNames(requestOptions.extensions).length, 0, "extensions should be an empty object");
    211    });
    212 
    213    add_task(function test_parseRequestOptionsFromJSON() {
    214      let requestOptionsJSON = {
    215        challenge: "QAfaZwEQCkQ",
    216        timeout: 25000,
    217        rpId: "example.com",
    218        allowCredentials: [{type: "public-key", id: "BTBXXGuXRTk", transports: ["smart-card"] }],
    219        userVerification: "discouraged",
    220        hints: ["client-device"],
    221        attestation: "enterprise",
    222        attestationFormats: ["packed"],
    223        extensions: {
    224              appid: "https://www.example.com/anotherAppID",
    225              prf: {
    226                    eval: {
    227                          first: "Zmlyc3Q",
    228                          second: "c2Vjb25k"
    229                        },
    230                    evalByCredential: {
    231                          "19TVpqBBOAM": {
    232                                first: "Zmlyc3Q",
    233                                second: "c2Vjb25k"
    234                              }
    235                        }
    236                  }
    237            },
    238      };
    239      let requestOptions = PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJSON);
    240      is(Object.getOwnPropertyNames(requestOptions).length, 7, "request options should have 7 properties");
    241      arrayBufferEqualsArray(requestOptions.challenge, [ 64, 7, 218, 103, 1, 16, 10, 68 ], "challenge should be as expected");
    242      is(requestOptions.timeout, 25000, "timeout should be 25000");
    243      is(requestOptions.rpId, "example.com", "rpId should be example.com");
    244      is(requestOptions.allowCredentials.length, 1, "allowCredentials should have one element");
    245      is(requestOptions.allowCredentials[0].type, "public-key", "allowCredentials[0].type should be public-key");
    246      arrayBufferEqualsArray(requestOptions.allowCredentials[0].id, [ 5, 48, 87, 92, 107, 151, 69, 57 ], "allowCredentials[0].id should be as expected");
    247      is(requestOptions.allowCredentials[0].transports.length, 1, "allowCredentials[0].transports should have one element");
    248      is(requestOptions.allowCredentials[0].transports[0], "smart-card", "allowCredentials[0].transports[0] should be usb");
    249      is(requestOptions.userVerification, "discouraged", "userVerification should be discouraged");
    250      stringArrayEquals(requestOptions.hints, requestOptionsJSON.hints, "requestOptions.hints should be as expected");
    251      is(requestOptions.extensions.appid, "https://www.example.com/anotherAppID", "extensions.appid should be https://www.example.com/anotherAppID");
    252      arrayBufferEqualsArray(requestOptions.extensions.prf.eval.first, [102, 105, 114, 115, 116], "extensions.prf.eval.first should be 'first'");
    253      arrayBufferEqualsArray(requestOptions.extensions.prf.eval.second, [115, 101, 99, 111, 110, 100], "extensions.prf.eval.second should be 'second'");
    254      arrayBufferEqualsArray(requestOptions.extensions.prf.evalByCredential["19TVpqBBOAM"].first, [102, 105, 114, 115, 116], "extensions.prf.evalByCredential[\"19TVpqBBOAM\"].first should be 'first'");
    255      arrayBufferEqualsArray(requestOptions.extensions.prf.evalByCredential["19TVpqBBOAM"].second, [115, 101, 99, 111, 110, 100], "extensions.prf.evalByCredential[\"19TVpqBBOAM\"].second should be 'second'");
    256    });
    257 
    258    add_task(function test_parseRequestOptionsFromJSON_malformed() {
    259      let challengeNotBase64 = {
    260        challenge: "/not+urlsafe+base64/",
    261      };
    262      shouldThrow(
    263        () => { PublicKeyCredential.parseRequestOptionsFromJSON(challengeNotBase64); },
    264        "PublicKeyCredential.parseRequestOptionsFromJSON: could not decode challenge as urlsafe base64",
    265        "should get encoding error if challenge is not urlsafe base64"
    266      );
    267 
    268      let allowCredentialsIdNotBase64 = {
    269        challenge: "QAfaZwEQCkQ",
    270        timeout: 25000,
    271        rpId: "example.com",
    272        allowCredentials: [{type: "public-key", id: "not urlsafe base64", transports: ["smart-card"] }],
    273        userVerification: "discouraged",
    274        hints: ["client-device"],
    275        attestation: "enterprise",
    276        attestationFormats: ["packed"],
    277        extensions: { appid: "https://www.example.com/anotherAppID" },
    278      };
    279      shouldThrow(
    280        () => { PublicKeyCredential.parseRequestOptionsFromJSON(allowCredentialsIdNotBase64); },
    281        "PublicKeyCredential.parseRequestOptionsFromJSON: could not decode allowed credential ID as urlsafe base64",
    282        "should get encoding error if allowCredentials[0].id is not urlsafe base64"
    283      );
    284    });
    285 
    286    add_task(async () => {
    287      await addVirtualAuthenticator();
    288    });
    289 
    290    function isUrlsafeBase64(urlsafeBase64) {
    291      try {
    292        atob(urlsafeBase64.replace(/_/g, "/").replace(/-/g, "+"));
    293        return true;
    294      } catch (_) {}
    295      return false;
    296    }
    297 
    298    add_task(async function test_registrationResponse_toJSON() {
    299      let publicKey = {
    300        rp: {id: document.domain, name: "none", icon: "none"},
    301        user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
    302        challenge: crypto.getRandomValues(new Uint8Array(16)),
    303        pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
    304        authenticatorSelection: { residentKey: "discouraged" },
    305        extensions: { credProps: true }
    306      };
    307      let registrationResponse = await navigator.credentials.create({publicKey});
    308      let registrationResponseJSON = registrationResponse.toJSON();
    309      is(Object.keys(registrationResponseJSON).length, 6, "registrationResponseJSON should have 6 properties");
    310      is(registrationResponseJSON.id, registrationResponseJSON.rawId, "registrationResponseJSON.id and rawId should be the same");
    311      ok(isUrlsafeBase64(registrationResponseJSON.id), "registrationResponseJSON.id should be urlsafe base64");
    312      is(Object.keys(registrationResponseJSON.response).length, 6, "registrationResponseJSON.response should have 6 properties");
    313      ok(isUrlsafeBase64(registrationResponseJSON.response.clientDataJSON), "registrationResponseJSON.response.clientDataJSON should be urlsafe base64");
    314      ok(isUrlsafeBase64(registrationResponseJSON.response.authenticatorData), "registrationResponseJSON.response.authenticatorData should be urlsafe base64");
    315      ok(isUrlsafeBase64(registrationResponseJSON.response.publicKey), "registrationResponseJSON.response.publicKey should be urlsafe base64");
    316      ok(isUrlsafeBase64(registrationResponseJSON.response.attestationObject), "registrationResponseJSON.response.attestationObject should be urlsafe base64");
    317      is(registrationResponseJSON.response.publicKeyAlgorithm, cose_alg_ECDSA_w_SHA256, "registrationResponseJSON.response.publicKeyAlgorithm should be ECDSA with SHA256 (COSE)");
    318      is(registrationResponseJSON.response.transports.length, 1, "registrationResponseJSON.response.transports.length should be 1");
    319      is(registrationResponseJSON.response.transports[0], "internal", "registrationResponseJSON.response.transports[0] should be internal");
    320      is(registrationResponseJSON.authenticatorAttachment, "platform", "registrationResponseJSON.authenticatorAttachment should be platform");
    321      is(registrationResponseJSON.clientExtensionResults?.credProps?.rk, false, "registrationResponseJSON.clientExtensionResults.credProps.rk should be false");
    322      is(registrationResponseJSON.type, "public-key", "registrationResponseJSON.type should be public-key");
    323    });
    324 
    325    add_task(async function test_assertionResponse_toJSON() {
    326      let registrationRequest = {
    327        publicKey: {
    328          rp: {id: document.domain, name: "none", icon: "none"},
    329          user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
    330          challenge: crypto.getRandomValues(new Uint8Array(16)),
    331          pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
    332          extensions: { prf: { enabled: true } }
    333        },
    334      };
    335      let registrationResponse = await navigator.credentials.create(registrationRequest);
    336 
    337      let assertionRequest = {
    338        publicKey: {
    339          challenge: crypto.getRandomValues(new Uint8Array(16)),
    340          allowCredentials: [{ type: "public-key", id: registrationResponse.rawId }],
    341          extensions: { prf: { eval: { first: new Uint8Array([1,2,3,4,5]).buffer } } }
    342        },
    343      };
    344      let assertionResponse = await navigator.credentials.get(assertionRequest);
    345      let assertionResponseJSON = assertionResponse.toJSON();
    346      is(Object.keys(assertionResponseJSON).length, 6, "assertionResponseJSON should have 6 properties");
    347      is(assertionResponseJSON.id, assertionResponseJSON.rawId, "assertionResponseJSON.id and rawId should be the same");
    348      ok(isUrlsafeBase64(assertionResponseJSON.id), "assertionResponseJSON.id should be urlsafe base64");
    349      is(Object.keys(assertionResponseJSON.response).length, 3, "assertionResponseJSON.response should have 3 properties");
    350      ok(isUrlsafeBase64(assertionResponseJSON.response.clientDataJSON), "assertionResponseJSON.response.clientDataJSON should be urlsafe base64");
    351      ok(isUrlsafeBase64(assertionResponseJSON.response.authenticatorData), "assertionResponseJSON.response.authenticatorData should be urlsafe base64");
    352      ok(isUrlsafeBase64(assertionResponseJSON.response.signature), "assertionResponseJSON.response.signature should be urlsafe base64");
    353      is(assertionResponseJSON.authenticatorAttachment, "platform", "assertionResponseJSON.authenticatorAttachment should be platform");
    354      is(Object.keys(assertionResponseJSON.clientExtensionResults).length, 1, "assertionResponseJSON.clientExtensionResults should have one entry");
    355      ok(isUrlsafeBase64(assertionResponseJSON.clientExtensionResults.prf.results.first), "assertionResponseJSON.clientExtensionResults.prf.results should be urlsafe base64");
    356      is(assertionResponseJSON.clientExtensionResults.prf.results.first.length, 43, "assertionResponseJSON.clientExtensionResults.prf.results should be of length 43");
    357      is(assertionResponseJSON.type, "public-key", "assertionResponseJSON.type should be public-key");
    358    });
    359  </script>
    360 
    361 </body>
    362 </html>