Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Certificate
DescriptionInsufficient validation of untrusted input in Certificate
ComponentCertificate
Bug ClassLogic Error
Tracker533515002
Fix commitbf0ffaeaa783 (chromium/src) +34/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-21

Changed Functions

FunctionChangeNotes
TEST_F
net/cert/internal/trust_store_nss_unittest.cc
modified

Files Changed

  • net/cert/internal/trust_store_nss.cc
  • net/cert/internal/trust_store_nss_unittest.cc
From bf0ffaeaa783ce553cf22df931fa1cb44631c25f Mon Sep 17 00:00:00 2001
From: Matt Mueller <[email protected]>
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 <[email protected]>
Commit-Queue: Hubert Chao <[email protected]>
Reviewed-by: Hubert Chao <[email protected]>
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();
Loading diff…

Regression Test / PoC

shipped with the fix
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();
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • net/cert/internal/trust_store_nss.h
  • net/cert/internal/system_trust_store.cc
  • net/cert/cert_verify_proc_builtin.cc

Estimated timestamp from git blame: 2023-02-25

  1. 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.


  1. Proof-of-Concept & Detailed Execution Flow

Prerequisites and Setup

  • Platform: Linux desktop (as use_nss_certs = is_linux || is_chromeos is enabled and Linux system trust store inclusion uses TrustStoreNSS).
  • 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.)

  1. Entry Point (Bytes Injection): The attacker intercepts the connection to https://victim.com or acts as a server visited by the client. The attacker delivers a TLS Certificate handshake message containing the forged chain [leaf, C], where:

    • leaf is an end-entity certificate for victim.com, signed with $K_{\text{evil}}$, where leaf.issuer matches $C$’s subject and SAN is set to victim.com.
    • $C$ is a malicious intermediate certificate crafted such that C.tbs.issuer matches the byte-exact DER of $T$’s issuer $I$, and C.tbs.serialNumber matches $T$’s serial $S$. It is self-signed (or dummy-signed) by $K_{\text{evil}}$, has basicConstraints CA:TRUE, keyUsage = keyCertSign, and its SPKI is set to $K_{\text{evil}}$.
  2. Handshake Parsing: At net/cert/cert_verify_proc_builtin.cc:1654, the verification process receives the TLS certificate chain and declares bssl::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.

  3. Path Construction: TryBuildPath is invoked at net/cert/cert_verify_proc_builtin.cc:1435. The path builder attempts to resolve the issuer of leaf. It queries the static intermediate source via path_builder.AddCertIssuerSource(&intermediates) (line 1444) and retrieves candidate certificate $C$ based on name matching.

  4. Trust Query Generation: The path builder invokes CertIssuersIter::AddIssuers(new_issuers) in third_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 to TrustStoreNSS::GetTrust in net/cert/internal/trust_store_nss.cc:272.
  5. NSS Aliased Lookup: Inside TrustStoreNSS::GetTrust, a SECItem representing $C$’s raw DER representation is initialized (trust_store_nss.cc:276-279) and queried using CERT_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_FindCertByDERCert via NSSTrustDomain_FindCertificateByEncodedCertificate, which extracts the issuer and serial using nssPKIX509_GetIssuerAndSerialFromDER and queries the underlying PKCS#11 token for the template {CKA_CLASS=CKO_CERTIFICATE, CKA_ISSUER, CKA_SERIAL_NUMBER}. Because the CKA_VALUE is absent from this search template, NSS retrieves and returns the CERTCertificate* corresponding to the legitimate trusted root $T$, not $C$.

  6. Trust Application Without Byte Validation: At net/cert/internal/trust_store_nss.cc:307, it calls GetTrustIgnoringSystemTrust(nss_cert.get()) without verifying that nss_cert->derCert is byte-equivalent to the input der_cert.

    • CERT_GetCertTrust(T) returns CERTDB_TRUSTED_CA (trust_store_nss.cc:320).
    • Since $T$ resides in an allowed user slot (trust_store_nss.cc:334-336), the fast path at trust_store_nss.cc:374 is hit, returning bssl::CertificateTrust::ForTrustAnchor() with enforce_anchor_constraints and enforce_anchor_expiry enabled.
    • The path builder erroneously binds this trust status to $C$’s entry in the IssuerEntry: entry.cert = C; entry.trust = TrustAnchor;.
  7. Anchor Processing and Sink: During path validation in third_party/boringssl/src/pki/verify_certificate_chain.cc:2010:

    • PathVerifier::ProcessRootCertificate evaluates the trust anchor $C$.
    • Constraints are evaluated against $C$’s attacker-controlled fields (such as basicConstraints CA:TRUE and keyUsage keyCertSign at verify_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 leaf certificate, successfully verifying its signature against working_public_key_ ($K_{\text{evil}}$).
  8. Policy Bypass: Since $C$ is a forged local anchor and not in the hardcoded Chrome Root Store, IsKnownRoot(C) returns false (net/cert/internal/system_trust_store.cc:149). Consequently, verify_result.is_issued_by_known_root is set to false.

    • 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.

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().


  1. 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_FindCertificateByEncodedCertificate in lib/pki/trustdomain.c and token template creation in lib/dev/devtoken.c confirm that PKCS#11 queries only provide {CKA_CLASS, CKA_ISSUER, CKA_SERIAL_NUMBER}.
  • Trust Store configuration: services/cert_verifier/cert_verifier_creation.cc:132-142 confirms that systems utilizing include_system_trust_store (the default) initialize TrustStoreNSS with the UseTrustFromAllUserSlots() flag.
  • DER validation missing: net/cert/internal/trust_store_nss.cc at GetTrust (lines 272-308) blindly trusts the NSS certificate handle returned by CERT_FindCertByDERCert without any post-lookup DER validation.
  • Policy bypass paths: Codebase investigation identified net/cert/cert_verify_proc_builtin.cc:640-644 and 1493-1499 setting is_issued_by_known_root. The subsequent evaluation of this flag bypasses CT at net/cert/require_ct_delegate.cc:13-21 and HPKP at net/http/transport_security_state.cc:462-476.
  • Public Key Extraction: In third_party/boringssl/src/pki/verify_certificate_chain.cc at lines 1745 and 2061, working_public_key_ = ParseAndCheckPublicKey(cert.tbs().spki_tlv, errors); correctly pulls from spki_tlv of 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.

View on issue tracker