tor-browser

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

QWACs.sys.mjs (16782B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 XPCOMUtils.defineLazyServiceGetters(lazy, {
     10  CertDB: ["@mozilla.org/security/x509certdb;1", Ci.nsIX509CertDB],
     11 });
     12 
     13 function arrayToString(a) {
     14  let s = "";
     15  for (let b of a) {
     16    s += String.fromCharCode(b);
     17  }
     18  return s;
     19 }
     20 
     21 function stringToArrayBuffer(str) {
     22  let bytes = new Uint8Array(str.length);
     23  for (let i = 0; i < str.length; i++) {
     24    bytes[i] = str.charCodeAt(i);
     25  }
     26  return bytes;
     27 }
     28 
     29 export var QWACs = {
     30  fromBase64URLEncoding(base64URLEncoded) {
     31    return atob(base64URLEncoded.replaceAll("-", "+").replaceAll("_", "/"));
     32  },
     33 
     34  toBase64URLEncoding(str) {
     35    return btoa(str)
     36      .replaceAll("+", "-")
     37      .replaceAll("/", "_")
     38      .replaceAll("=", "");
     39  },
     40 
     41  // Validates and returns the decoded parameters of a TLS certificate binding
     42  // header as specified by ETSI TS 119 411-5 V2.1.1 Annex B, ETSI TS 119 182-1
     43  // V1.2.1, and RFC 7515.
     44  // If the header contains invalid values or otherwise fails to validate,
     45  // returns false.
     46  // This should probably not be called directly outside of tests -
     47  // verifyTLSCertificateBinding is the main entrypoint of this implementation.
     48  validateTLSCertificateBindingHeader(header) {
     49    // ETSI TS 119 411-5 V2.1.1 Annex B specifies the TLS Certificate Binding
     50    // Profile and states that "Only header parameters specified in this
     51    // profile may be present in the header of the generated JAdES signature."
     52    const allowedHeaderKeys = new Set([
     53      "alg",
     54      "kid",
     55      "cty",
     56      "x5t#S256",
     57      "x5c",
     58      "iat",
     59      "exp",
     60      "sigD",
     61    ]);
     62    let headerKeys = new Set(Object.keys(header));
     63    if (!headerKeys.isSubsetOf(allowedHeaderKeys)) {
     64      console.error("header contains invalid parameter");
     65      return false;
     66    }
     67 
     68    // ETSI TS 119 182-1 V1.2.1 Section 5.1.2 specifies that "alg" shall be as
     69    // described in RFC 7515 Section 4.1.1, which references RFC 7518. None of
     70    // these specifications require support for a particular signature
     71    // algorithm, but RFC 7518 recommends supporting "RS256" (RSASSA-PKCS1-v1_5
     72    // with SHA-256) and "ES256" (ECDSA with P-256 and SHA-256), so those are
     73    // supported. Additionally, "PS256" (RSASSA-PSS with SHA-256) is supported
     74    // for compatibility and as better alternative to RSASSA-PKCS1-v1_5.
     75    // The specification says "alg" can't conflict with signing certificate
     76    // key. This is enforced when the signature is verified.
     77    if (!("alg" in header)) {
     78      console.error("header missing 'alg' field");
     79      return false;
     80    }
     81    let algorithm;
     82    switch (header.alg) {
     83      case "RS256":
     84        algorithm = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
     85        break;
     86      case "PS256":
     87        algorithm = { name: "RSA-PSS", saltLength: 32, hash: "SHA-256" };
     88        break;
     89      case "ES256":
     90        algorithm = { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" };
     91        break;
     92      default:
     93        console.error("unsupported alg:", header.alg);
     94        return false;
     95    }
     96 
     97    // RFC 7515 defines "kid" as an optional hint. It is unnecessary.
     98 
     99    // ETSI TS 119 411-5 V2.1.1 Annex B says that "cty" will have the value
    100    // "TLS-Certificate-Binding-v1". However, ETSI TS 119 182-1 V1.2.1 Section
    101    // 5.1.3 states that "The cty header parameter should not be present if the
    102    // sigD header parameter, specified in clause 5.2.8 of the present
    103    // document, is present within the JAdES signature."
    104    // ETSI TS 119 411-5 V2.1.1 Annex B also requires sigD to be present, so
    105    // either this is a mistake, or ETSI TS 119 411-5 Annex B deliberately
    106    // disregards ETSI TS 119 182-1.
    107    // Chrome's implementation requires "cty", so for compatibility, this
    108    // matches that behavior.
    109    if (!("cty" in header)) {
    110      console.error("header missing field 'cty'");
    111      return false;
    112    }
    113    if (header.cty != "TLS-Certificate-Binding-v1") {
    114      console.error("invalid value for cty:", header.cty);
    115      return false;
    116    }
    117 
    118    // RFC 7515 defines "x5t#S256" as "base64url-encoded SHA-256 thumbprint
    119    // (a.k.a. digest) of the DER encoding of the X.509 certificate [RFC5280]
    120    // corresponding to the key used to digitally sign the JWS".
    121    // It is optional. If present, it must match the digest of the 0th element
    122    // of "x5c" (this is checked after processing x5c, below).
    123    let x5tS256;
    124    if ("x5t#S256" in header) {
    125      x5tS256 = header["x5t#S256"];
    126    }
    127 
    128    // RFC 7515:
    129    //   The "x5c" (X.509 certificate chain) Header Parameter contains the
    130    //   X.509 public key certificate or certificate chain [RFC5280]
    131    //   corresponding to the key used to digitally sign the JWS.  The
    132    //   certificate or certificate chain is represented as a JSON array of
    133    //   certificate value strings.  Each string in the array is a
    134    //   base64-encoded (Section 4 of [RFC4648] -- not base64url-encoded) DER
    135    //   [ITU.X690.2008] PKIX certificate value.
    136    if (!("x5c" in header)) {
    137      console.error("header missing field 'x5c'");
    138      return false;
    139    }
    140    let certificates = [];
    141    for (let base64 of header.x5c) {
    142      try {
    143        certificates.push(lazy.CertDB.constructX509FromBase64(base64));
    144      } catch (e) {
    145        console.error("couldn't decode certificate");
    146        return false;
    147      }
    148    }
    149    // ETSI TS 119 411-5 V2.1.1 Annex B states that "x5c" consists of the full
    150    // certificate chain, including the trust anchor. However, only the signing
    151    // certificate and any intermediates relevant to path-building are strictly
    152    // necessary.
    153    if (certificates.length < 1) {
    154      console.error("header must specify certificate chain");
    155      return false;
    156    }
    157    if (x5tS256) {
    158      // signingCertificateHashHex will be of the form "AA:BB:..."
    159      let signingCertificateHashHex = certificates[0].sha256Fingerprint;
    160      let signingCertificateHashBytes = signingCertificateHashHex
    161        .split(":")
    162        .map(hexStr => parseInt(hexStr, 16));
    163      if (
    164        x5tS256 !=
    165        QWACs.toBase64URLEncoding(arrayToString(signingCertificateHashBytes))
    166      ) {
    167        console.error("x5t#S256 does not match signing certificate");
    168        return false;
    169      }
    170    }
    171 
    172    // ETSI TS 119 411-5 V2.1.1 Annex B's definition of "iat" reads "This field
    173    // contains the claimed signing time. The value shall be encoded as
    174    // specified in IETF RFC 7519 [9]." However, in RFC 7519, "iat" is a claim
    175    // that can be made by a JWT, not used as a header field in a JWS. In any
    176    // case, ETSI TS 119 411-5 offers no guidance on how this header affects
    177    // validation. Consequently, as it is optional, it is ignored.
    178 
    179    // Similarly, the definition of "exp" reads "This field contains the expiry
    180    // date of the binding. The maximum effective expiry time is whichever is
    181    // soonest of this field, the longest-lived TLS certificate identified in
    182    // the sigD member payload (below), or the notAfter time of the signing
    183    // certificate. The value shall be encoded as specified in IETF RFC 7519
    184    // [9]," again referencing a JWT claim and not a JWS header.
    185    // We interpret this to be an optional mechanism to expire bindings earlier
    186    // than the earliest "notAfter" value amongst the certificates specified in
    187    // "x5c".
    188    // RFC 7519 says this will be a NumericDate, which is a "JSON numeric value
    189    // representing the number of seconds from 1970-01-01T00:00:00Z UTC".
    190    if ("exp" in header) {
    191      let expirationSeconds = parseInt(header.exp);
    192      if (isNaN(expirationSeconds)) {
    193        console.error("invalid expiration time");
    194        return false;
    195      }
    196      let expiration = new Date(expirationSeconds * 1000);
    197      if (expiration < new Date()) {
    198        console.error("header has expired");
    199        return false;
    200      }
    201    }
    202 
    203    // "sigD" lists the TLS server certificates being bound, and must be
    204    // present.
    205    if (!("sigD" in header)) {
    206      console.error("header missing field 'sigD'");
    207      return false;
    208    }
    209    let sigD = header.sigD;
    210    const allowedSigDKeys = new Set(["mId", "pars", "hashM", "hashV"]);
    211    let sigDKeys = new Set(Object.keys(sigD));
    212    if (!sigDKeys.isSubsetOf(allowedSigDKeys)) {
    213      console.error("sigD contains invalid parameter");
    214      return false;
    215    }
    216    // ETSI TS 119 411-5 V2.1.1 Annex B requires that "sigD.mId" be
    217    // "http://uri.etsi.org/19182/ObjectIdByURIHash".
    218    if (!("mId" in sigD)) {
    219      console.error("header missing field 'sigD.mId'");
    220      return false;
    221    }
    222    if (sigD.mId != "http://uri.etsi.org/19182/ObjectIdByURIHash") {
    223      console.error("invalid value for sigD.mId:", sigD.mId);
    224      return false;
    225    }
    226 
    227    // ETSI TS 119 411-5 V2.1.1 Annex B defines "sigD.pars" as "A comma-separated
    228    // list of TLS certificate file names." The only thing to validate here is
    229    // that pars has as many elements as "hashV", later.
    230    if (!("pars" in sigD)) {
    231      console.error("header missing field 'sigD.pars'");
    232      return false;
    233    }
    234    let pars = sigD.pars;
    235 
    236    // ETSI TS 119 411-5 V2.1.1 Annex B defines "sigD.hashM" as 'The string
    237    // identifying one of the approved hashing algorithms identified by ETSI TS
    238    // 119 312 [8] for JAdES. This hashing algorithm is used to calculate the
    239    // hashes described in the "hashV" member below.' It further requires that
    240    // "SHA-256, SHA-384, and SHA-512 are supported, and it is assumed that
    241    // strings identifying them are S256, S384, and S512 respectively".
    242    if (!("hashM" in sigD)) {
    243      console.error("header missing field 'sigD.hashM'");
    244      return false;
    245    }
    246    let hashAlg;
    247    switch (sigD.hashM) {
    248      case "S256":
    249        hashAlg = "SHA-256";
    250        break;
    251      case "S384":
    252        hashAlg = "SHA-384";
    253        break;
    254      case "S512":
    255        hashAlg = "SHA-512";
    256        break;
    257      default:
    258        console.error("unsupported hashM:", sigD.hashM);
    259        return false;
    260    }
    261 
    262    // ETSI TS 119 411-5 V2.1.1 Annex B defines "sigD.hashV" as 'A
    263    // comma-separated list of TLS certificate file hashes. Each hash is
    264    // produced by taking the corresponding X.509 certificate, computing its
    265    // base64url encoding, and calculating its hash using the algorithm
    266    // identified in the "hashM" member above.'
    267    // This array must be the same length as the "sigD.pars" array.
    268    if (!("hashV" in sigD)) {
    269      console.error("header missing field 'sigD.hashV'");
    270      return false;
    271    }
    272    let hashes = sigD.hashV;
    273    if (hashes.length != pars.length) {
    274      console.error("header sigD.pars/hashV mismatch");
    275      return false;
    276    }
    277    for (let hash of hashes) {
    278      if (typeof hash != "string") {
    279        console.error("invalid hash:", hash);
    280        return false;
    281      }
    282    }
    283 
    284    return { algorithm, certificates, hashAlg, hashes };
    285  },
    286 
    287  // Given a TLS certificate binding, a TLS server certificate, and a hostname,
    288  // this function validates the binding, extracts its parameters, verifies
    289  // that the binding signing certificate is a 2-QWAC certificate valid for the
    290  // given hostname that chains to a QWAC trust anchor, verifies the signature
    291  // on the binding, and finally verifies that the binding covers the server
    292  // certificate.
    293  // Returns the QWAC upon success, and null otherwise.
    294  async verifyTLSCertificateBinding(
    295    tlsCertificateBinding,
    296    serverCertificate,
    297    hostname
    298  ) {
    299    // tlsCertificateBinding is a JAdES signature, which is a JWS. Because ETSI
    300    // TS 119 411-5 V2.1.1 Annex B requires sigD be present, and because ETSI
    301    // TS 119 182-1 V1.2.1 states "The sigD header parameter shall not appear
    302    // in JAdES signatures whose JWS Payload is attached",
    303    // tlsCertificateBinding must have a detached payload.
    304    // In other words, tlsCertificateBinding is a consists of:
    305    // "<base64url-encoded header>..<base64url-encoded signature>"
    306    let parts = tlsCertificateBinding.split(".");
    307    if (parts.length != 3) {
    308      console.error("invalid TLS certificate binding");
    309      return null;
    310    }
    311    if (parts[1] != "") {
    312      console.error("TLS certificate binding must have empty payload");
    313      return null;
    314    }
    315    let header;
    316    try {
    317      header = JSON.parse(QWACs.fromBase64URLEncoding(parts[0]));
    318    } catch (e) {
    319      console.error("header is not base64(JSON)");
    320      return null;
    321    }
    322    let params = QWACs.validateTLSCertificateBindingHeader(header);
    323    if (!params) {
    324      return null;
    325    }
    326 
    327    // The 0th certificate signed the binding. It must be a 2-QWAC that is
    328    // valid for the given hostname (ETSI TS 119 411-5 V2.1.1 Section 6.2.2
    329    // Step 4).
    330    let signingCertificate = params.certificates[0];
    331    let chain = params.certificates.slice(1);
    332    if (
    333      !(await lazy.CertDB.asyncVerifyQWAC(
    334        Ci.nsIX509CertDB.TwoQWAC,
    335        signingCertificate,
    336        hostname,
    337        chain
    338      ))
    339    ) {
    340      console.error("signing certificate not 2-QWAC");
    341      return null;
    342    }
    343 
    344    let spki = signingCertificate.subjectPublicKeyInfo;
    345    let signingKey;
    346    try {
    347      signingKey = await crypto.subtle.importKey(
    348        "spki",
    349        new Uint8Array(spki),
    350        params.algorithm,
    351        true,
    352        ["verify"]
    353      );
    354    } catch (e) {
    355      console.error("invalid signing key (algorithm mismatch?)");
    356      return null;
    357    }
    358 
    359    let signature;
    360    try {
    361      signature = QWACs.fromBase64URLEncoding(parts[2]);
    362    } catch (e) {
    363      console.error("signature is not base64");
    364      return null;
    365    }
    366 
    367    // Validate the signature (Step 5).
    368    let signatureValid;
    369    try {
    370      signatureValid = await crypto.subtle.verify(
    371        params.algorithm,
    372        signingKey,
    373        stringToArrayBuffer(signature),
    374        stringToArrayBuffer(parts[0] + ".")
    375      );
    376    } catch (e) {
    377      console.error("failed to verify signature");
    378      return null;
    379    }
    380    if (!signatureValid) {
    381      console.error("invalid signature");
    382      return null;
    383    }
    384 
    385    // The binding must list the server certificate's hash (Step 6).
    386    let serverCertificateHash = await crypto.subtle.digest(
    387      params.hashAlg,
    388      stringToArrayBuffer(
    389        QWACs.toBase64URLEncoding(arrayToString(serverCertificate.getRawDER()))
    390      )
    391    );
    392    if (
    393      !params.hashes.includes(
    394        QWACs.toBase64URLEncoding(
    395          arrayToString(new Uint8Array(serverCertificateHash))
    396        )
    397      )
    398    ) {
    399      console.error("TLS binding does not cover server certificate");
    400      return null;
    401    }
    402    return signingCertificate;
    403  },
    404 
    405  /**
    406   * Asynchronously determines the QWAC status of a document.
    407   *
    408   * @param secInfo {nsITransportSecurityInfo}
    409   *   The security information for the connection of the document.
    410   * @param uri {nsIURI}
    411   *   The URI of the document.
    412   * @param browsingContext {BrowsingContext}
    413   *   The browsing context of the load of the document.
    414   * @returns {Promise}
    415   *   A promise that will resolve to an nsIX509Cert representing the QWAC in
    416   *   use, if any, and null otherwise.
    417   */
    418  async determineQWACStatus(secInfo, uri, browsingContext) {
    419    if (!secInfo || !secInfo.serverCert) {
    420      return null;
    421    }
    422 
    423    // For some URIs, getting `host` will throw. ETSI TS 119 411-5 V2.1.1 only
    424    // mentions domain names, so the assumed intention in such cases is to
    425    // determine that the document is not using a QWAC.
    426    let hostname;
    427    try {
    428      hostname = uri.host;
    429    } catch {
    430      return null;
    431    }
    432 
    433    let windowGlobal = browsingContext.currentWindowGlobal;
    434    let actor = windowGlobal.getActor("TLSCertificateBinding");
    435    let tlsCertificateBinding = null;
    436    try {
    437      tlsCertificateBinding = await actor.sendQuery(
    438        "TLSCertificateBinding::Get"
    439      );
    440    } catch {
    441      // If the page is closed before the query resolves, the actor will be
    442      // destroyed, which causes a JS exception. We can safely ignore it,
    443      // because the page is going away.
    444      return null;
    445    }
    446    if (tlsCertificateBinding) {
    447      let twoQwac = await QWACs.verifyTLSCertificateBinding(
    448        tlsCertificateBinding,
    449        secInfo.serverCert,
    450        hostname
    451      );
    452      if (twoQwac) {
    453        return twoQwac;
    454      }
    455    }
    456 
    457    let is1qwac = await lazy.CertDB.asyncVerifyQWAC(
    458      Ci.nsIX509CertDB.OneQWAC,
    459      secInfo.serverCert,
    460      hostname,
    461      secInfo.handshakeCertificates.concat(secInfo.succeededCertChain)
    462    );
    463    if (is1qwac) {
    464      return secInfo.serverCert;
    465    }
    466 
    467    return null;
    468  },
    469 };