bug 1228175 - fix IsCertBuiltInRoot r=Cykesiopka,mgoodwin
☠☠ backed out by 38bf4f8e55bc ☠ ☠
authorDavid Keeler <dkeeler@mozilla.com>
Fri, 04 Mar 2016 17:06:33 -0800
changeset 287540 490eb9194ae1bdd73cf4e105654dae2bf4d1abfe
parent 287539 c7a8a54d90587a4c3da931c5aaad77df8440cf3e
child 287541 6d40b86b8cc17c4ffd7b4d3c5193a8ac5429ca26
push id73216
push userdkeeler@mozilla.com
push dateWed, 09 Mar 2016 20:54:29 +0000
treeherdermozilla-inbound@490eb9194ae1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersCykesiopka, mgoodwin
bugs1228175
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 1228175 - fix IsCertBuiltInRoot r=Cykesiopka,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/TestIsCertBuiltInRoot.cpp
security/manager/ssl/tests/compiled/moz.build
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;
 
 PRLogModuleInfo* gCertVerifierLog = nullptr;
 
 namespace mozilla { namespace psm {
@@ -66,45 +69,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
@@ -719,18 +719,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;
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/compiled/TestIsCertBuiltInRoot.cpp
@@ -0,0 +1,269 @@
+/* -*- 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 "nsIPrefService.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<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+  if (!prefs) {
+    fail("couldn't get pref service");
+    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',
+]
--- a/testing/cppunittest.ini
+++ b/testing/cppunittest.ini
@@ -36,16 +36,17 @@ skip-if = os != 'win'
 [TestFile]
 [TestFloatingPoint]
 [TestGetURL]
 [TestHashtables]
 [TestID]
 [TestInitializerList]
 [TestIntegerPrintfMacros]
 [TestIntegerRange]
+[TestIsCertBuiltInRoot]
 [TestJSONWriter]
 [TestJemalloc]
 [TestLineBreak]
 [TestMD4]
 [TestMacroArgs]
 [TestMacroForEach]
 [TestMaybe]
 [TestNativeXMLHttpRequest]