bug 1265113 - implement platform support for enterprise roots draft
authorDavid Keeler <dkeeler@mozilla.com>
Wed, 13 Apr 2016 15:36:22 -0700
changeset 371017 38e531840f5999792b0471c0b02cd2828851d78e
parent 370474 5511d54a3f172c1d68f98cc55dce4de1d0ba1b51
child 521882 aa6cb6c0922aeab8eef0515eaec072121a1550d9
push id19203
push userbmo:dkeeler@mozilla.com
push dateWed, 25 May 2016 20:04:50 +0000
bugs1265113
milestone49.0a1
bug 1265113 - implement platform support for enterprise roots MozReview-Commit-ID: JKxwCjoH0Oa
security/manager/ssl/nsIX509CertDB.idl
security/manager/ssl/nsNSSCertificateDB.cpp
security/manager/ssl/nsNSSComponent.cpp
security/manager/ssl/nsNSSComponent.h
security/manager/ssl/tests/unit/test_enterprise_roots.js
security/manager/ssl/tests/unit/xpcshell.ini
--- a/security/manager/ssl/nsIX509CertDB.idl
+++ b/security/manager/ssl/nsIX509CertDB.idl
@@ -385,9 +385,15 @@ interface nsIX509CertDB : nsISupports {
    * @param aName name of the cert for display purposes.
    */
   void addCertFromBase64(in string base64, in string aTrust, in string aName);
 
   /*
    * Get all the known certs in the database
    */
   nsIX509CertList getCerts();
+
+  /*
+   * Get a list of imported enterprise root certificates (currently only
+   * implemented on Windows).
+   */
+  nsIX509CertList getEnterpriseRoots();
 };
--- a/security/manager/ssl/nsNSSCertificateDB.cpp
+++ b/security/manager/ssl/nsNSSCertificateDB.cpp
@@ -1471,16 +1471,42 @@ nsNSSCertificateDB::GetCerts(nsIX509Cert
   // nsNSSCertList 1) adopts certList, and 2) handles the nullptr case fine.
   // (returns an empty list) 
   nssCertList = new nsNSSCertList(Move(certList), locker);
 
   nssCertList.forget(_retval);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsNSSCertificateDB::GetEnterpriseRoots(nsIX509CertList** enterpriseRoots)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!NS_IsMainThread()) {
+    return NS_ERROR_NOT_SAME_THREAD;
+  }
+
+  NS_ENSURE_ARG_POINTER(enterpriseRoots);
+
+  nsNSSShutDownPreventionLock locker;
+  if (isAlreadyShutDown()) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+#ifdef XP_WIN
+  nsCOMPtr<nsINSSComponent> psm(do_GetService(PSM_COMPONENT_CONTRACTID));
+  if (!psm) {
+    return NS_ERROR_FAILURE;
+  }
+  return psm->GetEnterpriseRoots(enterpriseRoots);
+#else
+  return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
 nsresult
 VerifyCertAtTime(nsIX509Cert* aCert,
                  int64_t /*SECCertificateUsage*/ aUsage,
                  uint32_t aFlags,
                  const char* aHostname,
                  mozilla::pkix::Time aTime,
                  nsIX509CertList** aVerifiedChain,
                  bool* aHasEVPolicy,
--- a/security/manager/ssl/nsNSSComponent.cpp
+++ b/security/manager/ssl/nsNSSComponent.cpp
@@ -7,23 +7,25 @@
 #include "nsNSSComponent.h"
 
 #include "ExtendedValidation.h"
 #include "NSSCertDBTrustDomain.h"
 #include "ScopedNSSTypes.h"
 #include "SharedSSLState.h"
 #include "cert.h"
 #include "certdb.h"
+#include "mozilla/ArrayUtils.h"
 #include "mozilla/Casting.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/PublicSSL.h"
 #include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/SyncRunnable.h"
 #include "mozilla/Telemetry.h"
+#include "mozilla/unused.h"
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsCRT.h"
 #include "nsCertVerificationThread.h"
 #include "nsClientAuthRemember.h"
 #include "nsComponentManagerUtils.h"
 #include "nsDirectoryServiceDefs.h"
 #include "nsICertOverrideService.h"
 #include "nsIFile.h"
@@ -437,17 +439,17 @@ GetUserSid(nsAString& sidString)
   DWORD lcAccountName = sizeof(lpAccountName) / sizeof(lpAccountName[0]);
   BOOL success = GetUserName(lpAccountName, &lcAccountName);
   if (!success) {
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("GetUserName failed"));
     return false;
   }
   char sid_buffer[SECURITY_MAX_SID_SIZE];
   SID* sid = BitwiseCast<SID*, char*>(sid_buffer);
-  DWORD cbSid = MOZ_ARRAY_LENGTH(sid_buffer);
+  DWORD cbSid = ArrayLength(sid_buffer);
   SID_NAME_USE eUse;
   // There doesn't appear to be a defined maximum length for the domain name
   // here. To deal with this, we start with a reasonable buffer length and
   // see if that works. If it fails and the error indicates insufficient length,
   // we use the indicated required length and try again.
   DWORD cchReferencedDomainName = 128;
   auto ReferencedDomainName(MakeUnique<WCHAR[]>(cchReferencedDomainName));
   success = LookupAccountName(nullptr, lpAccountName, sid, &cbSid,
@@ -647,77 +649,92 @@ AccountHasFamilySafetyEnabled(bool& enab
             ("failed to read value of Web\\Filter On"));
     return rv;
   }
   MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("Web\\Filter On: %u", webFilterOn));
   enabled = loggingRequired == 1 || webFilterOn == 1;
   return NS_OK;
 }
 
-const char* kImportedFamilySafetyRootPref =
-  "security.family_safety.imported_root.db_key";
+// It would be convenient to just use nsIX509CertDB in the following code.
+// However, since nsIX509CertDB depends on nsNSSComponent initialization (and
+// since this code runs during that initialization), we can't use it. Instead,
+// we can use NSS APIs directly (as long as we're called late enough in
+// nsNSSComponent initialization such that those APIs are safe to use).
 
-static nsresult
-MaybeImportFamilySafetyRoot(PCCERT_CONTEXT certificate,
-                            bool& wasFamilySafetyRoot)
+// Helper function to convert a PCCERT_CONTEXT (i.e. a certificate obtained via
+// a Windows API) to a temporary CERTCertificate (i.e. a certificate for use
+// with NSS APIs).
+static UniqueCERTCertificate
+PCCERT_CONTEXTToCERTCertificate(PCCERT_CONTEXT pccert)
 {
-  MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("MaybeImportFamilySafetyRoot"));
-  wasFamilySafetyRoot = false;
-
-  // It would be convenient to just use nsIX509CertDB here. However, since
-  // nsIX509CertDB depends on nsNSSComponent initialization, we can't use it.
-  // Instead, we can use NSS APIs directly (as long as we're called late enough
-  // in nsNSSComponent initialization such that those APIs are safe to use).
+  MOZ_ASSERT(pccert);
+  if (!pccert) {
+    return nullptr;
+  }
 
   SECItem derCert = {
     siBuffer,
-    certificate->pbCertEncoded,
-    certificate->cbCertEncoded
+    pccert->pbCertEncoded,
+    pccert->cbCertEncoded
   };
-  UniqueCERTCertificate nssCertificate(
+  return UniqueCERTCertificate(
     CERT_NewTempCertificate(CERT_GetDefaultCertDB(), &derCert,
                             nullptr, // nickname unnecessary
                             false, // not permanent
                             true)); // copy DER
+}
+
+static const char* kMicrosoftFamilySafetyCN = "Microsoft Family Safety";
+
+nsresult
+nsNSSComponent::MaybeImportFamilySafetyRoot(PCCERT_CONTEXT certificate,
+                                            bool& wasFamilySafetyRoot)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!NS_IsMainThread()) {
+    return NS_ERROR_NOT_SAME_THREAD;
+  }
+  MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("MaybeImportFamilySafetyRoot"));
+  wasFamilySafetyRoot = false;
+
+  UniqueCERTCertificate nssCertificate(
+    PCCERT_CONTEXTToCERTCertificate(certificate));
   if (!nssCertificate) {
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("couldn't decode certificate"));
     return NS_ERROR_FAILURE;
   }
   // Looking for a certificate with the common name 'Microsoft Family Safety'
   UniquePORTString subjectName(CERT_GetCommonName(&nssCertificate->subject));
   MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
           ("subject name is '%s'", subjectName.get()));
-  if (nsCRT::strcmp(subjectName.get(), "Microsoft Family Safety") == 0) {
+  if (nsCRT::strcmp(subjectName.get(), kMicrosoftFamilySafetyCN) == 0) {
     wasFamilySafetyRoot = true;
     CERTCertTrust trust = {
       CERTDB_TRUSTED_CA | CERTDB_VALID_CA | CERTDB_USER,
       0,
       0
     };
-    SECStatus srv = __CERT_AddTempCertToPerm(
-      nssCertificate.get(), "Microsoft Family Safety", &trust);
-    if (srv != SECSuccess) {
+    if (CERT_ChangeCertTrust(nullptr, nssCertificate.get(), &trust)
+          != SECSuccess) {
       MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-              ("couldn't permanently add certificate"));
+              ("couldn't trust certificate for TLS server auth"));
       return NS_ERROR_FAILURE;
     }
-    nsAutoCString dbKey;
-    nsresult rv = nsNSSCertificate::GetDbKey(nssCertificate, dbKey);
-    if (NS_FAILED(rv)) {
-      MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("GetDbKey failed"));
-      return rv;
-    }
-    Preferences::SetCString(kImportedFamilySafetyRootPref, dbKey);
+    MOZ_ASSERT(!mFamilySafetyRoot);
+    mFamilySafetyRoot = Move(nssCertificate);
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("added Family Safety root"));
   }
   return NS_OK;
 }
 
 // Because HCERTSTORE is just a typedef void*, we can't use any of the nice
-// scoped pointer templates.
+// scoped or unique pointer templates. To elaborate, any attempt would
+// instantiate those templates with T = void. When T gets used in the context
+// of T&, this results in void&, which isn't legal.
 class ScopedCertStore final
 {
 public:
   explicit ScopedCertStore(HCERTSTORE certstore) : certstore(certstore) {}
 
   ~ScopedCertStore()
   {
     CertCloseStore(certstore, 0);
@@ -729,77 +746,71 @@ public:
   }
 
 private:
   ScopedCertStore(const ScopedCertStore&) = delete;
   ScopedCertStore& operator=(const ScopedCertStore&) = delete;
   HCERTSTORE certstore;
 };
 
-static const wchar_t* WindowsDefaultRootStoreName = L"ROOT";
+static const wchar_t* kWindowsDefaultRootStoreName = L"ROOT";
 
-static nsresult
-LoadFamilySafetyRoot()
+nsresult
+nsNSSComponent::LoadFamilySafetyRoot()
 {
   ScopedCertStore certstore(
-    CertOpenSystemStore(0, WindowsDefaultRootStoreName));
+    CertOpenSystemStore(0, kWindowsDefaultRootStoreName));
   if (!certstore.get()) {
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-            ("couldn't get certstore '%S'", WindowsDefaultRootStoreName));
+            ("couldn't get certstore '%S'", kWindowsDefaultRootStoreName));
     return NS_ERROR_FAILURE;
   }
   // Any resources held by the certificate are released by the next call to
   // CertFindCertificateInStore.
   PCCERT_CONTEXT certificate = nullptr;
-  while (certificate = CertFindCertificateInStore(certstore.get(),
-                                                  X509_ASN_ENCODING, 0,
-                                                  CERT_FIND_ANY, nullptr,
-                                                  certificate)) {
+  while ((certificate = CertFindCertificateInStore(certstore.get(),
+                                                   X509_ASN_ENCODING, 0,
+                                                   CERT_FIND_ANY, nullptr,
+                                                   certificate))) {
     bool wasFamilySafetyRoot = false;
     nsresult rv = MaybeImportFamilySafetyRoot(certificate,
                                               wasFamilySafetyRoot);
     if (NS_SUCCEEDED(rv) && wasFamilySafetyRoot) {
       return NS_OK; // We're done (we're only expecting one root).
     }
   }
   return NS_ERROR_FAILURE;
 }
 
-static void
-UnloadFamilySafetyRoot()
+void
+nsNSSComponent::UnloadFamilySafetyRoot()
 {
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!NS_IsMainThread()) {
+    return;
+  }
   MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("UnloadFamilySafetyRoot"));
-  nsAdoptingCString dbKey = Preferences::GetCString(
-    kImportedFamilySafetyRootPref);
-  if (!dbKey || dbKey.IsEmpty()) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-            ("Family Safety root wasn't previously imported"));
+  if (!mFamilySafetyRoot) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("Family Safety Root wasn't present"));
     return;
   }
-  UniqueCERTCertificate cert;
-  nsresult rv = nsNSSCertificateDB::FindCertByDBKey(dbKey, cert);
-  if (NS_FAILED(rv)) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-            ("finding previously-imported Family Safety root failed"));
-    return;
-  }
-  if (!cert) {
+  // It would be intuitive to set the trust to { 0, 0, 0 } here. However, this
+  // doesn't work for temporary certificates because CERT_ChangeCertTrust first
+  // looks up the current trust settings in the permanent cert database, finds
+  // that such trust doesn't exist, considers the current trust to be
+  // { 0, 0, 0 }, and decides that it doesn't need to update the trust since
+  // they're the same. To work around this, we set a non-zero flag to ensure
+  // that the trust will get updated.
+  CERTCertTrust trust = { CERTDB_USER, 0, 0 };
+  if (CERT_ChangeCertTrust(nullptr, mFamilySafetyRoot.get(), &trust)
+        != SECSuccess) {
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-            ("previously-imported Family Safety root not found"));
-    return;
+            ("couldn't untrust certificate for TLS server auth"));
   }
-  SECStatus srv = SEC_DeletePermCertificate(cert.get());
-  if (srv != SECSuccess) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-            ("couldn't delete previously-imported Family Safety root"));
-    return;
-  }
-  MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-          ("deleted previously-imported Family Safety root"));
-  Preferences::ClearUser(kImportedFamilySafetyRootPref);
+  mFamilySafetyRoot = nullptr;
 }
 
 #endif // XP_WIN
 
 // The supported values of this pref are:
 // 0: disable detecting Family Safety mode and importing the root
 // 1: only attempt to detect Family Safety mode (don't import the root)
 // 2: detect Family Safety mode and import the root
@@ -807,18 +818,18 @@ const char* kFamilySafetyModePref = "sec
 
 // The telemetry gathered by this function is as follows:
 // 0-2: the value of the Family Safety mode pref
 // 3: detecting Family Safety mode failed
 // 4: Family Safety was not enabled
 // 5: Family Safety was enabled
 // 6: failed to import the Family Safety root
 // 7: successfully imported the root
-static void
-MaybeEnableFamilySafetyCompatibility()
+void
+nsNSSComponent::MaybeEnableFamilySafetyCompatibility()
 {
 #ifdef XP_WIN
   UnloadFamilySafetyRoot();
   if (!(IsWin8Point1OrLater() && !IsWin10OrLater())) {
     return;
   }
   // Detect but don't import by default.
   uint32_t familySafetyMode = Preferences::GetUint(kFamilySafetyModePref, 1);
@@ -848,16 +859,221 @@ MaybeEnableFamilySafetyCompatibility()
               ("failed to load Family Safety root"));
     } else {
       Telemetry::Accumulate(Telemetry::FAMILY_SAFETY, 7);
     }
   }
 #endif // XP_WIN
 }
 
+#ifdef XP_WIN
+// Helper function to determine if the OS considers the given certificate to be
+// a trust anchor for TLS server auth certificates. This is to be used in the
+// context of importing what are presumed to be root certificates from the OS.
+// If this function returns true but it turns out that the given certificate is
+// in some way unsuitable to issue certificates, mozilla::pkix will never build
+// a valid chain that includes the certificate, so importing it even if it
+// isn't a valid CA poses no risk.
+static bool
+CertIsTrustAnchorForTLSServerAuth(PCCERT_CONTEXT certificate)
+{
+  MOZ_ASSERT(certificate);
+  if (!certificate) {
+    return false;
+  }
+
+  PCCERT_CHAIN_CONTEXT pChainContext = nullptr;
+  CERT_ENHKEY_USAGE enhkeyUsage;
+  memset(&enhkeyUsage, 0, sizeof(CERT_ENHKEY_USAGE));
+  LPSTR identifiers[] = {
+    "1.3.6.1.5.5.7.3.1", // id-kp-serverAuth
+  };
+  enhkeyUsage.cUsageIdentifier = ArrayLength(identifiers);
+  enhkeyUsage.rgpszUsageIdentifier = identifiers;
+  CERT_USAGE_MATCH certUsage;
+  memset(&certUsage, 0, sizeof(CERT_USAGE_MATCH));
+  certUsage.dwType = USAGE_MATCH_TYPE_AND;
+  certUsage.Usage = enhkeyUsage;
+  CERT_CHAIN_PARA chainPara;
+  memset(&chainPara, 0, sizeof(CERT_CHAIN_PARA));
+  chainPara.cbSize = sizeof(CERT_CHAIN_PARA);
+  chainPara.RequestedUsage = certUsage;
+
+  if (!CertGetCertificateChain(nullptr, certificate, nullptr, nullptr,
+                               &chainPara, 0, nullptr, &pChainContext)) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("CertGetCertificateChain failed"));
+    return false;
+  }
+  bool trusted = pChainContext->TrustStatus.dwErrorStatus ==
+                 CERT_TRUST_NO_ERROR;
+  bool isRoot = pChainContext->cChain == 1;
+  CertFreeCertificateChain(pChainContext);
+  if (trusted && isRoot) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+            ("certificate is trust anchor for TLS server auth"));
+    return true;
+  }
+  MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+          ("certificate not trust anchor for TLS server auth"));
+  return false;
+}
+
+void
+nsNSSComponent::UnloadEnterpriseRoots()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!NS_IsMainThread()) {
+    return;
+  }
+  MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("UnloadEnterpriseRoots"));
+  if (!mEnterpriseRoots) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("no enterprise roots were present"));
+    return;
+  }
+  // It would be intuitive to set the trust to { 0, 0, 0 } here. However, this
+  // doesn't work for temporary certificates because CERT_ChangeCertTrust first
+  // looks up the current trust settings in the permanent cert database, finds
+  // that such trust doesn't exist, considers the current trust to be
+  // { 0, 0, 0 }, and decides that it doesn't need to update the trust since
+  // they're the same. To work around this, we set a non-zero flag to ensure
+  // that the trust will get updated.
+  CERTCertTrust trust = { CERTDB_USER, 0, 0 };
+  for (CERTCertListNode* n = CERT_LIST_HEAD(mEnterpriseRoots.get());
+       !CERT_LIST_END(n, mEnterpriseRoots.get()); n = CERT_LIST_NEXT(n)) {
+    if (!n || !n->cert) {
+      MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+              ("library failure: CERTCertListNode null or lacks cert"));
+      continue;
+    }
+    if (CERT_ChangeCertTrust(nullptr, n->cert, &trust) != SECSuccess) {
+      MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+              ("couldn't untrust certificate for TLS server auth"));
+    }
+  }
+  mEnterpriseRoots = nullptr;
+  MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("unloaded enterprise roots"));
+}
+
+NS_IMETHODIMP
+nsNSSComponent::GetEnterpriseRoots(nsIX509CertList** enterpriseRoots)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!NS_IsMainThread()) {
+    return NS_ERROR_NOT_SAME_THREAD;
+  }
+  NS_ENSURE_ARG_POINTER(enterpriseRoots);
+
+  nsNSSShutDownPreventionLock lock;
+  // nsNSSComponent isn't a nsNSSShutDownObject, so we can't check
+  // isAlreadyShutDown(). However, since mEnterpriseRoots is cleared when NSS
+  // shuts down, we can use that as a proxy for checking for NSS shutdown.
+  // (Of course, it may also be the case that no enterprise roots were imported,
+  // so we should just return a null list and NS_OK in this case.)
+  if (!mEnterpriseRoots) {
+    *enterpriseRoots = nullptr;
+    return NS_OK;
+  }
+  UniqueCERTCertList enterpriseRootsCopy(
+    nsNSSCertList::DupCertList(mEnterpriseRoots, lock));
+  if (!enterpriseRootsCopy) {
+    return NS_ERROR_FAILURE;
+  }
+  nsCOMPtr<nsIX509CertList> enterpriseRootsCertList(
+    new nsNSSCertList(Move(enterpriseRootsCopy), lock));
+  if (!enterpriseRootsCertList) {
+    return NS_ERROR_FAILURE;
+  }
+  enterpriseRootsCertList.forget(enterpriseRoots);
+  return NS_OK;
+}
+#endif // XP_WIN
+
+static const char* kEnterpriseRootModePref = "security.enterprise_roots.enabled";
+
+void
+nsNSSComponent::MaybeImportEnterpriseRoots()
+{
+#ifdef XP_WIN
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!NS_IsMainThread()) {
+    return;
+  }
+  UnloadEnterpriseRoots();
+  bool importEnterpriseRoots = Preferences::GetBool(kEnterpriseRootModePref,
+                                                    false);
+  if (!importEnterpriseRoots) {
+    return;
+  }
+  DWORD flags = CERT_SYSTEM_STORE_LOCAL_MACHINE |
+                CERT_STORE_OPEN_EXISTING_FLAG |
+                CERT_STORE_READONLY_FLAG;
+  // The certificate store being opened should consist only of certificates
+  // added by a user or administrator and not any certificates that are part
+  // of Microsoft's root store program.
+  // The 3rd parameter to CertOpenStore should be NULL according to
+  // https://msdn.microsoft.com/en-us/library/windows/desktop/aa376559%28v=vs.85%29.aspx
+  ScopedCertStore enterpriseRootStore(CertOpenStore(
+    CERT_STORE_PROV_SYSTEM_REGISTRY_W, 0, NULL, flags,
+    kWindowsDefaultRootStoreName));
+  if (!enterpriseRootStore.get()) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("failed to open enterprise root store"));
+    return;
+  }
+  MOZ_ASSERT(!mEnterpriseRoots);
+  mEnterpriseRoots.reset(CERT_NewCertList());
+  CERTCertTrust trust = {
+    CERTDB_TRUSTED_CA | CERTDB_VALID_CA | CERTDB_USER,
+    0,
+    0
+  };
+  PCCERT_CONTEXT certificate = nullptr;
+  uint32_t numImported = 0;
+  while ((certificate = CertFindCertificateInStore(enterpriseRootStore.get(),
+                                                   X509_ASN_ENCODING, 0,
+                                                   CERT_FIND_ANY, nullptr,
+                                                   certificate))) {
+    if (!CertIsTrustAnchorForTLSServerAuth(certificate)) {
+      MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+              ("skipping cert not trust anchor for TLS server auth"));
+      continue;
+    }
+    UniqueCERTCertificate nssCertificate(
+      PCCERT_CONTEXTToCERTCertificate(certificate));
+    if (!nssCertificate) {
+      MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("couldn't decode certificate"));
+      continue;
+    }
+    // Don't import the Microsoft Family Safety root (this prevents the
+    // Enterprise Roots feature from interacting poorly with the Family
+    // Safety support).
+    UniquePORTString subjectName(
+      CERT_GetCommonName(&nssCertificate->subject));
+    if (nsCRT::strcmp(subjectName.get(), kMicrosoftFamilySafetyCN) == 0) {
+      MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("skipping Family Safety Root"));
+      continue;
+    }
+    if (CERT_AddCertToListTail(mEnterpriseRoots.get(), nssCertificate.get())
+          != SECSuccess) {
+      MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("couldn't add cert to list"));
+      continue;
+    }
+    if (CERT_ChangeCertTrust(nullptr, nssCertificate.get(), &trust)
+          != SECSuccess) {
+      MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+              ("couldn't trust certificate for TLS server auth"));
+    }
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("Imported '%s'", subjectName.get()));
+    numImported++;
+    // now owned by mEnterpriseRoots
+    Unused << nssCertificate.release();
+  }
+  MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("imported %u roots", numImported));
+#endif // XP_WIN
+}
+
 void
 nsNSSComponent::LoadLoadableRoots()
 {
   nsNSSShutDownPreventionLock locker;
   SECMODModule* RootsModule = nullptr;
 
   // In the past we used SECMOD_AddNewModule to load our module containing
   // root CA certificates. This caused problems, refer to bug 176501.
@@ -1554,16 +1770,17 @@ nsNSSComponent::InitializeNSS()
   }
 
   DisableMD5();
   // Initialize the certverifier log before calling any functions that library.
   InitCertVerifierLog();
   LoadLoadableRoots();
 
   MaybeEnableFamilySafetyCompatibility();
+  MaybeImportEnterpriseRoots();
 
   ConfigureTLSSessionIdentifiers();
 
   bool requireSafeNegotiation =
     Preferences::GetBool("security.ssl.require_safe_negotiation",
                          REQUIRE_SAFE_NEGOTIATION_DEFAULT);
   SSL_OptionSetDefault(SSL_REQUIRE_SAFE_NEGOTIATION, requireSafeNegotiation);
 
@@ -1641,16 +1858,21 @@ nsNSSComponent::ShutdownNSS()
 
   if (mNSSInitialized) {
     mNSSInitialized = false;
 
     PK11_SetPasswordFunc((PK11PasswordFunc)nullptr);
 
     Preferences::RemoveObserver(this, "security.");
 
+#ifdef XP_WIN
+    mFamilySafetyRoot = nullptr;
+    mEnterpriseRoots = nullptr;
+#endif
+
 #ifndef MOZ_NO_SMART_CARDS
     ShutdownSmartCardThreads();
 #endif
     SSL_ClearSessionCache();
     UnloadLoadableRoots();
 #ifndef MOZ_NO_EV_CERTS
     CleanupIdentityInfo();
 #endif
@@ -1781,16 +2003,18 @@ nsNSSComponent::Observe(nsISupports* aSu
       mTestBuiltInRootHash = Preferences::GetString("security.test.built_in_root_hash");
 #endif // DEBUG
     } else if (prefName.Equals(kFamilySafetyModePref)) {
       MaybeEnableFamilySafetyCompatibility();
     } else if (prefName.EqualsLiteral("security.content.signature.root_hash")) {
       MutexAutoLock lock(mutex);
       mContentSigningRootHash =
         Preferences::GetString("security.content.signature.root_hash");
+    } else if (prefName.Equals(kEnterpriseRootModePref)) {
+      MaybeImportEnterpriseRoots();
     } else {
       clearSessionCache = false;
     }
     if (clearSessionCache)
       SSL_ClearSessionCache();
   }
 
   return NS_OK;
--- a/security/manager/ssl/nsNSSComponent.h
+++ b/security/manager/ssl/nsNSSComponent.h
@@ -2,28 +2,35 @@
  *
  * 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/. */
 
 #ifndef _nsNSSComponent_h_
 #define _nsNSSComponent_h_
 
+#include "ScopedNSSTypes.h"
+#include "SharedCertVerifier.h"
 #include "mozilla/Mutex.h"
 #include "mozilla/RefPtr.h"
 #include "nsCOMPtr.h"
+#include "nsIObserver.h"
 #include "nsIStringBundle.h"
-#include "nsIObserver.h"
 #include "nsNSSCallbacks.h"
-#include "SharedCertVerifier.h"
 #include "prerror.h"
 #include "sslt.h"
 
+#ifdef XP_WIN
+#include "windows.h" // this needs to be before the following includes
+#include "wincrypt.h"
+#endif // XP_WIN
+
 class nsIDOMWindow;
 class nsIPrompt;
+class nsIX509CertList;
 class SmartCardThreadList;
 
 namespace mozilla { namespace psm {
 
 MOZ_MUST_USE
   ::already_AddRefed<mozilla::psm::SharedCertVerifier>
   GetDefaultCertVerifier();
 
@@ -82,16 +89,20 @@ public:
   NS_IMETHOD IsNSSInitialized(bool* initialized) = 0;
 
 #ifdef DEBUG
   NS_IMETHOD IsCertTestBuiltInRoot(CERTCertificate* cert, bool& result) = 0;
 #endif
 
   NS_IMETHOD IsCertContentSigningRoot(CERTCertificate* cert, bool& result) = 0;
 
+#ifdef XP_WIN
+  NS_IMETHOD GetEnterpriseRoots(nsIX509CertList** enterpriseRoots) = 0;
+#endif
+
   virtual ::already_AddRefed<mozilla::psm::SharedCertVerifier>
     GetDefaultCertVerifier() = 0;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsINSSComponent, NS_INSSCOMPONENT_IID)
 
 class nsNSSShutDownList;
 class nsCertVerificationThread;
@@ -136,16 +147,20 @@ public:
   NS_IMETHOD IsNSSInitialized(bool* initialized) override;
 
 #ifdef DEBUG
   NS_IMETHOD IsCertTestBuiltInRoot(CERTCertificate* cert, bool& result) override;
 #endif
 
   NS_IMETHOD IsCertContentSigningRoot(CERTCertificate* cert, bool& result) override;
 
+#ifdef XP_WIN
+  NS_IMETHOD GetEnterpriseRoots(nsIX509CertList** enterpriseRoots) override;
+#endif
+
   ::already_AddRefed<mozilla::psm::SharedCertVerifier>
     GetDefaultCertVerifier() override;
 
   // The following two methods are thread-safe.
   static bool AreAnyWeakCiphersEnabled();
   static void UseWeakCiphersOnSocket(PRFileDesc* fd);
 
   static void FillTLSVersionRange(SSLVersionRange& rangeOut,
@@ -166,16 +181,30 @@ private:
                             const mozilla::MutexAutoLock& lock);
   nsresult setEnabledTLSVersions();
   nsresult InitializePIPNSSBundle();
   nsresult ConfigureInternalPKCS11Token();
   nsresult RegisterObservers();
 
   void DoProfileBeforeChange();
 
+  void MaybeEnableFamilySafetyCompatibility();
+  void MaybeImportEnterpriseRoots();
+#ifdef XP_WIN
+  nsresult MaybeImportFamilySafetyRoot(PCCERT_CONTEXT certificate,
+                                       bool& wasFamilySafetyRoot);
+  nsresult LoadFamilySafetyRoot();
+  void UnloadFamilySafetyRoot();
+
+  void UnloadEnterpriseRoots();
+
+  mozilla::UniqueCERTCertificate mFamilySafetyRoot;
+  mozilla::UniqueCERTCertList mEnterpriseRoots;
+#endif // XP_WIN
+
   mozilla::Mutex mutex;
 
   nsCOMPtr<nsIStringBundle> mPIPNSSBundle;
   nsCOMPtr<nsIStringBundle> mNSSErrorsBundle;
   bool mNSSInitialized;
   static int mInstanceCount;
 #ifndef MOZ_NO_SMART_CARDS
   SmartCardThreadList* mThreadList;
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_enterprise_roots.js
@@ -0,0 +1,58 @@
+// -*- 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/.
+
+"use strict";
+
+// Tests enterprise root certificate support. When configured to do so, the
+// platform will attempt to find and import enterprise root certificates. This
+// feature is specific to Windows.
+
+do_get_profile(); // must be called before getting nsIX509CertDB
+
+function check_no_enterprise_roots_imported(certDB, dbKey = undefined) {
+  let enterpriseRoots = certDB.getEnterpriseRoots();
+  equal(enterpriseRoots, null, "should not have imported any enterprise roots");
+  if (dbKey) {
+    let cert = certDB.findCertByDBKey(dbKey);
+    // If the garbage-collector hasn't run, there may be reachable copies of
+    // imported enterprise root certificates. If so, they shouldn't be trusted
+    // to issue TLS server auth certificates.
+    if (cert) {
+      ok(!certDB.isCertTrusted(cert, Ci.nsIX509Cert.CA_CERT,
+                               Ci.nsIX509CertDB.TRUSTED_SSL),
+         "previously-imported enterprise root shouldn't be trusted to issue " +
+         "TLS server auth certificates");
+    }
+  }
+}
+
+function check_some_enterprise_roots_imported(certDB) {
+  let enterpriseRoots = certDB.getEnterpriseRoots();
+  notEqual(enterpriseRoots, null, "should have imported some enterprise roots");
+  let enumerator = enterpriseRoots.getEnumerator();
+  let foundNonBuiltIn = false;
+  let savedDBKey = null;
+  while (enumerator.hasMoreElements()) {
+    let cert = enumerator.getNext().QueryInterface(Ci.nsIX509Cert);
+    if (!cert.isBuiltInRoot && !savedDBKey) {
+      foundNonBuiltIn = true;
+      savedDBKey = cert.dbKey;
+      do_print("saving dbKey from " + cert.commonName);
+    }
+  }
+  ok(foundNonBuiltIn, "should have found non-built-in root");
+  return savedDBKey;
+}
+
+function run_test() {
+  let certDB = Cc["@mozilla.org/security/x509certdb;1"]
+                 .getService(Ci.nsIX509CertDB);
+  Services.prefs.setBoolPref("security.enterprise_roots.enabled", false);
+  check_no_enterprise_roots_imported(certDB);
+  Services.prefs.setBoolPref("security.enterprise_roots.enabled", true);
+  let savedDBKey = check_some_enterprise_roots_imported(certDB);
+  Services.prefs.setBoolPref("security.enterprise_roots.enabled", false);
+  check_no_enterprise_roots_imported(certDB, savedDBKey);
+}
--- a/security/manager/ssl/tests/unit/xpcshell.ini
+++ b/security/manager/ssl/tests/unit/xpcshell.ini
@@ -52,16 +52,18 @@ run-sequentially = hardcoded ports
 [test_cert_version.js]
 [test_certDB_import.js]
 [test_certDB_import_pkcs12.js]
 [test_certviewer_invalid_oids.js]
 skip-if = toolkit == 'android' || buildapp == 'b2g'
 [test_constructX509FromBase64.js]
 [test_content_signing.js]
 [test_datasignatureverifier.js]
+[test_enterprise_roots.js]
+skip-if = os != 'win' # tests a Windows-specific feature
 [test_ev_certs.js]
 run-sequentially = hardcoded ports
 [test_getchain.js]
 [test_hash_algorithms.js]
 [test_hash_algorithms_wrap.js]
 # bug 1124289 - run_test_in_child violates the sandbox on b2g and android
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 [test_hmac.js]