diff --git a/.github/scripts/openssl-ech.sh b/.github/scripts/openssl-ech.sh index ca669de450..fd61f1d2f0 100644 --- a/.github/scripts/openssl-ech.sh +++ b/.github/scripts/openssl-ech.sh @@ -11,7 +11,7 @@ cleanup() { trap cleanup EXIT usage() { - echo "Usage: $0 [--suite ] [--pqc ] [--hrr] [--workspace ]" + echo "Usage: $0 [--suite ] [--pqc ] [--hrr] [--reject] [--workspace ]" exit 1 } @@ -22,6 +22,7 @@ MODE="" SUITE="" PQC="" FORCE_HRR=0 +REJECT=0 WORKSPACE=${GITHUB_WORKSPACE:-"."} @@ -51,6 +52,10 @@ while [ $# -gt 0 ]; do FORCE_HRR=1 shift ;; + --reject) + REJECT=1 + shift + ;; --workspace) [ -z "$2" ] && { echo "ERROR: --workspace requires a value"; exit 1; } WORKSPACE="$2" @@ -84,9 +89,12 @@ WOLFSSL_CLIENT=${WOLFSSL_CLIENT:-"$WORKSPACE/examples/client/client"} WOLFSSL_SERVER=${WOLFSSL_SERVER:-"$WORKSPACE/examples/server/server"} CERT_DIR=${CERT_DIR:-"$WORKSPACE/certs"} +# correct ECH config, but it's old, ECH will be rejected +REJECT_ECH_CONFIG="AD7+DQA6rAAgACCATZdDlHed6GlDeiYsu3r7sdWUkLVHZuTa3lbOf+hIbAAEAAEAAQALZXhhbXBsZS5jb20AAA==" + TMP_LOG="$WORKSPACE/tmp_file.log" PRIV_NAME="ech-private-name.com" -PUB_NAME="ech-public-name.com" +PUB_NAME="example.com" MAX_WAIT=50 # -------------------------------------------------------------------------- @@ -128,6 +136,8 @@ openssl_server(){ # parse ECH config from file ech_config=$(sed -n '/BEGIN ECHCONFIG/,/END ECHCONFIG/{/BEGIN ECHCONFIG\|END ECHCONFIG/d;p}' "$ech_file" | tr -d '\n') + # reject overrides the config the client connects with + [ "$REJECT" -ne 0 ] && ech_config="$REJECT_ECH_CONFIG" echo "parsed ech config: $ech_config" &>> "$TMP_LOG" # start OpenSSL ECH server with ephemeral port; line-buffer so the @@ -158,17 +168,29 @@ openssl_server(){ done echo "parsed port: $port" &>> "$TMP_LOG" - # test with wolfssl client - $WOLFSSL_CLIENT -v 4 \ - -p "$port" \ - -S "$PRIV_NAME" \ - --ech "$ech_config" \ - $wolfssl_extra \ - &>> "$TMP_LOG" - rm -f "$ech_file" - grep -q "ech_success=1" "$TMP_LOG" + # test with wolfssl client + if [ "$REJECT" -ne 0 ]; then + $WOLFSSL_CLIENT -v 4 \ + -p "$port" \ + -S "$PRIV_NAME" \ + --ech "$ech_config" \ + $wolfssl_extra \ + &>> "$TMP_LOG" || true + + grep -q "ECH offered but rejected by server" "$TMP_LOG" + grep -q "ech_success=0" "$TMP_LOG" + else + $WOLFSSL_CLIENT -v 4 \ + -p "$port" \ + -S "$PRIV_NAME" \ + --ech "$ech_config" \ + $wolfssl_extra \ + &>> "$TMP_LOG" + + grep -q "ech_success=1" "$TMP_LOG" + fi } # -------------------------------------------------------------------------- @@ -246,21 +268,39 @@ openssl_client(){ exit 1 fi done + # reject overrides the config the client connects with + [ "$REJECT" -ne 0 ] && ech_config="$REJECT_ECH_CONFIG" echo "parsed ech config: $ech_config" &>> "$TMP_LOG" - # test with OpenSSL s_client using ECH - echo "wolfssl" | $OPENSSL s_client \ - -tls1_3 \ - -connect "localhost:$port" \ - -cert "$CERT_DIR/client-cert.pem" \ - -key "$CERT_DIR/client-key.pem" \ - -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" + if [ "$REJECT" -ne 0 ]; then + # test with OpenSSL s_client using ECH + echo "wolfssl" | $OPENSSL s_client \ + -tls1_3 \ + -connect "localhost:$port" \ + -cert "$CERT_DIR/client-cert.pem" \ + -key "$CERT_DIR/client-key.pem" \ + -CAfile "$CERT_DIR/ca-cert.pem" \ + -servername "$PRIV_NAME" \ + -ech_config_list "$ech_config" \ + $openssl_groups \ + &>> "$TMP_LOG" || true + + grep -q "ECH: Got 1 retry-configs" "$TMP_LOG" + else + # test with OpenSSL s_client using ECH + echo "wolfssl" | $OPENSSL s_client \ + -tls1_3 \ + -connect "localhost:$port" \ + -cert "$CERT_DIR/client-cert.pem" \ + -key "$CERT_DIR/client-key.pem" \ + -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" + fi } rm -f "$TMP_LOG" diff --git a/.github/workflows/openssl-ech.yml b/.github/workflows/openssl-ech.yml index 4d3ae03e69..3332165370 100644 --- a/.github/workflows/openssl-ech.yml +++ b/.github/workflows/openssl-ech.yml @@ -167,6 +167,12 @@ jobs: echo -e "\nTesting weird suite with OpenSSL client and wolfSSL server\n" &>> "$LOG_FILE" bash ./openssl-ech.sh client --suite "18,1,2" &>> "$LOG_FILE" + echo -e "\nTesting rejection with OpenSSL server and wolfSSL client\n" &>> "$LOG_FILE" + bash ./openssl-ech.sh server --reject &>> "$LOG_FILE" + + echo -e "\nTesting rejection with OpenSSL client and wolfSSL server\n" &>> "$LOG_FILE" + bash ./openssl-ech.sh client --reject &>> "$LOG_FILE" + # cleanup rm -f "$LOG_FILE" diff --git a/src/tls.c b/src/tls.c index f9ca896ae3..6de5ed0aa3 100644 --- a/src/tls.c +++ b/src/tls.c @@ -2375,13 +2375,13 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, word16 offset = 0; int cacheOnly = 0; SNI *sni = NULL; + const char *hostName = NULL; byte type; byte matched; #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) TLSX* echX = NULL; WOLFSSL_ECH* ech = NULL; - WOLFSSL_EchConfig* workingConfig; - word16 privateNameLen; + WOLFSSL_EchConfig* workingConfig = NULL; #endif #endif /* !NO_WOLFSSL_SERVER */ TLSX *extension = TLSX_Find(ssl->extensions, TLSX_SERVER_NAME); @@ -2422,13 +2422,7 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, } #endif -#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) - if ((!extension || !extension->data) || - (ech != NULL && ech->sniState == ECH_INNER_SNI && - ech->privateName == NULL)) { -#else if (!extension || !extension->data) { -#endif /* This will keep SNI even though TLSX_UseSNI has not been called. * Enable it so that the received sni is available to functions * that use a custom callback when SNI is received. @@ -2476,28 +2470,6 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, if (!cacheOnly && !(sni = TLSX_SNI_Find((SNI*)extension->data, type))) return 0; /* not using this type of SNI. */ -#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) - if (ech != NULL && ech->sniState == ECH_INNER_SNI){ - /* SNI status is carried over from processing the outer hello so it is - * necessary to clear it before processing the inner hello */ - ech->sniState = ECH_INNER_SNI_ATTEMPT; - if (sni != NULL){ - sni->status = WOLFSSL_SNI_NO_MATCH; - } - } - else if (ech != NULL && ech->sniState == ECH_OUTER_SNI && - ech->privateName == NULL && sni != NULL){ - /* save the private SNI before it is overwritten by the public SNI */ - privateNameLen = (word16)XSTRLEN(sni->data.host_name) + 1; - ech->privateName = (char*)XMALLOC(privateNameLen, ssl->heap, - DYNAMIC_TYPE_TMP_BUFFER); - if (ech->privateName == NULL) - return MEMORY_E; - XMEMCPY((char*)ech->privateName, sni->data.host_name, - privateNameLen); - } -#endif - #if defined(WOLFSSL_TLS13) /* Don't process the second ClientHello SNI extension if there * was problems with the first. @@ -2506,42 +2478,46 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, return 0; #endif -#if defined(HAVE_ECH) - if (ech != NULL && ech->sniState == ECH_INNER_SNI_ATTEMPT && - ech->privateName != NULL) { - matched = cacheOnly || (XSTRLEN(ech->privateName) == size && - XSTRNCMP(ech->privateName, (const char*)input + offset, size) == 0); - } - else -#endif - { - const char* hostName = (sni != NULL) ? sni->data.host_name : NULL; - matched = cacheOnly || (hostName != NULL && - XSTRLEN(hostName) == size && - XSTRNCMP(hostName, (const char*)input + offset, size) == 0); - } + hostName = (sni != NULL) ? sni->data.host_name : NULL; + matched = (hostName != NULL && + XSTRLEN(hostName) == size && + XSTRNCMP(hostName, (const char*)input + offset, size) == 0); #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) - if (!matched && ech != NULL && ech->sniState == ECH_OUTER_SNI) { + /* While parsing the outer CH accept a match against any + * echConfig publicName */ + if (!matched && ech != NULL && !ssl->options.echProcessingInner) { workingConfig = ech->echConfig; while (workingConfig != NULL) { matched = XSTRLEN(workingConfig->publicName) == size && XSTRNCMP(workingConfig->publicName, (const char*)input + offset, size) == 0; - if (matched) break; - workingConfig = workingConfig->next; } + + /* If a publicName is matched then this SNI is not something that should + * be forcibly cached */ + if (matched) + cacheOnly = 0; } #endif + if (!matched) + matched = cacheOnly; + if (matched || (sni != NULL && (sni->options & WOLFSSL_SNI_ANSWER_ON_MISMATCH))) { int matchStat; - int r = TLSX_UseSNI(&ssl->extensions, type, input + offset, size, - ssl->heap); + int r; + TLSX** writeList = &ssl->extensions; +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) + if (workingConfig != NULL) + writeList = &ech->extensions; +#endif + + r = TLSX_UseSNI(writeList, type, input + offset, size, ssl->heap); if (r != WOLFSSL_SUCCESS) return r; /* throws error. */ @@ -2559,10 +2535,14 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, matchStat = WOLFSSL_SNI_FAKE_MATCH; } - TLSX_SNI_SetStatus(ssl->extensions, type, (byte)matchStat); + TLSX_SNI_SetStatus(*writeList, type, (byte)matchStat); + + if (!cacheOnly) { + extension = TLSX_Find(*writeList, TLSX_SERVER_NAME); - if (!cacheOnly) - TLSX_SetResponse(ssl, TLSX_SERVER_NAME); + if (extension) + extension->resp = 1; + } } else if ((sni == NULL) || !(sni->options & WOLFSSL_SNI_CONTINUE_ON_MISMATCH)) { @@ -2682,8 +2662,8 @@ int TLSX_UseSNI(TLSX** extensions, byte type, const void* data, word16 size, return WOLFSSL_SUCCESS; } -#ifndef NO_WOLFSSL_SERVER - +/* client-side needs this function when ECH is enabled */ +#if !defined(NO_WOLFSSL_SERVER) || defined(HAVE_ECH) /** Tells the SNI requested by the client. */ word16 TLSX_SNI_GetRequest(TLSX* extensions, byte type, void** data, byte ignoreStatus) @@ -2703,7 +2683,9 @@ word16 TLSX_SNI_GetRequest(TLSX* extensions, byte type, void** data, return 0; } +#endif +#ifndef NO_WOLFSSL_SERVER /** Sets the options for a SNI object. */ void TLSX_SNI_SetOptions(TLSX* extensions, byte type, byte options) { @@ -13833,19 +13815,26 @@ static int TLSX_ECH_Use(WOLFSSL_EchConfig* echConfig, TLSX** extensions, XFREE(ech, heap, DYNAMIC_TYPE_TMP_BUFFER); return MEMORY_E; } + ForceZero(ech->hpke, sizeof(Hpke)); ret = wc_HpkeInit(ech->hpke, ech->kemId, ech->cipherSuite.kdfId, ech->cipherSuite.aeadId, heap); /* setup the ephemeralKey */ if (ret == 0) ret = wc_HpkeGenerateKeyPair(ech->hpke, &ech->ephemeralKey, rng); + /* use the chosen config's public name for the outer SNI */ if (ret == 0) { - ret = TLSX_Push(extensions, TLSX_ECH, ech, heap); - if (ret != 0) { - wc_HpkeFreeKey(ech->hpke, ech->hpke->kem, ech->ephemeralKey, - ech->hpke->heap); - } + ret = TLSX_UseSNI(&ech->extensions, WOLFSSL_SNI_HOST_NAME, + echConfig->publicName, (word16)XSTRLEN(echConfig->publicName), + heap); + if (ret == WOLFSSL_SUCCESS) + ret = 0; } + if (ret == 0) + ret = TLSX_Push(extensions, TLSX_ECH, ech, heap); if (ret != 0) { + TLSX_FreeAll(ech->extensions, heap); + wc_HpkeFreeKey(ech->hpke, ech->hpke->kem, ech->ephemeralKey, + ech->hpke->heap); XFREE(ech->hpke, heap, DYNAMIC_TYPE_TMP_BUFFER); XFREE(ech, heap, DYNAMIC_TYPE_TMP_BUFFER); } @@ -14897,9 +14886,8 @@ static void TLSX_ECH_Free(WOLFSSL_ECH* ech, void* heap) { XFREE(ech->innerClientHello, heap, DYNAMIC_TYPE_TMP_BUFFER); if (ech->hpke != NULL) { - if (ech->ephemeralKey != NULL) - wc_HpkeFreeKey(ech->hpke, ech->hpke->kem, ech->ephemeralKey, - ech->hpke->heap); + wc_HpkeFreeKey(ech->hpke, ech->hpke->kem, ech->ephemeralKey, + ech->hpke->heap); /* wc_HpkeFreeEchSecret is intentionally not here, free it in * TLSX_ExtractEch / TLSX_FinalizeEch */ XFREE(ech->hpke, heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -14908,8 +14896,7 @@ static void TLSX_ECH_Free(WOLFSSL_ECH* ech, void* heap) ForceZero(ech->hpkeContext, sizeof(HpkeBaseContext)); XFREE(ech->hpkeContext, heap, DYNAMIC_TYPE_TMP_BUFFER); } - if (ech->privateName != NULL) - XFREE((char*)ech->privateName, heap, DYNAMIC_TYPE_TMP_BUFFER); + TLSX_FreeAll(ech->extensions, heap); XFREE(ech, heap, DYNAMIC_TYPE_TMP_BUFFER); (void)heap; @@ -16656,95 +16643,95 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) #if defined(WOLFSSL_TLS13) || !defined(NO_WOLFSSL_CLIENT) #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) -static int TLSX_EchChangeSNI(WOLFSSL* ssl, TLSX** pEchX, - char* serverName, TLSX** pServerNameX, - TLSX*** pExtensions) +/* Returns 1 if the extensions should be hidden for this write */ +static int TLSX_EchShouldHideInner(WOLFSSL_ECH* ech) { - int ret = 0; - TLSX* echX = NULL; - TLSX* serverNameX = NULL; - TLSX** extensions = NULL; - - /* calculate the rest of the extensions length with inner ech */ - if (ssl->extensions) - echX = TLSX_Find(ssl->extensions, TLSX_ECH); - - if (echX == NULL && ssl->ctx && ssl->ctx->extensions) - /* if not NULL the semaphore will stop it from being counted */ - echX = TLSX_Find(ssl->ctx->extensions, TLSX_ECH); - - /* if type is outer and this is a real ECH then change sni to public name */ - if (echX != NULL && ssl->echConfigs != NULL && - ((WOLFSSL_ECH*)echX->data)->type == ECH_TYPE_OUTER) { - if (ssl->extensions) { - serverNameX = TLSX_Find(ssl->extensions, TLSX_SERVER_NAME); - - if (serverNameX != NULL) - extensions = &ssl->extensions; - } - - if (serverNameX == NULL && ssl->ctx && ssl->ctx->extensions) { - serverNameX = TLSX_Find(ssl->ctx->extensions, TLSX_SERVER_NAME); - if (serverNameX != NULL) - extensions = &ssl->ctx->extensions; - } + if (ech == NULL || ech->type != ECH_TYPE_OUTER) + return 0; + return 1; +} - /* ECH requires an inner SNI to be present for ClientHelloInner. - * Without it, fail instead of mutating extension lists. */ - if (serverNameX == NULL) { - ret = BAD_FUNC_ARG; +/* Swap matching extension types between *sslExts and *echExts. + * Non-matched extensions in *echExts are prepended to *sslExts + * popCount is the number of leading extensions to move from + * *sslExts to *echExts + * + * Ordering is kept in mind for OuterExtensions. This is why the leading + * popCount extensions are 'reversed' off the list. + * + * Returns a count of extensions prepended to sslExts. */ +static word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, + word16 popCount) +{ + TLSX* chunk = NULL; + TLSX* node; + TLSX* outer; + TLSX* inner; + TLSX** outerLink; + TLSX** innerLink; + word16 prepended = 0; + + /* unhook popCount nodes off *sslExts head into chunk. + * Head-prepend undoes the reversal caused by appending onto sslExts. */ + while (popCount > 0 && *sslExts != NULL) { + node = *sslExts; + *sslExts = node->next; + node->next = chunk; + chunk = node; + popCount--; + } + + outerLink = echExts; + while (*outerLink != NULL) { + innerLink = sslExts; + outer = *outerLink; + + while (*innerLink != NULL && (*innerLink)->type != outer->type) + innerLink = &(*innerLink)->next; + + if (*innerLink != NULL) { + inner = *innerLink; + + *innerLink = outer; + *outerLink = inner; + node = outer->next; + outer->next = inner->next; + inner->next = node; + + outerLink = &inner->next; } - - /* store the inner server name */ - if (ret == 0 && serverNameX != NULL) { - char* hostName = ((SNI*)serverNameX->data)->data.host_name; - word32 hostNameSz = (word32)XSTRLEN(hostName) + 1; - - if (hostNameSz > WOLFSSL_HOST_NAME_MAX) - ret = BAD_LENGTH_E; - else - XMEMCPY(serverName, hostName, hostNameSz); + else { + *outerLink = outer->next; + outer->next = *sslExts; + *sslExts = outer; + prepended++; } + } - /* only swap the SNI if one was found; extensions is non-NULL if an - * SNI entry was found on ssl->extensions or ctx->extensions */ - if (ret == 0 && extensions != NULL) { - /* remove the inner server name */ - TLSX_Remove(extensions, TLSX_SERVER_NAME, ssl->heap); + /* outerLink is at the tail of *echExts; append the chunk */ + *outerLink = chunk; - /* set the public name as the server name */ - if ((ret = TLSX_UseSNI(extensions, WOLFSSL_SNI_HOST_NAME, - ((WOLFSSL_ECH*)echX->data)->echConfig->publicName, - XSTRLEN(((WOLFSSL_ECH*)echX->data)->echConfig->publicName), - ssl->heap)) == WOLFSSL_SUCCESS) - ret = 0; - } - } - *pServerNameX = serverNameX; - *pExtensions = extensions; - *pEchX = echX; - return ret; + return prepended; } -static int TLSX_EchRestoreSNI(WOLFSSL* ssl, char* serverName, - TLSX* serverNameX, TLSX** extensions) +/* If ECH is accepted, delete ech->extensions + * If rejected, replace matching ssl->extensions with ech->extensions, + * prepending to head if necessary */ +void TLSX_EchReplaceExtensions(WOLFSSL* ssl, byte accepted) { - int ret = 0; + TLSX* echX; + WOLFSSL_ECH* ech; - /* always remove the publicName SNI we injected, regardless of whether - * there was a prior inner SNI to restore */ - if (extensions != NULL) - TLSX_Remove(extensions, TLSX_SERVER_NAME, ssl->heap); + echX = TLSX_Find(ssl->extensions, TLSX_ECH); + if (echX == NULL || echX->data == NULL) + return; + ech = (WOLFSSL_ECH*)echX->data; - if (serverNameX != NULL) { - /* restore the inner server name */ - ret = TLSX_UseSNI(extensions, WOLFSSL_SNI_HOST_NAME, - serverName, XSTRLEN(serverName), ssl->heap); + if (!accepted) + (void)TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, 0); - if (ret == WOLFSSL_SUCCESS) - ret = 0; - } - return ret; + TLSX_FreeAll(ech->extensions, ssl->heap); + ech->extensions = NULL; } /* Returns 1 if the extension may be encoded into ech_outer_extensions, @@ -16854,39 +16841,42 @@ static int TLSX_ECH_BuildOuterExtensions(WOLFSSL* ssl, const byte* semaphore, static int TLSX_GetSizeWithEch(WOLFSSL* ssl, byte* semaphore, byte msgType, word16* pLength) { - int ret = 0, r = 0; + int ret = 0; 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); + word16 prepended = 0; + byte installed = 0; + if (ssl->extensions) + echX = TLSX_Find(ssl->extensions, TLSX_ECH); if (echX != NULL) ech = (WOLFSSL_ECH*)echX->data; + if (TLSX_EchShouldHideInner(ech)) { + prepended = TLSX_EchSwapExtensions(&ssl->extensions, + &ech->extensions, 0); + installed = 1; + } + /* 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) { + if (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) + if (ret == 0 && ssl->extensions) ret = TLSX_GetSize(ssl->extensions, semaphore, msgType, pLength); - if (r == 0 && ret == 0 && ssl->ctx && ssl->ctx->extensions) + if (ret == 0 && ssl->ctx && ssl->ctx->extensions) ret = TLSX_GetSize(ssl->ctx->extensions, semaphore, msgType, pLength); - if (r == 0) - r = TLSX_EchRestoreSNI(ssl, serverName, serverNameX, extensions); - WC_FREE_VAR_EX(serverName, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); - if (ret == 0 && r != 0) - ret = r; + if (installed) { + prepended = TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, + prepended); + if (ret == 0 && prepended != 0) { + ret = BAD_STATE_E; + } + } return ret; } #endif @@ -17016,19 +17006,24 @@ int TLSX_GetRequestSize(WOLFSSL* ssl, byte msgType, word32* pLength) static int TLSX_WriteWithEch(WOLFSSL* ssl, byte* output, byte* semaphore, byte msgType, word16* pOffset) { - int r = 0, ret = 0; + int ret = 0; TLSX* echX = NULL; - TLSX* serverNameX = NULL; - TLSX** extensions = NULL; WOLFSSL_ECH* ech = NULL; - WC_DECLARE_VAR(serverName, char, WOLFSSL_HOST_NAME_MAX, 0); + word16 prepended = 0; + byte installed = 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 (ssl->extensions) + echX = TLSX_Find(ssl->extensions, TLSX_ECH); + if (echX != NULL) ech = (WOLFSSL_ECH*)echX->data; + + if (TLSX_EchShouldHideInner(ech)) { + prepended = TLSX_EchSwapExtensions(&ssl->extensions, + &ech->extensions, 0); + installed = 1; + } + + if (echX != NULL) { /* turn ech on so it doesn't write, then write it last */ TURN_ON(semaphore, TLSX_ToSemaphore(echX->type)); } @@ -17080,7 +17075,7 @@ static int TLSX_WriteWithEch(WOLFSSL* ssl, byte* output, byte* semaphore, /* turn off and write it last */ TURN_OFF(semaphore, TLSX_ToSemaphore(echX->type)); - if (ret == 0 && ssl->extensions) { + if (ssl->extensions) { ret = TLSX_Write(ssl->extensions, output + *pOffset, semaphore, msgType, pOffset); } @@ -17091,12 +17086,13 @@ static int TLSX_WriteWithEch(WOLFSSL* ssl, byte* output, byte* semaphore, } } - if (r == 0) - r = TLSX_EchRestoreSNI(ssl, serverName, serverNameX, extensions); - WC_FREE_VAR_EX(serverName, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); - - if (ret == 0 && r != 0) - ret = r; + if (installed) { + prepended = TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, + prepended); + if (ret == 0 && prepended != 0) { + ret = BAD_STATE_E; + } + } return ret; } #endif diff --git a/src/tls13.c b/src/tls13.c index 5e377f40ef..9bfbd75248 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -4588,7 +4588,7 @@ typedef struct Sch13Args { word32 length; #if defined(HAVE_ECH) int clientRandomOffset; - int preXLength; + word32 preXLength; word32 expandedInnerLen; WOLFSSL_ECH* ech; #endif @@ -4782,6 +4782,8 @@ int SendTls13ClientHello(WOLFSSL* ssl) #if defined(HAVE_ECH) if (!ssl->options.disableECH) { TLSX* echX = TLSX_Find(ssl->extensions, TLSX_ECH); + void* hostName = NULL; + word16 nameLen; if (echX == NULL) return WOLFSSL_FATAL_ERROR; @@ -4809,7 +4811,7 @@ int SendTls13ClientHello(WOLFSSL* ssl) /* set the type to inner */ args->ech->type = ECH_TYPE_INNER; - args->preXLength = (int)args->length; + args->preXLength = args->length; /* get expanded inner size (used for transcript) */ ret = TLSX_GetRequestSize(ssl, client_hello, &args->length); @@ -4839,8 +4841,9 @@ int SendTls13ClientHello(WOLFSSL* ssl) return ret; /* calculate padding (RFC 9849, section 6.1.3) */ - if (args->ech->privateName != NULL) { - word16 nameLen = (word16)XSTRLEN(args->ech->privateName); + nameLen = TLSX_SNI_GetRequest(ssl->extensions, + WOLFSSL_SNI_HOST_NAME, &hostName, 1); + if (hostName != NULL) { if (nameLen > args->ech->echConfig->maxNameLen) args->ech->paddingLen = 0; else @@ -4862,7 +4865,7 @@ int SendTls13ClientHello(WOLFSSL* ssl) return BUFFER_E; /* restore the length to pre-ClientHelloInner computations */ - args->length = (word32)args->preXLength; + args->length = args->preXLength; } } #endif @@ -5300,6 +5303,12 @@ static int EchCheckAcceptance(WOLFSSL* ssl, byte* label, word16 labelSz, ssl->hsHashes = tmpHashes; } + /* Skip only when the HRR signals ECH acceptance + * -> CH2 still needs ech->extensions for inner/outer extension swap + * during write */ + if (msgType != hello_retry_request || !ssl->options.echAccepted) + TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); + return ret; } #endif /* HAVE_ECH */ @@ -5865,6 +5874,8 @@ int DoTls13ServerHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, /* server rejected ECH, fall back to outer */ Free_HS_Hashes(ssl->hsHashesEch, ssl->heap); ssl->hsHashesEch = NULL; + /* EchCheckAcceptance is bypassed, so replace extensions now */ + TLSX_EchReplaceExtensions(ssl, 0); } else { /* account for hrr extension instead of server random */ @@ -7678,15 +7689,18 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, #if defined(HAVE_ECH) if (!ssl->options.echProcessingInner && echX != NULL && - ((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE) { - if (((WOLFSSL_ECH*)echX->data)->innerClientHello != NULL) { + ssl->ctx->echConfigs != NULL && !ssl->options.disableECH) { + if (((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE && + ((WOLFSSL_ECH*)echX->data)->innerClientHello != NULL) { + /* ECH accepted: use private extensions */ + TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); /* Client sent real ECH and inner hello was decrypted, jump to * exit so the caller can re-invoke with the inner hello */ goto exit_dch; } else { - /* If ECH was accepted in ClientHello1 then ClientHello2 MUST - * contain an ECH extension */ + /* If ECH was accepted in CH1 then CH2 MUST contain + * an ECH extension */ if (ssl->options.serverState == SERVER_HELLO_RETRY_REQUEST_COMPLETE && ssl->options.echAccepted) { @@ -7694,10 +7708,16 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, "extension"); ERROR_OUT(INCOMPLETE_DATA, exit_dch); } - /* Server has ECH but client did not send ECH. Clear the - * response flag so the empty ECH extension is not written - * in EncryptedExtensions. */ - echX->resp = 0; + + /* Otherwise ECH rejected: use public extensions */ + if (((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE) { + TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); + echX->resp = 0; + } + else if (((WOLFSSL_ECH*)echX->data)->state == + ECH_WRITE_RETRY_CONFIGS) { + TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); + } } } #endif @@ -13776,18 +13796,12 @@ int DoTls13HandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, *inOutIdx = echInOutIdx; /* call again with the inner hello */ if (ret == 0) { - if (((WOLFSSL_ECH*)echX->data)->sniState == ECH_OUTER_SNI) { - ((WOLFSSL_ECH*)echX->data)->sniState = ECH_INNER_SNI; - } - ssl->options.echProcessingInner = 1; ret = DoTls13ClientHello(ssl, ((WOLFSSL_ECH*)echX->data)->innerClientHello, &echInOutIdx, ((WOLFSSL_ECH*)echX->data)->innerClientHelloLen); ssl->options.echProcessingInner = 0; - - ((WOLFSSL_ECH*)echX->data)->sniState = ECH_SNI_DONE; } if (ret == 0 && ((WOLFSSL_ECH*)echX->data)->state != ECH_PARSED_INTERNAL) { diff --git a/tests/api.c b/tests/api.c index 6208cc2b2d..0eefcd9d5b 100644 --- a/tests/api.c +++ b/tests/api.c @@ -7895,17 +7895,37 @@ static int test_wolfSSL_UseSNI_params(void) ExpectNotNull(ssl); /* invalid [ctx|ssl] */ - ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_CTX_UseSNI(NULL, 0, "ctx", 3)); - ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_UseSNI( NULL, 0, "ssl", 3)); + ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_CTX_UseSNI(NULL, WOLFSSL_SNI_HOST_NAME, + "ctx", 3)); + ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_UseSNI( NULL, WOLFSSL_SNI_HOST_NAME, + "ssl", 3)); /* invalid type */ ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_CTX_UseSNI(ctx, (byte)-1, "ctx", 3)); ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_UseSNI( ssl, (byte)-1, "ssl", 3)); /* invalid data */ - ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_CTX_UseSNI(ctx, 0, NULL, 3)); - ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_UseSNI( ssl, 0, NULL, 3)); + ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_CTX_UseSNI(ctx, WOLFSSL_SNI_HOST_NAME, + NULL, 3)); + ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_UseSNI( ssl, WOLFSSL_SNI_HOST_NAME, + NULL, 3)); + /* invalid length */ + if (EXPECT_SUCCESS()) { + /* 300 chars > WOLFSSL_HOST_NAME_MAX (256) */ + char longName[300]; + + XMEMSET(longName, 'a', sizeof(longName) - 1); + longName[sizeof(longName) - 1] = '\0'; + + /* host name >= WOLFSSL_HOST_NAME_MAX */ + ExpectIntEQ(BAD_LENGTH_E, wolfSSL_CTX_UseSNI(ctx, WOLFSSL_SNI_HOST_NAME, + longName, (word16)XSTRLEN(longName))); + ExpectIntEQ(BAD_LENGTH_E, wolfSSL_UseSNI( ssl, WOLFSSL_SNI_HOST_NAME, + longName, (word16)XSTRLEN(longName))); + } /* success case */ - ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_CTX_UseSNI(ctx, 0, "ctx", 3)); - ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_UseSNI( ssl, 0, "ssl", 3)); + ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_CTX_UseSNI(ctx, WOLFSSL_SNI_HOST_NAME, + "ctx", 3)); + ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_UseSNI( ssl, WOLFSSL_SNI_HOST_NAME, + "ssl", 3)); wolfSSL_free(ssl); wolfSSL_CTX_free(ctx); @@ -13770,6 +13790,8 @@ static int test_wolfSSL_Tls13_ECH_params_b64(void) const char* b64BadCiph = "AEX+DQBBFAAgACBuAoQI8+liEVYQbXKBDeVgTmF2rfXuKO2knhwrN7jgTgAE/v4AAQASY2xvdWRmbGFyZS1lY2guY29tAAA="; /* ech configs with unrecognized mandatory extension */ const char* b64Mandatory = "AEn+DQBFFAAgACBuAoQI8+liEVYQbXKBDeVgTmF2rfXuKO2knhwrN7jgTgAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAT6+gAA"; + /* ech configs with unrecognized mandatory extension first */ + const char* b64MandatoryFirst = "AJD+DQBGAQAgACCjR6+Qn9UYkMaWdXZzsby88vXFhPHJ2tWCDHQJLvMkEgAEAAEAAQATZWNoLXB1YmxpYy1uYW1lLmNvbQAE+voAAP4NAEICACAAIDDOry602zn7HwOn02yWPyLtC49sXhxDxlCXlMEBgGBeAAQAAQABABNlY2gtcHVibGljLW5hbWUuY29tAAA="; /* ech configs with bad version first */ const char* b64BadVers1 = "AIz+HQBCAQAgACCjR6+Qn9UYkMaWdXZzsby88vXFhPHJ2tWCDHQJLvMkEgAEAAEAAQATZWNoLXB1YmxpYy1uYW1lLmNvbQAA/g0AQgIAIAAgMM6vLrTbOfsfA6fTbJY/Iu0Lj2xeHEPGUJeUwQGAYF4ABAABAAEAE2VjaC1wdWJsaWMtbmFtZS5jb20AAA=="; /* ech configs with bad version second */ @@ -13837,6 +13859,20 @@ static int test_wolfSSL_Tls13_ECH_params_b64(void) ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_SetEchConfigsBase64(ssl, b64Mandatory, (word32)XSTRLEN(b64Mandatory))); + /* unrecognized mandatory extension */ + ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_CTX_SetEchConfigsBase64(ctx, + b64MandatoryFirst, (word32)XSTRLEN(b64MandatoryFirst))); + ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_SetEchConfigsBase64(ssl, + b64MandatoryFirst, (word32)XSTRLEN(b64MandatoryFirst))); + ExpectIntEQ(2, ctx->echConfigs->configId); + ExpectIntEQ(2, ssl->echConfigs->configId); + + /* clear configs */ + wolfSSL_CTX_SetEchEnable(ctx, 0); + wolfSSL_CTX_SetEchEnable(ctx, 1); + wolfSSL_SetEchEnable(ssl, 0); + wolfSSL_SetEchEnable(ssl, 1); + /* bad version first, should only have config 2 set */ ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_CTX_SetEchConfigsBase64(ctx, b64BadVers1, (word32)XSTRLEN(b64BadVers1))); @@ -14024,6 +14060,63 @@ static word16 echCbTestKemID = 0; static word16 echCbTestKdfID = 0; static word16 echCbTestAeadID = 0; +/* return the index of the first extension + * extLen will be updated with the length of the extensions field */ +static int ech_seek_extensions(byte* buf, word16* extLen) +{ + word16 idx; + byte sessionIdLen; + word16 cipherSuitesLen; + byte compressionLen; + + idx = OPAQUE16_LEN + RAN_LEN; + + sessionIdLen = buf[idx++]; + idx += sessionIdLen; + + ato16(buf + idx, &cipherSuitesLen); + idx += OPAQUE16_LEN + cipherSuitesLen; + + compressionLen = buf[idx++]; + idx += compressionLen; + + ato16(buf + idx, extLen); + idx += OPAQUE16_LEN; + + return idx; +} + +/* locate a particular extension: + * idx_p is updated with the location of that extension + * -> idx_p should start just after the handshake header + * 0 returned on success, error otherwise */ +static int ech_find_extension(byte* buf, word16* idx_p, word16 extType) +{ + word16 idx; + word16 extIdx; + word16 extLen; + + extIdx = ech_seek_extensions(buf + *idx_p, &extLen) + *idx_p; + idx = extIdx; + + while (idx - extIdx < extLen) { + word16 type; + word16 len; + + ato16(buf + idx, &type); + if (type == extType) { + *idx_p = idx; + return 0; + } + + idx += OPAQUE16_LEN; + ato16(buf + idx, &len); + idx += OPAQUE16_LEN + len; + } + + return BAD_FUNC_ARG; +} + /* the arg is whether the client has ech enabled or not */ static int test_ech_server_sni_callback(WOLFSSL* ssl, int* ad, void* arg) { @@ -14036,9 +14129,14 @@ static int test_ech_server_sni_callback(WOLFSSL* ssl, int* ad, void* arg) /* reached by *_disable_conn test: expect name to be the public SNI when * client has ECH enabled, otherwise it should be the private SNI */ - if (arg != NULL && *(int*)arg == 1 && - XSTRCMP(name, echCbTestPublicName) == 0) { - return 0; + if (arg != NULL && *(int*)arg == 1) { + if (XSTRCMP(name, echCbTestPublicName) == 0) { + return 0; + } + else { + *ad = WOLFSSL_AD_UNRECOGNIZED_NAME; + return fatal_return; + } } else if (XSTRCMP(name, echCbTestPrivateName) == 0) { return 0; @@ -14262,14 +14360,19 @@ static int test_wolfSSL_Tls13_ECH_no_private_name(void) ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + /* SNI is permissive by default, force a failure when SNI is absent */ + wolfSSL_SNI_SetOptions(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + WOLFSSL_SNI_ABORT_ON_ABSENCE); + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echCbTestConfigs, echCbTestConfigsLen), WOLFSSL_SUCCESS); ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + /* server fails before sending response to client */ ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), - WOLFSSL_ECH_STATUS_NOT_OFFERED); + WOLFSSL_ECH_STATUS_REJECTED); ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), - WOLFSSL_ECH_STATUS_NOT_OFFERED); + WOLFSSL_ECH_STATUS_ACCEPTED); test_ssl_memio_cleanup(&test_ctx); @@ -14287,11 +14390,13 @@ static int test_wolfSSL_Tls13_ECH_no_private_name(void) ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echCbTestConfigs, echCbTestConfigsLen), WOLFSSL_SUCCESS); - ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + /* it is odd to have no private SNI but it's not necessarily an issue, + * there are other methods that could be used to route the connection */ + ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), - WOLFSSL_ECH_STATUS_NOT_OFFERED); + WOLFSSL_ECH_STATUS_ACCEPTED); ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), - WOLFSSL_ECH_STATUS_NOT_OFFERED); + WOLFSSL_ECH_STATUS_ACCEPTED); test_ssl_memio_cleanup(&test_ctx); @@ -14398,6 +14503,42 @@ static int test_wolfSSL_Tls13_ECH_bad_configs_ex(int hrr, int sniCb) test_ssl_memio_cleanup(&test_ctx); + + /* verify with double public SNI */ + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* set public SNI for private SNI on client */ + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echCbTestConfigs, + echCbTestConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + echCbTestPublicName, (word16)XSTRLEN(echCbTestPublicName)), + WOLFSSL_SUCCESS); + + if (hrr) { + ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); + } + if (sniCb) { + wolfSSL_CTX_set_servername_callback(test_ctx.s_ctx, + test_ech_server_sni_callback); + } + + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_REJECTED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); + + test_ssl_memio_cleanup(&test_ctx); + return EXPECT_RESULT(); } @@ -14895,6 +15036,135 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) return EXPECT_RESULT(); } +/* The public name must be visible and the private name must not be visible */ +static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int accept) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + word16 idx; + word16 nameLen; + word16 publicLen = (word16)XSTRLEN(echCbTestPublicName); + const char* expectedSni = + accept ? echCbTestPrivateName : echCbTestPublicName; + void* sniName = NULL; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; + /* Accept path uses the correct configs */ + if (accept) + test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* Reject path installs bad configs (with the correct public name) */ + if (!accept) { + WOLFSSL_CTX* tempCtx = NULL; + byte badConfig[128]; + word32 badConfigLen = sizeof(badConfig); + + ExpectNotNull(tempCtx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(tempCtx, echCbTestPublicName, + 0, 0, 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(tempCtx, badConfig, + &badConfigLen), WOLFSSL_SUCCESS); + wolfSSL_CTX_free(tempCtx); + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, badConfig, + badConfigLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + WOLFSSL_SUCCESS); + } + + /* force HelloRetryRequest */ + ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); + + /* On reject, client aborts with ech_required and won't send a cert. */ + if (!accept) { + wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL); + } + + /* client writes CH1 into s_buff */ + ExpectIntEQ(wolfSSL_connect(test_ctx.c_ssl), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + /* check sent SNI is correct */ + idx = RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ; + ExpectIntEQ(ech_find_extension(test_ctx.s_buff, &idx, TLSXT_SERVER_NAME), + 0); + ExpectIntEQ(test_ctx.s_buff[idx + 6], WOLFSSL_SNI_HOST_NAME); + ato16(test_ctx.s_buff + idx + 7, &nameLen); + ExpectIntEQ(nameLen, publicLen); + ExpectIntEQ(XMEMCMP(test_ctx.s_buff + idx + 9, echCbTestPublicName, + publicLen), 0); + + /* server consumes CH1 and writes HRR into c_buff */ + ExpectIntEQ(wolfSSL_accept(test_ctx.s_ssl), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(test_ctx.s_ssl->options.serverState, + SERVER_HELLO_RETRY_REQUEST_COMPLETE); + + /* client reads HRR from c_buff and writes CH2 into s_buff */ + ExpectIntEQ(wolfSSL_connect(test_ctx.c_ssl), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + /* check sent SNI is correct */ + idx = RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ; + ExpectIntEQ(ech_find_extension(test_ctx.s_buff, &idx, TLSXT_SERVER_NAME), + 0); + ExpectIntEQ(test_ctx.s_buff[idx + 6], WOLFSSL_SNI_HOST_NAME); + ato16(test_ctx.s_buff + idx + 7, &nameLen); + ExpectIntEQ(nameLen, publicLen); + ExpectIntEQ(XMEMCMP(test_ctx.s_buff + idx + 9, echCbTestPublicName, + publicLen), 0); + + /* sanity check: finish the handshake and verify ECH acceptance */ + if (accept) { + ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); + } + else { + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_REJECTED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_REJECTED); + } + + /* verify the correct SNI is authoritative */ + ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, accept); + wolfSSL_SNI_GetRequest(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, &sniName); + ExpectStrEQ((const char*)sniName, expectedSni); + sniName = NULL; + wolfSSL_SNI_GetRequest(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, &sniName); + ExpectStrEQ((const char*)sniName, expectedSni); + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + +static int test_wolfSSL_Tls13_ECH_wire_sni(void) +{ + EXPECT_DECLS; + ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(0), TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(1), TEST_SUCCESS); + return EXPECT_RESULT(); +} + static int test_wolfSSL_Tls13_ECH_disable_conn_ex(int enableServer, int enableClient) { @@ -14962,103 +15232,6 @@ static int test_wolfSSL_Tls13_ECH_disable_conn(void) return EXPECT_RESULT(); } -/* Regression test: an inner SNI hostname >= MAX_PUBLIC_NAME_SZ (256) bytes - * must not cause a stack-buffer-overflow in TLSX_EchRestoreSNI. Before the - * fix, the truncated copy omitted the NUL terminator and XSTRLEN read past - * the buffer. */ -static int test_wolfSSL_Tls13_ECH_long_SNI(void) -{ - EXPECT_DECLS; -#if !defined(NO_WOLFSSL_CLIENT) - test_ssl_memio_ctx test_ctx; - /* 300 chars > MAX_PUBLIC_NAME_SZ (256) to exercise truncation */ - char longName[300]; - - XMEMSET(longName, 'a', sizeof(longName) - 1); - longName[sizeof(longName) - 1] = '\0'; - - XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - - test_ctx.s_cb.method = wolfTLSv1_3_server_method; - test_ctx.c_cb.method = wolfTLSv1_3_client_method; - - test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; - test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; - - ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); - - /* Set ECH configs on the client */ - ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echCbTestConfigs, - echCbTestConfigsLen), WOLFSSL_SUCCESS); - - /* Try to set the over-long SNI as the inner hostname -- after the fix, this - * is expected to fail. - */ - ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, - longName, (word16)XSTRLEN(longName)), BAD_LENGTH_E); - - /* Before the fix, the handshake would trigger TLSX_EchChangeSNI / - * TLSX_EchRestoreSNI, which would then stack-buffer-overflow in XSTRLEN. - */ - (void)test_ssl_memio_do_handshake(&test_ctx, 10, NULL); - - test_ssl_memio_cleanup(&test_ctx); -#endif /* !NO_WOLFSSL_CLIENT */ - - return EXPECT_RESULT(); -} - -static int ech_seek_extensions(byte* buf, word16* innerExtLen) -{ - word16 idx; - byte sessionIdLen; - word16 cipherSuitesLen; - byte compressionLen; - - idx = OPAQUE16_LEN + RAN_LEN; - - sessionIdLen = buf[idx++]; - idx += sessionIdLen; - - ato16(buf + idx, &cipherSuitesLen); - idx += OPAQUE16_LEN + cipherSuitesLen; - - compressionLen = buf[idx++]; - idx += compressionLen; - - ato16(buf + idx, innerExtLen); - idx += OPAQUE16_LEN; - - return idx; -} - -static int ech_find_extension(byte* buf, word16* idx_p, word16 extType) -{ - word16 idx; - word16 innerExtIdx; - word16 innerExtLen; - - innerExtIdx = ech_seek_extensions(buf + *idx_p, &innerExtLen) + *idx_p; - idx = innerExtIdx; - - while (idx - innerExtIdx < innerExtLen) { - word16 type; - word16 len; - - ato16(buf + idx, &type); - if (type == extType) { - *idx_p = idx; - return 0; - } - - idx += OPAQUE16_LEN; - ato16(buf + idx, &len); - idx += OPAQUE16_LEN + len; - } - - return BAD_FUNC_ARG; -} - /* Test the HRR ECH rejection fallback path: * client offers ECH, HRR is triggered, server sends HRR without ECH extension, * client falls back to the outer transcript, then aborts with ech_required. @@ -35111,8 +35284,8 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_Tls13_ECH_new_config), TEST_DECL(test_wolfSSL_Tls13_ECH_trial_decrypt), TEST_DECL(test_wolfSSL_Tls13_ECH_GREASE), + TEST_DECL(test_wolfSSL_Tls13_ECH_wire_sni), TEST_DECL(test_wolfSSL_Tls13_ECH_disable_conn), - TEST_DECL(test_wolfSSL_Tls13_ECH_long_SNI), TEST_DECL(test_wolfSSL_Tls13_ECH_HRR_rejection), TEST_DECL(test_wolfSSL_Tls13_ECH_ch2_no_ech), TEST_DECL(test_wolfSSL_Tls13_ECH_ch2_decrypt_error), diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 8b3231227b..426caf3e3d 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3129,13 +3129,6 @@ typedef enum { ECH_PARSED_INTERNAL, } EchState; -typedef enum { - ECH_OUTER_SNI, - ECH_INNER_SNI, - ECH_INNER_SNI_ATTEMPT, - ECH_SNI_DONE, -} EchStateSNI; - typedef struct EchCipherSuite { word16 kdfId; word16 aeadId; @@ -3159,11 +3152,12 @@ typedef struct WOLFSSL_ECH { Hpke* hpke; HpkeBaseContext* hpkeContext; const byte* aad; - const char* privateName; void* ephemeralKey; WOLFSSL_EchConfig* echConfig; byte* innerClientHello; byte* outerClientPayload; + /* the 'public' extensions (i.e., the public SNI would be stored here) */ + TLSX* extensions; byte* confBuf; EchCipherSuite cipherSuite; word32 aadLen; @@ -3172,7 +3166,6 @@ typedef struct WOLFSSL_ECH { word16 kemId; word16 encLen; EchState state; - EchStateSNI sniState; byte type; byte configId; byte enc[HPKE_Npk_MAX]; @@ -3185,6 +3178,8 @@ WOLFSSL_LOCAL int EchConfigGetSupportedCipherSuite(WOLFSSL_EchConfig* config); WOLFSSL_LOCAL int TLSX_FinalizeEch(WOLFSSL* ssl, WOLFSSL_ECH* ech, byte* aad, word32 aadLen); +WOLFSSL_LOCAL void TLSX_EchReplaceExtensions(WOLFSSL* ssl, byte accepted); + WOLFSSL_LOCAL int SetEchConfigsEx(WOLFSSL_EchConfig** outputConfigs, void* heap, const byte* echConfigs, word32 echConfigsLen);