CVE-2026-16422
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fnet/cert/internal/trust_store_nss_unittest.cc |
modified |
Files Changed
net/cert/internal/trust_store_nss.ccnet/cert/internal/trust_store_nss_unittest.cc
Patch
From bf0ffaeaa783ce553cf22df931fa1cb44631c25f Mon Sep 17 00:00:00 2001
From: Matt Mueller <mattm@chromium.org>
Date: Tue, 14 Jul 2026 16:45:41 -0700
Subject: [PATCH] TrustStoreNSS: CERT_FindCertByDERCert may return a different cert
Fixed: 533515002
Change-Id: I5d98e2b778ea7fc0f325f7e0fd8a41cb84f06a31
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8087128
Auto-Submit: Matt Mueller <mattm@chromium.org>
Commit-Queue: Hubert Chao <hchao@chromium.org>
Reviewed-by: Hubert Chao <hchao@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1662261}
---
diff --git a/net/cert/internal/trust_store_nss.cc b/net/cert/internal/trust_store_nss.cc
index 08f16e05..d02c4cb 100644
--- a/net/cert/internal/trust_store_nss.cc
+++ b/net/cert/internal/trust_store_nss.cc
@@ -304,6 +304,14 @@
return bssl::CertificateTrust::ForUnspecified();
}
+ // CERT_FindCertByDERCert may have returned a different cert that has the same
+ // issuer+serial. A trust record should only be used if it's really the same
+ // cert.
+ if (x509_util::CERTCertificateAsSpan(nss_cert.get()) != cert->der_cert()) {
+ DVLOG(1) << "skipped non-identical cert returned by CERT_FindCertByDERCert";
+ return bssl::CertificateTrust::ForUnspecified();
+ }
+
return GetTrustIgnoringSystemTrust(nss_cert.get());
}
diff --git a/net/cert/internal/trust_store_nss_unittest.cc b/net/cert/internal/trust_store_nss_unittest.cc
index bff17420..fb87637 100644
--- a/net/cert/internal/trust_store_nss_unittest.cc
+++ b/net/cert/internal/trust_store_nss_unittest.cc
@@ -23,6 +23,7 @@
#include "net/cert/scoped_nss_types.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"
@@ -747,6 +748,31 @@
EXPECT_TRUE(HasTrust({newroot_}, ExpectedTrustForAnchor()));
}
+// Trust for a cert shouldn't apply to a different cert even if most parts of
+// the cert are the same.
+TEST_F(TrustStoreNSSTestWithoutSlotFilter, TrustedCertWithDifferentKey) {
+ auto [leaf_builder, root_builder] = CertBuilder::CreateSimpleChain2();
+
+ auto trusted_root = bssl::ParsedCertificate::Create(
+ root_builder->DupCertBuffer(),
+ x509_util::DefaultParseCertificateOptions(), nullptr);
+ ASSERT_TRUE(trusted_root);
+ AddCertToNSSSlotWithTrust(trusted_root.get(), test_nssdb_.slot(),
+ bssl::CertificateTrustType::TRUSTED_ANCHOR);
+
+ // Regenerate the private key of the cert builder to create a similar cert
+ // where the only difference is the SPKI (and the self-signature).
+ root_builder->GenerateECKey();
+ auto imposter_root = bssl::ParsedCertificate::Create(
+ root_builder->DupCertBuffer(),
+ x509_util::DefaultParseCertificateOptions(), nullptr);
+ ASSERT_TRUE(imposter_root);
+
+ EXPECT_TRUE(HasTrust({trusted_root}, ExpectedTrustForAnchor()));
+ EXPECT_TRUE(
+ HasTrust({imposter_root}, bssl::CertificateTrust::ForUnspecified()));
+}
+
// Distrust a single self-signed CA certificate.
TEST_F(TrustStoreNSSTestWithoutSlotFilter, DistrustedCA) {
AddCertsToNSS();
Regression Test / PoC
diff --git a/net/cert/internal/trust_store_nss_unittest.cc b/net/cert/internal/trust_store_nss_unittest.cc
index bff17420..fb87637 100644
--- a/net/cert/internal/trust_store_nss_unittest.cc
+++ b/net/cert/internal/trust_store_nss_unittest.cc
@@ -23,6 +23,7 @@
#include "net/cert/scoped_nss_types.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"
@@ -747,6 +748,31 @@
EXPECT_TRUE(HasTrust({newroot_}, ExpectedTrustForAnchor()));
}
+// Trust for a cert shouldn't apply to a different cert even if most parts of
+// the cert are the same.
+TEST_F(TrustStoreNSSTestWithoutSlotFilter, TrustedCertWithDifferentKey) {
+ auto [leaf_builder, root_builder] = CertBuilder::CreateSimpleChain2();
+
+ auto trusted_root = bssl::ParsedCertificate::Create(
+ root_builder->DupCertBuffer(),
+ x509_util::DefaultParseCertificateOptions(), nullptr);
+ ASSERT_TRUE(trusted_root);
+ AddCertToNSSSlotWithTrust(trusted_root.get(), test_nssdb_.slot(),
+ bssl::CertificateTrustType::TRUSTED_ANCHOR);
+
+ // Regenerate the private key of the cert builder to create a similar cert
+ // where the only difference is the SPKI (and the self-signature).
+ root_builder->GenerateECKey();
+ auto imposter_root = bssl::ParsedCertificate::Create(
+ root_builder->DupCertBuffer(),
+ x509_util::DefaultParseCertificateOptions(), nullptr);
+ ASSERT_TRUE(imposter_root);
+
+ EXPECT_TRUE(HasTrust({trusted_root}, ExpectedTrustForAnchor()));
+ EXPECT_TRUE(
+ HasTrust({imposter_root}, bssl::CertificateTrust::ForUnspecified()));
+}
+
// Distrust a single self-signed CA certificate.
TEST_F(TrustStoreNSSTestWithoutSlotFilter, DistrustedCA) {
AddCertsToNSS();
Original Bug Report
GetTrust returns trust anchor status for attacker cert via NSS (issuer, serial) aliasing
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential certificate-verification bypass exists in TrustStoreNSS on Linux because it queries NSS via CERT_FindCertByDERCert, which resolves solely by (issuer, serialNumber) without checking full DER equality. An attacker can craft a certificate with the same (issuer, serial) as a locally-trusted NSS root but with their own SPKI to fully spoof any HTTPS domain. This permits a complete TLS server authenticity bypass for users with custom enterprise CA roots.
Affected files:
net/cert/internal/trust_store_nss.ccnet/cert/internal/trust_store_nss.hnet/cert/internal/system_trust_store.ccnet/cert/cert_verify_proc_builtin.cc
Estimated timestamp from git blame: 2023-02-25
- Summary of the Issue (Meant for Human Triage)
A potential certificate verification bypass vulnerability (CWE-295) exists in Chromium’s network certificate verification stack on Linux. The root cause lies in how TrustStoreNSS::GetTrust retrieves the trust status of a certificate. Specifically, the implementation queries the Network Security Services (NSS) database via CERT_FindCertByDERCert. However, under the hood, NSS resolves this query solely based on the certificate’s (issuer, serialNumber) tuple on standard softoken databases, without validating that the retrieved certificate’s DER matches the input DER bytes.
An on-path network attacker (MITM) who knows the issuer DN and serial number of any user-imported CA root can craft a certificate containing the identical issuer and serial fields but using an attacker-controlled Subject Public Key Info (SPKI). Since Chrome’s path builder blindly trusts the output of GetTrust and assigns the matched trust anchor’s status to the attacker-controlled certificate, the signature check on the leaf certificate succeeds using the attacker’s key. This completely bypasses TLS server authenticity validation, allowing the attacker to spoof any HTTPS origin for Linux users with custom enterprise root certificates.
- Proof-of-Concept & Detailed Execution Flow
Prerequisites and Setup
- Platform: Linux desktop (as
use_nss_certs = is_linux || is_chromeosis enabled and Linux system trust store inclusion usesTrustStoreNSS). - Environment Configuration: The victim has imported at least one custom CA certificate $T$ (with issuer $I$ and serial number $S$) into their NSS user database (
~/.pki/nssdb) with SSL trust flags enabled. - Attacker Knowledge: The attacker knows the public issuer DN and serial number of $T$ (which is typical as enterprise root certificates are publicly distributed for client configuration).
Step-by-Step Execution Flow
(Note: These are suggested steps to trigger the vulnerability, as our tooling agent doesn’t yet have the ability to run code.)
-
Entry Point (Bytes Injection): The attacker intercepts the connection to
https://victim.comor acts as a server visited by the client. The attacker delivers a TLSCertificatehandshake message containing the forged chain[leaf, C], where:leafis an end-entity certificate forvictim.com, signed with $K_{\text{evil}}$, whereleaf.issuermatches $C$’s subject and SAN is set tovictim.com.- $C$ is a malicious intermediate certificate crafted such that
C.tbs.issuermatches the byte-exact DER of $T$’s issuer $I$, andC.tbs.serialNumbermatches $T$’s serial $S$. It is self-signed (or dummy-signed) by $K_{\text{evil}}$, hasbasicConstraints CA:TRUE,keyUsage = keyCertSign, and its SPKI is set to $K_{\text{evil}}$.
-
Handshake Parsing: At
net/cert/cert_verify_proc_builtin.cc:1654, the verification process receives the TLS certificate chain and declaresbssl::CertIssuerSourceStatic intermediates. At line 1655,AddIntermediatesToIssuerSource(input_cert, &intermediates, net_log)is called. Under the hood (net/cert/cert_verify_proc_builtin.cc:1289-1305), the intermediate $C$ is parsed and registered in the static issuer source. -
Path Construction:
TryBuildPathis invoked atnet/cert/cert_verify_proc_builtin.cc:1435. The path builder attempts to resolve the issuer ofleaf. It queries the static intermediate source viapath_builder.AddCertIssuerSource(&intermediates)(line 1444) and retrieves candidate certificate $C$ based on name matching. -
Trust Query Generation: The path builder invokes
CertIssuersIter::AddIssuers(new_issuers)inthird_party/boringssl/src/pki/path_builder.cc:312:- The path builder deduplicates candidates based on full DER comparison (line 314-318). Since $C$ has a different SPKI and subject than $T$, its full DER is distinct, and it is inserted into the
present_issuers_set. - It queries trust for $C$’s bytes by calling
trust_store_->GetTrust(C)(line 323). This routes down toTrustStoreNSS::GetTrustinnet/cert/internal/trust_store_nss.cc:272.
- The path builder deduplicates candidates based on full DER comparison (line 314-318). Since $C$ has a different SPKI and subject than $T$, its full DER is distinct, and it is inserted into the
-
NSS Aliased Lookup: Inside
TrustStoreNSS::GetTrust, aSECItemrepresenting $C$’s raw DER representation is initialized (trust_store_nss.cc:276-279) and queried usingCERT_FindCertByDERCert:SECItem der_cert; der_cert.data = const_cast<uint8_t*>(cert->der_cert().data()); der_cert.len = base::checked_cast<unsigned>(cert->der_cert().size()); der_cert.type = siDERCertBuffer; ScopedCERTCertificate nss_cert( CERT_FindCertByDERCert(CERT_GetDefaultCertDB(), &der_cert));NSS resolves
CERT_FindCertByDERCertviaNSSTrustDomain_FindCertificateByEncodedCertificate, which extracts the issuer and serial usingnssPKIX509_GetIssuerAndSerialFromDERand queries the underlying PKCS#11 token for the template{CKA_CLASS=CKO_CERTIFICATE, CKA_ISSUER, CKA_SERIAL_NUMBER}. Because theCKA_VALUEis absent from this search template, NSS retrieves and returns theCERTCertificate*corresponding to the legitimate trusted root $T$, not $C$. -
Trust Application Without Byte Validation: At
net/cert/internal/trust_store_nss.cc:307, it callsGetTrustIgnoringSystemTrust(nss_cert.get())without verifying thatnss_cert->derCertis byte-equivalent to the inputder_cert.CERT_GetCertTrust(T)returnsCERTDB_TRUSTED_CA(trust_store_nss.cc:320).- Since $T$ resides in an allowed user slot (
trust_store_nss.cc:334-336), the fast path attrust_store_nss.cc:374is hit, returningbssl::CertificateTrust::ForTrustAnchor()withenforce_anchor_constraintsandenforce_anchor_expiryenabled. - The path builder erroneously binds this trust status to $C$’s entry in the
IssuerEntry:entry.cert = C; entry.trust = TrustAnchor;.
-
Anchor Processing and Sink: During path validation in
third_party/boringssl/src/pki/verify_certificate_chain.cc:2010:PathVerifier::ProcessRootCertificateevaluates the trust anchor $C$.- Constraints are evaluated against $C$’s attacker-controlled fields (such as
basicConstraints CA:TRUEandkeyUsage keyCertSignatverify_certificate_chain.cc:2053), which trivially pass. - Because $C$ is treated as the trust anchor, its self-signature is not validated. Instead, the working public key is populated directly from $C$’s SPKI (line 2061):
working_public_key_ = ParseAndCheckPublicKey(cert.tbs().spki_tlv, errors); // cert = C, so key is K_evil - Validation proceeds to the end-entity
leafcertificate, successfully verifying its signature againstworking_public_key_($K_{\text{evil}}$).
-
Policy Bypass: Since $C$ is a forged local anchor and not in the hardcoded Chrome Root Store,
IsKnownRoot(C)returnsfalse(net/cert/internal/system_trust_store.cc:149). Consequently,verify_result.is_issued_by_known_rootis set tofalse.- This bypasses Certificate Transparency (CT) enforcement in
net/cert/require_ct_delegate.cc:13-21. - This bypasses HTTP Public Key Pinning (HPKP) requirements in
net/http/transport_security_state.cc:462-476. - The TLS handshake completes successfully without any warnings or interstitials, granting the attacker a complete Server Authenticity bypass.
- This bypasses Certificate Transparency (CT) enforcement in
Proposed Fix
In net/cert/internal/trust_store_nss.cc:272-308, after the CERT_FindCertByDERCert call retrieves an nss_cert, add a full byte-for-byte comparison of the returned certificate’s DER data against the input der_cert. If nss_cert->derCert does not match the input exactly, it should return bssl::CertificateTrust::ForUnspecified().
- Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)
Prior Critic Verdict (Verbatim)
The vulnerability is a High (S1) severity certificate verification bypass (NET-BYP, B3 Server Authenticity).
**Justification:**
- **Bug class & Impact:** An A-SERVER (MITM) attacker can serve a forged certificate chain that Chrome will validate and attribute to any domain (e.g., `https://victim`). This maps directly to the High (S1) row: 'The browser attributes a response to `https://victim` that did not arrive over a connection whose peer cert chains for `victim`: cert-verification bypass (e.g., trust-store aliasing)'.
- **Root Cause:** `TrustStoreNSS::GetTrust` uses `CERT_FindCertByDERCert` to query NSS for a certificate's trust. However, on softokens, `CERT_FindCertByDERCert` resolves purely by `(issuer, serialNumber)` without comparing the full DER bytes. An attacker can craft a certificate `C` with the same issuer and serial as a trusted user-imported root `T`, but with an attacker-controlled SPKI. `TrustStoreNSS` incorrectly returns `T`'s `ForTrustAnchor()` status for `C`.
- **Path Builder Handling:** In `third_party/boringssl/src/pki/path_builder.cc`, the path builder associates the returned `ForTrustAnchor()` status with the attacker's certificate `C`. Downstream verification then uses `C`'s attacker-controlled SPKI to validate the leaf certificate's signature, resulting in a completely valid path.
- **Mitigating Factors Evaluated:**
- **Platform:** The vulnerability is Linux-only. Per the rubric, 'Platform-specific //net findings ... rate on the target platform', and platform subsetting is not a downgrade.
- **Preconditions:** The victim must have at least one user-imported NSS CA (a common enterprise configuration). Enterprise CAs are publicly distributed, making the `(issuer, serial)` pair trivially discoverable without any user interaction or key compromise on the attack path.
- **HPKP/CT:** Because the attacker's certificate `C` is not in the hardcoded Chrome Root Store (`IsKnownRoot` returns false), HPKP and CT enforcement are bypassed (treated as a local/enterprise anchor).
- **Ceilings:** Rated High (S1) rather than Critical (S0) because it is a logic/authenticity bypass (B3) rather than an RCE or memory corruption (B1) within the network service.
Codebase Verification
- NSS Softoken Lookup Logic: Verified via vendored NSS source code (e.g.,
nss_3.98.orig.tar.gz).nssTrustDomain_FindCertificateByEncodedCertificateinlib/pki/trustdomain.cand token template creation inlib/dev/devtoken.cconfirm that PKCS#11 queries only provide{CKA_CLASS, CKA_ISSUER, CKA_SERIAL_NUMBER}. - Trust Store configuration:
services/cert_verifier/cert_verifier_creation.cc:132-142confirms that systems utilizinginclude_system_trust_store(the default) initializeTrustStoreNSSwith theUseTrustFromAllUserSlots()flag. - DER validation missing:
net/cert/internal/trust_store_nss.ccatGetTrust(lines 272-308) blindly trusts the NSS certificate handle returned byCERT_FindCertByDERCertwithout any post-lookup DER validation. - Policy bypass paths: Codebase investigation identified
net/cert/cert_verify_proc_builtin.cc:640-644and1493-1499settingis_issued_by_known_root. The subsequent evaluation of this flag bypasses CT atnet/cert/require_ct_delegate.cc:13-21and HPKP atnet/http/transport_security_state.cc:462-476. - Public Key Extraction: In
third_party/boringssl/src/pki/verify_certificate_chain.ccat lines 1745 and 2061,working_public_key_ = ParseAndCheckPublicKey(cert.tbs().spki_tlv, errors);correctly pulls fromspki_tlvof the trust anchor candidate without verifying its own self-signature (valid RFC 5280 semantics).
Evaluated with Chrome root at commit: f4cb78b4ec077b7f51b504af9350cdc166d10c2f
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.