Repository navigation
Size session ticket buffers for tickets without a peer cert - #11991
Conversation
With OPENSSL_ALL and KEEP_PEER_CERT the session ticket can carry the peer certificate, and every buffer derived from the ticket size was sized for the largest one (MAX_TICKET_PEER_CERT_SZ). Each WOLFSSL_SESSION had a ~2.3 KB static ticket buffer, MAX_PSK_ID_LEN followed the same size so Arrays carried two ~2.3 KB identity buffers and WOLFSSL_CTX one, and the default static session cache was about 86 KB. - WOLFSSL_SESSION.staticTicket is SESSION_TICKET_STATIC_LEN, the length of a ticket without a certificate. Longer tickets use the existing dynamic ticket buffer. Builds that do not put the cert in the ticket keep their sizes. - CreateTicket() picks the peer certificate first and sizes the ticket from it: the space given to the encrypt callback is WOLFSSL_TICKET_ENC_SZ less the certificate room the ticket does not use. A bad encrypt length now fails without leaving ticketLen past the end of the buffer, and ticketLen is cleared until the new ticket is made. - MAX_PSK_ID_LEN defaults to SESSION_TICKET_STATIC_LEN, as in builds without OPENSSL_ALL. MAX_PSK_ID_WIRE_LEN lets a server accept its own longer tickets as TLS 1.3 PSK identities; longer identities are not copied into Arrays or offered to the PSK callbacks. - Checks for "fits in the static buffer" use sizeof(staticTicket). GetSessionFromCache() records the size it allocated, wolfSSL_set_SessionTicket() reallocates based on ticketLenAlloc, and a restored persistent cache drops a ticket that was not in the static buffer instead of clamping it. Behaviour change: with OPENSSL_ALL and certificates in tickets, MAX_PSK_ID_LEN used to be the full ticket size (about 2.3 KB) and is now the size of a ticket without a certificate (a few hundred bytes), the limit builds without OPENSSL_ALL already have. A TLS 1.3 server no longer offers a longer external PSK identity to its PSK callback (a debug message says so), a longer TLS 1.2 PSK identity fails the handshake, and identity hints are cut to the new length. Define MAX_PSK_ID_LEN to keep a larger limit. On a Cortex-M33 with OPENSSL_ALL: Arrays 4976 -> 880 bytes, WOLFSSL_SESSION 2588 -> 540, WOLFSSL_CTX 2888 -> 840, static session cache 85536 -> 17952. In a TLS 1.3 client handshake the heap peak drops by 8 KB. Tests: resumption from a ticket carrying the client certificate over TLS 1.3 and TLS 1.2, including copies of the session, and an encrypt callback that reports too long an output.
|
There was a problem hiding this comment.
🟡 Changes recommended
The session layout change lacks the required cache-version bump, and failed ticket replacement can leave a stale length that causes out-of-bounds reads.
2 open issues
What changed in this PR
This PR reduces wolfSSL’s session and PSK buffer footprint by sizing static storage for tickets without peer certificates, while keeping larger tickets in dynamic storage.
Changes:
- Separates static ticket and PSK buffer limits from the maximum accepted ticket size.
- Updates ticket allocation, copying, restoration, and encryption error handling.
- Adds certificate-carrying ticket resumption and oversized callback-output tests.
| File | Description |
|---|---|
wolfssl/internal.h |
Defines separate static and wire limits; shrinks session ticket storage. |
tests/api/test_tls13.h |
Registers the new ticket tests. |
tests/api/test_tls13.c |
Tests certificate-bearing ticket resumption and callback output limits. |
tests/api/test_tls_msgtype.c |
Updates PSK parsing tests to use the wire limit. |
tests/api/test_ssl_ext.c |
Updates a comment naming the static ticket limit. |
tests/api/test_session.c |
Adjusts dynamic-ticket thresholds and cache restoration expectations. |
src/tls13.c |
Keeps long ticket identities out of static identity buffers and PSK callbacks. |
src/tls.c |
Accepts PSK identities up to the separate wire limit. |
src/ssl_sess.c |
Updates ticket storage checks and cache allocation bookkeeping. |
src/ssl_api_ext.c |
Uses allocated capacity for ticket replacement and clears length on allocation failure. |
src/internal.c |
Sizes ticket creation buffers from certificate content and rejects invalid callback lengths. |
Review effort: Balanced
SetTicket() frees a dynamic ticket and points the session back at the static buffer before allocating the buffer for the new ticket. When that allocation failed, ticketLen kept the old ticket's length, which is longer than the static buffer, so wolfSSL_get_SessionTicket() or a session copy could read past it. Certificate-carrying tickets now use the dynamic buffer, which makes this path reachable for ordinary tickets. Test: a client holding a certificate-carrying ticket fails to allocate the buffer for a second one and is left without a ticket.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #11991
Scan targets checked: wolfssl-src, wolfssl-bugs
Not reviewed (no scan target covers them): tests/api/test_tls13.h
Coverage: 6 of 10 in-scope changed file(s) opened by the reviewer; not opened: src/tls.c, tests/api/test_ssl_ext.c, tests/api/test_tls13.c, tests/api/test_tls_msgtype.c
Fenrir result: Approved ✅
No new issues found in the changed files.
Advisory only — this automated result does not count as a GitHub approval.
Review tier: Lite

What and why
With
OPENSSL_ALLandKEEP_PEER_CERT(and withoutNO_CERT_IN_TICKET) a session ticket can carry the peer certificate, soSESSION_TICKET_LENincludesMAX_TICKET_PEER_CERT_SZ(2048 bytes). Every buffer sized from it was sized for the largest possible ticket:WOLFSSL_SESSIONhad a ~2.3 KBstaticTicket, including every row of the static session cache;MAX_PSK_ID_LENfollowed the same size, soArrayscarried two ~2.3 KB identity buffers andWOLFSSL_CTXone.Most tickets carry no certificate, so this PR sizes those buffers for a ticket without one. A longer ticket uses the existing dynamic ticket buffer (
ticket/ticketLenAlloc).WOLFSSL_SESSION.staticTicketisSESSION_TICKET_STATIC_LEN, the length of a ticket without a certificate. Builds that do not put the certificate in the ticket keep their sizes (SESSION_TICKET_STATIC_LEN == SESSION_TICKET_LENthere).CreateTicket()selects the peer certificate first and sizes the ticket from it. The encrypt callback getsWOLFSSL_TICKET_ENC_BASE_SZplus the certificate length, which isWOLFSSL_TICKET_ENC_SZminus the certificate space the ticket does not use. The ticket format and server-side decryption are unchanged.MAX_PSK_ID_LENdefaults toSESSION_TICKET_STATIC_LEN, as in builds withoutOPENSSL_ALL. The newMAX_PSK_ID_WIRE_LENlets a TLS 1.3 server still accept its own longer tickets as PSK identities. A longer identity is not copied intoArraysand is not offered to the PSK callbacks, on either the TLS 1.3 or the DTLS 1.3 stateless path (the bound is inFindPskSuite()).sizeof(staticTicket)in the session cache,wolfSSL_SESSION_dup(),d2iandwolfSSL_set_SessionTicket().Visible change: MAX_PSK_ID_LEN default
With
OPENSSL_ALLand certificates in tickets,MAX_PSK_ID_LENused to be the full ticket size (about 2.3 KB). It is now the size of a ticket without a certificate (a few hundred bytes), the same limit builds withoutOPENSSL_ALLalready have. As a result:Define
MAX_PSK_ID_LENto keep a larger limit.Fixes found along the way
CreateTicket(): when the encrypt callback reported too long an output,BAD_TICKET_KEY_CB_SZwas set but the code did not jump to the error path, soticketLencould point past the end of the buffer. It now fails cleanly.ticketLenis also cleared until the new ticket is complete.SetTicket()(client receiving a NewSessionTicket): when the allocation for a longer ticket failed after the old dynamic ticket was freed,ticketLenkept the old length on the static buffer. It is now cleared. Certificate-carrying tickets make this path reachable for ordinary tickets.GetSessionFromCache(): it recorded the 512-byte scratch size asticketLenAllocfor a buffer it had allocated withticketLenbytes. It now records the size it allocated.wolfSSL_set_SessionTicket(): it decided whether to reallocate from the previousticketLenrather than fromticketLenAlloc, and leftticketLenset when the allocation failed.SessionSanityPointerSet()): an oversizeticketLenwas clamped to the static size, which would send a truncated ticket of stale bytes. The ticket is now dropped. Trade-off: a saved cache only holds the static buffer, so withOPENSSL_ALLa cert-carrying ticket is no longer kept across a session cache save and restore (file or memory). The session itself is kept.Measured savings
Cortex-M33 (STM32U585),
OPENSSL_ALL, before -> after:ArraysWOLFSSL_SESSIONWOLFSSL_CTXHandshake times did not change. Builds without
OPENSSL_ALL(or withNO_CERT_IN_TICKET) have the same sizes as before.Tests
New in
tests/api/test_tls13.c:test_tls13_ticket_peer_cert_resume: mutual auth with the server session cache off, over TLS 1.3 and TLS 1.2. It checks that both sides hold the cert-carrying ticket in the dynamic buffer, then resumes from the session, from awolfSSL_SESSION_dup()copy and from ani2d/d2icopy, and compares the resumed peer certificate with the client certificate.test_tls13_ticket_enc_too_long: an encrypt callback that reports one byte more than it was given. It expectsBAD_TICKET_KEY_CB_SZandticketLen == 0, and it fails without theCreateTicket()fix.test_tls13_ticket_set_alloc_fail: a client holding a cert-carrying ticket fails to allocate the buffer for a second one. It expects the session to be left without a ticket, and it fails without theSetTicket()fix.Existing tests that hard-coded
SESSION_TICKET_LENorMAX_PSK_ID_LENuse the new limits.make checkpasses on Linux/gcc for--enable-opensslall,--enable-all,--enable-opensslextraand--enable-session-ticket, and on macOS/clang for--enable-opensslalland--enable-all. Before the rebase onto current master it also passed under ASan+UBSan with--enable-all, with a smallMAX_PSK_ID_LEN=128, and with--enable-savesession+WOLFSSL_SESSION_INDEX.Related
This PR is based on current master and does not depend on any other PR. The peer certificate pointer change (#11990,
WOLFSSL_DYNAMIC_PEER_CERT) is a separate PR, and the two touch the same line. Whichever lands second changesCreateTicket()'sssl->peerCert.derCerttoGetPeerCert(ssl)->derCert.