Repository navigation
Conversation
Node's QUIC client binds the name to verify with OpenSSL SSL_set1_host, which tries X509_VERIFY_PARAM_set1_ip_asc first and falls back to set1_host. Bun called BoringSSL X509_VERIFY_PARAM_set1_host only, which matches DNS names, so servername: "127.0.0.1" never matched an IP:127.0.0.1 subjectAltName and failed with HOSTNAME_MISMATCH. Parse the servername as an IP literal and bind it with X509_VERIFY_PARAM_set1_ip when it is one.
|
Updated 7:42 AM PT - Oct 17th, 2026
✅ @robobun, your commit 1907059d4844c23774a949347f0a2e359a577c09 passed in 🧪 To try this PR locally: bunx bun-pr 45713That installs a local version of the PR into your bun-45713 --bun |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughQUIC TLS certificate verification now checks IP server names against certificate IP subject alternative names. DNS server names continue to use hostname verification. Tests cover matching and mismatched IP and DNS names. ChangesQUIC IP certificate verification
Suggested reviewers: Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding, I also checked: the new X509_VERIFY_PARAM_set1_ip declaration matches the adjacent set1_host decl and SSL_CTX_get0_param's *mut c_void type, and the v4/v6 octet buffers only live until the FFI call, which copies them. The two behavior shifts that come with mirroring OpenSSL (a non-conformant DNS:127.0.0.1 SAN no longer matching an IP servername, and IP mismatches reporting UNSPECIFIED instead of HOSTNAME_MISMATCH as the code) are intentional per the description and match Node, so I did not flag them.
Extended reasoning...
The change touches QUIC client certificate verification in src/runtime/node/quic/tls.rs, adds one BoringSSL FFI declaration, and adds five test cases. It is security-sensitive (TLS hostname/IP binding on the peer certificate), and one confirmed finding about leading-zero IPv4 literals is posted inline, so a human should still look.
Fixes #45514
Problem
node:quicclient withservername: "127.0.0.1"and a CA-trusted certificate that carriesIP:127.0.0.1in its subjectAltName rejectssession.openedwithPeer certificate validation failed: Hostname mismatch [HOSTNAME_MISMATCH].servername: "localhost"against the same certificate verifies. Node verifies both.src/runtime/node/quic/tls.rs:523binds the servername with BoringSSLX509_VERIFY_PARAM_set1_host, which matches DNS names only. Node calls OpenSSLSSL_set1_host(src/quic/tlscontext.cc), which triesX509_VERIFY_PARAM_set1_ip_ascfirst and falls back toset1_host.Fix
TlsContext::newparses the servername withbun_core::ip_address::parse_strict. An IPv4 or IPv6 literal is bound withX509_VERIFY_PARAM_set1_ip(raw octets). Any other name still goes throughset1_host.SSL_set1_host.parse_strictis the same IP-vs-DNS split bun's other TLS clients use (check_x509_server_identityinsrc/boringssl/lib.rs).OSSLContext::set_hostname).test/js/node/quic/quic-sni.test.ts(5 new cases, 4 fail on the released bun). Also ran all oftest/js/node/quic/.Background
X509_VERIFY_PARAMis the per-context set of checks BoringSSL runs on the peer chain during the handshake.set1_hoststores a DNS name pattern,set1_ipstores 4 or 16 address octets. An IP SAN only ever matches the latter.verifyPeer: "manual", skips the servername binding and the auto-reject insession.opened. The chain is still verified and the result arrives invalidationErrorReason, so the caller has to check that field and callpeerCertificate.checkIP()by hand. Unmodified Node code with the defaultverifyPeer: "auto"never does that.X509_VERIFY_PARAM_set1_ip_ascdirectly: it needs a NUL-terminated copy and a second IP parser. Considered replacing the verify-param check with bun's sharedcheck_server_identitymatcher: that has Nodelib/tls.jssemantics, and Node's QUIC uses OpenSSL's verify-param semantics, so it would diverge from the reference.Downsides
DNS:127.0.0.1SAN no longer matchesservername: "127.0.0.1". OpenSSL and Node behave the same way.verifyHostnameon pays oneparse_strictover the servername (at most 45 bytes, no allocation). A DNS servername longer than 45 bytes is rejected by the length check before any scan.Notes
IP address mismatch [UNSPECIFIED]:X509_V_ERR_IP_ADDRESS_MISMATCHis not in theX509Pointer::ErrorCodename table (src/jsc/bindings/ncrypto.cpp), which is a port of Node's table and stops atHOSTNAME_MISMATCH. The test asserts on the reason text.SSL_set1_host(vendor/boringssl/ssl/ssl_x509.cc:1153) does not do the IP fallback, so calling it instead ofX509_VERIFY_PARAM_set1_hostwould not have fixed this.X509_VERIFY_PARAM_set1_ipcopies the octets (vendor/boringssl/crypto/x509/x509_vpm.cc:340), so the stack buffer does not outlive the call.010.0.0.1) is not an IP literal here. BoringSSL's owna2i_ipaddrejects extra leading zeros because some parsers read them as octal (vendor/boringssl/crypto/x509/v3_utl.cc,get_ipv4_component). OpenSSL reads it as decimal 10. Such a name falls to the DNS-name path and fails closed.test/js/node/quic/quic-sni.test.ts,test/js/node/quic/quic-endpoint.test.ts,test/js/node/quic/quic-stream.test.tson the debug build.