/* * 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 "SSLTokensCache.h" #include "mozilla/BasePrincipal.h" #include "mozilla/RefPtr.h" #include "nsCRT.h" #include "nsIDataStorage.h" #include "nsINSSComponent.h" #include "nsIObserverService.h" #include "nsJSUtils.h" #include "nsNSSComponent.h" #include "nsNetUtil.h" #include "nsPrintfCString.h" #include "nsPromiseFlatString.h" #include "nsThreadUtils.h" #include "nspr.h" #include "pk11pub.h" #include "sechash.h" #ifdef XP_MACOSX # include # include # include "KeychainSecret.h" // for ScopedCFType #endif using namespace mozilla; using namespace mozilla::psm; NS_IMPL_ISUPPORTS(nsClientAuthRememberService, nsIClientAuthRememberService) // Builds a pattern that matches exactly aAttrs (every field // OriginAttributesPattern::Matches checks), so eviction is scoped to the // affected origin attributes rather than every OA sharing the same // privateBrowsingId. static OriginAttributesPattern PatternForExactOA( const OriginAttributes& aAttrs) { OriginAttributesPattern pattern; pattern.mPrivateBrowsingId.Construct(aAttrs.mPrivateBrowsingId); pattern.mUserContextId.Construct(aAttrs.mUserContextId); pattern.mFirstPartyDomain.Construct(aAttrs.mFirstPartyDomain); pattern.mGeckoViewSessionContextId.Construct( aAttrs.mGeckoViewSessionContextId); pattern.mPartitionKey.Construct(aAttrs.mPartitionKey); return pattern; } NS_IMPL_ISUPPORTS(nsClientAuthRemember, nsIClientAuthRememberRecord) 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; } nsCOMPtr dataStorageManager( do_GetService("@mozilla.org/security/datastoragemanager;1")); if (!dataStorageManager) { return NS_ERROR_FAILURE; } nsresult rv = dataStorageManager->Get(nsIDataStorageManager::ClientAuthRememberList, getter_AddRefs(mClientAuthRememberList)); if (NS_FAILED(rv)) { return rv; } if (!mClientAuthRememberList) { return NS_ERROR_FAILURE; } return NS_OK; } NS_IMETHODIMP nsClientAuthRememberService::ForgetRememberedDecision(const nsACString& key) { nsresult rv = mClientAuthRememberList->Remove( PromiseFlatCString(key), nsIDataStorage::DataType::Persistent); if (NS_FAILED(rv)) { return rv; } rv = mClientAuthRememberList->Remove(PromiseFlatCString(key), nsIDataStorage::DataType::Temporary); if (NS_FAILED(rv)) { return rv; } // The client-auth entry key has the format "host,,". nsAutoCString host(key); int32_t firstComma = host.FindChar(','); OriginAttributesPattern pattern; if (firstComma != kNotFound) { // OA suffix starts two characters after the first comma (skip ",,"). OriginAttributes oa; nsDependentCSubstring oaSuffix = Substring(key, firstComma + 2); if (!oaSuffix.IsEmpty()) { (void)oa.PopulateFromSuffix(oaSuffix); } pattern = PatternForExactOA(oa); host.Truncate(firstComma); } if (!host.IsEmpty()) { mozilla::net::SSLTokensCache::ClearSessionCacheAndTokensForHost(host, pattern); } return NS_OK; } NS_IMETHODIMP nsClientAuthRememberService::GetDecisions( nsTArray>& results) { nsTArray> decisions; nsresult rv = mClientAuthRememberList->GetAll(decisions); if (NS_FAILED(rv)) { return rv; } for (const auto& decision : decisions) { nsIDataStorage::DataType type; rv = decision->GetType(&type); if (NS_FAILED(rv)) { return rv; } if (type == nsIDataStorage::DataType::Persistent || type == nsIDataStorage::DataType::Temporary) { nsAutoCString key; rv = decision->GetKey(key); if (NS_FAILED(rv)) { return rv; } nsAutoCString value; rv = decision->GetValue(value); if (NS_FAILED(rv)) { return rv; } RefPtr tmp = MakeRefPtr(key, value); results.AppendElement(std::move(tmp)); } } return NS_OK; } NS_IMETHODIMP nsClientAuthRememberService::ClearRememberedDecisions() { nsresult rv = mClientAuthRememberList->Clear(); if (NS_FAILED(rv)) { return rv; } mozilla::net::SSLTokensCache::ClearSessionCacheAndTokens(); return NS_OK; } 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; } bool isPrivateContext = attrs.IsPrivateBrowsing(); nsTArray> decisions; nsresult rv = mClientAuthRememberList->GetAll(decisions); if (NS_FAILED(rv)) { return rv; } for (const auto& decision : decisions) { nsIDataStorage::DataType type; nsresult rv = decision->GetType(&type); if (NS_FAILED(rv)) { return rv; } bool isPrivateDecision = type == nsIDataStorage::DataType::Private; if (isPrivateContext == isPrivateDecision) { nsAutoCString key; rv = decision->GetKey(key); if (NS_FAILED(rv)) { return rv; } nsAutoCString value; rv = decision->GetValue(value); if (NS_FAILED(rv)) { return rv; } RefPtr tmp = MakeRefPtr(key, value); nsAutoCString asciiHost; tmp->GetAsciiHost(asciiHost); if (asciiHost.Equals(aHostName)) { rv = mClientAuthRememberList->Remove(key, type); if (NS_FAILED(rv)) { return rv; } } } } OriginAttributesPattern pattern; pattern.mPrivateBrowsingId.Construct(attrs.mPrivateBrowsingId); mozilla::net::SSLTokensCache::ClearSessionCacheAndTokensForHost(aHostName, pattern); return NS_OK; } NS_IMETHODIMP nsClientAuthRememberService::RememberDecisionScriptable( const nsACString& aHostName, JS::Handle aOriginAttributes, nsIX509Cert* aClientCert, Duration aDuration, JSContext* aCx) { OriginAttributes attrs; if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } return RememberDecision(aHostName, attrs, aClientCert, aDuration); } NS_IMETHODIMP nsClientAuthRememberService::RememberDecision( const nsACString& aHostName, const OriginAttributes& aOriginAttributes, nsIX509Cert* aClientCert, Duration aDuration) { if (aHostName.IsEmpty()) { return NS_ERROR_INVALID_ARG; } // If a decision is to only be used once, it doesn't need to be remembered in // any way. if (aDuration == nsIClientAuthRememberService::Duration::Once) { return NS_OK; } // 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, aDuration); } return AddEntryToList(aHostName, aOriginAttributes, nsClientAuthRemember::SentinelValue, aDuration); } #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(), nullptr, nullptr)); if (!identity) { // No preferred identity for this hostname, leave aCertDBKey empty and // return return NS_OK; } SecCertificateRef certRefRaw = nullptr; OSStatus copyResult = ::SecIdentityCopyCertificate(identity.get(), &certRefRaw); ScopedCFType certRef(certRefRaw); if (copyResult != errSecSuccess || certRef.get() == nullptr) { 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() { auto migrated = mMigrated.Lock(); if (*migrated) { return; } *migrated = true; nsTArray> decisions; nsresult rv = mClientAuthRememberList->GetAll(decisions); if (NS_FAILED(rv)) { return; } for (const auto& decision : decisions) { nsIDataStorage::DataType type; if (NS_FAILED(decision->GetType(&type))) { continue; } if (type != nsIDataStorage::DataType::Persistent) { continue; } nsAutoCString key; if (NS_FAILED(decision->GetKey(key))) { continue; } nsAutoCString value; if (NS_FAILED(decision->GetValue(value))) { continue; } RefPtr entry(new nsClientAuthRemember(key, value)); nsAutoCString newKey; if (NS_FAILED(entry->GetEntryKey(newKey))) { continue; } if (newKey != key) { if (NS_FAILED(mClientAuthRememberList->Remove( key, nsIDataStorage::DataType::Persistent))) { continue; } if (NS_FAILED(mClientAuthRememberList->Put( newKey, value, nsIDataStorage::DataType::Persistent))) { continue; } } } } 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; } *aRetVal = false; aCertDBKey.Truncate(); Migrate(); nsAutoCString entryKey; RefPtr entry( new nsClientAuthRemember(aHostName, aOriginAttributes)); nsresult rv = entry->GetEntryKey(entryKey); if (NS_FAILED(rv)) { return rv; } nsTArray typesToTry; if (aOriginAttributes.IsPrivateBrowsing()) { typesToTry.AppendElement(nsIDataStorage::DataType::Private); } else { typesToTry.AppendElement(nsIDataStorage::DataType::Persistent); typesToTry.AppendElement(nsIDataStorage::DataType::Temporary); } for (const auto& storageType : typesToTry) { nsAutoCString listEntry; rv = mClientAuthRememberList->Get(entryKey, storageType, listEntry); if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) { return rv; } if (NS_SUCCEEDED(rv) && !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, Duration aDuration) { nsAutoCString entryKey; RefPtr entry( new nsClientAuthRemember(aHostName, aOriginAttributes)); nsresult rv = entry->GetEntryKey(entryKey); if (NS_FAILED(rv)) { return rv; } nsIDataStorage::DataType storageType; if (aOriginAttributes.IsPrivateBrowsing()) { storageType = nsIDataStorage::DataType::Private; } else if (aDuration == nsIClientAuthRememberService::Duration::Permanent) { storageType = nsIDataStorage::DataType::Persistent; } else if (aDuration == nsIClientAuthRememberService::Duration::Session) { storageType = nsIDataStorage::DataType::Temporary; } else { return NS_ERROR_INVALID_ARG; } 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); }