tor-browser

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

u2futil.js (14045B)


      1 // Used by local_addTest() / local_completeTest()
      2 var _countCompletions = 0;
      3 var _expectedCompletions = 0;
      4 
      5 const flag_TUP = 0x01;
      6 const flag_UV = 0x04;
      7 const flag_AT = 0x40;
      8 
      9 const cose_kty = 1;
     10 const cose_kty_ec2 = 2;
     11 const cose_alg = 3;
     12 const cose_alg_ECDSA_w_SHA256 = -7;
     13 const cose_alg_ECDSA_w_SHA512 = -36;
     14 const cose_crv = -1;
     15 const cose_crv_P256 = 1;
     16 const cose_crv_x = -2;
     17 const cose_crv_y = -3;
     18 
     19 var { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
     20  "resource://gre/modules/AppConstants.sys.mjs"
     21 );
     22 
     23 async function addVirtualAuthenticator(
     24  protocol = "ctap2_1",
     25  transport = "internal",
     26  hasResidentKey = true,
     27  hasUserVerification = true,
     28  isUserConsenting = true,
     29  isUserVerified = true
     30 ) {
     31  let id = await SpecialPowers.spawnChrome(
     32    [
     33      protocol,
     34      transport,
     35      hasResidentKey,
     36      hasUserVerification,
     37      isUserConsenting,
     38      isUserVerified,
     39    ],
     40    (
     41      protocol,
     42      transport,
     43      hasResidentKey,
     44      hasUserVerification,
     45      isUserConsenting,
     46      isUserVerified
     47    ) => {
     48      let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
     49        Ci.nsIWebAuthnService
     50      );
     51      let id = webauthnService.addVirtualAuthenticator(
     52        protocol,
     53        transport,
     54        hasResidentKey,
     55        hasUserVerification,
     56        isUserConsenting,
     57        isUserVerified
     58      );
     59      return id;
     60    }
     61  );
     62 
     63  SimpleTest.registerCleanupFunction(async () => {
     64    await SpecialPowers.spawnChrome([id], id => {
     65      let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
     66        Ci.nsIWebAuthnService
     67      );
     68      webauthnService.removeVirtualAuthenticator(id);
     69    });
     70  });
     71 
     72  return id;
     73 }
     74 
     75 async function setUserVerified(authenticatorId, isUserVerified) {
     76  await SpecialPowers.spawnChrome(
     77    [authenticatorId, isUserVerified],
     78    (authenticatorId, isUserVerified) => {
     79      let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
     80        Ci.nsIWebAuthnService
     81      );
     82      webauthnService.setUserVerified(authenticatorId, isUserVerified);
     83    }
     84  );
     85 }
     86 
     87 function handleEventMessage(event) {
     88  if ("test" in event.data) {
     89    let summary = event.data.test + ": " + event.data.msg;
     90    log(event.data.status + ": " + summary);
     91    ok(event.data.status, summary);
     92  } else if ("done" in event.data) {
     93    SimpleTest.finish();
     94  } else {
     95    ok(false, "Unexpected message in the test harness: " + event.data);
     96  }
     97 }
     98 
     99 function log(msg) {
    100  console.log(msg);
    101  let logBox = document.getElementById("log");
    102  if (logBox) {
    103    logBox.textContent += "\n" + msg;
    104  }
    105 }
    106 
    107 function local_is(value, expected, message) {
    108  if (value === expected) {
    109    local_ok(true, message);
    110  } else {
    111    local_ok(false, message + " unexpectedly: " + value + " !== " + expected);
    112  }
    113 }
    114 
    115 function local_isnot(value, expected, message) {
    116  if (value !== expected) {
    117    local_ok(true, message);
    118  } else {
    119    local_ok(false, message + " unexpectedly: " + value + " === " + expected);
    120  }
    121 }
    122 
    123 function local_ok(expression, message) {
    124  let body = { test: this.location.pathname, status: expression, msg: message };
    125  parent.postMessage(body, "http://mochi.test:8888");
    126 }
    127 
    128 function local_doesThrow(fn, name) {
    129  let gotException = false;
    130  try {
    131    fn();
    132  } catch (ex) {
    133    gotException = true;
    134  }
    135  local_ok(gotException, name);
    136 }
    137 
    138 function local_expectThisManyTests(count) {
    139  if (_expectedCompletions > 0) {
    140    local_ok(
    141      false,
    142      "Error: local_expectThisManyTests should only be called once."
    143    );
    144  }
    145  _expectedCompletions = count;
    146 }
    147 
    148 function local_completeTest() {
    149  _countCompletions += 1;
    150  if (_countCompletions == _expectedCompletions) {
    151    log("All tests completed.");
    152    local_finished();
    153  }
    154  if (_countCompletions > _expectedCompletions) {
    155    local_ok(
    156      false,
    157      "Error: local_completeTest called more than local_addTest."
    158    );
    159  }
    160 }
    161 
    162 function local_finished() {
    163  parent.postMessage({ done: true }, "http://mochi.test:8888");
    164 }
    165 
    166 function string2buffer(str) {
    167  return new Uint8Array(str.length).map((x, i) => str.charCodeAt(i));
    168 }
    169 
    170 function buffer2string(buf) {
    171  let str = "";
    172  if (!(buf.constructor === Uint8Array)) {
    173    buf = new Uint8Array(buf);
    174  }
    175  buf.map(function (x) {
    176    return (str += String.fromCharCode(x));
    177  });
    178  return str;
    179 }
    180 
    181 function bytesToBase64(u8a) {
    182  let CHUNK_SZ = 0x8000;
    183  let c = [];
    184  let array = new Uint8Array(u8a);
    185  for (let i = 0; i < array.length; i += CHUNK_SZ) {
    186    c.push(String.fromCharCode.apply(null, array.subarray(i, i + CHUNK_SZ)));
    187  }
    188  return window.btoa(c.join(""));
    189 }
    190 
    191 function base64ToBytes(b64encoded) {
    192  return new Uint8Array(
    193    window
    194      .atob(b64encoded)
    195      .split("")
    196      .map(function (c) {
    197        return c.charCodeAt(0);
    198      })
    199  );
    200 }
    201 
    202 function bytesToBase64UrlSafe(buf) {
    203  return bytesToBase64(buf)
    204    .replace(/\+/g, "-")
    205    .replace(/\//g, "_")
    206    .replace(/=/g, "");
    207 }
    208 
    209 function base64ToBytesUrlSafe(str) {
    210  if (str.length % 4 == 1) {
    211    throw "Improper b64 string";
    212  }
    213 
    214  var b64 = str.replace(/\-/g, "+").replace(/\_/g, "/");
    215  while (b64.length % 4 != 0) {
    216    b64 += "=";
    217  }
    218  return base64ToBytes(b64);
    219 }
    220 
    221 function hexEncode(buf) {
    222  return Array.from(buf)
    223    .map(x => ("0" + x.toString(16)).substr(-2))
    224    .join("");
    225 }
    226 
    227 function hexDecode(str) {
    228  return new Uint8Array(str.match(/../g).map(x => parseInt(x, 16)));
    229 }
    230 
    231 function hasOnlyKeys(obj, ...keys) {
    232  let okeys = new Set(Object.keys(obj));
    233  return keys.length == okeys.size && keys.every(k => okeys.has(k));
    234 }
    235 
    236 function webAuthnDecodeCBORAttestation(aCborAttBuf) {
    237  let attObj = CBOR.decode(aCborAttBuf);
    238  console.log(":: Attestation CBOR Object ::");
    239  if (!hasOnlyKeys(attObj, "authData", "fmt", "attStmt")) {
    240    return Promise.reject("Invalid CBOR Attestation Object");
    241  }
    242  if (attObj.fmt == "fido-u2f" && !hasOnlyKeys(attObj.attStmt, "sig", "x5c")) {
    243    return Promise.reject("Invalid CBOR Attestation Statement");
    244  }
    245  if (
    246    attObj.fmt == "packed" &&
    247    !(
    248      hasOnlyKeys(attObj.attStmt, "alg", "sig") ||
    249      hasOnlyKeys(attObj.attStmt, "alg", "sig", "x5c")
    250    )
    251  ) {
    252    return Promise.reject("Invalid CBOR Attestation Statement");
    253  }
    254  if (attObj.fmt == "none" && Object.keys(attObj.attStmt).length) {
    255    return Promise.reject("Invalid CBOR Attestation Statement");
    256  }
    257 
    258  return webAuthnDecodeAuthDataArray(new Uint8Array(attObj.authData)).then(
    259    function (aAuthDataObj) {
    260      attObj.authDataObj = aAuthDataObj;
    261      return Promise.resolve(attObj);
    262    }
    263  );
    264 }
    265 
    266 function webAuthnDecodeAuthDataArray(aAuthData) {
    267  let rpIdHash = aAuthData.slice(0, 32);
    268  let flags = aAuthData.slice(32, 33);
    269  let counter = aAuthData.slice(33, 37);
    270 
    271  console.log(":: Authenticator Data ::");
    272  console.log("RP ID Hash: " + hexEncode(rpIdHash));
    273  console.log("Counter: " + hexEncode(counter) + " Flags: " + flags);
    274 
    275  if ((flags & flag_AT) == 0x00) {
    276    // No Attestation Data, so we're done.
    277    return Promise.resolve({
    278      rpIdHash,
    279      flags,
    280      counter,
    281    });
    282  }
    283 
    284  if (aAuthData.length < 38) {
    285    return Promise.reject(
    286      "Authenticator Data flag was set, but not enough data passed in!"
    287    );
    288  }
    289 
    290  let attData = {};
    291  attData.aaguid = aAuthData.slice(37, 53);
    292  attData.credIdLen = (aAuthData[53] << 8) + aAuthData[54];
    293  attData.credId = aAuthData.slice(55, 55 + attData.credIdLen);
    294 
    295  console.log(":: Authenticator Data ::");
    296  console.log("AAGUID: " + hexEncode(attData.aaguid));
    297 
    298  let cborPubKey = aAuthData.slice(55 + attData.credIdLen);
    299  var pubkeyObj = CBOR.decode(cborPubKey.buffer);
    300  if (
    301    !(
    302      cose_kty in pubkeyObj &&
    303      cose_alg in pubkeyObj &&
    304      cose_crv in pubkeyObj &&
    305      cose_crv_x in pubkeyObj &&
    306      cose_crv_y in pubkeyObj
    307    )
    308  ) {
    309    throw "Invalid CBOR Public Key Object";
    310  }
    311  if (pubkeyObj[cose_kty] != cose_kty_ec2) {
    312    throw "Unexpected key type";
    313  }
    314  if (pubkeyObj[cose_alg] != cose_alg_ECDSA_w_SHA256) {
    315    throw "Unexpected public key algorithm";
    316  }
    317  if (pubkeyObj[cose_crv] != cose_crv_P256) {
    318    throw "Unexpected curve";
    319  }
    320 
    321  let pubKeyBytes = assemblePublicKeyBytesData(
    322    pubkeyObj[cose_crv_x],
    323    pubkeyObj[cose_crv_y]
    324  );
    325  console.log(":: CBOR Public Key Object Data ::");
    326  console.log("kty: " + pubkeyObj[cose_kty] + " (EC2)");
    327  console.log("alg: " + pubkeyObj[cose_alg] + " (ES256)");
    328  console.log("crv: " + pubkeyObj[cose_crv] + " (P256)");
    329  console.log("X: " + pubkeyObj[cose_crv_x]);
    330  console.log("Y: " + pubkeyObj[cose_crv_y]);
    331  console.log("Uncompressed (hex): " + hexEncode(pubKeyBytes));
    332 
    333  return importPublicKey(pubKeyBytes).then(function (aKeyHandle) {
    334    return Promise.resolve({
    335      rpIdHash,
    336      flags,
    337      counter,
    338      attestationAuthData: attData,
    339      publicKeyBytes: pubKeyBytes,
    340      publicKeyHandle: aKeyHandle,
    341    });
    342  });
    343 }
    344 
    345 function importPublicKey(keyBytes) {
    346  if (keyBytes[0] != 0x04 || keyBytes.byteLength != 65) {
    347    throw "Bad public key octet string";
    348  }
    349  var jwk = {
    350    kty: "EC",
    351    crv: "P-256",
    352    x: bytesToBase64UrlSafe(keyBytes.slice(1, 33)),
    353    y: bytesToBase64UrlSafe(keyBytes.slice(33)),
    354  };
    355  return crypto.subtle.importKey(
    356    "jwk",
    357    jwk,
    358    { name: "ECDSA", namedCurve: "P-256" },
    359    true,
    360    ["verify"]
    361  );
    362 }
    363 
    364 function deriveAppAndChallengeParam(appId, clientData, attestation) {
    365  var appIdBuf = string2buffer(appId);
    366  return Promise.all([
    367    crypto.subtle.digest("SHA-256", appIdBuf),
    368    crypto.subtle.digest("SHA-256", clientData),
    369  ]).then(function (digests) {
    370    return {
    371      appParam: new Uint8Array(digests[0]),
    372      challengeParam: new Uint8Array(digests[1]),
    373      attestation,
    374    };
    375  });
    376 }
    377 
    378 function assemblePublicKeyBytesData(xCoord, yCoord) {
    379  // Produce an uncompressed EC key point. These start with 0x04, and then
    380  // two 32-byte numbers denoting X and Y.
    381  if (xCoord.length != 32 || yCoord.length != 32) {
    382    throw "Coordinates must be 32 bytes long";
    383  }
    384  let keyBytes = new Uint8Array(65);
    385  keyBytes[0] = 0x04;
    386  xCoord.map((x, i) => (keyBytes[1 + i] = x));
    387  yCoord.map((x, i) => (keyBytes[33 + i] = x));
    388  return keyBytes;
    389 }
    390 
    391 function assembleSignedData(appParam, flags, counter, challengeParam) {
    392  let signedData = new Uint8Array(32 + 1 + 4 + 32);
    393  new Uint8Array(appParam).map((x, i) => (signedData[0 + i] = x));
    394  signedData[32] = new Uint8Array(flags)[0];
    395  new Uint8Array(counter).map((x, i) => (signedData[33 + i] = x));
    396  new Uint8Array(challengeParam).map((x, i) => (signedData[37 + i] = x));
    397  return signedData;
    398 }
    399 
    400 function assembleRegistrationSignedData(
    401  appParam,
    402  challengeParam,
    403  keyHandle,
    404  pubKey
    405 ) {
    406  let signedData = new Uint8Array(1 + 32 + 32 + keyHandle.length + 65);
    407  signedData[0] = 0x00;
    408  new Uint8Array(appParam).map((x, i) => (signedData[1 + i] = x));
    409  new Uint8Array(challengeParam).map((x, i) => (signedData[33 + i] = x));
    410  new Uint8Array(keyHandle).map((x, i) => (signedData[65 + i] = x));
    411  new Uint8Array(pubKey).map(
    412    (x, i) => (signedData[65 + keyHandle.length + i] = x)
    413  );
    414  return signedData;
    415 }
    416 
    417 function sanitizeSigArray(arr) {
    418  // ECDSA signature fields into WebCrypto must be exactly 32 bytes long, so
    419  // this method strips leading padding bytes, if added, and also appends
    420  // padding zeros, if needed.
    421  if (arr.length > 32) {
    422    arr = arr.slice(arr.length - 32);
    423  }
    424  let ret = new Uint8Array(32);
    425  ret.set(arr, ret.length - arr.length);
    426  return ret;
    427 }
    428 
    429 function verifySignature(key, data, derSig) {
    430  if (derSig.byteLength < 68) {
    431    return Promise.reject(
    432      "Invalid signature (length=" +
    433        derSig.byteLength +
    434        "): " +
    435        hexEncode(new Uint8Array(derSig))
    436    );
    437  }
    438 
    439  // Copy signature data into the current context.
    440  let derSigCopy = new ArrayBuffer(derSig.byteLength);
    441  new Uint8Array(derSigCopy).set(new Uint8Array(derSig));
    442 
    443  let sigAsn1 = org.pkijs.fromBER(derSigCopy);
    444 
    445  // pkijs.asn1 seems to erroneously set an error code when calling some
    446  // internal function. The test suite doesn't like dangling globals.
    447  delete window.error;
    448 
    449  let sigR = new Uint8Array(
    450    sigAsn1.result.value_block.value[0].value_block.value_hex
    451  );
    452  let sigS = new Uint8Array(
    453    sigAsn1.result.value_block.value[1].value_block.value_hex
    454  );
    455 
    456  // The resulting R and S values from the ASN.1 Sequence must be fit into 32
    457  // bytes. Sometimes they have leading zeros, sometimes they're too short, it
    458  // all depends on what lib generated the signature.
    459  let R = sanitizeSigArray(sigR);
    460  let S = sanitizeSigArray(sigS);
    461 
    462  console.log("Verifying these bytes: " + bytesToBase64UrlSafe(data));
    463 
    464  let sigData = new Uint8Array(R.length + S.length);
    465  sigData.set(R);
    466  sigData.set(S, R.length);
    467 
    468  let alg = { name: "ECDSA", hash: "SHA-256" };
    469  return crypto.subtle.verify(alg, key, sigData, data);
    470 }
    471 
    472 async function addCredential(authenticatorId, rpId) {
    473  let keyPair = await crypto.subtle.generateKey(
    474    {
    475      name: "ECDSA",
    476      namedCurve: "P-256",
    477    },
    478    true,
    479    ["sign"]
    480  );
    481 
    482  let credId = new Uint8Array(32);
    483  crypto.getRandomValues(credId);
    484  credId = bytesToBase64UrlSafe(credId);
    485 
    486  let privateKey = await crypto.subtle
    487    .exportKey("pkcs8", keyPair.privateKey)
    488    .then(privateKey => bytesToBase64UrlSafe(privateKey));
    489 
    490  await SpecialPowers.spawnChrome(
    491    [authenticatorId, credId, rpId, privateKey],
    492    (authenticatorId, credId, rpId, privateKey) => {
    493      let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
    494        Ci.nsIWebAuthnService
    495      );
    496 
    497      webauthnService.addCredential(
    498        authenticatorId,
    499        credId,
    500        true, // resident key
    501        rpId,
    502        privateKey,
    503        "VGVzdCBVc2Vy", // "Test User"
    504        0 // sign count
    505      );
    506    }
    507  );
    508 
    509  return credId;
    510 }
    511 
    512 async function removeCredential(authenticatorId, credId) {
    513  await SpecialPowers.spawnChrome(
    514    [authenticatorId, credId],
    515    (authenticatorId, credId) => {
    516      let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
    517        Ci.nsIWebAuthnService
    518      );
    519 
    520      webauthnService.removeCredential(authenticatorId, credId);
    521    }
    522  );
    523 }