Skip to content

Fix session ticket key rotation when hint exceeds key lifetime#10822

Open
mattia-moffa wants to merge 2 commits into
wolfSSL:masterfrom
mattia-moffa:20260629-ticket-hint-window-check
Open

Fix session ticket key rotation when hint exceeds key lifetime#10822
mattia-moffa wants to merge 2 commits into
wolfSSL:masterfrom
mattia-moffa:20260629-ticket-hint-window-check

Conversation

@mattia-moffa

@mattia-moffa mattia-moffa commented Jun 30, 2026

Copy link
Copy Markdown
Member

Description

Disallows a ticketHint > WOLFSSL_TICKET_KEY_LIFETIME / 2.

The default ticket encryption callback rotates two keys and can only honor a hint below half the ticket key lifetime; a larger hint previously left no key available for encryption, failing every handshake after the first on a reused server CTX. Now these values are rejected.

Fixes #10796

Testing

./configure --enable-dtls13 --enable-session-ticket && make && make check

Checklist

  • added tests

@mattia-moffa mattia-moffa self-assigned this Jun 30, 2026
Copilot AI review requested due to automatic review settings June 30, 2026 21:38

Copilot AI 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.

Pull request overview

Fixes a session-ticket encryption key rotation edge case where ticketHint can exceed the default ticket key lifetime, previously causing TicketEncCbCtx_ChooseKey() to return BAD_STATE_E and break subsequent handshakes when reusing a server WOLFSSL_CTX.

Changes:

  • Update default session ticket key selection/rotation logic to avoid returning BAD_STATE_E when both keys are decrypt-valid but neither covers ticketHint.
  • Add a new API regression test that reuses server/client contexts across multiple handshakes with an oversized ticket hint.
  • Register the new test in the ssl extension test list.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/internal.c Adjusts default ticket key selection behavior when neither key covers the configured hint window.
tests/api/test_ssl_ext.c Adds a regression test covering repeated handshakes with a reused server context and oversized ticket hint.
tests/api/test_ssl_ext.h Exposes/registers the new regression test in the ssl_ext group.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/internal.c Outdated
Comment thread tests/api/test_ssl_ext.c Outdated
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

gcc-arm-cortex-m0plus

  • FLASH: .text +524 B (+0.8%, 64,107 B / 262,144 B, total: 24% used)

gcc-arm-cortex-m3

  • FLASH: .text +688 B (+0.6%, 122,221 B / 262,144 B, total: 47% used)

gcc-arm-cortex-m4

  • FLASH: .rodata.wolfSSL_ERR_reason_error_string.str1.1 +18 B, .text +640 B (+0.3%, 199,902 B / 262,144 B, total: 76% used)

gcc-arm-cortex-m4-baremetal

  • FLASH: .text +576 B (+0.9%, 66,763 B / 262,144 B, total: 25% used)

gcc-arm-cortex-m4-crypto-only

  • FLASH: .text +512 B (+0.3%, 174,314 B / 262,144 B, total: 66% used)

gcc-arm-cortex-m4-dtls13

  • FLASH: .text +704 B (+0.4%, 180,632 B / 1,048,576 B, total: 17% used)

gcc-arm-cortex-m4-min-ecc

  • FLASH: .text +512 B (+0.8%, 61,677 B / 262,144 B, total: 24% used)

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .rodata +208 B, .text +1,280 B (+0.2%, 769,812 B / 1,048,576 B, total: 73% used)

gcc-arm-cortex-m4-pkcs7

  • FLASH: .text +640 B (+0.3%, 212,206 B / 262,144 B, total: 81% used)

gcc-arm-cortex-m4-pq

  • FLASH: .rodata +24 B, .text +960 B (+0.4%, 279,048 B / 1,048,576 B, total: 27% used)

gcc-arm-cortex-m4-rsa-only

  • FLASH: .rodata +24 B, .text +960 B (+0.3%, 324,648 B / 1,048,576 B, total: 31% used)

gcc-arm-cortex-m4-sp-math

  • FLASH: .text +512 B (+0.8%, 61,677 B / 262,144 B, total: 24% used)

gcc-arm-cortex-m4-tls12

  • FLASH: .text +640 B (+0.5%, 122,957 B / 262,144 B, total: 47% used)

gcc-arm-cortex-m4-tls13

  • FLASH: .rodata.wolfSSL_ERR_reason_error_string.str1.1 +18 B, .text +896 B (+0.4%, 235,792 B / 262,144 B, total: 90% used)

gcc-arm-cortex-m7

  • FLASH: .rodata.wolfSSL_ERR_reason_error_string.str1.1 +18 B, .text +704 B (+0.4%, 199,902 B / 262,144 B, total: 76% used)

gcc-arm-cortex-m7-pq

  • FLASH: .rodata +24 B, .text +960 B (+0.4%, 279,624 B / 1,048,576 B, total: 27% used)

gcc-arm-cortex-m7-tls13

  • FLASH: .rodata.wolfSSL_ERR_reason_error_string.str1.1 +18 B, .text +896 B (+0.4%, 235,792 B / 262,144 B, total: 90% used)

linuxkm-pie

  • Data: __patchable_function_entries +216 B (+0.9%, 25,416 B)

linuxkm-standard

  • Data: __patchable_function_entries +248 B (+0.5%, 48,048 B)

stm32-sim-stm32h753

  • FLASH: .text +512 B (+0.3%, 183,076 B / 2,097,152 B, total: 9% used)

The default ticket encryption callback rotates two keys and can only
honor a hint below WOLFSSL_TICKET_KEY_LIFETIME/2; a larger hint
previously left no key available for encryption, failing every handshake
after the first on a reused server CTX. Now these values are rejected.
@mattia-moffa mattia-moffa force-pushed the 20260629-ticket-hint-window-check branch from 3125631 to 9514b21 Compare July 1, 2026 19:48
@mattia-moffa

Copy link
Copy Markdown
Member Author

Jenkins retest this please

@dgarske dgarske requested a review from SparkiDev July 7, 2026 22:13

@dgarske dgarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Skoll Multi-Scan Review

Modes: review + review-securityOverall recommendation: REQUEST_CHANGES
Findings: 5 total — 4 posted, 1 skipped
2 finding(s) posted as inline comments (see file-level comments below)
2 finding(s) not tied to a diff line (full detail below)

Posted findings

  • [Medium] [review+review-security] Hint rejection also fires with a custom TicketEncCb -- macro gate reflects macro presence, not the default callbacksrc/ssl_api_ext.c:1073-1080
  • [Low] [review] Compile-time and runtime hint checks disagree by one for odd WOLFSSL_TICKET_KEY_LIFETIMEwolfssl/internal.h:2050-2052

Findings not tied to a diff line

New hint cap regresses OpenSSL-compat SSL_CTX_set_timeout for common timeouts (= 30 min)

File: src/ssl_api_ext.c:1073-1080 (manifests via src/ssl_sess.c:958-968)
Function: wolfSSL_CTX_set_TicketHint / wolfSSL_CTX_set_timeout
Severity: High
Category: Regression

wolfSSL_CTX_set_TicketHint is also called internally by the OpenSSL-compat wolfSSL_CTX_set_timeout (SSL_CTX_set_timeout). Under OPENSSL_EXTRA && HAVE_SESSION_TICKET && !NO_WOLFSSL_SERVER, wolfSSL_CTX_set_timeout(ctx, to) forwards the session timeout straight into wolfSSL_CTX_set_TicketHint(ctx, (int)to) (ssl_sess.c:968). The new check rejects any hint >= WOLFSSL_TICKET_KEY_LIFETIME / 2, i.e. any value >= 1800 seconds (30 min) with the default 3600s key lifetime. Session timeouts of 30+ minutes are the norm (OpenSSL's own default is 7200s; values like 3600/7200/86400 are routine), so SSL_CTX_set_timeout(ctx, 7200) -- which previously succeeded (only >604800 was rejected) -- now returns BAD_FUNC_ARG. This is a backward-incompatible break of a widely-used public compat API. Worse, ctx->timeout is already assigned at ssl_sess.c:958 BEFORE the ticket-hint call fails, so the session-cache timeout is silently applied while the function reports failure, leaving inconsistent state for callers that check the return code. Severity views differ (review: High/BLOCK; review-security: Medium) -- the stricter High is kept because this breaks a public API for common inputs; the session-cache timeout is conceptually independent of ticket-key rotation. Not covered by CI: test_wolfSSL_CTX_set_timeout only uses values below 1800.

Recommendation: Decouple the session-cache timeout from the ticket-hint constraint. In wolfSSL_CTX_set_timeout, clamp the value passed to wolfSSL_CTX_set_TicketHint (e.g. min(to, WOLFSSL_TICKET_KEY_LIFETIME/2 - 1)) or ignore a ticket-hint rejection when setting the session timeout, so a large session timeout still succeeds and ctx state stays consistent. Keep the hard rejection only for the direct public setter if desired. Add a test_wolfSSL_CTX_set_timeout case with a value >= WOLFSSL_TICKET_KEY_LIFETIME/2 (e.g. 3600) to lock in the intended behavior.

Referenced code: src/ssl_api_ext.c:1073-1080 (manifests via src/ssl_sess.c:958-968) (10 lines)


No test covers the wolfSSL_CTX_set_timeout - set_TicketHint regression path

File: tests/api.c:27884-27913
Function: test_wolfSSL_CTX_set_timeout
Severity: Medium
Category: test

The new tests in test_ssl_ext.c exercise the direct setter, but the highest-impact behavior change is via the internal caller wolfSSL_CTX_set_timeout. The existing test_wolfSSL_CTX_set_timeout only uses small values (20, 30, 100), all below the new 1800s cap, so it will neither catch the regression nor lock in whatever the intended behavior becomes. Since the change silently alters what SSL_CTX_set_timeout returns for common values, this path needs explicit coverage.

Recommendation: Add a case under OPENSSL_EXTRA + HAVE_SESSION_TICKET + server that calls wolfSSL_CTX_set_timeout with a value >= WOLFSSL_TICKET_KEY_LIFETIME/2 (e.g. 3600) and asserts the intended outcome (success with clamped hint per the fix above), and verify ctx state is consistent.


Skipped findings

  • [Info] Stricter compile-time #error may break previously-valid custom lifetime/hint configurations

Review generated by Skoll

Comment thread src/ssl_api_ext.c Outdated
Comment thread wolfssl/internal.h Outdated

@dgarske dgarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

SparkiDev
SparkiDev previously approved these changes Jul 7, 2026
@dgarske

dgarske commented Jul 8, 2026

Copy link
Copy Markdown
Member

Jenkins retest this please: "Error cloning remote repo 'origin'". Timeout

@dgarske dgarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Skoll Multi-Scan Review

Modes: review + review-securityOverall recommendation: COMMENT
Findings: 4 total — 4 posted, 0 skipped
3 finding(s) posted as inline comments (see file-level comments below)
1 finding(s) not tied to a diff line (full detail below)

Posted findings

  • [Medium] [review+review-security] Oversized ticket hint returns FATAL and tears down the whole handshake instead of degrading to a ticket-less connectionsrc/internal.c:41749
  • [Low] [review] New failure uses WOLFSSL_TICKET_RET_FATAL while other encrypt-side returns in this function use REJECTsrc/internal.c:41751
  • [Info] [review] Trivial custom ticket-encryption callback duplicated across two test filestests/api/test_ssl_ext.c:203

Findings not tied to a diff line

Compile-time ticket lifetime guard does not cover the new runtime 2*hint constraint

File: wolfssl/internal.h:2050
Function: DefTicketEncCb
Severity: Low
Category: Convention

internal.h statically enforces WOLFSSL_TICKET_KEY_LIFETIME > SESSION_TICKET_HINT_DEFAULT, but the new runtime rule requires 2 * ticketHint < WOLFSSL_TICKET_KEY_LIFETIME. With the defaults (lifetime 3600, default hint 300) there is no problem (600 < 3600). However, a user who overrides WOLFSSL_TICKET_KEY_LIFETIME to a value in the range (SESSION_TICKET_HINT_DEFAULT, 2*SESSION_TICKET_HINT_DEFAULT] (e.g. 400-600) passes the compile-time #error check yet now has every default-hint ticket handshake fail at runtime, with no build-time warning. This is a narrow, already-degenerate configuration, but the failure surfaces only at runtime.

Recommendation: Consider tightening the static assert to #if WOLFSSL_TICKET_KEY_LIFETIME <= 2 * SESSION_TICKET_HINT_DEFAULT (or documenting the 2*hint requirement near WOLFSSL_TICKET_KEY_LIFETIME) so users who shrink the key lifetime are warned at build time rather than seeing runtime handshake failures.

Referenced code: wolfssl/internal.h:2050-2052 (3 lines)


Review generated by Skoll

Comment thread src/internal.c

/* The two-key rotation can only honor a hint below half the key lifetime.
* Refuse to issue a ticket whose advertised lifetime it cannot cover. */
if (enc && 2 * ctx->ticketHint >= WOLFSSL_TICKET_KEY_LIFETIME) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟠 [Medium] Oversized ticket hint returns FATAL and tears down the whole handshake instead of degrading to a ticket-less connection · Logic

The new check if (enc && 2 * ctx->ticketHint >= WOLFSSL_TICKET_KEY_LIFETIME) return WOLFSSL_TICKET_RET_FATAL; runs on every ticket-issuing (enc) call of the default callback. FATAL (-1) propagates DefTicketEncCb -> CreateTicket (goto error) -> SendTls13NewSessionTicket / TLS1.2 SendTicket -> wolfSSL_accept returns WOLFSSL_FATAL_ERROR, so the entire TLS handshake fails and the client cannot connect at all. Session tickets are optional per RFC 8446, so a server that merely cannot issue a resumption ticket could instead complete the handshake and serve a ticket-less (no-resumption) connection; in TLS 1.3 the NewSessionTicket is sent after the handshake is already cryptographically complete, so a pure configuration problem is surfaced as a full connection failure. This is an easily-triggered functional regression, not attacker-controlled: in OpenSSL-compat builds (OPENSSL_EXTRA && HAVE_SESSION_TICKET && !NO_WOLFSSL_SERVER), wolfSSL_CTX_set_timeout(ctx, to) copies the session timeout straight into ctx->ticketHint (src/ssl_sess.c:968, clamped only to [0,604800]). With the default WOLFSSL_TICKET_KEY_LIFETIME of 3600, any timeout >= 1800 (e.g. 3600, or OpenSSL's common 7200) makes 2*ticketHint >= 3600 true, so EVERY handshake that issues a ticket now fails. BEFORE this PR the same misconfiguration let the FIRST connection succeed (it generated the secondary key and issued a ticket) and only failed reused-CTX connections thereafter (issue #10796); AFTER this PR even the first connection fails. The PR's own test fixture had to install a custom callback to avoid this new limit…

Fix: Confirm with maintainers that failing the handshake is the intended contract. Prefer a less destructive response: clamp the advertised ticket lifetime hint down to a value the two-key rotation can cover, or skip issuing the ticket (full handshake next time) via a distinct 'skip ticket' return path in CreateTicket, instead of returning FATAL. At minimum, guard the OpenSSL-compat propagation in wolfSSL_CTX_set_timeout so large session timeouts do not silently push ticketHint past WOLFSSL_TICKET_KEY_LIFETIME/2, and document in release notes that SSL_CTX_set_timeout / ticket-hint values >= half the ticket key lifetime disable default-callback tickets and will fail the handshake.

Comment thread src/internal.c
* Refuse to issue a ticket whose advertised lifetime it cannot cover. */
if (enc && 2 * ctx->ticketHint >= WOLFSSL_TICKET_KEY_LIFETIME) {
WOLFSSL_MSG("Ticket hint exceeds half the ticket key lifetime");
return WOLFSSL_TICKET_RET_FATAL;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔵 [Low] New failure uses WOLFSSL_TICKET_RET_FATAL while other encrypt-side returns in this function use REJECT · Convention

Every other failure on the encryption (enc) path inside DefTicketEncCb returns WOLFSSL_TICKET_RET_REJECT (RNG failure at 41781, encrypt failure at 41843), while this new check returns WOLFSSL_TICKET_RET_FATAL. On the encrypt/create path both values are treated identically by the caller (both hit goto error at internal.c:40393 and fail the handshake), so this is not a functional defect. FATAL arguably communicates 'permanent misconfiguration' better than REJECT, but the inconsistency within the same function is worth a deliberate choice.

Fix: Either keep FATAL intentionally (permanent config error) with a brief comment, or use REJECT to match the other encrypt-side error returns in this function.

Comment thread tests/api/test_ssl_ext.c
&& !defined(NO_WOLFSSL_SERVER)
/* Trivial custom ticket encryption callback: it has no key-lifetime constraint,
* so it must be able to issue a ticket for any hint. */
static int test_TicketHint_custom_encCb(WOLFSSL* ssl,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⚪ [Info] Trivial custom ticket-encryption callback duplicated across two test files · Style

test_TicketHint_custom_encCb (test_ssl_ext.c) and test_tls13_max_lifetime_ticketEncCb (test_tls13.c:5737-5754) are byte-for-byte equivalent trivial XOR-0xA5 ticket callbacks. Cross-file duplication of small test helpers is common in this suite and not harmful, but a shared helper in tests/utils.* would avoid two copies drifting.

Fix: Optional: factor the trivial XOR ticket callback into a shared test utility if it is reused again; otherwise leave as-is.

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.

[Bug]: DefTicketEncCb: BAD_STATE_E on second handshake when ticket hint > WOLFSSL_TICKET_KEY_LIFETIME / 2

5 participants