Skip to content

tls: apply sessionTimeout to TLS 1.3 sessions too - #38145

Open
robobun wants to merge 1 commit into
mainfrom
farm/1a505f5e/tls13-session-timeout
Open

robobun wants to merge 1 commit into
mainfrom
farm/1a505f5e/tls13-session-timeout

Conversation

@robobun

@robobun robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • The sessionTimeout TLS option (tls.createServer / createSecureContext / tls.connect, Bun.serve({ tls }), Bun.listen({ tls })) only changes the lifetime of TLS 1.2 sessions. TLS 1.3 tickets, which is what every current client negotiates, still carry BoringSSL's 2-day default and stay resumable for 2 days no matter what was configured.

  • tls.createServer({ key, cert, sessionTimeout: 7 }), read back with openssl s_client -sess_out + openssl sess_id -text:

    TLSv1.2 ticket lifetime hint TLSv1.3 ticket lifetime hint
    node v26.3.0 7 7
    bun 1.4.0 7 172800
  • Cause: us_ssl_ctx_build_raw (packages/bun-usockets/src/crypto/openssl.c:1325) applies the option with SSL_CTX_set_timeout() alone. In BoringSSL that function sets the lifetime of "TLS 1.2 (or earlier) sessions" only (vendor/boringssl/include/openssl/ssl.h, SSL_CTX_set_timeout); TLS 1.3 sessions take theirs from SSL_CTX's session_psk_dhe_timeout (vendor/boringssl/ssl/ssl_session.cc, ssl_get_new_session), which nothing in bun ever set.

Fix

  • us_ssl_ctx_build_raw also calls SSL_CTX_set_session_psk_dhe_timeout() with the same value, inside the existing > 0 guard, so omitting the option (or 0) still leaves both BoringSSL defaults in place.
  • Correct because it matches Node: Node calls OpenSSL's SSL_CTX_set_timeout(), and in OpenSSL that one value is stamped onto sessions of every protocol version. Bun exposes one option too, so it has to reach both of BoringSSL's knobs. Every TLS 1.3 lifetime in BoringSSL (fresh tickets, tickets re-issued on resumption, and the client-side cap on received tickets) reads session_psk_dhe_timeout, so this one call covers all of them.
  • us_ssl_ctx_build_raw is the single place any SSL_CTX is built (us_ssl_ctx_from_options for node:tls, Bun.serve including SNI contexts, Bun.listen/Bun.connect; quic.c directly for HTTP/3, which is TLS 1.3 only and so ignored the option entirely until now), so every entry point picks this up.
  • Tests: describe("sessionTimeout") in test/js/node/tls/node-tls-context.test.ts reads the lifetime out of the client's serialized session ('session' event) for tls.createServer on TLS 1.3 and TLS 1.2, tls.connect({ sessionTimeout }) (client-side cap), Bun.serve({ tls }), and the defaults when the option is omitted. The three TLS 1.3 cases fail on the current build with 172800, the TLS 1.2 and defaults cases pass before and after; all five pass with the fix.
  • Also checked with the fix: the openssl s_client probe above prints 7 for both versions; a sessionTimeout: 3 server resumes a TLS 1.3 ticket right away and refuses it after 4 s (before the fix it still resumed it), same as TLS 1.2 already did; ssl-ctx-cache.test.ts, node-tls-server.test.ts and Node's test-tls-session-timeout*.js still pass.
  • Related: tls: apply sessionTimeout to the server's SSL context #33513 is an older, now conflicting attempt at plumbing the option that predates node:tls: sync the test suite to Node v26.3.0 and fix the gaps it surfaces (+24 tests, 155→179 of 221 upstream passing) #32630 (which landed the plumbing but only the TLS 1.2 call); this PR is the remaining TLS 1.3 half. types: declare allowPartialTrustChain, sessionTimeout, sigalgs and ecdhCurve on Bun.TLSOptions #38134 adds the Bun.TLSOptions typings for this option and currently documents it as TLS 1.2 only; its wording can drop that caveat once this lands.

Background

  • TLS session resumption: after a full handshake the server hands the client a ticket (TLS 1.2 NewSessionTicket right after the handshake; TLS 1.3 one or more NewSessionTicket messages after it, which BoringSSL flushes with the server's first write). The ticket carries a lifetime in seconds; the server refuses tickets older than that and the client stops offering them. sessionTimeout is that lifetime.
  • BoringSSL keeps two lifetimes on an SSL_CTX because it treats the two protocol versions differently: session_timeout (set by SSL_CTX_set_timeout, default 2 hours) for TLS <= 1.2, where resumption reuses the old key material, and session_psk_dhe_timeout (set by SSL_CTX_set_session_psk_dhe_timeout, default 2 days) for TLS 1.3, where resumption mixes in a fresh key exchange. OpenSSL has a single timeout for both, which is why the one Node option maps onto one OpenSSL call but two BoringSSL calls.
  • The test reads the lifetime from the DER that bun's 'session' event emits (i2d_SSL_SESSION): a TLS 1.3 client session stores min(server's ticket lifetime, client's own psk_dhe timeout) in its timeout field, a TLS 1.2 client session stores the server's value in ticketLifetimeHint and the client's own timeout in timeout. Both peers count lifetimes from ticket issuance, so a handshake that straddles second boundaries reports that many seconds less than configured; the test allows for the seconds the connection took instead of asserting an exact value.

BoringSSL's SSL_CTX_set_timeout only sets the lifetime of TLS 1.2 and
earlier sessions; TLS 1.3 sessions take theirs from a separate
SSL_CTX_set_session_psk_dhe_timeout knob, so a configured sessionTimeout
left TLS 1.3 tickets at the 2-day default. OpenSSL (and so Node) applies
the one value to every protocol version; set both knobs to match.
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 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: 18de7190-e9fe-41cc-92da-c88d49a6ead8

📥 Commits

Reviewing files that changed from the base of the PR and between b7a0431 and 9167ab5.

📒 Files selected for processing (3)
  • packages/bun-usockets/src/crypto/openssl.c
  • packages/bun-usockets/src/libusockets.h
  • test/js/node/tls/node-tls-context.test.ts

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

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on bun 1.4.0 (tls.createServer({ sessionTimeout: 7 }) advertises a 172800 s lifetime in TLS 1.3 NewSessionTicket, 7 s in TLS 1.2; Node v26.3.0 advertises 7 s for both). Fix is the extra SSL_CTX_set_session_psk_dhe_timeout() call in us_ssl_ctx_build_raw; the new sessionTimeout block in test/js/node/tls/node-tls-context.test.ts fails without it (three TLS 1.3 cases read back 172800) and passes with it. Waiting on CI.

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

LGTM — one-line BoringSSL call to make sessionTimeout reach TLS 1.3 tickets, matching Node/OpenSSL semantics.

What was reviewed

  • Confirmed SSL_CTX_set_session_psk_dhe_timeout is BoringSSL's documented TLS 1.3 counterpart to SSL_CTX_set_timeout; the call sits inside the existing > 0 guard so unset/0 still keeps both library defaults.
  • Security direction: this makes a user-configured resumption window actually apply to TLS 1.3 (previously silently left at 2 days) — no default is weakened.
  • New tests follow harness conventions (port 0, using, error/close wired to reject, skew-tolerant assertions) and cover TLS 1.2/1.3, client cap, Bun.serve, and the omitted-option defaults.
Extended reasoning...

Overview

The PR adds one call, SSL_CTX_set_session_psk_dhe_timeout(ssl_context, options.session_timeout), alongside the existing SSL_CTX_set_timeout call in us_ssl_ctx_build_raw (packages/bun-usockets/src/crypto/openssl.c:1330), updates the field comment in libusockets.h, and adds a describe("sessionTimeout") block with five tests to test/js/node/tls/node-tls-context.test.ts. BoringSSL, unlike OpenSSL, keeps TLS ≤1.2 and TLS 1.3 session lifetimes on two separate SSL_CTX fields; Node's single sessionTimeout option maps to one OpenSSL call that covers both, so bun (built against BoringSSL) needs both calls to match. us_ssl_ctx_build_raw is the single choke point every SSL_CTX in bun goes through (node:tls, Bun.serve/SNI, Bun.listen/connect, QUIC), so no sibling site needs the same edit.

Security risks

None introduced. The new call only fires when the user explicitly passes sessionTimeout > 0, and it makes that value take effect on TLS 1.3 tickets instead of being silently ignored in favor of BoringSSL's 2-day default. In the common case (users shortening the resumption window), this is a strict security improvement. In the uncommon case of a user setting a value above 2 days, the longer lifetime is what they explicitly requested and matches Node; TLS 1.3 resumption still runs a fresh (EC)DHE, which is exactly why BoringSSL's own default for it is longer than for TLS 1.2. No default is changed, no verification is bypassed, no flag is removed. The int → uint32_t argument conversion is safe inside the > 0 guard.

Level of scrutiny

Medium. TLS code deserves care, but the actual code delta is a single call to a documented BoringSSL API whose semantics are stated in BoringSSL's own header comments ("lifetime for TLS 1.3 sessions"), placed exactly where its TLS 1.2 sibling already lives, guarded identically. This is the archetypal "BoringSSL split one OpenSSL knob into two, call the second one" fix — mechanical and low-ambiguity.

Other factors

The test additions are well-constructed: they parse the serialized SSL_SESSION DER for the timeout [2] and ticketLifetimeHint [9] context-tagged fields, use a bounded skew derived from wall-clock elapsed time rather than exact equality (avoiding second-boundary flakes), wire error/close to reject the awaited promise, use port: 0 and using/try-finally cleanup, and cover the variant matrix (TLS 1.2 vs 1.3, server vs client option, tls.createServer vs Bun.serve, option omitted). The PR description documents that the three TLS 1.3 cases fail with 172800 on the unfixed build and pass with the fix, and that adjacent suites (ssl-ctx-cache.test.ts, Node's test-tls-session-timeout*.js) still pass. No CODEOWNERS entry covers these files. No prior human review comments are outstanding.

@robobun

robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:05 AM PT - Aug 13th, 2026

❌ @robobun, your commit 9167ab5 has some failures in Build #94542 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38145

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

bun-38145 --bun

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.

2 participants