diff options
Diffstat (limited to 'security/manager/ssl/nsCertOverrideService.cpp')
-rw-r--r-- | security/manager/ssl/nsCertOverrideService.cpp | 705 |
1 files changed, 705 insertions, 0 deletions
diff --git a/security/manager/ssl/nsCertOverrideService.cpp b/security/manager/ssl/nsCertOverrideService.cpp new file mode 100644 index 000000000..6582eda03 --- /dev/null +++ b/security/manager/ssl/nsCertOverrideService.cpp @@ -0,0 +1,705 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsCertOverrideService.h" + +#include "NSSCertDBTrustDomain.h" +#include "ScopedNSSTypes.h" +#include "SharedSSLState.h" +#include "mozilla/Telemetry.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsCRT.h" +#include "nsILineInputStream.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsISafeOutputStream.h" +#include "nsIX509Cert.h" +#include "nsNSSCertHelper.h" +#include "nsNSSCertificate.h" +#include "nsNSSComponent.h" +#include "nsNetUtil.h" +#include "nsPromiseFlatString.h" +#include "nsStreamUtils.h" +#include "nsStringBuffer.h" +#include "nsThreadUtils.h" +#include "ssl.h" // For SSL_ClearSessionCache + +using namespace mozilla; +using namespace mozilla::psm; + +#define CERT_OVERRIDE_FILE_NAME "cert_override.txt" + +void +nsCertOverride::convertBitsToString(OverrideBits ob, nsACString &str) +{ + str.Truncate(); + + if (ob & ob_Mismatch) + str.Append('M'); + + if (ob & ob_Untrusted) + str.Append('U'); + + if (ob & ob_Time_error) + str.Append('T'); +} + +void +nsCertOverride::convertStringToBits(const nsACString &str, OverrideBits &ob) +{ + const nsPromiseFlatCString &flat = PromiseFlatCString(str); + const char *walk = flat.get(); + + ob = ob_None; + + for ( ; *walk; ++walk) + { + switch (*walk) + { + case 'm': + case 'M': + ob = (OverrideBits)(ob | ob_Mismatch); + break; + + case 'u': + case 'U': + ob = (OverrideBits)(ob | ob_Untrusted); + break; + + case 't': + case 'T': + ob = (OverrideBits)(ob | ob_Time_error); + break; + + default: + break; + } + } +} + +NS_IMPL_ISUPPORTS(nsCertOverrideService, + nsICertOverrideService, + nsIObserver, + nsISupportsWeakReference) + +nsCertOverrideService::nsCertOverrideService() + : monitor("nsCertOverrideService.monitor") +{ +} + +nsCertOverrideService::~nsCertOverrideService() +{ +} + +nsresult +nsCertOverrideService::Init() +{ + if (!NS_IsMainThread()) { + NS_NOTREACHED("nsCertOverrideService initialized off main thread"); + return NS_ERROR_NOT_SAME_THREAD; + } + + // Note that the names of these variables would seem to indicate that at one + // point another hash algorithm was used and is still supported for backwards + // compatibility. This is not the case. It has always been SHA256. + mOidTagForStoringNewHashes = SEC_OID_SHA256; + mDottedOidForStoringNewHashes.Assign("OID.2.16.840.1.101.3.4.2.1"); + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + + // If we cannot add ourselves as a profile change observer, then we will not + // attempt to read/write any settings file. Otherwise, we would end up + // reading/writing the wrong settings file after a profile change. + if (observerService) { + observerService->AddObserver(this, "profile-before-change", true); + observerService->AddObserver(this, "profile-do-change", true); + // simulate a profile change so we read the current profile's settings file + Observe(nullptr, "profile-do-change", nullptr); + } + + SharedSSLState::NoteCertOverrideServiceInstantiated(); + return NS_OK; +} + +NS_IMETHODIMP +nsCertOverrideService::Observe(nsISupports *, + const char *aTopic, + const char16_t *aData) +{ + // check the topic + if (!nsCRT::strcmp(aTopic, "profile-before-change")) { + // The profile is about to change, + // or is going away because the application is shutting down. + + RemoveAllFromMemory(); + } else if (!nsCRT::strcmp(aTopic, "profile-do-change")) { + // The profile has already changed. + // Now read from the new profile location. + // we also need to update the cached file location + + ReentrantMonitorAutoEnter lock(monitor); + + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mSettingsFile)); + if (NS_SUCCEEDED(rv)) { + mSettingsFile->AppendNative(NS_LITERAL_CSTRING(CERT_OVERRIDE_FILE_NAME)); + } else { + mSettingsFile = nullptr; + } + Read(); + CountPermanentOverrideTelemetry(); + } + + return NS_OK; +} + +void +nsCertOverrideService::RemoveAllFromMemory() +{ + ReentrantMonitorAutoEnter lock(monitor); + mSettingsTable.Clear(); +} + +void +nsCertOverrideService::RemoveAllTemporaryOverrides() +{ + ReentrantMonitorAutoEnter lock(monitor); + for (auto iter = mSettingsTable.Iter(); !iter.Done(); iter.Next()) { + nsCertOverrideEntry *entry = iter.Get(); + if (entry->mSettings.mIsTemporary) { + entry->mSettings.mCert = nullptr; + iter.Remove(); + } + } + // no need to write, as temporaries are never written to disk +} + +nsresult +nsCertOverrideService::Read() +{ + ReentrantMonitorAutoEnter lock(monitor); + + // If we don't have a profile, then we won't try to read any settings file. + if (!mSettingsFile) + return NS_OK; + + nsresult rv; + nsCOMPtr<nsIInputStream> fileInputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), mSettingsFile); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsILineInputStream> lineInputStream = do_QueryInterface(fileInputStream, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString buffer; + bool isMore = true; + int32_t hostIndex = 0, algoIndex, fingerprintIndex, overrideBitsIndex, dbKeyIndex; + + /* file format is: + * + * host:port \t fingerprint-algorithm \t fingerprint \t override-mask \t dbKey + * + * where override-mask is a sequence of characters, + * M meaning hostname-Mismatch-override + * U meaning Untrusted-override + * T meaning Time-error-override (expired/not yet valid) + * + * if this format isn't respected we move onto the next line in the file. + */ + + while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) { + if (buffer.IsEmpty() || buffer.First() == '#') { + continue; + } + + // this is a cheap, cheesy way of parsing a tab-delimited line into + // string indexes, which can be lopped off into substrings. just for + // purposes of obfuscation, it also checks that each token was found. + // todo: use iterators? + if ((algoIndex = buffer.FindChar('\t', hostIndex) + 1) == 0 || + (fingerprintIndex = buffer.FindChar('\t', algoIndex) + 1) == 0 || + (overrideBitsIndex = buffer.FindChar('\t', fingerprintIndex) + 1) == 0 || + (dbKeyIndex = buffer.FindChar('\t', overrideBitsIndex) + 1) == 0) { + continue; + } + + const nsASingleFragmentCString &tmp = Substring(buffer, hostIndex, algoIndex - hostIndex - 1); + const nsASingleFragmentCString &algo_string = Substring(buffer, algoIndex, fingerprintIndex - algoIndex - 1); + const nsASingleFragmentCString &fingerprint = Substring(buffer, fingerprintIndex, overrideBitsIndex - fingerprintIndex - 1); + const nsASingleFragmentCString &bits_string = Substring(buffer, overrideBitsIndex, dbKeyIndex - overrideBitsIndex - 1); + const nsASingleFragmentCString &db_key = Substring(buffer, dbKeyIndex, buffer.Length() - dbKeyIndex); + + nsAutoCString host(tmp); + nsCertOverride::OverrideBits bits; + nsCertOverride::convertStringToBits(bits_string, bits); + + int32_t port; + int32_t portIndex = host.RFindChar(':'); + if (portIndex == kNotFound) + continue; // Ignore broken entries + + nsresult portParseError; + nsAutoCString portString(Substring(host, portIndex+1)); + port = portString.ToInteger(&portParseError); + if (NS_FAILED(portParseError)) + continue; // Ignore broken entries + + host.Truncate(portIndex); + + AddEntryToList(host, port, + nullptr, // don't have the cert + false, // not temporary + algo_string, fingerprint, bits, db_key); + } + + return NS_OK; +} + +nsresult +nsCertOverrideService::Write() +{ + ReentrantMonitorAutoEnter lock(monitor); + + // If we don't have any profile, then we won't try to write any file + if (!mSettingsFile) { + return NS_OK; + } + + nsresult rv; + nsCOMPtr<nsIOutputStream> fileOutputStream; + rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(fileOutputStream), + mSettingsFile, + -1, + 0600); + if (NS_FAILED(rv)) { + NS_ERROR("failed to open cert_warn_settings.txt for writing"); + return rv; + } + + // get a buffered output stream 4096 bytes big, to optimize writes + nsCOMPtr<nsIOutputStream> bufferedOutputStream; + rv = NS_NewBufferedOutputStream(getter_AddRefs(bufferedOutputStream), fileOutputStream, 4096); + if (NS_FAILED(rv)) { + return rv; + } + + static const char kHeader[] = + "# PSM Certificate Override Settings file" NS_LINEBREAK + "# This is a generated file! Do not edit." NS_LINEBREAK; + + /* see ::Read for file format */ + + uint32_t unused; + bufferedOutputStream->Write(kHeader, sizeof(kHeader) - 1, &unused); + + static const char kTab[] = "\t"; + for (auto iter = mSettingsTable.Iter(); !iter.Done(); iter.Next()) { + nsCertOverrideEntry *entry = iter.Get(); + + const nsCertOverride &settings = entry->mSettings; + if (settings.mIsTemporary) { + continue; + } + + nsAutoCString bits_string; + nsCertOverride::convertBitsToString(settings.mOverrideBits, bits_string); + + bufferedOutputStream->Write(entry->mHostWithPort.get(), + entry->mHostWithPort.Length(), &unused); + bufferedOutputStream->Write(kTab, sizeof(kTab) - 1, &unused); + bufferedOutputStream->Write(settings.mFingerprintAlgOID.get(), + settings.mFingerprintAlgOID.Length(), &unused); + bufferedOutputStream->Write(kTab, sizeof(kTab) - 1, &unused); + bufferedOutputStream->Write(settings.mFingerprint.get(), + settings.mFingerprint.Length(), &unused); + bufferedOutputStream->Write(kTab, sizeof(kTab) - 1, &unused); + bufferedOutputStream->Write(bits_string.get(), + bits_string.Length(), &unused); + bufferedOutputStream->Write(kTab, sizeof(kTab) - 1, &unused); + bufferedOutputStream->Write(settings.mDBKey.get(), + settings.mDBKey.Length(), &unused); + bufferedOutputStream->Write(NS_LINEBREAK, NS_LINEBREAK_LEN, &unused); + } + + // All went ok. Maybe except for problems in Write(), but the stream detects + // that for us + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(bufferedOutputStream); + NS_ASSERTION(safeStream, "expected a safe output stream!"); + if (safeStream) { + rv = safeStream->Finish(); + if (NS_FAILED(rv)) { + NS_WARNING("failed to save cert warn settings file! possible dataloss"); + return rv; + } + } + + return NS_OK; +} + +static nsresult +GetCertFingerprintByOidTag(nsIX509Cert *aCert, + SECOidTag aOidTag, + nsCString &fp) +{ + UniqueCERTCertificate nsscert(aCert->GetCert()); + if (!nsscert) { + return NS_ERROR_FAILURE; + } + return GetCertFingerprintByOidTag(nsscert.get(), aOidTag, fp); +} + +NS_IMETHODIMP +nsCertOverrideService::RememberValidityOverride(const nsACString& aHostName, + int32_t aPort, + nsIX509Cert* aCert, + uint32_t aOverrideBits, + bool aTemporary) +{ + NS_ENSURE_ARG_POINTER(aCert); + if (aHostName.IsEmpty()) + return NS_ERROR_INVALID_ARG; + if (aPort < -1) + return NS_ERROR_INVALID_ARG; + + UniqueCERTCertificate nsscert(aCert->GetCert()); + if (!nsscert) { + return NS_ERROR_FAILURE; + } + + nsAutoCString nickname; + nsresult rv = DefaultServerNicknameForCert(nsscert.get(), nickname); + if (!aTemporary && NS_SUCCEEDED(rv)) { + UniquePK11SlotInfo slot(PK11_GetInternalKeySlot()); + if (!slot) { + return NS_ERROR_FAILURE; + } + + SECStatus srv = PK11_ImportCert(slot.get(), nsscert.get(), CK_INVALID_HANDLE, + nickname.get(), false); + if (srv != SECSuccess) { + return NS_ERROR_FAILURE; + } + } + + nsAutoCString fpStr; + rv = GetCertFingerprintByOidTag(nsscert.get(), mOidTagForStoringNewHashes, + fpStr); + if (NS_FAILED(rv)) + return rv; + + nsAutoCString dbkey; + rv = aCert->GetDbKey(dbkey); + if (NS_FAILED(rv)) { + return rv; + } + + { + ReentrantMonitorAutoEnter lock(monitor); + AddEntryToList(aHostName, aPort, + aTemporary ? aCert : nullptr, + // keep a reference to the cert for temporary overrides + aTemporary, + mDottedOidForStoringNewHashes, fpStr, + (nsCertOverride::OverrideBits)aOverrideBits, + dbkey); + if (!aTemporary) { + Write(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCertOverrideService::RememberTemporaryValidityOverrideUsingFingerprint( + const nsACString& aHostName, + int32_t aPort, + const nsACString& aCertFingerprint, + uint32_t aOverrideBits) +{ + if(aCertFingerprint.IsEmpty() || aHostName.IsEmpty() || (aPort < -1)) { + return NS_ERROR_INVALID_ARG; + } + + ReentrantMonitorAutoEnter lock(monitor); + AddEntryToList(aHostName, aPort, + nullptr, // No cert to keep alive + true, // temporary + mDottedOidForStoringNewHashes, + aCertFingerprint, + (nsCertOverride::OverrideBits)aOverrideBits, + EmptyCString()); // dbkey + + return NS_OK; +} + +NS_IMETHODIMP +nsCertOverrideService::HasMatchingOverride(const nsACString & aHostName, int32_t aPort, + nsIX509Cert *aCert, + uint32_t *aOverrideBits, + bool *aIsTemporary, + bool *_retval) +{ + if (aHostName.IsEmpty()) + return NS_ERROR_INVALID_ARG; + if (aPort < -1) + return NS_ERROR_INVALID_ARG; + + NS_ENSURE_ARG_POINTER(aCert); + NS_ENSURE_ARG_POINTER(aOverrideBits); + NS_ENSURE_ARG_POINTER(aIsTemporary); + NS_ENSURE_ARG_POINTER(_retval); + *_retval = false; + *aOverrideBits = nsCertOverride::ob_None; + + nsAutoCString hostPort; + GetHostWithPort(aHostName, aPort, hostPort); + nsCertOverride settings; + + { + ReentrantMonitorAutoEnter lock(monitor); + nsCertOverrideEntry *entry = mSettingsTable.GetEntry(hostPort.get()); + + if (!entry) + return NS_OK; + + settings = entry->mSettings; // copy + } + + *aOverrideBits = settings.mOverrideBits; + *aIsTemporary = settings.mIsTemporary; + + nsAutoCString fpStr; + nsresult rv; + + // This code was originally written in a way that suggested that other hash + // algorithms are supported for backwards compatibility. However, this was + // always unnecessary, because only SHA256 has ever been used here. + if (settings.mFingerprintAlgOID.Equals(mDottedOidForStoringNewHashes)) { + rv = GetCertFingerprintByOidTag(aCert, mOidTagForStoringNewHashes, fpStr); + if (NS_FAILED(rv)) { + return rv; + } + } else { + return NS_ERROR_UNEXPECTED; + } + + *_retval = settings.mFingerprint.Equals(fpStr); + return NS_OK; +} + +NS_IMETHODIMP +nsCertOverrideService::GetValidityOverride(const nsACString & aHostName, int32_t aPort, + nsACString & aHashAlg, + nsACString & aFingerprint, + uint32_t *aOverrideBits, + bool *aIsTemporary, + bool *_found) +{ + NS_ENSURE_ARG_POINTER(_found); + NS_ENSURE_ARG_POINTER(aIsTemporary); + NS_ENSURE_ARG_POINTER(aOverrideBits); + *_found = false; + *aOverrideBits = nsCertOverride::ob_None; + + nsAutoCString hostPort; + GetHostWithPort(aHostName, aPort, hostPort); + nsCertOverride settings; + + { + ReentrantMonitorAutoEnter lock(monitor); + nsCertOverrideEntry *entry = mSettingsTable.GetEntry(hostPort.get()); + + if (entry) { + *_found = true; + settings = entry->mSettings; // copy + } + } + + if (*_found) { + *aOverrideBits = settings.mOverrideBits; + *aIsTemporary = settings.mIsTemporary; + aFingerprint = settings.mFingerprint; + aHashAlg = settings.mFingerprintAlgOID; + } + + return NS_OK; +} + +nsresult +nsCertOverrideService::AddEntryToList(const nsACString &aHostName, int32_t aPort, + nsIX509Cert *aCert, + const bool aIsTemporary, + const nsACString &fingerprintAlgOID, + const nsACString &fingerprint, + nsCertOverride::OverrideBits ob, + const nsACString &dbKey) +{ + nsAutoCString hostPort; + GetHostWithPort(aHostName, aPort, hostPort); + + { + ReentrantMonitorAutoEnter lock(monitor); + nsCertOverrideEntry *entry = mSettingsTable.PutEntry(hostPort.get()); + + if (!entry) { + NS_ERROR("can't insert a null entry!"); + return NS_ERROR_OUT_OF_MEMORY; + } + + entry->mHostWithPort = hostPort; + + nsCertOverride &settings = entry->mSettings; + settings.mAsciiHost = aHostName; + settings.mPort = aPort; + settings.mIsTemporary = aIsTemporary; + settings.mFingerprintAlgOID = fingerprintAlgOID; + settings.mFingerprint = fingerprint; + settings.mOverrideBits = ob; + settings.mDBKey = dbKey; + // remove whitespace from stored dbKey for backwards compatibility + settings.mDBKey.StripWhitespace(); + settings.mCert = aCert; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCertOverrideService::ClearValidityOverride(const nsACString & aHostName, int32_t aPort) +{ + if (aPort == 0 && + aHostName.EqualsLiteral("all:temporary-certificates")) { + RemoveAllTemporaryOverrides(); + return NS_OK; + } + nsAutoCString hostPort; + GetHostWithPort(aHostName, aPort, hostPort); + { + ReentrantMonitorAutoEnter lock(monitor); + mSettingsTable.RemoveEntry(hostPort.get()); + Write(); + } + + if (EnsureNSSInitialized(nssEnsure)) { + SSL_ClearSessionCache(); + } else { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; +} + +void +nsCertOverrideService::CountPermanentOverrideTelemetry() +{ + ReentrantMonitorAutoEnter lock(monitor); + uint32_t overrideCount = 0; + for (auto iter = mSettingsTable.Iter(); !iter.Done(); iter.Next()) { + if (!iter.Get()->mSettings.mIsTemporary) { + overrideCount++; + } + } + Telemetry::Accumulate(Telemetry::SSL_PERMANENT_CERT_ERROR_OVERRIDES, + overrideCount); +} + +static bool +matchesDBKey(nsIX509Cert* cert, const nsCString& matchDbKey) +{ + nsAutoCString dbKey; + nsresult rv = cert->GetDbKey(dbKey); + if (NS_FAILED(rv)) { + return false; + } + return dbKey.Equals(matchDbKey); +} + +NS_IMETHODIMP +nsCertOverrideService::IsCertUsedForOverrides(nsIX509Cert *aCert, + bool aCheckTemporaries, + bool aCheckPermanents, + uint32_t *_retval) +{ + NS_ENSURE_ARG(aCert); + NS_ENSURE_ARG(_retval); + + uint32_t counter = 0; + { + ReentrantMonitorAutoEnter lock(monitor); + for (auto iter = mSettingsTable.Iter(); !iter.Done(); iter.Next()) { + const nsCertOverride &settings = iter.Get()->mSettings; + + if (( settings.mIsTemporary && !aCheckTemporaries) || + (!settings.mIsTemporary && !aCheckPermanents)) { + continue; + } + + if (matchesDBKey(aCert, settings.mDBKey)) { + nsAutoCString cert_fingerprint; + nsresult rv = NS_ERROR_UNEXPECTED; + if (settings.mFingerprintAlgOID.Equals(mDottedOidForStoringNewHashes)) { + rv = GetCertFingerprintByOidTag(aCert, + mOidTagForStoringNewHashes, cert_fingerprint); + } + if (NS_SUCCEEDED(rv) && + settings.mFingerprint.Equals(cert_fingerprint)) { + counter++; + } + } + } + } + *_retval = counter; + return NS_OK; +} + +nsresult +nsCertOverrideService::EnumerateCertOverrides(nsIX509Cert *aCert, + CertOverrideEnumerator aEnumerator, + void *aUserData) +{ + ReentrantMonitorAutoEnter lock(monitor); + for (auto iter = mSettingsTable.Iter(); !iter.Done(); iter.Next()) { + const nsCertOverride &settings = iter.Get()->mSettings; + + if (!aCert) { + aEnumerator(settings, aUserData); + } else { + if (matchesDBKey(aCert, settings.mDBKey)) { + nsAutoCString cert_fingerprint; + nsresult rv = NS_ERROR_UNEXPECTED; + if (settings.mFingerprintAlgOID.Equals(mDottedOidForStoringNewHashes)) { + rv = GetCertFingerprintByOidTag(aCert, + mOidTagForStoringNewHashes, cert_fingerprint); + } + if (NS_SUCCEEDED(rv) && + settings.mFingerprint.Equals(cert_fingerprint)) { + aEnumerator(settings, aUserData); + } + } + } + } + return NS_OK; +} + +void +nsCertOverrideService::GetHostWithPort(const nsACString & aHostName, int32_t aPort, nsACString& _retval) +{ + nsAutoCString hostPort(aHostName); + if (aPort == -1) { + aPort = 443; + } + if (!hostPort.IsEmpty()) { + hostPort.Append(':'); + hostPort.AppendInt(aPort); + } + _retval.Assign(hostPort); +} |