Skip to content

[Bug]: DTLS 1.3 ECH is never accepted; client fails with ECH_REQUIRED_E (-519) #12013

Open

Description

@HMBSbige

Contact Details

No response

Version

5.9.4

Description

With --enable-dtls13 --enable-ech, a DTLS 1.3 handshake between the example client and server never accepts ECH: the client aborts with ECH_REQUIRED_E (-519), with or without the server's HRR cookie. The same commands over TLS 1.3 work.

The DTLS code paths have several independent problems (links point to v5.9.4-stable):

  1. Client: the inner ClientHello and the AAD are taken at TLS offsets. In DTLS the ClientHello body follows a 13-byte record header and a 12-byte handshake header (tls13.c L4930-L4935), but the inner ClientHello is copied from output + RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ (L5253-L5257) and the same offset is passed to TLSX_FinalizeEch as the AAD (L5344-L5346). Both start 16 bytes early, so they include header bytes that RFC 9849 §5.1/§5.2 exclude, and the inner ClientHello loses the last 16 bytes before its extensions. The server cannot decrypt it ("ECH rejected").

  2. Server: the stateless HelloRetryRequest ignores ECH. --enable-dtls13 enables WOLFSSL_SEND_HRR_COOKIE, and DTLS 1.3 servers turn the cookie on by default (internal.c L2442-L2458), so the first ClientHello is answered by SendStatelessReplyDtls13 (dtls.c L668-L972). It never processes encrypted_client_hello, so the HRR carries no confirmation (RFC 9849 §7.2.1) and the client treats ECH as rejected (§6.1.4, tls13.c L6173-L6182).

  3. Server: ECH ClientHello parsing ignores legacy_cookie. For DTLS, RFC 9849 §5.1/§5.2 use the ClientHello of RFC 9147 §5.3, which has legacy_cookie after legacy_session_id. TLSX_ECH_CheckInnerPadding (tls.c L14511-L14520), TLSX_ECH_ExpandOuterExtensions (L14742-L14746) and TLSX_ECH_FindOuterExtension (L14575-L14581) parse it with the TLS layout. A correctly encoded inner ClientHello fails with BUFFER_ERROR (-328) right after "ECH accepted"; past that check, extensions referenced by ech_outer_extensions are not found (INVALID_PARAMETER).

  4. Server: no acceptance confirmation in DTLS. SendTls13ServerHello calls EchWriteAcceptance only in the TLS branch (tls13.c L8884-L8887); the DTLS branch only hashes the message (L8860-L8865). The server continues with ClientHelloInner while the client, finding no confirmation, continues with ClientHelloOuter, and the handshake stalls.

  5. The ECH transcript includes DTLS-only header fields. EchHashHelloInner (tls13.c L4061-L4075) and EchCalcAcceptance (L4149-L4172) hash the full 12-byte DTLS handshake header, including message_seq, fragment_offset and fragment_length, which RFC 9147 §5.2 excludes from the transcript (as Dtls13HashHandshake does, dtls13.c L529-L550). This goes unnoticed between two wolfSSL peers, but both the confirmation and the transcript after acceptance differ from those of a peer that follows RFC 9147. In the HRR case, messageHashHeader receives a 4-byte header (AddTls13HandShakeHeader special-cases message_hash), yet 12 bytes are hashed, 8 of them uninitialized: built with -ftrivial-auto-var-init=pattern, the hashed bytes are fe 00 00 30 fe fe fe fe fe fe fe fe.

With 1, 3 and 4 fixed locally, the handshake with -J n (no HRR cookie) completes with ECH accepted on both sides; with the default cookie it still fails because of 2.

Build: ./configure --enable-dtls13 --enable-ech --enable-debug; x86_64 Linux, GCC 16.2.1.

Reproduction steps

  1. ./configure --enable-dtls13 --enable-ech --enable-debug && make
  2. ./examples/server/server -u -v 4 -d --ech public.example, which prints ECH config (base64): <config>
  3. ./examples/client/client -u -v 4 -d -S secret.example --ech <config> fails with -519 (problem 2).
  4. Repeat with -J n added to the server. The client still fails with -519 and the server logs "ECH rejected" (problem 1).

-d on both sides lets the fallback handshake finish: the test certificate does not match these names, and after the rejection the client sends an empty Certificate (RFC 9849 §6.1.7).

Relevant log output

# Excerpts from the --enable-debug output
# Step 3, server
wolfSSL Entering DoClientHelloStateless
wolfSSL Entering DoClientHelloStateless
ECH rejected
# Step 3, client
HelloRetryRequest format
SendAlert: 121 ech_required
wolfSSL_connect error -519, ECH offered but rejected by server

# Step 4 (-J n), server
ECH rejected
# Step 4 (-J n), client
ECH rejected
SendAlert: 121 ech_required
wolfSSL_connect error -519, ECH offered but rejected by server

# Step 4 with 1 fixed locally, server
ECH accepted
SSL_accept error -328, malformed buffer input error
# client
Alert type: decode_error

# Step 4 with 1 and 3 fixed locally, server
ECH accepted
ECH: expanded ech_outer_extensions successfully
# client (the handshake then stalls)
ECH rejected

Activity

  1. sebastian-carpenter commented on Nov 5, 2026

    @sebastian-carpenter
    Contributor

    Hey HMBSbige,

    Thank you for the report. Currently, there has not been much work put into ECH with DTLS, so I'm not surprised it doesn't work. I'll add this to our feature request list for the time being.

    I'll put up a PR shortly to error/warn when a client or server attempts ECH over DTLS.

  2. linked a pull request that will close this issueerror on dtls with ech #12047on Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions