diff --git a/.github/scripts/openssl-ech.sh b/.github/scripts/openssl-ech.sh index ca669de450..66c8bac1ad 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,16 @@ 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" +# Will need to look into validating the name against the cert for the OSSL cli. +# This is fine, but should be upgraded to use a second cert in the future. PRIV_NAME="ech-private-name.com" -PUB_NAME="ech-public-name.com" +# example.com is taken from the server certificate, +# echConfigs needs to authenticate against the cert with this name to succeed +PUB_NAME="example.com" MAX_WAIT=50 # -------------------------------------------------------------------------- @@ -128,6 +140,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 +172,24 @@ openssl_server(){ done echo "parsed port: $port" &>> "$TMP_LOG" + rm -f "$ech_file" + # test with wolfssl client + # in reject mode the client is expected to error out, so tolerate a + # nonzero exit $WOLFSSL_CLIENT -v 4 \ -p "$port" \ -S "$PRIV_NAME" \ --ech "$ech_config" \ $wolfssl_extra \ - &>> "$TMP_LOG" + &>> "$TMP_LOG" || [ "$REJECT" -ne 0 ] - rm -f "$ech_file" - - grep -q "ech_success=1" "$TMP_LOG" + if [ "$REJECT" -ne 0 ]; then + grep -q "ECH offered but rejected by server" "$TMP_LOG" + grep -q "ech_success=0" "$TMP_LOG" + else + grep -q "ech_success=1" "$TMP_LOG" + fi } # -------------------------------------------------------------------------- @@ -246,9 +267,13 @@ 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 + # in reject mode the s_client is expected to error out, so tolerate a + # nonzero exit echo "wolfssl" | $OPENSSL s_client \ -tls1_3 \ -connect "localhost:$port" \ @@ -258,9 +283,13 @@ openssl_client(){ -servername "$PRIV_NAME" \ -ech_config_list "$ech_config" \ $openssl_groups \ - &>> "$TMP_LOG" + &>> "$TMP_LOG" || [ "$REJECT" -ne 0 ] - grep -q "ECH: success: 1" "$TMP_LOG" + if [ "$REJECT" -ne 0 ]; then + grep -q "ECH: Got 1 retry-configs" "$TMP_LOG" + else + 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 501e57ad28..4593a02bf7 100644 --- a/src/tls.c +++ b/src/tls.c @@ -1737,15 +1737,24 @@ int TLSX_HandleUnsupportedExtension(WOLFSSL* ssl) #endif #if !defined(NO_WOLFSSL_SERVER) || defined(WOLFSSL_TLS13) -void TLSX_SetResponse(WOLFSSL* ssl, TLSX_Type type); -/** Mark an extension to be sent back to the client. */ -void TLSX_SetResponse(WOLFSSL* ssl, TLSX_Type type) +void TLSX_SetResponseInList(TLSX* list, TLSX_Type type); +/** Mark an extension to be sent back to the client. + * Operates on a list instead of the ssl. + * (Should only be used on ssl->extensions or ech->extensions) */ +void TLSX_SetResponseInList(TLSX* list, TLSX_Type type) { - TLSX *extension = TLSX_Find(ssl->extensions, type); + TLSX *extension = TLSX_Find(list, type); if (extension) extension->resp = 1; } + +void TLSX_SetResponse(WOLFSSL* ssl, TLSX_Type type); +/** Mark an extension to be sent back to the client. */ +void TLSX_SetResponse(WOLFSSL* ssl, TLSX_Type type) +{ + TLSX_SetResponseInList(ssl->extensions, type); +} #endif /******************************************************************************/ @@ -2384,14 +2393,15 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, word16 size = 0; word16 offset = 0; int cacheOnly = 0; - SNI *sni = NULL; + int checkPublic = 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); @@ -2423,22 +2433,7 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, } #ifndef NO_WOLFSSL_SERVER -#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) - if (!ssl->options.disableECH) { - echX = TLSX_Find(ssl->extensions, TLSX_ECH); - if (echX != NULL) { - ech = (WOLFSSL_ECH*)(echX->data); - } - } -#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. @@ -2454,8 +2449,20 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, WOLFSSL_MSG("Forcing SSL object to store SNI parameter"); } else { - /* Skipping, SNI not enabled at server side. */ - return 0; +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) + /* No server SNI configured: when ECH is active the outer SNI still + * needs to be parsed so it can be matched against the echConfig + * publicName and recorded on ech->extensions. */ + if (!ssl->options.disableECH && !ssl->options.echProcessingInner && + TLSX_Find(ssl->extensions, TLSX_ECH) != NULL) { + checkPublic = 1; + } + if (!checkPublic) +#endif + { + /* Skipping, SNI not enabled at server side. */ + return 0; + } } } @@ -2483,31 +2490,10 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, if (offset + size != length || size == 0) return BUFFER_ERROR; - if (!cacheOnly && !(sni = TLSX_SNI_Find((SNI*)extension->data, type))) + if (!cacheOnly && !checkPublic && + !(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. @@ -2516,42 +2502,62 @@ 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 && !ssl->options.disableECH && + !ssl->options.echProcessingInner) { + echX = TLSX_Find(ssl->extensions, TLSX_ECH); + if (echX != NULL) { + ech = (WOLFSSL_ECH*)(echX->data); + } + } + if (ech != NULL) { workingConfig = ech->echConfig; while (workingConfig != NULL) { - matched = XSTRLEN(workingConfig->publicName) == size && - XSTRNCMP(workingConfig->publicName, - (const char*)input + offset, size) == 0; - - if (matched) + if (XSTRLEN(workingConfig->publicName) == size && + XSTRNCMP(workingConfig->publicName, + (const char*)input + offset, size) == 0) { + matched = 1; break; - + } workingConfig = workingConfig->next; } + + /* If a publicName is matched then this SNI is not something that should + * be forcibly cached. This allows an SNI response to be given for the + * public name */ + if (matched) + cacheOnly = 0; } #endif + if (!matched) + matched = cacheOnly; + + /* No server SNI configured and the outer name did not match a publicName: + * stay permissive and record nothing. If ECH is accepted, the absent + * publicName match is caught after the outer parse. */ + if (!matched && checkPublic) + return 0; + 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) + /* install onto ech->extensions if the public name was matched */ + 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. */ @@ -2569,10 +2575,10 @@ 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) - TLSX_SetResponse(ssl, TLSX_SERVER_NAME); + TLSX_SetResponseInList(*writeList, TLSX_SERVER_NAME); } else if ((sni == NULL) || !(sni->options & WOLFSSL_SNI_CONTINUE_ON_MISMATCH)) { @@ -2692,8 +2698,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) @@ -2713,7 +2719,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) { @@ -13879,19 +13887,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); } @@ -14943,9 +14958,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); @@ -14954,8 +14968,6 @@ 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); XFREE(ech, heap, DYNAMIC_TYPE_TMP_BUFFER); (void)heap; @@ -15064,6 +15076,10 @@ int TLSX_FinalizeEch(WOLFSSL* ssl, WOLFSSL_ECH* ech, byte* aad, word32 aadLen) void TLSX_FreeAll(TLSX* list, void* heap) { TLSX* extension; +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) + TLSX* echList; + TLSX* tail; +#endif while ((extension = list)) { list = extension->next; @@ -15235,6 +15251,20 @@ void TLSX_FreeAll(TLSX* list, void* heap) #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) case TLSX_ECH: WOLFSSL_MSG("ECH extension free"); + /* append the ech extensions to the tail of the list so a + * recursive TLSX_FreeAll is not necessary */ + echList = ((WOLFSSL_ECH*)extension->data)->extensions; + if (echList != NULL) { + if (list == NULL) { + list = echList; + } + else { + tail = list; + while (tail->next != NULL) + tail = tail->next; + tail->next = echList; + } + } ECH_FREE((WOLFSSL_ECH*)extension->data, heap); break; #endif @@ -16702,95 +16732,126 @@ 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; + return ech != NULL && ech->type == ECH_TYPE_OUTER; +} - /* calculate the rest of the extensions length with inner ech */ - if (ssl->extensions) - echX = TLSX_Find(ssl->extensions, TLSX_ECH); +/* Swap matching extension types between *sslExts and *echExts. + * Non-matched extensions in *echExts are appended to the tail of *sslExts + * popCount is the number of trailing extensions to move from + * *sslExts back to *echExts (the undo of a prior append) + * + * Extensions are stored in reverse wire order, so non-matched extensions are + * appended to the tail rather than the head; this avoids displacing the leading + * extension (e.g. pre_shared_key, which must stay last on the wire). + * + * Returns a count of extensions appended to sslExts. */ +WOLFSSL_TEST_VIS word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, + word16 popCount) +{ + TLSX* chunk = NULL; + TLSX* node; + TLSX* outer; + TLSX* inner; + TLSX** outerLink; + TLSX** innerLink; + TLSX** sslTail; + word16 len = 0; + word16 appended = 0; + + if (popCount > 0) { + for (node = *sslExts; node != NULL; node = node->next) + len++; + sslTail = sslExts; + while (len > popCount) { + sslTail = &(*sslTail)->next; + len--; + } + chunk = *sslTail; + *sslTail = NULL; + } - 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); + outerLink = echExts; + while (*outerLink != NULL) { + innerLink = sslExts; + outer = *outerLink; - /* 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); + while (*innerLink != NULL && (*innerLink)->type != outer->type) + innerLink = &(*innerLink)->next; - if (serverNameX != NULL) - extensions = &ssl->extensions; - } + if (*innerLink != NULL) { + inner = *innerLink; - if (serverNameX == NULL && ssl->ctx && ssl->ctx->extensions) { - serverNameX = TLSX_Find(ssl->ctx->extensions, TLSX_SERVER_NAME); - if (serverNameX != NULL) - extensions = &ssl->ctx->extensions; - } + *innerLink = outer; + *outerLink = inner; + node = outer->next; + outer->next = inner->next; + inner->next = node; - /* 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; + outerLink = &inner->next; } + else { + *outerLink = outer->next; + *innerLink = outer; + outer->next = NULL; + appended++; + } + } - /* store the inner server name */ - if (ret == 0 && serverNameX != NULL) { - char* hostName = ((SNI*)serverNameX->data)->data.host_name; - word32 hostNameSz = (word32)XSTRLEN(hostName) + 1; + /* outerLink is at the tail of *echExts; append the chunk */ + *outerLink = chunk; - if (hostNameSz > WOLFSSL_HOST_NAME_MAX) - ret = BAD_LENGTH_E; - else - XMEMCPY(serverName, hostName, hostNameSz); - } + return appended; +} - /* 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); +/* returns 1 if extensions were concealed, 0 if not */ +static int TLSX_EchConcealExtensions(WOLFSSL* ssl, WOLFSSL_ECH* ech, + word16* appended) +{ + if (TLSX_EchShouldHideInner(ech)) { + *appended = TLSX_EchSwapExtensions(&ssl->extensions, + &ech->extensions, 0); + return 1; + } + return 0; +} - /* 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; +/* returns ret on success, or BAD_STATE_E on failure */ +static int TLSX_EchExposeExtensions(WOLFSSL* ssl, WOLFSSL_ECH* ech, + word16 appended, int installed, int ret) +{ + if (installed) { + appended = TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, + appended); + if (ret == 0 && appended != 0) { + WOLFSSL_MSG("Bad restore with TLSX_EchSwapExtensions"); + ret = BAD_STATE_E; } } - *pServerNameX = serverNameX; - *pExtensions = extensions; - *pEchX = echX; + return ret; } -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, + * appending to the tail 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, @@ -16900,39 +16961,32 @@ 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; + int installed; 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 appended = 0; + if (ssl->extensions) + echX = TLSX_Find(ssl->extensions, TLSX_ECH); if (echX != NULL) ech = (WOLFSSL_ECH*)echX->data; + installed = TLSX_EchConcealExtensions(ssl, ech, &appended); + /* 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; + ret = TLSX_EchExposeExtensions(ssl, ech, appended, installed, ret); return ret; } #endif @@ -17062,19 +17116,20 @@ 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; + int installed = 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 appended = 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; + + installed = TLSX_EchConcealExtensions(ssl, ech, &appended); + + if (echX != NULL) { /* turn ech on so it doesn't write, then write it last */ TURN_ON(semaphore, TLSX_ToSemaphore(echX->type)); } @@ -17126,7 +17181,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); } @@ -17137,12 +17192,7 @@ 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; + ret = TLSX_EchExposeExtensions(ssl, ech, appended, installed, ret); return ret; } #endif @@ -18702,6 +18752,60 @@ WOLFSSL_TEST_VIS int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, } #endif +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) + /* Reconcile ECH inner/outer extensions before verifying SNI so the verify + * pass sees the authoritative list */ + if (ret == 0 && msgType == client_hello && isRequest && + !ssl->options.echProcessingInner && + ssl->ctx->echConfigs != NULL && !ssl->options.disableECH) { + TLSX* echX = TLSX_Find(ssl->extensions, TLSX_ECH); + WOLFSSL_ECH* ech = NULL; + if (echX != NULL) + ech = (WOLFSSL_ECH*)echX->data; + + if (ech != NULL) { + if (ech->state == ECH_WRITE_NONE && ech->innerClientHello != NULL) { + /* The outer ClientHello must have carried the echConfig + * publicName as its SNI */ + if (ssl->options.serverState < + SERVER_HELLO_RETRY_REQUEST_COMPLETE && + TLSX_Find(ech->extensions, TLSX_SERVER_NAME) == NULL) { + WOLFSSL_MSG("ECH: outer ClientHello did not carry the " + "public name SNI"); + WOLFSSL_ERROR_VERBOSE(INVALID_PARAMETER); + return INVALID_PARAMETER; + } + /* ECH accepted: use private extensions */ + TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); + + /* return early, this is intentional since the inner hello + * needs to be parsed before doing the VERIFY's */ + return 0; + } + else { + /* 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) { + WOLFSSL_MSG("Client did not send an EncryptedClientHello " + "extension"); + WOLFSSL_ERROR_VERBOSE(INCOMPLETE_DATA); + return INCOMPLETE_DATA; + } + /* Otherwise ECH rejected: use public extensions */ + if (ech->state == ECH_WRITE_NONE || + ech->state == ECH_WRITE_RETRY_CONFIGS) { + TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); + if (ech->state == ECH_WRITE_NONE) { + echX->resp = 0; + } + } + } + } + } +#endif + if (ret == 0) ret = SNI_VERIFY_PARSE(ssl, isRequest); if (ret == 0) diff --git a/src/tls13.c b/src/tls13.c index 18ca7c5dcd..9f156d8935 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,13 @@ 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 (nameLen == 0 && ssl->ctx != NULL) + nameLen = TLSX_SNI_GetRequest(ssl->ctx->extensions, + WOLFSSL_SNI_HOST_NAME, &hostName, 1); + + if (nameLen != 0) { if (nameLen > args->ech->echConfig->maxNameLen) args->ech->paddingLen = 0; else @@ -4862,7 +4869,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 +5307,13 @@ 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 (ret == 0 && + (msgType != hello_retry_request || !ssl->options.echAccepted)) + TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); + return ret; } #endif /* HAVE_ECH */ @@ -5865,6 +5879,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 */ @@ -7680,28 +7696,13 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, #endif #if defined(HAVE_ECH) + /* ECH accept/reject reconciliation is done at the end of TLSX_Parse. On + * acceptance the inner hello was decrypted, so jump to exit and let the + * caller re-invoke with the inner hello. */ if (!ssl->options.echProcessingInner && echX != NULL && - ((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE) { - if (((WOLFSSL_ECH*)echX->data)->innerClientHello != NULL) { - /* 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 (ssl->options.serverState == - SERVER_HELLO_RETRY_REQUEST_COMPLETE && - ssl->options.echAccepted) { - WOLFSSL_MSG("Client did not send an EncryptedClientHello " - "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; - } + ((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE && + ((WOLFSSL_ECH*)echX->data)->innerClientHello != NULL) { + goto exit_dch; } #endif @@ -13778,18 +13779,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 e81fee2b5d..74d3ae9471 100644 --- a/tests/api.c +++ b/tests/api.c @@ -7920,17 +7920,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); @@ -13647,6 +13667,233 @@ static int test_wolfSSL_Tls13_Key_Logging_ech_rejected(void) } #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) + +#define TEST_ECH_LIST_SIZE(list) ((int)(sizeof((list)) / sizeof((list)[0]))) +#define TEST_ECH_SWAP_MAX 5 +#define TEST_ECH_TLSX_SSL 0 +#define TEST_ECH_TLSX_ECH 1 +#define TEST_ECH_TLSX_MIX 2 +#define TEST_ECH_TLSX_A TLSX_SUPPORTED_GROUPS +#define TEST_ECH_TLSX_B TLSX_KEY_SHARE +#define TEST_ECH_TLSX_C TLSX_COOKIE +#define TEST_ECH_TLSX_D TLSX_EC_POINT_FORMATS +#define TEST_ECH_TLSX_E TLSX_SUPPORTED_VERSIONS + +/* Init count items in storage to the corresponding item in types, + * mark each storage item to identify it as SSL or ECH */ +static void test_TLSX_EchSwapExtensions_init(TLSX* storage, const word16* types, + int count, word32 mark) +{ + while (count > 0) { + storage->type = (TLSX_Type)*types; + storage->data = NULL; + storage->val = mark; + storage->resp = 0; + storage->next = storage + 1; + + storage++; + types++; + count--; + } + + storage--; + storage->next = NULL; +} + +/* Confirm every extension in echList is present in sslList carrying the ECH + * mark (TLSX.val == TEST_ECH_TLSX_ECH) + * The types must also match what is present in sslSwap */ +static int test_TLSX_EchSwapExtensions_eqEch(TLSX* sslList, const TLSX* sslSwap, + const TLSX* echGoal) +{ + const TLSX* found; + const TLSX* echList = echGoal; + + while (echList != NULL) { + found = TLSX_Find(sslList, echList->type); + if (found == NULL || found->val != TEST_ECH_TLSX_ECH) + return 0; + echList = echList->next; + } + + while (sslList != NULL && sslSwap != NULL) { + if (sslList->type != sslSwap->type) + return 0; + sslList = sslList->next; + sslSwap = sslSwap->next; + } + + return (sslList == NULL) && (sslSwap == NULL); +} + +/* Confirm the lists are identical over: type and mark (SSL, ECH) */ +static int test_TLSX_EchSwapExtensions_eqAll(const TLSX* a, const TLSX* b) +{ + while (a != NULL && b != NULL) { + if (a->type != b->type || a->val != b->val) + return 0; + a = a->next; + b = b->next; + } + + return (a == NULL) && (b == NULL); +} + +/* Swap and check the result matches *Swap, swap again and check against *Goal. + * The TLSX.val marks are checked to prove the extensions changed lists. */ +static int test_TLSX_EchSwapExtensions_case(TLSX* sslExts, TLSX* echExts, + const TLSX* sslSwap, const TLSX* echSwap, const TLSX* sslGoal, + const TLSX* echGoal) +{ + EXPECT_DECLS; + word16 prepended; + + /* swap ech extensions into the ssl list */ + prepended = TLSX_EchSwapExtensions(&sslExts, &echExts, 0); + /* every ech extension is now in ssl and must be marked with ECH, + * similarly the displaced ssl extensions are in ech marked with SSL */ + ExpectIntEQ(test_TLSX_EchSwapExtensions_eqEch(sslExts, sslSwap, echGoal), 1); + ExpectIntEQ(test_TLSX_EchSwapExtensions_eqAll(echExts, echSwap), 1); + + /* swapping the prepended count back restores both lists and their marks */ + if (EXPECT_SUCCESS()) { + prepended = TLSX_EchSwapExtensions(&sslExts, &echExts, prepended); + ExpectIntEQ(prepended, 0); + ExpectIntEQ(test_TLSX_EchSwapExtensions_eqAll(sslExts, sslGoal), 1); + ExpectIntEQ(test_TLSX_EchSwapExtensions_eqAll(echExts, echGoal), 1); + } + + return EXPECT_RESULT(); +} + +static int test_TLSX_EchSwapExtensions(void) +{ + EXPECT_DECLS; + TLSX sslExts[TEST_ECH_SWAP_MAX]; + TLSX echExts[TEST_ECH_SWAP_MAX]; + TLSX sslSwap[TEST_ECH_SWAP_MAX]; + TLSX echSwap[TEST_ECH_SWAP_MAX]; + TLSX sslGoal[TEST_ECH_SWAP_MAX]; + TLSX echGoal[TEST_ECH_SWAP_MAX]; + static const word16 ssl3[] = { TEST_ECH_TLSX_A, TEST_ECH_TLSX_B, + TEST_ECH_TLSX_C }; + /* everything matches */ + static const word16 echMatch[] = { TEST_ECH_TLSX_B, TEST_ECH_TLSX_C }; + /* nothing matches */ + static const word16 echAppend[] = { TEST_ECH_TLSX_D, TEST_ECH_TLSX_E }; + static const word16 appendSwap[] = { TEST_ECH_TLSX_A, TEST_ECH_TLSX_B, + TEST_ECH_TLSX_C, TEST_ECH_TLSX_D, TEST_ECH_TLSX_E }; + /* matches and non-matches */ + static const word16 echMix[] = { TEST_ECH_TLSX_C, TEST_ECH_TLSX_B, + TEST_ECH_TLSX_D, TEST_ECH_TLSX_E }; + static const word16 sslMixSwap[] = { TEST_ECH_TLSX_A, TEST_ECH_TLSX_B, + TEST_ECH_TLSX_C, TEST_ECH_TLSX_D, TEST_ECH_TLSX_E }; + static const word16 echMixSwap[] = { TEST_ECH_TLSX_C, TEST_ECH_TLSX_B }; + /* matches and non-matches, relative ordering must be preserved */ + static const word16 echUlt[] = { TEST_ECH_TLSX_B, TEST_ECH_TLSX_E, + TEST_ECH_TLSX_C, TEST_ECH_TLSX_D }; + static const word16 sslUltSwap[] = { TEST_ECH_TLSX_A, TEST_ECH_TLSX_B, + TEST_ECH_TLSX_C, TEST_ECH_TLSX_E, TEST_ECH_TLSX_D }; + static const word16 echUltSwap[] = { TEST_ECH_TLSX_B, TEST_ECH_TLSX_C }; + static const word16 echUltGoal[] = { TEST_ECH_TLSX_B, TEST_ECH_TLSX_C, + TEST_ECH_TLSX_E, TEST_ECH_TLSX_D }; + + /* empty ech: ssl is returned unchanged and the round trip is a no-op */ + test_TLSX_EchSwapExtensions_init(sslExts, ssl3, + TEST_ECH_LIST_SIZE(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(sslSwap, ssl3, + TEST_ECH_LIST_SIZE(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(sslGoal, ssl3, + TEST_ECH_LIST_SIZE(ssl3), TEST_ECH_TLSX_SSL); + + ExpectIntEQ(test_TLSX_EchSwapExtensions_case(sslExts, NULL, sslSwap, + NULL, sslGoal, NULL), TEST_SUCCESS); + + /* all matched: ssl keeps its order, ech keeps its order */ + if (EXPECT_SUCCESS()) { + test_TLSX_EchSwapExtensions_init(sslExts, ssl3, + TEST_ECH_LIST_SIZE(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echExts, echMatch, + TEST_ECH_LIST_SIZE(echMatch), TEST_ECH_TLSX_ECH); + test_TLSX_EchSwapExtensions_init(sslSwap, ssl3, + TEST_ECH_LIST_SIZE(ssl3), TEST_ECH_TLSX_MIX); + test_TLSX_EchSwapExtensions_init(echSwap, echMatch, + TEST_ECH_LIST_SIZE(echMatch), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echGoal, echMatch, + TEST_ECH_LIST_SIZE(echMatch), TEST_ECH_TLSX_ECH); + + ExpectIntEQ(test_TLSX_EchSwapExtensions_case(sslExts, echExts, sslSwap, + echSwap, sslGoal, echGoal), TEST_SUCCESS); + } + + /* all unmatched: ech extension's are appended to ssl and ech is emptied */ + if (EXPECT_SUCCESS()) { + test_TLSX_EchSwapExtensions_init(sslExts, ssl3, + TEST_ECH_LIST_SIZE(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echExts, echAppend, + TEST_ECH_LIST_SIZE(echAppend), TEST_ECH_TLSX_ECH); + test_TLSX_EchSwapExtensions_init(sslSwap, appendSwap, + TEST_ECH_LIST_SIZE(appendSwap), TEST_ECH_TLSX_MIX); + test_TLSX_EchSwapExtensions_init(echGoal, echAppend, + TEST_ECH_LIST_SIZE(echAppend), TEST_ECH_TLSX_ECH); + + ExpectIntEQ(test_TLSX_EchSwapExtensions_case(sslExts, echExts, sslSwap, + NULL, sslGoal, echGoal), TEST_SUCCESS); + } + + /* mixed: exact ordering of echExts will be maintained */ + if (EXPECT_SUCCESS()) { + test_TLSX_EchSwapExtensions_init(sslExts, ssl3, + TEST_ECH_LIST_SIZE(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echExts, echMix, + TEST_ECH_LIST_SIZE(echMix), TEST_ECH_TLSX_ECH); + test_TLSX_EchSwapExtensions_init(sslSwap, sslMixSwap, + TEST_ECH_LIST_SIZE(sslMixSwap), TEST_ECH_TLSX_MIX); + test_TLSX_EchSwapExtensions_init(echSwap, echMixSwap, + TEST_ECH_LIST_SIZE(echMixSwap), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echGoal, echMix, + TEST_ECH_LIST_SIZE(echMix), TEST_ECH_TLSX_ECH); + + ExpectIntEQ(test_TLSX_EchSwapExtensions_case(sslExts, echExts, sslSwap, + echSwap, sslGoal, echGoal), TEST_SUCCESS); + } + + /* ultimate: relative order of echExts will be maintained, + * successive calls must preserve exact ordering */ + if (EXPECT_SUCCESS()) { + test_TLSX_EchSwapExtensions_init(sslExts, ssl3, + TEST_ECH_LIST_SIZE(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echExts, echUlt, + TEST_ECH_LIST_SIZE(echUlt), TEST_ECH_TLSX_ECH); + test_TLSX_EchSwapExtensions_init(sslSwap, sslUltSwap, + TEST_ECH_LIST_SIZE(sslUltSwap), TEST_ECH_TLSX_MIX); + test_TLSX_EchSwapExtensions_init(echSwap, echUltSwap, + TEST_ECH_LIST_SIZE(echUltSwap), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echGoal, echUltGoal, + TEST_ECH_LIST_SIZE(echUltGoal), TEST_ECH_TLSX_ECH); + + /* absolute ordering changes */ + ExpectIntEQ(test_TLSX_EchSwapExtensions_case(sslExts, echExts, sslSwap, + echSwap, sslGoal, echGoal), TEST_SUCCESS); + /* absolute ordering remains (previous case mutates echExts) */ + ExpectIntEQ(test_TLSX_EchSwapExtensions_case(sslExts, echExts, sslSwap, + echSwap, sslGoal, echGoal), TEST_SUCCESS); + } + + return EXPECT_RESULT(); +} + +#undef TEST_ECH_LIST_SIZE +#undef TEST_ECH_SWAP_MAX +#undef TEST_ECH_TLSX_SSL +#undef TEST_ECH_TLSX_ECH +#undef TEST_ECH_TLSX_MIX +#undef TEST_ECH_TLSX_A +#undef TEST_ECH_TLSX_B +#undef TEST_ECH_TLSX_C +#undef TEST_ECH_TLSX_D +#undef TEST_ECH_TLSX_E + #if defined(HAVE_IO_TESTS_DEPENDENCIES) static int test_wolfSSL_Tls13_ECH_params(void) { @@ -13805,6 +14052,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 */ @@ -13872,6 +14121,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))); @@ -14059,6 +14322,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) { @@ -14071,9 +14391,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; @@ -14297,14 +14622,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); @@ -14322,11 +14652,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); @@ -14338,39 +14670,25 @@ static int test_wolfSSL_Tls13_ECH_bad_configs_ex(int hrr, int sniCb) { EXPECT_DECLS; struct test_ssl_memio_ctx test_ctx; - WOLFSSL_CTX* tempCtx = NULL; const char* badPrivateName = "ech-bad-private-name.com"; - byte badPublicConfig[128]; - word32 badPublicConfigLen = sizeof(badPublicConfig); - /* verify with bad public SNI / config */ + /* verify with bad private 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; - /* server generates its own ECH config */ 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); - /* generate throwaway ECH config for client to use */ - 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, badPublicConfig, - &badPublicConfigLen), WOLFSSL_SUCCESS); - wolfSSL_CTX_free(tempCtx); - tempCtx = NULL; - - /* set bad public config on client */ - ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, badPublicConfig, - badPublicConfigLen), WOLFSSL_SUCCESS); + /* set bad 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, - echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), - WOLFSSL_SUCCESS); + badPrivateName, (word16)XSTRLEN(badPrivateName)), WOLFSSL_SUCCESS); /* client will send empty cert on rejection, so server should not ask for * cert */ @@ -14387,13 +14705,14 @@ static int test_wolfSSL_Tls13_ECH_bad_configs_ex(int hrr, int sniCb) ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), WOLFSSL_ECH_STATUS_REJECTED); + /* server decrypts inner successfully but rejects SNI, thus the client does + * not receive the acceptance signal */ ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), - WOLFSSL_ECH_STATUS_REJECTED); + WOLFSSL_ECH_STATUS_ACCEPTED); test_ssl_memio_cleanup(&test_ctx); - - /* verify with bad private SNI */ + /* verify with double public SNI */ XMEMSET(&test_ctx, 0, sizeof(test_ctx)); @@ -14405,15 +14724,37 @@ static int test_wolfSSL_Tls13_ECH_bad_configs_ex(int hrr, int sniCb) ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); - /* set bad private SNI on client */ + /* 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, - badPrivateName, (word16)XSTRLEN(badPrivateName)), WOLFSSL_SUCCESS); + echCbTestPublicName, (word16)XSTRLEN(echCbTestPublicName)), + WOLFSSL_SUCCESS); - /* client will send empty cert on rejection, so server should not ask for - * cert */ - wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_NONE, NULL); + 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); + + /* verify with private and public swapped */ + + 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; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); if (hrr) { ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); @@ -14423,11 +14764,29 @@ static int test_wolfSSL_Tls13_ECH_bad_configs_ex(int hrr, int sniCb) test_ech_server_sni_callback); } + /* generate config with private name */ + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfigEx(test_ctx.s_ctx, + echCbTestPrivateName, echCbTestKemID, echCbTestKdfID, echCbTestAeadID, + XSTRLEN(echCbTestPrivateName)), WOLFSSL_SUCCESS); + if (EXPECT_SUCCESS()) { + echCbTestConfigsLen = sizeof(echCbTestConfigs); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(test_ctx.s_ctx, echCbTestConfigs, + &echCbTestConfigsLen), WOLFSSL_SUCCESS); + } + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + WOLFSSL_SUCCESS); + + /* use config and set public name as target */ + 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); + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), WOLFSSL_ECH_STATUS_REJECTED); - /* server decrypts inner successfully but rejects SNI, thus the client does - * not receive the acceptance signal */ ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), WOLFSSL_ECH_STATUS_ACCEPTED); @@ -14818,6 +15177,10 @@ static int test_wolfSSL_Tls13_ECH_trial_decrypt(void) /* opt into trial decryption on the SSL */ wolfSSL_SetEchEnableTrialDecrypt(test_ctx.s_ssl, 1); ExpectIntEQ(test_ctx.s_ssl->options.enableEchTrialDecrypt, 1); + /* Also verify the connection succeeds when 'answer on mismatch' is set. + * This is a secondary regression test */ + wolfSSL_SNI_SetOptions(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + WOLFSSL_SNI_ANSWER_ON_MISMATCH); /* alter the client's configId so it does not match the server's configId */ ExpectNotNull(test_ctx.c_ssl->echConfigs); @@ -14930,6 +15293,141 @@ 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); + } + else { + /* Also verify the connection succeeds when 'abort on absence' is set. + * This is a secondary regression test */ + wolfSSL_SNI_SetOptions(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + WOLFSSL_SNI_ABORT_ON_ABSENCE); + } + + /* 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) { @@ -14997,103 +15495,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. @@ -15416,6 +15817,55 @@ static int test_wolfSSL_Tls13_ECH_rejected_empty_client_cert(void) return EXPECT_RESULT(); } + +/* verify the server aborts a connection which provides a good ECH config but + * where the given public SNI does not match the known set + * + * If more leniency is desired in the future this test will need to change */ +static int test_wolfSSL_Tls13_ECH_outer_sni_mismatch(void) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + const char* mismatchName = "mismatch.io"; + + 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; + + /* Server offers ECH but configures no SNI of its own. A server-side SNI + * would make TLSX_SNI_Parse reject the outer name before the ECH + * reconciliation runs, so the public_name match is the only thing that + * could record an outer SNI on ech->extensions. */ + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* Change the outer SNI to a name the server does not advertise. Same length + * as echCbTestPublicName so only the public_name string changes. */ + ExpectIntEQ((int)XSTRLEN(mismatchName), (int)XSTRLEN(echCbTestPublicName)); + ExpectNotNull(test_ctx.c_ssl->echConfigs); + if (EXPECT_SUCCESS()) { + XMEMCPY(test_ctx.c_ssl->echConfigs->publicName, mismatchName, + XSTRLEN(mismatchName)); + } + + /* Inner decrypts, but the outer SNI does not match the public_name: the + * server aborts the handshake. */ + 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); + ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0), + WC_NO_ERR_TRACE(INVALID_PARAMETER)); + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} #endif /* HAVE_SSL_MEMIO_TESTS_DEPENDENCIES */ /* verify that ECH can be enabled/disabled without issue */ @@ -35220,6 +35670,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_tls_ext_word16_overflow), TEST_DECL(test_tls_bad_legacy_version), #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) + TEST_DECL(test_TLSX_EchSwapExtensions), #if defined(HAVE_IO_TESTS_DEPENDENCIES) TEST_DECL(test_wolfSSL_Tls13_ECH_params), TEST_DECL(test_wolfSSL_Tls13_ECH_params_b64), @@ -35238,13 +35689,14 @@ 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), TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_cert_valid), TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_empty_client_cert), + TEST_DECL(test_wolfSSL_Tls13_ECH_outer_sni_mismatch), #endif #if defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TEST_ECH) && \ !defined(WOLFSSL_NO_TLS12) diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 90856252b3..06c50e0ad8 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,13 @@ 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); + +#ifdef WOLFSSL_API_PREFIX_MAP + #define TLSX_EchSwapExtensions wolfSSL_TLSX_EchSwapExtensions +#endif +WOLFSSL_TEST_VIS word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, + word16 popCount); WOLFSSL_LOCAL int SetEchConfigsEx(WOLFSSL_EchConfig** outputConfigs, void* heap, const byte* echConfigs, word32 echConfigsLen);