From e3b291589d6d2cfe57306e9efe1e4e8c67c6511c Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Mon, 20 Apr 2026 15:22:33 -0600 Subject: [PATCH 1/3] TLS ECH outerExtensions (client-side) --- src/tls.c | 167 ++++++++++++++++++++++++++++++++++++++++++--- src/tls13.c | 135 ++++++++++++++++++++++++------------ wolfssl/internal.h | 1 + 3 files changed, 253 insertions(+), 50 deletions(-) diff --git a/src/tls.c b/src/tls.c index 347761b263..5b3812f2b4 100644 --- a/src/tls.c +++ b/src/tls.c @@ -14105,6 +14105,9 @@ static int TLSX_ECH_ExpandOuterExtensions(WOLFSSL* ssl, WOLFSSL_ECH* ech, sessionIdLen, copyLen); } else { + innerExtIdx = headerSz + innerExtIdx - OPAQUE16_LEN - + sessionIdLen + ssl->session->sessionIDSz; + copyLen = echOuterExtIdx - OPAQUE16_LEN - RAN_LEN - OPAQUE8_LEN - sessionIdLen; XMEMCPY(newInnerChRef, innerCh + OPAQUE16_LEN + RAN_LEN + OPAQUE8_LEN + @@ -14113,7 +14116,7 @@ static int TLSX_ECH_ExpandOuterExtensions(WOLFSSL* ssl, WOLFSSL_ECH* ech, /* update extensions length in the new ClientHello */ c16toa(innerExtLen - echOuterExtLen + (word16)extraSize, - newInnerChRef - OPAQUE16_LEN); + newInnerCh + innerExtIdx); ret = TLSX_ECH_CopyOuterExtensions(outerCh, outerChLen, &newInnerChRef, &newInnerChLen, numOuterRefs, outerRefTypes); @@ -16326,6 +16329,105 @@ static int TLSX_EchRestoreSNI(WOLFSSL* ssl, char* serverName, return ret; } +/* Returns 1 if the extension may be encoded into ech_outer_extensions, + * 0 otherwise */ +static int TLSX_ECH_IsEncodeable(word16 type) +{ + switch (type) { + case TLSX_SERVER_NAME: + case TLSX_ECH: + case TLSX_APPLICATION_LAYER_PROTOCOL: +#if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK) + case TLSX_PRE_SHARED_KEY: +#endif +#ifdef WOLFSSL_EARLY_DATA + case TLSX_EARLY_DATA: +#endif + return 0; + default: + return 1; + } +} + +/* find extensions that can be encoded into ech_outer_extensions. + * If output is non-NULL, then write the encoded form. + * + * Layout of OuterExtensions (RFC 9849, S5.1): + * 2-byte extension_type + 2-byte extension_data length + + * 1-byte list length + 2*count bytes of extension types + */ +static int TLSX_ECH_BuildOuterExtensions(WOLFSSL* ssl, const byte* semaphore, + byte msgType, byte* output, word16* pOffset, word16* outCount, + byte* encodeMask) +{ + TLSX* list; + TLSX* extension; + byte* typesStart = NULL; + int listIdx; + word16 count = 0; + byte isRequest = (msgType == client_hello || + msgType == certificate_request); + byte seen[SEMAPHORE_SIZE]; + + /* backup semaphore so it can be aliased by encodeMask */ + XMEMCPY(seen, semaphore, SEMAPHORE_SIZE); + + if (output != NULL && pOffset != NULL) { + typesStart = output + *pOffset + + HELLO_EXT_TYPE_SZ + OPAQUE16_LEN + OPAQUE8_LEN; + } + + for (listIdx = 0; listIdx < 2; listIdx++) { + list = (listIdx == 0) ? ssl->extensions : + (ssl->ctx != NULL ? ssl->ctx->extensions : NULL); + for (extension = list; extension != NULL; extension = extension->next) { + word16 type = (word16)extension->type; + word16 semIdx = TLSX_ToSemaphore(type); + + /* OuterExtensions is <2..254>, so reference at most 127 types */ + if (count >= 127) { + WOLFSSL_MSG("ECH: cannot encode more than 127 extensions"); + break; + } + + if (!isRequest && !extension->resp) + continue; + if (!IS_OFF(seen, semIdx)) + continue; + TURN_ON(seen, semIdx); + if (type == TLSX_ECH || !TLSX_ECH_IsEncodeable(type)) + continue; + + if (typesStart != NULL) + c16toa(type, typesStart + count * OPAQUE16_LEN); + count++; + TURN_ON(encodeMask, semIdx); + } + } + + if (count > 0 && pOffset != NULL) { + word16 listLen = (word16)(OPAQUE16_LEN * count); + word16 blockSz = (word16)(HELLO_EXT_TYPE_SZ + OPAQUE16_LEN + + OPAQUE8_LEN + listLen); + if ((word32)*pOffset + blockSz > WOLFSSL_MAX_16BIT) { + WOLFSSL_MSG("ECH OuterExtensions overflows extensions length"); + return BUFFER_E; + } + if (output != NULL) { + byte* hdr = output + *pOffset; + c16toa(TLSXT_ECH_OUTER_EXTENSIONS, hdr); + c16toa((word16)(OPAQUE8_LEN + listLen), hdr + OPAQUE16_LEN); + hdr[OPAQUE16_LEN + OPAQUE16_LEN] = (byte)listLen; + } + + /* accumulate offset even if nothing is written */ + *pOffset += blockSz; + } + + *outCount = count; + return 0; +} + /* because the size of ech depends on the size of other extensions we need to * get the size with ech special and process ech last, return status */ static int TLSX_GetSizeWithEch(WOLFSSL* ssl, byte* semaphore, byte msgType, @@ -16335,18 +16437,32 @@ static int TLSX_GetSizeWithEch(WOLFSSL* ssl, byte* semaphore, byte msgType, TLSX* echX = NULL; TLSX* serverNameX = NULL; TLSX** extensions = NULL; + WOLFSSL_ECH* ech = NULL; + word16 count = 0; WC_DECLARE_VAR(serverName, char, WOLFSSL_HOST_NAME_MAX, 0); WC_ALLOC_VAR_EX(serverName, char, WOLFSSL_HOST_NAME_MAX, NULL, DYNAMIC_TYPE_TMP_BUFFER, return MEMORY_E); + r = TLSX_EchChangeSNI(ssl, &echX, serverName, &serverNameX, &extensions); + + if (echX != NULL) + ech = (WOLFSSL_ECH*)echX->data; + /* If ECH won't be written exclude it from the size calculation */ - if (r == 0 && echX != NULL && - !ssl->options.echAccepted && - ((WOLFSSL_ECH*)echX->data)->innerCount != 0) { + if (r == 0 !ssl->options.echAccepted && ech != NULL && + ech->innerCount != 0) { TURN_ON(semaphore, TLSX_ToSemaphore(echX->type)); } - if (r == 0 && ssl->extensions) + + /* if encoding, then count encoded form of inner ClientHello. + * `semaphore` is in/out so encodable extensions will later be ignored */ + if (r == 0 && ech != NULL && ech->type == ECH_TYPE_INNER && + ech->writeEncoded) { + ret = TLSX_ECH_BuildOuterExtensions(ssl, semaphore, msgType, + NULL, pLength, &count, semaphore); + } + if (r == 0 && ret == 0 && ssl->extensions) ret = TLSX_GetSize(ssl->extensions, semaphore, msgType, pLength); if (r == 0 && ret == 0 && ssl->ctx && ssl->ctx->extensions) ret = TLSX_GetSize(ssl->ctx->extensions, semaphore, msgType, pLength); @@ -16490,27 +16606,62 @@ static int TLSX_WriteWithEch(WOLFSSL* ssl, byte* output, byte* semaphore, TLSX* echX = NULL; TLSX* serverNameX = NULL; TLSX** extensions = NULL; + WOLFSSL_ECH* ech = NULL; WC_DECLARE_VAR(serverName, char, WOLFSSL_HOST_NAME_MAX, 0); WC_ALLOC_VAR_EX(serverName, char, WOLFSSL_HOST_NAME_MAX, NULL, DYNAMIC_TYPE_TMP_BUFFER, return MEMORY_E); r = TLSX_EchChangeSNI(ssl, &echX, serverName, &serverNameX, &extensions); ret = r; - if (ret == 0 && echX != NULL) + if (ret == 0 && echX != NULL) { + ech = (WOLFSSL_ECH*)echX->data; /* turn ech on so it doesn't write, then write it last */ TURN_ON(semaphore, TLSX_ToSemaphore(echX->type)); + } + /* for ECH inner, print the encodable block first, then the non-encodables. + * This allows the same transcript to be produced on either side + * (the transcript is over the expanded form). */ + if (ret == 0 && ech != NULL && ech->type == ECH_TYPE_INNER) { + byte encodeMask[SEMAPHORE_SIZE]; + byte* mask = ech->writeEncoded ? semaphore : encodeMask; + word16 count = 0; + int i; + + XMEMSET(encodeMask, 0, SEMAPHORE_SIZE); + + ret = TLSX_ECH_BuildOuterExtensions(ssl, semaphore, msgType, + ech->writeEncoded ? output : NULL, + ech->writeEncoded ? pOffset : NULL, + &count, mask); + if (ret == 0 && count >= 1 && !ech->writeEncoded) { + /* expanded: print encodable block normally */ + for (i = 0; i < SEMAPHORE_SIZE; i++) { + semaphore[i] |= encodeMask[i]; + encodeMask[i] = (byte)~encodeMask[i]; + } + if (ssl->extensions) { + ret = TLSX_Write(ssl->extensions, output + *pOffset, + encodeMask, msgType, pOffset); + } + if (ret == 0 && ssl->ctx && ssl->ctx->extensions) { + ret = TLSX_Write(ssl->ctx->extensions, output + *pOffset, + encodeMask, msgType, pOffset); + } + } + } + + /* print non-encodable block */ if (ret == 0 && ssl->extensions) { ret = TLSX_Write(ssl->extensions, output + *pOffset, semaphore, msgType, pOffset); } - if (ret == 0 && ssl->ctx && ssl->ctx->extensions) { ret = TLSX_Write(ssl->ctx->extensions, output + *pOffset, semaphore, msgType, pOffset); } - /* only write if have a shot at acceptance */ + /* only write ECH if have a shot at acceptance */ if (ret == 0 && echX != NULL && (ssl->options.echAccepted || ((WOLFSSL_ECH*)echX->data)->innerCount == 0)) { diff --git a/src/tls13.c b/src/tls13.c index 73ed518374..8681feb34e 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -3836,6 +3836,7 @@ int EchConfigGetSupportedCipherSuite(WOLFSSL_EchConfig* config) } /* Hash the inner client hello, initializing the hsHashesEch field if needed. + * This should receive the client hello without outer_extensions 'encoding' * * ssl SSL/TLS object. * ech ECH object. @@ -3863,11 +3864,6 @@ static int EchHashHelloInner(WOLFSSL* ssl, WOLFSSL_ECH* ech) #endif realSz = ech->innerClientHelloLen; -#ifndef NO_WOLFSSL_CLIENT - if (ssl->options.side == WOLFSSL_CLIENT_END) { - realSz -= ech->paddingLen + ech->hpke->Nt; - } -#endif tmpHashes = ssl->hsHashes; @@ -3876,7 +3872,6 @@ static int EchHashHelloInner(WOLFSSL* ssl, WOLFSSL_ECH* ech) ret = InitHandshakeHashes(ssl); if (ret == 0) { ssl->hsHashesEch = ssl->hsHashes; - ech->innerCount = 1; } } @@ -4582,6 +4577,7 @@ typedef struct Sch13Args { #if defined(HAVE_ECH) int clientRandomOffset; int preXLength; + word32 expandedInnerLen; WOLFSSL_ECH* ech; #endif } Sch13Args; @@ -4783,24 +4779,49 @@ int SendTls13ClientHello(WOLFSSL* ssl) /* only prepare if we have a chance at acceptance */ if (ssl->options.echAccepted || args->ech->innerCount == 0) { + word32 encodedLen; + byte downgrade; + + /* ensure that a version less than TLS1.3 is never offered */ + downgrade = ssl->options.downgrade; + ssl->options.downgrade = 0; + /* set the type to inner */ args->ech->type = ECH_TYPE_INNER; args->preXLength = (int)args->length; - /* get size for inner */ - ret = TLSX_GetRequestSize(ssl, client_hello, &args->length); + /* get encoded inner size */ + args->ech->writeEncoded = 1; + encodedLen = args->length; + ret = TLSX_GetRequestSize(ssl, client_hello, &encodedLen); + args->ech->writeEncoded = 0; + if (ret != 0) { + args->ech->type = ECH_TYPE_OUTER; + ssl->options.downgrade = downgrade; + return ret; + } + /* innerClientHelloLen and padding are based on the + * encoded (sealed) inner */ + args->ech->paddingLen = 31 - ((encodedLen - 1) % 32); + args->ech->innerClientHelloLen = encodedLen + + args->ech->paddingLen + args->ech->hpke->Nt; + + /* get expanded inner size (used for transcript) */ + ret = TLSX_GetRequestSize(ssl, client_hello, &args->length); /* set the type to outer */ args->ech->type = ECH_TYPE_OUTER; + ssl->options.downgrade = downgrade; if (ret != 0) return ret; - /* set innerClientHelloLen to ClientHelloInner + padding + tag */ - args->ech->paddingLen = 31 - ((args->length - 1) % 32); - args->ech->innerClientHelloLen = args->length + - args->ech->paddingLen + args->ech->hpke->Nt; - if (args->ech->innerClientHelloLen > 0xFFFF) + /* args->expandedInnerLen carries the length for the hash */ + args->expandedInnerLen = args->length; + + if (args->ech->innerClientHelloLen > 0xFFFF || + args->expandedInnerLen > 0xFFFF) return BUFFER_E; + /* set the length back to before we computed ClientHelloInner size */ args->length = (word32)args->preXLength; } @@ -4928,10 +4949,18 @@ int SendTls13ClientHello(WOLFSSL* ssl) args->output[args->idx++] = NO_COMPRESSION; #if defined(HAVE_ECH) - /* write inner then outer */ + /* Build the expanded inner ClientHello */ if (ssl->echConfigs != NULL && !ssl->options.disableECH && (ssl->options.echAccepted || args->ech->innerCount == 0)) { byte downgrade; + + /* calculate maximum buffer size needed */ + word32 encodedBodyLen = args->ech->innerClientHelloLen - + args->ech->hpke->Nt; + word32 innerBufSize = args->expandedInnerLen; + if (encodedBodyLen > innerBufSize) + innerBufSize = encodedBodyLen; + /* set the type to inner */ args->ech->type = ECH_TYPE_INNER; /* innerClientHello may already exist from hrr, free if it does */ @@ -4941,21 +4970,16 @@ int SendTls13ClientHello(WOLFSSL* ssl) } /* allocate the inner */ args->ech->innerClientHello = - (byte*)XMALLOC(args->ech->innerClientHelloLen - args->ech->hpke->Nt, - ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + (byte*)XMALLOC(innerBufSize, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); if (args->ech->innerClientHello == NULL) { args->ech->type = ECH_TYPE_OUTER; return MEMORY_E; } - /* set the padding bytes to 0 */ - XMEMSET(args->ech->innerClientHello + args->ech->innerClientHelloLen - - args->ech->hpke->Nt - args->ech->paddingLen, 0, - args->ech->paddingLen); - /* copy the client hello to the ech innerClientHello, exclude record */ - /* and handshake headers */ + /* copy everything before extensions into the innerClientHello + * ignore record and handshake headers */ XMEMCPY(args->ech->innerClientHello, args->output + RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ, - args->idx - (RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ)); + args->preXLength); /* copy the client random to inner - only for first CH, not after HRR */ if (!ssl->options.echAccepted) { XMEMCPY(ssl->arrays->clientRandomInner, ssl->arrays->clientRandom, @@ -4976,17 +5000,46 @@ int SendTls13ClientHello(WOLFSSL* ssl) /* copy the new client random */ XMEMCPY(ssl->arrays->clientRandom, args->output + args->clientRandomOffset, RAN_LEN); - /* write the extensions for inner - * ensuring that a version less than TLS1.3 is never offered */ - args->length = 0; + + /* ensure that a version less than TLS1.3 is never offered */ downgrade = ssl->options.downgrade; ssl->options.downgrade = 0; - ret = TLSX_WriteRequest(ssl, args->ech->innerClientHello + - args->idx - (RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ), - client_hello, &args->length); - ssl->options.downgrade = downgrade; + + /* write the expanded extensions into the inner buffer */ + args->length = 0; + ret = TLSX_WriteRequest(ssl, + args->ech->innerClientHello + args->preXLength, client_hello, + &args->length); + if (ret != 0) { + args->ech->type = ECH_TYPE_OUTER; + ssl->options.downgrade = downgrade; + return ret; + } + + /* hash expanded form */ + args->ech->innerClientHelloLen = args->expandedInnerLen; + ret = EchHashHelloInner(ssl, args->ech); + args->ech->innerClientHelloLen = encodedBodyLen + args->ech->hpke->Nt; + if (ret != 0) { + args->ech->type = ECH_TYPE_OUTER; + ssl->options.downgrade = downgrade; + return ret; + } + + /* Rewrite inner buffer with the encoded form for sealing */ + XMEMSET(args->ech->innerClientHello + + args->ech->innerClientHelloLen - args->ech->hpke->Nt - + args->ech->paddingLen, 0, args->ech->paddingLen); + args->ech->writeEncoded = 1; + args->length = 0; + ret = TLSX_WriteRequest(ssl, + args->ech->innerClientHello + args->preXLength, client_hello, + &args->length); + args->ech->writeEncoded = 0; + /* set the type to outer */ args->ech->type = ECH_TYPE_OUTER; + ssl->options.downgrade = downgrade; if (ret != 0) return ret; } @@ -5002,9 +5055,9 @@ int SendTls13ClientHello(WOLFSSL* ssl) args->idx += args->length; #if defined(HAVE_ECH) - /* encrypt and pack the ech innerClientHello */ + /* HPKE-seal inner hello and place into outer ECH extension's payload */ if (ssl->echConfigs != NULL && !ssl->options.disableECH && - (ssl->options.echAccepted || args->ech->innerCount == 0)) { + (ssl->options.echAccepted || args->ech->innerCount == 0)) { #if defined(WOLFSSL_TEST_ECH) if (ssl->echInnerHelloCb != NULL) { ret = ssl->echInnerHelloCb(args->ech->innerClientHello, @@ -5019,6 +5072,9 @@ int SendTls13ClientHello(WOLFSSL* ssl) if (ret != 0) return ret; + + /* innerCount gates HRR re-prep and the server's copyRandom logic. */ + args->ech->innerCount = 1; } #endif @@ -5040,16 +5096,10 @@ int SendTls13ClientHello(WOLFSSL* ssl) else #endif /* WOLFSSL_DTLS13 */ { -#if defined(HAVE_ECH) - /* compute the inner hash */ - if (ssl->echConfigs != NULL && !ssl->options.disableECH && - (ssl->options.echAccepted || args->ech->innerCount == 0)) { - ret = EchHashHelloInner(ssl, args->ech); - } -#endif - /* compute the outer hash */ - if (ret == 0) - ret = HashOutput(ssl, args->output, (int)args->idx, 0); + /* compute the outer hash (the inner hash was fed into + * hsHashesEch earlier, before the inner buffer was overwritten + * with the encoded form for HPKE sealing) */ + ret = HashOutput(ssl, args->output, (int)args->idx, 0); } } if (ret != 0) @@ -7583,6 +7633,7 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, ret = EchHashHelloInner(ssl, (WOLFSSL_ECH*)echX->data); if (ret != 0) goto exit_dch; + ((WOLFSSL_ECH*)echX->data)->innerCount = 1; } #endif diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 256d3d76c2..27becf72a6 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3168,6 +3168,7 @@ typedef struct WOLFSSL_ECH { byte configId; byte enc[HPKE_Npk_MAX]; byte innerCount; + byte writeEncoded; } WOLFSSL_ECH; WOLFSSL_LOCAL int EchConfigGetSupportedCipherSuite(WOLFSSL_EchConfig* config); From 9d938c12eaa0d00b16a7f16761f2d66c0353e8ad Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Fri, 24 Apr 2026 10:24:19 -0600 Subject: [PATCH 2/3] supported_versions added to non-encode list --- .github/scripts/openssl-ech.sh | 47 +++++++++++++++++++++++++++-- .github/workflows/openssl-ech.yml | 14 ++++++++- src/tls.c | 13 ++++---- src/tls13.c | 50 ++++++++++++++++--------------- 4 files changed, 92 insertions(+), 32 deletions(-) diff --git a/.github/scripts/openssl-ech.sh b/.github/scripts/openssl-ech.sh index d3dde32b6e..689e66f5d9 100644 --- a/.github/scripts/openssl-ech.sh +++ b/.github/scripts/openssl-ech.sh @@ -11,12 +11,14 @@ cleanup() { trap cleanup EXIT usage() { - echo "Usage: $0 [--suite ] [--workspace ]" + echo "Usage: $0 [--suite ] [--pqc ] [--hrr] [--workspace ]" exit 1 } MODE="" SUITE="" +PQC="" +FORCE_HRR=0 WORKSPACE=${GITHUB_WORKSPACE:-"."} @@ -40,15 +42,29 @@ while [ $# -gt 0 ]; do echo "Using suite: $SUITE" echo "" ;; + --pqc) + [ -z "$2" ] && { echo "ERROR: --pqc requires a value"; exit 1; } + PQC="$2" + shift 2 + ;; --workspace) [ -z "$2" ] && { echo "ERROR: --workspace requires a value"; exit 1; } WORKSPACE="$2" shift 2 ;; + --hrr) + FORCE_HRR=1 + shift + ;; *) echo "Unknown argument: $1"; usage ;; esac done +if [ "$FORCE_HRR" -ne 0 ] && [ -n "$PQC" ]; then + echo "ERROR: --hrr and --pqc are mutually exclusive" + exit 1 +fi + OPENSSL=${OPENSSL:-"openssl"} WOLFSSL_CLIENT=${WOLFSSL_CLIENT:-"$WORKSPACE/examples/client/client"} WOLFSSL_SERVER=${WOLFSSL_SERVER:-"$WORKSPACE/examples/server/server"} @@ -63,6 +79,15 @@ openssl_server(){ local ech_file="$WORKSPACE/ech_config.pem" local ech_config="" local port="" + local groups_arg="" + + # restrict group based on arguments + if [ "$FORCE_HRR" -eq 0 ]; then + groups_arg="-groups secp256r1" + if [ -n "$PQC" ]; then + groups_arg="-groups ${PQC#--pqc }" + fi + fi rm -f "$ech_file" @@ -82,6 +107,7 @@ openssl_server(){ -key2 "$CERT_DIR/server-key.pem" \ -ech_key "$ech_file" \ -servername "$PRIV_NAME" \ + $groups_arg \ -accept 0 \ -naccept 1 \ &>> "$TMP_LOG" <<< "wolfssl!" & @@ -104,6 +130,7 @@ openssl_server(){ -p "$port" \ -S "$PRIV_NAME" \ --ech "$ech_config" \ + $PQC \ &>> "$TMP_LOG" rm -f "$ech_file" @@ -127,6 +154,7 @@ openssl_client(){ -S "$PRIV_NAME" \ --ech "$PUB_NAME" \ $SUITE \ + $PQC \ &>> "$TMP_LOG" & # wait for server to be ready, then get port @@ -157,7 +185,15 @@ openssl_client(){ done echo "parsed ech config: $ech_config" &>> "$TMP_LOG" - # Test with OpenSSL s_client using ECH + # restrict group based on arguments + local groups_arg="-groups secp256r1" + if [ -n "$PQC" ]; then + groups_arg="-groups ${PQC#--pqc }" + elif [ "$FORCE_HRR" -ne 0 ]; then + groups_arg="" + fi + + # test with OpenSSL s_client using ECH echo "wolfssl" | $OPENSSL s_client \ -tls1_3 \ -connect "localhost:$port" \ @@ -166,6 +202,7 @@ openssl_client(){ -CAfile "$CERT_DIR/ca-cert.pem" \ -servername "$PRIV_NAME" \ -ech_config_list "$ech_config" \ + $groups_arg \ &>> "$TMP_LOG" grep -q "ECH: success: 1" "$TMP_LOG" @@ -178,12 +215,18 @@ case "$MODE" in if [ -n "$SUITE" ]; then SUITE="-suite $SUITE" fi + if [ -n "$PQC" ]; then + PQC="--pqc $PQC" + fi openssl_server ;; client) if [ -n "$SUITE" ]; then SUITE="--ech-suite $SUITE" fi + if [ -n "$PQC" ]; then + PQC="--pqc $PQC" + fi openssl_client ;; *) diff --git a/.github/workflows/openssl-ech.yml b/.github/workflows/openssl-ech.yml index 76bdbad975..9c9e06375b 100644 --- a/.github/workflows/openssl-ech.yml +++ b/.github/workflows/openssl-ech.yml @@ -24,7 +24,7 @@ jobs: with: path: wolfssl configure: >- - --enable-ech --enable-sha512 --enable-aes + --enable-ech --enable-sha512 --enable-aes --enable-mlkem CFLAGS='-DUSE_FLAT_TEST_H -DWOLFSSL_TEST_ECH' check: true install: true @@ -147,6 +147,18 @@ jobs: echo -e "\nTesting default suite with OpenSSL client and wolfSSL server\n" &>> "$LOG_FILE" bash ./openssl-ech.sh client &>> "$LOG_FILE" + echo -e "\nTesting default suite with OpenSSL server and wolfSSL client (PQC)\n" &>> "$LOG_FILE" + bash ./openssl-ech.sh server --pqc SecP384r1MLKEM1024 &>> "$LOG_FILE" + + echo -e "\nTesting default suite with OpenSSL client and wolfSSL server (PQC)\n" &>> "$LOG_FILE" + bash ./openssl-ech.sh client --pqc SecP384r1MLKEM1024 &>> "$LOG_FILE" + + echo -e "\nTesting default suite with OpenSSL server and wolfSSL client (HRR)\n" &>> "$LOG_FILE" + bash ./openssl-ech.sh server --hrr &>> "$LOG_FILE" + + echo -e "\nTesting default suite with OpenSSL client and wolfSSL server (HRR)\n" &>> "$LOG_FILE" + bash ./openssl-ech.sh client --hrr &>> "$LOG_FILE" + # weird suite (DHKEM_P521_HKDF_SHA512, HKDF_SHA256, HPKE_AES_256_GCM) echo -e "\nTesting weird suite with OpenSSL server and wolfSSL client\n" &>> "$LOG_FILE" bash ./openssl-ech.sh server --suite "18,1,2" &>> "$LOG_FILE" diff --git a/src/tls.c b/src/tls.c index 5b3812f2b4..c2b7371a8e 100644 --- a/src/tls.c +++ b/src/tls.c @@ -16331,12 +16331,15 @@ static int TLSX_EchRestoreSNI(WOLFSSL* ssl, char* serverName, /* Returns 1 if the extension may be encoded into ech_outer_extensions, * 0 otherwise */ -static int TLSX_ECH_IsEncodeable(word16 type) +static int TLSX_ECH_IsEncodable(word16 type) { + /* supported_versions being here prevents the inner hello from advertising + * a version less than TLS1.3 */ switch (type) { case TLSX_SERVER_NAME: - case TLSX_ECH: case TLSX_APPLICATION_LAYER_PROTOCOL: + case TLSX_SUPPORTED_VERSIONS: + case TLSX_ECH: #if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK) case TLSX_PRE_SHARED_KEY: #endif @@ -16395,7 +16398,7 @@ static int TLSX_ECH_BuildOuterExtensions(WOLFSSL* ssl, const byte* semaphore, if (!IS_OFF(seen, semIdx)) continue; TURN_ON(seen, semIdx); - if (type == TLSX_ECH || !TLSX_ECH_IsEncodeable(type)) + if (!TLSX_ECH_IsEncodable(type)) continue; if (typesStart != NULL) @@ -16450,7 +16453,7 @@ static int TLSX_GetSizeWithEch(WOLFSSL* ssl, byte* semaphore, byte msgType, ech = (WOLFSSL_ECH*)echX->data; /* If ECH won't be written exclude it from the size calculation */ - if (r == 0 !ssl->options.echAccepted && ech != NULL && + if (r == 0 && !ssl->options.echAccepted && ech != NULL && ech->innerCount != 0) { TURN_ON(semaphore, TLSX_ToSemaphore(echX->type)); } @@ -16661,7 +16664,7 @@ static int TLSX_WriteWithEch(WOLFSSL* ssl, byte* output, byte* semaphore, msgType, pOffset); } - /* only write ECH if have a shot at acceptance */ + /* only write ECH if there is a shot at acceptance */ if (ret == 0 && echX != NULL && (ssl->options.echAccepted || ((WOLFSSL_ECH*)echX->data)->innerCount == 0)) { diff --git a/src/tls13.c b/src/tls13.c index 8681feb34e..edfc47bda6 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -4790,39 +4790,43 @@ int SendTls13ClientHello(WOLFSSL* ssl) args->ech->type = ECH_TYPE_INNER; args->preXLength = (int)args->length; - /* get encoded inner size */ - args->ech->writeEncoded = 1; - encodedLen = args->length; - ret = TLSX_GetRequestSize(ssl, client_hello, &encodedLen); - args->ech->writeEncoded = 0; + /* get expanded inner size (used for transcript) */ + ret = TLSX_GetRequestSize(ssl, client_hello, &args->length); if (ret != 0) { args->ech->type = ECH_TYPE_OUTER; ssl->options.downgrade = downgrade; return ret; } + /* args->expandedInnerLen carries the length for the hash */ + args->expandedInnerLen = args->length; + if (args->expandedInnerLen > 0xFFFF) { + args->ech->type = ECH_TYPE_OUTER; + ssl->options.downgrade = downgrade; + return BUFFER_E; + } + + /* get encoded inner size */ + args->ech->writeEncoded = 1; + encodedLen = args->preXLength; + ret = TLSX_GetRequestSize(ssl, client_hello, &encodedLen); + args->ech->writeEncoded = 0; + /* set the type to outer */ + args->ech->type = ECH_TYPE_OUTER; + ssl->options.downgrade = downgrade; + if (ret != 0) + return ret; + /* innerClientHelloLen and padding are based on the * encoded (sealed) inner */ args->ech->paddingLen = 31 - ((encodedLen - 1) % 32); args->ech->innerClientHelloLen = encodedLen + args->ech->paddingLen + args->ech->hpke->Nt; - /* get expanded inner size (used for transcript) */ - ret = TLSX_GetRequestSize(ssl, client_hello, &args->length); - /* set the type to outer */ - args->ech->type = ECH_TYPE_OUTER; - ssl->options.downgrade = downgrade; - if (ret != 0) - return ret; - - /* args->expandedInnerLen carries the length for the hash */ - args->expandedInnerLen = args->length; - - if (args->ech->innerClientHelloLen > 0xFFFF || - args->expandedInnerLen > 0xFFFF) + if (args->ech->innerClientHelloLen > 0xFFFF) return BUFFER_E; - /* set the length back to before we computed ClientHelloInner size */ + /* restore the length to pre-ClientHelloInner computations */ args->length = (word32)args->preXLength; } } @@ -5026,17 +5030,17 @@ int SendTls13ClientHello(WOLFSSL* ssl) return ret; } - /* Rewrite inner buffer with the encoded form for sealing */ + /* zero padding bytes sealed with the inner hello */ XMEMSET(args->ech->innerClientHello + args->ech->innerClientHelloLen - args->ech->hpke->Nt - args->ech->paddingLen, 0, args->ech->paddingLen); + /* Rewrite inner buffer with the encoded form for sealing */ args->ech->writeEncoded = 1; args->length = 0; ret = TLSX_WriteRequest(ssl, args->ech->innerClientHello + args->preXLength, client_hello, &args->length); args->ech->writeEncoded = 0; - /* set the type to outer */ args->ech->type = ECH_TYPE_OUTER; ssl->options.downgrade = downgrade; @@ -5096,9 +5100,7 @@ int SendTls13ClientHello(WOLFSSL* ssl) else #endif /* WOLFSSL_DTLS13 */ { - /* compute the outer hash (the inner hash was fed into - * hsHashesEch earlier, before the inner buffer was overwritten - * with the encoded form for HPKE sealing) */ + /* compute the outer hash */ ret = HashOutput(ssl, args->output, (int)args->idx, 0); } } From 15b8c88bf6a7025cc4b575622e16324b5639dfd1 Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Wed, 6 May 2026 11:33:38 -0600 Subject: [PATCH 3/3] Write ECH last in HRR to promote interop --- .github/scripts/openssl-ech.sh | 145 +++++++++++++++++++++------------ src/tls.c | 17 ++-- 2 files changed, 103 insertions(+), 59 deletions(-) diff --git a/.github/scripts/openssl-ech.sh b/.github/scripts/openssl-ech.sh index 689e66f5d9..ca669de450 100644 --- a/.github/scripts/openssl-ech.sh +++ b/.github/scripts/openssl-ech.sh @@ -15,6 +15,9 @@ usage() { exit 1 } +# -------------------------------------------------------------------------- +# Argument parsing +# -------------------------------------------------------------------------- MODE="" SUITE="" PQC="" @@ -38,24 +41,21 @@ while [ $# -gt 0 ]; do [ -z "$2" ] && { echo "ERROR: --suite requires a value"; exit 1; } SUITE="$2" shift 2 - echo "" - echo "Using suite: $SUITE" - echo "" ;; --pqc) [ -z "$2" ] && { echo "ERROR: --pqc requires a value"; exit 1; } PQC="$2" shift 2 ;; + --hrr) + FORCE_HRR=1 + shift + ;; --workspace) [ -z "$2" ] && { echo "ERROR: --workspace requires a value"; exit 1; } WORKSPACE="$2" shift 2 ;; - --hrr) - FORCE_HRR=1 - shift - ;; *) echo "Unknown argument: $1"; usage ;; esac done @@ -65,6 +65,20 @@ if [ "$FORCE_HRR" -ne 0 ] && [ -n "$PQC" ]; then exit 1 fi +# Pick exactly one test variant. The variant decides which -groups go to +# each side and any extra flags needed to drive the desired handshake. +# default - both sides use secp256r1 (no HRR) +# pqc - both sides use the chosen PQC group +# hrr - pin one side to a group the other doesn't keyshare by +# default, forcing the server to send HelloRetryRequest +if [ -n "$PQC" ]; then + VARIANT="pqc" +elif [ "$FORCE_HRR" -ne 0 ]; then + VARIANT="hrr" +else + VARIANT="default" +fi + OPENSSL=${OPENSSL:-"openssl"} WOLFSSL_CLIENT=${WOLFSSL_CLIENT:-"$WORKSPACE/examples/client/client"} WOLFSSL_SERVER=${WOLFSSL_SERVER:-"$WORKSPACE/examples/server/server"} @@ -75,30 +89,49 @@ PRIV_NAME="ech-private-name.com" PUB_NAME="ech-public-name.com" MAX_WAIT=50 +# -------------------------------------------------------------------------- +# server mode -- OpenSSL is the server, wolfSSL is the client +# -------------------------------------------------------------------------- openssl_server(){ local ech_file="$WORKSPACE/ech_config.pem" local ech_config="" local port="" - local groups_arg="" - # restrict group based on arguments - if [ "$FORCE_HRR" -eq 0 ]; then - groups_arg="-groups secp256r1" - if [ -n "$PQC" ]; then - groups_arg="-groups ${PQC#--pqc }" - fi - fi + # Per-variant args. + # openssl_groups : -groups passed to OpenSSL s_server + # openssl_suite : -suite passed to `openssl ech` for key generation + # wolfssl_extra : extra flags for the wolfSSL client + local openssl_groups="" + local openssl_suite="" + local wolfssl_extra="" + + case "$VARIANT" in + default) + openssl_groups="-groups secp256r1" + ;; + pqc) + openssl_groups="-groups $PQC" + wolfssl_extra="--pqc $PQC" + ;; + hrr) + # wolfSSL client keyshares X25519 by default; pin OpenSSL + # server to secp384r1 so it must send HelloRetryRequest. + openssl_groups="-groups secp384r1" + ;; + esac + [ -n "$SUITE" ] && openssl_suite="-suite $SUITE" rm -f "$ech_file" - $OPENSSL ech -public_name "$PUB_NAME" -out "$ech_file" $SUITE &>> "$TMP_LOG" + $OPENSSL ech -public_name "$PUB_NAME" -out "$ech_file" $openssl_suite \ + &>> "$TMP_LOG" # parse ECH config from file ech_config=$(sed -n '/BEGIN ECHCONFIG/,/END ECHCONFIG/{/BEGIN ECHCONFIG\|END ECHCONFIG/d;p}' "$ech_file" | tr -d '\n') echo "parsed ech config: $ech_config" &>> "$TMP_LOG" - # start OpenSSL ECH server with ephemeral port and make sure it is - # line-buffered + # start OpenSSL ECH server with ephemeral port; line-buffer so the + # log can be grepped stdbuf -oL $OPENSSL s_server \ -tls1_3 \ -cert "$CERT_DIR/server-cert.pem" \ @@ -107,7 +140,7 @@ openssl_server(){ -key2 "$CERT_DIR/server-key.pem" \ -ech_key "$ech_file" \ -servername "$PRIV_NAME" \ - $groups_arg \ + $openssl_groups \ -accept 0 \ -naccept 1 \ &>> "$TMP_LOG" <<< "wolfssl!" & @@ -130,7 +163,7 @@ openssl_server(){ -p "$port" \ -S "$PRIV_NAME" \ --ech "$ech_config" \ - $PQC \ + $wolfssl_extra \ &>> "$TMP_LOG" rm -f "$ech_file" @@ -138,23 +171,53 @@ openssl_server(){ grep -q "ech_success=1" "$TMP_LOG" } +# -------------------------------------------------------------------------- +# client mode -- wolfSSL is the server, OpenSSL is the client +# -------------------------------------------------------------------------- openssl_client(){ local ready_file="$WORKSPACE/wolfssl_tls13_ready$$" local ech_config="" local port=0 + # Per-variant args. + # openssl_groups : -groups passed to OpenSSL s_client + # wolfssl_suite : --ech-suite passed to wolfSSL server for key gen + # wolfssl_extra : extra flags for the wolfSSL server + local openssl_groups="" + local wolfssl_suite="" + local wolfssl_extra="" + + case "$VARIANT" in + default) + openssl_groups="-groups secp256r1" + ;; + pqc) + openssl_groups="-groups $PQC" + wolfssl_extra="--pqc $PQC" + ;; + hrr) + # Pin wolfSSL server to SECP384R1 only. Have OpenSSL offer + # X25519 as keyshare with P-384 in supported_groups: the + # mismatched keyshare forces HelloRetryRequest, and P-384 in + # supported_groups lets the client answer it. + openssl_groups="-groups X25519:P-384" + wolfssl_extra="--force-curve SECP384R1" + ;; + esac + [ -n "$SUITE" ] && wolfssl_suite="--ech-suite $SUITE" + rm -f "$ready_file" - # start server with ephemeral port + ready file - # also set server to be line buffered so the log can be grepped + # start server with ephemeral port + ready file; line-buffer so the + # log can be grepped stdbuf -oL $WOLFSSL_SERVER \ -v 4 \ -R "$ready_file" \ -p "$port" \ -S "$PRIV_NAME" \ --ech "$PUB_NAME" \ - $SUITE \ - $PQC \ + $wolfssl_suite \ + $wolfssl_extra \ &>> "$TMP_LOG" & # wait for server to be ready, then get port @@ -185,14 +248,6 @@ openssl_client(){ done echo "parsed ech config: $ech_config" &>> "$TMP_LOG" - # restrict group based on arguments - local groups_arg="-groups secp256r1" - if [ -n "$PQC" ]; then - groups_arg="-groups ${PQC#--pqc }" - elif [ "$FORCE_HRR" -ne 0 ]; then - groups_arg="" - fi - # test with OpenSSL s_client using ECH echo "wolfssl" | $OPENSSL s_client \ -tls1_3 \ @@ -202,7 +257,7 @@ openssl_client(){ -CAfile "$CERT_DIR/ca-cert.pem" \ -servername "$PRIV_NAME" \ -ech_config_list "$ech_config" \ - $groups_arg \ + $openssl_groups \ &>> "$TMP_LOG" grep -q "ECH: success: 1" "$TMP_LOG" @@ -211,25 +266,7 @@ openssl_client(){ rm -f "$TMP_LOG" case "$MODE" in - server) - if [ -n "$SUITE" ]; then - SUITE="-suite $SUITE" - fi - if [ -n "$PQC" ]; then - PQC="--pqc $PQC" - fi - openssl_server - ;; - client) - if [ -n "$SUITE" ]; then - SUITE="--ech-suite $SUITE" - fi - if [ -n "$PQC" ]; then - PQC="--pqc $PQC" - fi - openssl_client - ;; - *) - exit 1 - ;; + server) openssl_server ;; + client) openssl_client ;; + *) exit 1 ;; esac diff --git a/src/tls.c b/src/tls.c index c2b7371a8e..7bf26442d3 100644 --- a/src/tls.c +++ b/src/tls.c @@ -17064,11 +17064,6 @@ int TLSX_WriteResponse(WOLFSSL *ssl, byte* output, byte msgType, word16* pOffset TURN_OFF(semaphore, TLSX_ToSemaphore(TLSX_KEY_SHARE)); } #endif -#ifdef HAVE_ECH - /* send the special confirmation */ - TURN_OFF(semaphore, TLSX_ToSemaphore(TLSX_ECH)); -#endif - /* Cookie is written below as last extension. */ break; #endif @@ -17152,6 +17147,18 @@ int TLSX_WriteResponse(WOLFSSL *ssl, byte* output, byte msgType, word16* pOffset } #endif +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) + /* write ECH last to promote interop with other implementations */ + if (msgType == hello_retry_request) { + XMEMSET(semaphore, 0xff, SEMAPHORE_SIZE); + TURN_OFF(semaphore, TLSX_ToSemaphore(TLSX_ECH)); + ret = TLSX_Write(ssl->extensions, output + offset, semaphore, + msgType, &offset); + if (ret != 0) + return ret; + } +#endif + #ifdef HAVE_EXTENDED_MASTER if (ssl->options.haveEMS && msgType == server_hello && !IsAtLeastTLSv1_3(ssl->version)) {