diff options
Diffstat (limited to 'security/manager/ssl/SSLServerCertVerification.cpp')
-rw-r--r-- | security/manager/ssl/SSLServerCertVerification.cpp | 1795 |
1 files changed, 1795 insertions, 0 deletions
diff --git a/security/manager/ssl/SSLServerCertVerification.cpp b/security/manager/ssl/SSLServerCertVerification.cpp new file mode 100644 index 000000000..4ef79f54a --- /dev/null +++ b/security/manager/ssl/SSLServerCertVerification.cpp @@ -0,0 +1,1795 @@ +/* -*- 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/. */ + +// For connections that are not processed on the socket transport thread, we do +// NOT use the async logic described below. Instead, we authenticate the +// certificate on the thread that the connection's I/O happens on, +// synchronously. This allows us to do certificate verification for blocking +// (not non-blocking) sockets and sockets that have their I/O processed on a +// thread other than the socket transport service thread. Also, we DO NOT +// support blocking sockets on the socket transport service thread at all. +// +// During certificate authentication, we call CERT_PKIXVerifyCert or +// CERT_VerifyCert. These functions may make zero or more HTTP requests +// for OCSP responses, CRLs, intermediate certificates, etc. Our fetching logic +// for these requests processes them on the socket transport service thread. +// +// If the connection for which we are verifying the certificate is happening +// on the socket transport thread (the usually case, at least for HTTP), then +// if our cert auth hook were to call the CERT_*Verify* functions directly, +// there would be a deadlock: The CERT_*Verify* function would cause an event +// to be asynchronously posted to the socket transport thread, and then it +// would block the socket transport thread waiting to be notified of the HTTP +// response. However, the HTTP request would never actually be processed +// because the socket transport thread would be blocked and so it wouldn't be +// able process HTTP requests. (i.e. Deadlock.) +// +// Consequently, when we are asked to verify a certificate on the socket +// transport service thread, we must always call the CERT_*Verify* cert +// functions on another thread. To accomplish this, our auth cert hook +// dispatches a SSLServerCertVerificationJob to a pool of background threads, +// and then immediately returns SECWouldBlock to libssl. These jobs are where +// the CERT_*Verify* functions are actually called. +// +// When our auth cert hook returns SECWouldBlock, libssl will carry on the +// handshake while we validate the certificate. This will free up the socket +// transport thread so that HTTP requests--in particular, the OCSP/CRL/cert +// requests needed for cert verification as mentioned above--can be processed. +// +// Once the CERT_*Verify* function returns, the cert verification job +// dispatches a SSLServerCertVerificationResult to the socket transport thread; +// the SSLServerCertVerificationResult will notify libssl that the certificate +// authentication is complete. Once libssl is notified that the authentication +// is complete, it will continue the SSL handshake (if it hasn't already +// finished) and it will begin allowing us to send/receive data on the +// connection. +// +// Timeline of events (for connections managed by the socket transport service): +// +// * libssl calls SSLServerCertVerificationJob::Dispatch on the socket +// transport thread. +// * SSLServerCertVerificationJob::Dispatch queues a job +// (instance of SSLServerCertVerificationJob) to its background thread +// pool and returns. +// * One of the background threads calls CERT_*Verify*, which may enqueue +// some HTTP request(s) onto the socket transport thread, and then +// blocks that background thread waiting for the responses and/or timeouts +// or errors for those requests. +// * Once those HTTP responses have all come back or failed, the +// CERT_*Verify* function returns a result indicating that the validation +// succeeded or failed. +// * If the validation succeeded, then a SSLServerCertVerificationResult +// event is posted to the socket transport thread, and the cert +// verification thread becomes free to verify other certificates. +// * Otherwise, a CertErrorRunnable is posted to the socket transport thread +// and then to the main thread (blocking both, see CertErrorRunnable) to +// do cert override processing and bad cert listener notification. Then +// the cert verification thread becomes free to verify other certificates. +// * After processing cert overrides, the CertErrorRunnable will dispatch a +// SSLServerCertVerificationResult event to the socket transport thread to +// notify it of the result of the override processing; then it returns, +// freeing up the main thread. +// * The SSLServerCertVerificationResult event will either wake up the +// socket (using SSL_RestartHandshakeAfterServerCert) if validation +// succeeded or there was an error override, or it will set an error flag +// so that the next I/O operation on the socket will fail, causing the +// socket transport thread to close the connection. +// +// Cert override processing must happen on the main thread because it accesses +// the nsICertOverrideService, and that service must be accessed on the main +// thread because some extensions (Selenium, in particular) replace it with a +// Javascript implementation, and chrome JS must always be run on the main +// thread. +// +// SSLServerCertVerificationResult must be dispatched to the socket transport +// thread because we must only call SSL_* functions on the socket transport +// thread since they may do I/O, because many parts of nsNSSSocketInfo (the +// subclass of TransportSecurityInfo used when validating certificates during +// an SSL handshake) and the PSM NSS I/O layer are not thread-safe, and because +// we need the event to interrupt the PR_Poll that may waiting for I/O on the +// socket for which we are validating the cert. + +#include "SSLServerCertVerification.h" + +#include <cstring> + +#include "BRNameMatchingPolicy.h" +#include "CertVerifier.h" +#include "CryptoTask.h" +#include "ExtendedValidation.h" +#include "NSSCertDBTrustDomain.h" +#include "PSMRunnable.h" +#include "RootCertificateTelemetryUtils.h" +#include "ScopedNSSTypes.h" +#include "SharedCertVerifier.h" +#include "SharedSSLState.h" +#include "TransportSecurityInfo.h" // For RememberCertErrorsTable +#include "cert.h" +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Telemetry.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/net/DNS.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIBadCertListener2.h" +#include "nsICertOverrideService.h" +#include "nsISiteSecurityService.h" +#include "nsISocketProvider.h" +#include "nsIThreadPool.h" +#include "nsNSSCertificate.h" +#include "nsNSSComponent.h" +#include "nsNSSIOLayer.h" +#include "nsNSSShutDown.h" +#include "nsSSLStatus.h" +#include "nsServiceManagerUtils.h" +#include "nsURLHelper.h" +#include "nsXPCOMCIDInternal.h" +#include "pkix/pkix.h" +#include "pkix/pkixnss.h" +#include "secerr.h" +#include "secoidt.h" +#include "secport.h" +#include "ssl.h" +#include "sslerr.h" + +extern mozilla::LazyLogModule gPIPNSSLog; + +using namespace mozilla::pkix; + +namespace mozilla { namespace psm { + +namespace { + +// do not use a nsCOMPtr to avoid static initializer/destructor +nsIThreadPool* gCertVerificationThreadPool = nullptr; + +// We avoid using a mutex for the success case to avoid lock-related +// performance issues. However, we do use a lock in the error case to simplify +// the code, since performance in the error case is not important. +Mutex* gSSLVerificationTelemetryMutex = nullptr; + +// We add a mutex to serialize PKCS11 database operations +Mutex* gSSLVerificationPK11Mutex = nullptr; + +} // unnamed namespace + +// Called when the socket transport thread starts, to initialize the SSL cert +// verification thread pool. By tying the thread pool startup/shutdown directly +// to the STS thread's lifetime, we ensure that they are *always* available for +// SSL connections and that there are no races during startup and especially +// shutdown. (Previously, we have had multiple problems with races in PSM +// background threads, and the race-prevention/shutdown logic used there is +// brittle. Since this service is critical to things like downloading updates, +// we take no chances.) Also, by doing things this way, we avoid the need for +// locks, since gCertVerificationThreadPool is only ever accessed on the socket +// transport thread. +void +InitializeSSLServerCertVerificationThreads() +{ + gSSLVerificationTelemetryMutex = new Mutex("SSLVerificationTelemetryMutex"); + gSSLVerificationPK11Mutex = new Mutex("SSLVerificationPK11Mutex"); + // TODO: tuning, make parameters preferences + // XXX: instantiate nsThreadPool directly, to make this more bulletproof. + // Currently, the nsThreadPool.h header isn't exported for us to do so. + nsresult rv = CallCreateInstance(NS_THREADPOOL_CONTRACTID, + &gCertVerificationThreadPool); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to create SSL cert verification threads."); + return; + } + + (void) gCertVerificationThreadPool->SetIdleThreadLimit(5); + (void) gCertVerificationThreadPool->SetIdleThreadTimeout(30 * 1000); + (void) gCertVerificationThreadPool->SetThreadLimit(5); + (void) gCertVerificationThreadPool->SetName(NS_LITERAL_CSTRING("SSL Cert")); +} + +// Called when the socket transport thread finishes, to destroy the thread +// pool. Since the socket transport service has stopped processing events, it +// will not attempt any more SSL I/O operations, so it is clearly safe to shut +// down the SSL cert verification infrastructure. Also, the STS will not +// dispatch many SSL verification result events at this point, so any pending +// cert verifications will (correctly) fail at the point they are dispatched. +// +// The other shutdown race condition that is possible is a race condition with +// shutdown of the nsNSSComponent service. We use the +// nsNSSShutdownPreventionLock where needed (not here) to prevent that. +void StopSSLServerCertVerificationThreads() +{ + if (gCertVerificationThreadPool) { + gCertVerificationThreadPool->Shutdown(); + NS_RELEASE(gCertVerificationThreadPool); + } + if (gSSLVerificationTelemetryMutex) { + delete gSSLVerificationTelemetryMutex; + gSSLVerificationTelemetryMutex = nullptr; + } + if (gSSLVerificationPK11Mutex) { + delete gSSLVerificationPK11Mutex; + gSSLVerificationPK11Mutex = nullptr; + } +} + +namespace { + +void +LogInvalidCertError(nsNSSSocketInfo* socketInfo, + PRErrorCode errorCode, + ::mozilla::psm::SSLErrorMessageType errorMessageType) +{ + nsString message; + socketInfo->GetErrorLogMessage(errorCode, errorMessageType, message); + if (!message.IsEmpty()) { + nsContentUtils::LogSimpleConsoleError(message, "SSL"); + } +} + +// Dispatched to the STS thread to notify the infoObject of the verification +// result. +// +// This will cause the PR_Poll in the STS thread to return, so things work +// correctly even if the STS thread is blocked polling (only) on the file +// descriptor that is waiting for this result. +class SSLServerCertVerificationResult : public Runnable +{ +public: + NS_DECL_NSIRUNNABLE + + SSLServerCertVerificationResult(nsNSSSocketInfo* infoObject, + PRErrorCode errorCode, + Telemetry::ID telemetryID = Telemetry::HistogramCount, + uint32_t telemetryValue = -1, + SSLErrorMessageType errorMessageType = + PlainErrorMessage); + + void Dispatch(); +private: + const RefPtr<nsNSSSocketInfo> mInfoObject; +public: + const PRErrorCode mErrorCode; + const SSLErrorMessageType mErrorMessageType; + const Telemetry::ID mTelemetryID; + const uint32_t mTelemetryValue; +}; + +class CertErrorRunnable : public SyncRunnableBase +{ + public: + CertErrorRunnable(const void* fdForLogging, + nsIX509Cert* cert, + nsNSSSocketInfo* infoObject, + PRErrorCode defaultErrorCodeToReport, + uint32_t collectedErrors, + PRErrorCode errorCodeTrust, + PRErrorCode errorCodeMismatch, + PRErrorCode errorCodeTime, + uint32_t providerFlags) + : mFdForLogging(fdForLogging), mCert(cert), mInfoObject(infoObject), + mDefaultErrorCodeToReport(defaultErrorCodeToReport), + mCollectedErrors(collectedErrors), + mErrorCodeTrust(errorCodeTrust), + mErrorCodeMismatch(errorCodeMismatch), + mErrorCodeTime(errorCodeTime), + mProviderFlags(providerFlags) + { + } + + virtual void RunOnTargetThread(); + RefPtr<SSLServerCertVerificationResult> mResult; // out +private: + SSLServerCertVerificationResult* CheckCertOverrides(); + + const void* const mFdForLogging; // may become an invalid pointer; do not dereference + const nsCOMPtr<nsIX509Cert> mCert; + const RefPtr<nsNSSSocketInfo> mInfoObject; + const PRErrorCode mDefaultErrorCodeToReport; + const uint32_t mCollectedErrors; + const PRErrorCode mErrorCodeTrust; + const PRErrorCode mErrorCodeMismatch; + const PRErrorCode mErrorCodeTime; + const uint32_t mProviderFlags; +}; + +// A probe value of 1 means "no error". +uint32_t +MapOverridableErrorToProbeValue(PRErrorCode errorCode) +{ + switch (errorCode) + { + case SEC_ERROR_UNKNOWN_ISSUER: return 2; + case SEC_ERROR_CA_CERT_INVALID: return 3; + case SEC_ERROR_UNTRUSTED_ISSUER: return 4; + case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE: return 5; + case SEC_ERROR_UNTRUSTED_CERT: return 6; + case SEC_ERROR_INADEQUATE_KEY_USAGE: return 7; + case SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED: return 8; + case SSL_ERROR_BAD_CERT_DOMAIN: return 9; + case SEC_ERROR_EXPIRED_CERTIFICATE: return 10; + case mozilla::pkix::MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY: return 11; + case mozilla::pkix::MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA: return 12; + case mozilla::pkix::MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE: return 13; + case mozilla::pkix::MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE: return 14; + case mozilla::pkix::MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE: + return 15; + case SEC_ERROR_INVALID_TIME: return 16; + case mozilla::pkix::MOZILLA_PKIX_ERROR_EMPTY_ISSUER_NAME: return 17; + } + NS_WARNING("Unknown certificate error code. Does MapOverridableErrorToProbeValue " + "handle everything in DetermineCertOverrideErrors?"); + return 0; +} + +static uint32_t +MapCertErrorToProbeValue(PRErrorCode errorCode) +{ + uint32_t probeValue; + switch (errorCode) + { + // see security/pkix/include/pkix/Result.h +#define MOZILLA_PKIX_MAP(name, value, nss_name) case nss_name: probeValue = value; break; + MOZILLA_PKIX_MAP_LIST +#undef MOZILLA_PKIX_MAP + default: return 0; + } + + // Since FATAL_ERROR_FLAG is 0x800, fatal error values are much larger than + // non-fatal error values. To conserve space, we remap these so they start at + // (decimal) 90 instead of 0x800. Currently there are ~50 non-fatal errors + // mozilla::pkix might return, so saving space for 90 should be sufficient + // (similarly, there are 4 fatal errors, so saving space for 10 should also + // be sufficient). + static_assert(FATAL_ERROR_FLAG == 0x800, + "mozilla::pkix::FATAL_ERROR_FLAG is not what we were expecting"); + if (probeValue & FATAL_ERROR_FLAG) { + probeValue ^= FATAL_ERROR_FLAG; + probeValue += 90; + } + return probeValue; +} + +SECStatus +DetermineCertOverrideErrors(const UniqueCERTCertificate& cert, + const char* hostName, + PRTime now, PRErrorCode defaultErrorCodeToReport, + /*out*/ uint32_t& collectedErrors, + /*out*/ PRErrorCode& errorCodeTrust, + /*out*/ PRErrorCode& errorCodeMismatch, + /*out*/ PRErrorCode& errorCodeTime) +{ + MOZ_ASSERT(cert); + MOZ_ASSERT(hostName); + MOZ_ASSERT(collectedErrors == 0); + MOZ_ASSERT(errorCodeTrust == 0); + MOZ_ASSERT(errorCodeMismatch == 0); + MOZ_ASSERT(errorCodeTime == 0); + + // Assumes the error prioritization described in mozilla::pkix's + // BuildForward function. Also assumes that CheckCertHostname was only + // called if CertVerifier::VerifyCert succeeded. + switch (defaultErrorCodeToReport) { + case SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED: + case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE: + case SEC_ERROR_UNKNOWN_ISSUER: + case SEC_ERROR_CA_CERT_INVALID: + case mozilla::pkix::MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY: + case mozilla::pkix::MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE: + case mozilla::pkix::MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA: + case mozilla::pkix::MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE: + case mozilla::pkix::MOZILLA_PKIX_ERROR_EMPTY_ISSUER_NAME: + { + collectedErrors = nsICertOverrideService::ERROR_UNTRUSTED; + errorCodeTrust = defaultErrorCodeToReport; + + SECCertTimeValidity validity = CERT_CheckCertValidTimes(cert.get(), now, + false); + if (validity == secCertTimeUndetermined) { + // This only happens if cert is null. CERT_CheckCertValidTimes will + // have set the error code to SEC_ERROR_INVALID_ARGS. We should really + // be using mozilla::pkix here anyway. + MOZ_ASSERT(PR_GetError() == SEC_ERROR_INVALID_ARGS); + return SECFailure; + } + if (validity == secCertTimeExpired) { + collectedErrors |= nsICertOverrideService::ERROR_TIME; + errorCodeTime = SEC_ERROR_EXPIRED_CERTIFICATE; + } else if (validity == secCertTimeNotValidYet) { + collectedErrors |= nsICertOverrideService::ERROR_TIME; + errorCodeTime = + mozilla::pkix::MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE; + } + break; + } + + case SEC_ERROR_INVALID_TIME: + case SEC_ERROR_EXPIRED_CERTIFICATE: + case mozilla::pkix::MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE: + collectedErrors = nsICertOverrideService::ERROR_TIME; + errorCodeTime = defaultErrorCodeToReport; + break; + + case SSL_ERROR_BAD_CERT_DOMAIN: + collectedErrors = nsICertOverrideService::ERROR_MISMATCH; + errorCodeMismatch = SSL_ERROR_BAD_CERT_DOMAIN; + break; + + case 0: + NS_ERROR("No error code set during certificate validation failure."); + PR_SetError(PR_INVALID_STATE_ERROR, 0); + return SECFailure; + + default: + PR_SetError(defaultErrorCodeToReport, 0); + return SECFailure; + } + + if (defaultErrorCodeToReport != SSL_ERROR_BAD_CERT_DOMAIN) { + Input certInput; + if (certInput.Init(cert->derCert.data, cert->derCert.len) != Success) { + PR_SetError(SEC_ERROR_BAD_DER, 0); + return SECFailure; + } + Input hostnameInput; + Result result = hostnameInput.Init( + BitwiseCast<const uint8_t*, const char*>(hostName), + strlen(hostName)); + if (result != Success) { + PR_SetError(SEC_ERROR_INVALID_ARGS, 0); + return SECFailure; + } + // Use a lax policy so as to not generate potentially spurious name + // mismatch "hints". + BRNameMatchingPolicy nameMatchingPolicy( + BRNameMatchingPolicy::Mode::DoNotEnforce); + // CheckCertHostname expects that its input represents a certificate that + // has already been successfully validated by BuildCertChain. This is + // obviously not the case, however, because we're in the error path of + // certificate verification. Thus, this is problematic. In the future, it + // would be nice to remove this optimistic additional error checking and + // simply punt to the front-end, which can more easily (and safely) perform + // extra checks to give the user hints as to why verification failed. + result = CheckCertHostname(certInput, hostnameInput, nameMatchingPolicy); + // Treat malformed name information as a domain mismatch. + if (result == Result::ERROR_BAD_DER || + result == Result::ERROR_BAD_CERT_DOMAIN) { + collectedErrors |= nsICertOverrideService::ERROR_MISMATCH; + errorCodeMismatch = SSL_ERROR_BAD_CERT_DOMAIN; + } else if (IsFatalError(result)) { + // Because its input has not been validated by BuildCertChain, + // CheckCertHostname can return an error that is less important than the + // original certificate verification error. Only return an error result + // from this function if we've encountered a fatal error. + PR_SetError(MapResultToPRErrorCode(result), 0); + return SECFailure; + } + } + + return SECSuccess; +} + +SSLServerCertVerificationResult* +CertErrorRunnable::CheckCertOverrides() +{ + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("[%p][%p] top of CheckCertOverrides\n", + mFdForLogging, this)); + // "Use" mFdForLogging in non-PR_LOGGING builds, too, to suppress + // clang's -Wunused-private-field build warning for this variable: + Unused << mFdForLogging; + + if (!NS_IsMainThread()) { + NS_ERROR("CertErrorRunnable::CheckCertOverrides called off main thread"); + return new SSLServerCertVerificationResult(mInfoObject, + mDefaultErrorCodeToReport); + } + + int32_t port; + mInfoObject->GetPort(&port); + + nsAutoCString hostWithPortString(mInfoObject->GetHostName()); + hostWithPortString.Append(':'); + hostWithPortString.AppendInt(port); + + uint32_t remaining_display_errors = mCollectedErrors; + + + // If this is an HTTP Strict Transport Security host or a pinned host and the + // certificate is bad, don't allow overrides (RFC 6797 section 12.1, + // HPKP draft spec section 2.6). + bool strictTransportSecurityEnabled = false; + bool hasPinningInformation = false; + nsCOMPtr<nsISiteSecurityService> sss(do_GetService(NS_SSSERVICE_CONTRACTID)); + if (!sss) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p][%p] couldn't get nsISiteSecurityService to check for HSTS/HPKP\n", + mFdForLogging, this)); + return new SSLServerCertVerificationResult(mInfoObject, + mDefaultErrorCodeToReport); + } + nsresult nsrv = sss->IsSecureHost(nsISiteSecurityService::HEADER_HSTS, + mInfoObject->GetHostNameRaw(), + mProviderFlags, + nullptr, + &strictTransportSecurityEnabled); + if (NS_FAILED(nsrv)) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p][%p] checking for HSTS failed\n", mFdForLogging, this)); + return new SSLServerCertVerificationResult(mInfoObject, + mDefaultErrorCodeToReport); + } + nsrv = sss->IsSecureHost(nsISiteSecurityService::HEADER_HPKP, + mInfoObject->GetHostNameRaw(), + mProviderFlags, + nullptr, + &hasPinningInformation); + if (NS_FAILED(nsrv)) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p][%p] checking for HPKP failed\n", mFdForLogging, this)); + return new SSLServerCertVerificationResult(mInfoObject, + mDefaultErrorCodeToReport); + } + + if (!strictTransportSecurityEnabled && !hasPinningInformation) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p][%p] no HSTS or HPKP - overrides allowed\n", + mFdForLogging, this)); + nsCOMPtr<nsICertOverrideService> overrideService = + do_GetService(NS_CERTOVERRIDE_CONTRACTID); + // it is fine to continue without the nsICertOverrideService + + uint32_t overrideBits = 0; + + if (overrideService) + { + bool haveOverride; + bool isTemporaryOverride; // we don't care + const nsACString& hostString(mInfoObject->GetHostName()); + nsrv = overrideService->HasMatchingOverride(hostString, port, + mCert, + &overrideBits, + &isTemporaryOverride, + &haveOverride); + if (NS_SUCCEEDED(nsrv) && haveOverride) + { + // remove the errors that are already overriden + remaining_display_errors &= ~overrideBits; + } + } + + if (!remaining_display_errors) { + // This can double- or triple-count one certificate with multiple + // different types of errors. Since this is telemetry and we just + // want a ballpark answer, we don't care. + if (mErrorCodeTrust != 0) { + uint32_t probeValue = MapOverridableErrorToProbeValue(mErrorCodeTrust); + Telemetry::Accumulate(Telemetry::SSL_CERT_ERROR_OVERRIDES, probeValue); + } + if (mErrorCodeMismatch != 0) { + uint32_t probeValue = MapOverridableErrorToProbeValue(mErrorCodeMismatch); + Telemetry::Accumulate(Telemetry::SSL_CERT_ERROR_OVERRIDES, probeValue); + } + if (mErrorCodeTime != 0) { + uint32_t probeValue = MapOverridableErrorToProbeValue(mErrorCodeTime); + Telemetry::Accumulate(Telemetry::SSL_CERT_ERROR_OVERRIDES, probeValue); + } + + // all errors are covered by override rules, so let's accept the cert + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p][%p] All errors covered by override rules\n", + mFdForLogging, this)); + return new SSLServerCertVerificationResult(mInfoObject, 0); + } + } else { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p][%p] HSTS or HPKP - no overrides allowed\n", + mFdForLogging, this)); + } + + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p][%p] Certificate error was not overridden\n", + mFdForLogging, this)); + + // Ok, this is a full stop. + // First, deliver the technical details of the broken SSL status. + + // Try to get a nsIBadCertListener2 implementation from the socket consumer. + nsCOMPtr<nsISSLSocketControl> sslSocketControl = do_QueryInterface( + NS_ISUPPORTS_CAST(nsITransportSecurityInfo*, mInfoObject)); + if (sslSocketControl) { + nsCOMPtr<nsIInterfaceRequestor> cb; + sslSocketControl->GetNotificationCallbacks(getter_AddRefs(cb)); + if (cb) { + nsCOMPtr<nsIBadCertListener2> bcl = do_GetInterface(cb); + if (bcl) { + nsIInterfaceRequestor* csi + = static_cast<nsIInterfaceRequestor*>(mInfoObject); + bool suppressMessage = false; // obsolete, ignored + nsrv = bcl->NotifyCertProblem(csi, mInfoObject->SSLStatus(), + hostWithPortString, &suppressMessage); + } + } + } + + // pick the error code to report by priority + PRErrorCode errorCodeToReport = mErrorCodeTrust ? mErrorCodeTrust + : mErrorCodeMismatch ? mErrorCodeMismatch + : mErrorCodeTime ? mErrorCodeTime + : mDefaultErrorCodeToReport; + + SSLServerCertVerificationResult* result = + new SSLServerCertVerificationResult(mInfoObject, + errorCodeToReport, + Telemetry::HistogramCount, + -1, + OverridableCertErrorMessage); + + LogInvalidCertError(mInfoObject, + result->mErrorCode, + result->mErrorMessageType); + + return result; +} + +void +CertErrorRunnable::RunOnTargetThread() +{ + MOZ_ASSERT(NS_IsMainThread()); + + mResult = CheckCertOverrides(); + + MOZ_ASSERT(mResult); +} + +// Returns null with the error code (PR_GetError()) set if it does not create +// the CertErrorRunnable. +CertErrorRunnable* +CreateCertErrorRunnable(CertVerifier& certVerifier, + PRErrorCode defaultErrorCodeToReport, + nsNSSSocketInfo* infoObject, + const UniqueCERTCertificate& cert, + const void* fdForLogging, + uint32_t providerFlags, + PRTime now) +{ + MOZ_ASSERT(infoObject); + MOZ_ASSERT(cert); + + uint32_t probeValue = MapCertErrorToProbeValue(defaultErrorCodeToReport); + Telemetry::Accumulate(Telemetry::SSL_CERT_VERIFICATION_ERRORS, probeValue); + + uint32_t collected_errors = 0; + PRErrorCode errorCodeTrust = 0; + PRErrorCode errorCodeMismatch = 0; + PRErrorCode errorCodeTime = 0; + if (DetermineCertOverrideErrors(cert, infoObject->GetHostNameRaw(), now, + defaultErrorCodeToReport, collected_errors, + errorCodeTrust, errorCodeMismatch, + errorCodeTime) != SECSuccess) { + // Attempt to enforce that if DetermineCertOverrideErrors failed, + // PR_SetError was set with a non-overridable error. This is because if we + // return from CreateCertErrorRunnable without calling + // infoObject->SetStatusErrorBits, we won't have the required information + // to actually add a certificate error override. This results in a broken + // UI which is annoying but not a security disaster. + MOZ_ASSERT(!ErrorIsOverridable(PR_GetError())); + return nullptr; + } + + RefPtr<nsNSSCertificate> nssCert(nsNSSCertificate::Create(cert.get())); + if (!nssCert) { + NS_ERROR("nsNSSCertificate::Create failed"); + PR_SetError(SEC_ERROR_NO_MEMORY, 0); + return nullptr; + } + + if (!collected_errors) { + // This will happen when CERT_*Verify* only returned error(s) that are + // not on our whitelist of overridable certificate errors. + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("[%p] !collected_errors: %d\n", + fdForLogging, static_cast<int>(defaultErrorCodeToReport))); + PR_SetError(defaultErrorCodeToReport, 0); + return nullptr; + } + + infoObject->SetStatusErrorBits(nssCert, collected_errors); + + return new CertErrorRunnable(fdForLogging, + static_cast<nsIX509Cert*>(nssCert.get()), + infoObject, defaultErrorCodeToReport, + collected_errors, errorCodeTrust, + errorCodeMismatch, errorCodeTime, + providerFlags); +} + +// When doing async cert processing, we dispatch one of these runnables to the +// socket transport service thread, which blocks the socket transport +// service thread while it waits for the inner CertErrorRunnable to execute +// CheckCertOverrides on the main thread. CheckCertOverrides must block events +// on both of these threads because it calls TransportSecurityInfo::GetInterface(), +// which may call nsHttpConnection::GetInterface() through +// TransportSecurityInfo::mCallbacks. nsHttpConnection::GetInterface must always +// execute on the main thread, with the socket transport service thread +// blocked. +class CertErrorRunnableRunnable : public Runnable +{ +public: + explicit CertErrorRunnableRunnable(CertErrorRunnable* certErrorRunnable) + : mCertErrorRunnable(certErrorRunnable) + { + } +private: + NS_IMETHOD Run() override + { + nsresult rv = mCertErrorRunnable->DispatchToMainThreadAndWait(); + // The result must run on the socket transport thread, which we are already + // on, so we can just run it directly, instead of dispatching it. + if (NS_SUCCEEDED(rv)) { + rv = mCertErrorRunnable->mResult ? mCertErrorRunnable->mResult->Run() + : NS_ERROR_UNEXPECTED; + } + return rv; + } + RefPtr<CertErrorRunnable> mCertErrorRunnable; +}; + +class SSLServerCertVerificationJob : public Runnable +{ +public: + // Must be called only on the socket transport thread + static SECStatus Dispatch(const RefPtr<SharedCertVerifier>& certVerifier, + const void* fdForLogging, + nsNSSSocketInfo* infoObject, + const UniqueCERTCertificate& serverCert, + const UniqueCERTCertList& peerCertChain, + const SECItem* stapledOCSPResponse, + const SECItem* sctsFromTLSExtension, + uint32_t providerFlags, + Time time, + PRTime prtime); +private: + NS_DECL_NSIRUNNABLE + + // Must be called only on the socket transport thread + SSLServerCertVerificationJob(const RefPtr<SharedCertVerifier>& certVerifier, + const void* fdForLogging, + nsNSSSocketInfo* infoObject, + const UniqueCERTCertificate& cert, + UniqueCERTCertList peerCertChain, + const SECItem* stapledOCSPResponse, + const SECItem* sctsFromTLSExtension, + uint32_t providerFlags, + Time time, + PRTime prtime); + const RefPtr<SharedCertVerifier> mCertVerifier; + const void* const mFdForLogging; + const RefPtr<nsNSSSocketInfo> mInfoObject; + const UniqueCERTCertificate mCert; + UniqueCERTCertList mPeerCertChain; + const uint32_t mProviderFlags; + const Time mTime; + const PRTime mPRTime; + const TimeStamp mJobStartTime; + const UniqueSECItem mStapledOCSPResponse; + const UniqueSECItem mSCTsFromTLSExtension; +}; + +SSLServerCertVerificationJob::SSLServerCertVerificationJob( + const RefPtr<SharedCertVerifier>& certVerifier, const void* fdForLogging, + nsNSSSocketInfo* infoObject, const UniqueCERTCertificate& cert, + UniqueCERTCertList peerCertChain, const SECItem* stapledOCSPResponse, + const SECItem* sctsFromTLSExtension, + uint32_t providerFlags, Time time, PRTime prtime) + : mCertVerifier(certVerifier) + , mFdForLogging(fdForLogging) + , mInfoObject(infoObject) + , mCert(CERT_DupCertificate(cert.get())) + , mPeerCertChain(Move(peerCertChain)) + , mProviderFlags(providerFlags) + , mTime(time) + , mPRTime(prtime) + , mJobStartTime(TimeStamp::Now()) + , mStapledOCSPResponse(SECITEM_DupItem(stapledOCSPResponse)) + , mSCTsFromTLSExtension(SECITEM_DupItem(sctsFromTLSExtension)) +{ +} + +// This function assumes that we will only use the SPDY connection coalescing +// feature on connections where we have negotiated SPDY using NPN. If we ever +// talk SPDY without having negotiated it with SPDY, this code will give wrong +// and perhaps unsafe results. +// +// Returns SECSuccess on the initial handshake of all connections, on +// renegotiations for any connections where we did not negotiate SPDY, or on any +// SPDY connection where the server's certificate did not change. +// +// Prohibit changing the server cert only if we negotiated SPDY, +// in order to support SPDY's cross-origin connection pooling. +static SECStatus +BlockServerCertChangeForSpdy(nsNSSSocketInfo* infoObject, + const UniqueCERTCertificate& serverCert) +{ + // Get the existing cert. If there isn't one, then there is + // no cert change to worry about. + nsCOMPtr<nsIX509Cert> cert; + + RefPtr<nsSSLStatus> status(infoObject->SSLStatus()); + if (!status) { + // If we didn't have a status, then this is the + // first handshake on this connection, not a + // renegotiation. + return SECSuccess; + } + + status->GetServerCert(getter_AddRefs(cert)); + if (!cert) { + NS_NOTREACHED("every nsSSLStatus must have a cert" + "that implements nsIX509Cert"); + PR_SetError(SEC_ERROR_LIBRARY_FAILURE, 0); + return SECFailure; + } + + // Filter out sockets that did not neogtiate SPDY via NPN + nsAutoCString negotiatedNPN; + nsresult rv = infoObject->GetNegotiatedNPN(negotiatedNPN); + NS_ASSERTION(NS_SUCCEEDED(rv), + "GetNegotiatedNPN() failed during renegotiation"); + + if (NS_SUCCEEDED(rv) && !StringBeginsWith(negotiatedNPN, + NS_LITERAL_CSTRING("spdy/"))) { + return SECSuccess; + } + // If GetNegotiatedNPN() failed we will assume spdy for safety's safe + if (NS_FAILED(rv)) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("BlockServerCertChangeForSpdy failed GetNegotiatedNPN() call." + " Assuming spdy.\n")); + } + + // Check to see if the cert has actually changed + UniqueCERTCertificate c(cert->GetCert()); + NS_ASSERTION(c, "very bad and hopefully impossible state"); + bool sameCert = CERT_CompareCerts(c.get(), serverCert.get()); + if (sameCert) { + return SECSuccess; + } + + // Report an error - changed cert is confirmed + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("SPDY Refused to allow new cert during renegotiation\n")); + PR_SetError(SSL_ERROR_RENEGOTIATION_NOT_ALLOWED, 0); + return SECFailure; +} + +void +AccumulateSubjectCommonNameTelemetry(const char* commonName, + bool commonNameInSubjectAltNames) +{ + if (!commonName) { + // 1 means no common name present + Telemetry::Accumulate(Telemetry::BR_9_2_2_SUBJECT_COMMON_NAME, 1); + } else if (!commonNameInSubjectAltNames) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("BR telemetry: common name '%s' not in subject alt. names " + "(or the subject alt. names extension is not present)\n", + commonName)); + // 2 means the common name is not present in subject alt names + Telemetry::Accumulate(Telemetry::BR_9_2_2_SUBJECT_COMMON_NAME, 2); + } else { + // 0 means the common name is present in subject alt names + Telemetry::Accumulate(Telemetry::BR_9_2_2_SUBJECT_COMMON_NAME, 0); + } +} + +// Returns true if and only if commonName ends with altName (minus its leading +// "*"). altName has already been checked to be of the form "*.<something>". +// commonName may be NULL. +static bool +TryMatchingWildcardSubjectAltName(const char* commonName, + const nsACString& altName) +{ + return commonName && + StringEndsWith(nsDependentCString(commonName), Substring(altName, 1)); +} + +// Gathers telemetry on Baseline Requirements 9.2.1 (Subject Alternative +// Names Extension) and 9.2.2 (Subject Common Name Field). +// Specifically: +// - whether or not the subject common name field is present +// - whether or not the subject alternative names extension is present +// - if there is a malformed entry in the subject alt. names extension +// - if there is an entry in the subject alt. names extension corresponding +// to the subject common name +// Telemetry is only gathered for certificates that chain to a trusted root +// in Mozilla's Root CA program. +// certList consists of a validated certificate chain. The end-entity +// certificate is first and the root (trust anchor) is last. +void +GatherBaselineRequirementsTelemetry(const UniqueCERTCertList& certList) +{ + CERTCertListNode* endEntityNode = CERT_LIST_HEAD(certList); + CERTCertListNode* rootNode = CERT_LIST_TAIL(certList); + PR_ASSERT(!(CERT_LIST_END(endEntityNode, certList) || + CERT_LIST_END(rootNode, certList))); + if (CERT_LIST_END(endEntityNode, certList) || + CERT_LIST_END(rootNode, certList)) { + return; + } + CERTCertificate* cert = endEntityNode->cert; + PR_ASSERT(cert); + if (!cert) { + return; + } + UniquePORTString commonName(CERT_GetCommonName(&cert->subject)); + // This only applies to certificates issued by authorities in our root + // program. + CERTCertificate* rootCert = rootNode->cert; + PR_ASSERT(rootCert); + if (!rootCert) { + return; + } + bool isBuiltIn = false; + Result result = IsCertBuiltInRoot(rootCert, isBuiltIn); + if (result != Success || !isBuiltIn) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("BR telemetry: root certificate for '%s' is not a built-in root " + "(or IsCertBuiltInRoot failed)\n", commonName.get())); + return; + } + ScopedAutoSECItem altNameExtension; + SECStatus rv = CERT_FindCertExtension(cert, SEC_OID_X509_SUBJECT_ALT_NAME, + &altNameExtension); + if (rv != SECSuccess) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("BR telemetry: no subject alt names extension for '%s'\n", + commonName.get())); + // 1 means there is no subject alt names extension + Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 1); + AccumulateSubjectCommonNameTelemetry(commonName.get(), false); + return; + } + + UniquePLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE)); + CERTGeneralName* subjectAltNames = + CERT_DecodeAltNameExtension(arena.get(), &altNameExtension); + if (!subjectAltNames) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("BR telemetry: could not decode subject alt names for '%s'\n", + commonName.get())); + // 2 means the subject alt names extension could not be decoded + Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 2); + AccumulateSubjectCommonNameTelemetry(commonName.get(), false); + return; + } + + CERTGeneralName* currentName = subjectAltNames; + bool commonNameInSubjectAltNames = false; + bool nonDNSNameOrIPAddressPresent = false; + bool malformedDNSNameOrIPAddressPresent = false; + bool nonFQDNPresent = false; + do { + nsAutoCString altName; + if (currentName->type == certDNSName) { + altName.Assign(BitwiseCast<char*, unsigned char*>( + currentName->name.other.data), + currentName->name.other.len); + nsDependentCString altNameWithoutWildcard(altName, 0); + if (StringBeginsWith(altNameWithoutWildcard, NS_LITERAL_CSTRING("*."))) { + altNameWithoutWildcard.Rebind(altName, 2); + commonNameInSubjectAltNames |= + TryMatchingWildcardSubjectAltName(commonName.get(), altName); + } + // net_IsValidHostName appears to return true for valid IP addresses, + // which would be invalid for a DNS name. + // Note that the net_IsValidHostName check will catch things like + // "a.*.example.com". + if (!net_IsValidHostName(altNameWithoutWildcard) || + net_IsValidIPv4Addr(altName.get(), altName.Length()) || + net_IsValidIPv6Addr(altName.get(), altName.Length())) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("BR telemetry: DNSName '%s' not valid (for '%s')\n", + altName.get(), commonName.get())); + malformedDNSNameOrIPAddressPresent = true; + } + if (!altName.Contains('.')) { + nonFQDNPresent = true; + } + } else if (currentName->type == certIPAddress) { + // According to DNS.h, this includes space for the null-terminator + char buf[net::kNetAddrMaxCStrBufSize] = { 0 }; + PRNetAddr addr; + if (currentName->name.other.len == 4) { + addr.inet.family = PR_AF_INET; + memcpy(&addr.inet.ip, currentName->name.other.data, + currentName->name.other.len); + if (PR_NetAddrToString(&addr, buf, sizeof(buf) - 1) != PR_SUCCESS) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("BR telemetry: IPAddress (v4) not valid (for '%s')\n", + commonName.get())); + malformedDNSNameOrIPAddressPresent = true; + } else { + altName.Assign(buf); + } + } else if (currentName->name.other.len == 16) { + addr.inet.family = PR_AF_INET6; + memcpy(&addr.ipv6.ip, currentName->name.other.data, + currentName->name.other.len); + if (PR_NetAddrToString(&addr, buf, sizeof(buf) - 1) != PR_SUCCESS) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("BR telemetry: IPAddress (v6) not valid (for '%s')\n", + commonName.get())); + malformedDNSNameOrIPAddressPresent = true; + } else { + altName.Assign(buf); + } + } else { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("BR telemetry: IPAddress not valid (for '%s')\n", + commonName.get())); + malformedDNSNameOrIPAddressPresent = true; + } + } else { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("BR telemetry: non-DNSName, non-IPAddress present for '%s'\n", + commonName.get())); + nonDNSNameOrIPAddressPresent = true; + } + if (commonName && altName.Equals(commonName.get())) { + commonNameInSubjectAltNames = true; + } + currentName = CERT_GetNextGeneralName(currentName); + } while (currentName && currentName != subjectAltNames); + + if (nonDNSNameOrIPAddressPresent) { + // 3 means there's an entry that isn't an ip address or dns name + Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 3); + } + if (malformedDNSNameOrIPAddressPresent) { + // 4 means there's a malformed ip address or dns name entry + Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 4); + } + if (nonFQDNPresent) { + // 5 means there's a DNS name entry with a non-fully-qualified domain name + Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 5); + } + if (!nonDNSNameOrIPAddressPresent && !malformedDNSNameOrIPAddressPresent && + !nonFQDNPresent) { + // 0 means the extension is acceptable + Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 0); + } + + AccumulateSubjectCommonNameTelemetry(commonName.get(), + commonNameInSubjectAltNames); +} + +// Gather telemetry on whether the end-entity cert for a server has the +// required TLS Server Authentication EKU, or any others +void +GatherEKUTelemetry(const UniqueCERTCertList& certList) +{ + CERTCertListNode* endEntityNode = CERT_LIST_HEAD(certList); + CERTCertListNode* rootNode = CERT_LIST_TAIL(certList); + PR_ASSERT(!(CERT_LIST_END(endEntityNode, certList) || + CERT_LIST_END(rootNode, certList))); + if (CERT_LIST_END(endEntityNode, certList) || + CERT_LIST_END(rootNode, certList)) { + return; + } + CERTCertificate* endEntityCert = endEntityNode->cert; + PR_ASSERT(endEntityCert); + if (!endEntityCert) { + return; + } + + // Only log telemetry if the root CA is built-in + CERTCertificate* rootCert = rootNode->cert; + PR_ASSERT(rootCert); + if (!rootCert) { + return; + } + bool isBuiltIn = false; + Result rv = IsCertBuiltInRoot(rootCert, isBuiltIn); + if (rv != Success || !isBuiltIn) { + return; + } + + // Find the EKU extension, if present + bool foundEKU = false; + SECOidTag oidTag; + CERTCertExtension* ekuExtension = nullptr; + for (size_t i = 0; endEntityCert->extensions && endEntityCert->extensions[i]; + i++) { + oidTag = SECOID_FindOIDTag(&endEntityCert->extensions[i]->id); + if (oidTag == SEC_OID_X509_EXT_KEY_USAGE) { + foundEKU = true; + ekuExtension = endEntityCert->extensions[i]; + } + } + + if (!foundEKU) { + Telemetry::Accumulate(Telemetry::SSL_SERVER_AUTH_EKU, 0); + return; + } + + // Parse the EKU extension + UniqueCERTOidSequence ekuSequence( + CERT_DecodeOidSequence(&ekuExtension->value)); + if (!ekuSequence) { + return; + } + + // Search through the available EKUs + bool foundServerAuth = false; + bool foundOther = false; + for (SECItem** oids = ekuSequence->oids; oids && *oids; oids++) { + oidTag = SECOID_FindOIDTag(*oids); + if (oidTag == SEC_OID_EXT_KEY_USAGE_SERVER_AUTH) { + foundServerAuth = true; + } else { + foundOther = true; + } + } + + // Cases 3 is included only for completeness. It should never + // appear in these statistics, because CheckExtendedKeyUsage() + // should require the EKU extension, if present, to contain the + // value id_kp_serverAuth. + if (foundServerAuth && !foundOther) { + Telemetry::Accumulate(Telemetry::SSL_SERVER_AUTH_EKU, 1); + } else if (foundServerAuth && foundOther) { + Telemetry::Accumulate(Telemetry::SSL_SERVER_AUTH_EKU, 2); + } else if (!foundServerAuth) { + Telemetry::Accumulate(Telemetry::SSL_SERVER_AUTH_EKU, 3); + } +} + +// Gathers telemetry on which CA is the root of a given cert chain. +// If the root is a built-in root, then the telemetry makes a count +// by root. Roots that are not built-in are counted in one bin. +void +GatherRootCATelemetry(const UniqueCERTCertList& certList) +{ + CERTCertListNode* rootNode = CERT_LIST_TAIL(certList); + PR_ASSERT(rootNode); + if (!rootNode) { + return; + } + PR_ASSERT(!CERT_LIST_END(rootNode, certList)); + if (CERT_LIST_END(rootNode, certList)) { + return; + } + CERTCertificate* rootCert = rootNode->cert; + PR_ASSERT(rootCert); + if (!rootCert) { + return; + } + AccumulateTelemetryForRootCA(Telemetry::CERT_VALIDATION_SUCCESS_BY_CA, + rootCert); +} + +// These time are appoximate, i.e., doesn't account for leap seconds, etc +const uint64_t ONE_WEEK_IN_SECONDS = (7 * (24 * 60 *60)); +const uint64_t ONE_YEAR_IN_WEEKS = 52; + +// Gathers telemetry on the certificate lifetimes we observe in the wild +void +GatherEndEntityTelemetry(const UniqueCERTCertList& certList) +{ + CERTCertListNode* endEntityNode = CERT_LIST_HEAD(certList); + MOZ_ASSERT(endEntityNode && !CERT_LIST_END(endEntityNode, certList)); + if (!endEntityNode || CERT_LIST_END(endEntityNode, certList)) { + return; + } + + CERTCertificate* endEntityCert = endEntityNode->cert; + PR_ASSERT(endEntityCert); + if (!endEntityCert) { + return; + } + + PRTime notBefore; + PRTime notAfter; + + if (CERT_GetCertTimes(endEntityCert, ¬Before, ¬After) != SECSuccess) { + return; + } + + PR_ASSERT(notAfter > notBefore); + if (notAfter <= notBefore) { + return; + } + + uint64_t durationInWeeks = (notAfter - notBefore) + / PR_USEC_PER_SEC + / ONE_WEEK_IN_SECONDS; + + if (durationInWeeks > (2 * ONE_YEAR_IN_WEEKS)) { + durationInWeeks = (2 * ONE_YEAR_IN_WEEKS) + 1; + } + + Telemetry::Accumulate(Telemetry::SSL_OBSERVED_END_ENTITY_CERTIFICATE_LIFETIME, + durationInWeeks); +} + +// There are various things that we want to measure about certificate +// chains that we accept. This is a single entry point for all of them. +void +GatherSuccessfulValidationTelemetry(const UniqueCERTCertList& certList) +{ + GatherBaselineRequirementsTelemetry(certList); + GatherEKUTelemetry(certList); + GatherRootCATelemetry(certList); + GatherEndEntityTelemetry(certList); +} + +void +GatherTelemetryForSingleSCT(const ct::SignedCertificateTimestamp& sct) +{ + // See SSL_SCTS_ORIGIN in Histograms.json. + uint32_t origin = 0; + switch (sct.origin) { + case ct::SignedCertificateTimestamp::Origin::Embedded: + origin = 1; + break; + case ct::SignedCertificateTimestamp::Origin::TLSExtension: + origin = 2; + break; + case ct::SignedCertificateTimestamp::Origin::OCSPResponse: + origin = 3; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected SCT::Origin type"); + } + Telemetry::Accumulate(Telemetry::SSL_SCTS_ORIGIN, origin); + + // See SSL_SCTS_VERIFICATION_STATUS in Histograms.json. + uint32_t verificationStatus = 0; + switch (sct.verificationStatus) { + case ct::SignedCertificateTimestamp::VerificationStatus::OK: + verificationStatus = 1; + break; + case ct::SignedCertificateTimestamp::VerificationStatus::UnknownLog: + verificationStatus = 2; + break; + case ct::SignedCertificateTimestamp::VerificationStatus::InvalidSignature: + verificationStatus = 3; + break; + case ct::SignedCertificateTimestamp::VerificationStatus::InvalidTimestamp: + verificationStatus = 4; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected SCT::VerificationStatus type"); + } + Telemetry::Accumulate(Telemetry::SSL_SCTS_VERIFICATION_STATUS, + verificationStatus); +} + +void +GatherCertificateTransparencyTelemetry(const UniqueCERTCertList& certList, + const CertificateTransparencyInfo& info) +{ + if (!info.enabled) { + // No telemetry is gathered when CT is disabled. + return; + } + + if (!info.processedSCTs) { + // We didn't receive any SCT data for this connection. + Telemetry::Accumulate(Telemetry::SSL_SCTS_PER_CONNECTION, 0); + return; + } + + for (const ct::SignedCertificateTimestamp& sct : info.verifyResult.scts) { + GatherTelemetryForSingleSCT(sct); + } + + // Decoding errors are reported to the 0th bucket + // of the SSL_SCTS_VERIFICATION_STATUS enumerated probe. + for (size_t i = 0; i < info.verifyResult.decodingErrors; ++i) { + Telemetry::Accumulate(Telemetry::SSL_SCTS_VERIFICATION_STATUS, 0); + } + + // Handle the histogram of SCTs counts. + uint32_t sctsCount = static_cast<uint32_t>(info.verifyResult.scts.length()); + // Note that sctsCount can be 0 in case we've received SCT binary data, + // but it failed to parse (e.g. due to unsupported CT protocol version). + Telemetry::Accumulate(Telemetry::SSL_SCTS_PER_CONNECTION, sctsCount); +} + +// Note: Takes ownership of |peerCertChain| if SECSuccess is not returned. +SECStatus +AuthCertificate(CertVerifier& certVerifier, + nsNSSSocketInfo* infoObject, + const UniqueCERTCertificate& cert, + UniqueCERTCertList& peerCertChain, + const SECItem* stapledOCSPResponse, + const SECItem* sctsFromTLSExtension, + uint32_t providerFlags, + Time time) +{ + MOZ_ASSERT(infoObject); + MOZ_ASSERT(cert); + + // We want to avoid storing any intermediate cert information when browsing + // in private, transient contexts. + bool saveIntermediates = + !(providerFlags & nsISocketProvider::NO_PERMANENT_STORAGE); + + SECOidTag evOidPolicy; + UniqueCERTCertList certList; + CertVerifier::OCSPStaplingStatus ocspStaplingStatus = + CertVerifier::OCSP_STAPLING_NEVER_CHECKED; + KeySizeStatus keySizeStatus = KeySizeStatus::NeverChecked; + SHA1ModeResult sha1ModeResult = SHA1ModeResult::NeverChecked; + PinningTelemetryInfo pinningTelemetryInfo; + CertificateTransparencyInfo certificateTransparencyInfo; + + int flags = 0; + if (!infoObject->SharedState().IsOCSPStaplingEnabled() || + !infoObject->SharedState().IsOCSPMustStapleEnabled()) { + flags |= CertVerifier::FLAG_TLS_IGNORE_STATUS_REQUEST; + } + + Result rv = certVerifier.VerifySSLServerCert(cert, stapledOCSPResponse, + sctsFromTLSExtension, time, + infoObject, + infoObject->GetHostNameRaw(), + certList, saveIntermediates, + flags, infoObject-> + GetOriginAttributes(), + &evOidPolicy, + &ocspStaplingStatus, + &keySizeStatus, &sha1ModeResult, + &pinningTelemetryInfo, + &certificateTransparencyInfo); + + uint32_t evStatus = (rv != Success) ? 0 // 0 = Failure + : (evOidPolicy == SEC_OID_UNKNOWN) ? 1 // 1 = DV + : 2; // 2 = EV + Telemetry::Accumulate(Telemetry::CERT_EV_STATUS, evStatus); + + if (ocspStaplingStatus != CertVerifier::OCSP_STAPLING_NEVER_CHECKED) { + Telemetry::Accumulate(Telemetry::SSL_OCSP_STAPLING, ocspStaplingStatus); + } + if (keySizeStatus != KeySizeStatus::NeverChecked) { + Telemetry::Accumulate(Telemetry::CERT_CHAIN_KEY_SIZE_STATUS, + static_cast<uint32_t>(keySizeStatus)); + } + if (sha1ModeResult != SHA1ModeResult::NeverChecked) { + Telemetry::Accumulate(Telemetry::CERT_CHAIN_SHA1_POLICY_STATUS, + static_cast<uint32_t>(sha1ModeResult)); + } + + if (pinningTelemetryInfo.accumulateForRoot) { + Telemetry::Accumulate(Telemetry::CERT_PINNING_FAILURES_BY_CA, + pinningTelemetryInfo.rootBucket); + } + + if (pinningTelemetryInfo.accumulateResult) { + Telemetry::Accumulate(pinningTelemetryInfo.certPinningResultHistogram, + pinningTelemetryInfo.certPinningResultBucket); + } + + if (rv == Success) { + // Certificate verification succeeded. Delete any potential record of + // certificate error bits. + RememberCertErrorsTable::GetInstance().RememberCertHasError(infoObject, + nullptr, + SECSuccess); + GatherSuccessfulValidationTelemetry(certList); + GatherCertificateTransparencyTelemetry(certList, + certificateTransparencyInfo); + + // The connection may get terminated, for example, if the server requires + // a client cert. Let's provide a minimal SSLStatus + // to the caller that contains at least the cert and its status. + RefPtr<nsSSLStatus> status(infoObject->SSLStatus()); + if (!status) { + status = new nsSSLStatus(); + infoObject->SetSSLStatus(status); + } + + if (!status->HasServerCert()) { + EVStatus evStatus; + if (evOidPolicy == SEC_OID_UNKNOWN) { + evStatus = EVStatus::NotEV; + } else { + evStatus = EVStatus::EV; + } + + RefPtr<nsNSSCertificate> nsc = nsNSSCertificate::Create(cert.get()); + status->SetServerCert(nsc, evStatus); + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("AuthCertificate setting NEW cert %p", nsc.get())); + } + + status->SetCertificateTransparencyInfo(certificateTransparencyInfo); + } + + if (rv != Success) { + // Certificate validation failed; store the peer certificate chain on + // infoObject so it can be used for error reporting. + infoObject->SetFailedCertChain(Move(peerCertChain)); + PR_SetError(MapResultToPRErrorCode(rv), 0); + } + + return rv == Success ? SECSuccess : SECFailure; +} + +/*static*/ SECStatus +SSLServerCertVerificationJob::Dispatch( + const RefPtr<SharedCertVerifier>& certVerifier, + const void* fdForLogging, + nsNSSSocketInfo* infoObject, + const UniqueCERTCertificate& serverCert, + const UniqueCERTCertList& peerCertChain, + const SECItem* stapledOCSPResponse, + const SECItem* sctsFromTLSExtension, + uint32_t providerFlags, + Time time, + PRTime prtime) +{ + // Runs on the socket transport thread + if (!certVerifier || !infoObject || !serverCert) { + NS_ERROR("Invalid parameters for SSL server cert validation"); + PR_SetError(PR_INVALID_ARGUMENT_ERROR, 0); + return SECFailure; + } + + // Copy the certificate list so the runnable can take ownership of it in the + // constructor. + // We can safely skip checking if NSS has already shut down here since we're + // in the middle of verifying a certificate. + nsNSSShutDownPreventionLock lock; + UniqueCERTCertList peerCertChainCopy = + nsNSSCertList::DupCertList(peerCertChain, lock); + if (!peerCertChainCopy) { + PR_SetError(SEC_ERROR_NO_MEMORY, 0); + return SECFailure; + } + + RefPtr<SSLServerCertVerificationJob> job( + new SSLServerCertVerificationJob(certVerifier, fdForLogging, infoObject, + serverCert, Move(peerCertChainCopy), + stapledOCSPResponse, sctsFromTLSExtension, + providerFlags, time, prtime)); + + nsresult nrv; + if (!gCertVerificationThreadPool) { + nrv = NS_ERROR_NOT_INITIALIZED; + } else { + nrv = gCertVerificationThreadPool->Dispatch(job, NS_DISPATCH_NORMAL); + } + if (NS_FAILED(nrv)) { + // We can't call SetCertVerificationResult here to change + // mCertVerificationState because SetCertVerificationResult will call + // libssl functions that acquire SSL locks that are already being held at + // this point. infoObject->mCertVerificationState will be stuck at + // waiting_for_cert_verification here, but that is OK because we already + // have to be able to handle cases where we encounter non-cert errors while + // in that state. + PRErrorCode error = nrv == NS_ERROR_OUT_OF_MEMORY + ? SEC_ERROR_NO_MEMORY + : PR_INVALID_STATE_ERROR; + PORT_SetError(error); + return SECFailure; + } + + PORT_SetError(PR_WOULD_BLOCK_ERROR); + return SECWouldBlock; +} + +NS_IMETHODIMP +SSLServerCertVerificationJob::Run() +{ + // Runs on a cert verification thread + + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p] SSLServerCertVerificationJob::Run\n", mInfoObject.get())); + + PRErrorCode error; + + nsNSSShutDownPreventionLock nssShutdownPrevention; + if (mInfoObject->isAlreadyShutDown()) { + error = SEC_ERROR_USER_CANCELLED; + } else { + Telemetry::ID successTelemetry + = Telemetry::SSL_SUCCESFUL_CERT_VALIDATION_TIME_MOZILLAPKIX; + Telemetry::ID failureTelemetry + = Telemetry::SSL_INITIAL_FAILED_CERT_VALIDATION_TIME_MOZILLAPKIX; + + // Reset the error code here so we can detect if AuthCertificate fails to + // set the error code if/when it fails. + PR_SetError(0, 0); + SECStatus rv = AuthCertificate(*mCertVerifier, mInfoObject, mCert, + mPeerCertChain, mStapledOCSPResponse.get(), + mSCTsFromTLSExtension.get(), + mProviderFlags, mTime); + MOZ_ASSERT(mPeerCertChain || rv != SECSuccess, + "AuthCertificate() should take ownership of chain on failure"); + if (rv == SECSuccess) { + uint32_t interval = (uint32_t) ((TimeStamp::Now() - mJobStartTime).ToMilliseconds()); + RefPtr<SSLServerCertVerificationResult> restart( + new SSLServerCertVerificationResult(mInfoObject, 0, + successTelemetry, interval)); + restart->Dispatch(); + Telemetry::Accumulate(Telemetry::SSL_CERT_ERROR_OVERRIDES, 1); + return NS_OK; + } + + // Note: the interval is not calculated once as PR_GetError MUST be called + // before any other function call + error = PR_GetError(); + { + TimeStamp now = TimeStamp::Now(); + MutexAutoLock telemetryMutex(*gSSLVerificationTelemetryMutex); + Telemetry::AccumulateTimeDelta(failureTelemetry, mJobStartTime, now); + } + if (error != 0) { + RefPtr<CertErrorRunnable> runnable( + CreateCertErrorRunnable(*mCertVerifier, error, mInfoObject, mCert, + mFdForLogging, mProviderFlags, mPRTime)); + if (!runnable) { + // CreateCertErrorRunnable set a new error code + error = PR_GetError(); + } else { + // We must block the the socket transport service thread while the + // main thread executes the CertErrorRunnable. The CertErrorRunnable + // will dispatch the result asynchronously, so we don't have to block + // this thread waiting for it. + + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p][%p] Before dispatching CertErrorRunnable\n", + mFdForLogging, runnable.get())); + + nsresult nrv; + nsCOMPtr<nsIEventTarget> stsTarget + = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &nrv); + if (NS_SUCCEEDED(nrv)) { + nrv = stsTarget->Dispatch(new CertErrorRunnableRunnable(runnable), + NS_DISPATCH_NORMAL); + } + if (NS_SUCCEEDED(nrv)) { + return NS_OK; + } + + NS_ERROR("Failed to dispatch CertErrorRunnable"); + error = PR_INVALID_STATE_ERROR; + } + } + } + + if (error == 0) { + NS_NOTREACHED("no error set during certificate validation failure"); + error = PR_INVALID_STATE_ERROR; + } + + RefPtr<SSLServerCertVerificationResult> failure( + new SSLServerCertVerificationResult(mInfoObject, error)); + failure->Dispatch(); + return NS_OK; +} + +} // unnamed namespace + +// Extracts whatever information we need out of fd (using SSL_*) and passes it +// to SSLServerCertVerificationJob::Dispatch. SSLServerCertVerificationJob should +// never do anything with fd except logging. +SECStatus +AuthCertificateHook(void* arg, PRFileDesc* fd, PRBool checkSig, PRBool isServer) +{ + RefPtr<SharedCertVerifier> certVerifier(GetDefaultCertVerifier()); + if (!certVerifier) { + PR_SetError(SEC_ERROR_NOT_INITIALIZED, 0); + return SECFailure; + } + + // Runs on the socket transport thread + + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p] starting AuthCertificateHook\n", fd)); + + // Modern libssl always passes PR_TRUE for checkSig, and we have no means of + // doing verification without checking signatures. + NS_ASSERTION(checkSig, "AuthCertificateHook: checkSig unexpectedly false"); + + // PSM never causes libssl to call this function with PR_TRUE for isServer, + // and many things in PSM assume that we are a client. + NS_ASSERTION(!isServer, "AuthCertificateHook: isServer unexpectedly true"); + + nsNSSSocketInfo* socketInfo = static_cast<nsNSSSocketInfo*>(arg); + + UniqueCERTCertificate serverCert(SSL_PeerCertificate(fd)); + + if (!checkSig || isServer || !socketInfo || !serverCert) { + PR_SetError(PR_INVALID_STATE_ERROR, 0); + return SECFailure; + } + + // Get the peer certificate chain for error reporting + UniqueCERTCertList peerCertChain(SSL_PeerCertificateChain(fd)); + if (!peerCertChain) { + PR_SetError(PR_INVALID_STATE_ERROR, 0); + return SECFailure; + } + + socketInfo->SetFullHandshake(); + + Time now(Now()); + PRTime prnow(PR_Now()); + + if (BlockServerCertChangeForSpdy(socketInfo, serverCert) != SECSuccess) + return SECFailure; + + nsCOMPtr<nsISSLSocketControl> sslSocketControl = do_QueryInterface( + NS_ISUPPORTS_CAST(nsITransportSecurityInfo*, socketInfo)); + if (sslSocketControl && sslSocketControl->GetBypassAuthentication()) { + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p] Bypass Auth in AuthCertificateHook\n", fd)); + return SECSuccess; + } + + bool onSTSThread; + nsresult nrv; + nsCOMPtr<nsIEventTarget> sts + = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &nrv); + if (NS_SUCCEEDED(nrv)) { + nrv = sts->IsOnCurrentThread(&onSTSThread); + } + + if (NS_FAILED(nrv)) { + NS_ERROR("Could not get STS service or IsOnCurrentThread failed"); + PR_SetError(PR_UNKNOWN_ERROR, 0); + return SECFailure; + } + + // SSL_PeerStapledOCSPResponses will never return a non-empty response if + // OCSP stapling wasn't enabled because libssl wouldn't have let the server + // return a stapled OCSP response. + // We don't own these pointers. + const SECItemArray* csa = SSL_PeerStapledOCSPResponses(fd); + SECItem* stapledOCSPResponse = nullptr; + // we currently only support single stapled responses + if (csa && csa->len == 1) { + stapledOCSPResponse = &csa->items[0]; + } + + const SECItem* sctsFromTLSExtension = SSL_PeerSignedCertTimestamps(fd); + if (sctsFromTLSExtension && sctsFromTLSExtension->len == 0) { + // SSL_PeerSignedCertTimestamps returns null on error and empty item + // when no extension was returned by the server. We always use null when + // no extension was received (for whatever reason), ignoring errors. + sctsFromTLSExtension = nullptr; + } + + uint32_t providerFlags = 0; + socketInfo->GetProviderFlags(&providerFlags); + + if (onSTSThread) { + + // We *must* do certificate verification on a background thread because + // we need the socket transport thread to be free for our OCSP requests, + // and we *want* to do certificate verification on a background thread + // because of the performance benefits of doing so. + socketInfo->SetCertVerificationWaiting(); + SECStatus rv = SSLServerCertVerificationJob::Dispatch( + certVerifier, static_cast<const void*>(fd), socketInfo, + serverCert, peerCertChain, stapledOCSPResponse, + sctsFromTLSExtension, providerFlags, now, prnow); + return rv; + } + + // We can't do certificate verification on a background thread, because the + // thread doing the network I/O may not interrupt its network I/O on receipt + // of our SSLServerCertVerificationResult event, and/or it might not even be + // a non-blocking socket. + + SECStatus rv = AuthCertificate(*certVerifier, socketInfo, serverCert, + peerCertChain, stapledOCSPResponse, + sctsFromTLSExtension, providerFlags, now); + MOZ_ASSERT(peerCertChain || rv != SECSuccess, + "AuthCertificate() should take ownership of chain on failure"); + if (rv == SECSuccess) { + Telemetry::Accumulate(Telemetry::SSL_CERT_ERROR_OVERRIDES, 1); + return SECSuccess; + } + + PRErrorCode error = PR_GetError(); + if (error != 0) { + RefPtr<CertErrorRunnable> runnable( + CreateCertErrorRunnable(*certVerifier, error, socketInfo, serverCert, + static_cast<const void*>(fd), providerFlags, + prnow)); + if (!runnable) { + // CreateCertErrorRunnable sets a new error code when it fails + error = PR_GetError(); + } else { + // We have to return SECSuccess or SECFailure based on the result of the + // override processing, so we must block this thread waiting for it. The + // CertErrorRunnable will NOT dispatch the result at all, since we passed + // false for CreateCertErrorRunnable's async parameter + nrv = runnable->DispatchToMainThreadAndWait(); + if (NS_FAILED(nrv)) { + NS_ERROR("Failed to dispatch CertErrorRunnable"); + PR_SetError(PR_INVALID_STATE_ERROR, 0); + return SECFailure; + } + + if (!runnable->mResult) { + NS_ERROR("CertErrorRunnable did not set result"); + PR_SetError(PR_INVALID_STATE_ERROR, 0); + return SECFailure; + } + + if (runnable->mResult->mErrorCode == 0) { + return SECSuccess; // cert error override occurred. + } + + // We must call SetCanceled here to set the error message type + // in case it isn't PlainErrorMessage, which is what we would + // default to if we just called + // PR_SetError(runnable->mResult->mErrorCode, 0) and returned + // SECFailure without doing this. + socketInfo->SetCanceled(runnable->mResult->mErrorCode, + runnable->mResult->mErrorMessageType); + error = runnable->mResult->mErrorCode; + } + } + + if (error == 0) { + NS_ERROR("error code not set"); + error = PR_UNKNOWN_ERROR; + } + + PR_SetError(error, 0); + return SECFailure; +} + +SSLServerCertVerificationResult::SSLServerCertVerificationResult( + nsNSSSocketInfo* infoObject, PRErrorCode errorCode, + Telemetry::ID telemetryID, uint32_t telemetryValue, + SSLErrorMessageType errorMessageType) + : mInfoObject(infoObject) + , mErrorCode(errorCode) + , mErrorMessageType(errorMessageType) + , mTelemetryID(telemetryID) + , mTelemetryValue(telemetryValue) +{ +// We accumulate telemetry for (only) successful validations on the main thread +// to avoid adversely affecting performance by acquiring the mutex that we use +// when accumulating the telemetry for unsuccessful validations. Unsuccessful +// validations times are accumulated elsewhere. +MOZ_ASSERT(telemetryID == Telemetry::HistogramCount || errorCode == 0); +} + +void +SSLServerCertVerificationResult::Dispatch() +{ + nsresult rv; + nsCOMPtr<nsIEventTarget> stsTarget + = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + NS_ASSERTION(stsTarget, + "Failed to get socket transport service event target"); + rv = stsTarget->Dispatch(this, NS_DISPATCH_NORMAL); + NS_ASSERTION(NS_SUCCEEDED(rv), + "Failed to dispatch SSLServerCertVerificationResult"); +} + +NS_IMETHODIMP +SSLServerCertVerificationResult::Run() +{ + // TODO: Assert that we're on the socket transport thread + if (mTelemetryID != Telemetry::HistogramCount) { + Telemetry::Accumulate(mTelemetryID, mTelemetryValue); + } + // XXX: This cast will be removed by the next patch + ((nsNSSSocketInfo*) mInfoObject.get()) + ->SetCertVerificationResult(mErrorCode, mErrorMessageType); + return NS_OK; +} + +} } // namespace mozilla::psm |