toolkit/modules/CertUtils.jsm
author David Keeler <dkeeler@mozilla.com>
Mon, 16 Apr 2018 15:19:51 -0700
changeset 467668 a231670d9c66f178453520089f837f36390d0c55
parent 466662 5800890780bc9d72194621b24d8a26c00497f763
child 469711 8db81ce4d3a29fa0f9f69b904d2868e3b954bf39
permissions -rw-r--r--
bug 1454504 - use a more performant API to find a root certificate in CertUtils.checkCert r=kmag,mossop nsIX509Cert.issuer performs synchronous certificate verification and isn't even guaranteed to return a verified result. Luckily we can replace this with nsISSLStatus.succeededCertChain, which contains the already-verified certificate chain of the connection we're interested in. MozReview-Commit-ID: I8jPDVlUOvf

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var EXPORTED_SYMBOLS = ["CertUtils"];

const Ce = Components.Exception;

ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {});

/**
 * Reads a set of expected certificate attributes from preferences. The returned
 * array can be passed to validateCert or checkCert to validate that a
 * certificate matches the expected attributes. The preferences should look like
 * this:
 *   prefix.1.attribute1
 *   prefix.1.attribute2
 *   prefix.2.attribute1
 *   etc.
 * Each numeric branch contains a set of required attributes for a single
 * certificate. Having multiple numeric branches means that multiple
 * certificates would be accepted by validateCert.
 *
 * @param  aPrefBranch
 *         The prefix for all preferences, should end with a ".".
 * @return An array of JS objects with names / values corresponding to the
 *         expected certificate's attribute names / values.
 */
function readCertPrefs(aPrefBranch) {
  if (Services.prefs.getBranch(aPrefBranch).getChildList("").length == 0)
    return null;

  let certs = [];
  let counter = 1;
  while (true) {
    let prefBranchCert = Services.prefs.getBranch(aPrefBranch + counter + ".");
    let prefCertAttrs = prefBranchCert.getChildList("");
    if (prefCertAttrs.length == 0)
      break;

    let certAttrs = {};
    for (let prefCertAttr of prefCertAttrs)
      certAttrs[prefCertAttr] = prefBranchCert.getCharPref(prefCertAttr);

    certs.push(certAttrs);
    counter++;
  }

  return certs;
}

/**
 * Verifies that an nsIX509Cert matches the expected certificate attribute
 * values.
 *
 * @param  aCertificate
 *         The nsIX509Cert to compare to the expected attributes.
 * @param  aCerts
 *         An array of JS objects with names / values corresponding to the
 *         expected certificate's attribute names / values. If this is null or
 *         an empty array then no checks are performed.
 * @throws NS_ERROR_ILLEGAL_VALUE if a certificate attribute name from the
 *         aCerts param does not exist or the value for a certificate attribute
 *         from the aCerts param is different than the expected value or
 *         aCertificate wasn't specified and aCerts is not null or an empty
 *         array.
 */
function validateCert(aCertificate, aCerts) {
  // If there are no certificate requirements then just exit
  if (!aCerts || aCerts.length == 0)
    return;

  if (!aCertificate) {
    const missingCertErr = "A required certificate was not present.";
    Cu.reportError(missingCertErr);
    throw new Ce(missingCertErr, Cr.NS_ERROR_ILLEGAL_VALUE);
  }

  var errors = [];
  for (var i = 0; i < aCerts.length; ++i) {
    var error = false;
    var certAttrs = aCerts[i];
    for (var name in certAttrs) {
      if (!(name in aCertificate)) {
        error = true;
        errors.push("Expected attribute '" + name + "' not present in " +
                    "certificate.");
        break;
      }
      if (aCertificate[name] != certAttrs[name]) {
        error = true;
        errors.push("Expected certificate attribute '" + name + "' " +
                    "value incorrect, expected: '" + certAttrs[name] +
                    "', got: '" + aCertificate[name] + "'.");
        break;
      }
    }

    if (!error)
      break;
  }

  if (error) {
    errors.forEach(Cu.reportError.bind(Cu));
    const certCheckErr = "Certificate checks failed. See previous errors " +
                         "for details.";
    Cu.reportError(certCheckErr);
    throw new Ce(certCheckErr, Cr.NS_ERROR_ILLEGAL_VALUE);
  }
}

/**
 * Checks if the connection must be HTTPS and if so, only allows built-in
 * certificates and validates application specified certificate attribute
 * values.
 * See bug 340198 and bug 544442.
 *
 * @param  aChannel
 *         The nsIChannel that will have its certificate checked.
 * @param  aAllowNonBuiltInCerts (optional)
 *         When true certificates that aren't builtin are allowed. When false
 *         or not specified the certificate must be a builtin certificate.
 * @param  aCerts (optional)
 *         An array of JS objects with names / values corresponding to the
 *         channel's expected certificate's attribute names / values. If it
 *         isn't null or not specified the the scheme for the channel's
 *         originalURI must be https.
 * @throws NS_ERROR_UNEXPECTED if a certificate is expected and the URI scheme
 *         is not https.
 *         NS_ERROR_ILLEGAL_VALUE if a certificate attribute name from the
 *         aCerts param does not exist or the value for a certificate attribute
 *         from the aCerts  param is different than the expected value.
 *         NS_ERROR_ABORT if the certificate issuer is not built-in.
 */
function checkCert(aChannel, aAllowNonBuiltInCerts, aCerts) {
  if (!aChannel.originalURI.schemeIs("https")) {
    // Require https if there are certificate values to verify
    if (aCerts) {
      throw new Ce("SSL is required and URI scheme is not https.",
                   Cr.NS_ERROR_UNEXPECTED);
    }
    return;
  }

  let sslStatus = aChannel.securityInfo.QueryInterface(Ci.nsISSLStatusProvider)
                          .SSLStatus;
  let cert = sslStatus.serverCert;

  validateCert(cert, aCerts);

  if (aAllowNonBuiltInCerts === true) {
    return;
  }

  let certEnumerator = sslStatus.succeededCertChain.getEnumerator();
  let issuerCert = null;
  for (issuerCert of XPCOMUtils.IterSimpleEnumerator(certEnumerator,
                                                     Ci.nsIX509Cert));

  const certNotBuiltInErr = "Certificate issuer is not built-in.";
  if (!issuerCert) {
    throw new Ce(certNotBuiltInErr, Cr.NS_ERROR_ABORT);
  }

  if (!issuerCert.isBuiltInRoot) {
    throw new Ce(certNotBuiltInErr, Cr.NS_ERROR_ABORT);
  }
}

/**
 * This class implements nsIBadCertListener.  Its job is to prevent "bad cert"
 * security dialogs from being shown to the user.  It is better to simply fail
 * if the certificate is bad. See bug 304286.
 *
 * @param  aAllowNonBuiltInCerts (optional)
 *         When true certificates that aren't builtin are allowed. When false
 *         or not specified the certificate must be a builtin certificate.
 */
function BadCertHandler(aAllowNonBuiltInCerts) {
  this.allowNonBuiltInCerts = aAllowNonBuiltInCerts;
}
BadCertHandler.prototype = {

  // nsIChannelEventSink
  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
    if (this.allowNonBuiltInCerts) {
      callback.onRedirectVerifyCallback(Cr.NS_OK);
      return;
    }

    // make sure the certificate of the old channel checks out before we follow
    // a redirect from it.  See bug 340198.
    // Don't call checkCert for internal redirects. See bug 569648.
    if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL))
      checkCert(oldChannel);

    callback.onRedirectVerifyCallback(Cr.NS_OK);
  },

  // nsIInterfaceRequestor
  getInterface(iid) {
    return this.QueryInterface(iid);
  },

  // nsISupports
  QueryInterface(iid) {
    if (!iid.equals(Ci.nsIChannelEventSink) &&
        !iid.equals(Ci.nsIInterfaceRequestor) &&
        !iid.equals(Ci.nsISupports))
      throw Cr.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

var CertUtils = {
  BadCertHandler,
  checkCert,
  readCertPrefs,
  validateCert,
};