summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/TLSClientAuthCertSelection.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'security/manager/ssl/TLSClientAuthCertSelection.cpp')
-rw-r--r--security/manager/ssl/TLSClientAuthCertSelection.cpp926
1 files changed, 926 insertions, 0 deletions
diff --git a/security/manager/ssl/TLSClientAuthCertSelection.cpp b/security/manager/ssl/TLSClientAuthCertSelection.cpp
new file mode 100644
index 0000000000..8a2e7fd8b6
--- /dev/null
+++ b/security/manager/ssl/TLSClientAuthCertSelection.cpp
@@ -0,0 +1,926 @@
+/* -*- 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/. */
+
+// Implements the client authentication certificate selection callback for NSS.
+// nsNSSIOLayer.cpp sets the callback by calling SSL_GetClientAuthDataHook and
+// identifying SSLGetClientAuthDataHook as the function to call when a TLS
+// server requests a client authentication certificate.
+//
+// In the general case, SSLGetClientAuthDataHook (running on the socket thread),
+// dispatches an event to the main thread to ask the user to select a client
+// authentication certificate. Meanwhile, it returns SECWouldBlock so that other
+// network I/O can occur. When the user selects a client certificate (or opts
+// not to send one), an event is dispatched to the socket thread that gives NSS
+// the appropriate information to proceed with the TLS connection.
+//
+// If networking is being done on the socket process, SSLGetClientAuthDataHook
+// sends an IPC call to the parent process to ask the user to select a
+// certificate. Meanwhile, it again returns SECWouldBlock so other network I/O
+// can occur. When a certificate (or no certificate) has been selected, the
+// parent process sends an IPC call back to the socket process, which causes an
+// event to be dispatched to the socket thread to continue to the TLS
+// connection.
+
+#include "TLSClientAuthCertSelection.h"
+#include "cert_storage/src/cert_storage.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/psm/SelectTLSClientAuthCertChild.h"
+#include "mozilla/psm/SelectTLSClientAuthCertParent.h"
+#include "nsArray.h"
+#include "nsArrayUtils.h"
+#include "nsNSSComponent.h"
+#include "nsIClientAuthDialogs.h"
+#include "nsIMutableArray.h"
+#include "nsINSSComponent.h"
+#include "NSSCertDBTrustDomain.h"
+#include "nsIClientAuthRememberService.h"
+#include "nsIX509CertDB.h"
+#include "nsNSSHelper.h"
+#include "mozpkix/pkixnss.h"
+#include "mozpkix/pkixutil.h"
+#include "mozpkix/pkix.h"
+#include "secerr.h"
+#include "sslerr.h"
+
+using namespace mozilla;
+using namespace mozilla::pkix;
+using namespace mozilla::psm;
+
+extern LazyLogModule gPIPNSSLog;
+
+// Possible behaviors for choosing a cert for client auth.
+enum class UserCertChoice {
+ // Ask the user to choose a cert.
+ Ask = 0,
+ // Automatically choose a cert.
+ Auto = 1,
+};
+
+// Returns the most appropriate user cert choice based on the value of the
+// security.default_personal_cert preference.
+UserCertChoice nsGetUserCertChoice() {
+ nsAutoCString value;
+ nsresult rv =
+ Preferences::GetCString("security.default_personal_cert", value);
+ if (NS_FAILED(rv)) {
+ return UserCertChoice::Ask;
+ }
+
+ // There are three cases for what the preference could be set to:
+ // 1. "Select Automatically" -> Auto.
+ // 2. "Ask Every Time" -> Ask.
+ // 3. Something else -> Ask. This might be a nickname from a migrated cert,
+ // but we no longer support this case.
+ return value.EqualsLiteral("Select Automatically") ? UserCertChoice::Auto
+ : UserCertChoice::Ask;
+}
+
+static bool hasExplicitKeyUsageNonRepudiation(CERTCertificate* cert) {
+ // There is no extension, v1 or v2 certificate
+ if (!cert->extensions) return false;
+
+ SECStatus srv;
+ SECItem keyUsageItem;
+ keyUsageItem.data = nullptr;
+
+ srv = CERT_FindKeyUsageExtension(cert, &keyUsageItem);
+ if (srv == SECFailure) return false;
+
+ unsigned char keyUsage = keyUsageItem.data[0];
+ PORT_Free(keyUsageItem.data);
+
+ return !!(keyUsage & KU_NON_REPUDIATION);
+}
+
+ClientAuthInfo::ClientAuthInfo(const nsACString& hostName,
+ const OriginAttributes& originAttributes,
+ int32_t port, uint32_t providerFlags,
+ uint32_t providerTlsFlags)
+ : mHostName(hostName),
+ mOriginAttributes(originAttributes),
+ mPort(port),
+ mProviderFlags(providerFlags),
+ mProviderTlsFlags(providerTlsFlags) {}
+
+ClientAuthInfo::ClientAuthInfo(ClientAuthInfo&& aOther) noexcept
+ : mHostName(std::move(aOther.mHostName)),
+ mOriginAttributes(std::move(aOther.mOriginAttributes)),
+ mPort(aOther.mPort),
+ mProviderFlags(aOther.mProviderFlags),
+ mProviderTlsFlags(aOther.mProviderTlsFlags) {}
+
+const nsACString& ClientAuthInfo::HostName() const { return mHostName; }
+
+const OriginAttributes& ClientAuthInfo::OriginAttributesRef() const {
+ return mOriginAttributes;
+}
+
+int32_t ClientAuthInfo::Port() const { return mPort; }
+
+uint32_t ClientAuthInfo::ProviderFlags() const { return mProviderFlags; }
+
+uint32_t ClientAuthInfo::ProviderTlsFlags() const { return mProviderTlsFlags; }
+
+nsTArray<nsTArray<uint8_t>> CollectCANames(CERTDistNames* caNames) {
+ MOZ_ASSERT(caNames);
+
+ nsTArray<nsTArray<uint8_t>> collectedCANames;
+ if (!caNames) {
+ return collectedCANames;
+ }
+
+ for (int i = 0; i < caNames->nnames; i++) {
+ nsTArray<uint8_t> caName;
+ caName.AppendElements(caNames->names[i].data, caNames->names[i].len);
+ collectedCANames.AppendElement(std::move(caName));
+ }
+ return collectedCANames;
+}
+
+// This TrustDomain only exists to facilitate the mozilla::pkix path building
+// algorithm. It considers any certificate with an issuer distinguished name in
+// the set of given CA names to be a trust anchor. It does essentially no
+// validation or verification (in particular, the signature checking function
+// always returns "Success").
+class ClientAuthCertNonverifyingTrustDomain final : public TrustDomain {
+ public:
+ ClientAuthCertNonverifyingTrustDomain(
+ nsTArray<nsTArray<uint8_t>>& caNames,
+ nsTArray<nsTArray<uint8_t>>& thirdPartyCertificates)
+ : mCANames(caNames),
+ mCertStorage(do_GetService(NS_CERT_STORAGE_CID)),
+ mThirdPartyCertificates(thirdPartyCertificates) {}
+
+ virtual mozilla::pkix::Result GetCertTrust(
+ pkix::EndEntityOrCA endEntityOrCA, const pkix::CertPolicyId& policy,
+ pkix::Input candidateCertDER,
+ /*out*/ pkix::TrustLevel& trustLevel) override;
+ virtual mozilla::pkix::Result FindIssuer(pkix::Input encodedIssuerName,
+ IssuerChecker& checker,
+ pkix::Time time) override;
+
+ virtual mozilla::pkix::Result CheckRevocation(
+ EndEntityOrCA endEntityOrCA, const pkix::CertID& certID, Time time,
+ mozilla::pkix::Duration validityDuration,
+ /*optional*/ const Input* stapledOCSPresponse,
+ /*optional*/ const Input* aiaExtension,
+ /*optional*/ const Input* sctExtension) override {
+ return pkix::Success;
+ }
+
+ virtual mozilla::pkix::Result IsChainValid(
+ const pkix::DERArray& certChain, pkix::Time time,
+ const pkix::CertPolicyId& requiredPolicy) override;
+
+ virtual mozilla::pkix::Result CheckSignatureDigestAlgorithm(
+ pkix::DigestAlgorithm digestAlg, pkix::EndEntityOrCA endEntityOrCA,
+ pkix::Time notBefore) override {
+ return pkix::Success;
+ }
+ virtual mozilla::pkix::Result CheckRSAPublicKeyModulusSizeInBits(
+ pkix::EndEntityOrCA endEntityOrCA,
+ unsigned int modulusSizeInBits) override {
+ return pkix::Success;
+ }
+ virtual mozilla::pkix::Result VerifyRSAPKCS1SignedData(
+ pkix::Input data, pkix::DigestAlgorithm, pkix::Input signature,
+ pkix::Input subjectPublicKeyInfo) override {
+ return pkix::Success;
+ }
+ virtual mozilla::pkix::Result VerifyRSAPSSSignedData(
+ pkix::Input data, pkix::DigestAlgorithm, pkix::Input signature,
+ pkix::Input subjectPublicKeyInfo) override {
+ return pkix::Success;
+ }
+ virtual mozilla::pkix::Result CheckECDSACurveIsAcceptable(
+ pkix::EndEntityOrCA endEntityOrCA, pkix::NamedCurve curve) override {
+ return pkix::Success;
+ }
+ virtual mozilla::pkix::Result VerifyECDSASignedData(
+ pkix::Input data, pkix::DigestAlgorithm, pkix::Input signature,
+ pkix::Input subjectPublicKeyInfo) override {
+ return pkix::Success;
+ }
+ virtual mozilla::pkix::Result CheckValidityIsAcceptable(
+ pkix::Time notBefore, pkix::Time notAfter,
+ pkix::EndEntityOrCA endEntityOrCA,
+ pkix::KeyPurposeId keyPurpose) override {
+ return pkix::Success;
+ }
+ virtual mozilla::pkix::Result NetscapeStepUpMatchesServerAuth(
+ pkix::Time notBefore,
+ /*out*/ bool& matches) override {
+ matches = true;
+ return pkix::Success;
+ }
+ virtual void NoteAuxiliaryExtension(pkix::AuxiliaryExtension extension,
+ pkix::Input extensionData) override {}
+ virtual mozilla::pkix::Result DigestBuf(pkix::Input item,
+ pkix::DigestAlgorithm digestAlg,
+ /*out*/ uint8_t* digestBuf,
+ size_t digestBufLen) override {
+ return pkix::DigestBufNSS(item, digestAlg, digestBuf, digestBufLen);
+ }
+
+ nsTArray<nsTArray<uint8_t>> TakeBuiltChain() {
+ return std::move(mBuiltChain);
+ }
+
+ private:
+ nsTArray<nsTArray<uint8_t>>& mCANames; // non-owning
+ nsCOMPtr<nsICertStorage> mCertStorage;
+ nsTArray<nsTArray<uint8_t>>& mThirdPartyCertificates; // non-owning
+ nsTArray<nsTArray<uint8_t>> mBuiltChain;
+};
+
+mozilla::pkix::Result ClientAuthCertNonverifyingTrustDomain::GetCertTrust(
+ pkix::EndEntityOrCA endEntityOrCA, const pkix::CertPolicyId& policy,
+ pkix::Input candidateCertDER,
+ /*out*/ pkix::TrustLevel& trustLevel) {
+ // If the server did not specify any CA names, all client certificates are
+ // acceptable.
+ if (mCANames.Length() == 0) {
+ trustLevel = pkix::TrustLevel::TrustAnchor;
+ return pkix::Success;
+ }
+ BackCert cert(candidateCertDER, endEntityOrCA, nullptr);
+ mozilla::pkix::Result rv = cert.Init();
+ if (rv != pkix::Success) {
+ return rv;
+ }
+ // If this certificate's issuer distinguished name is in the set of acceptable
+ // CA names, we say this is a trust anchor so that the client certificate
+ // issued from this certificate will be presented as an option for the user.
+ // We also check the certificate's subject distinguished name to account for
+ // the case where client certificates that have the id-kp-OCSPSigning EKU
+ // can't be trust anchors according to mozilla::pkix, and thus we may be
+ // looking directly at the issuer.
+ pkix::Input issuer(cert.GetIssuer());
+ pkix::Input subject(cert.GetSubject());
+ for (const auto& caName : mCANames) {
+ pkix::Input caNameInput;
+ rv = caNameInput.Init(caName.Elements(), caName.Length());
+ if (rv != pkix::Success) {
+ continue; // probably too big
+ }
+ if (InputsAreEqual(issuer, caNameInput) ||
+ InputsAreEqual(subject, caNameInput)) {
+ trustLevel = pkix::TrustLevel::TrustAnchor;
+ return pkix::Success;
+ }
+ }
+ trustLevel = pkix::TrustLevel::InheritsTrust;
+ return pkix::Success;
+}
+
+// In theory this implementation should only need to consider intermediate
+// certificates, since in theory it should only need to look at the issuer
+// distinguished name of each certificate to determine if the client
+// certificate is considered acceptable to the server.
+// However, because we need to account for client certificates with the
+// id-kp-OCSPSigning EKU, and because mozilla::pkix doesn't allow such
+// certificates to be trust anchors, we need to consider the issuers of such
+// certificates directly. These issuers could be roots, so we have to consider
+// roots here.
+mozilla::pkix::Result ClientAuthCertNonverifyingTrustDomain::FindIssuer(
+ pkix::Input encodedIssuerName, IssuerChecker& checker, pkix::Time time) {
+ // First try all relevant certificates known to Gecko, which avoids calling
+ // CERT_CreateSubjectCertList, because that can be expensive.
+ Vector<pkix::Input> geckoCandidates;
+ if (!mCertStorage) {
+ return mozilla::pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ nsTArray<uint8_t> subject;
+ subject.AppendElements(encodedIssuerName.UnsafeGetData(),
+ encodedIssuerName.GetLength());
+ nsTArray<nsTArray<uint8_t>> certs;
+ nsresult rv = mCertStorage->FindCertsBySubject(subject, certs);
+ if (NS_FAILED(rv)) {
+ return mozilla::pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ for (auto& cert : certs) {
+ pkix::Input certDER;
+ mozilla::pkix::Result rv = certDER.Init(cert.Elements(), cert.Length());
+ if (rv != pkix::Success) {
+ continue; // probably too big
+ }
+ if (!geckoCandidates.append(certDER)) {
+ return mozilla::pkix::Result::FATAL_ERROR_NO_MEMORY;
+ }
+ }
+
+ for (const auto& thirdPartyCertificate : mThirdPartyCertificates) {
+ pkix::Input thirdPartyCertificateInput;
+ mozilla::pkix::Result rv = thirdPartyCertificateInput.Init(
+ thirdPartyCertificate.Elements(), thirdPartyCertificate.Length());
+ if (rv != pkix::Success) {
+ continue; // probably too big
+ }
+ if (!geckoCandidates.append(thirdPartyCertificateInput)) {
+ return mozilla::pkix::Result::FATAL_ERROR_NO_MEMORY;
+ }
+ }
+
+ bool keepGoing = true;
+ for (pkix::Input candidate : geckoCandidates) {
+ mozilla::pkix::Result rv = checker.Check(candidate, nullptr, keepGoing);
+ if (rv != pkix::Success) {
+ return rv;
+ }
+ if (!keepGoing) {
+ return pkix::Success;
+ }
+ }
+
+ SECItem encodedIssuerNameItem =
+ pkix::UnsafeMapInputToSECItem(encodedIssuerName);
+ // NSS seems not to differentiate between "no potential issuers found" and
+ // "there was an error trying to retrieve the potential issuers." We assume
+ // there was no error if CERT_CreateSubjectCertList returns nullptr.
+ UniqueCERTCertList candidates(CERT_CreateSubjectCertList(
+ nullptr, CERT_GetDefaultCertDB(), &encodedIssuerNameItem, 0, false));
+ Vector<pkix::Input> nssCandidates;
+ if (candidates) {
+ for (CERTCertListNode* n = CERT_LIST_HEAD(candidates);
+ !CERT_LIST_END(n, candidates); n = CERT_LIST_NEXT(n)) {
+ pkix::Input certDER;
+ mozilla::pkix::Result rv =
+ certDER.Init(n->cert->derCert.data, n->cert->derCert.len);
+ if (rv != pkix::Success) {
+ continue; // probably too big
+ }
+ if (!nssCandidates.append(certDER)) {
+ return mozilla::pkix::Result::FATAL_ERROR_NO_MEMORY;
+ }
+ }
+ }
+
+ for (pkix::Input candidate : nssCandidates) {
+ mozilla::pkix::Result rv = checker.Check(candidate, nullptr, keepGoing);
+ if (rv != pkix::Success) {
+ return rv;
+ }
+ if (!keepGoing) {
+ return pkix::Success;
+ }
+ }
+ return pkix::Success;
+}
+
+mozilla::pkix::Result ClientAuthCertNonverifyingTrustDomain::IsChainValid(
+ const pkix::DERArray& certArray, pkix::Time, const pkix::CertPolicyId&) {
+ mBuiltChain.Clear();
+
+ size_t numCerts = certArray.GetLength();
+ for (size_t i = 0; i < numCerts; ++i) {
+ nsTArray<uint8_t> certBytes;
+ const pkix::Input* certInput = certArray.GetDER(i);
+ MOZ_ASSERT(certInput != nullptr);
+ if (!certInput) {
+ return mozilla::pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ certBytes.AppendElements(certInput->UnsafeGetData(),
+ certInput->GetLength());
+ mBuiltChain.AppendElement(std::move(certBytes));
+ }
+
+ return pkix::Success;
+}
+
+void ClientAuthCertificateSelectedBase::SetSelectedClientAuthData(
+ nsTArray<uint8_t>&& selectedCertBytes,
+ nsTArray<nsTArray<uint8_t>>&& selectedCertChainBytes) {
+ mSelectedCertBytes = std::move(selectedCertBytes);
+ mSelectedCertChainBytes = std::move(selectedCertChainBytes);
+}
+
+NS_IMETHODIMP
+ClientAuthCertificateSelected::Run() {
+ mSocketInfo->ClientAuthCertificateSelected(mSelectedCertBytes,
+ mSelectedCertChainBytes);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SelectClientAuthCertificate::Run() {
+ DoSelectClientAuthCertificate();
+ if (mSelectedCertBytes.Length() > 0) {
+ nsTArray<nsTArray<uint8_t>> selectedCertChainBytes;
+ if (BuildChainForCertificate(mSelectedCertBytes, selectedCertChainBytes) !=
+ pkix::Success) {
+ selectedCertChainBytes.Clear();
+ }
+ mContinuation->SetSelectedClientAuthData(std::move(mSelectedCertBytes),
+ std::move(selectedCertChainBytes));
+ }
+ nsCOMPtr<nsIEventTarget> socketThread =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID);
+ if (NS_WARN_IF(!socketThread)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = socketThread->Dispatch(mContinuation, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;
+}
+
+// Helper function to build a certificate chain from the given certificate to a
+// trust anchor in the set indicated by the peer (mCANames). This is essentially
+// best-effort, so no signature verification occurs.
+mozilla::pkix::Result SelectClientAuthCertificate::BuildChainForCertificate(
+ nsTArray<uint8_t>& certBytes, nsTArray<nsTArray<uint8_t>>& certChainBytes) {
+ ClientAuthCertNonverifyingTrustDomain trustDomain(mCANames,
+ mEnterpriseCertificates);
+ pkix::Input certDER;
+ mozilla::pkix::Result result =
+ certDER.Init(certBytes.Elements(), certBytes.Length());
+ if (result != pkix::Success) {
+ return result;
+ }
+ // Client certificates shouldn't be CAs, but for interoperability reasons we
+ // attempt to build a path with each certificate as an end entity and then as
+ // a CA if that fails.
+ const pkix::EndEntityOrCA kEndEntityOrCAParams[] = {
+ pkix::EndEntityOrCA::MustBeEndEntity, pkix::EndEntityOrCA::MustBeCA};
+ // mozilla::pkix rejects certificates with id-kp-OCSPSigning unless it is
+ // specifically required. A client certificate should never have this EKU.
+ // Unfortunately, there are some client certificates in private PKIs that
+ // have this EKU. For interoperability, we attempt to work around this
+ // restriction in mozilla::pkix by first building the certificate chain with
+ // no particular EKU required and then again with id-kp-OCSPSigning required
+ // if that fails.
+ const pkix::KeyPurposeId kKeyPurposeIdParams[] = {
+ pkix::KeyPurposeId::anyExtendedKeyUsage,
+ pkix::KeyPurposeId::id_kp_OCSPSigning};
+ for (const auto& endEntityOrCAParam : kEndEntityOrCAParams) {
+ for (const auto& keyPurposeIdParam : kKeyPurposeIdParams) {
+ mozilla::pkix::Result result = BuildCertChain(
+ trustDomain, certDER, Now(), endEntityOrCAParam,
+ KeyUsage::noParticularKeyUsageRequired, keyPurposeIdParam,
+ pkix::CertPolicyId::anyPolicy, nullptr);
+ if (result == pkix::Success) {
+ certChainBytes = trustDomain.TakeBuiltChain();
+ return pkix::Success;
+ }
+ }
+ }
+ return mozilla::pkix::Result::ERROR_UNKNOWN_ISSUER;
+}
+
+void SelectClientAuthCertificate::DoSelectClientAuthCertificate() {
+ // We check the value of a pref, so this should only be run on the main
+ // thread.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsINSSComponent> component(do_GetService(PSM_COMPONENT_CONTRACTID));
+ if (NS_WARN_IF(!component)) {
+ return;
+ }
+ nsresult rv = component->GetEnterpriseIntermediates(mEnterpriseCertificates);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ nsTArray<nsTArray<uint8_t>> enterpriseRoots;
+ rv = component->GetEnterpriseRoots(enterpriseRoots);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ mEnterpriseCertificates.AppendElements(std::move(enterpriseRoots));
+
+ if (NS_WARN_IF(NS_FAILED(CheckForSmartCardChanges()))) {
+ return;
+ }
+
+ if (!mPotentialClientCertificates) {
+ return;
+ }
+
+ CERTCertListNode* n = CERT_LIST_HEAD(mPotentialClientCertificates);
+ while (!CERT_LIST_END(n, mPotentialClientCertificates)) {
+ nsTArray<nsTArray<uint8_t>> unusedBuiltChain;
+ nsTArray<uint8_t> certBytes;
+ certBytes.AppendElements(n->cert->derCert.data, n->cert->derCert.len);
+ mozilla::pkix::Result result =
+ BuildChainForCertificate(certBytes, unusedBuiltChain);
+ if (result != pkix::Success) {
+ MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+ ("removing cert '%s'", n->cert->subjectName));
+ CERTCertListNode* toRemove = n;
+ n = CERT_LIST_NEXT(n);
+ CERT_RemoveCertListNode(toRemove);
+ continue;
+ }
+ MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+ ("keeping cert '%s'\n", n->cert->subjectName));
+ n = CERT_LIST_NEXT(n);
+ }
+
+ if (CERT_LIST_EMPTY(mPotentialClientCertificates)) {
+ MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+ ("no client certificates available after filtering by CA"));
+ return;
+ }
+
+ // find valid user cert and key pair
+ if (nsGetUserCertChoice() == UserCertChoice::Auto) {
+ // automatically find the right cert
+ UniqueCERTCertificate lowPrioNonrepCert;
+ // loop through the list until we find a cert with a key
+ for (CERTCertListNode* node = CERT_LIST_HEAD(mPotentialClientCertificates);
+ !CERT_LIST_END(node, mPotentialClientCertificates);
+ node = CERT_LIST_NEXT(node)) {
+ UniqueSECKEYPrivateKey tmpKey(PK11_FindKeyByAnyCert(node->cert, nullptr));
+ if (tmpKey) {
+ if (hasExplicitKeyUsageNonRepudiation(node->cert)) {
+ // Not a preferred cert
+ if (!lowPrioNonrepCert) { // did not yet find a low prio cert
+ lowPrioNonrepCert.reset(CERT_DupCertificate(node->cert));
+ }
+ } else {
+ // this is a good cert to present
+ mSelectedCertBytes.AppendElements(node->cert->derCert.data,
+ node->cert->derCert.len);
+ return;
+ }
+ }
+ if (PR_GetError() == SEC_ERROR_BAD_PASSWORD) {
+ // problem with password: bail
+ break;
+ }
+ }
+
+ if (lowPrioNonrepCert) {
+ mSelectedCertBytes.AppendElements(lowPrioNonrepCert->derCert.data,
+ lowPrioNonrepCert->derCert.len);
+ }
+ return;
+ }
+
+ // Not Auto => ask
+ // Get the SSL Certificate
+ const nsACString& hostname = mInfo.HostName();
+ nsCOMPtr<nsIClientAuthRememberService> cars = nullptr;
+
+ if (mInfo.ProviderTlsFlags() == 0) {
+ cars = do_GetService(NS_CLIENTAUTHREMEMBERSERVICE_CONTRACTID);
+ }
+
+ if (cars) {
+ nsCString rememberedDBKey;
+ bool found;
+ nsresult rv = cars->HasRememberedDecision(
+ hostname, mInfo.OriginAttributesRef(), rememberedDBKey, &found);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ if (found) {
+ // An empty dbKey indicates that the user chose not to use a certificate
+ // and chose to remember this decision
+ if (rememberedDBKey.IsEmpty()) {
+ return;
+ }
+ nsCOMPtr<nsIX509CertDB> certdb = do_GetService(NS_X509CERTDB_CONTRACTID);
+ if (NS_WARN_IF(!certdb)) {
+ return;
+ }
+ nsCOMPtr<nsIX509Cert> foundCert;
+ nsresult rv =
+ certdb->FindCertByDBKey(rememberedDBKey, getter_AddRefs(foundCert));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ if (foundCert) {
+ Unused << NS_WARN_IF(
+ NS_FAILED(foundCert->GetRawDER(mSelectedCertBytes)));
+ return;
+ }
+ }
+ }
+
+ // ask the user to select a certificate
+ nsCOMPtr<nsIClientAuthDialogs> dialogs;
+ UniquePORTString corg(CERT_GetOrgName(&mServerCert->subject));
+ nsAutoCString org(corg.get());
+
+ UniquePORTString cissuer(CERT_GetOrgName(&mServerCert->issuer));
+ nsAutoCString issuer(cissuer.get());
+
+ nsCOMPtr<nsIMutableArray> certArray = nsArrayBase::Create();
+ if (NS_WARN_IF(!certArray)) {
+ return;
+ }
+
+ for (CERTCertListNode* node = CERT_LIST_HEAD(mPotentialClientCertificates);
+ !CERT_LIST_END(node, mPotentialClientCertificates);
+ node = CERT_LIST_NEXT(node)) {
+ nsCOMPtr<nsIX509Cert> tempCert = new nsNSSCertificate(node->cert);
+ nsresult rv = certArray->AppendElement(tempCert);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ }
+
+ // Throw up the client auth dialog and get back the index of the selected
+ // cert
+ rv = getNSSDialogs(getter_AddRefs(dialogs), NS_GET_IID(nsIClientAuthDialogs),
+ NS_CLIENTAUTHDIALOGS_CONTRACTID);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ uint32_t selectedIndex = 0;
+ bool certChosen = false;
+
+ // even if the user has canceled, we want to remember that, to avoid
+ // repeating prompts
+ bool wantRemember = false;
+ rv =
+ dialogs->ChooseCertificate(hostname, mInfo.Port(), org, issuer, certArray,
+ &selectedIndex, &wantRemember, &certChosen);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsCOMPtr<nsIX509Cert> selectedCert;
+ if (certChosen) {
+ selectedCert = do_QueryElementAt(certArray, selectedIndex);
+ if (NS_WARN_IF(!selectedCert)) {
+ return;
+ }
+ if (NS_WARN_IF(NS_FAILED(selectedCert->GetRawDER(mSelectedCertBytes)))) {
+ return;
+ }
+ }
+
+ if (cars && wantRemember) {
+ rv = cars->RememberDecision(hostname, mInfo.OriginAttributesRef(),
+ selectedCert);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+}
+
+SECStatus SSLGetClientAuthDataHook(void* arg, PRFileDesc* socket,
+ CERTDistNames* caNamesDecoded,
+ CERTCertificate** pRetCert,
+ SECKEYPrivateKey** pRetKey) {
+ MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+ ("[%p] SSLGetClientAuthDataHook", socket));
+
+ if (!arg || !socket || !caNamesDecoded || !pRetCert || !pRetKey) {
+ PR_SetError(PR_INVALID_ARGUMENT_ERROR, 0);
+ return SECFailure;
+ }
+
+ *pRetCert = nullptr;
+ *pRetKey = nullptr;
+
+ RefPtr<NSSSocketControl> info(static_cast<NSSSocketControl*>(arg));
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_CLIENT_AUTH_CERT_USAGE,
+ u"requested"_ns, 1);
+
+ if (info->GetDenyClientCert()) {
+ MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+ ("[%p] Not returning client cert due to denyClientCert attribute",
+ socket));
+ return SECSuccess;
+ }
+
+ if (info->GetJoined()) {
+ // We refuse to send a client certificate when there are multiple hostnames
+ // joined on this connection, because we only show the user one hostname
+ // (mHostName) in the client certificate UI.
+ MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+ ("[%p] Not returning client cert due to previous join", socket));
+ return SECSuccess;
+ }
+
+ UniqueCERTCertificate serverCert(SSL_PeerCertificate(socket));
+ if (!serverCert) {
+ PR_SetError(SSL_ERROR_NO_CERTIFICATE, 0);
+ return SECFailure;
+ }
+
+ nsTArray<nsTArray<uint8_t>> caNames(CollectCANames(caNamesDecoded));
+
+ // Currently, the IPC client certs module only refreshes its view of
+ // available certificates and keys if the platform issues a search for all
+ // certificates or keys. In the socket process, such a search may not have
+ // happened, so this ensures it has.
+ // Additionally, instantiating certificates in NSS is not thread-safe and has
+ // performance implications, so search for them here (on the socket thread)
+ // when not in the socket process.
+ UniqueCERTCertList potentialClientCertificates(
+ FindClientCertificatesWithPrivateKeys());
+
+ RefPtr<ClientAuthCertificateSelected> continuation(
+ new ClientAuthCertificateSelected(info));
+ // If this is the socket process, dispatch an IPC call to select a client
+ // authentication certificate in the parent process.
+ // Otherwise, dispatch an event to the main thread to do the selection.
+ // When those events finish, they will run the continuation, which gives the
+ // appropriate information to the NSSSocketControl, which then calls
+ // SSL_ClientCertCallbackComplete to continue the connection.
+ if (XRE_IsSocketProcess()) {
+ RefPtr<SelectTLSClientAuthCertChild> selectClientAuthCertificate(
+ new SelectTLSClientAuthCertChild(continuation));
+ nsAutoCString hostname(info->GetHostName());
+ nsTArray<uint8_t> serverCertBytes;
+ nsTArray<ByteArray> caNamesBytes;
+ for (const auto& caName : caNames) {
+ caNamesBytes.AppendElement(ByteArray(std::move(caName)));
+ }
+ serverCertBytes.AppendElements(serverCert->derCert.data,
+ serverCert->derCert.len);
+ OriginAttributes originAttributes(info->GetOriginAttributes());
+ int32_t port(info->GetPort());
+ uint32_t providerFlags(info->GetProviderFlags());
+ uint32_t providerTlsFlags(info->GetProviderTlsFlags());
+ nsCOMPtr<nsIRunnable> remoteSelectClientAuthCertificate(
+ NS_NewRunnableFunction(
+ "RemoteSelectClientAuthCertificate",
+ [selectClientAuthCertificate(
+ std::move(selectClientAuthCertificate)),
+ hostname(std::move(hostname)),
+ originAttributes(std::move(originAttributes)), port, providerFlags,
+ providerTlsFlags, serverCertBytes(std::move(serverCertBytes)),
+ caNamesBytes(std::move(caNamesBytes))]() {
+ mozilla::ipc::PBackgroundChild* actorChild =
+ mozilla::ipc::BackgroundChild::
+ GetOrCreateForSocketParentBridgeForCurrentThread();
+ if (actorChild) {
+ Unused << actorChild->SendPSelectTLSClientAuthCertConstructor(
+ selectClientAuthCertificate, hostname, originAttributes,
+ port, providerFlags, providerTlsFlags,
+ ByteArray(serverCertBytes), caNamesBytes);
+ }
+ }));
+ info->SetPendingSelectClientAuthCertificate(
+ std::move(remoteSelectClientAuthCertificate));
+ } else {
+ ClientAuthInfo authInfo(info->GetHostName(), info->GetOriginAttributes(),
+ info->GetPort(), info->GetProviderFlags(),
+ info->GetProviderTlsFlags());
+ nsCOMPtr<nsIRunnable> selectClientAuthCertificate(
+ new SelectClientAuthCertificate(
+ std::move(authInfo), std::move(serverCert), std::move(caNames),
+ std::move(potentialClientCertificates), continuation));
+ info->SetPendingSelectClientAuthCertificate(
+ std::move(selectClientAuthCertificate));
+ }
+
+ // Meanwhile, tell NSS this connection is blocking for now.
+ PR_SetError(PR_WOULD_BLOCK_ERROR, 0);
+ return SECWouldBlock;
+}
+
+using mozilla::ipc::AssertIsOnBackgroundThread;
+using mozilla::ipc::IsOnBackgroundThread;
+
+// Helper continuation for when a client authentication certificate has been
+// selected in the parent process and the information needs to be sent to the
+// socket process.
+class RemoteClientAuthCertificateSelected
+ : public ClientAuthCertificateSelectedBase {
+ public:
+ explicit RemoteClientAuthCertificateSelected(
+ SelectTLSClientAuthCertParent* selectTLSClientAuthCertParent)
+ : mSelectTLSClientAuthCertParent(selectTLSClientAuthCertParent),
+ mEventTarget(NS_GetCurrentThread()) {}
+
+ NS_IMETHOD Run() override;
+
+ private:
+ RefPtr<SelectTLSClientAuthCertParent> mSelectTLSClientAuthCertParent;
+ nsCOMPtr<nsIEventTarget> mEventTarget;
+};
+
+NS_IMETHODIMP
+RemoteClientAuthCertificateSelected::Run() {
+ // When this runs, it dispatches an event to the IPC thread it originally came
+ // from in order to send the IPC call to the socket process that a client
+ // authentication certificate has been selected.
+ return mEventTarget->Dispatch(
+ NS_NewRunnableFunction(
+ "psm::RemoteClientAuthCertificateSelected::Run",
+ [parent(mSelectTLSClientAuthCertParent),
+ certBytes(std::move(mSelectedCertBytes)),
+ builtCertChain(std::move(mSelectedCertChainBytes))]() mutable {
+ parent->TLSClientAuthCertSelected(certBytes,
+ std::move(builtCertChain));
+ }),
+ NS_DISPATCH_NORMAL);
+}
+
+namespace mozilla::psm {
+
+// Given some information from the socket process about a connection that
+// requested a client authentication certificate, this function dispatches an
+// event to the main thread to ask the user to select one. When the user does so
+// (or selects no certificate), the continuation runs and sends the information
+// back via IPC.
+bool SelectTLSClientAuthCertParent::Dispatch(
+ const nsACString& aHostName, const OriginAttributes& aOriginAttributes,
+ const int32_t& aPort, const uint32_t& aProviderFlags,
+ const uint32_t& aProviderTlsFlags, const ByteArray& aServerCertBytes,
+ nsTArray<ByteArray>&& aCANames) {
+ RefPtr<ClientAuthCertificateSelectedBase> continuation(
+ new RemoteClientAuthCertificateSelected(this));
+ ClientAuthInfo authInfo(aHostName, aOriginAttributes, aPort, aProviderFlags,
+ aProviderTlsFlags);
+ nsCOMPtr<nsIEventTarget> socketThread =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID);
+ if (NS_WARN_IF(!socketThread)) {
+ return false;
+ }
+ // Dispatch the work of instantiating a CERTCertificate and searching for
+ // client certificates to the socket thread.
+ nsresult rv = socketThread->Dispatch(NS_NewRunnableFunction(
+ "SelectTLSClientAuthCertParent::Dispatch",
+ [authInfo(std::move(authInfo)), continuation(std::move(continuation)),
+ serverCertBytes(aServerCertBytes),
+ caNames(std::move(aCANames))]() mutable {
+ SECItem serverCertItem{
+ siBuffer,
+ const_cast<uint8_t*>(serverCertBytes.data().Elements()),
+ static_cast<unsigned int>(serverCertBytes.data().Length()),
+ };
+ UniqueCERTCertificate serverCert(CERT_NewTempCertificate(
+ CERT_GetDefaultCertDB(), &serverCertItem, nullptr, false, true));
+ if (!serverCert) {
+ return;
+ }
+ nsTArray<nsTArray<uint8_t>> caNamesArray;
+ for (auto& caName : caNames) {
+ caNamesArray.AppendElement(std::move(caName.data()));
+ }
+ UniqueCERTCertList potentialClientCertificates(
+ FindClientCertificatesWithPrivateKeys());
+ RefPtr<SelectClientAuthCertificate> selectClientAuthCertificate(
+ new SelectClientAuthCertificate(
+ std::move(authInfo), std::move(serverCert),
+ std::move(caNamesArray), std::move(potentialClientCertificates),
+ continuation));
+ Unused << NS_DispatchToMainThread(selectClientAuthCertificate);
+ }));
+ return NS_SUCCEEDED(rv);
+}
+
+void SelectTLSClientAuthCertParent::TLSClientAuthCertSelected(
+ const nsTArray<uint8_t>& aSelectedCertBytes,
+ nsTArray<nsTArray<uint8_t>>&& aSelectedCertChainBytes) {
+ AssertIsOnBackgroundThread();
+
+ if (!CanSend()) {
+ return;
+ }
+
+ nsTArray<ByteArray> selectedCertChainBytes;
+ for (auto& certBytes : aSelectedCertChainBytes) {
+ selectedCertChainBytes.AppendElement(ByteArray(certBytes));
+ }
+
+ Unused << SendTLSClientAuthCertSelected(aSelectedCertBytes,
+ selectedCertChainBytes);
+ Unused << Send__delete__(this);
+}
+
+void SelectTLSClientAuthCertParent::ActorDestroy(
+ mozilla::ipc::IProtocol::ActorDestroyReason aWhy) {}
+
+SelectTLSClientAuthCertChild::SelectTLSClientAuthCertChild(
+ ClientAuthCertificateSelected* continuation)
+ : mContinuation(continuation) {}
+
+// When the user has selected (or not) a client authentication certificate in
+// the parent, this function receives that information in the socket process and
+// dispatches a continuation to the socket process to continue the connection.
+ipc::IPCResult SelectTLSClientAuthCertChild::RecvTLSClientAuthCertSelected(
+ ByteArray&& aSelectedCertBytes,
+ nsTArray<ByteArray>&& aSelectedCertChainBytes) {
+ nsTArray<uint8_t> selectedCertBytes(std::move(aSelectedCertBytes.data()));
+ nsTArray<nsTArray<uint8_t>> selectedCertChainBytes;
+ for (auto& certBytes : aSelectedCertChainBytes) {
+ selectedCertChainBytes.AppendElement(std::move(certBytes.data()));
+ }
+ mContinuation->SetSelectedClientAuthData(std::move(selectedCertBytes),
+ std::move(selectedCertChainBytes));
+
+ nsCOMPtr<nsIEventTarget> socketThread =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID);
+ if (NS_WARN_IF(!socketThread)) {
+ return IPC_OK();
+ }
+ nsresult rv = socketThread->Dispatch(mContinuation, NS_DISPATCH_NORMAL);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+
+ return IPC_OK();
+}
+
+} // namespace mozilla::psm