464 lines
19 KiB
C++
464 lines
19 KiB
C++
// Copyright 2021 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/cert/internal/trust_store_win.h"
|
|
|
|
#include "base/hash/sha1.h"
|
|
#include "base/location.h"
|
|
#include "base/logging.h"
|
|
#include "base/memory/ptr_util.h"
|
|
#include "base/ranges/algorithm.h"
|
|
#include "base/strings/string_number_conversions.h"
|
|
#include "base/threading/scoped_blocking_call.h"
|
|
#include "net/base/features.h"
|
|
#include "net/cert/internal/trust_store_features.h"
|
|
#include "net/cert/pki/cert_errors.h"
|
|
#include "net/cert/pki/parsed_certificate.h"
|
|
#include "net/cert/x509_util.h"
|
|
#include "net/third_party/mozilla_win/cert/win_util.h"
|
|
|
|
namespace net {
|
|
|
|
namespace {
|
|
|
|
// Returns true if the cert can be used for server authentication, based on
|
|
// certificate properties.
|
|
//
|
|
// While there are a variety of certificate properties that can affect how
|
|
// trust is computed, the main property is CERT_ENHKEY_USAGE_PROP_ID, which
|
|
// is intersected with the certificate's EKU extension (if present).
|
|
// The intersection is documented in the Remarks section of
|
|
// CertGetEnhancedKeyUsage, and is as follows:
|
|
// - No EKU property, and no EKU extension = Trusted for all purpose
|
|
// - Either an EKU property, or EKU extension, but not both = Trusted only
|
|
// for the listed purposes
|
|
// - Both an EKU property and an EKU extension = Trusted for the set
|
|
// intersection of the listed purposes
|
|
// CertGetEnhancedKeyUsage handles this logic, and if an empty set is
|
|
// returned, the distinction between the first and third case can be
|
|
// determined by GetLastError() returning CRYPT_E_NOT_FOUND.
|
|
//
|
|
// See:
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certgetenhancedkeyusage
|
|
//
|
|
// If we run into any errors reading the certificate properties, we fail
|
|
// closed.
|
|
bool IsCertTrustedForServerAuth(PCCERT_CONTEXT cert) {
|
|
DWORD usage_size = 0;
|
|
|
|
if (!CertGetEnhancedKeyUsage(cert, 0, nullptr, &usage_size)) {
|
|
return false;
|
|
}
|
|
|
|
std::vector<BYTE> usage_bytes(usage_size);
|
|
CERT_ENHKEY_USAGE* usage =
|
|
reinterpret_cast<CERT_ENHKEY_USAGE*>(usage_bytes.data());
|
|
if (!CertGetEnhancedKeyUsage(cert, 0, usage, &usage_size)) {
|
|
return false;
|
|
}
|
|
|
|
if (usage->cUsageIdentifier == 0) {
|
|
// check GetLastError
|
|
HRESULT error_code = GetLastError();
|
|
|
|
switch (error_code) {
|
|
case CRYPT_E_NOT_FOUND:
|
|
return true;
|
|
case S_OK:
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
for (DWORD i = 0; i < usage->cUsageIdentifier; i++) {
|
|
base::StringPiece eku = base::StringPiece(usage->rgpszUsageIdentifier[i]);
|
|
if ((eku == szOID_PKIX_KP_SERVER_AUTH) ||
|
|
(eku == szOID_ANY_ENHANCED_KEY_USAGE)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TrustStoreWin::CertStores::CertStores() = default;
|
|
TrustStoreWin::CertStores::~CertStores() = default;
|
|
TrustStoreWin::CertStores::CertStores(CertStores&& other) = default;
|
|
TrustStoreWin::CertStores& TrustStoreWin::CertStores::operator=(
|
|
CertStores&& other) = default;
|
|
|
|
// static
|
|
TrustStoreWin::CertStores
|
|
TrustStoreWin::CertStores::CreateInMemoryStoresForTesting() {
|
|
TrustStoreWin::CertStores stores;
|
|
stores.roots = crypto::ScopedHCERTSTORE(CertOpenStore(
|
|
CERT_STORE_PROV_MEMORY, X509_ASN_ENCODING, NULL, 0, nullptr));
|
|
stores.intermediates = crypto::ScopedHCERTSTORE(CertOpenStore(
|
|
CERT_STORE_PROV_MEMORY, X509_ASN_ENCODING, NULL, 0, nullptr));
|
|
stores.trusted_people = crypto::ScopedHCERTSTORE(CertOpenStore(
|
|
CERT_STORE_PROV_MEMORY, X509_ASN_ENCODING, NULL, 0, nullptr));
|
|
stores.disallowed = crypto::ScopedHCERTSTORE(CertOpenStore(
|
|
CERT_STORE_PROV_MEMORY, X509_ASN_ENCODING, NULL, 0, nullptr));
|
|
stores.InitializeAllCertsStore();
|
|
return stores;
|
|
}
|
|
|
|
TrustStoreWin::CertStores
|
|
TrustStoreWin::CertStores::CreateNullStoresForTesting() {
|
|
return TrustStoreWin::CertStores();
|
|
}
|
|
|
|
// static
|
|
TrustStoreWin::CertStores TrustStoreWin::CertStores::CreateWithCollections() {
|
|
TrustStoreWin::CertStores stores;
|
|
stores.roots = crypto::ScopedHCERTSTORE(
|
|
CertOpenStore(CERT_STORE_PROV_COLLECTION, 0, NULL, 0, nullptr));
|
|
stores.intermediates = crypto::ScopedHCERTSTORE(
|
|
CertOpenStore(CERT_STORE_PROV_COLLECTION, 0, NULL, 0, nullptr));
|
|
stores.trusted_people = crypto::ScopedHCERTSTORE(
|
|
CertOpenStore(CERT_STORE_PROV_COLLECTION, 0, NULL, 0, nullptr));
|
|
stores.disallowed = crypto::ScopedHCERTSTORE(
|
|
CertOpenStore(CERT_STORE_PROV_COLLECTION, 0, NULL, 0, nullptr));
|
|
stores.InitializeAllCertsStore();
|
|
return stores;
|
|
}
|
|
|
|
void TrustStoreWin::CertStores::InitializeAllCertsStore() {
|
|
all = crypto::ScopedHCERTSTORE(
|
|
CertOpenStore(CERT_STORE_PROV_COLLECTION, 0, NULL, 0, nullptr));
|
|
if (is_null()) {
|
|
return;
|
|
}
|
|
// Add intermediate and root cert stores to the all_cert_store collection so
|
|
// SyncGetIssuersOf will find them. disallowed_cert_store is not added
|
|
// because the certs are distrusted; making them non-findable in
|
|
// SyncGetIssuersOf helps us fail path-building faster.
|
|
// `trusted_people` is not added because it can only contain end-entity
|
|
// certs, so checking it for issuers during path building is not necessary.
|
|
if (!CertAddStoreToCollection(all.get(), intermediates.get(),
|
|
/*dwUpdateFlags=*/0, /*dwPriority=*/0)) {
|
|
return;
|
|
}
|
|
if (!CertAddStoreToCollection(all.get(), roots.get(),
|
|
/*dwUpdateFlags=*/0, /*dwPriority=*/0)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
class TrustStoreWin::Impl {
|
|
public:
|
|
// Creates a TrustStoreWin.
|
|
Impl() {
|
|
base::ScopedBlockingCall scoped_blocking_call(
|
|
FROM_HERE, base::BlockingType::MAY_BLOCK);
|
|
|
|
CertStores stores = CertStores::CreateWithCollections();
|
|
if (stores.is_null()) {
|
|
// If there was an error initializing the cert store collections, give
|
|
// up. The Impl object will still be created but any calls to its public
|
|
// methods will return no results.
|
|
return;
|
|
}
|
|
|
|
// Grab the user-added roots.
|
|
GatherEnterpriseCertsForLocation(stores.roots.get(),
|
|
CERT_SYSTEM_STORE_LOCAL_MACHINE, L"ROOT");
|
|
GatherEnterpriseCertsForLocation(
|
|
stores.roots.get(), CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY,
|
|
L"ROOT");
|
|
GatherEnterpriseCertsForLocation(stores.roots.get(),
|
|
CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE,
|
|
L"ROOT");
|
|
GatherEnterpriseCertsForLocation(stores.roots.get(),
|
|
CERT_SYSTEM_STORE_CURRENT_USER, L"ROOT");
|
|
GatherEnterpriseCertsForLocation(
|
|
stores.roots.get(), CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY,
|
|
L"ROOT");
|
|
|
|
// Grab the user-added intermediates.
|
|
GatherEnterpriseCertsForLocation(stores.intermediates.get(),
|
|
CERT_SYSTEM_STORE_LOCAL_MACHINE, L"CA");
|
|
GatherEnterpriseCertsForLocation(
|
|
stores.intermediates.get(),
|
|
CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY, L"CA");
|
|
GatherEnterpriseCertsForLocation(stores.intermediates.get(),
|
|
CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE,
|
|
L"CA");
|
|
GatherEnterpriseCertsForLocation(stores.intermediates.get(),
|
|
CERT_SYSTEM_STORE_CURRENT_USER, L"CA");
|
|
GatherEnterpriseCertsForLocation(
|
|
stores.intermediates.get(), CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY,
|
|
L"CA");
|
|
|
|
// Grab the user-added trusted server certs. Trusted end-entity certs are
|
|
// only allowed for server auth in the "local machine" store, but not in the
|
|
// "current user" store.
|
|
GatherEnterpriseCertsForLocation(stores.trusted_people.get(),
|
|
CERT_SYSTEM_STORE_LOCAL_MACHINE,
|
|
L"TrustedPeople");
|
|
GatherEnterpriseCertsForLocation(
|
|
stores.trusted_people.get(),
|
|
CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY, L"TrustedPeople");
|
|
GatherEnterpriseCertsForLocation(stores.trusted_people.get(),
|
|
CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE,
|
|
L"TrustedPeople");
|
|
|
|
// Grab the user-added disallowed certs.
|
|
GatherEnterpriseCertsForLocation(stores.disallowed.get(),
|
|
CERT_SYSTEM_STORE_LOCAL_MACHINE,
|
|
L"Disallowed");
|
|
GatherEnterpriseCertsForLocation(
|
|
stores.disallowed.get(), CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY,
|
|
L"Disallowed");
|
|
GatherEnterpriseCertsForLocation(stores.disallowed.get(),
|
|
CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE,
|
|
L"Disallowed");
|
|
GatherEnterpriseCertsForLocation(
|
|
stores.disallowed.get(), CERT_SYSTEM_STORE_CURRENT_USER, L"Disallowed");
|
|
GatherEnterpriseCertsForLocation(
|
|
stores.disallowed.get(), CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY,
|
|
L"Disallowed");
|
|
|
|
// Auto-sync all of the cert stores to get updates to the cert store.
|
|
// Auto-syncing on all_certs_store seems to work to resync the nested
|
|
// stores, although the docs at
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certcontrolstore
|
|
// are somewhat unclear. If and when root store changes are linked to
|
|
// clearing various caches, this should be replaced with
|
|
// CERT_STORE_CTRL_NOTIFY_CHANGE and CERT_STORE_CTRL_RESYNC.
|
|
if (!CertControlStore(stores.all.get(), 0, CERT_STORE_CTRL_AUTO_RESYNC,
|
|
0) ||
|
|
!CertControlStore(stores.trusted_people.get(), 0,
|
|
CERT_STORE_CTRL_AUTO_RESYNC, 0) ||
|
|
!CertControlStore(stores.disallowed.get(), 0,
|
|
CERT_STORE_CTRL_AUTO_RESYNC, 0)) {
|
|
PLOG(ERROR) << "Error enabling CERT_STORE_CTRL_AUTO_RESYNC";
|
|
}
|
|
|
|
root_cert_store_ = std::move(stores.roots);
|
|
intermediate_cert_store_ = std::move(stores.intermediates);
|
|
trusted_people_cert_store_ = std::move(stores.trusted_people);
|
|
disallowed_cert_store_ = std::move(stores.disallowed);
|
|
all_certs_store_ = std::move(stores.all);
|
|
}
|
|
|
|
Impl(CertStores stores)
|
|
: root_cert_store_(std::move(stores.roots)),
|
|
intermediate_cert_store_(std::move(stores.intermediates)),
|
|
all_certs_store_(std::move(stores.all)),
|
|
trusted_people_cert_store_(std::move(stores.trusted_people)),
|
|
disallowed_cert_store_(std::move(stores.disallowed)) {}
|
|
|
|
~Impl() = default;
|
|
Impl(const Impl& other) = delete;
|
|
Impl& operator=(const Impl& other) = delete;
|
|
|
|
void SyncGetIssuersOf(const ParsedCertificate* cert,
|
|
ParsedCertificateList* issuers) {
|
|
if (!root_cert_store_.get() || !intermediate_cert_store_.get() ||
|
|
!trusted_people_cert_store_.get() || !all_certs_store_.get() ||
|
|
!disallowed_cert_store_.get()) {
|
|
return;
|
|
}
|
|
base::span<const uint8_t> issuer_span = cert->issuer_tlv().AsSpan();
|
|
|
|
CERT_NAME_BLOB cert_issuer_blob;
|
|
cert_issuer_blob.cbData = static_cast<DWORD>(issuer_span.size());
|
|
cert_issuer_blob.pbData = const_cast<uint8_t*>(issuer_span.data());
|
|
|
|
PCCERT_CONTEXT cert_from_store = nullptr;
|
|
// TODO(https://crbug.com/1239270): figure out if this is thread-safe or if
|
|
// we need locking here
|
|
while ((cert_from_store = CertFindCertificateInStore(
|
|
all_certs_store_.get(), X509_ASN_ENCODING, 0,
|
|
CERT_FIND_SUBJECT_NAME, &cert_issuer_blob, cert_from_store))) {
|
|
bssl::UniquePtr<CRYPTO_BUFFER> der_crypto =
|
|
x509_util::CreateCryptoBuffer(base::make_span(
|
|
cert_from_store->pbCertEncoded, cert_from_store->cbCertEncoded));
|
|
CertErrors errors;
|
|
ParsedCertificate::CreateAndAddToVector(
|
|
std::move(der_crypto), x509_util::DefaultParseCertificateOptions(),
|
|
issuers, &errors);
|
|
}
|
|
}
|
|
|
|
CertificateTrust GetTrust(const ParsedCertificate* cert,
|
|
base::SupportsUserData* debug_data) {
|
|
if (!root_cert_store_.get() || !intermediate_cert_store_.get() ||
|
|
!trusted_people_cert_store_.get() || !all_certs_store_.get() ||
|
|
!disallowed_cert_store_.get()) {
|
|
return CertificateTrust::ForUnspecified();
|
|
}
|
|
|
|
base::span<const uint8_t> cert_span = cert->der_cert().AsSpan();
|
|
base::SHA1Digest cert_hash = base::SHA1HashSpan(cert_span);
|
|
CRYPT_HASH_BLOB cert_hash_blob;
|
|
cert_hash_blob.cbData = static_cast<DWORD>(cert_hash.size());
|
|
cert_hash_blob.pbData = cert_hash.data();
|
|
|
|
PCCERT_CONTEXT cert_from_store = nullptr;
|
|
|
|
// Check Disallowed store first.
|
|
while ((cert_from_store = CertFindCertificateInStore(
|
|
disallowed_cert_store_.get(), X509_ASN_ENCODING, 0,
|
|
CERT_FIND_SHA1_HASH, &cert_hash_blob, cert_from_store))) {
|
|
base::span<const uint8_t> cert_from_store_span = base::make_span(
|
|
cert_from_store->pbCertEncoded, cert_from_store->cbCertEncoded);
|
|
// If a cert is in the windows distruted store, it is considered
|
|
// distrusted for all purporses. EKU isn't checked. See crbug.com/1355961.
|
|
if (base::ranges::equal(cert_span, cert_from_store_span)) {
|
|
return CertificateTrust::ForDistrusted();
|
|
}
|
|
}
|
|
|
|
// TODO(https://crbug.com/1239270): figure out if this is thread-safe or if
|
|
// we need locking here
|
|
while ((cert_from_store = CertFindCertificateInStore(
|
|
root_cert_store_.get(), X509_ASN_ENCODING, 0,
|
|
CERT_FIND_SHA1_HASH, &cert_hash_blob, cert_from_store))) {
|
|
base::span<const uint8_t> cert_from_store_span = base::make_span(
|
|
cert_from_store->pbCertEncoded, cert_from_store->cbCertEncoded);
|
|
if (base::ranges::equal(cert_span, cert_from_store_span)) {
|
|
// If we find at least one version of the cert that is trusted for TLS
|
|
// Server Auth, we will trust the cert.
|
|
if (IsCertTrustedForServerAuth(cert_from_store)) {
|
|
if (base::FeatureList::IsEnabled(
|
|
features::kTrustStoreTrustedLeafSupport)) {
|
|
// Certificates in the Roots store may be used as either trust
|
|
// anchors or trusted leafs (if self-signed).
|
|
return CertificateTrust::ForTrustAnchorOrLeaf()
|
|
.WithEnforceAnchorExpiry()
|
|
.WithEnforceAnchorConstraints(
|
|
IsLocalAnchorConstraintsEnforcementEnabled())
|
|
.WithRequireLeafSelfSigned();
|
|
} else {
|
|
return CertificateTrust::ForTrustAnchor()
|
|
.WithEnforceAnchorExpiry()
|
|
.WithEnforceAnchorConstraints(
|
|
IsLocalAnchorConstraintsEnforcementEnabled());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (base::FeatureList::IsEnabled(features::kTrustStoreTrustedLeafSupport)) {
|
|
while ((cert_from_store = CertFindCertificateInStore(
|
|
trusted_people_cert_store_.get(), X509_ASN_ENCODING, 0,
|
|
CERT_FIND_SHA1_HASH, &cert_hash_blob, cert_from_store))) {
|
|
base::span<const uint8_t> cert_from_store_span = base::make_span(
|
|
cert_from_store->pbCertEncoded, cert_from_store->cbCertEncoded);
|
|
if (base::ranges::equal(cert_span, cert_from_store_span)) {
|
|
// If we find at least one version of the cert that is trusted for TLS
|
|
// Server Auth, we will trust the cert.
|
|
if (IsCertTrustedForServerAuth(cert_from_store)) {
|
|
// Certificates in the Trusted People store may be trusted leafs (if
|
|
// self-signed).
|
|
return CertificateTrust::ForTrustedLeaf()
|
|
.WithRequireLeafSelfSigned();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we fall through here, we've either
|
|
//
|
|
// (a) found the cert but it is not usable for server auth. Treat this as
|
|
// Unspecified trust. Originally this was treated as Distrusted, but
|
|
// this is inconsistent with how the Windows verifier works, which is to
|
|
// union all of the EKU usages for all instances of the cert, whereas
|
|
// sending back Distrusted would not do that.
|
|
//
|
|
// or
|
|
//
|
|
// (b) Haven't found the cert. Tell everyone Unspecified.
|
|
return CertificateTrust::ForUnspecified();
|
|
}
|
|
|
|
private:
|
|
// Cert Collection containing all user-added trust anchors.
|
|
crypto::ScopedHCERTSTORE root_cert_store_;
|
|
|
|
// Cert Collection containing all user-added intermediates.
|
|
crypto::ScopedHCERTSTORE intermediate_cert_store_;
|
|
|
|
// Cert Collection for searching via SyncGetIssuersOf()
|
|
crypto::ScopedHCERTSTORE all_certs_store_;
|
|
|
|
// Cert Collection containing all user-added trust leafs.
|
|
crypto::ScopedHCERTSTORE trusted_people_cert_store_;
|
|
|
|
// Cert Collection for all disallowed certs.
|
|
crypto::ScopedHCERTSTORE disallowed_cert_store_;
|
|
};
|
|
|
|
// TODO(https://crbug.com/1239268): support CTLs.
|
|
TrustStoreWin::TrustStoreWin() = default;
|
|
|
|
void TrustStoreWin::InitializeStores() {
|
|
// Don't need return value
|
|
MaybeInitializeAndGetImpl();
|
|
}
|
|
|
|
TrustStoreWin::Impl* TrustStoreWin::MaybeInitializeAndGetImpl() {
|
|
base::AutoLock lock(init_lock_);
|
|
if (!impl_) {
|
|
impl_ = std::make_unique<TrustStoreWin::Impl>();
|
|
}
|
|
return impl_.get();
|
|
}
|
|
|
|
std::unique_ptr<TrustStoreWin> TrustStoreWin::CreateForTesting(
|
|
CertStores stores) {
|
|
return base::WrapUnique(new TrustStoreWin(
|
|
std::make_unique<TrustStoreWin::Impl>(std::move(stores))));
|
|
}
|
|
|
|
TrustStoreWin::TrustStoreWin(std::unique_ptr<Impl> impl)
|
|
: impl_(std::move(impl)) {}
|
|
|
|
TrustStoreWin::~TrustStoreWin() = default;
|
|
|
|
void TrustStoreWin::SyncGetIssuersOf(const ParsedCertificate* cert,
|
|
ParsedCertificateList* issuers) {
|
|
MaybeInitializeAndGetImpl()->SyncGetIssuersOf(cert, issuers);
|
|
}
|
|
|
|
// As documented in IsCertTrustedForServerAuth(), on Windows, the
|
|
// set of extended key usages present in a certificate can be further
|
|
// scoped down by user setting; effectively, disabling a given EKU for
|
|
// a given intermediate or root.
|
|
//
|
|
// Windows uses this during path building when filtering the EKUs; if it
|
|
// encounters this property, it uses the combined EKUs to determine
|
|
// whether to continue path building, but doesn't treat the certificate
|
|
// as affirmatively revoked/distrusted.
|
|
//
|
|
// This behaviour is replicated here by returning Unspecified trust if
|
|
// we find instances of the cert that do not have the correct EKUs set
|
|
// for TLS Server Auth. This allows path building to continue and allows
|
|
// us to later trust the cert if it is present in Chrome Root Store.
|
|
//
|
|
// Windows does have some idiosyncrasies here, which result in the
|
|
// following treatment:
|
|
//
|
|
// - If a certificate is in the Disallowed store, it is distrusted for
|
|
// all purposes regardless of any EKUs that are set.
|
|
// - If a certificate is in the ROOT store, and usable for TLS Server Auth,
|
|
// then it's trusted.
|
|
// - If a certificate is in the root store, and lacks the EKU, then continue
|
|
// path building, but don't treat it as trusted (aka Unspecified).
|
|
// - If we can't find the cert anywhere, then continue path
|
|
// building, but don't treat it as trusted (aka Unspecified).
|
|
//
|
|
// If a certificate is found multiple times in the ROOT store, it is trusted
|
|
// for TLS server auth if any instance of the certificate found
|
|
// is usable for TLS server auth.
|
|
CertificateTrust TrustStoreWin::GetTrust(const ParsedCertificate* cert,
|
|
base::SupportsUserData* debug_data) {
|
|
return MaybeInitializeAndGetImpl()->GetTrust(cert, debug_data);
|
|
}
|
|
|
|
} // namespace net
|