Skip to content

node:tls: implement the server ticketKeys option and getTicketKeys/setTicketKeys - #28691

Open
robobun wants to merge 7 commits into
mainfrom
farm/0c86e962/tls-ticket-keys
Open

robobun wants to merge 7 commits into
mainfrom
farm/0c86e962/tls-ticket-keys

Conversation

@robobun

@robobun robobun commented Mar 30, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #14604

Problem

Three related gaps in tls.Server:

  • The ticketKeys server option was validated (type, 48 bytes) and then discarded. Two servers created with the same ticketKeys, the standard cluster / multi-instance TLS deployment, could never resume each other's sessions, so every cross-instance reconnect was a full handshake.
  • server.getTicketKeys() and server.setTicketKeys() were stubs that threw Not implented in Bun yet.
  • Feature detection (typeof server.getTicketKeys === "function") reported the API as supported.

Node v26.3.0 for reference: the option is applied to the SSL_CTX and both methods work before and after listen(), and on servers that never listen() but receive sockets via server.emit('connection', sock) (the pattern in Node's own test-tls-ticket.js).

Cause

validateSecureContextOptions checked options.ticketKeys but nothing consumed it, and the prototype methods were never wired to BoringSSL's SSL_CTX_get_tlsext_ticket_keys / SSL_CTX_set_tlsext_ticket_keys (same 48-byte name || hmac || aes layout as OpenSSL).

Fix

Native side (Listener.rs, boringssl_sys):

  • jsGetTicketKeys / jsSetTicketKeys host functions backed by SSL_CTX_get/set_tlsext_ticket_keys. They accept either a Listener (the listen() path's secure_ctx, or the Windows named-pipe ctx) or a native SecureContext (the emit('connection') path's _sharedCreds.context).

JS side (tls.ts, net.ts):

  • The key material is stored on the Server as _ticketKeys. setTicketKeys() copies the caller's bytes there and applies them to the live SSL_CTX when one exists; the ticketKeys option routes through the same method.
  • getTicketKeys() reads from the live SSL_CTX when listening; before listen() it returns the stored bytes, generating them lazily on first access. After listen(), the first read pins the bytes on the SSL_CTX so BoringSSL's default-key auto-rotation does not change what later calls return (Node/OpenSSL fixes them at ctx creation).
  • kRealListen applies the stored bytes to the freshly built SSL_CTX before the first accept.
  • buildSharedCreds applies them to the lazily built SecureContext for injected connections, opting out of the digest-interned SSL_CTX cache for that ctx so per-server key material does not leak into unrelated consumers. setTicketKeys drops _sharedCreds so the next injected socket rebuilds with the new keys.

setTicketKeys validation matches Node: ERR_INVALID_ARG_TYPE for a non-buffer, ERR_INTERNAL_ASSERTION for the wrong length (Node uses internal/assert there).

Verification

USE_SYSTEM_BUN=1 bun test test/js/node/tls/node-tls-ticket-keys.test.ts
  8 fail, 1 pass
bun bd test test/js/node/tls/node-tls-ticket-keys.test.ts
  9 pass

The new suite includes the cross-instance resumption repro (two listen() servers sharing ticketKeys, client captures a ticket from A and resumes on B with isSessionReused() === true), the emit('connection') variant of the same, and round-trips through Buffer / Uint8Array / DataView.

Also passing under bun bd: node-tls-context.test.ts, node-tls-connect.test.ts, ssl-ctx-cache.test.ts, and upstream test-tls-basic-validations.js, test-tls-ticket-invalid-arg.js.


[review] gate passed · iteration 18 · 5 files touched

fails on main (without fix)
ASAN without fix: 8 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/tls/node-tls-ticket-keys.test.ts
bun test v1.4.0 (06f5cdb05)

test/js/node/tls/node-tls-ticket-keys.test.ts:
46 | describe("tls.Server ticketKeys", () => {
47 |   test("getTicketKeys returns the ticketKeys option and setTicketKeys replaces them", async () => {
48 |     const keys = Buffer.alloc(48, 7);
49 |     const server = tls.createServer({ ...COMMON_CERT, ticketKeys: keys });
50 | 
51 |     const before = server.getTicketKeys();
                               ^
error: Not implented in Bun yet
      at <anonymous> (node:tls:1092:16)
      at <anonymous> (/workspace/bun/test/js/node/tls/node-tls-ticket-keys.test.ts:51:27)
      at <anonymous> (/workspace/bun/test/js/node/tls/node-tls-ticket-keys.test.ts:47:87)
(fail) tls.Server ticketKeys > getTicketKeys returns the ticketKeys option and setTicketKeys replaces them [118.41ms]
77 |     }
78 |   });
79 | 
80 |   test("getTicketKeys without the ticketKeys option returns stable 48 bytes", async () => {
81 |     const server = tls.createServer({ ...COMMON_CERT });
82 |     c
... (truncated)

release without fix: 8 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/js/node/tls/node-tls-ticket-keys.test.ts:
46 | describe("tls.Server ticketKeys", () => {
47 |   test("getTicketKeys returns the ticketKeys option and setTicketKeys replaces them", async () => {
48 |     const keys = Buffer.alloc(48, 7);
49 |     const server = tls.createServer({ ...COMMON_CERT, ticketKeys: keys });
50 | 
51 |     const before = server.getTicketKeys();
                               ^
error: Not implented in Bun yet
      at <anonymous> (node:tls:847:16)
      at <anonymous> (/workspace/bun/test/js/node/tls/node-tls-ticket-keys.test.ts:51:27)
(fail) tls.Server ticketKeys > getTicketKeys returns the ticketKeys option and setTicketKeys replaces them [1.51ms]
77 |     }
78 |   });
79 | 
80 |   test("getTicketKeys without the ticketKeys option returns stable 48 bytes", async () => {
81 |     const server = tls.createServer({ ...COMMON_CERT });
82 |     const k = server.getTicketKeys();
                          ^
error: Not implented in Bun yet
      at <anonymous> (node:tls:847:16)
      at <anonymous> (/workspace/bun/test/js/node/tls/node-tls-ticket-keys.test.ts:82:22)
(fail) tls.Server ticketKeys > getTicketK
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/tls/node-tls-ticket-keys.test.ts
bun test v1.4.0 (06f5cdb05)

test/js/node/tls/node-tls-ticket-keys.test.ts:
(pass) tls.Server ticketKeys > getTicketKeys returns the ticketKeys option and setTicketKeys replaces them [485.15ms]
(pass) tls.Server ticketKeys > getTicketKeys without the ticketKeys option returns stable 48 bytes [30.98ms]
(pass) tls.Server ticketKeys > getTicketKeys first called after listen() returns stable 48 bytes [51.89ms]
(pass) tls.Server ticketKeys > setTicketKeys accepts Buffer, Uint8Array and DataView [41.42ms]
(pass) tls.Server ticketKeys > two servers sharing ticketKeys resume each other's sessions [845.56ms]
(pass) tls.Server ticketKeys > two servers with different ticketKeys do not resume each other's sessions [273.10ms]
(pass) tls.Server ticketKeys > setTicketKeys after listen enables cross-server resumption [249.61ms]
(pass) tls.Server ticketKeys > ticketKeys reach injected connections (server.emit('connection') without listen) [355.48ms]
(pass) tls.Server ticketKeys > setTicketKeys validation ma
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     06f5cdb05e
  features     baseline

22 deps, 108 codegen, 1171 objects in 5408ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1234] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[2/1234] fetch picohttpparser
[picohttpparser] up to date
[3/1234] fetch zlib
[zlib] up to date
[4/1234] fetch tinycc
[tinycc] up to date
[5/1234] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
[6/1234] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[7/1234] subst deps/zlib/zconf.h
[8/1234] subst deps/zlib/zlib.h
[9/1234] fetch zstd
[zstd] up to date
[10/1234] gen bindgenv2
[11/1234] fetch nodejs (prebuilt)
[nodejs] up to date
[12/1234] gen ProcessBindingBuffer.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingBuffer.lut.h from /workspace/bun/src/jsc/bindings/Process
... (truncated)
diff hotspot
src/boringssl_sys/boringssl.rs                |   7 +
 src/js/node/net.ts                            |   7 +
 src/js/node/tls.ts                            |  60 +++++-
 src/runtime/socket/Listener.rs                | 110 ++++++++++
 test/js/node/tls/node-tls-ticket-keys.test.ts | 278 ++++++++++++++++++++++++++
 5 files changed, 455 insertions(+), 7 deletions(-)

gate history · 1 passed · 1 rejected · iteration 18

evidence per changed file
file                                           reads  edits  tests
src/boringssl_sys/boringssl.rs                     1      2      3
src/js/node/net.ts                                 1      1      3
src/js/node/tls.ts                                15     15     10
src/runtime/socket/Listener.rs                     6      2      4
test/js/node/tls/node-tls-ticket-keys.test.ts      0      0      1

root cause · written by the author bot

Bun's tls.Server shipped getTicketKeys and setTicketKeys as unimplemented stubs that threw, so session ticket key management and cross-server session resumption did not work as in Node.js. The fix implements both methods natively by exposing SSL_CTX_get_tlsext_ticket_keys and SSL_CTX_set_tlsext_ticket_keys through the Listener, with 48 byte length and type validation enforced on both the JavaScript and native sides, and adds a ticketKeys constructor option that is buffered pre listen and applied once a handle exists. Because Bun interns SSL_CTX objects by configuration digest, servers with …

@robobun

robobun commented Mar 30, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:08 PM PT - Jul 25th, 2026

❌ @robobun, your commit 06f5cdb has 1 failures in Build #81021 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 28691

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

bun-28691 --bun

@coderabbitai

coderabbitai Bot commented Mar 30, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Added TLS session-ticket key support to the native Listener, exposed JS bindings for getTicketKeys/setTicketKeys with argument validation, updated Node.js tls.Server wrappers to call the native bindings, and added regression tests covering retrieval, setting, validation, and pre-listen behavior.

Changes

Cohort / File(s) Summary
TLS Listener Implementation
src/bun.js/api/bun/socket/Listener.zig
Introduced ticket_key_size = 48, helper to obtain SSL_CTX, and native getTicketKeys / setTicketKeys that read/write exactly 48 bytes via SSL_CTX_get_tlsext_ticket_keys / SSL_CTX_set_tlsext_ticket_keys. Added JS-callable wrappers jsGetTicketKeys / jsSetTicketKeys with argument/type validation.
Node.js Compatibility
src/js/node/tls.ts
Replaced placeholder throws with working Server.prototype.getTicketKeys() and Server.prototype.setTicketKeys(keys). getTicketKeys returns _getTicketKeys(this._handle) or throws if server not running. setTicketKeys validates ArrayBufferView-compatible input, enforces 48-byte length, and calls _setTicketKeys when a handle exists.
Regression Tests
test/regression/issue/14604.test.ts
Added tests that assert getTicketKeys() returns a 48-byte Buffer, setTicketKeys() persists keys, rejects wrong lengths and invalid types, accepts Uint8Array/DataView 48-byte inputs, and checks behavior when called before the server is listening.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully implements the required functionality: getTicketKeys and setTicketKeys with proper validation and behavior matching Node.js, addressing issue #14604.
Out of Scope Changes check ✅ Passed All changes directly support the ticket keys implementation. Test adjustments are necessary for preventing resource leaks and test failures.
Title check ✅ Passed The title clearly summarizes the main change: implementing tls.Server ticket key support and the get/setTicketKeys APIs.
Description check ✅ Passed The description covers what changed and how it was verified, though it uses custom sections instead of the template headings.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bun.js/api/bun/socket/Listener.zig`:
- Around line 924-943: The setTicketKeys function currently throws "Server is
not listening" when this.socket_context is null; change it to silently no-op by
returning .js_undefined instead to match the JS tls.ts wrapper behavior. Locate
the code in setTicketKeys where you read const ctx = this.socket_context orelse
return global.throw("Server is not listening", .{}); and replace that throw-path
with a plain return .js_undefined; so that when socket_context is null the
function returns undefined rather than raising an exception. Ensure you keep the
rest of the function unchanged (including the ssl check and ticket key
validations).

In `@test/regression/issue/14604.test.ts`:
- Around line 1-5: Add a single-line comment with the GitHub issue URL at the
very top of the test file test/regression/issue/14604.test.ts (above the
existing imports). The comment should follow repository convention (one line,
e.g. `// https://github.com/<org>/<repo>/issues/14604`) so it’s discoverable
alongside the test that references this issue; place it before the import
statements that include `import { expect, test } from "bun:test";`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 441b39fb-98c7-4b8d-9a16-2b755b9f1958

📥 Commits

Reviewing files that changed from the base of the PR and between 17616ae and dbccc02f59fba458f4d36b609600a064282cb301.

📒 Files selected for processing (3)
  • src/bun.js/api/bun/socket/Listener.zig
  • src/js/node/tls.ts
  • test/regression/issue/14604.test.ts

Comment thread src/runtime/socket/Listener.zig Outdated
Comment thread test/regression/issue/14604.test.ts Outdated
Comment thread src/js/node/tls.ts Outdated
Comment thread src/runtime/socket/Listener.zig Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/14604.test.ts`:
- Around line 50-63: Add an additional assertion path that exercises the
DataView branch: create a DataView over a new ArrayBuffer(48) (populate its
bytes to 0xab via a Uint8Array view of that buffer), call
server.setTicketKeys(dataView), then call server.getTicketKeys() and assert
byteLength is 48 and first/last bytes equal 0xab; reuse the existing test setup
(test name, tls_mod.createServer, server.listen, once) and the same expectations
to keep JS validator and Zig coercion aligned.
- Around line 65-70: Add a complementary assertion that calling getTicketKeys()
on the created server before it starts listening does not throw: in the same
test where you create the server via tls_mod.createServer(tls) and assert
setTicketKeys is a no-op, also call server.getTicketKeys() and assert it does
not throw (e.g., expect(() => server.getTicketKeys()).not.toThrow()), then close
the server; reference the existing test and the server variable to locate where
to insert this assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8a5ffa46-9e45-4310-be32-1ef9aeada3e1

📥 Commits

Reviewing files that changed from the base of the PR and between dbccc02f59fba458f4d36b609600a064282cb301 and 2281bec4d4403c2ba8e1159be643df4a80d195c9.

📒 Files selected for processing (3)
  • src/bun.js/api/bun/socket/Listener.zig
  • src/js/node/tls.ts
  • test/regression/issue/14604.test.ts

Comment thread test/regression/issue/14604.test.ts Outdated
Comment thread test/regression/issue/14604.test.ts Outdated
Comment thread src/bun.js/api/bun/socket/Listener.zig Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/14604.test.ts`:
- Around line 43-48: The test that calls tls_mod.createServer and invokes
(server as any).setTicketKeys should be converted to an async test that uses
`await using` (or `using`) to deterministically dispose the server instead of
manually calling server.close(); specifically replace the synchronous test body
with an async function that does `await using (const server =
tls_mod.createServer(tls)) { ... }` (or equivalent) so assertions run inside the
using block and the server is always closed even if an assertion throws; apply
the same change to the other test that allocates `server` in this file (the
similar test around the later range) so both use deterministic disposal rather
than trailing server.close().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5b2ef405-481c-45cc-84d1-87f2df64466e

📥 Commits

Reviewing files that changed from the base of the PR and between 2281bec4d4403c2ba8e1159be643df4a80d195c9 and 52d835dccf5928f010b342a9a4cfe750aba22695.

📒 Files selected for processing (2)
  • src/bun.js/api/bun/socket/Listener.zig
  • test/regression/issue/14604.test.ts

Comment thread test/regression/issue/14604.test.ts Outdated

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, reopen this pull request to trigger a review.

@robobun
robobun force-pushed the farm/0c86e962/tls-ticket-keys branch from 4e6faf9 to 8427161 Compare March 31, 2026 04:06
@ignoramous

Copy link
Copy Markdown

Wow. This is wild.

@robobun
robobun force-pushed the farm/0c86e962/tls-ticket-keys branch from 8427161 to f13ed5a Compare May 5, 2026 19:05
Comment thread src/runtime/socket/Listener.zig Outdated
Comment thread test/regression/issue/14604.test.ts Outdated
Comment thread test/regression/issue/14604.test.ts Outdated
Comment thread src/js/node/tls.ts

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

No further issues found after the 780b107 fix — deferring to a human since this adds new unsafe Rust/BoringSSL FFI for TLS session ticket key material.

Extended reasoning...

Overview

This PR wires up tls.Server.getTicketKeys() / setTicketKeys() end-to-end: two new FFI declarations in src/boringssl_sys/boringssl.rs, ~100 lines of new Rust in src/runtime/socket/Listener.rs (an ssl_ctx() helper, get_ticket_keys/set_ticket_keys impls, and two #[bun_jsc::host_fn] shims with unsafe FFI and pointer derefs), ~20 lines in src/js/node/tls.ts replacing the stubs and validating input, and a 97-line regression test. All prior inline feedback (mine and CodeRabbit's) has been addressed and resolved; the most recent fix in 780b107 restores the Node-compatible validation text so test-tls-ticket-invalid-arg.js passes again.

Security risks

Session ticket keys are cryptographic key material used to encrypt TLS session state. The implementation is a thin pass-through to BoringSSL's SSL_CTX_{get,set}_tlsext_ticket_keys with a fixed 48-byte length check on both the JS and Rust sides, so there's no obvious injection, overflow, or key-exposure surface introduced. get_ticket_keys copies into a stack [u8; 48] and returns a fresh Buffer; set_ticket_keys length-checks before the FFI call. The unsafe blocks are narrowly scoped and commented. Still, this is exactly the category (TLS/crypto + raw FFI) where a second pair of human eyes is the right bar.

Level of scrutiny

Medium-high. The change is conceptually simple (two BoringSSL accessors), but it spans the JS↔native boundary, adds new unsafe Rust against an SSL_CTX*, and handles cryptographic key material in a production-critical path. That puts it outside the "simple/mechanical" envelope I'd auto-approve.

Other factors

The bug-hunting system found nothing on the current revision. Test coverage is reasonable (round-trip set→get, length/type validation, Uint8Array/DataView inputs, pre-listen behavior), and the upstream Node parallel test test-tls-ticket-invalid-arg.js is now expected to pass. The robobun CI status comment still reflects the previous commit (d23a99e) with musl build failures and the now-fixed ticket-invalid-arg failures; CI for 780b107 hadn't reported when I reviewed. The author declined the test-file-location nit, which is fine — it was non-blocking.

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

All prior feedback has been addressed and I didn't find anything new in this revision, but since this adds native unsafe FFI handling TLS session-ticket key material it's worth a human pass before merging.

Extended reasoning...

Overview

This PR implements tls.Server.getTicketKeys() / setTicketKeys(), replacing "Not implented in Bun yet" stubs. It touches four files: two FFI declarations in boringssl_sys/boringssl.rs, ~20 lines of JS glue in src/js/node/tls.ts, ~100 lines of new native Rust (with several unsafe blocks) in src/runtime/socket/Listener.rs, and a new 7-case test file.

Security risks

TLS session-ticket keys are cryptographic secrets used to encrypt resumption tickets; the implementation passes raw 48-byte buffers between JS and BoringSSL via unsafe FFI, with pointer casts and lifetime reasoning around SSL_CTX* (including a Windows named-pipe branch). A mistake here could leak key material, read/write past buffer bounds, or operate on a freed context. The validation looks correct (48-byte length checked on both JS and Rust sides; ssl_ctx() handles the not-listening case), but this is exactly the kind of code that benefits from a second pair of eyes.

Level of scrutiny

Medium-high. This is production TLS-server code, not sandbox/experimental. It's a first-time implementation of a Node.js-compat surface rather than a refactor, and it crosses the JS↔Rust↔BoringSSL boundary with unsafe. The change is moderately sized but conceptually focused.

Other factors

The PR has gone through many review iterations (wrong error macro, Windows named-pipe SSL_CTX path, banned-API usage, Zig→Rust field rename after refactor, Node-compat error text) — all of which the author fixed promptly and are now resolved. The current revision passed the bug-hunting system cleanly. Test coverage is good (Buffer/Uint8Array/DataView inputs, length/type validation, pre-listen behavior, round-trip persistence). CI build #63141 on 5bbf4bf is in progress. Given the security-sensitive surface and the amount of churn it took to get here, deferring to a human reviewer rather than auto-approving.

@robobun

robobun commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

Diff is green on all lanes it touches. The red lanes across the last few builds are unrelated flakes, none of them in TLS/ticket-key code:

  • `test/integration/next-pages/test/dev-server.test.ts` (darwin-26) — puppeteer/Chrome download + next.js dev server
  • `test/cli/install/bun-install-registry.test.ts` (windows-11 aarch64) — install registry
  • `test/js/web/fetch/fetch-leak.test.ts` (aarch64) — RSS-threshold leak test
  • `test/regression/issue/30205.test.ts` (x64-asan) — NAPI finalizer

Scraped build annotations show zero failures mentioning tls / ticket / Listener / boringssl. The new regression test (test/regression/issue/14604.test.ts, 7 cases) and the upstream test/js/node/test/parallel/test-tls-ticket-invalid-arg.js both pass locally under bun bd. All CodeRabbit and claude review threads are resolved. Ready for a maintainer pass on the native FFI.

@robobun
robobun force-pushed the farm/0c86e962/tls-ticket-keys branch from 5bbf4bf to 73cf25f Compare June 27, 2026 11:00
@robobun

robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main and force-pushed. The only conflict was trivial: main renamed the binding helper from $newZigFunction("Listener.zig", ...) to $newRustFunction("Listener.rs", ...), so the two ticket-key bindings now use $newRustFunction to match. The implementation (native get_ticket_keys/set_ticket_keys on the Listener, the bun_boringssl_sys externs, and the Node-compatible setTicketKeys error text) replayed cleanly. Verified locally under bun bd: all 7 cases in test/regression/issue/14604.test.ts pass and test/js/node/test/parallel/test-tls-ticket-invalid-arg.js exits 0.

Comment thread src/js/node/tls.ts Outdated
@robobun

robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

Good follow-up note. The ticketKeys constructor option being validated-then-discarded is pre-existing (the validation at the top of validateSecureContextOptions predates this PR) and out of scope here, which is focused on the getTicketKeys/setTicketKeys prototype stubs for #14604. Wiring the constructor option through (stash on this, apply once _handle exists, mirroring addContext) is a clean follow-up now that _setTicketKeys exists, but Bun creates the SSL_CTX lazily at listen() so it needs the same pre-listen buffering — better as its own change.

Separately, pushed b57b2aa: the oxlint-plugin-bun CI failure was real (bun(no-duplicate-conditional-property-access) on this._handle read twice in both methods); fixed by reading _handle into a local. update_interactive_install.test.ts on windows-2019-baseline is an unrelated flake.

Comment thread test/regression/issue/14604.test.ts Outdated
@robobun robobun changed the title Implement tls.Server.getTicketKeys() and setTicketKeys() node:tls: implement the server ticketKeys option and getTicketKeys/setTicketKeys Jun 28, 2026
@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 3e7b850e8f, which expands this PR to cover the whole feature rather than just the two prototype stubs.

A report surfaced that with only the stubs fixed, the common deployment is still broken: the ticketKeys constructor option was validated and then discarded, so two servers created with the same 48-byte ticketKeys (what cluster sets up automatically, and what every multi-instance TLS terminator does manually) still could not resume each other's sessions. On top of that, getTicketKeys() threw ERR_SERVER_NOT_RUNNING before listen() and setTicketKeys() silently dropped the keys there, both unlike Node, which builds the SSL_CTX in the constructor.

I earlier called the option "better as its own change", but since it is ~45 lines of JS on top of the exact native methods this PR already adds, and a second PR would have to rewrite the same two prototype methods, folding it in here is less total review than two PRs. The Rust in this PR is unchanged by the new commit; the delta is tls.ts / net.ts plus tests.

What the new commit adds:

  • The key material lives on the Server (_ticketKeys). setTicketKeys() copies the caller's bytes there and applies them to the live SSL_CTX when one exists; the ticketKeys option routes through the same method.
  • getTicketKeys() before listen() returns the stored bytes (generated lazily on first access when no option was given) instead of throwing. After listen() it reads from the SSL_CTX as before.
  • net.ts applies the stored bytes to the freshly built SSL_CTX right after Bun.listen().
  • test/js/node/tls/node-tls-ticket-keys.test.ts: two servers sharing ticketKeys resume each other's sessions (and two servers with different keys do not), plus option / pre-listen / copy-semantics coverage.
  • The last case of test/regression/issue/14604.test.ts said "either outcome is acceptable" for the pre-listen behavior; now that it is pinned to Node's, it asserts that.

Verified locally: all 13 cases across both test files pass under bun bd and 9 of them fail under USE_SYSTEM_BUN=1; the reported differential repro now produces byte-for-byte the same output as Node v26.3.0. PR body updated with the full details.

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

The expanded scope (wiring the ticketKeys constructor option through kRealListen, pre-listen _ticketKeys buffering with a randomBytes(48) default, and the new end-to-end resumption suite) looks correct and the bug hunt came back clean — but this is TLS session-ticket key material plumbed through new unsafe BoringSSL FFI, so deferring to the maintainer pass you already flagged.

Extended reasoning...

Overview

Since my last review (2026-06-27) the PR grew substantially: commit 3e7b850 addressed the pre-existing follow-up I'd noted by (a) stashing options.ticketKeys / setTicketKeys() input on this._ticketKeys before listen, (b) applying it in net.ts kRealListen immediately after Bun.listen() builds the SSL_CTX, (c) making getTicketKeys() return the stashed keys (or a lazily-generated randomBytes(48)) pre-listen instead of throwing, and (d) adding a 195-line end-to-end suite in test/js/node/tls/node-tls-ticket-keys.test.ts that verifies cross-server session resumption with shared keys actually works on the wire. The native side (Listener.rs get_ticket_keys/set_ticket_keys/ssl_ctx(), boringssl.rs externs) is unchanged from what I last looked at.

Security risks

Session ticket keys are the symmetric secret that protects TLS resumption state — getting the plumbing wrong (e.g. silently dropping user-supplied keys, or leaking/reusing default keys across contexts) has direct forward-secrecy implications. The implementation looks right: the JS layer copies caller buffers (Buffer.from(new Uint8Array(...))) so post-call mutation can't change what's applied; the native side enforces the 48-byte length and reads/writes through a live SSL_CTX*; and the e2e tests assert both that shared keys enable resumption and that distinct keys prevent it. Still, this is exactly the kind of crypto-adjacent surface where a maintainer should sign off.

Level of scrutiny

High. New unsafe Rust FFI into BoringSSL (SSL_CTX_get/set_tlsext_ticket_keys), changes to node:tls server lifecycle, and a hook added inside net.ts kRealListen. The author explicitly requested a maintainer pass on the native FFI back on 2026-06-17, and the scope has only grown since.

Other factors

All prior review threads (mine and CodeRabbit's) are resolved; the bug-hunting system found nothing on the current diff. The robobun status comment still shows ❌ on build #65459 for b57b2aa — the author previously characterized the red lanes as unrelated flakes, but the latest commit (3e7b850) doesn't yet have a reported green build, so I'm not approving on CI grounds either.

@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Verified 3e7b850 (the ticketKeys server-option + pre-listen get/set enhancement) locally on a fresh bun bd:

  • test/regression/issue/14604.test.ts → 7 pass
  • test/js/node/tls/node-tls-ticket-keys.test.ts (cross-server resumption e2e) → 6 pass
  • test/js/node/test/parallel/test-tls-ticket-invalid-arg.js → exit 0
  • oxlint on src/js/node/tls.ts + src/js/node/net.ts → 0 errors

setTicketKeys copies via Buffer.from(new Uint8Array(...)) so caller mutation cannot alter what is applied, and kRealListen pushes the stored bytes to the SSL_CTX right after Bun.listen(). All review threads resolved. Ready for a maintainer pass on the native FFI.

@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Heads up on build #65866 (3e7b850): the ~20 red build-cpp/build-rust/build-bun lanes are not a real build break. In Buildkite they are expired/broken with exit None (never ran) plus ~205 waiting_failed cascading from them; 60 jobs passed. This commit only touches JS builtins (src/js/node/tls.ts, src/js/node/net.ts, tests), which cannot affect cross-platform C++/Rust compilation, and it builds + passes locally (bun bd: regression 7/7, node-tls-ticket-keys e2e 6/6, upstream test-tls-ticket-invalid-arg.js exit 0, oxlint 0 errors). Looks like a CI scheduler/agent cascade; a rebuild should clear it.

…tTicketKeys

Fixes #14604.

tls.Server.getTicketKeys() and tls.Server.setTicketKeys() were stubs
that threw 'Not implented in Bun yet', and the ticketKeys constructor
option was validated and then discarded. Two servers created with the
same ticketKeys (the standard cluster / multi-instance TLS deployment)
could never resume each other's sessions, so every cross-instance
reconnect was a full handshake.

Native side (Listener.rs, boringssl_sys): jsGetTicketKeys /
jsSetTicketKeys host functions backed by
SSL_CTX_get/set_tlsext_ticket_keys on the listener's secure_ctx (or the
Windows named-pipe context's ctx).

JS side (tls.ts, net.ts): the key material is stored on the Server as
_ticketKeys. setTicketKeys() copies the caller's bytes there and applies
them to the live SSL_CTX when one exists; the ticketKeys option routes
through the same method. getTicketKeys() reads from the live SSL_CTX
when listening; before listen() it returns the stored bytes, generating
them lazily on first access (Node's constructor-time SSL_CTX has the
same effect). kRealListen applies the stored bytes to the freshly built
SSL_CTX before the first accept.
@robobun
robobun force-pushed the farm/0c86e962/tls-ticket-keys branch from 3e7b850 to 485d7b6 Compare July 25, 2026 10:23
Comment thread src/js/node/net.ts Outdated
Comment thread src/js/node/tls.ts Outdated
Comment thread src/js/node/tls.ts Outdated
Comment thread src/runtime/socket/Listener.rs Outdated
@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (df6c7ee) as a single squashed commit.

Main had drifted since June: secure_ctx is now Cell<Option<...>>, Listener::from_js became as_class_ref::<Listener>(), and setSecureContext restructured its option assignment. The implementation is otherwise unchanged from the last reviewed version.

Verified locally on bun bd:

  • test/regression/issue/14604.test.ts: 7 pass
  • test/js/node/tls/node-tls-ticket-keys.test.ts (cross-server resumption e2e): 6 pass
  • upstream test-tls-ticket-invalid-arg.js, test-tls-basic-validations.js: exit 0
  • node-tls-connect.test.ts: 31 pass, 0 fail
  • oxlint on touched files: 0 errors

Fail-before on released bun: 9 fail, 4 pass.

Comment thread src/js/node/tls.ts
Comment thread test/regression/issue/14604.test.ts Outdated
Comment thread src/js/node/tls.ts Outdated
Comment thread test/js/node/tls/node-tls-ticket-keys.test.ts
robobun added 2 commits July 25, 2026 13:01
- Make jsGetTicketKeys/jsSetTicketKeys accept a native SecureContext in
  addition to a Listener, and apply _ticketKeys in buildSharedCreds so
  server.emit('connection', sock) without listen() honors the keys.
- Opt buildSharedCreds out of the digest-interned SSL_CTX cache when
  _ticketKeys is set so per-server key material doesn't leak into other
  consumers of the same cert.
- setTicketKeys length check now throws ERR_INTERNAL_ASSERTION (Error),
  matching Node's internal/assert path.
- Pin the SSL_CTX's default ticket keys on first getTicketKeys() after
  listen() so BoringSSL's 48h auto-rotation doesn't change what future
  calls return (Node/OpenSSL fixes the bytes at ctx creation).
- Fold the 14604 regression tests into node-tls-ticket-keys.test.ts
  (not a regression: the API was never implemented) and add a test for
  the emit('connection') path.
@robobun
robobun force-pushed the farm/0c86e962/tls-ticket-keys branch from c00d5e6 to c2f0e58 Compare July 25, 2026 13:28
Comment thread src/js/node/tls.ts Outdated
Comment thread src/js/node/tls.ts Outdated
Comment thread src/js/node/tls.ts Outdated
Comment thread src/runtime/socket/Listener.rs Outdated
Comment thread src/js/node/tls.ts
Comment thread test/js/node/tls/node-tls-ticket-keys.test.ts
@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Self-review addressed in 667c0b7..cbb8737:

  • emit('connection') mode: jsGetTicketKeys/jsSetTicketKeys now also accept a native SecureContext, and buildSharedCreds applies _ticketKeys to the lazily built ctx (opting out of the digest-interned SSL_CTX cache so per-server key material stays per-server). New test mirrors Node's test-tls-ticket.js pattern: one net.Server fronts two never-listened tls.Servers with shared ticketKeys, cross-server resumption works.
  • setTicketKeys length check now throws ERR_INTERNAL_ASSERTION (Error) to match Node's internal/assert.
  • First getTicketKeys() after listen() pins the bytes it reads so BoringSSL's default-key auto-rotation doesn't change later reads.
  • Tests consolidated into test/js/node/tls/node-tls-ticket-keys.test.ts with the issue link; test/regression/issue/14604.test.ts dropped (the API was never implemented, so not a regression).
  • Review nit: await listen() moved inside try so cleanup runs if it rejects.
  • comment-cop: multi-line comments shortened to one line.

bun bd test test/js/node/tls/node-tls-ticket-keys.test.ts: 9 pass, 0 fail. Fail-before on released bun: 8 fail, 1 pass. All review threads resolved.

The post-listen getTicketKeys pin was the one _ticketKeys
undefined->defined transition that left a stale cached _sharedCreds, so
injected sockets kept handshaking under the old interned SSL_CTX while
getTicketKeys reported the pinned key. Also read sc.context into a local
in buildSharedCreds to satisfy no-duplicate-conditional-property-access.

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

06f5cdb addresses both remaining nits — the post-listen pin branch now invalidates _sharedCreds like the other two sites, and the Verification block matches the single 9-test file. No new findings this pass; deferring only because the native side (new unsafe BoringSSL FFI in Listener.rs + the SSL_CTX cache opt-out when ticketKeys is set) is TLS-adjacent and worth a maintainer look.

What was reviewed: the three _ticketKeys undefined→defined transitions now all drop _sharedCreds consistently; buildSharedCreds opts out of the digest-interned cache so per-server ticket keys can't leak onto a shared SSL_CTX; the 48-byte length is enforced in both JS and Rust before the FFI call; SSL_CTX_set_tlsext_ticket_keys copies its input so the borrowed byte_slice() lifetime is sound.

Extended reasoning...

Overview

Implements tls.Server ticketKeys option + getTicketKeys()/setTicketKeys(). Five files: two BoringSSL FFI declarations in boringssl_sys/boringssl.rs; ~110 lines of new Rust in Listener.rs (two #[host_fn]s + helpers, four unsafe blocks calling SSL_CTX_get/set_tlsext_ticket_keys); ~60 lines in tls.ts replacing the two stub methods and threading _ticketKeys through buildSharedCreds/setSecureContext; a 6-line hook in net.ts kRealListen; and a 278-line test file with 9 tests including two end-to-end cross-server resumption checks.

Security risks

Session ticket keys are security-relevant (a leaked key lets an attacker decrypt captured TLS 1.2 sessions), but this PR wires an existing OpenSSL/BoringSSL API through to JS rather than changing the security model. The one meaningful risk — writing per-server ticket keys onto a digest-interned SSL_CTX shared with unrelated consumers — is handled: buildSharedCreds passes cached = (ticketKeys === undefined) so a keyed context is createPrivate, not intern. The 48-byte length is validated on the JS side before any native call and again in Rust before the FFI. SSL_CTX_set_tlsext_ticket_keys memcpy's its input, so passing a borrowed byte_slice() view is sound. No new user-controlled data reaches an allocation size or index.

Level of scrutiny

Medium-high. It's a Node-compat feature fill, not a design change, and it's been through ~5 review rounds with substantive fixes each time (the injected-emit('connection') path, error-code parity, cache opt-out, _sharedCreds invalidation). But it adds unsafe FFI into BoringSSL and touches the per-VM SSL_CTX cache policy, both of which the repo's REVIEW.md flags as areas where a maintainer should sign off.

Other factors

All prior findings from my earlier passes are addressed in the current diff; the bug-hunting system found nothing this run. Test coverage is solid: cross-server resumption over both the listen() and emit('connection') paths, negative resumption with mismatched keys, Buffer/Uint8Array/DataView round-trips, copy-semantics for both get and set, and error-code assertions matching Node's ERR_INVALID_ARG_TYPE/ERR_INTERNAL_ASSERTION. The Windows named-pipe ssl_ctx() branch is untested (no Windows-specific test), which is worth noting but consistent with the surrounding code.

Jarred-Sumner pushed a commit that referenced this pull request Sep 25, 2026
…context (#42998)

### Problem

- `tls.Server` with `ca: caA` plus `addContext("b.test", { ca: caB, key,
cert })` refuses a caA client certificate at `b.test`
(`UNABLE_TO_VERIFY_LEAF_SIGNATURE`). The same client keeps `a.test`'s
`session`, offers it as `servername: "b.test"`, and the handler runs
with `authorized: true`. TLS 1.3 and 1.2.
- A resumed handshake skips client authentication. BoringSSL resumes
only when the session's id context equals the connection's
(`vendor/boringssl/ssl/ssl_session.cc:474`). No `node:tls` context sets
one.
- #36174 fixed this for `Bun.serve` `serverName` entries only. nginx
fixed the class as CVE-2025-23419.

### Fix

- Each `SSL_CTX` a server can serve gets the SHA-256 of its options as
its session id context (`create_ssl_context_with_digest`): the
`tls.Server` and `Bun.listen` listeners, `SSLContextCache`,
`tls.createSecureContext()`. `addCACert` folds its certificate in.
- BoringSSL checks the id after the SNI switch, and `SSL_set_SSL_CTX`
copies the selected context's id. A mismatch is not an error: a full
handshake runs and verifies the certificate against that context's CA.
- The id is content-derived, so an `SNICallback` that builds a context
per handshake still resumes. Client sockets keep no id: one `SSL_CTX`
backs clients and servers, and a client aborts a resume when the ids
differ.
- Verified: `test/js/node/tls/node-tls-context.test.ts`, 12 new tests, 8
fail on main, each client clear covered by one that fails without it.
Other suites and vendored tests: see the notes.

### Background

- Session resumption: the client presents a ticket from an earlier
connection, both sides skip the certificate exchange, and the server
reports the verdict stored in the session.
- Session id context: an opaque string on an `SSL_CTX`, copied into each
new session. BoringSSL refuses a session whose id differs, and tells
servers to partition sessions between SNI hosts this way
(`include/openssl/ssl.h:2199-2207`). RFC 6066 section 3 allows resuming
only a session established for the requested name.
- SNI context: `addContext()` and `SNICallback` pick an `SSL_CTX` per
ClientHello. The ticket keys belong to the listener default context, so
every context of that listener decrypts its tickets.

<details><summary>Notes</summary>

**Outcome table** (client certificate issued by caA, session from
`a.test` offered at `b.test`, which trusts only caB):

| | bun main | this PR |
|---|---|---|
| `authorized` | `true` | `false` |
| `authorizationError` | none | `UNABLE_TO_VERIFY_LEAF_SIGNATURE` |
| `isSessionReused()` | `true` | `false` |
| handler reached with the default `rejectUnauthorized` | yes | no |
| `a.test` session at `a.test` | resumed | resumed |

An independent client (`openssl s_client -servername a.test -sess_out`,
then `-servername b.test -sess_in`) shows the same before and after, so
the door does not depend on Bun's client.

**Why the id is derived from the options and not from the context
object.** #36174 uses a counter, which fits `Bun.serve` because those
contexts are built once per server. A `node:tls` `SNICallback` commonly
calls `tls.createSecureContext()` for each handshake. With an id per
object such a server would never resume. With the options digest, two
contexts built from the same options share sessions, which is safe
because they authenticate clients the same way. The digest is the
existing `BunSocketContextOptions::digest()`, the key of
`SSLContextCache`: inline PEM content, and path plus mtime and size for
the file options. It is broader than the client-certificate policy, so a
change to an unrelated option (`ciphers`, `sigalgs`) also ends
resumption for old sessions. That errs toward a full handshake.

**Why the server side needs no code at the SNI switch.**
`SSL_set_SSL_CTX` replaces the connection's `CERT` with a copy of the
new context's `CERT`, and the session id context is a field of `CERT`.
So the servername callback, the static SNI tree, the asynchronous
`SNICallback` resume and `socket.setKeyCert()` all move the connection
to the id of the selected context with no extra call.

**Context sharing.** `SSLContextCache` shares one `SSL_CTX` between all
users of the same options, clients and servers, which is why the client
side clears the id per socket.

**Client clears.** The two places that build an `SSL` are
`us_internal_ssl_attach` (socket) and `SSLWrapper::init_with_ctx`
(duplex, named pipe). `setKeyCert` is the only other caller of
`SSL_set_SSL_CTX`, and on a client Bun applies it (Node ignores it
there), so it needs the clear too. Tests: `lets a client offer a session
under other client options` covers the first two (TCP and Duplex), `lets
a client that calls setKeyCert() still resume` the third. Each fails
with `ERR_SSL_ATTEMPT_TO_REUSE_SESSION_IN_DIFFERENT_CONTEXT` when its
clear is removed. A future `SSL_new` site for a client needs the same
line. There is no lint for that.

**Landing order.** #28691 (server `ticketKeys`) should land after this
one: shared ticket keys let one process decrypt another's tickets, which
extends the same door across servers and processes, and it needs a
negative test for that. The options digest keeps cross-process
resumption working for identical options, which a counter or a pointer
would not. #33483 and #42050 touch the SNI switch and
`setSecureContext`; they do not set a session id context and do not
conflict.

**Not covered.** `addCACert()` on a context that already serves
connections: a handshake that selected the context before the call, and
verifies its client certificate after it, issues a session under the old
id. A second context built from the same options without that CA can
then resume it. The window is one round trip and needs that sibling
context. On `main` any context resumes any session, so this PR narrows
that door. Node allows `addCACert()` at any time, so this PR adds no
refusal.

TLS 1.2 with an `ALPNCallback` that calls `socket.setKeyCert(ctx)`:
BoringSSL decides TLS 1.2 resumption before the ALPN callback runs, so a
session id context cannot reach that path. TLS 1.3 with the same setup
is covered, because ALPN runs before session selection there. The
`sessionIdContext` option is still accepted and dropped; honoring it
belongs with #28691. `node:quic` has its own context path and is not
changed.

**Suites run on the debug build:** `node-tls-context.test.ts` (29 pass),
`node-tls-connect.test.ts` (57 pass), `node-tls-server.test.ts` (77
pass, 1 failure that also fails without this diff: `localhost` resolves
to `::1` first in this container), `node-tls-cert.test.ts`,
`node-tls-namedpipes.test.ts`, `ssl-ctx-cache.test.ts`,
`node-tls-upgrade.test.ts`, `node-tls-internals.test.ts`,
`renegotiation.test.ts`,
`node-tls-connect-hostname-verification.test.ts`,
`bun-serve-ssl.test.ts` (18 pass), `fetch.tls.test.ts` (41 pass),
`fetch-session.test.ts` (32 pass), `proxy.test.ts` (90 pass). Vendored
Node tests: `test-tls-add-context`, `test-tls-sni-option`,
`test-tls-sni-server-client`, `test-tls-sni-servername`,
`test-tls-snicallback-error`, `test-tls-empty-sni-context`,
`test-tls-client-resume`, `test-tls-client-resume-12`,
`test-https-client-resume`, `test-tls-ticket`,
`test-tls-ticket-cluster`, `test-tls-session-cache`,
`test-tls-secure-session`, `test-tls-connect-secure-context`,
`test-tls-secure-context-usage-order`, `test-tls-alpn-server-client`,
`test-tls-psk-circuit`, `test-tls-psk-server`,
`test-tls-reuse-host-from-socket`, `test-https-agent-session-reuse`,
`test-https-agent-disable-session-reuse`,
`test-https-agent-session-eviction`,
`test-https-agent-session-injection`, `test-https-agent-sni`.

**Comments.** The rationale for the deliberate refusal (RFC 6066 section
3, the BoringSSL header) sits on `us_ssl_apply_selected_ctx` in
`openssl.c`, next to the SNI switch, and on the test block. The Rust
call sites carry one line each.

**Self-reviewed:** 6 concerns raised, 6 addressed
(default-`rejectUnauthorized` coverage, the Windows named-pipe listener,
the `setKeyCert` clear test, the scope claim above, the
deliberate-refusal comments at the helper and the test, and the landing
order).

</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 1 · 9 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 8 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/tls/node-tls-context.test.ts
bun test v1.4.3 (c6b7fcb)

test/js/node/tls/node-tls-context.test.ts:
(pass) tls.Server > addContext [898.64ms]
(pass) tls.Server > should select the most recently added SecureContext [175.29ms]
(pass) tls.Server > should allow multiple CA [175.40ms]
(pass) tls.Server > should allow multiple CA in newline-separated strings [53.03ms]
(pass) tls.Server > SNI tls.Server + tls.connect [286.16ms]
475 | 
476 |       // b.test verifies the certificate again, against ca2, whichever
477 |       // context's session the client offers.
478 |       const fromDefault = await connect(server, "b.test", { session: atDefault.session });
479 |       const fromA = await connect(server, "b.test", { session: atA.session });
480 |       expect([fromDefault.seen, fromA.seen]).toEqual([refused("b.test"), refused("b.test")]);
                                                   ^
error: expect(received).toEqual(expected)

  [
    {
-     "authorized": false,
-     "error": "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
+     "autho
... (truncated)

release without fix: all passed
bun test v1.4.3-canary.1 (e0d54a6)

test/js/node/tls/node-tls-context.test.ts:
(pass) tls.Server > addContext [29.23ms]
(pass) tls.Server > should select the most recently added SecureContext [6.86ms]
(pass) tls.Server > should allow multiple CA [4.38ms]
(pass) tls.Server > should allow multiple CA in newline-separated strings [2.53ms]
(pass) tls.Server > SNI tls.Server + tls.connect [11.38ms]
(pass) session resumption across SNI contexts (TLSv1.3) > runs a full handshake under a context that trusts another CA [20.26ms]
(pass) session resumption across SNI contexts (TLSv1.3) > keeps a refused client out of the handler with the default rejectUnauthorized [9.98ms]
(pass) session resumption across SNI contexts (TLSv1.3) > covers the contexts an SNICallback builds for each handshake [10.63ms]
(pass) session resumption across SNI contexts (TLSv1.3) > covers contexts that differ only by addCACert [11.52ms]
(pass) session resumption across SNI contexts (TLSv1.3) > lets a client offer a session under other client options [9.06ms]
(pass) session resumption across SNI contexts (TLSv1.3) > lets a client that calls setKeyCert() still resume [4.72ms]
(pass) session resumption 
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/tls/node-tls-context.test.ts
bun test v1.4.3 (c6b7fcb)

test/js/node/tls/node-tls-context.test.ts:
(pass) tls.Server > addContext [765.80ms]
(pass) tls.Server > should select the most recently added SecureContext [124.51ms]
(pass) tls.Server > should allow multiple CA [201.14ms]
(pass) tls.Server > should allow multiple CA in newline-separated strings [75.66ms]
(pass) tls.Server > SNI tls.Server + tls.connect [278.18ms]
(pass) session resumption across SNI contexts (TLSv1.3) > runs a full handshake under a context that trusts another CA [495.27ms]
(pass) session resumption across SNI contexts (TLSv1.3) > keeps a refused client out of the handler with the default rejectUnauthorized [221.28ms]
(pass) session resumption across SNI contexts (TLSv1.3) > covers the contexts an SNICallback builds for each handshake [164.83ms]
(pass) session resumption across SNI contexts (TLSv1.3) > covers contexts that differ only by addCACert [121.09ms]
(pass) session resumption across SNI contexts (TLSv1.3) > lets a client offer a session und
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 783ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/129] gen generated_host_exports.rs
generated_host_exports.rs: 121 exports (host=5, lazy=10, generic=106, rust=0); 242 extern-C blocks audited
[2/129] gen cpp.rs (cppbind)
[3/129] gen JSSink.{cpp,h,lut.h,rs}
generated_jssink.rs: 7 sinks, 84 exported symbols
Generating /workspace/bun/build/release/codegen/JSSink.lut.h from /workspace/bun/build/release/codegen/JSSink.lut.txt
[4/129] gen JS modules (bundle-modules)
Preprocess modules (12205ms)
Bundle modules (134ms)
Postprocesss modules (434ms)
Bundle Functions (731ms)
Generate Code (51ms)

[13.57s] Bundled "src/js" for production
  2600 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[4/27] cargo bun_runtime → libbun_runtime.a
�[1m�[33mwarning�[0m�[1m: binary `bun_shim_impl` should have a kebab-case name�[0m
   �[1m�[94m|�[0m
�[1m�[94m 1�[0m �[1m�[94m|�[0m /workspace/bun/build/release/rust-target/.../bun_shim_impl
   �[1m�[94m|�[0m                                              �[1m�[33m^^^^^^^^^^^^^�[0m
   �[1m�[94m|�
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
packages/bun-usockets/src/crypto/openssl.c |  15 +-
 src/boringssl_sys/boringssl.rs             |  10 ++
 src/runtime/api/bun/SSLContextCache.rs     |   2 +-
 src/runtime/api/bun/SecureContext.rs       |  27 ++-
 src/runtime/socket/Listener.rs             |   5 +-
 src/runtime/socket/tls_socket_functions.rs |   4 +
 src/uws/lib.rs                             |   6 +-
 src/uws_sys/SocketContext.rs               |  19 ++
 test/js/node/tls/node-tls-context.test.ts  | 280 ++++++++++++++++++++++++++++-
 9 files changed, 356 insertions(+), 12 deletions(-)
```

</details>

**gate history** · 2 passed · 0 rejected · iteration 1

<details><summary>evidence per changed file</summary>

```
file                                        reads  edits  tests
packages/bun-usockets/src/crypto/openssl.c      5      2     23
src/boringssl_sys/boringssl.rs                  1      3     23
src/runtime/api/bun/SSLContextCache.rs          1      1     23
src/runtime/api/bun/SecureContext.rs            2      3     23
src/runtime/socket/Listener.rs                  3      2     23
src/runtime/socket/tls_socket_functions.rs      1      1     23
src/uws/lib.rs                                  1      2     24
src/uws_sys/SocketContext.rs                    3      2     23
test/js/node/tls/node-tls-context.test.ts       6      7     23
```

</details>

<!-- robobun:evidence:end -->

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

node compat: missing tls.Server.setTicketKeys([48]byte)

2 participants