diff options
Diffstat (limited to 'security/manager/ssl/nsClientAuthRemember.cpp')
-rw-r--r-- | security/manager/ssl/nsClientAuthRemember.cpp | 363 |
1 files changed, 363 insertions, 0 deletions
diff --git a/security/manager/ssl/nsClientAuthRemember.cpp b/security/manager/ssl/nsClientAuthRemember.cpp new file mode 100644 index 0000000000..1c7422fc2f --- /dev/null +++ b/security/manager/ssl/nsClientAuthRemember.cpp @@ -0,0 +1,363 @@ +/* -*- 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 <CoreFoundation/CoreFoundation.h> +# include <Security/Security.h> +# 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<nsINSSComponent> nssComponent(do_GetService(NS_NSSCOMPONENT_CID)); + if (!nssComponent) { + return NS_ERROR_NOT_AVAILABLE; + } + return nssComponent->ClearSSLExternalAndInternalSessionCache(); +} + +NS_IMETHODIMP +nsClientAuthRememberService::GetDecisions( + nsTArray<RefPtr<nsIClientAuthRememberRecord>>& results) { + nsTArray<DataStorageItem> decisions; + mClientAuthRememberList->GetAll(&decisions); + + for (const DataStorageItem& decision : decisions) { + if (decision.type == DataStorageType::DataStorage_Persistent) { + RefPtr<nsIClientAuthRememberRecord> tmp = + new nsClientAuthRemember(decision.key, decision.value); + + results.AppendElement(tmp); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsClientAuthRememberService::ClearRememberedDecisions() { + mClientAuthRememberList->Clear(); + nsCOMPtr<nsINSSComponent> 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<JS::Value> aOriginAttributes, + JSContext* aCx) { + OriginAttributes attrs; + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + DataStorageType storageType = GetDataStorageType(attrs); + + nsTArray<DataStorageItem> decisions; + mClientAuthRememberList->GetAll(&decisions); + + for (const DataStorageItem& decision : decisions) { + if (decision.type == storageType) { + RefPtr<nsIClientAuthRememberRecord> tmp = + new nsClientAuthRemember(decision.key, decision.value); + nsAutoCString asciiHost; + tmp->GetAsciiHost(asciiHost); + if (asciiHost.Equals(aHostName)) { + mClientAuthRememberList->Remove(decision.key, decision.type); + } + } + } + nsCOMPtr<nsINSSComponent> 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<JS::Value> 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<CFStringRef> host(::CFStringCreateWithCString( + kCFAllocatorDefault, fakeUrl.get(), kCFStringEncodingUTF8)); + if (!host) { + return NS_ERROR_UNEXPECTED; + } + ScopedCFType<SecIdentityRef> 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<SecCertificateRef> certRef(certRefRaw); + if (copyResult != errSecSuccess || certRef.get() == NULL) { + return NS_ERROR_UNEXPECTED; + } + ScopedCFType<CFDataRef> der(::SecCertificateCopyData(certRef.get())); + if (!der) { + return NS_ERROR_UNEXPECTED; + } + + nsTArray<uint8_t> derArray(::CFDataGetBytePtr(der.get()), + ::CFDataGetLength(der.get())); + nsCOMPtr<nsIX509Cert> 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<DataStorageItem> decisions; + mClientAuthRememberList->GetAll(&decisions); + for (const auto& decision : decisions) { + if (decision.type != DataStorage_Persistent) { + continue; + } + RefPtr<nsClientAuthRemember> 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<nsClientAuthRemember> 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<JS::Value> 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<nsClientAuthRemember> 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; +} |