From bc2fb233169a0d38e718d86befdf2eeb868a95fa Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Mon, 4 May 2026 10:04:50 -0600 Subject: [PATCH 1/5] ech public extension management --- src/tls.c | 250 ++++++++++++++++++++++++--------------------- src/tls13.c | 33 ++++-- wolfssl/internal.h | 4 + 3 files changed, 160 insertions(+), 127 deletions(-) diff --git a/src/tls.c b/src/tls.c index 411a97271e..a4a10a3f46 100644 --- a/src/tls.c +++ b/src/tls.c @@ -13848,14 +13848,20 @@ static int TLSX_ECH_Use(WOLFSSL_EchConfig* echConfig, TLSX** extensions, /* 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); } @@ -14907,9 +14913,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); @@ -14918,8 +14923,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; @@ -16666,95 +16670,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* ssl, 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 ssl->options.echAccepted || ech->innerCount == 0; +} - /* 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 appended = 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; + 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); + /* 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 appended; } -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 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, @@ -16864,39 +16868,43 @@ 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); + word16 appended = 0; + byte installed = 0; - WC_ALLOC_VAR_EX(serverName, char, WOLFSSL_HOST_NAME_MAX, NULL, - DYNAMIC_TYPE_TMP_BUFFER, return MEMORY_E); + if (ssl->extensions) + echX = TLSX_Find(ssl->extensions, TLSX_ECH); + if (echX == NULL && ssl->ctx && ssl->ctx->extensions) + echX = TLSX_Find(ssl->ctx->extensions, TLSX_ECH); + if (echX != NULL) + ech = (WOLFSSL_ECH*)echX->data; - r = TLSX_EchChangeSNI(ssl, &echX, serverName, &serverNameX, &extensions); + if (TLSX_EchShouldHideInner(ssl, ech)) { + appended = TLSX_EchSwapExtensions(&ssl->extensions, + &ech->extensions, 0); + installed = 1; + } if (echX != NULL) ech = (WOLFSSL_ECH*)echX->data; /* 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) + (void)TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, + appended); return ret; } #endif @@ -17026,19 +17034,26 @@ 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 appended = 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 && ssl->ctx && ssl->ctx->extensions) + echX = TLSX_Find(ssl->ctx->extensions, TLSX_ECH); + if (echX != NULL) ech = (WOLFSSL_ECH*)echX->data; + + if (TLSX_EchShouldHideInner(ssl, ech)) { + appended = 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)); } @@ -17090,7 +17105,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); } @@ -17101,12 +17116,9 @@ 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) + (void)TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, + appended); return ret; } #endif diff --git a/src/tls13.c b/src/tls13.c index 5e377f40ef..f318447413 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -5300,6 +5300,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 +5871,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 +7686,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 +7705,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 diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 80dcb24c70..7c458ce02a 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3164,6 +3164,8 @@ typedef struct WOLFSSL_ECH { 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; @@ -3185,6 +3187,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); From 96cb4d194299f99d6646205acea942e3c3f5f754 Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Mon, 4 May 2026 10:06:23 -0600 Subject: [PATCH 2/5] add public sni handling to public ext --- src/tls.c | 63 +++++----------- src/tls13.c | 6 -- tests/api.c | 183 +++++++++++++++++++++++++++++++++++++++++++-- wolfssl/internal.h | 9 --- 4 files changed, 192 insertions(+), 69 deletions(-) diff --git a/src/tls.c b/src/tls.c index a4a10a3f46..191d9382d2 100644 --- a/src/tls.c +++ b/src/tls.c @@ -2390,8 +2390,7 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, #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); @@ -2432,13 +2431,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. @@ -2486,28 +2479,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. @@ -2516,14 +2487,6 @@ 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 && @@ -2532,16 +2495,14 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, } #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) - if (!matched && ech != NULL && ech->sniState == ECH_OUTER_SNI) { + 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; } } @@ -2550,8 +2511,14 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, 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. */ @@ -2569,10 +2536,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) - TLSX_SetResponse(ssl, TLSX_SERVER_NAME); + if (!cacheOnly) { + extension = TLSX_Find(*writeList, TLSX_SERVER_NAME); + + if (extension) + extension->resp = 1; + } } else if ((sni == NULL) || !(sni->options & WOLFSSL_SNI_CONTINUE_ON_MISMATCH)) { diff --git a/src/tls13.c b/src/tls13.c index f318447413..fc0210bf42 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -13793,18 +13793,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 9cac424dca..422b506ab4 100644 --- a/tests/api.c +++ b/tests/api.c @@ -14071,9 +14071,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; @@ -14259,7 +14264,9 @@ static int test_wolfSSL_Tls13_ECH_all_algos(void) return EXPECT_RESULT(); } -/* Test ECH when no private SNI is set */ +/* Test ECH when no private SNI is set + * SNI is by default permissive so these should pass + * (inner SNI is not required by ECH, only the outer SNI is required) */ static int test_wolfSSL_Tls13_ECH_no_private_name(void) { EXPECT_DECLS; @@ -14302,9 +14309,9 @@ static int test_wolfSSL_Tls13_ECH_no_private_name(void) ExpectIntNE(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_REJECTED); ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), - WOLFSSL_ECH_STATUS_NOT_OFFERED); + WOLFSSL_ECH_STATUS_REJECTED); test_ssl_memio_cleanup(&test_ctx); @@ -14324,9 +14331,9 @@ static int test_wolfSSL_Tls13_ECH_no_private_name(void) ExpectIntNE(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_REJECTED); ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), - WOLFSSL_ECH_STATUS_NOT_OFFERED); + WOLFSSL_ECH_STATUS_REJECTED); test_ssl_memio_cleanup(&test_ctx); @@ -14433,6 +14440,39 @@ 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(test_ctx.c_ssl->options.echAccepted, 0); + + test_ssl_memio_cleanup(&test_ctx); + return EXPECT_RESULT(); } @@ -14983,6 +15023,132 @@ static int test_wolfSSL_Tls13_ECH_disable_conn_ex(int enableServer, return EXPECT_RESULT(); } +static const byte* test_find_bytes(const char* needle, + const byte* haystack, int hayLen) +{ + int needleLen = (int)XSTRLEN(needle); + int i; + if (hayLen < needleLen) + return NULL; + for (i = 0; i <= hayLen - needleLen; i++) { + if (XMEMCMP(haystack + i, needle, needleLen) == 0) + return haystack + i; + } + return NULL; +} + +/* The public name must be visible and the private name must not be visible */ +static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int hrr, int accept) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + WOLFSSL_CTX* tempCtx = NULL; + byte badConfig[128]; + word32 badConfigLen = sizeof(badConfig); + 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) { + 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); + } + + if (hrr) + 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); + + /* CH1 wire bytes */ + ExpectNotNull(test_find_bytes(echCbTestPublicName, test_ctx.s_buff, + test_ctx.s_len)); + ExpectNull(test_find_bytes(echCbTestPrivateName, test_ctx.s_buff, + test_ctx.s_len)); + + if (hrr) { + /* 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); + + /* CH2 wire bytes: same property must hold */ + ExpectNotNull(test_find_bytes(echCbTestPublicName, test_ctx.s_buff, + test_ctx.s_len)); + ExpectNull(test_find_bytes(echCbTestPrivateName, test_ctx.s_buff, + test_ctx.s_len)); + } + + /* drive remaining rounds and verify the correct SNI is authoritative */ + if (accept) { + ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + } + else { + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + } + + ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, accept ? 1 : 0); + 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, 0), TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(0, 1), TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(1, 0), TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(1, 1), TEST_SUCCESS); + return EXPECT_RESULT(); +} + /* setup a server and client with ECH then disable on one, the other, or both. * Verifies that disabling ECH prevents ECH from being used and that the * public/private SNI's are verified correctly */ @@ -35168,6 +35334,7 @@ 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), diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 7c458ce02a..0d1c186ad6 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,7 +3152,6 @@ typedef struct WOLFSSL_ECH { Hpke* hpke; HpkeBaseContext* hpkeContext; const byte* aad; - const char* privateName; void* ephemeralKey; WOLFSSL_EchConfig* echConfig; byte* innerClientHello; @@ -3174,7 +3166,6 @@ typedef struct WOLFSSL_ECH { word16 kemId; word16 encLen; EchState state; - EchStateSNI sniState; byte type; byte configId; byte enc[HPKE_Npk_MAX]; From bc7768c2282fb3715feb19fc43a2f7a6ec4edd2b Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Mon, 4 May 2026 13:58:34 -0600 Subject: [PATCH 3/5] minor fixes --- src/tls.c | 75 ++++++++++++++++++++++++++++++----------------------- src/tls13.c | 13 ++++++---- tests/api.c | 35 ++++++++++++++++++++----- 3 files changed, 79 insertions(+), 44 deletions(-) diff --git a/src/tls.c b/src/tls.c index 191d9382d2..c982dc695b 100644 --- a/src/tls.c +++ b/src/tls.c @@ -2385,6 +2385,7 @@ 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) @@ -2487,14 +2488,14 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, return 0; #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) + /* 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) { @@ -2505,9 +2506,17 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, 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; @@ -13814,6 +13823,7 @@ 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 */ @@ -16642,11 +16652,11 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) /* Returns 1 if the extensions should be hidden for this write */ -static int TLSX_EchShouldHideInner(WOLFSSL* ssl, WOLFSSL_ECH* ech) +static int TLSX_EchShouldHideInner(WOLFSSL_ECH* ech) { if (ech == NULL || ech->type != ECH_TYPE_OUTER) return 0; - return ssl->options.echAccepted || ech->innerCount == 0; + return 1; } /* Swap matching extension types between *sslExts and *echExts. @@ -16667,7 +16677,7 @@ static word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, TLSX* inner; TLSX** outerLink; TLSX** innerLink; - word16 appended = 0; + word16 prepended = 0; /* unhook popCount nodes off *sslExts head into chunk. * Head-prepend undoes the reversal caused by appending onto sslExts. */ @@ -16702,19 +16712,19 @@ static word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, *outerLink = outer->next; outer->next = *sslExts; *sslExts = outer; - appended++; + prepended++; } } /* outerLink is at the tail of *echExts; append the chunk */ *outerLink = chunk; - return appended; + return prepended; } /* If ECH is accepted, delete ech->extensions - * If rejected, replace matching ssl->extensions with ech->extensions, appending - * to head if necessary */ + * If rejected, replace matching ssl->extensions with ech->extensions, + * prepending to head if necessary */ void TLSX_EchReplaceExtensions(WOLFSSL* ssl, byte accepted) { TLSX* echX; @@ -16843,25 +16853,20 @@ static int TLSX_GetSizeWithEch(WOLFSSL* ssl, byte* semaphore, byte msgType, TLSX* echX = NULL; WOLFSSL_ECH* ech = NULL; word16 count = 0; - word16 appended = 0; + word16 prepended = 0; byte installed = 0; if (ssl->extensions) echX = TLSX_Find(ssl->extensions, TLSX_ECH); - if (echX == NULL && ssl->ctx && ssl->ctx->extensions) - echX = TLSX_Find(ssl->ctx->extensions, TLSX_ECH); if (echX != NULL) ech = (WOLFSSL_ECH*)echX->data; - if (TLSX_EchShouldHideInner(ssl, ech)) { - appended = TLSX_EchSwapExtensions(&ssl->extensions, + if (TLSX_EchShouldHideInner(ech)) { + prepended = TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, 0); installed = 1; } - if (echX != NULL) - ech = (WOLFSSL_ECH*)echX->data; - /* if encoding, then count encoded form of inner ClientHello. * `semaphore` is in/out so encodable extensions will later be ignored */ if (ech != NULL && ech->type == ECH_TYPE_INNER && ech->writeEncoded) { @@ -16873,9 +16878,13 @@ static int TLSX_GetSizeWithEch(WOLFSSL* ssl, byte* semaphore, byte msgType, if (ret == 0 && ssl->ctx && ssl->ctx->extensions) ret = TLSX_GetSize(ssl->ctx->extensions, semaphore, msgType, pLength); - if (installed) - (void)TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, - appended); + if (installed) { + prepended = TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, + prepended); + if (ret == 0 && prepended != 0) { + ret = BAD_STATE_E; + } + } return ret; } #endif @@ -17008,18 +17017,16 @@ static int TLSX_WriteWithEch(WOLFSSL* ssl, byte* output, byte* semaphore, int ret = 0; TLSX* echX = NULL; WOLFSSL_ECH* ech = NULL; - word16 appended = 0; + word16 prepended = 0; byte installed = 0; if (ssl->extensions) echX = TLSX_Find(ssl->extensions, TLSX_ECH); - if (echX == NULL && ssl->ctx && ssl->ctx->extensions) - echX = TLSX_Find(ssl->ctx->extensions, TLSX_ECH); if (echX != NULL) ech = (WOLFSSL_ECH*)echX->data; - if (TLSX_EchShouldHideInner(ssl, ech)) { - appended = TLSX_EchSwapExtensions(&ssl->extensions, + if (TLSX_EchShouldHideInner(ech)) { + prepended = TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, 0); installed = 1; } @@ -17087,9 +17094,13 @@ static int TLSX_WriteWithEch(WOLFSSL* ssl, byte* output, byte* semaphore, } } - if (installed) - (void)TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, - appended); + 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 fc0210bf42..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 diff --git a/tests/api.c b/tests/api.c index 422b506ab4..252cca622d 100644 --- a/tests/api.c +++ b/tests/api.c @@ -13805,6 +13805,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 +13874,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))); @@ -14264,9 +14280,7 @@ static int test_wolfSSL_Tls13_ECH_all_algos(void) return EXPECT_RESULT(); } -/* Test ECH when no private SNI is set - * SNI is by default permissive so these should pass - * (inner SNI is not required by ECH, only the outer SNI is required) */ +/* Test ECH when no private SNI is set */ static int test_wolfSSL_Tls13_ECH_no_private_name(void) { EXPECT_DECLS; @@ -14304,14 +14318,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_REJECTED); ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), - WOLFSSL_ECH_STATUS_REJECTED); + WOLFSSL_ECH_STATUS_ACCEPTED); test_ssl_memio_cleanup(&test_ctx); @@ -14329,11 +14348,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_REJECTED); + WOLFSSL_ECH_STATUS_ACCEPTED); ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), - WOLFSSL_ECH_STATUS_REJECTED); + WOLFSSL_ECH_STATUS_ACCEPTED); test_ssl_memio_cleanup(&test_ctx); From 31ddc5b612a2194c3b0b5f46b49c5d3ca1f41151 Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Mon, 1 Jun 2026 11:50:25 -0600 Subject: [PATCH 4/5] move long_sni regression test --- tests/api.c | 84 +++++++++++++++++++---------------------------------- 1 file changed, 30 insertions(+), 54 deletions(-) diff --git a/tests/api.c b/tests/api.c index 252cca622d..a9bcc8a871 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); @@ -14490,7 +14510,10 @@ 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(test_ctx.c_ssl->options.echAccepted, 0); + 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); @@ -15184,52 +15207,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; @@ -35357,7 +35334,6 @@ TEST_CASE testCases[] = { 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), From a9657588e242337dc7ecae97fe0e8c4409b1fa2e Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Mon, 8 Jun 2026 10:42:47 -0600 Subject: [PATCH 5/5] testing improvements (from #10542): - *_wire_sni test is now more efficient - openssl-ech workflow now does interop with ECH rejection extra improvements: - tested TLSX_EchSwapExtensions - added ctx level SNI to padding calculation --- .github/scripts/openssl-ech.sh | 88 +++-- .github/workflows/openssl-ech.yml | 6 + src/tls.c | 39 ++- src/tls13.c | 6 +- tests/api.c | 535 +++++++++++++++++++++--------- wolfssl/internal.h | 5 + 6 files changed, 496 insertions(+), 183 deletions(-) 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 c982dc695b..29e1f4ec57 100644 --- a/src/tls.c +++ b/src/tls.c @@ -2384,8 +2384,8 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, word16 size = 0; word16 offset = 0; int cacheOnly = 0; - SNI *sni = NULL; - const char *hostName = NULL; + SNI* sni = NULL; + const char* hostName = NULL; byte type; byte matched; #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) @@ -2548,6 +2548,8 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, TLSX_SNI_SetStatus(*writeList, type, (byte)matchStat); if (!cacheOnly) { + /* This is just a reimplementation of TLSX_SetResponse to accept a + * TLSX* instead */ extension = TLSX_Find(*writeList, TLSX_SERVER_NAME); if (extension) @@ -2672,8 +2674,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) @@ -2693,7 +2695,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) { @@ -14904,7 +14908,6 @@ static void TLSX_ECH_Free(WOLFSSL_ECH* ech, void* heap) ForceZero(ech->hpkeContext, sizeof(HpkeBaseContext)); XFREE(ech->hpkeContext, heap, DYNAMIC_TYPE_TMP_BUFFER); } - TLSX_FreeAll(ech->extensions, heap); XFREE(ech, heap, DYNAMIC_TYPE_TMP_BUFFER); (void)heap; @@ -15013,6 +15016,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; @@ -15184,6 +15191,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 @@ -16654,9 +16675,7 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) /* Returns 1 if the extensions should be hidden for this write */ static int TLSX_EchShouldHideInner(WOLFSSL_ECH* ech) { - if (ech == NULL || ech->type != ECH_TYPE_OUTER) - return 0; - return 1; + return ech != NULL && ech->type == ECH_TYPE_OUTER; } /* Swap matching extension types between *sslExts and *echExts. @@ -16668,7 +16687,7 @@ static int TLSX_EchShouldHideInner(WOLFSSL_ECH* ech) * popCount extensions are 'reversed' off the list. * * Returns a count of extensions prepended to sslExts. */ -static word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, +WOLFSSL_TEST_VIS word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, word16 popCount) { TLSX* chunk = NULL; @@ -16882,6 +16901,7 @@ static int TLSX_GetSizeWithEch(WOLFSSL* ssl, byte* semaphore, byte msgType, prepended = TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, prepended); if (ret == 0 && prepended != 0) { + WOLFSSL_MSG("Bad restore with TLSX_EchSwapExtensions"); ret = BAD_STATE_E; } } @@ -17098,6 +17118,7 @@ static int TLSX_WriteWithEch(WOLFSSL* ssl, byte* output, byte* semaphore, prepended = TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, prepended); if (ret == 0 && prepended != 0) { + WOLFSSL_MSG("Bad restore with TLSX_EchSwapExtensions"); ret = BAD_STATE_E; } } diff --git a/src/tls13.c b/src/tls13.c index 9bfbd75248..23525ada8e 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -4843,7 +4843,11 @@ int SendTls13ClientHello(WOLFSSL* ssl) /* calculate padding (RFC 9849, section 6.1.3) */ nameLen = TLSX_SNI_GetRequest(ssl->extensions, WOLFSSL_SNI_HOST_NAME, &hostName, 1); - if (hostName != NULL) { + 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 diff --git a/tests/api.c b/tests/api.c index a9bcc8a871..8d9f05981d 100644 --- a/tests/api.c +++ b/tests/api.c @@ -13667,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) (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 echPrep[] = { TEST_ECH_TLSX_D, TEST_ECH_TLSX_E }; + static const word16 prepSwap[] = { TEST_ECH_TLSX_E, TEST_ECH_TLSX_D, + TEST_ECH_TLSX_A, TEST_ECH_TLSX_B, TEST_ECH_TLSX_C }; + /* 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_E, TEST_ECH_TLSX_D, + TEST_ECH_TLSX_A, TEST_ECH_TLSX_B, TEST_ECH_TLSX_C }; + 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_D, TEST_ECH_TLSX_E, + TEST_ECH_TLSX_A, TEST_ECH_TLSX_B, TEST_ECH_TLSX_C }; + 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 prepended 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, echPrep, + TEST_ECH_LIST_SIZE(echPrep), TEST_ECH_TLSX_ECH); + test_TLSX_EchSwapExtensions_init(sslSwap, prepSwap, + TEST_ECH_LIST_SIZE(prepSwap), TEST_ECH_TLSX_MIX); + test_TLSX_EchSwapExtensions_init(echGoal, echPrep, + TEST_ECH_LIST_SIZE(echPrep), 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) { @@ -14095,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) { @@ -15014,81 +15298,14 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) return EXPECT_RESULT(); } -static int test_wolfSSL_Tls13_ECH_disable_conn_ex(int enableServer, - int enableClient) -{ - EXPECT_DECLS; - test_ssl_memio_ctx test_ctx; - - 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; - - /* both server and client will be setup to use ECH */ - test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; - test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; - test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; - - ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); - - /* this callback will ensure that the correct SNI is being held */ - wolfSSL_CTX_set_servername_callback(test_ctx.s_ctx, - test_ech_server_sni_callback); - ExpectIntEQ(wolfSSL_CTX_set_servername_arg(test_ctx.s_ctx, &enableClient), - WOLFSSL_SUCCESS); - - /* disable ECH on the appropriate side(s) */ - wolfSSL_SetEchEnable(test_ctx.s_ssl, enableServer); - wolfSSL_SetEchEnable(test_ctx.c_ssl, enableClient); - - if (!enableClient) { - /* client ECH disabled: no ECH extension sent, handshake succeeds - * normally but ECH is not accepted */ - ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), - TEST_SUCCESS); - ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), - WOLFSSL_ECH_STATUS_NOT_OFFERED); - } - else if (!enableServer) { - /* client sends ECH but server can't process it: server has no ECH - * keys so it processes the outer ClientHello, client detects ECH - * rejection and 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_NOT_OFFERED); - - test_ssl_memio_cleanup(&test_ctx); - - return EXPECT_RESULT(); -} - -static const byte* test_find_bytes(const char* needle, - const byte* haystack, int hayLen) -{ - int needleLen = (int)XSTRLEN(needle); - int i; - if (hayLen < needleLen) - return NULL; - for (i = 0; i <= hayLen - needleLen; i++) { - if (XMEMCMP(haystack + i, needle, needleLen) == 0) - return haystack + i; - } - return NULL; -} - /* The public name must be visible and the private name must not be visible */ -static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int hrr, int accept) +static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int accept) { EXPECT_DECLS; test_ssl_memio_ctx test_ctx; - WOLFSSL_CTX* tempCtx = NULL; - byte badConfig[128]; - word32 badConfigLen = sizeof(badConfig); + word16 idx; + word16 nameLen; + word16 publicLen = (word16)XSTRLEN(echCbTestPublicName); const char* expectedSni = accept ? echCbTestPrivateName : echCbTestPublicName; void* sniName = NULL; @@ -15108,6 +15325,10 @@ static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int hrr, int accept) /* 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); @@ -15121,8 +15342,8 @@ static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int hrr, int accept) WOLFSSL_SUCCESS); } - if (hrr) - ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), 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) { @@ -15135,43 +15356,58 @@ static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int hrr, int accept) ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, WOLFSSL_FATAL_ERROR), WOLFSSL_ERROR_WANT_READ); - /* CH1 wire bytes */ - ExpectNotNull(test_find_bytes(echCbTestPublicName, test_ctx.s_buff, - test_ctx.s_len)); - ExpectNull(test_find_bytes(echCbTestPrivateName, test_ctx.s_buff, - test_ctx.s_len)); - - if (hrr) { - /* 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); + /* 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); + /* 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); - /* CH2 wire bytes: same property must hold */ - ExpectNotNull(test_find_bytes(echCbTestPublicName, test_ctx.s_buff, - test_ctx.s_len)); - ExpectNull(test_find_bytes(echCbTestPrivateName, test_ctx.s_buff, - test_ctx.s_len)); - } + /* 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); - /* drive remaining rounds and verify the correct SNI is authoritative */ + /* 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); } - ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, accept ? 1 : 0); + /* 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; @@ -15186,76 +15422,76 @@ static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int hrr, int accept) static int test_wolfSSL_Tls13_ECH_wire_sni(void) { EXPECT_DECLS; - ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(0, 0), TEST_SUCCESS); - ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(0, 1), TEST_SUCCESS); - ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(1, 0), TEST_SUCCESS); - ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(1, 1), TEST_SUCCESS); + 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(); } -/* setup a server and client with ECH then disable on one, the other, or both. - * Verifies that disabling ECH prevents ECH from being used and that the - * public/private SNI's are verified correctly */ -static int test_wolfSSL_Tls13_ECH_disable_conn(void) +static int test_wolfSSL_Tls13_ECH_disable_conn_ex(int enableServer, + int enableClient) { EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; - ExpectIntEQ(test_wolfSSL_Tls13_ECH_disable_conn_ex(0, 1), TEST_SUCCESS); - ExpectIntEQ(test_wolfSSL_Tls13_ECH_disable_conn_ex(1, 0), TEST_SUCCESS); - ExpectIntEQ(test_wolfSSL_Tls13_ECH_disable_conn_ex(0, 0), TEST_SUCCESS); + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - return EXPECT_RESULT(); -} + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; -static int ech_seek_extensions(byte* buf, word16* innerExtLen) -{ - word16 idx; - byte sessionIdLen; - word16 cipherSuitesLen; - byte compressionLen; + /* both server and client will be setup to use ECH */ + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; + test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; - idx = OPAQUE16_LEN + RAN_LEN; + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); - sessionIdLen = buf[idx++]; - idx += sessionIdLen; + /* this callback will ensure that the correct SNI is being held */ + wolfSSL_CTX_set_servername_callback(test_ctx.s_ctx, + test_ech_server_sni_callback); + ExpectIntEQ(wolfSSL_CTX_set_servername_arg(test_ctx.s_ctx, &enableClient), + WOLFSSL_SUCCESS); - ato16(buf + idx, &cipherSuitesLen); - idx += OPAQUE16_LEN + cipherSuitesLen; + /* disable ECH on the appropriate side(s) */ + wolfSSL_SetEchEnable(test_ctx.s_ssl, enableServer); + wolfSSL_SetEchEnable(test_ctx.c_ssl, enableClient); - compressionLen = buf[idx++]; - idx += compressionLen; + if (!enableClient) { + /* client ECH disabled: no ECH extension sent, handshake succeeds + * normally but ECH is not accepted */ + ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_NOT_OFFERED); + } + else if (!enableServer) { + /* client sends ECH but server can't process it: server has no ECH + * keys so it processes the outer ClientHello, client detects ECH + * rejection and 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_NOT_OFFERED); - ato16(buf + idx, innerExtLen); - idx += OPAQUE16_LEN; + test_ssl_memio_cleanup(&test_ctx); - return idx; + return EXPECT_RESULT(); } -static int ech_find_extension(byte* buf, word16* idx_p, word16 extType) +/* setup a server and client with ECH then disable on one, the other, or both. + * Verifies that disabling ECH prevents ECH from being used and that the + * public/private SNI's are verified correctly */ +static int test_wolfSSL_Tls13_ECH_disable_conn(void) { - 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; - } + EXPECT_DECLS; - idx += OPAQUE16_LEN; - ato16(buf + idx, &len); - idx += OPAQUE16_LEN + len; - } + ExpectIntEQ(test_wolfSSL_Tls13_ECH_disable_conn_ex(0, 1), TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_disable_conn_ex(1, 0), TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_disable_conn_ex(0, 0), TEST_SUCCESS); - return BAD_FUNC_ARG; + return EXPECT_RESULT(); } /* Test the HRR ECH rejection fallback path: @@ -35314,6 +35550,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), diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 0d1c186ad6..6581e1d29b 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3180,6 +3180,11 @@ WOLFSSL_LOCAL int TLSX_FinalizeEch(WOLFSSL* ssl, WOLFSSL_ECH* ech, byte* aad, 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);