bug 1265113 - implement platform support for enterprise roots r=Cykesiopka,mhowell,rbarnes
authorDavid Keeler <dkeeler@mozilla.com>
Wed, 13 Apr 2016 15:36:22 -0700
changeset 338223 d9659c22b3c5
parent 338222 20c1a4ec995f
child 338224 4715ad5e6035
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersCykesiopka, mhowell, rbarnes
bugs1265113
milestone49.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 1265113 - implement platform support for enterprise roots r=Cykesiopka,mhowell,rbarnes 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);
 
@@ -1652,16 +1869,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();
     // TLSServerSocket may be run with the session cache enabled. This ensures
     // those resources are cleaned up.
     Unused << SSL_ShutdownServerSessionIDCache();
     UnloadLoadableRoots();
@@ -1795,16 +2017,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]