tor-browser

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

helper.js (9406B)


      1 //
      2 // Exciting constants we'll use for test cases below:
      3 //
      4 const kValidKeys = {
      5  // https://www.rfc-editor.org/rfc/rfc9421.html#name-example-ed25519-test-key
      6  rfc: "JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=",
      7 
      8  // Randomly generated key:
      9  //
     10  // {
     11  //   "crv": "Ed25519",
     12  //   "d": "MTodZiTA9CBsuIvSfO679TThkG3b7ce6R3sq_CdyVp4",
     13  //   "ext": true,
     14  //   "kty": "OKP",
     15  //   "x": "xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE"
     16  // }
     17  //
     18  arbitrary: "xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE="
     19 };
     20 
     21 // As above in kValidKeys, but in JWK format (including the private key).
     22 const kValidKeysJWK = {
     23  // https://www.rfc-editor.org/rfc/rfc9421.html#name-example-ed25519-test-key
     24  rfc: {
     25    "kty": "OKP",
     26    "crv": "Ed25519",
     27    "kid": "test-key-ed25519",
     28    "d": "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU",
     29    "x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
     30  },
     31 
     32  // Matching private key to arbitrary public key above.
     33  arbitrary: {
     34    "crv": "Ed25519",
     35    "d": "MTodZiTA9CBsuIvSfO679TThkG3b7ce6R3sq_CdyVp4",
     36    "ext": true,
     37    "kty": "OKP",
     38    "x": "xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE"
     39  }
     40 };
     41 
     42 // A key with the right length that cannot be used to verify the HTTP response
     43 // above.
     44 const kInvalidKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
     45 
     46 // Generated test expectations are more readable if we're using something
     47 // other than a boolean.
     48 const EXPECT_BLOCKED = "block";
     49 const EXPECT_LOADED = "loaded";
     50 
     51 const kAcceptSignature = "accept-signature";
     52 
     53 // Given `{ digest: "...", body: "...", cors: true, type: "..." }`, generates
     54 // the URL to a script resource that has the given characteristics.
     55 let counter = 0;
     56 function resourceURL(data, server_origin) {
     57  counter++;
     58  data.type ??= "application/javascript";
     59  data.counter = counter;
     60  let params = new URLSearchParams(data);
     61  let result = new URL("/subresource-integrity/signatures/resource.py?" + params.toString(), server_origin ?? self.location.origin);
     62  return result.href;
     63 }
     64 
     65 // Given a signature base (actually an arbitrary string) and a key in JWK
     66 // format, generates a base64-encoded Ed25519 signature. Only available over
     67 // HTTPS.
     68 async function signSignatureBase(signatureBase, privateKeyJWK) {
     69  assert_true(self.isSecureContext, "Signatures can only be generated in secure contexts.");
     70  const privateKey = await crypto.subtle.importKey(
     71    'jwk',
     72    privateKeyJWK,
     73    'Ed25519',
     74    true, // extractable
     75    ['sign']
     76  );
     77 
     78  const encoder = new TextEncoder();
     79  const messageBytes = encoder.encode(signatureBase);
     80 
     81  const signatureBytes = await crypto.subtle.sign(
     82    { name: 'Ed25519' },
     83    privateKey,
     84    messageBytes
     85  );
     86 
     87  return btoa(String.fromCharCode(...new Uint8Array(signatureBytes)));
     88 }
     89 
     90 function generate_fetch_test(request_data, options, expectation, description) {
     91  promise_test(test => {
     92    const url = resourceURL(request_data, options.origin);
     93    let fetch_options = {};
     94    if (options.mode) {
     95      fetch_options.mode = options.mode;
     96    } else if (options.origin) {
     97      fetch_options.mode = "cors";
     98    }
     99    if (options.integrity) {
    100      fetch_options.integrity = options.integrity;
    101    }
    102 
    103    let fetcher = fetch(url, fetch_options);
    104    if (expectation == EXPECT_LOADED) {
    105      return fetcher.then(r => {
    106        const expected_status = options.mode == "no-cors" ? 0 : (request_data.status ?? 200);
    107        assert_equals(r.status, expected_status, `Response status is ${expected_status}.`);
    108 
    109        // Verify `accept-signature`: if the invalid key is present, both a valid and invalid
    110        // key were set. If just the valid key is present, that's the only key we should see
    111        // in the header.
    112        if (options.integrity?.includes(`ed25519-${kInvalidKey}`)) {
    113          assert_equals(r.headers.get(kAcceptSignature),
    114                        `sig0=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="ed25519-integrity", sig1=("unencoded-digest";sf);keyid="${kInvalidKey}";tag="ed25519-integrity"`,
    115                        "`accept-signature` was set.");
    116        } else if (options.integrity?.includes(`ed25519-${kValidKeys['rfc']}`)) {
    117          assert_equals(r.headers.get(kAcceptSignature),
    118                        `sig0=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="ed25519-integrity"`,
    119                        "`accept-signature` was set.");
    120        }
    121      });
    122    } else {
    123      return promise_rejects_js(test, TypeError, fetcher);
    124    }
    125  }, "`fetch()`: " + description);
    126 }
    127 
    128 function generate_query_test(query, options, expectation, description) {
    129  promise_test(test => {
    130    let url = new URL("/subresource-integrity/signatures/query-resource.py?" + query, self.location);
    131 
    132    let fetch_options = {};
    133    if (options.mode) {
    134      fetch_options.mode = options.mode;
    135    }
    136    if (options.integrity) {
    137      fetch_options.integrity = options.integrity;
    138    }
    139 
    140    let fetcher = fetch(url, fetch_options);
    141    if (expectation == EXPECT_LOADED) {
    142      return fetcher.then(r => {
    143        const expected_status = options.mode == "no-cors" ? 0 : 200;
    144        assert_equals(r.status, expected_status, `Response status is ${expected_status}.`);
    145      });
    146    } else {
    147      return promise_rejects_js(test, TypeError, fetcher);
    148    }
    149  }, "`fetch()`: " + description);
    150 }
    151 
    152 /*
    153 * Script tests
    154 *
    155 * Metadata for a script which expects to execute correctly and a script that
    156 * does not.
    157 */
    158 const kScriptToExecute = {
    159  body: "window.hello = `world`;",
    160  hash: "PZJ+9CdAAIacg7wfUe4t/RkDQJVKM0mCZ2K7qiRhHFc=",
    161 
    162  signatures: {
    163    // ```
    164    // "unencoded-digest";sf: sha-256=:PZJ+9CdAAIacg7wfUe4t/RkDQJVKM0mCZ2K7qiRhHFc=:
    165    // "@signature-params": ("unencoded-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri"
    166    // ```
    167    rfc: "A1wOGCGrcfN34uMe2Umt7hJ6Su1MQFUL1QuT5nmk1R8I761eXUt2Zv4D5fOt1h1+4DlHPiA1FVwfJLbwlWnpBw==",
    168 
    169    // ```
    170    // "unencoded-digest";sf: sha-256=:PZJ+9CdAAIacg7wfUe4t/RkDQJVKM0mCZ2K7qiRhHFc=:
    171    // "@signature-params": ("unencoded-digest";sf);keyid="xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE=";tag="sri"
    172    // ```
    173    arbitrary: "odk/ec9gO/DCcLPa1xSW1cSmB2s4XU3iDOxJAiod4v5/YBESjvwEJNAO9x4Frn/7rRIZ7sL5LwRNaymdHokOBQ=="
    174  }
    175 };
    176 
    177 const kScriptToBlock = {
    178  body: "assert_unreached(`This code should not execute.`);",
    179  hash: "FUSFR1N3vTmSGbI7q9jaMbHq+ogNeBfpznOIufaIfpc=",
    180 
    181  signatures: {
    182    // ```
    183    // "unencoded-digest";sf: sha-256=:FUSFR1N3vTmSGbI7q9jaMbHq+ogNeBfpznOIufaIfpc=:
    184    // "@signature-params": ("unencoded-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri"
    185    // ```
    186    rfc: "bR3fU6kzmMLol4GIcgj19+It0GB0dlKrD4ssH+SCz0vTLAdT3zt6Kfq4V60NnDdn62XGNr20b0TEKtfcremcDw==",
    187 
    188    // ```
    189    // "unencoded-digest";sf: sha-256=:FUSFR1N3vTmSGbI7q9jaMbHq+ogNeBfpznOIufaIfpc=:
    190    // "@signature-params": ("unencoded-digest";sf);keyid="xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE";tag="sri"
    191    // ```
    192    arbitrary: "+5Iol+V65SW2qkpsTCyqYQJC4NZIsUGeNbO5kS9WdTboa9gg/nV6LwnySM02612YvPm++671nN9dBDJPYncuBA=="
    193  }
    194 };
    195 
    196 // These constants use the metadata above to create dictionaries that can be
    197 // passed into `generate_script_test` below.
    198 const kUnsignedShouldExecute = { body: kScriptToExecute['body'] };
    199 const kUnsignedShouldBlock = { body: kScriptToBlock['body'] };
    200 const kSignedShouldExecute = {
    201  body: kScriptToExecute['body'],
    202  digest: `sha-256=:${kScriptToExecute['hash']}:`,
    203  signatureInput: `signature=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri"`,
    204  signature: `signature=:${kScriptToExecute['signatures']['rfc']}:`
    205 };
    206 const kSignedShouldBlock = {
    207  body: kScriptToBlock['body'],
    208  digest: `sha-256=:${kScriptToBlock['hash']}:`,
    209  signatureInput: `signature=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri"`,
    210  signature: `signature=:${kScriptToBlock['signatures']['rfc']}:`
    211 };
    212 const kMultiplySignedShouldExecute = {
    213  body: kScriptToExecute['body'],
    214  digest: `sha-256=:${kScriptToExecute['hash']}:`,
    215  signatureInput: `signature1=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri", ` +
    216                  `signature2=("unencoded-digest";sf);keyid="${kValidKeys['arbitrary']}";tag="sri"`,
    217  signature: `signature1=:${kScriptToExecute['signatures']['rfc']}:, ` +
    218             `signature2=:${kScriptToExecute['signatures']['arbitrary']}:`
    219 };
    220 const kMultiplySignedShouldBlock = {
    221  body: kScriptToBlock['body'],
    222  digest: `sha-256=:${kScriptToBlock['hash']}:`,
    223  signatureInput: `signature1=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri", ` +
    224                  `signature2=("unencoded-digest";sf);keyid="${kValidKeys['arbitrary']}";tag="sri"`,
    225  signature: `signature1=:${kScriptToBlock['signatures']['rfc']}:, ` +
    226             `signature2=:${kScriptToBlock['signatures']['arbitrary']}:`
    227 };
    228 
    229 function generate_script_test(request_data, integrity, expectation, description) {
    230  async_test(t => {
    231    let s = document.createElement('script');
    232    s.src = resourceURL(request_data);
    233    s.integrity = integrity;
    234    if (expectation == EXPECT_BLOCKED) {
    235      s.onerror = t.step_func_done(e => {
    236        assert_equals("error", e.type);
    237      });
    238      s.onload = t.unreached_func("Script should not execute.");
    239    } else {
    240      s.onload = t.step_func_done(e => {
    241        assert_equals("load", e.type);
    242      });
    243      s.onerror = t.unreached_func("Script should not fail.");
    244    }
    245    document.body.appendChild(s);
    246  }, "`<script>`: " + description);
    247 }