diff --git a/.github/scripts/openssl-ech.sh b/.github/scripts/openssl-ech.sh index d3dde32b6e..ca669de450 100644 --- a/.github/scripts/openssl-ech.sh +++ b/.github/scripts/openssl-ech.sh @@ -11,12 +11,17 @@ cleanup() { trap cleanup EXIT usage() { - echo "Usage: $0 [--suite ] [--workspace ]" + echo "Usage: $0 [--suite ] [--pqc ] [--hrr] [--workspace ]" exit 1 } +# -------------------------------------------------------------------------- +# Argument parsing +# -------------------------------------------------------------------------- MODE="" SUITE="" +PQC="" +FORCE_HRR=0 WORKSPACE=${GITHUB_WORKSPACE:-"."} @@ -36,9 +41,15 @@ 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; } @@ -49,6 +60,25 @@ while [ $# -gt 0 ]; do esac done +if [ "$FORCE_HRR" -ne 0 ] && [ -n "$PQC" ]; then + echo "ERROR: --hrr and --pqc are mutually exclusive" + 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"} @@ -59,21 +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="" + # 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" \ @@ -82,6 +140,7 @@ openssl_server(){ -key2 "$CERT_DIR/server-key.pem" \ -ech_key "$ech_file" \ -servername "$PRIV_NAME" \ + $openssl_groups \ -accept 0 \ -naccept 1 \ &>> "$TMP_LOG" <<< "wolfssl!" & @@ -104,6 +163,7 @@ openssl_server(){ -p "$port" \ -S "$PRIV_NAME" \ --ech "$ech_config" \ + $wolfssl_extra \ &>> "$TMP_LOG" rm -f "$ech_file" @@ -111,22 +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 \ + $wolfssl_suite \ + $wolfssl_extra \ &>> "$TMP_LOG" & # wait for server to be ready, then get port @@ -157,7 +248,7 @@ openssl_client(){ done echo "parsed ech config: $ech_config" &>> "$TMP_LOG" - # Test with OpenSSL s_client using ECH + # test with OpenSSL s_client using ECH echo "wolfssl" | $OPENSSL s_client \ -tls1_3 \ -connect "localhost:$port" \ @@ -166,6 +257,7 @@ openssl_client(){ -CAfile "$CERT_DIR/ca-cert.pem" \ -servername "$PRIV_NAME" \ -ech_config_list "$ech_config" \ + $openssl_groups \ &>> "$TMP_LOG" grep -q "ECH: success: 1" "$TMP_LOG" @@ -174,19 +266,7 @@ openssl_client(){ rm -f "$TMP_LOG" case "$MODE" in - server) - if [ -n "$SUITE" ]; then - SUITE="-suite $SUITE" - fi - openssl_server - ;; - client) - if [ -n "$SUITE" ]; then - SUITE="--ech-suite $SUITE" - fi - openssl_client - ;; - *) - exit 1 - ;; + server) openssl_server ;; + client) openssl_client ;; + *) exit 1 ;; esac 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 347761b263..7bf26442d3 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,108 @@ 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_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_APPLICATION_LAYER_PROTOCOL: + case TLSX_SUPPORTED_VERSIONS: + case TLSX_ECH: +#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 (!TLSX_ECH_IsEncodable(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 +16440,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 +16609,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 there is a shot at acceptance */ if (ret == 0 && echX != NULL && (ssl->options.echAccepted || ((WOLFSSL_ECH*)echX->data)->innerCount == 0)) { @@ -16910,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 @@ -16998,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)) { diff --git a/src/tls13.c b/src/tls13.c index 73ed518374..edfc47bda6 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,25 +4779,54 @@ 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 */ + /* 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; - /* set innerClientHelloLen to ClientHelloInner + padding + tag */ - args->ech->paddingLen = 31 - ((args->length - 1) % 32); - args->ech->innerClientHelloLen = args->length + + /* 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; + 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; } } @@ -4928,10 +4953,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 +4974,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 +5004,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; + } + + /* 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; if (ret != 0) return ret; } @@ -5002,9 +5059,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 +5076,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 +5100,8 @@ 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); + ret = HashOutput(ssl, args->output, (int)args->idx, 0); } } if (ret != 0) @@ -7583,6 +7635,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);