Skip to content

tls: apply CA name constraints to the subject CN of certificates without a DNS subjectAltName - #37758

Open
Jarred-Sumner wants to merge 2 commits into
mainfrom
claude/tls-cn-fallback-name-constraints
Open

Jarred-Sumner wants to merge 2 commits into
mainfrom
claude/tls-cn-fallback-name-constraints

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

What does this PR do?

Aligns certificate chain verification with how Bun (and Node.js) match host names, for CAs that carry an X.509 nameConstraints extension.

Bun, like Node.js, matches the host name against the subject common name whenever a certificate has no DNS subjectAltName entry. Node.js pairs that rule with OpenSSL's NAME_CONSTRAINTS_check_CN: during chain verification, a host-name-like common name of such a leaf must satisfy the DNS name constraints of every issuing CA. Upstream BoringSSL keys its common-name handling to its own X509_check_host instead (which only consults the common name when there is no subjectAltName extension at all, and then rejects any DNS-like common name under a constrained CA outright), so the certificates whose common name was held to the constraints were not the same certificates whose common name Bun uses for matching, and legacy SAN-less certificates with an in-subtree common name were rejected where Node.js accepts them.

This adds patches/boringssl/name-constraints-common-name.patch (applied through the existing patches: list in scripts/build/deps/boringssl.ts), which replaces that block of check_name_constraints() with the OpenSSL behaviour: when the leaf has no dNSName SAN, each common name that reads as a host name is matched against the issuer's constraints with BoringSSL's own nc_match, and a mismatch is reported through the normal verify result (permitted subtree violation / excluded subtree violation). Because it is part of X509_verify_cert, every client and server path (fetch, WebSocket, node:tls/node:https, Bun.connect/Bun.listen, the SQL and Valkey clients) observes it the same way, through rejectUnauthorized / authorizationError, exactly like Node.js.

Which common names count as host names is decided by BoringSSL's existing x509v3_looks_like_dns_name (the same heuristic X509_check_host uses for its common-name fallback, so on SAN-less certificates the two stay in step), with two adjustments: a leftmost label containing a single partial wildcard (w*.example.com, which Node-style matching honours) is widened to * first, so it is constrained like the whole-label wildcard that covers it; and next to a subjectAltName extension a single-label common name (localhost, jsmith) is not taken to be a DNS identifier, as in OpenSSL's cn2dnsid, so client and device certificates of that shape keep verifying as they do in Node.js and in earlier Bun releases. OpenSSL also skips wildcard and absolute (trailing-dot) common names; Bun does not, so for that shape Bun is deliberately stricter than Node.js: under a name-constrained CA, a certificate without a DNS subjectAltName whose common name is a wildcard pattern outside the permitted subtree fails chain verification (whatever name or address it is dialed by). Every other case in the comparison below matches Node.js. Decorative common names (Some Server, jdoe@example.com) are not treated as host names.

How did you verify your code works?

  • New fixtures under test/js/node/tls/fixtures/name-constraints-*: root CA -> intermediate with nameConstraints=critical,permitted;DNS:.allowed.test -> five leaves sharing one key: inside (CN=host.allowed.test, email SAN), outside (CN=outside.example.test, email SAN), wildcard (CN=.example.test, email SAN), partial-wildcard (CN=w.example.test, email SAN), nosan (CN=legacy.allowed.test, no SAN extension).
  • test/js/node/tls/node-tls-connect-hostname-verification.test.ts: tls.connect rejects outside, wildcard and partial-wildcard with permitted subtree violation (and reports authorized === false / authorizationError === "UNSPECIFIED" with rejectUnauthorized: false, matching Node.js), and accepts inside and nosan.
  • test/js/web/fetch/fetch.tls.test.ts: fetch rejects outside, wildcard and partial-wildcard with PERMITTED_VIOLATION and succeeds for inside and nosan.
  • Both tests fail on Bun 1.4.0 and pass with this change.
  • Compared a matrix of 42 constraint/common-name combinations (leading-dot and bare constraints, excluded subtrees, multiple CNs, single-label/decorative/uppercase/trailing-dot/underscore CNs, IP/URI/email-only and absent SANs, whole and partial wildcard CNs, client certificates verified by a server, untrusted chains) between node v26.3.0, openssl verify 3.2 and this build: they agree in 35; the 7 differences are the wildcard-pattern and label-boundary-hyphen common names described above plus a SAN-less single-label common name (rejected before and after this change), all of which Bun rejects and Node.js accepts. Relative to Bun 1.4, SAN-less certificates whose common name lies inside the permitted subtree (or whose issuers only constrain other name types) are now accepted, as in Node.js.
  • Existing suites: test/js/node/tls/*, test/js/node/crypto/x509.test.ts, test/js/bun/http/bun-serve-ssl.test.ts and the vendored test-tls-* / test-crypto-x509 Node.js tests pass.

…out a DNS subjectAltName

Bun (like Node.js) matches the host name against the subject common name
whenever a certificate has no DNS subjectAltName. Chain verification now
holds host-name-like common names of such certificates to the DNS name
constraints of every issuing CA, as OpenSSL's NAME_CONSTRAINTS_check_CN
does, so the certificates whose common name is constrained are the same
ones whose common name is used for matching. This lives in a BoringSSL
patch (patches/boringssl/name-constraints-common-name.patch) so every
X509_verify_cert caller reports it through the normal verify result.
Wildcard common names are constrained too; SAN-less certificates with an
in-subtree common name now verify as they do in Node.js.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b9a15262-dc35-44d2-b10d-ff9d480e26c9

📥 Commits

Reviewing files that changed from the base of the PR and between 626034f and 867c9e5.

⛔ Files ignored due to path filters (8)
  • test/js/node/tls/fixtures/name-constraints-ca-cert.pem is excluded by !**/*.pem
  • test/js/node/tls/fixtures/name-constraints-inside-cert.pem is excluded by !**/*.pem
  • test/js/node/tls/fixtures/name-constraints-intermediate-cert.pem is excluded by !**/*.pem
  • test/js/node/tls/fixtures/name-constraints-leaf-key.pem is excluded by !**/*.pem
  • test/js/node/tls/fixtures/name-constraints-nosan-cert.pem is excluded by !**/*.pem
  • test/js/node/tls/fixtures/name-constraints-outside-cert.pem is excluded by !**/*.pem
  • test/js/node/tls/fixtures/name-constraints-partial-wildcard-cert.pem is excluded by !**/*.pem
  • test/js/node/tls/fixtures/name-constraints-wildcard-cert.pem is excluded by !**/*.pem
📒 Files selected for processing (4)
  • patches/boringssl/name-constraints-common-name.patch
  • scripts/build/deps/boringssl.ts
  • test/js/node/tls/node-tls-connect-hostname-verification.test.ts
  • test/js/web/fetch/fetch.tls.test.ts

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the automated bug hunt found no issues. Because it patches BoringSSL's X.509 chain-verification path (check_name_constraints in x509_vfy.cc) and changes which certificates are accepted under a name-constrained CA, a human review of the security semantics is warranted.

Checked: the range-for over x_impl->altname when the leaf has no SAN extension (nullptr stack) — BoringSSL's STACK_OF iterator treats that as empty, and the nosan fixture exercises it; widen_partial_wildcard_label writes in place into the ASN1_STRING_to_UTF8 buffer, which is heap-owned by free_idval and only narrows the view; the i == 0 guard restricts the CN check to the leaf, matching OpenSSL's NAME_CONSTRAINTS_check_CN placement.

Extended reasoning...

Overview

This PR adds a source patch to the vendored BoringSSL (patches/boringssl/name-constraints-common-name.patch, wired into scripts/build/deps/boringssl.ts) that replaces upstream's reject_dns_name_in_common_name handling in check_name_constraints() with an OpenSSL-style NAME_CONSTRAINTS_check_CN equivalent (x509_check_common_name_constraints). It also adds eight PEM fixtures and new test cases in node-tls-connect-hostname-verification.test.ts and fetch.tls.test.ts covering inside/outside/wildcard/partial-wildcard/no-SAN leaves under a DNS-name-constrained intermediate.

Security risks

This is squarely security-sensitive: it modifies certificate chain verification inside a crypto library, and it loosens one behavior relative to upstream BoringSSL (SAN-less leaves with a DNS-like CN inside the permitted subtree now pass verification where upstream rejected them outright with X509_V_ERR_NAME_CONSTRAINTS_WITHOUT_SANS) while tightening another (leaves with a non-DNS SAN and an out-of-subtree CN, including wildcard/partial-wildcard CNs, now fail). The reasoning is well-argued and aligns with Node.js/OpenSSL, but any change to which certificates X509_verify_cert accepts affects every TLS client and server path in Bun and deserves a maintainer's eyes on the fail-open/fail-closed boundaries — particularly the single-label-CN carve-out next to a SAN extension and the deliberate divergence from OpenSSL on wildcard CNs.

Level of scrutiny

High. The repo review guidelines call out TLS/crypto paths and vendored-dependency patches as areas where flags and checks should not be altered without explicit understanding. Patching x509_vfy.cc is exactly the kind of change a human should sign off on regardless of how careful the implementation looks.

Other factors

Test coverage is solid (both accept and reject cases across tls.connect and fetch, including rejectUnauthorized: false reporting), and the PR description documents a 42-case comparison against Node v26 and openssl verify. I found no memory-safety issues in the new C++: the UTF-8 buffer is RAII-owned by UniquePtr, embedded-NUL handling matches OpenSSL's cn2dnsid, and the stack-allocated ASN1_IA5STRING/GENERAL_NAME passed to nc_match mirrors how NAME_CONSTRAINTS_check builds its temporary in the same file. Still deferring given the subject matter.

@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 6:05 AM PT - Aug 12th, 2026

❌ @Jarred-Sumner, your commit 867c9e5 has 1 failures in Build #92963 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 37758

That installs a local version of the PR into your bun-37758 executable, so you can run:

bun-37758 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it patches BoringSSL's X.509 chain-verification path (check_name_constraints) and changes when certificates are accepted or rejected under a name-constrained CA, a human look is still warranted.

Checked: the range-for over x_impl->altname is safe when the leaf has no SAN extension (BoringSSL's stack iterators handle nullptr, and the nosan fixture exercises it); widen_partial_wildcard_label writes only into the ASN1_STRING_to_UTF8 buffer owned by free_idval; the i == 0 guard restricts the new CN check to the leaf, matching OpenSSL's NAME_CONSTRAINTS_check_CN placement.

Extended reasoning...

Overview

This PR adds a BoringSSL patch (patches/boringssl/name-constraints-common-name.patch) that rewrites part of check_name_constraints() in crypto/x509/x509_vfy.cc and adds x509_check_common_name_constraints() to v3_ncons.cc. It replaces upstream BoringSSL's blanket rejection of DNS-like common names on SAN-less leaves under a constrained CA with OpenSSL-style behaviour: when the leaf has no dNSName SAN, each host-name-like CN is matched against the issuer's DNS name constraints via nc_match. The patch is wired into the build via scripts/build/deps/boringssl.ts, and eight new PEM fixtures plus tests in node-tls-connect-hostname-verification.test.ts and fetch.tls.test.ts cover accept/reject cases for in-subtree, out-of-subtree, wildcard, partial-wildcard and no-SAN CNs.

Security risks

This is directly security-sensitive: it modifies the certificate chain verification path that every TLS client and server in Bun (fetch, node:tls, Bun.connect, WebSocket, SQL/Valkey clients) relies on. The change both loosens verification in one direction (SAN-less leaves with an in-subtree CN are now accepted where upstream BoringSSL rejected them) and tightens it in another (leaves with a non-DNS SAN whose CN falls outside the permitted subtree are now rejected). The heuristics for which CNs count as DNS names (x509v3_looks_like_dns_name + partial-wildcard widening + single-label exemption next to a SAN) determine whether a name-constraint bypass is possible, so any gap between what host-name matching honours and what this function constrains would be a security hole. The PR description documents a 42-case comparison against Node.js/OpenSSL and the residual differences are all strict-side (Bun rejects, Node accepts), which is the safe direction — but that analysis deserves human confirmation.

Level of scrutiny

High. Per the repo's own review rules, TLS/crypto verification paths are called out explicitly ("Never remove a flag you don't understand in a TLS/crypto path", "Security checks fail closed and cover every path to the protected effect"). Patching a vendored crypto library's X509_verify_cert internals is not a mechanical change; the interaction between X509_check_host's CN fallback, Node-style checkServerIdentity, and this new constraint check is subtle and the PR itself enumerates seven cases where behaviour intentionally diverges from Node.js.

Other factors

The PR is very well-documented and thoroughly tested (both tls.connect and fetch paths, both accept and reject cases, both rejectUnauthorized values). The bug-hunting pass found nothing. I spot-checked memory handling (UniquePtr frees the UTF-8 buffer on every path; the in-place '*' write stays inside that buffer), null-SAN iteration, and that the new check is scoped to the leaf (i == 0). Nothing looks wrong to me — but given this is chain verification for every TLS connection in the runtime, this is exactly the kind of change that should not land on automated review alone.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants