Contact Details
page.cal@gmail.com
Version
wolfSSL 5.9.1 (v5.9.1-stable-430-g426dc7bb7 — 430 commits past the v5.9.1 tag, current HEAD 426dc7b)
Description
issue-tmp — Three src/tls13.c hardening fixes
Bundles three independent defensive fixes against the same translation unit.
Each is small (1–10 lines), each has a concrete failure mode, and none of
them are covered by an existing PR or open issue at the time of writing
(see Upstream status below).
- Length guards in
Tls13_Exporter for outLen and labelLen.
- NULL-pointer guards in
wolfSSL_get_cipher_name_by_hash.
DoTls13CertificateRequest: defer linking the new CertReqCtx into
ssl->certReqCtx until after the message is fully validated, so a
malformed/short CertificateRequest cannot leave a half-validated
context in the active list.
The bug
1. Tls13_Exporter — unbounded length parameters
src/tls13.c:963-1049
int Tls13_Exporter(WOLFSSL* ssl, unsigned char *out, size_t outLen,
const char *label, size_t labelLen,
const unsigned char *context, size_t contextLen)
{
...
/* Sanity check contextLen to prevent truncation when cast to word32. */
if (contextLen > WOLFSSL_MAX_32BIT) {
return BAD_FUNC_ARG;
}
...
ret = Tls13HKDFExpandLabel(ssl, out, (word32)outLen, firstExpand, hashLen,
protocol, protocolLen, exporterLabel, EXPORTER_LABEL_SZ,
hashOut, hashLen, (int)hashType);
contextLen is sanity-checked. outLen and labelLen are not. Both are
size_t and both are unconditionally cast to word32 before being passed
into the HKDF layer:
- Line 1029:
(word32)labelLen
- Line 1044:
(word32)outLen
On a 64-bit host, a value > 0xFFFFFFFF silently truncates. The downstream
HKDF wire encoder (wc_Tls13_HKDF_Expand_Label_Alloc in
wolfcrypt/src/kdf.c:538-541) then writes:
data[idx++] = (byte)(okmLen >> 8); /* 2 bytes */
data[idx++] = (byte)okmLen;
data[idx++] = (byte)(protocolLen + labelLen); /* 1 byte */
So the API contract is in fact stricter than word32:
outLen must fit in two bytes (≤ 0xFFFF); the TLS 1.3 RFC 8446 §7.5
HkdfLabel encodes length as uint16.
protocolLen + labelLen must fit in one byte (≤ 0xFF); the protocol
label is tls13 (6 bytes), so labelLen must be ≤ 249.
A caller that passes a larger value gets either truncated output (the
HKDF _Alloc variant just truncates and produces wrong key material) or
BUFFER_E (the _ex variant rejects via MAX_TLS13_HKDF_LABEL_SZ,
which is 47 + WC_MAX_DIGEST_SIZE ≈ 111 bytes total). Either way the
caller cannot tell the difference between “HKDF rejected my buffer
sizes” and any other internal error, and on the truncating path the
caller silently gets the wrong key material.
The fix: add structural guards at the API surface, matching the
existing contextLen pattern.
2. wolfSSL_get_cipher_name_by_hash — missing NULL checks
src/tls13.c:14954-14977
const char* wolfSSL_get_cipher_name_by_hash(WOLFSSL* ssl, const char* hash)
{
const char* name = NULL;
byte mac = no_mac;
int i;
const Suites* suites = WOLFSSL_SUITES(ssl);
if (XSTRCMP(hash, "SHA256") == 0) {
...
This is a WOLFSSL_API function exposed in wolfssl/ssl.h:3230. It is
callable from any consumer of the public ABI. Two NULL paths:
WOLFSSL_SUITES(ssl) expands to
((ssl)->suites != NULL ? (ssl)->suites : (ssl)->ctx->suites).
If ssl == NULL, the macro dereferences NULL. If ssl->suites == NULL
and ssl->ctx == NULL, same story.
XSTRCMP(hash, "SHA256") dereferences hash; passing NULL from a
caller crashes.
Then if mac ends up set, the function dereferences suites->suiteSz and
indexes suites->suites[] without ever checking suites != NULL.
The fix: short-circuit return NULL on any of these inputs.
3. DoTls13CertificateRequest — CertReqCtx linked before validation
src/tls13.c:5936-6041
certReqCtx = (CertReqCtx*)XMALLOC(sizeof(CertReqCtx) +
(len == 0 ? 0 : len - 1), ssl->heap,
DYNAMIC_TYPE_TMP_BUFFER);
if (certReqCtx == NULL)
return MEMORY_E;
certReqCtx->next = ssl->certReqCtx;
certReqCtx->len = len;
XMEMCPY(&certReqCtx->ctx, input + *inOutIdx, len);
ssl->certReqCtx = certReqCtx; /* <-- linked here */
#endif
*inOutIdx += len;
if ((*inOutIdx - begin) + OPAQUE16_LEN > size)
return BUFFER_ERROR; /* <-- four error paths follow,
CertReqCtx already in list */
ato16(input + *inOutIdx, &len);
*inOutIdx += OPAQUE16_LEN;
if ((*inOutIdx - begin) + len > size)
return BUFFER_ERROR;
if (len == 0)
return INVALID_PARAMETER;
if ((ret = TLSX_Parse(ssl, input + *inOutIdx, len, certificate_request,
&peerSuites))) {
return ret;
}
The newly allocated CertReqCtx is appended to ssl->certReqCtx before
the rest of the message is validated. After that point there are four
distinct error paths (three BUFFER_ERROR, one INVALID_PARAMETER,
plus whatever TLSX_Parse returns). Each one returns up the stack with
a half-validated context already wired into the active list.
Two consequences:
- Eventual cleanup masks the problem. The chain is freed when
SSL_ResourceFree runs (src/internal.c:9030-9034), so on a failed
handshake that immediately tears down the SSL there is no observable
leak. But under post-handshake auth a server-supplied malformed
request that fails after linking still leaves an entry in the list
for the next legitimate SendTls13Certificate call to consume
(src/tls13.c:9298-9300), so the client may then reply to the
attacker’s unvalidated context bytes rather than failing cleanly.
- Defensive correctness. The function’s contract is: on a
0
return, the message is accepted and the context is registered.
Any other return code means the message was rejected. The current
code violates that contract for every non-zero exit reached after
line 5979.
The fix: defer the ssl->certReqCtx = certReqCtx; linkage until after
all validation and TLSX_Parse succeed. Free the locally-held block on
every error path between allocation and linkage.
Upstream status
Searched wolfSSL/wolfssl PRs and issues (open and closed) on
2026-04-25. No PR or issue is currently open or merged that addresses
any of these three concerns.
| Concern |
Search terms |
Result |
| Exporter length guards |
Tls13_Exporter, TLS 1.3 exporter, outLen labelLen |
None |
wolfSSL_get_cipher_name_by_hash NULL |
wolfSSL_get_cipher_name_by_hash, get_cipher_name_by_hash null |
None |
DoTls13CertificateRequest CertReqCtx leak |
DoTls13CertificateRequest, CertReqCtx |
Adjacent only |
Adjacent prior work on the same function:
Root cause
All three follow the same shape: an API function trusts its caller for
input shape that the caller cannot guarantee.
- Exporter: the public
wolfSSL_export_keying_material (src/ssl.c:5989)
takes size_t parameters, the underlying HKDF wire encoder takes
uint8/uint16 lengths, and there is no checked narrowing in the
middle layer. The contextLen check at tls13.c:1035 was added; the
matching outLen/labelLen checks were not.
wolfSSL_get_cipher_name_by_hash is a thin lookup function written
under the assumption that it is only called mid-handshake from
internal PSK code with a non-NULL ssl. Public ABI exposure breaks
that assumption.
DoTls13CertificateRequest linkage was placed early so the failure
path could rely on SSL_ResourceFree for cleanup — but that puts
unvalidated state into the active list, which is observable while
the SSL is still alive.
Proposed fix
Three small edits in src/tls13.c. Total: ~30 lines added, ~1 line moved.
1. Tls13_Exporter — add structural guards
/* Sanity check contextLen to prevent truncation when cast to word32. */
if (contextLen > WOLFSSL_MAX_32BIT) {
return BAD_FUNC_ARG;
}
/* outLen is encoded as uint16 in HkdfLabel (RFC 8446 §7.5). */
if (outLen > WOLFSSL_MAX_16BIT) {
return BAD_FUNC_ARG;
}
/* protocolLen + labelLen is encoded in a single byte; protocol is
* "tls13 " (6 bytes), so labelLen must leave room. */
if (labelLen > (size_t)(0xFF - protocolLen)) {
return BAD_FUNC_ARG;
}
2. wolfSSL_get_cipher_name_by_hash — early NULL return
const char* wolfSSL_get_cipher_name_by_hash(WOLFSSL* ssl, const char* hash)
{
const char* name = NULL;
byte mac = no_mac;
int i;
const Suites* suites;
if (ssl == NULL || hash == NULL)
return NULL;
suites = WOLFSSL_SUITES(ssl);
if (suites == NULL)
return NULL;
...
3. DoTls13CertificateRequest — defer linkage
Allocate the context locally, perform every validation step against the
local pointer, and only assign to ssl->certReqCtx immediately before
the success exit. On any error path between allocation and linkage,
XFREE the local pointer.
Caveats and risks
- The exporter guards reject inputs that would previously have either
silently truncated or hit BUFFER_E deeper in HKDF. No legitimate
caller passes ≥ 64 KiB of derived key material out of one exporter
call — the TLS 1.3 spec does not even encode that — so behavioral
impact on real callers is nil. Bad callers now get a deterministic
BAD_FUNC_ARG instead of an HKDF-internal error code.
- The NULL guard on
wolfSSL_get_cipher_name_by_hash cannot regress
any legitimate caller; a NULL ssl already crashed.
- The deferred-linkage edit changes the moment
ssl->certReqCtx is
observable to other code on the same SSL object. Callers that read
ssl->certReqCtx mid-DoTls13CertificateRequest would behave
differently — but no such caller exists; DoTls13CertificateRequest
is single-threaded with respect to its own SSL.
Tests to add
- A unit test that calls
Tls13_Exporter with outLen = 0x10000 and
asserts BAD_FUNC_ARG.
- A unit test that calls
Tls13_Exporter with labelLen = 250 and
asserts BAD_FUNC_ARG.
- A unit test that calls
wolfSSL_get_cipher_name_by_hash(NULL, "SHA256")
and asserts the function returns NULL without crashing, and the
same for a NULL hash.
- A wire-level test for
DoTls13CertificateRequest that injects a
truncated post-allocation message and asserts (a) the function
returns an error and (b) ssl->certReqCtx is unchanged from before
the call. This is harder to fixture in the existing test harness;
the C reproducer in this directory exercises the same condition by
calling the function directly.
Files touched by the proposed patch
| File |
Lines |
Severity |
src/tls13.c |
+27 / −1 |
Defensive (no known exploit, observable misbehavior on malformed/oversized inputs) |
Appendix — end-to-end verification
The test harness in test.sh performs the following passes against an
unpatched and a patched tree:
- Build wolfSSL with
--enable-harden-tls --enable-postauth --enable-keylog-export --enable-keying-material --enable-psk.
- Compile
issue-tmp-test.c against the resulting library.
- Run the reproducer:
- Case A:
Tls13_Exporter-style overflow (calls
wolfSSL_export_keying_material with outLen = 0x10000).
BEFORE: returns WOLFSSL_SUCCESS (silent truncation in HKDF wire
buffer) or unspecified error. AFTER: returns WOLFSSL_FAILURE
consistently.
- Case B:
wolfSSL_get_cipher_name_by_hash(NULL, "SHA256").
BEFORE: SIGSEGV. AFTER: returns NULL.
- Case C: simulate a malformed
DoTls13CertificateRequest
directly. BEFORE: ssl->certReqCtx non-NULL after error.
AFTER: ssl->certReqCtx remains NULL after error.
PASS gate: all three cases must show the post-patch behavior on the
patched tree.
Running the test harness
cd wolfssl-issues/issue-tmp
./test.sh
The script resets src/tls13.c to the clean tree before BEFORE,
applies issue-tmp.patch for AFTER, restores the tree on every
exit path, and prints a four-line colored summary.
The rest of the files mentioned are at https://github.com/CpsourceInc/wolfssl-issues.git
Reproduction steps
No response
Relevant log output
Contact Details
page.cal@gmail.com
Version
wolfSSL 5.9.1 (v5.9.1-stable-430-g426dc7bb7 — 430 commits past the v5.9.1 tag, current HEAD 426dc7b)
Description
issue-tmp — Three
src/tls13.chardening fixesBundles three independent defensive fixes against the same translation unit.
Each is small (1–10 lines), each has a concrete failure mode, and none of
them are covered by an existing PR or open issue at the time of writing
(see Upstream status below).
Tls13_ExporterforoutLenandlabelLen.wolfSSL_get_cipher_name_by_hash.DoTls13CertificateRequest: defer linking the newCertReqCtxintossl->certReqCtxuntil after the message is fully validated, so amalformed/short
CertificateRequestcannot leave a half-validatedcontext in the active list.
The bug
1.
Tls13_Exporter— unbounded length parameterssrc/tls13.c:963-1049contextLenis sanity-checked.outLenandlabelLenare not. Both aresize_tand both are unconditionally cast toword32before being passedinto the HKDF layer:
(word32)labelLen(word32)outLenOn a 64-bit host, a value
> 0xFFFFFFFFsilently truncates. The downstreamHKDF wire encoder (
wc_Tls13_HKDF_Expand_Label_Allocinwolfcrypt/src/kdf.c:538-541) then writes:So the API contract is in fact stricter than
word32:outLenmust fit in two bytes (≤0xFFFF); the TLS 1.3 RFC 8446 §7.5HkdfLabel encodes
lengthasuint16.protocolLen + labelLenmust fit in one byte (≤0xFF); the protocollabel is
tls13(6 bytes), solabelLenmust be ≤ 249.A caller that passes a larger value gets either truncated output (the
HKDF
_Allocvariant just truncates and produces wrong key material) orBUFFER_E(the_exvariant rejects viaMAX_TLS13_HKDF_LABEL_SZ,which is 47 + WC_MAX_DIGEST_SIZE ≈ 111 bytes total). Either way the
caller cannot tell the difference between “HKDF rejected my buffer
sizes” and any other internal error, and on the truncating path the
caller silently gets the wrong key material.
The fix: add structural guards at the API surface, matching the
existing
contextLenpattern.2.
wolfSSL_get_cipher_name_by_hash— missing NULL checkssrc/tls13.c:14954-14977This is a
WOLFSSL_APIfunction exposed inwolfssl/ssl.h:3230. It iscallable from any consumer of the public ABI. Two NULL paths:
WOLFSSL_SUITES(ssl)expands to((ssl)->suites != NULL ? (ssl)->suites : (ssl)->ctx->suites).If
ssl == NULL, the macro dereferences NULL. Ifssl->suites == NULLand
ssl->ctx == NULL, same story.XSTRCMP(hash, "SHA256")dereferenceshash; passingNULLfrom acaller crashes.
Then if
macends up set, the function dereferencessuites->suiteSzandindexes
suites->suites[]without ever checkingsuites != NULL.The fix: short-circuit return
NULLon any of these inputs.3.
DoTls13CertificateRequest—CertReqCtxlinked before validationsrc/tls13.c:5936-6041The newly allocated
CertReqCtxis appended tossl->certReqCtxbeforethe rest of the message is validated. After that point there are four
distinct error paths (three
BUFFER_ERROR, oneINVALID_PARAMETER,plus whatever
TLSX_Parsereturns). Each one returns up the stack witha half-validated context already wired into the active list.
Two consequences:
SSL_ResourceFreeruns (src/internal.c:9030-9034), so on a failedhandshake that immediately tears down the SSL there is no observable
leak. But under post-handshake auth a server-supplied malformed
request that fails after linking still leaves an entry in the list
for the next legitimate
SendTls13Certificatecall to consume(
src/tls13.c:9298-9300), so the client may then reply to theattacker’s unvalidated context bytes rather than failing cleanly.
0return, the message is accepted and the context is registered.
Any other return code means the message was rejected. The current
code violates that contract for every non-zero exit reached after
line 5979.
The fix: defer the
ssl->certReqCtx = certReqCtx;linkage until afterall validation and
TLSX_Parsesucceed. Free the locally-held block onevery error path between allocation and linkage.
Upstream status
Searched
wolfSSL/wolfsslPRs and issues (open and closed) on2026-04-25. No PR or issue is currently open or merged that addresses
any of these three concerns.
Tls13_Exporter,TLS 1.3 exporter,outLen labelLenwolfSSL_get_cipher_name_by_hashNULLwolfSSL_get_cipher_name_by_hash,get_cipher_name_by_hash nullDoTls13CertificateRequestCertReqCtx leakDoTls13CertificateRequest,CertReqCtxAdjacent prior work on the same function:
Set upper bound on post-auth cert reqs. Capsthe number of post-handshake requests via
certReqCtx->ctx == 255in
wolfSSL_request_certificate. Orthogonal to the validation/leakconcern; Set upper bound on post-auth cert reqs #9908 is server-side rate-limit, this is client-side
link-before-validate.
DoTls13CertificateRequest: call CertSetupCbWrapper only once. Removes a duplicate callbackinvocation. Touches the same function but a different fault.
flagged
sizeof(CertReqCtx) + len - 1undersized allocation in thissame function but not the link-before-validate path.
3 critiques
wolfSSL_request_certificate’s predictablecertificate_request_context(server side); does not touchDoTls13CertificateRequest.Root cause
All three follow the same shape: an API function trusts its caller for
input shape that the caller cannot guarantee.
wolfSSL_export_keying_material(src/ssl.c:5989)takes
size_tparameters, the underlying HKDF wire encoder takesuint8/uint16lengths, and there is no checked narrowing in themiddle layer. The contextLen check at
tls13.c:1035was added; thematching outLen/labelLen checks were not.
wolfSSL_get_cipher_name_by_hashis a thin lookup function writtenunder the assumption that it is only called mid-handshake from
internal PSK code with a non-NULL ssl. Public ABI exposure breaks
that assumption.
DoTls13CertificateRequestlinkage was placed early so the failurepath could rely on
SSL_ResourceFreefor cleanup — but that putsunvalidated state into the active list, which is observable while
the SSL is still alive.
Proposed fix
Three small edits in
src/tls13.c. Total: ~30 lines added, ~1 line moved.1. Tls13_Exporter — add structural guards
2. wolfSSL_get_cipher_name_by_hash — early NULL return
3. DoTls13CertificateRequest — defer linkage
Allocate the context locally, perform every validation step against the
local pointer, and only assign to
ssl->certReqCtximmediately beforethe success exit. On any error path between allocation and linkage,
XFREEthe local pointer.Caveats and risks
silently truncated or hit
BUFFER_Edeeper in HKDF. No legitimatecaller passes ≥ 64 KiB of derived key material out of one exporter
call — the TLS 1.3 spec does not even encode that — so behavioral
impact on real callers is nil. Bad callers now get a deterministic
BAD_FUNC_ARGinstead of an HKDF-internal error code.wolfSSL_get_cipher_name_by_hashcannot regressany legitimate caller; a NULL ssl already crashed.
ssl->certReqCtxisobservable to other code on the same SSL object. Callers that read
ssl->certReqCtxmid-DoTls13CertificateRequestwould behavedifferently — but no such caller exists;
DoTls13CertificateRequestis single-threaded with respect to its own SSL.
Tests to add
Tls13_ExporterwithoutLen = 0x10000andasserts
BAD_FUNC_ARG.Tls13_ExporterwithlabelLen = 250andasserts
BAD_FUNC_ARG.wolfSSL_get_cipher_name_by_hash(NULL, "SHA256")and asserts the function returns
NULLwithout crashing, and thesame for a NULL
hash.DoTls13CertificateRequestthat injects atruncated post-allocation message and asserts (a) the function
returns an error and (b)
ssl->certReqCtxis unchanged from beforethe call. This is harder to fixture in the existing test harness;
the C reproducer in this directory exercises the same condition by
calling the function directly.
Files touched by the proposed patch
src/tls13.cAppendix — end-to-end verification
The test harness in
test.shperforms the following passes against anunpatched and a patched tree:
--enable-harden-tls --enable-postauth --enable-keylog-export --enable-keying-material --enable-psk.issue-tmp-test.cagainst the resulting library.Tls13_Exporter-style overflow (callswolfSSL_export_keying_materialwithoutLen = 0x10000).BEFORE: returns
WOLFSSL_SUCCESS(silent truncation in HKDF wirebuffer) or unspecified error. AFTER: returns
WOLFSSL_FAILUREconsistently.
wolfSSL_get_cipher_name_by_hash(NULL, "SHA256").BEFORE: SIGSEGV. AFTER: returns NULL.
DoTls13CertificateRequestdirectly. BEFORE:
ssl->certReqCtxnon-NULL after error.AFTER:
ssl->certReqCtxremains NULL after error.PASS gate: all three cases must show the post-patch behavior on the
patched tree.
Running the test harness
cd wolfssl-issues/issue-tmp ./test.shThe script resets
src/tls13.cto the clean tree before BEFORE,applies
issue-tmp.patchfor AFTER, restores the tree on everyexit path, and prints a four-line colored summary.
The rest of the files mentioned are at https://github.com/CpsourceInc/wolfssl-issues.git
Reproduction steps
No response
Relevant log output