diff options
Diffstat (limited to '')
-rw-r--r-- | security/manager/ssl/EnterpriseRoots.cpp | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/security/manager/ssl/EnterpriseRoots.cpp b/security/manager/ssl/EnterpriseRoots.cpp new file mode 100644 index 0000000000..b54233b7cd --- /dev/null +++ b/security/manager/ssl/EnterpriseRoots.cpp @@ -0,0 +1,373 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "EnterpriseRoots.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Logging.h" +#include "mozilla/Unused.h" +#include "mozpkix/Result.h" +#include "nsThreadUtils.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/java/EnterpriseRootsWrappers.h" +#endif // MOZ_WIDGET_ANDROID + +#ifdef XP_MACOSX +# include <Security/Security.h> +# include "KeychainSecret.h" // for ScopedCFType + +# include "nsCocoaFeatures.h" +#endif // XP_MACOSX + +#ifdef XP_WIN +# include <windows.h> +# include <wincrypt.h> +#endif // XP_WIN + +extern mozilla::LazyLogModule gPIPNSSLog; + +using namespace mozilla; + +nsresult EnterpriseCert::Init(const uint8_t* data, size_t len, bool isRoot) { + mDER.clear(); + if (!mDER.append(data, len)) { + return NS_ERROR_OUT_OF_MEMORY; + } + mIsRoot = isRoot; + + return NS_OK; +} + +nsresult EnterpriseCert::Init(const EnterpriseCert& orig) { + return Init(orig.mDER.begin(), orig.mDER.length(), orig.mIsRoot); +} + +nsresult EnterpriseCert::CopyBytes(nsTArray<uint8_t>& dest) const { + dest.Clear(); + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + dest.AppendElements(mDER.begin(), mDER.length()); + return NS_OK; +} + +pkix::Result EnterpriseCert::GetInput(pkix::Input& input) const { + return input.Init(mDER.begin(), mDER.length()); +} + +bool EnterpriseCert::GetIsRoot() const { return mIsRoot; } + +#ifdef XP_WIN +const wchar_t* kWindowsDefaultRootStoreNames[] = {L"ROOT", L"CA"}; + +// 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 void CertIsTrustAnchorForTLSServerAuth(PCCERT_CONTEXT certificate, + bool& isTrusted, bool& isRoot) { + isTrusted = false; + isRoot = false; + MOZ_ASSERT(certificate); + if (!certificate) { + return; + } + + PCCERT_CHAIN_CONTEXT pChainContext = nullptr; + CERT_ENHKEY_USAGE enhkeyUsage; + memset(&enhkeyUsage, 0, sizeof(CERT_ENHKEY_USAGE)); + LPCSTR identifiers[] = { + "1.3.6.1.5.5.7.3.1", // id-kp-serverAuth + }; + enhkeyUsage.cUsageIdentifier = ArrayLength(identifiers); + enhkeyUsage.rgpszUsageIdentifier = + const_cast<LPSTR*>(identifiers); // -Wwritable-strings + 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; + // Disable anything that could result in network I/O. + DWORD flags = CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY | + CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL | + CERT_CHAIN_DISABLE_AUTH_ROOT_AUTO_UPDATE | + CERT_CHAIN_DISABLE_AIA; + if (!CertGetCertificateChain(nullptr, certificate, nullptr, nullptr, + &chainPara, flags, nullptr, &pChainContext)) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("CertGetCertificateChain failed")); + return; + } + isTrusted = pChainContext->TrustStatus.dwErrorStatus == CERT_TRUST_NO_ERROR; + if (isTrusted && pChainContext->cChain > 0) { + // The so-called "final chain" is what we're after: + // https://docs.microsoft.com/en-us/windows/desktop/api/wincrypt/ns-wincrypt-_cert_chain_context + CERT_SIMPLE_CHAIN* finalChain = + pChainContext->rgpChain[pChainContext->cChain - 1]; + // This is a root if the final chain consists of only one certificate (i.e. + // this one). + isRoot = finalChain->cElement == 1; + } + CertFreeCertificateChain(pChainContext); +} + +// Because HCERTSTORE is just a typedef void*, we can't use any of the nice +// 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); } + + HCERTSTORE get() { return certstore; } + + private: + ScopedCertStore(const ScopedCertStore&) = delete; + ScopedCertStore& operator=(const ScopedCertStore&) = delete; + HCERTSTORE certstore; +}; + +// Loads the enterprise roots at the registry location corresponding to the +// given location flag. +// Supported flags are: +// CERT_SYSTEM_STORE_LOCAL_MACHINE +// (for HKLM\SOFTWARE\Microsoft\SystemCertificates) +// CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY +// (for HKLM\SOFTWARE\Policy\Microsoft\SystemCertificates) +// CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE +// (for HKLM\SOFTWARE\Microsoft\EnterpriseCertificates) +// CERT_SYSTEM_STORE_CURRENT_USER +// (for HKCU\SOFTWARE\Microsoft\SystemCertificates) +// CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY +// (for HKCU\SOFTWARE\Policy\Microsoft\SystemCertificates) +static void GatherEnterpriseCertsForLocation(DWORD locationFlag, + Vector<EnterpriseCert>& certs) { + MOZ_ASSERT(locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE || + locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY || + locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE || + locationFlag == CERT_SYSTEM_STORE_CURRENT_USER || + locationFlag == CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY, + "unexpected locationFlag for GatherEnterpriseRootsForLocation"); + if (!(locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE || + locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY || + locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE || + locationFlag == CERT_SYSTEM_STORE_CURRENT_USER || + locationFlag == CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY)) { + return; + } + + DWORD flags = + locationFlag | 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 + for (auto name : kWindowsDefaultRootStoreNames) { + ScopedCertStore enterpriseRootStore( + CertOpenStore(CERT_STORE_PROV_SYSTEM_REGISTRY_W, 0, NULL, flags, name)); + if (!enterpriseRootStore.get()) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("failed to open enterprise root store")); + continue; + } + PCCERT_CONTEXT certificate = nullptr; + uint32_t numImported = 0; + while ((certificate = CertFindCertificateInStore( + enterpriseRootStore.get(), X509_ASN_ENCODING, 0, CERT_FIND_ANY, + nullptr, certificate))) { + bool isTrusted; + bool isRoot; + CertIsTrustAnchorForTLSServerAuth(certificate, isTrusted, isRoot); + if (!isTrusted) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("skipping cert not trusted for TLS server auth")); + continue; + } + EnterpriseCert enterpriseCert; + if (NS_FAILED(enterpriseCert.Init(certificate->pbCertEncoded, + certificate->cbCertEncoded, isRoot))) { + // Best-effort. We probably ran out of memory. + continue; + } + if (!certs.append(std::move(enterpriseCert))) { + // Best-effort again. + continue; + } + numImported++; + } + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("imported %u certs from %S", numImported, name)); + } +} + +static void GatherEnterpriseCertsWindows(Vector<EnterpriseCert>& certs) { + GatherEnterpriseCertsForLocation(CERT_SYSTEM_STORE_LOCAL_MACHINE, certs); + GatherEnterpriseCertsForLocation(CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY, + certs); + GatherEnterpriseCertsForLocation(CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE, + certs); + GatherEnterpriseCertsForLocation(CERT_SYSTEM_STORE_CURRENT_USER, certs); + GatherEnterpriseCertsForLocation(CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY, + certs); +} +#endif // XP_WIN + +#ifdef XP_MACOSX +OSStatus GatherEnterpriseCertsMacOS(Vector<EnterpriseCert>& certs) { + // The following builds a search dictionary corresponding to: + // { class: "certificate", + // match limit: "match all", + // policy: "SSL (TLS)", + // only include trusted certificates: true } + // This operates on items that have been added to the keychain and thus gives + // us all 3rd party certificates that have been trusted for SSL (TLS), which + // is what we want (thus we don't import built-in root certificates that ship + // with the OS). + const CFStringRef keys[] = {kSecClass, kSecMatchLimit, kSecMatchPolicy, + kSecMatchTrustedOnly}; + // https://developer.apple.com/documentation/security/1392592-secpolicycreatessl + ScopedCFType<SecPolicyRef> sslPolicy(SecPolicyCreateSSL(true, nullptr)); + const void* values[] = {kSecClassCertificate, kSecMatchLimitAll, + sslPolicy.get(), kCFBooleanTrue}; + static_assert(ArrayLength(keys) == ArrayLength(values), + "mismatched SecItemCopyMatching key/value array sizes"); + // https://developer.apple.com/documentation/corefoundation/1516782-cfdictionarycreate + ScopedCFType<CFDictionaryRef> searchDictionary(CFDictionaryCreate( + nullptr, (const void**)&keys, (const void**)&values, ArrayLength(keys), + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); + CFTypeRef items; + // https://developer.apple.com/documentation/security/1398306-secitemcopymatching + OSStatus rv = SecItemCopyMatching(searchDictionary.get(), &items); + if (rv != errSecSuccess) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("SecItemCopyMatching failed")); + return rv; + } + // If given a match limit greater than 1 (which we did), SecItemCopyMatching + // returns a CFArrayRef. + ScopedCFType<CFArrayRef> arr(reinterpret_cast<CFArrayRef>(items)); + CFIndex count = CFArrayGetCount(arr.get()); + uint32_t numImported = 0; + for (CFIndex i = 0; i < count; i++) { + const CFTypeRef c = CFArrayGetValueAtIndex(arr.get(), i); + SecTrustRef trust; + rv = SecTrustCreateWithCertificates(c, sslPolicy.get(), &trust); + if (rv != errSecSuccess) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("SecTrustCreateWithCertificates failed")); + continue; + } + ScopedCFType<SecTrustRef> trustHandle(trust); + // Disable AIA chasing to avoid network I/O. + rv = SecTrustSetNetworkFetchAllowed(trustHandle.get(), false); + if (rv != errSecSuccess) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("SecTrustSetNetworkFetchAllowed failed")); + continue; + } + bool isTrusted = false; + bool fallBackToDeprecatedAPI = true; + if (nsCocoaFeatures::OnMojaveOrLater()) { + // This is an awkward way to express what we want, but the compiler + // complains if we try to put __builtin_available in a compound logical + // statement. + if (__builtin_available(macOS 10.14, *)) { + isTrusted = SecTrustEvaluateWithError(trustHandle.get(), nullptr); + fallBackToDeprecatedAPI = false; + } + } + if (fallBackToDeprecatedAPI) { + SecTrustResultType result; + rv = SecTrustEvaluate(trustHandle.get(), &result); + if (rv != errSecSuccess) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("SecTrustEvaluate failed")); + continue; + } + // 'kSecTrustResultProceed' means the trust evaluation succeeded and that + // this is a trusted certificate. + // 'kSecTrustResultUnspecified' means the trust evaluation succeeded and + // that this certificate inherits its trust. + isTrusted = result == kSecTrustResultProceed || + result == kSecTrustResultUnspecified; + } + if (!isTrusted) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("skipping cert not trusted")); + continue; + } + CFIndex count = SecTrustGetCertificateCount(trustHandle.get()); + bool isRoot = count == 1; + + // Because we asked for certificates, each CFTypeRef in the array is really + // a SecCertificateRef. + const SecCertificateRef s = (const SecCertificateRef)c; + ScopedCFType<CFDataRef> der(SecCertificateCopyData(s)); + EnterpriseCert enterpriseCert; + if (NS_FAILED(enterpriseCert.Init(CFDataGetBytePtr(der.get()), + CFDataGetLength(der.get()), isRoot))) { + // Best-effort. We probably ran out of memory. + continue; + } + if (!certs.append(std::move(enterpriseCert))) { + // Best-effort again. + continue; + } + numImported++; + } + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("imported %u certs", numImported)); + return errSecSuccess; +} +#endif // XP_MACOSX + +#ifdef MOZ_WIDGET_ANDROID +void GatherEnterpriseCertsAndroid(Vector<EnterpriseCert>& certs) { + if (!jni::IsAvailable()) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("JNI not available")); + return; + } + jni::ObjectArray::LocalRef roots = + java::EnterpriseRoots::GatherEnterpriseRoots(); + for (size_t i = 0; i < roots->Length(); i++) { + jni::ByteArray::LocalRef root = roots->GetElement(i); + EnterpriseCert cert; + // Currently we treat all certificates gleaned from the Android + // CA store as roots. + if (NS_SUCCEEDED(cert.Init( + reinterpret_cast<uint8_t*>(root->GetElements().Elements()), + root->Length(), true))) { + Unused << certs.append(std::move(cert)); + } + } +} +#endif // MOZ_WIDGET_ANDROID + +nsresult GatherEnterpriseCerts(Vector<EnterpriseCert>& certs) { + MOZ_ASSERT(!NS_IsMainThread()); + if (NS_IsMainThread()) { + return NS_ERROR_NOT_SAME_THREAD; + } + + certs.clear(); +#ifdef XP_WIN + GatherEnterpriseCertsWindows(certs); +#endif // XP_WIN +#ifdef XP_MACOSX + OSStatus rv = GatherEnterpriseCertsMacOS(certs); + if (rv != errSecSuccess) { + return NS_ERROR_FAILURE; + } +#endif // XP_MACOSX +#ifdef MOZ_WIDGET_ANDROID + GatherEnterpriseCertsAndroid(certs); +#endif // MOZ_WIDGET_ANDROID + return NS_OK; +} |