Skip to content

wolfSSL accepts trailing data in TLS 1.3 pre_shared_key extension #11134

Description

@LiD0209

wolfSSL accepts trailing data in TLS 1.3 pre_shared_key extension

Summary

  • Root cause: missing end-of-structure validation in the ClientHello pre_shared_key extension parser

RFC 9846 requires a receiver that processes an implemented, non-ignored extension to abort the handshake with decode_error if bytes remain after parsing the extension-specific structure. wolfSSL parses the TLS 1.3 ClientHello pre_shared_key extension but accepts an extension_data value where a complete OfferedPsks structure is followed by one extra byte.

The issue is confirmed by a fresh runtime rerun on 2026-08-10. Both the direct PSK parser probe and the outer TLSX_Parse() extension-dispatch probe return success for the malformed input.

Standard Requirement

Official standard links:

RFC 9846 Section 4.3 defines the general extension rule: extension payloads are normally TLS presentation-language structures, trailing data is forbidden unless the specification says otherwise, and receivers processing such an extension must abort with decode_error if bytes remain after the structure has been parsed.

RFC 9846 Section 4.3.11 defines the pre_shared_key extension_data as a PreSharedKeyExtension. For a ClientHello, that selected structure is OfferedPsks:

struct {
    PskIdentity identities<7..2^16-1>;
    PskBinderEntry binders<33..2^16-1>;
} OfferedPsks;

struct {
    select (Handshake.msg_type) {
        case client_hello: OfferedPsks;
        case server_hello: uint16 selected_identity;
    };
} PreSharedKeyExtension;

The separate RFC 9846 rule that pre_shared_key must be the last extension in ClientHello is an extension-list ordering rule. It does not authorize extra bytes inside the pre_shared_key extension_data after the OfferedPsks structure.

Implementation Evidence

Relevant implementation files:

  • implementions/wolfssl-master/src/tls.c
  • implementions/wolfssl-master/src/tls13.c

The TLS 1.3 ClientHello path calls the generic extension parser:

/* src/tls13.c:7795 */
if ((ret = TLSX_Parse(ssl, input + args->idx, totalExtSz, client_hello,
                                                        ssl->clSuites))) {
    goto exit_dch;
}

The generic parser recognizes TLSX_PRE_SHARED_KEY and dispatches it to the PSK parser:

/* src/tls.c:18863 */
case TLSX_PRE_SHARED_KEY:
    WOLFSSL_MSG("Pre-Shared Key extension received");

    if (!IsAtLeastTLSv1_3(ssl->version))
        break;

    if (msgType != client_hello &&
        msgType != server_hello) {
        WOLFSSL_ERROR_VERBOSE(EXT_NOT_ALLOWED);
        return EXT_NOT_ALLOWED;
    }

    ret = PSK_PARSE(ssl, input + offset, size, msgType);
    pskDone = 1;
    break;

Before dispatch, TLSX_Parse() only checks that the declared extension TLV length fits within the containing extension block:

/* src/tls.c:18475 */
if (length - offset < size)
    return BUFFER_ERROR;

After the extension-specific parser returns, the outer parser advances by the declared extension size:

/* src/tls.c:19191 */
/* offset should be updated here! */
offset += size;

The ClientHello PSK parser consumes the identities vector, then checks only that the binder vector length fits in the remaining declared extension data:

/* src/tls.c:12459 */
if (idx + OPAQUE16_LEN > length)
    return BUFFER_E;
ato16(input + idx, &len);
idx += OPAQUE16_LEN;
if (len < MIN_PSK_BINDERS_LEN || length - idx < len)
    return BUFFER_E;

It then consumes exactly the declared binder vector contents and verifies only that the binder list matches the identity list:

/* src/tls.c:12467 */
while (list != NULL && len > 0) {
    list->binderLen = input[idx++];
    if (list->binderLen < WC_SHA256_DIGEST_SIZE ||
            list->binderLen > WC_MAX_DIGEST_SIZE)
        return BUFFER_E;
    if (len < OPAQUE8_LEN + list->binderLen)
        return BUFFER_E;

    XMEMCPY(list->binder, input + idx, list->binderLen);
    idx += (word16)list->binderLen;
    len -= OPAQUE8_LEN + (word16)list->binderLen;
    list = list->next;
}
if (list != NULL || len != 0)
    return BUFFER_E;

return 0;

There is no final idx == length check before success. Therefore bytes that remain after OfferedPsks are not rejected by the PSK parser. The outer parser then advances by the full declared extension length, silently skipping those bytes.

Runtime Evidence

Fresh rerun date: 2026-08-10.

Action: a direct parser probe called TLSX_PreSharedKey_Parse_ClientHello() with two payloads:

  • control: a complete ClientHello PreSharedKeyExtension containing one identity and one 32-byte binder;
  • malformed: the same complete OfferedPsks structure followed by one trailing byte 0xaa.

The direct parser probe was rerun from the workspace root with the wolfSSL audit build on PATH.

Observed result:

control_no_trailing_ret=0
trailing_extra_byte_ret=0
result=ISSUE_CONFIRMED
exit_code=0

Action: a stronger outer parser probe constructed complete TLS extension TLVs:

  • control TLV: extension type 0x0029, length 0x002c, exact OfferedPsks;
  • malformed TLV: extension type 0x0029, length 0x002d, the same OfferedPsks plus trailing byte 0xaa.

The outer parser probe was rerun from the workspace root with the same wolfSSL audit build on PATH.

Observed result:

outer_control_no_trailing_ret=0
outer_trailing_extra_byte_ret=0
result=ISSUE_CONFIRMED_OUTER_TLSX_PARSE
exit_code=0

The outer probe confirms the issue at the actual TLSX_Parse() extension-dispatch layer. This matters because it shows the malformed extension is accepted as a complete pre_shared_key extension TLV, not merely accepted by an isolated helper.

Compliance Decision

RFC 9846 requires a receiver processing an implemented extension to reject leftover bytes after the extension-specific structure. wolfSSL implements and processes the TLS 1.3 pre_shared_key extension, so the exception for unimplemented or configured-ignored extensions does not apply.

The implementation accepts a ClientHello pre_shared_key extension_data value in which OfferedPsks is complete but followed by an extra byte. Runtime evidence confirms both the direct parser and the outer TLSX_Parse() path return success for that malformed input. This is inconsistent with the required decode_error rejection.

Impact

A malformed TLS 1.3 ClientHello can include extra bytes after the pre_shared_key OfferedPsks structure and still pass wolfSSL extension parsing. Later PSK selection or binder verification may fail for unrelated reasons, but the protocol compliance issue is already present at extension processing: the receiver does not reject forbidden trailing data at the required point.

Fix Direction

Add an end-of-structure check in TLSX_PreSharedKey_Parse_ClientHello() after binder parsing succeeds:

if (idx != length)
    return BUFFER_E;

The propagated parse error should continue to map to a fatal decode_error alert in the TLS 1.3 handshake path. A regression test should cover both the direct PSK parser and the outer TLSX_Parse() path with one trailing byte after a valid ClientHello OfferedPsks value.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions