| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/ssl/client_cert_store_win.h" |
| |
| #include <algorithm> |
| #include <functional> |
| #include <memory> |
| #include <string> |
| |
| #define SECURITY_WIN32 // Needs to be defined before including security.h |
| #include <windows.h> |
| #include <security.h> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/scoped_generic.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/win/wincrypt_shim.h" |
| #include "net/cert/x509_util.h" |
| #include "net/cert/x509_util_win.h" |
| #include "net/ssl/ssl_platform_key_util.h" |
| #include "net/ssl/ssl_platform_key_win.h" |
| #include "net/ssl/ssl_private_key.h" |
| #include "third_party/boringssl/src/include/openssl/pool.h" |
| |
| namespace net { |
| |
| namespace { |
| |
| using ScopedHCERTSTOREWithChecks = base::ScopedGeneric< |
| HCERTSTORE, |
| crypto::CAPITraitsWithFlags<HCERTSTORE, |
| CertCloseStore, |
| CERT_CLOSE_STORE_CHECK_FLAG>>; |
| |
| class ClientCertIdentityWin : public ClientCertIdentity { |
| public: |
| ClientCertIdentityWin( |
| scoped_refptr<net::X509Certificate> cert, |
| crypto::ScopedPCCERT_CONTEXT cert_context, |
| scoped_refptr<base::SingleThreadTaskRunner> key_task_runner) |
| : ClientCertIdentity(std::move(cert)), |
| cert_context_(std::move(cert_context)), |
| key_task_runner_(std::move(key_task_runner)) {} |
| |
| void AcquirePrivateKey(base::OnceCallback<void(scoped_refptr<SSLPrivateKey>)> |
| private_key_callback) override { |
| key_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&FetchClientCertPrivateKey, |
| base::Unretained(certificate()), cert_context_.get()), |
| std::move(private_key_callback)); |
| } |
| |
| private: |
| crypto::ScopedPCCERT_CONTEXT cert_context_; |
| scoped_refptr<base::SingleThreadTaskRunner> key_task_runner_; |
| }; |
| |
| // Callback required by Windows API function CertFindChainInStore(). In addition |
| // to filtering by extended/enhanced key usage, we do not show expired |
| // certificates and require digital signature usage in the key usage extension. |
| // |
| // This matches our behavior on Mac OS X and that of NSS. It also matches the |
| // default behavior of IE8. See http://support.microsoft.com/kb/890326 and |
| // http://blogs.msdn.com/b/askie/archive/2009/06/09/my-expired-client-certifica |
| // tes-no-longer-display-when-connecting-to-my-web-server-using-ie8.aspx |
| static BOOL WINAPI ClientCertFindCallback(PCCERT_CONTEXT cert_context, |
| void* find_arg) { |
| // Verify the certificate key usage is appropriate or not specified. |
| BYTE key_usage; |
| if (CertGetIntendedKeyUsage(X509_ASN_ENCODING, cert_context->pCertInfo, |
| &key_usage, 1)) { |
| if (!(key_usage & CERT_DIGITAL_SIGNATURE_KEY_USAGE)) |
| return FALSE; |
| } else { |
| DWORD err = GetLastError(); |
| // If |err| is non-zero, it's an actual error. Otherwise the extension |
| // just isn't present, and we treat it as if everything was allowed. |
| if (err) { |
| DLOG(ERROR) << "CertGetIntendedKeyUsage failed: " << err; |
| return FALSE; |
| } |
| } |
| |
| // Verify the current time is within the certificate's validity period. |
| if (CertVerifyTimeValidity(nullptr, cert_context->pCertInfo) != 0) |
| return FALSE; |
| |
| // Verify private key metadata is associated with this certificate. |
| // TODO(ppi): Is this really needed? Isn't it equivalent to leaving |
| // CERT_CHAIN_FIND_BY_ISSUER_NO_KEY_FLAG not set in |find_flags| argument of |
| // CertFindChainInStore()? |
| DWORD size = 0; |
| if (!CertGetCertificateContextProperty( |
| cert_context, CERT_KEY_PROV_INFO_PROP_ID, nullptr, &size)) { |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| ClientCertIdentityList GetClientCertsImpl(HCERTSTORE cert_store, |
| const SSLCertRequestInfo& request) { |
| ClientCertIdentityList selected_identities; |
| |
| scoped_refptr<base::SingleThreadTaskRunner> current_thread = |
| base::SingleThreadTaskRunner::GetCurrentDefault(); |
| |
| const size_t auth_count = request.cert_authorities.size(); |
| std::vector<CERT_NAME_BLOB> issuers(auth_count); |
| for (size_t i = 0; i < auth_count; ++i) { |
| issuers[i].cbData = static_cast<DWORD>(request.cert_authorities[i].size()); |
| issuers[i].pbData = reinterpret_cast<BYTE*>( |
| const_cast<char*>(request.cert_authorities[i].data())); |
| } |
| |
| // Enumerate the client certificates. |
| CERT_CHAIN_FIND_BY_ISSUER_PARA find_by_issuer_para; |
| memset(&find_by_issuer_para, 0, sizeof(find_by_issuer_para)); |
| find_by_issuer_para.cbSize = sizeof(find_by_issuer_para); |
| find_by_issuer_para.pszUsageIdentifier = szOID_PKIX_KP_CLIENT_AUTH; |
| find_by_issuer_para.cIssuer = static_cast<DWORD>(auth_count); |
| find_by_issuer_para.rgIssuer = |
| reinterpret_cast<CERT_NAME_BLOB*>(issuers.data()); |
| find_by_issuer_para.pfnFindCallback = ClientCertFindCallback; |
| |
| PCCERT_CHAIN_CONTEXT chain_context = nullptr; |
| DWORD find_flags = CERT_CHAIN_FIND_BY_ISSUER_CACHE_ONLY_FLAG | |
| CERT_CHAIN_FIND_BY_ISSUER_CACHE_ONLY_URL_FLAG; |
| for (;;) { |
| // Find a certificate chain. |
| chain_context = CertFindChainInStore(cert_store, |
| X509_ASN_ENCODING, |
| find_flags, |
| CERT_CHAIN_FIND_BY_ISSUER, |
| &find_by_issuer_para, |
| chain_context); |
| if (!chain_context) { |
| if (GetLastError() != static_cast<DWORD>(CRYPT_E_NOT_FOUND)) |
| DPLOG(ERROR) << "CertFindChainInStore failed: "; |
| break; |
| } |
| |
| // Get the leaf certificate. |
| PCCERT_CONTEXT cert_context = |
| chain_context->rgpChain[0]->rgpElement[0]->pCertContext; |
| // Copy the certificate, so that it is valid after |cert_store| is closed. |
| crypto::ScopedPCCERT_CONTEXT cert_context2; |
| PCCERT_CONTEXT raw = nullptr; |
| BOOL ok = CertAddCertificateContextToStore( |
| nullptr, cert_context, CERT_STORE_ADD_USE_EXISTING, &raw); |
| if (!ok) { |
| NOTREACHED(); |
| continue; |
| } |
| cert_context2.reset(raw); |
| |
| // Grab the intermediates, if any. |
| std::vector<crypto::ScopedPCCERT_CONTEXT> intermediates_storage; |
| std::vector<PCCERT_CONTEXT> intermediates; |
| for (DWORD i = 1; i < chain_context->rgpChain[0]->cElement; ++i) { |
| PCCERT_CONTEXT chain_intermediate = |
| chain_context->rgpChain[0]->rgpElement[i]->pCertContext; |
| PCCERT_CONTEXT copied_intermediate = nullptr; |
| ok = CertAddCertificateContextToStore(nullptr, chain_intermediate, |
| CERT_STORE_ADD_USE_EXISTING, |
| &copied_intermediate); |
| if (ok) { |
| intermediates.push_back(copied_intermediate); |
| intermediates_storage.emplace_back(copied_intermediate); |
| } |
| } |
| |
| // Drop the self-signed root, if any. Match Internet Explorer in not sending |
| // it. Although the root's signature is irrelevant for authentication, some |
| // servers reject chains if the root is explicitly sent and has a weak |
| // signature algorithm. See https://crbug.com/607264. |
| // |
| // The leaf or a intermediate may also have a weak signature algorithm but, |
| // in that case, assume it is a configuration error. |
| if (!intermediates.empty() && |
| x509_util::IsSelfSigned(intermediates.back())) { |
| intermediates.pop_back(); |
| intermediates_storage.pop_back(); |
| } |
| |
| // Allow UTF-8 inside PrintableStrings in client certificates. See |
| // crbug.com/770323. |
| X509Certificate::UnsafeCreateOptions options; |
| options.printable_string_is_utf8 = true; |
| scoped_refptr<X509Certificate> cert = |
| x509_util::CreateX509CertificateFromCertContexts( |
| cert_context2.get(), intermediates, options); |
| if (cert) { |
| selected_identities.push_back(std::make_unique<ClientCertIdentityWin>( |
| std::move(cert), |
| std::move(cert_context2), // Takes ownership of |cert_context2|. |
| current_thread)); // The key must be acquired on the same thread, as |
| // the PCCERT_CONTEXT may not be thread safe. |
| } |
| } |
| |
| std::sort(selected_identities.begin(), selected_identities.end(), |
| ClientCertIdentitySorter()); |
| return selected_identities; |
| } |
| |
| } // namespace |
| |
| ClientCertStoreWin::ClientCertStoreWin() = default; |
| |
| ClientCertStoreWin::ClientCertStoreWin( |
| base::RepeatingCallback<crypto::ScopedHCERTSTORE()> cert_store_callback) |
| : cert_store_callback_(std::move(cert_store_callback)) { |
| DCHECK(!cert_store_callback_.is_null()); |
| } |
| |
| ClientCertStoreWin::~ClientCertStoreWin() = default; |
| |
| void ClientCertStoreWin::GetClientCerts(const SSLCertRequestInfo& request, |
| ClientCertListCallback callback) { |
| GetSSLPlatformKeyTaskRunner()->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| // Caller is responsible for keeping the |request| alive |
| // until the callback is run, so std::cref is safe. |
| base::BindOnce(&ClientCertStoreWin::GetClientCertsWithCertStore, |
| std::cref(request), cert_store_callback_), |
| std::move(callback)); |
| } |
| |
| // static |
| ClientCertIdentityList ClientCertStoreWin::GetClientCertsWithCertStore( |
| const SSLCertRequestInfo& request, |
| const base::RepeatingCallback<crypto::ScopedHCERTSTORE()>& |
| cert_store_callback) { |
| ScopedHCERTSTOREWithChecks cert_store; |
| if (cert_store_callback.is_null()) { |
| // Always open a new instance of the "MY" store, to ensure that there |
| // are no previously cached certificates being reused after they're |
| // no longer available (some smartcard providers fail to update the "MY" |
| // store handles and instead interpose CertOpenSystemStore). To help confirm |
| // this, use `ScopedHCERTSTOREWithChecks` and `CERT_CLOSE_STORE_CHECK_FLAG` |
| // to DCHECK that `cert_store` is not inadvertently ref-counted. |
| cert_store.reset(CertOpenSystemStore(NULL, L"MY")); |
| } else { |
| cert_store.reset(cert_store_callback.Run().release()); |
| } |
| if (!cert_store.is_valid()) { |
| PLOG(ERROR) << "Could not open certificate store: "; |
| return ClientCertIdentityList(); |
| } |
| return GetClientCertsImpl(cert_store.get(), request); |
| } |
| |
| bool ClientCertStoreWin::SelectClientCertsForTesting( |
| const CertificateList& input_certs, |
| const SSLCertRequestInfo& request, |
| ClientCertIdentityList* selected_identities) { |
| ScopedHCERTSTOREWithChecks test_store( |
| CertOpenStore(CERT_STORE_PROV_MEMORY, 0, NULL, 0, nullptr)); |
| if (!test_store.is_valid()) |
| return false; |
| |
| // Add available certificates to the test store. |
| for (const auto& input_cert : input_certs) { |
| // Add the certificate to the test store. |
| PCCERT_CONTEXT cert = nullptr; |
| if (!CertAddEncodedCertificateToStore( |
| test_store.get(), X509_ASN_ENCODING, |
| reinterpret_cast<const BYTE*>( |
| CRYPTO_BUFFER_data(input_cert->cert_buffer())), |
| base::checked_cast<DWORD>( |
| CRYPTO_BUFFER_len(input_cert->cert_buffer())), |
| CERT_STORE_ADD_NEW, &cert)) { |
| return false; |
| } |
| // Hold the reference to the certificate (since we requested a copy). |
| crypto::ScopedPCCERT_CONTEXT scoped_cert(cert); |
| |
| // Add dummy private key data to the certificate - otherwise the certificate |
| // would be discarded by the filtering routines. |
| CRYPT_KEY_PROV_INFO private_key_data; |
| memset(&private_key_data, 0, sizeof(private_key_data)); |
| if (!CertSetCertificateContextProperty(cert, |
| CERT_KEY_PROV_INFO_PROP_ID, |
| 0, &private_key_data)) { |
| return false; |
| } |
| } |
| |
| *selected_identities = GetClientCertsImpl(test_store.get(), request); |
| return true; |
| } |
| |
| } // namespace net |