bug 1228175 - fix IsCertBuiltInRoot r?Cykesiopka r?mgoodwin draft
authorDavid Keeler <dkeeler@mozilla.com>
Fri, 04 Mar 2016 17:06:33 -0800
changeset 339997 9bce48571e58ecd43272623e852db3b83c4f1ed4
parent 339874 f0c0480732d36153e8839c7f17394d45f679f87d
child 516110 c3985599a6b1c3bbc83c600bb3b1f468382457e2
push id12868
push userdkeeler@mozilla.com
push dateMon, 14 Mar 2016 17:48:51 +0000
reviewersCykesiopka, mgoodwin
bugs1228175
milestone48.0a1
bug 1228175 - fix IsCertBuiltInRoot r?Cykesiopka r?mgoodwin When a built-in root certificate has its trust changed from the default value, the platform has to essentially create a copy of it in the read/write certificate database with the new trust settings. At that point, the desired behavior is that the platform still considers that certificate a built-in root. Before this patch, this would indeed happen for the duration of that run of the platform, but as soon as it restarted, the certificate in question would only appear to be from the read/write database, and thus was not considered a built-in root. This patch changes the test of built-in-ness to explicitly search the built-in certificate slot for the certificate in question. If found, it is considered a built-in root. MozReview-Commit-ID: HCtZpPQVEGZ
config/external/nss/nss.symbols
security/certverifier/CertVerifier.cpp
security/certverifier/CertVerifier.h
security/certverifier/NSSCertDBTrustDomain.cpp
security/manager/ssl/SSLServerCertVerification.cpp
security/manager/ssl/nsIX509Cert.idl
security/manager/ssl/nsNSSCertificate.cpp
security/manager/ssl/nsNSSCertificateFakeTransport.cpp
security/manager/ssl/nsSiteSecurityService.cpp
security/manager/ssl/tests/compiled/TestCertDB.cpp
security/manager/ssl/tests/compiled/TestIsCertBuiltInRoot.cpp
security/manager/ssl/tests/compiled/moz.build
security/manager/ssl/tests/unit/test_cert_isBuiltInRoot.js
security/manager/ssl/tests/unit/xpcshell.ini
testing/cppunittest.ini
--- a/config/external/nss/nss.symbols
+++ b/config/external/nss/nss.symbols
@@ -325,16 +325,17 @@ PK11_DigestBegin
 PK11_DigestFinal
 PK11_DigestOp
 PK11_DoesMechanism
 PK11_Encrypt
 PK11_ExportDERPrivateKeyInfo
 PK11_ExportEncryptedPrivKeyInfo
 PK11_ExtractKeyValue
 PK11_FindCertFromNickname
+PK11_FindCertInSlot
 PK11_FindCertsFromEmailAddress
 PK11_FindCertsFromNickname
 PK11_FindKeyByAnyCert
 PK11_FindKeyByDERCert
 PK11_FindKeyByKeyID
 PK11_FindSlotByName
 PK11_FindSlotsByNames
 PK11_FreeSlot
@@ -543,16 +544,17 @@ SECMOD_AddNewModuleEx
 SECMOD_CancelWait
 SECMOD_CanDeleteInternalModule
 SECMOD_CloseUserDB
 SECMOD_CreateModule
 SECMOD_DeleteInternalModule
 SECMOD_DeleteModule
 SECMOD_DestroyModule
 SECMOD_FindModule
+SECMOD_FindSlot
 SECMOD_GetDeadModuleList
 SECMOD_GetDefaultModuleList
 SECMOD_GetDefaultModuleListLock
 SECMOD_GetInternalModule
 SECMOD_GetModuleSpecList
 SECMOD_GetReadLock
 SECMOD_HasRemovableSlots
 SECMOD_InternaltoPubMechFlags
--- a/security/certverifier/CertVerifier.cpp
+++ b/security/certverifier/CertVerifier.cpp
@@ -7,21 +7,24 @@
 #include "CertVerifier.h"
 
 #include <stdint.h>
 
 #include "ExtendedValidation.h"
 #include "NSSCertDBTrustDomain.h"
 #include "NSSErrorsService.h"
 #include "cert.h"
+#include "nsNSSComponent.h"
+#include "nsServiceManagerUtils.h"
 #include "pk11pub.h"
 #include "pkix/pkix.h"
 #include "pkix/pkixnss.h"
 #include "prerror.h"
 #include "secerr.h"
+#include "secmod.h"
 #include "sslerr.h"
 
 using namespace mozilla::pkix;
 using namespace mozilla::psm;
 
 mozilla::LazyLogModule gCertVerifierLog("certverifier");
 
 namespace mozilla { namespace psm {
@@ -63,45 +66,53 @@ IsCertChainRootBuiltInRoot(CERTCertList*
   CERTCertListNode* rootNode = CERT_LIST_TAIL(chain);
   if (!rootNode) {
     return Result::FATAL_ERROR_LIBRARY_FAILURE;
   }
   CERTCertificate* root = rootNode->cert;
   if (!root) {
     return Result::FATAL_ERROR_LIBRARY_FAILURE;
   }
-  SECStatus srv = IsCertBuiltInRoot(root, result);
-  if (srv != SECSuccess) {
-    return MapPRErrorCodeToResult(PR_GetError());
-  }
-  return Success;
+  return IsCertBuiltInRoot(root, result);
 }
 
-SECStatus
+Result
 IsCertBuiltInRoot(CERTCertificate* cert, bool& result)
 {
   result = false;
-  UniquePK11SlotList slots(PK11_GetAllSlotsForCert(cert, nullptr));
-  if (!slots) {
-    if (PORT_GetError() == SEC_ERROR_NO_TOKEN) {
-      // no list
-      return SECSuccess;
-    }
-    return SECFailure;
+  nsCOMPtr<nsINSSComponent> component(do_GetService(PSM_COMPONENT_CONTRACTID));
+  if (!component) {
+    return Result::FATAL_ERROR_LIBRARY_FAILURE;
+  }
+  nsAutoString modName;
+  nsresult rv = component->GetPIPNSSBundleString("RootCertModuleName", modName);
+  if (NS_FAILED(rv)) {
+    return Result::FATAL_ERROR_LIBRARY_FAILURE;
+  }
+  NS_ConvertUTF16toUTF8 modNameUTF8(modName);
+  UniqueSECMODModule builtinRootsModule(SECMOD_FindModule(modNameUTF8.get()));
+  // If the built-in roots module isn't loaded, nothing is a built-in root.
+  if (!builtinRootsModule) {
+    return Success;
   }
-  for (PK11SlotListElement* le = slots->head; le; le = le->next) {
-    char* token = PK11_GetTokenName(le->slot);
-    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
-           ("BuiltInRoot? subject=%s token=%s",cert->subjectName, token));
-    if (strcmp("Builtin Object Token", token) == 0) {
-      result = true;
-      return SECSuccess;
-    }
+  UniquePK11SlotInfo builtinSlot(SECMOD_FindSlot(builtinRootsModule.get(),
+                                                 "Builtin Object Token"));
+  // This could happen if the user loaded a module that is acting like the
+  // built-in roots module but doesn't actually have a slot called "Builtin
+  // Object Token". In that case, again nothing is a built-in root.
+  if (!builtinSlot) {
+    return Success;
   }
-  return SECSuccess;
+  // Attempt to find a copy of the given certificate in the "Builtin Object
+  // Token" slot of the built-in root module. If we get a valid handle, this
+  // certificate exists in the root module, so we consider it a built-in root.
+  CK_OBJECT_HANDLE handle = PK11_FindCertInSlot(builtinSlot.get(), cert,
+                                                nullptr);
+  result = (handle != CK_INVALID_HANDLE);
+  return Success;
 }
 
 static Result
 BuildCertChainForOneKeyUsage(NSSCertDBTrustDomain& trustDomain, Input certDER,
                              Time time, KeyUsage ku1, KeyUsage ku2,
                              KeyUsage ku3, KeyPurposeId eku,
                              const CertPolicyId& requiredPolicy,
                              const Input* stapledOCSPResponse,
--- a/security/certverifier/CertVerifier.h
+++ b/security/certverifier/CertVerifier.h
@@ -139,16 +139,16 @@ private:
   // Returns true if the configured SHA1 mode is more restrictive than the given
   // mode. SHA1Mode::Forbidden is more restrictive than any other mode except
   // Forbidden. Next is Before2016, then ImportedRoot, then Allowed.
   // (A mode is never more restrictive than itself.)
   bool SHA1ModeMoreRestrictiveThanGivenMode(SHA1Mode mode);
 };
 
 void InitCertVerifierLog();
-SECStatus IsCertBuiltInRoot(CERTCertificate* cert, bool& result);
+mozilla::pkix::Result IsCertBuiltInRoot(CERTCertificate* cert, bool& result);
 mozilla::pkix::Result CertListContainsExpectedKeys(
   const CERTCertList* certList, const char* hostname, mozilla::pkix::Time time,
   CertVerifier::PinningMode pinningMode);
 
 } } // namespace mozilla::psm
 
 #endif // mozilla_psm__CertVerifier_h
--- a/security/certverifier/NSSCertDBTrustDomain.cpp
+++ b/security/certverifier/NSSCertDBTrustDomain.cpp
@@ -775,19 +775,19 @@ NSSCertDBTrustDomain::IsChainValid(const
                                  WhitelistedCNNICHashBinarySearchComparator(
                                    certHash, certHashLen),
                                  &unused)) {
       return Result::ERROR_REVOKED_CERTIFICATE;
     }
   }
 
   bool isBuiltInRoot = false;
-  srv = IsCertBuiltInRoot(root, isBuiltInRoot);
-  if (srv != SECSuccess) {
-    return MapPRErrorCodeToResult(PR_GetError());
+  Result rv = IsCertBuiltInRoot(root, isBuiltInRoot);
+  if (rv != Success) {
+    return rv;
   }
   bool skipPinningChecksBecauseOfMITMMode =
     (!isBuiltInRoot && mPinningMode == CertVerifier::pinningAllowUserCAMITM);
   // If mHostname isn't set, we're not verifying in the context of a TLS
   // handshake, so don't verify HPKP in those cases.
   if (mHostname && (mPinningMode != CertVerifier::pinningDisabled) &&
       !skipPinningChecksBecauseOfMITMMode) {
     bool enforceTestMode =
--- a/security/manager/ssl/SSLServerCertVerification.cpp
+++ b/security/manager/ssl/SSLServerCertVerification.cpp
@@ -896,26 +896,26 @@ GatherBaselineRequirementsTelemetry(cons
   // This only applies to certificates issued by authorities in our root
   // program.
   CERTCertificate* rootCert = rootNode->cert;
   PR_ASSERT(rootCert);
   if (!rootCert) {
     return;
   }
   bool isBuiltIn = false;
-  SECStatus rv = IsCertBuiltInRoot(rootCert, isBuiltIn);
-  if (rv != SECSuccess || !isBuiltIn) {
+  Result result = IsCertBuiltInRoot(rootCert, isBuiltIn);
+  if (result != Success || !isBuiltIn) {
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
            ("BR telemetry: root certificate for '%s' is not a built-in root "
             "(or IsCertBuiltInRoot failed)\n", commonName.get()));
     return;
   }
   SECItem altNameExtension;
-  rv = CERT_FindCertExtension(cert, SEC_OID_X509_SUBJECT_ALT_NAME,
-                              &altNameExtension);
+  SECStatus rv = CERT_FindCertExtension(cert, SEC_OID_X509_SUBJECT_ALT_NAME,
+                                        &altNameExtension);
   if (rv != SECSuccess) {
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
            ("BR telemetry: no subject alt names extension for '%s'\n",
             commonName.get()));
     // 1 means there is no subject alt names extension
     Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 1);
     AccumulateSubjectCommonNameTelemetry(commonName.get(), false);
     return;
@@ -1059,18 +1059,18 @@ GatherEKUTelemetry(const ScopedCERTCertL
 
   // Only log telemetry if the root CA is built-in
   CERTCertificate* rootCert = rootNode->cert;
   PR_ASSERT(rootCert);
   if (!rootCert) {
     return;
   }
   bool isBuiltIn = false;
-  SECStatus rv = IsCertBuiltInRoot(rootCert, isBuiltIn);
-  if (rv != SECSuccess || !isBuiltIn) {
+  Result rv = IsCertBuiltInRoot(rootCert, isBuiltIn);
+  if (rv != Success || !isBuiltIn) {
     return;
   }
 
   // Find the EKU extension, if present
   bool foundEKU = false;
   SECOidTag oidTag;
   CERTCertExtension* ekuExtension = nullptr;
   for (size_t i = 0; endEntityCert->extensions && endEntityCert->extensions[i];
--- a/security/manager/ssl/nsIX509Cert.idl
+++ b/security/manager/ssl/nsIX509Cert.idl
@@ -34,16 +34,21 @@ interface nsIX509Cert : nsISupports {
   readonly attribute AString nickname;
 
   /**
    *  The primary email address of the certificate, if present.
    */
   readonly attribute AString emailAddress;
 
   /**
+   * Did this certificate ship with the platform as a built-in root?
+   */
+  readonly attribute bool isBuiltInRoot;
+
+  /**
    *  Obtain a list of all email addresses
    *  contained in the certificate.
    *
    *  @param length The number of strings in the returned array.
    *  @return An array of email addresses.
    */
   void getEmailAddresses(out unsigned long length,
                          [retval, array, size_is(length)] out wstring addresses);
--- a/security/manager/ssl/nsNSSCertificate.cpp
+++ b/security/manager/ssl/nsNSSCertificate.cpp
@@ -215,16 +215,32 @@ nsNSSCertificate::GetIsSelfSigned(bool* 
   nsNSSShutDownPreventionLock locker;
   if (isAlreadyShutDown())
     return NS_ERROR_NOT_AVAILABLE;
 
   *aIsSelfSigned = mCert->isRoot;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsNSSCertificate::GetIsBuiltInRoot(bool* aIsBuiltInRoot)
+{
+  NS_ENSURE_ARG(aIsBuiltInRoot);
+
+  nsNSSShutDownPreventionLock locker;
+  if (isAlreadyShutDown()) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+  Result rv = IsCertBuiltInRoot(mCert, *aIsBuiltInRoot);
+  if (rv != Success) {
+    return NS_ERROR_FAILURE;
+  }
+  return NS_OK;
+}
+
 nsresult
 nsNSSCertificate::MarkForPermDeletion()
 {
   nsNSSShutDownPreventionLock locker;
   if (isAlreadyShutDown())
     return NS_ERROR_NOT_AVAILABLE;
 
   // make sure user is logged in to the token
--- a/security/manager/ssl/nsNSSCertificateFakeTransport.cpp
+++ b/security/manager/ssl/nsNSSCertificateFakeTransport.cpp
@@ -344,16 +344,23 @@ nsNSSCertificateFakeTransport::GetCertTy
 NS_IMETHODIMP
 nsNSSCertificateFakeTransport::GetIsSelfSigned(bool*)
 {
   NS_NOTREACHED("Unimplemented on content process");
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
+nsNSSCertificateFakeTransport::GetIsBuiltInRoot(bool* aIsBuiltInRoot)
+{
+  NS_NOTREACHED("Unimplemented on content process");
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
 nsNSSCertificateFakeTransport::RequestUsagesArrayAsync(
   nsICertVerificationListener*)
 {
   NS_NOTREACHED("Unimplemented on content process");
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
--- a/security/manager/ssl/nsSiteSecurityService.cpp
+++ b/security/manager/ssl/nsSiteSecurityService.cpp
@@ -712,18 +712,18 @@ nsSiteSecurityService::ProcessPKPHeader(
     return NS_ERROR_FAILURE;
   }
 
   CERTCertListNode* rootNode = CERT_LIST_TAIL(certList);
   if (CERT_LIST_END(rootNode, certList)) {
     return NS_ERROR_FAILURE;
   }
   bool isBuiltIn = false;
-  SECStatus srv = IsCertBuiltInRoot(rootNode->cert, isBuiltIn);
-  if (srv != SECSuccess) {
+  mozilla::pkix::Result result = IsCertBuiltInRoot(rootNode->cert, isBuiltIn);
+  if (result != mozilla::pkix::Success) {
     return NS_ERROR_FAILURE;
   }
 
   if (!isBuiltIn && !mProcessPKPHeadersFromNonBuiltInRoots) {
     if (aFailureResult) {
       *aFailureResult = nsISiteSecurityService::ERROR_ROOT_NOT_BUILT_IN;
     }
     return NS_ERROR_FAILURE;
--- a/security/manager/ssl/tests/compiled/TestCertDB.cpp
+++ b/security/manager/ssl/tests/compiled/TestCertDB.cpp
@@ -13,20 +13,17 @@ main(int argc, char* argv[])
 {
   {
     NS_InitXPCOM2(nullptr, nullptr, nullptr);
     nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
     if (!prefs) {
       return -1;
     }
     // When NSS initializes, it attempts to get some localized strings.
-    // As a result, OS X and Windows flip out if this isn't set.
-    // (This isn't done automatically since this test doesn't have a
-    // lot of the other boilerplate components that would otherwise
-    // keep the certificate db alive longer than we want it to.)
+    // As a result, Android flips out if this isn't set.
     nsresult rv = prefs->SetBoolPref("intl.locale.matchOS", true);
     if (NS_FAILED(rv)) {
       return -1;
     }
     nsCOMPtr<nsIX509CertDB> certdb(do_GetService(NS_X509CERTDB_CONTRACTID));
     if (!certdb) {
       return -1;
     }
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/compiled/TestIsCertBuiltInRoot.cpp
@@ -0,0 +1,263 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#define CERT_AddTempCertToPerm __CERT_AddTempCertToPerm
+
+#include "ScopedNSSTypes.h"
+#include "TestHarness.h"
+#include "cert.h"
+#include "certdb.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIX509Cert.h"
+#include "nsIX509CertDB.h"
+#include "nsIX509CertList.h"
+#include "nsServiceManagerUtils.h"
+#include "nss.h"
+#include "prerror.h"
+#include "secerr.h"
+
+// This is a certificate that (currently) ships with the platform. This test
+// loads this certificate into the read/write certificate database, which
+// simulates the situation where a built-in certificate's trust settings have
+// been changed. It should still be considered a built-in root.
+static char sGeoTrustPEM[] = "-----BEGIN CERTIFICATE-----\n\
+MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL\n\
+MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj\n\
+KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2\n\
+MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0\n\
+eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV\n\
+BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw\n\
+NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV\n\
+BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH\n\
+MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL\n\
+So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal\n\
+tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO\n\
+BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG\n\
+CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT\n\
+qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz\n\
+rD6ogRLQy7rQkgu2npaqBA+K\n\
+-----END CERTIFICATE-----";
+
+static char sGeoTrustNickname[] =
+  "GeoTrust Primary Certification Authority - G2";
+
+static char sGeoTrustCertDBKey[] = "AAAAAAAAAAAAAAAQAAAAmzyy9EgK\n\
+AOL+6yQ7XmA+w2swgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJ\n\
+bmMuMTkwNwYDVQQLEzAoYykgMjAwNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhv\n\
+cml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlm\n\
+aWNhdGlvbiBBdXRob3JpdHkgLSBHMg==";
+
+// This is the DB key (see nsIX509Cert.idl) of another built-in certificate.
+// This test makes no changes to its trust settings. It should be considered a
+// built-in root.
+static char sVeriSignCertDBKey[] = "AAAAAAAAAAAAAAAQAAAAzS+A/iOM\n\
+DiIPSGcSKJGHrLMwgcoxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwg\n\
+SW5jLjEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMx\n\
+KGMpIDIwMDcgVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s\n\
+eTFFMEMGA1UEAxM8VmVyaVNpZ24gQ2xhc3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0\n\
+aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0";
+
+// This is a certificate that does not ship with the platform.
+// It should not be considered a built-in root.
+static char sLetsEncryptPEM[] = "-----BEGIN CERTIFICATE-----\n\
+MIIEqDCCA5CgAwIBAgIRAJgT9HUT5XULQ+dDHpceRL0wDQYJKoZIhvcNAQELBQAw\n\
+PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\n\
+Ew5EU1QgUm9vdCBDQSBYMzAeFw0xNTEwMTkyMjMzMzZaFw0yMDEwMTkyMjMzMzZa\n\
+MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD\n\
+ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTCCASIwDQYJKoZIhvcNAQEBBQAD\n\
+ggEPADCCAQoCggEBAJzTDPBa5S5Ht3JdN4OzaGMw6tc1Jhkl4b2+NfFwki+3uEtB\n\
+BaupnjUIWOyxKsRohwuj43Xk5vOnYnG6eYFgH9eRmp/z0HhncchpDpWRz/7mmelg\n\
+PEjMfspNdxIknUcbWuu57B43ABycrHunBerOSuu9QeU2mLnL/W08lmjfIypCkAyG\n\
+dGfIf6WauFJhFBM/ZemCh8vb+g5W9oaJ84U/l4avsNwa72sNlRZ9xCugZbKZBDZ1\n\
+gGusSvMbkEl4L6KWTyogJSkExnTA0DHNjzE4lRa6qDO4Q/GxH8Mwf6J5MRM9LTb4\n\
+4/zyM2q5OTHFr8SNDR1kFjOq+oQpttQLwNh9w5MCAwEAAaOCAZIwggGOMBIGA1Ud\n\
+EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAy\n\
+BggrBgEFBQcwAYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5j\n\
+b20wOwYIKwYBBQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMv\n\
+ZHN0cm9vdGNheDMucDdjMB8GA1UdIwQYMBaAFMSnsaR7LHH62+FLkHX/xBVghYkQ\n\
+MFQGA1UdIARNMEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUH\n\
+AgEWImh0dHA6Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUw\n\
+MzAxoC+gLYYraHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JM\n\
+LmNybDATBgNVHR4EDDAKoQgwBoIELm1pbDAdBgNVHQ4EFgQUqEpqYwR93brm0Tm3\n\
+pkVl7/Oo7KEwDQYJKoZIhvcNAQELBQADggEBANHIIkus7+MJiZZQsY14cCoBG1hd\n\
+v0J20/FyWo5ppnfjL78S2k4s2GLRJ7iD9ZDKErndvbNFGcsW+9kKK/TnY21hp4Dd\n\
+ITv8S9ZYQ7oaoqs7HwhEMY9sibED4aXw09xrJZTC9zK1uIfW6t5dHQjuOWv+HHoW\n\
+ZnupyxpsEUlEaFb+/SCI4KCSBdAsYxAcsHYI5xxEI4LutHp6s3OT2FuO90WfdsIk\n\
+6q78OMSdn875bNjdBYAqxUp2/LEIHfDBkLoQz0hFJmwAbYahqKaLn73PAAm1X2kj\n\
+f1w8DdnkabOLGeOVcj9LQ+s67vBykx4anTjURkbqZslUEUsn2k5xeua2zUk=\n\
+-----END CERTIFICATE-----";
+
+static char sLetsEncryptNickname[] = "Let's Encrypt Authority X1";
+
+static char sLetsEncryptCertDBKey[] = "AAAAAAAAAAAAAAARAAAAQQCYE\n\
+/R1E+V1C0PnQx6XHkS9MD8xJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRyd\n\
+XN0IENvLjEXMBUGA1UEAxMORFNUIFJvb3QgQ0EgWDM=";
+
+static SECItem* sCertDER = nullptr;
+
+static SECStatus
+GetCertDER(void*, SECItem** certs, int numcerts)
+{
+  if (numcerts != 1) {
+    fail("numcerts should be 1");
+    PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
+    return SECFailure;
+  }
+  sCertDER = SECITEM_DupItem(certs[0]);
+  if (!sCertDER) {
+    fail("failed to copy data out (out of memory?)");
+    PR_SetError(SEC_ERROR_NO_MEMORY, 0);
+    return SECFailure;
+  }
+  passed("GetCertDER succeeded");
+  return SECSuccess;
+}
+
+bool
+AddCertificate(char* pem, char* nickname)
+{
+  if (CERT_DecodeCertPackage(pem, strlen(pem), GetCertDER, nullptr)
+        != SECSuccess) {
+    fail("CERT_DecodeCertPackage failed");
+    return false;
+  }
+
+  if (!sCertDER) {
+    fail("sCertDER didn't get set as expected");
+    return false;
+  }
+
+  mozilla::ScopedCERTCertificate cert(
+    CERT_NewTempCertificate(CERT_GetDefaultCertDB(), sCertDER, nickname, false,
+                            true));
+  if (!cert) {
+    fail("CERT_NewTempCertificate failed");
+    return false;
+  }
+
+  CERTCertTrust trust;
+  trust.sslFlags = 0;
+  trust.emailFlags = 0;
+  trust.objectSigningFlags = 0;
+  if (CERT_AddTempCertToPerm(cert, nickname, &trust) != SECSuccess) {
+    fail("CERT_AddTempCertToPerm failed");
+    return false;
+  }
+  passed("AddCertificate succeeded");
+  return true;
+}
+
+bool
+PreloadNSSCertDB(const char* profilePath)
+{
+  if (NSS_IsInitialized()) {
+    fail("NSS shouldn't already be initialized, or part of this test is moot");
+    return false;
+  }
+  if (NSS_Initialize(profilePath, "", "", SECMOD_DB, 0) != SECSuccess) {
+    fail("couldn't initialize NSS the first time");
+    return false;
+  }
+
+  if (!AddCertificate(sGeoTrustPEM, sGeoTrustNickname)) {
+    fail("couldn't add GeoTrust certificate to NSS");
+    return false;
+  }
+
+  if (!AddCertificate(sLetsEncryptPEM, sLetsEncryptNickname)) {
+    fail("couldn't add Let's Encrypt certificate to NSS");
+    return false;
+  }
+
+  if (NSS_Shutdown() != SECSuccess) {
+    fail("couldn't shut down NSS the first time");
+    return false;
+  }
+  passed("PreloadNSSCertDB succeeded");
+  return true;
+}
+
+bool
+TestIsCertBuiltIn(const char* certDBKey, bool expectedIsBuiltIn)
+{
+  nsCOMPtr<nsIX509CertDB> certDB(do_GetService(NS_X509CERTDB_CONTRACTID));
+  if (!certDB) {
+    fail("couldn't get certDB");
+    return false;
+  }
+
+  nsCOMPtr<nsIX509Cert> cert;
+  if (NS_FAILED(certDB->FindCertByDBKey(certDBKey, getter_AddRefs(cert)))) {
+    fail("couldn't find root certificate in database (maybe it was removed?)");
+    return false;
+  }
+  if (!cert) {
+    fail("FindCertByDBKey says it succeeded but it clearly didn't");
+    return false;
+  }
+
+  bool isBuiltInRoot;
+  if (NS_FAILED(cert->GetIsBuiltInRoot(&isBuiltInRoot))) {
+    fail("couldn't determine if the certificate was a built-in or not");
+    return false;
+  }
+  if (isBuiltInRoot != expectedIsBuiltIn) {
+    fail("did not get expected value for isBuiltInRoot");
+    return false;
+  }
+  passed("got expected value for isBuiltInRoot");
+  return true;
+}
+
+int
+main(int argc, char* argv[])
+{
+  ScopedXPCOM xpcom("TestIsCertBuiltInRoot");
+  if (xpcom.failed()) {
+    fail("couldn't initialize XPCOM");
+    return 1;
+  }
+  nsCOMPtr<nsIFile> profileDirectory(xpcom.GetProfileDirectory());
+  if (!profileDirectory) {
+    fail("couldn't get profile directory");
+    return 1;
+  }
+  nsAutoCString profilePath;
+  if (NS_FAILED(profileDirectory->GetNativePath(profilePath))) {
+    fail("couldn't get profile path");
+    return 1;
+  }
+  // One of the cases we want to test is when (in a previous run of the
+  // platform) a built-in root certificate has had its trust modified from the
+  // defaults. We can't initialize XPCOM twice in this test, but we can use NSS
+  // directly to create a certficate database (before instantiating any XPCOM
+  // objects that rely on NSS and thus would initialize it) with the appropriate
+  // certificates for these tests.
+  if (!PreloadNSSCertDB(profilePath.get())) {
+    fail("couldn't set up NSS certificate DB for test");
+    return 1;
+  }
+
+  if (!TestIsCertBuiltIn(sVeriSignCertDBKey, true)) {
+    fail("built-in root with no modified trust should be considered built-in");
+  } else {
+    passed("built-in root with no modified trust considered built-in");
+  }
+
+  if (!TestIsCertBuiltIn(sGeoTrustCertDBKey, true)) {
+    fail("built-in root with modified trust should be considered built-in");
+  } else {
+    passed("built-in root with modified trust considered built-in");
+  }
+
+  if (!TestIsCertBuiltIn(sLetsEncryptCertDBKey, false)) {
+    fail("non-built-in root should not be considered built-in");
+  } else {
+    passed("non-built-in root should not considered built-in");
+  }
+
+  return gFailCount;
+}
--- a/security/manager/ssl/tests/compiled/moz.build
+++ b/security/manager/ssl/tests/compiled/moz.build
@@ -1,18 +1,23 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 CppUnitTests([
+    'TestIsCertBuiltInRoot',
     'TestSTSParser',
 ])
 
 LOCAL_INCLUDES += [
     '/security/manager/ssl/',
 ]
 
 GeckoCppUnitTests([
     'TestCertDB',
     'TestMD4',
 ])
+
+USE_LIBS += [
+    'nss',
+]
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_cert_isBuiltInRoot.js
@@ -0,0 +1,71 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 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/.
+
+// Tests that nsIX509Cert.isBuiltInRoot works as expected.
+
+"use strict";
+
+do_get_profile(); // must be called before getting nsIX509CertDB
+const certdb = Cc["@mozilla.org/security/x509certdb;1"]
+                 .getService(Ci.nsIX509CertDB);
+
+// This is a certificate that (currently) ships with the platform.
+// It should be considered a built-in root.
+const sGeoTrustBase64 = "" +
+  "MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL" +
+  "MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj" +
+  "KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2" +
+  "MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0" +
+  "eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV" +
+  "BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw" +
+  "NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV" +
+  "BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH" +
+  "MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL" +
+  "So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal" +
+  "tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO" +
+  "BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG" +
+  "CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT" +
+  "qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz" +
+  "rD6ogRLQy7rQkgu2npaqBA+K";
+
+
+// This is a certificate that does not ship with the platform.
+// It should not be considered a built-in root.
+const sLetsEncryptBase64 = "" +
+  "MIIEqDCCA5CgAwIBAgIRAJgT9HUT5XULQ+dDHpceRL0wDQYJKoZIhvcNAQELBQAw" +
+  "PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD" +
+  "Ew5EU1QgUm9vdCBDQSBYMzAeFw0xNTEwMTkyMjMzMzZaFw0yMDEwMTkyMjMzMzZa" +
+  "MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD" +
+  "ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTCCASIwDQYJKoZIhvcNAQEBBQAD" +
+  "ggEPADCCAQoCggEBAJzTDPBa5S5Ht3JdN4OzaGMw6tc1Jhkl4b2+NfFwki+3uEtB" +
+  "BaupnjUIWOyxKsRohwuj43Xk5vOnYnG6eYFgH9eRmp/z0HhncchpDpWRz/7mmelg" +
+  "PEjMfspNdxIknUcbWuu57B43ABycrHunBerOSuu9QeU2mLnL/W08lmjfIypCkAyG" +
+  "dGfIf6WauFJhFBM/ZemCh8vb+g5W9oaJ84U/l4avsNwa72sNlRZ9xCugZbKZBDZ1" +
+  "gGusSvMbkEl4L6KWTyogJSkExnTA0DHNjzE4lRa6qDO4Q/GxH8Mwf6J5MRM9LTb4" +
+  "4/zyM2q5OTHFr8SNDR1kFjOq+oQpttQLwNh9w5MCAwEAAaOCAZIwggGOMBIGA1Ud" +
+  "EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAy" +
+  "BggrBgEFBQcwAYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5j" +
+  "b20wOwYIKwYBBQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMv" +
+  "ZHN0cm9vdGNheDMucDdjMB8GA1UdIwQYMBaAFMSnsaR7LHH62+FLkHX/xBVghYkQ" +
+  "MFQGA1UdIARNMEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUH" +
+  "AgEWImh0dHA6Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUw" +
+  "MzAxoC+gLYYraHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JM" +
+  "LmNybDATBgNVHR4EDDAKoQgwBoIELm1pbDAdBgNVHQ4EFgQUqEpqYwR93brm0Tm3" +
+  "pkVl7/Oo7KEwDQYJKoZIhvcNAQELBQADggEBANHIIkus7+MJiZZQsY14cCoBG1hd" +
+  "v0J20/FyWo5ppnfjL78S2k4s2GLRJ7iD9ZDKErndvbNFGcsW+9kKK/TnY21hp4Dd" +
+  "ITv8S9ZYQ7oaoqs7HwhEMY9sibED4aXw09xrJZTC9zK1uIfW6t5dHQjuOWv+HHoW" +
+  "ZnupyxpsEUlEaFb+/SCI4KCSBdAsYxAcsHYI5xxEI4LutHp6s3OT2FuO90WfdsIk" +
+  "6q78OMSdn875bNjdBYAqxUp2/LEIHfDBkLoQz0hFJmwAbYahqKaLn73PAAm1X2kj" +
+  "f1w8DdnkabOLGeOVcj9LQ+s67vBykx4anTjURkbqZslUEUsn2k5xeua2zUk=";
+
+function run_test() {
+  let builtInCert = certdb.constructX509FromBase64(sGeoTrustBase64);
+  ok(builtInCert, "should be able to decode base-64 of built-in cert");
+  ok(builtInCert.isBuiltInRoot, "cert should be considered built-in");
+
+  let notBuiltInCert = certdb.constructX509FromBase64(sLetsEncryptBase64);
+  ok(notBuiltInCert, "should be able to decode base-64 of built-in cert");
+  ok(!notBuiltInCert.isBuiltInRoot, "cert should not be considered built-in");
+}
--- a/security/manager/ssl/tests/unit/xpcshell.ini
+++ b/security/manager/ssl/tests/unit/xpcshell.ini
@@ -31,16 +31,17 @@ support-files =
 skip-if = buildapp == "b2g"
 tags = addons psm
 [test_cert_chains.js]
 run-sequentially = hardcoded ports
 [test_cert_dbKey.js]
 [test_cert_eku.js]
 [test_cert_embedded_null.js]
 [test_cert_keyUsage.js]
+[test_cert_isBuiltInRoot.js]
 [test_cert_overrides.js]
 run-sequentially = hardcoded ports
 [test_cert_override_bits_mismatches.js]
 run-sequentially = hardcoded ports
 [test_cert_sha1.js]
 [test_cert_signatures.js]
 [test_cert_trust.js]
 [test_cert_version.js]
--- a/testing/cppunittest.ini
+++ b/testing/cppunittest.ini
@@ -36,16 +36,18 @@ skip-if = os != 'win'
 [TestFile]
 [TestFloatingPoint]
 [TestGetURL]
 [TestHashtables]
 [TestID]
 [TestInitializerList]
 [TestIntegerPrintfMacros]
 [TestIntegerRange]
+[TestIsCertBuiltInRoot]
+skip-if = os == 'android' # bug 1228175, bug 929655
 [TestJSONWriter]
 [TestJemalloc]
 [TestLineBreak]
 [TestMD4]
 [TestMacroArgs]
 [TestMacroForEach]
 [TestMaybe]
 [TestNativeXMLHttpRequest]