/* -*- 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 "nsClientAuthRemember.h" #include "mozilla/BasePrincipal.h" #include "mozilla/DataStorage.h" #include "mozilla/RefPtr.h" #include "nsCRT.h" #include "nsINSSComponent.h" #include "nsPrintfCString.h" #include "nsNSSComponent.h" #include "nsIObserverService.h" #include "nsNetUtil.h" #include "nsPromiseFlatString.h" #include "nsThreadUtils.h" #include "nsStringBuffer.h" #include "cert.h" #include "nspr.h" #include "pk11pub.h" #include "certdb.h" #include "sechash.h" #include "SharedSSLState.h" #include "nsJSUtils.h" #ifdef XP_MACOSX # include # include # include "KeychainSecret.h" // for ScopedCFType #endif // XP_MACOSX using namespace mozilla; using namespace mozilla::psm; NS_IMPL_ISUPPORTS(nsClientAuthRememberService, nsIClientAuthRememberService) NS_IMPL_ISUPPORTS(nsClientAuthRemember, nsIClientAuthRememberRecord) const nsCString nsClientAuthRemember::SentinelValue = "no client certificate"_ns; NS_IMETHODIMP nsClientAuthRemember::GetAsciiHost(/*out*/ nsACString& aAsciiHost) { aAsciiHost = mAsciiHost; return NS_OK; } NS_IMETHODIMP nsClientAuthRemember::GetDbKey(/*out*/ nsACString& aDBKey) { aDBKey = mDBKey; return NS_OK; } NS_IMETHODIMP nsClientAuthRemember::GetEntryKey(/*out*/ nsACString& aEntryKey) { aEntryKey.Assign(mAsciiHost); aEntryKey.Append(','); // This used to include the SHA-256 hash of the server certificate. aEntryKey.Append(','); aEntryKey.Append(mOriginAttributesSuffix); return NS_OK; } nsresult nsClientAuthRememberService::Init() { if (!NS_IsMainThread()) { NS_ERROR("nsClientAuthRememberService::Init called off the main thread"); return NS_ERROR_NOT_SAME_THREAD; } mClientAuthRememberList = mozilla::DataStorage::Get(DataStorageClass::ClientAuthRememberList); nsresult rv = mClientAuthRememberList->Init(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } NS_IMETHODIMP nsClientAuthRememberService::ForgetRememberedDecision(const nsACString& key) { mClientAuthRememberList->Remove(PromiseFlatCString(key), mozilla::DataStorage_Persistent); nsCOMPtr nssComponent(do_GetService(NS_NSSCOMPONENT_CID)); if (!nssComponent) { return NS_ERROR_NOT_AVAILABLE; } return nssComponent->ClearSSLExternalAndInternalSessionCache(); } NS_IMETHODIMP nsClientAuthRememberService::GetDecisions( nsTArray>& results) { nsTArray decisions; mClientAuthRememberList->GetAll(&decisions); for (const DataStorageItem& decision : decisions) { if (decision.type == DataStorageType::DataStorage_Persistent) { RefPtr tmp = new nsClientAuthRemember(decision.key, decision.value); results.AppendElement(tmp); } } return NS_OK; } NS_IMETHODIMP nsClientAuthRememberService::ClearRememberedDecisions() { mClientAuthRememberList->Clear(); nsCOMPtr nssComponent(do_GetService(NS_NSSCOMPONENT_CID)); if (!nssComponent) { return NS_ERROR_NOT_AVAILABLE; } return nssComponent->ClearSSLExternalAndInternalSessionCache(); } NS_IMETHODIMP nsClientAuthRememberService::DeleteDecisionsByHost( const nsACString& aHostName, JS::Handle aOriginAttributes, JSContext* aCx) { OriginAttributes attrs; if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } DataStorageType storageType = GetDataStorageType(attrs); nsTArray decisions; mClientAuthRememberList->GetAll(&decisions); for (const DataStorageItem& decision : decisions) { if (decision.type == storageType) { RefPtr tmp = new nsClientAuthRemember(decision.key, decision.value); nsAutoCString asciiHost; tmp->GetAsciiHost(asciiHost); if (asciiHost.Equals(aHostName)) { mClientAuthRememberList->Remove(decision.key, decision.type); } } } nsCOMPtr nssComponent(do_GetService(NS_NSSCOMPONENT_CID)); if (!nssComponent) { return NS_ERROR_NOT_AVAILABLE; } return nssComponent->ClearSSLExternalAndInternalSessionCache(); } NS_IMETHODIMP nsClientAuthRememberService::RememberDecisionScriptable( const nsACString& aHostName, JS::Handle aOriginAttributes, nsIX509Cert* aClientCert, JSContext* aCx) { OriginAttributes attrs; if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } return RememberDecision(aHostName, attrs, aClientCert); } NS_IMETHODIMP nsClientAuthRememberService::RememberDecision( const nsACString& aHostName, const OriginAttributes& aOriginAttributes, nsIX509Cert* aClientCert) { if (aHostName.IsEmpty()) { return NS_ERROR_INVALID_ARG; } // aClientCert == nullptr means: remember that user does not want to use a // cert if (aClientCert) { nsAutoCString dbkey; nsresult rv = aClientCert->GetDbKey(dbkey); if (NS_FAILED(rv)) { return rv; } return AddEntryToList(aHostName, aOriginAttributes, dbkey); } return AddEntryToList(aHostName, aOriginAttributes, nsClientAuthRemember::SentinelValue); } #ifdef XP_MACOSX // On macOS, users can add "identity preference" items in the keychain. These // can be added via the Keychain Access tool. These specify mappings from // URLs/wildcards like "*.mozilla.org" to specific client certificates. This // function retrieves the preferred client certificate for a hostname by // querying a system API that checks for these identity preferences. nsresult CheckForPreferredCertificate(const nsACString& aHostName, nsACString& aCertDBKey) { aCertDBKey.Truncate(); // SecIdentityCopyPreferred seems to expect a proper URI which it can use // for prefix and wildcard matches. // We don't have the full URL but we can turn the hostname into a URI with // an authority section, so that it matches against macOS identity preferences // like `*.foo.com`. If we know that this connection is always going to be // https, then we should put that in the URI as well, so that it matches // identity preferences like `https://foo.com/` as well. If we can plumb // the path or the full URL into this function we could also match identity // preferences like `https://foo.com/bar/` but for now we cannot. nsPrintfCString fakeUrl("//%s/", PromiseFlatCString(aHostName).get()); ScopedCFType host(::CFStringCreateWithCString( kCFAllocatorDefault, fakeUrl.get(), kCFStringEncodingUTF8)); if (!host) { return NS_ERROR_UNEXPECTED; } ScopedCFType identity( ::SecIdentityCopyPreferred(host.get(), NULL, NULL)); if (!identity) { // No preferred identity for this hostname, leave aCertDBKey empty and // return return NS_OK; } SecCertificateRef certRefRaw = NULL; OSStatus copyResult = ::SecIdentityCopyCertificate(identity.get(), &certRefRaw); ScopedCFType certRef(certRefRaw); if (copyResult != errSecSuccess || certRef.get() == NULL) { return NS_ERROR_UNEXPECTED; } ScopedCFType der(::SecCertificateCopyData(certRef.get())); if (!der) { return NS_ERROR_UNEXPECTED; } nsTArray derArray(::CFDataGetBytePtr(der.get()), ::CFDataGetLength(der.get())); nsCOMPtr cert(new nsNSSCertificate(std::move(derArray))); return cert->GetDbKey(aCertDBKey); } #endif void nsClientAuthRememberService::Migrate() { MOZ_ASSERT(NS_IsMainThread()); static bool migrated = false; if (migrated) { return; } nsTArray decisions; mClientAuthRememberList->GetAll(&decisions); for (const auto& decision : decisions) { if (decision.type != DataStorage_Persistent) { continue; } RefPtr entry( new nsClientAuthRemember(decision.key, decision.value)); nsAutoCString newKey; if (NS_FAILED(entry->GetEntryKey(newKey))) { continue; } if (newKey != decision.key) { mClientAuthRememberList->Remove(decision.key, DataStorage_Persistent); (void)mClientAuthRememberList->Put(newKey, decision.value, DataStorage_Persistent); } } migrated = true; } NS_IMETHODIMP nsClientAuthRememberService::HasRememberedDecision( const nsACString& aHostName, const OriginAttributes& aOriginAttributes, nsACString& aCertDBKey, bool* aRetVal) { NS_ENSURE_ARG_POINTER(aRetVal); if (aHostName.IsEmpty()) { return NS_ERROR_INVALID_ARG; } if (!NS_IsMainThread()) { return NS_ERROR_NOT_SAME_THREAD; } *aRetVal = false; aCertDBKey.Truncate(); Migrate(); nsAutoCString entryKey; RefPtr entry( new nsClientAuthRemember(aHostName, aOriginAttributes)); nsresult rv = entry->GetEntryKey(entryKey); if (NS_FAILED(rv)) { return rv; } DataStorageType storageType = GetDataStorageType(aOriginAttributes); nsCString listEntry = mClientAuthRememberList->Get(entryKey, storageType); if (!listEntry.IsEmpty()) { if (!listEntry.Equals(nsClientAuthRemember::SentinelValue)) { aCertDBKey = listEntry; } *aRetVal = true; return NS_OK; } #ifdef XP_MACOSX rv = CheckForPreferredCertificate(aHostName, aCertDBKey); if (NS_FAILED(rv)) { return rv; } if (!aCertDBKey.IsEmpty()) { *aRetVal = true; return NS_OK; } #endif return NS_OK; } NS_IMETHODIMP nsClientAuthRememberService::HasRememberedDecisionScriptable( const nsACString& aHostName, JS::Handle aOriginAttributes, nsACString& aCertDBKey, JSContext* aCx, bool* aRetVal) { OriginAttributes attrs; if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } return HasRememberedDecision(aHostName, attrs, aCertDBKey, aRetVal); } nsresult nsClientAuthRememberService::AddEntryToList( const nsACString& aHostName, const OriginAttributes& aOriginAttributes, const nsACString& aDBKey) { nsAutoCString entryKey; RefPtr entry( new nsClientAuthRemember(aHostName, aOriginAttributes)); nsresult rv = entry->GetEntryKey(entryKey); if (NS_FAILED(rv)) { return rv; } DataStorageType storageType = GetDataStorageType(aOriginAttributes); nsCString tmpDbKey(aDBKey); rv = mClientAuthRememberList->Put(entryKey, tmpDbKey, storageType); if (NS_FAILED(rv)) { return rv; } return NS_OK; } bool nsClientAuthRememberService::IsPrivateBrowsingKey( const nsCString& entryKey) { const int32_t separator = entryKey.Find(":"); nsCString suffix; if (separator >= 0) { entryKey.Left(suffix, separator); } else { suffix = entryKey; } return OriginAttributes::IsPrivateBrowsing(suffix); } DataStorageType nsClientAuthRememberService::GetDataStorageType( const OriginAttributes& aOriginAttributes) { if (aOriginAttributes.mPrivateBrowsingId > 0) { return DataStorage_Private; } return DataStorage_Persistent; }