diff options
Diffstat (limited to 'toolkit/modules/CertUtils.jsm')
-rw-r--r-- | toolkit/modules/CertUtils.jsm | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/toolkit/modules/CertUtils.jsm b/toolkit/modules/CertUtils.jsm new file mode 100644 index 000000000..e61ea9de7 --- /dev/null +++ b/toolkit/modules/CertUtils.jsm @@ -0,0 +1,222 @@ +/* -*- 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/. */ +this.EXPORTED_SYMBOLS = [ "BadCertHandler", "checkCert", "readCertPrefs", "validateCert" ]; + +const Ce = Components.Exception; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Components.utils.import("resource://gre/modules/Services.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. + */ +this.readCertPrefs = + 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. + */ +this.validateCert = + 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. + */ +this.checkCert = + 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; + } + + var cert = + aChannel.securityInfo.QueryInterface(Ci.nsISSLStatusProvider). + SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert; + + validateCert(cert, aCerts); + + if (aAllowNonBuiltInCerts === true) + return; + + var issuerCert = cert; + while (issuerCert.issuer && !issuerCert.issuer.equals(issuerCert)) + issuerCert = issuerCert.issuer; + + const certNotBuiltInErr = "Certificate issuer is not built-in."; + if (!issuerCert) + throw new Ce(certNotBuiltInErr, Cr.NS_ERROR_ABORT); + + var tokenNames = issuerCert.getAllTokenNames({}); + + if (!tokenNames || !tokenNames.some(isBuiltinToken)) + throw new Ce(certNotBuiltInErr, Cr.NS_ERROR_ABORT); +} + +function isBuiltinToken(tokenName) { + return tokenName == "Builtin Object Token"; +} + +/** + * 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. + */ +this.BadCertHandler = + function BadCertHandler(aAllowNonBuiltInCerts) { + this.allowNonBuiltInCerts = aAllowNonBuiltInCerts; +} +BadCertHandler.prototype = { + + // nsIChannelEventSink + asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) { + if (this.allowNonBuiltInCerts) { + callback.onRedirectVerifyCallback(Components.results.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(Components.results.NS_OK); + }, + + // nsIInterfaceRequestor + getInterface: function(iid) { + return this.QueryInterface(iid); + }, + + // nsISupports + QueryInterface: function(iid) { + if (!iid.equals(Ci.nsIChannelEventSink) && + !iid.equals(Ci.nsIInterfaceRequestor) && + !iid.equals(Ci.nsISupports)) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + } +}; |