From 1aea5b9ab9cb3b7772b1c22bbba5858b3cf18c66 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 ace0f7f979a..97df2892753 100644 --- a/src/tls.c +++ b/src/tls.c @@ -13891,14 +13891,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); } @@ -14950,9 +14956,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); @@ -14961,8 +14966,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; @@ -16709,95 +16713,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, @@ -16907,39 +16911,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 @@ -17069,19 +17077,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)); } @@ -17133,7 +17148,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); } @@ -17144,12 +17159,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 ecfd5946dd8..c7ef8d9bf35 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 */ @@ -7681,15 +7689,18 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, #if defined(HAVE_ECH) if (!ssl->options.echProcessingInner && echX != NULL && - ((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE) { - if (((WOLFSSL_ECH*)echX->data)->innerClientHello != NULL) { + ssl->ctx->echConfigs != NULL && !ssl->options.disableECH) { + if (((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE && + ((WOLFSSL_ECH*)echX->data)->innerClientHello != NULL) { + /* ECH accepted: use private extensions */ + TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); /* Client sent real ECH and inner hello was decrypted, jump to * exit so the caller can re-invoke with the inner hello */ goto exit_dch; } else { - /* If ECH was accepted in ClientHello1 then ClientHello2 MUST - * contain an ECH extension */ + /* If ECH was accepted in CH1 then CH2 MUST contain + * an ECH extension */ if (ssl->options.serverState == SERVER_HELLO_RETRY_REQUEST_COMPLETE && ssl->options.echAccepted) { @@ -7697,10 +7708,16 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, "extension"); ERROR_OUT(INCOMPLETE_DATA, exit_dch); } - /* Server has ECH but client did not send ECH. Clear the - * response flag so the empty ECH extension is not written - * in EncryptedExtensions. */ - echX->resp = 0; + + /* Otherwise ECH rejected: use public extensions */ + if (((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE) { + TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); + echX->resp = 0; + } + else if (((WOLFSSL_ECH*)echX->data)->state == + ECH_WRITE_RETRY_CONFIGS) { + TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); + } } } #endif diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 3cd37c739bd..290c7cbe3fd 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3166,6 +3166,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; @@ -3187,6 +3189,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 e93718743e66a854f242025a26efc3685af70f1a 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 97df2892753..d1b5012fc88 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 c7ef8d9bf35..a3129323889 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -13810,18 +13810,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 32b90b9a079..a1e6e6ec732 100644 --- a/tests/api.c +++ b/tests/api.c @@ -14281,9 +14281,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; @@ -14469,7 +14474,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; @@ -14512,9 +14519,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); @@ -14534,9 +14541,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); @@ -14643,6 +14650,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(); } @@ -15193,6 +15233,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 */ @@ -35476,6 +35642,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 290c7cbe3fd..323090264d1 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3131,13 +3131,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; @@ -3161,7 +3154,6 @@ typedef struct WOLFSSL_ECH { Hpke* hpke; HpkeBaseContext* hpkeContext; const byte* aad; - const char* privateName; void* ephemeralKey; WOLFSSL_EchConfig* echConfig; byte* innerClientHello; @@ -3176,7 +3168,6 @@ typedef struct WOLFSSL_ECH { word16 kemId; word16 encLen; EchState state; - EchStateSNI sniState; byte type; byte configId; byte enc[HPKE_Npk_MAX]; From bbeb552fd19ee58b0ee9dec981b927ec0df10bd9 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 d1b5012fc88..bcb18539540 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; @@ -13857,6 +13866,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 */ @@ -16685,11 +16695,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. @@ -16710,7 +16720,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. */ @@ -16745,19 +16755,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; @@ -16886,25 +16896,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) { @@ -16916,9 +16921,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 @@ -17051,18 +17060,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; } @@ -17130,9 +17137,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 a3129323889..b71081e7169 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 a1e6e6ec732..04acece1874 100644 --- a/tests/api.c +++ b/tests/api.c @@ -14015,6 +14015,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 */ @@ -14082,6 +14084,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))); @@ -14474,9 +14490,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; @@ -14514,14 +14528,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); @@ -14539,11 +14558,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 413b148459dba84f67bce8f5a47a106bc7a87c53 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 04acece1874..8b45b87a324 100644 --- a/tests/api.c +++ b/tests/api.c @@ -8130,17 +8130,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); @@ -14700,7 +14720,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); @@ -15394,52 +15417,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; @@ -35665,7 +35642,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 d3f80bf3ec5627124ec73b36f4940bf1a41e4145 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 - Improvement of SNI handling for ECH - Changed EchSwapExtensions to append instead of prepend - Reworked ECH testing --- .github/scripts/openssl-ech.sh | 45 +- .github/workflows/openssl-ech.yml | 6 + src/tls.c | 309 +++++--- src/tls13.c | 45 +- tests/api.c | 1166 +++++++++++++++++++---------- wolfssl/internal.h | 11 + 6 files changed, 1029 insertions(+), 553 deletions(-) diff --git a/.github/scripts/openssl-ech.sh b/.github/scripts/openssl-ech.sh index ca669de4501..555cfbbb0c2 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 e43f6b9c8d5..21dca0a71e6 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 bcb18539540..924b28a241d 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) +static 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) */ +static 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,10 +2393,10 @@ 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; + int checkPublic = 0; + SNI* sni = NULL; byte type; - byte matched; + byte matched = 0; #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) TLSX* echX = NULL; WOLFSSL_ECH* ech = NULL; @@ -2424,7 +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) { + if (!ssl->options.disableECH && !ssl->options.echProcessingInner) { echX = TLSX_Find(ssl->extensions, TLSX_ECH); if (echX != NULL) { ech = (WOLFSSL_ECH*)(echX->data); @@ -2448,8 +2457,19 @@ 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 (ech != NULL) { + checkPublic = 1; + } + if (!checkPublic) + #endif + { + /* Skipping, SNI not enabled at server side. */ + return 0; + } } } @@ -2477,7 +2497,8 @@ 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) @@ -2488,34 +2509,42 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, return 0; #endif - 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) { + 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 */ + * be forcibly cached. This allows an SNI response to be given for the + * public name */ if (matched) cacheOnly = 0; } + if (!matched) #endif + { + const char* hostName; + hostName = (sni != NULL) ? sni->data.host_name : NULL; + matched = cacheOnly || (hostName != NULL && + XSTRLEN(hostName) == size && + XSTRNCMP(hostName, (const char*)input + offset, size) == 0); + } - 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))) { @@ -2523,6 +2552,7 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, 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 @@ -2547,12 +2577,8 @@ static int TLSX_SNI_Parse(WOLFSSL* ssl, const byte* input, word16 length, TLSX_SNI_SetStatus(*writeList, type, (byte)matchStat); - if (!cacheOnly) { - extension = TLSX_Find(*writeList, TLSX_SERVER_NAME); - - if (extension) - extension->resp = 1; - } + if (!cacheOnly) + TLSX_SetResponseInList(*writeList, TLSX_SERVER_NAME); } else if ((sni == NULL) || !(sni->options & WOLFSSL_SNI_CONTINUE_ON_MISMATCH)) { @@ -2672,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) @@ -2693,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) { @@ -13786,11 +13814,9 @@ static int TLSX_GreaseECH_Use(TLSX** extensions, void* heap, WC_RNG* rng) ech = (WOLFSSL_ECH*)XMALLOC(sizeof(WOLFSSL_ECH), heap, DYNAMIC_TYPE_TMP_BUFFER); - if (ech == NULL) return MEMORY_E; - - ForceZero(ech, sizeof(WOLFSSL_ECH)); + XMEMSET(ech, 0, sizeof(WOLFSSL_ECH)); ech->state = ECH_WRITE_GREASE; @@ -13841,7 +13867,7 @@ static int TLSX_ECH_Use(WOLFSSL_EchConfig* echConfig, TLSX** extensions, DYNAMIC_TYPE_TMP_BUFFER); if (ech == NULL) return MEMORY_E; - ForceZero(ech, sizeof(WOLFSSL_ECH)); + XMEMSET(ech, 0, sizeof(WOLFSSL_ECH)); ech->state = ECH_WRITE_REAL; ech->echConfig = echConfig; /* 0 for outer */ @@ -13866,26 +13892,27 @@ 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) { + /* use the chosen config's public name for the outer SNI */ ret = TLSX_UseSNI(&ech->extensions, WOLFSSL_SNI_HOST_NAME, echConfig->publicName, (word16)XSTRLEN(echConfig->publicName), heap); if (ret == WOLFSSL_SUCCESS) - ret = 0; + ret = TLSX_Push(extensions, TLSX_ECH, ech, heap); + else if (ret == WC_NO_ERR_TRACE(WOLFSSL_FAILURE)) + ret = BAD_STATE_E; + if (ret != 0) { + TLSX_FreeAll(ech->extensions, heap); + wc_HpkeFreeKey(ech->hpke, ech->hpke->kem, ech->ephemeralKey, + ech->hpke->heap); + } } - 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); } @@ -13893,7 +13920,7 @@ static int TLSX_ECH_Use(WOLFSSL_EchConfig* echConfig, TLSX** extensions, } /* return status after setting up ech to read and decrypt */ -static int TLSX_ServerECH_Use(TLSX** extensions, void* heap, +WOLFSSL_TEST_VIS int TLSX_ServerECH_Use(TLSX** extensions, void* heap, WOLFSSL_EchConfig* configs) { int ret; @@ -13909,7 +13936,7 @@ static int TLSX_ServerECH_Use(TLSX** extensions, void* heap, DYNAMIC_TYPE_TMP_BUFFER); if (ech == NULL) return MEMORY_E; - ForceZero(ech, sizeof(WOLFSSL_ECH)); + XMEMSET(ech, 0, sizeof(WOLFSSL_ECH)); ech->state = ECH_WRITE_NONE; /* 0 for outer */ ech->type = ECH_TYPE_OUTER; @@ -14947,7 +14974,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; @@ -15056,6 +15082,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; @@ -15227,6 +15257,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 @@ -16697,21 +16741,20 @@ 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. - * Non-matched extensions in *echExts are prepended to *sslExts - * popCount is the number of leading extensions to move from - * *sslExts to *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) * - * Ordering is kept in mind for OuterExtensions. This is why the leading - * popCount extensions are 'reversed' off the list. + * 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 prepended to sslExts. */ -static word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, + * Returns a count of extensions appended to sslExts. */ +WOLFSSL_TEST_VIS word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, word16 popCount) { TLSX* chunk = NULL; @@ -16720,16 +16763,20 @@ static word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, TLSX* inner; TLSX** outerLink; TLSX** innerLink; - word16 prepended = 0; - - /* unhook popCount nodes off *sslExts head into chunk. - * Head-prepend undoes the reversal caused by appending onto sslExts. */ - while (popCount > 0 && *sslExts != NULL) { - node = *sslExts; - *sslExts = node->next; - node->next = chunk; - chunk = node; - popCount--; + 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; } outerLink = echExts; @@ -16753,21 +16800,49 @@ static word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, } else { *outerLink = outer->next; - outer->next = *sslExts; - *sslExts = outer; - prepended++; + *innerLink = outer; + outer->next = NULL; + appended++; } } /* outerLink is at the tail of *echExts; append the chunk */ *outerLink = chunk; - return prepended; + return appended; +} + +/* 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; +} + +/* 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; + } + } + + return ret; } /* If ECH is accepted, delete ech->extensions * If rejected, replace matching ssl->extensions with ech->extensions, - * prepending to head if necessary */ + * appending to the tail if necessary */ void TLSX_EchReplaceExtensions(WOLFSSL* ssl, byte accepted) { TLSX* echX; @@ -16893,22 +16968,18 @@ static int TLSX_GetSizeWithEch(WOLFSSL* ssl, byte* semaphore, byte msgType, word16* pLength) { int ret = 0; + int installed; TLSX* echX = NULL; WOLFSSL_ECH* ech = NULL; word16 count = 0; - word16 prepended = 0; - byte installed = 0; + word16 appended = 0; if (ssl->extensions) echX = TLSX_Find(ssl->extensions, TLSX_ECH); if (echX != NULL) ech = (WOLFSSL_ECH*)echX->data; - if (TLSX_EchShouldHideInner(ech)) { - prepended = TLSX_EchSwapExtensions(&ssl->extensions, - &ech->extensions, 0); - installed = 1; - } + 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 */ @@ -16921,13 +16992,7 @@ 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) { - prepended = TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, - prepended); - if (ret == 0 && prepended != 0) { - ret = BAD_STATE_E; - } - } + ret = TLSX_EchExposeExtensions(ssl, ech, appended, installed, ret); return ret; } #endif @@ -17058,21 +17123,17 @@ static int TLSX_WriteWithEch(WOLFSSL* ssl, byte* output, byte* semaphore, byte msgType, word16* pOffset) { int ret = 0; + int installed = 0; TLSX* echX = NULL; WOLFSSL_ECH* ech = NULL; - word16 prepended = 0; - byte installed = 0; + word16 appended = 0; if (ssl->extensions) echX = TLSX_Find(ssl->extensions, TLSX_ECH); if (echX != NULL) ech = (WOLFSSL_ECH*)echX->data; - if (TLSX_EchShouldHideInner(ech)) { - prepended = TLSX_EchSwapExtensions(&ssl->extensions, - &ech->extensions, 0); - installed = 1; - } + installed = TLSX_EchConcealExtensions(ssl, ech, &appended); if (echX != NULL) { /* turn ech on so it doesn't write, then write it last */ @@ -17137,13 +17198,7 @@ static int TLSX_WriteWithEch(WOLFSSL* ssl, byte* output, byte* semaphore, } } - if (installed) { - prepended = TLSX_EchSwapExtensions(&ssl->extensions, &ech->extensions, - prepended); - if (ret == 0 && prepended != 0) { - ret = BAD_STATE_E; - } - } + ret = TLSX_EchExposeExtensions(ssl, ech, appended, installed, ret); return ret; } #endif @@ -18703,6 +18758,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 b71081e7169..e2de2406086 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 @@ -5306,7 +5310,8 @@ static int EchCheckAcceptance(WOLFSSL* ssl, byte* label, word16 labelSz, /* 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) + if (ret == 0 && + (msgType != hello_retry_request || !ssl->options.echAccepted)) TLSX_EchReplaceExtensions(ssl, ssl->options.echAccepted); return ret; @@ -7691,37 +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 && - 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 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"); - ERROR_OUT(INCOMPLETE_DATA, exit_dch); - } - - /* 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); - } - } + ((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE && + ((WOLFSSL_ECH*)echX->data)->innerClientHello != NULL) { + goto exit_dch; } #endif diff --git a/tests/api.c b/tests/api.c index 8b45b87a324..730db41048d 100644 --- a/tests/api.c +++ b/tests/api.c @@ -341,6 +341,13 @@ int testDevId = INVALID_DEVID; #define MESSAGE_TYPE_CAST void* #endif +#ifdef HAVE_ECH + #define echPublicName "example.com" + #define echPrivateName "ech-private-name.com" + #define echOtherName "mismatch.io" + #define ECH_CONFIG_LEN 256 +#endif + /*----------------------------------------------------------------------------* | BIO with fixed read/write size *----------------------------------------------------------------------------*/ @@ -13786,6 +13793,44 @@ static int test_wolfSSL_Tls13_Key_Logging_test(void) return EXPECT_RESULT(); } +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) && \ + defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) +/* modify the ECHConfig present on s_ctx and install it onto c_ssl */ +static int test_ech_set_bad_echconfigs(WOLFSSL_CTX* s_ctx, WOLFSSL* c_ssl) +{ + EXPECT_DECLS; + byte configs[ECH_CONFIG_LEN]; + word32 configsLen = sizeof(configs); + word16 idx = 0; + + /* start from the server's real ECHConfigList */ + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(s_ctx, configs, &configsLen), + WOLFSSL_SUCCESS); + + if (EXPECT_SUCCESS()) { + /* skip to offset of the first config's public key; skip: + * - 2 byte list length + * - first config's 2 byte version + * - 2 byte length + * - 1 byte config id + * - 2 byte kem id + * - 2 byte public key length */ + idx = OPAQUE16_LEN + OPAQUE16_LEN + OPAQUE16_LEN + OPAQUE8_LEN + + OPAQUE16_LEN + OPAQUE16_LEN; + ExpectIntLT(idx, configsLen); + + /* flip the first byte of the public key so decrypt fails */ + if (EXPECT_SUCCESS()) + configs[idx] ^= 0xFF; + } + + ExpectIntEQ(wolfSSL_SetEchConfigs(c_ssl, configs, configsLen), + WOLFSSL_SUCCESS); + + return EXPECT_RESULT(); +} +#endif + /* When ECH is rejected the inner random is never swapped in, so ECH_SECRET and * CLIENT_HANDSHAKE_TRAFFIC_SECRET are both logged against the outer random. */ static int test_wolfSSL_Tls13_Key_Logging_ech_rejected(void) @@ -13795,9 +13840,6 @@ static int test_wolfSSL_Tls13_Key_Logging_ech_rejected(void) defined(HAVE_SECRET_CALLBACK) && defined(HAVE_ECH) && \ defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) test_ssl_memio_ctx test_ctx; - WOLFSSL_CTX* tempCtx = NULL; - byte badConfig[128]; - word32 badConfigLen = sizeof(badConfig); XFILE fp = XBADFILE; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); @@ -13810,18 +13852,9 @@ static int test_wolfSSL_Tls13_Key_Logging_ech_rejected(void) ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); - /* generate a throwaway ECH config the server cannot decrypt */ - ExpectNotNull(tempCtx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); - ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(tempCtx, "ech-public-name.com", - 0, 0, 0), WOLFSSL_SUCCESS); - ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(tempCtx, badConfig, &badConfigLen), - WOLFSSL_SUCCESS); - wolfSSL_CTX_free(tempCtx); - tempCtx = NULL; - - /* client uses the bad config so the server rejects ECH */ - ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, badConfig, badConfigLen), - WOLFSSL_SUCCESS); + /* bad config derived from the server's real one to force ECH rejection */ + ExpectIntEQ(test_ech_set_bad_echconfigs(test_ctx.s_ctx, test_ctx.c_ssl), + TEST_SUCCESS); /* set inner SNI */ ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, "ech-private-name.com", (word16)XSTRLEN("ech-private-name.com")), @@ -13877,6 +13910,231 @@ static int test_wolfSSL_Tls13_Key_Logging_ech_rejected(void) } #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) + +#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 appended; + + /* swap ech extensions into the ssl list */ + appended = 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 appended count back restores both lists and their marks */ + if (EXPECT_SUCCESS()) { + appended = TLSX_EchSwapExtensions(&sslExts, &echExts, appended); + ExpectIntEQ(appended, 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, + XELEM_CNT(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(sslSwap, ssl3, + XELEM_CNT(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(sslGoal, ssl3, + XELEM_CNT(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, + XELEM_CNT(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echExts, echMatch, + XELEM_CNT(echMatch), TEST_ECH_TLSX_ECH); + test_TLSX_EchSwapExtensions_init(sslSwap, ssl3, + XELEM_CNT(ssl3), TEST_ECH_TLSX_MIX); + test_TLSX_EchSwapExtensions_init(echSwap, echMatch, + XELEM_CNT(echMatch), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echGoal, echMatch, + XELEM_CNT(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, + XELEM_CNT(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echExts, echAppend, + XELEM_CNT(echAppend), TEST_ECH_TLSX_ECH); + test_TLSX_EchSwapExtensions_init(sslSwap, appendSwap, + XELEM_CNT(appendSwap), TEST_ECH_TLSX_MIX); + test_TLSX_EchSwapExtensions_init(echGoal, echAppend, + XELEM_CNT(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, + XELEM_CNT(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echExts, echMix, + XELEM_CNT(echMix), TEST_ECH_TLSX_ECH); + test_TLSX_EchSwapExtensions_init(sslSwap, sslMixSwap, + XELEM_CNT(sslMixSwap), TEST_ECH_TLSX_MIX); + test_TLSX_EchSwapExtensions_init(echSwap, echMixSwap, + XELEM_CNT(echMixSwap), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echGoal, echMix, + XELEM_CNT(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, + XELEM_CNT(ssl3), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echExts, echUlt, + XELEM_CNT(echUlt), TEST_ECH_TLSX_ECH); + test_TLSX_EchSwapExtensions_init(sslSwap, sslUltSwap, + XELEM_CNT(sslUltSwap), TEST_ECH_TLSX_MIX); + test_TLSX_EchSwapExtensions_init(echSwap, echUltSwap, + XELEM_CNT(echUltSwap), TEST_ECH_TLSX_SSL); + test_TLSX_EchSwapExtensions_init(echGoal, echUltGoal, + XELEM_CNT(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_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) { @@ -14185,7 +14443,7 @@ static int test_wolfSSL_ECH_conn_ex(method_provider serverMeth, int privateNameLen = 20; char reply[1024]; int replyLen = 0; - byte rawEchConfig[128]; + byte rawEchConfig[ECH_CONFIG_LEN]; word32 rawEchConfigLen = sizeof(rawEchConfig); InitTcpReady(&ready); @@ -14297,14 +14555,69 @@ static int test_wolfSSL_SubTls13_ECH(void) #ifdef HAVE_SSL_MEMIO_TESTS_DEPENDENCIES /* Static storage for passing ECH config between server and client callbacks */ -static byte echCbTestConfigs[512]; +static byte echCbTestConfigs[ECH_CONFIG_LEN]; static word32 echCbTestConfigsLen; -static const char* echCbTestPublicName = "example.com"; -static const char* echCbTestPrivateName = "ech-private-name.com"; 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) { @@ -14318,7 +14631,7 @@ 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) { - if (XSTRCMP(name, echCbTestPublicName) == 0) { + if (XSTRCMP(name, echPublicName) == 0) { return 0; } else { @@ -14326,7 +14639,7 @@ static int test_ech_server_sni_callback(WOLFSSL* ssl, int* ad, void* arg) return fatal_return; } } - else if (XSTRCMP(name, echCbTestPrivateName) == 0) { + else if (XSTRCMP(name, echPrivateName) == 0) { return 0; } else { @@ -14341,9 +14654,9 @@ static int test_ech_server_ctx_ready(WOLFSSL_CTX* ctx) int ret; /* +20 for this isn't significant, it just exercises the padding code */ - ret = wolfSSL_CTX_GenerateEchConfigEx(ctx, echCbTestPublicName, + ret = wolfSSL_CTX_GenerateEchConfigEx(ctx, echPublicName, echCbTestKemID, echCbTestKdfID, echCbTestAeadID, - XSTRLEN(echCbTestPublicName) + 20); + XSTRLEN(echPublicName) + 20); if (ret != WOLFSSL_SUCCESS) return TEST_FAIL; @@ -14371,8 +14684,8 @@ static int test_ech_server_ssl_ready(WOLFSSL* ssl) { int ret; - ret = wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, echCbTestPrivateName, - (word16)XSTRLEN(echCbTestPrivateName)); + ret = wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, echPrivateName, + (word16)XSTRLEN(echPrivateName)); if (ret != WOLFSSL_SUCCESS) return TEST_FAIL; @@ -14388,8 +14701,8 @@ static int test_ech_client_ssl_ready(WOLFSSL* ssl) if (ret != WOLFSSL_SUCCESS) return TEST_FAIL; - ret = wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, echCbTestPrivateName, - (word16)XSTRLEN(echCbTestPrivateName)); + ret = wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, echPrivateName, + (word16)XSTRLEN(echPrivateName)); if (ret != WOLFSSL_SUCCESS) return TEST_FAIL; @@ -14510,76 +14823,24 @@ static int test_wolfSSL_Tls13_ECH_all_algos(void) return EXPECT_RESULT(); } -/* Test ECH when no private SNI is set */ +/* End-to-end test of ECH when no private SNI is set */ static int test_wolfSSL_Tls13_ECH_no_private_name(void) { EXPECT_DECLS; struct test_ssl_memio_ctx test_ctx; - /* client sends private SNI, server does not have one set */ - - 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.c_cb.ssl_ready = test_ech_client_ssl_ready; - - ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); - - 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); - - test_ssl_memio_cleanup(&test_ctx); - - /* client does not send private SNI, server has one set */ - 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); - /* 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_ACCEPTED); - - test_ssl_memio_cleanup(&test_ctx); - - /* client does not send private SNI, server does not have one set */ - - 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; - - ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); - - ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echCbTestConfigs, - echCbTestConfigsLen), WOLFSSL_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_ACCEPTED); @@ -14591,166 +14852,12 @@ static int test_wolfSSL_Tls13_ECH_no_private_name(void) return EXPECT_RESULT(); } -/* Test ECH rejection when configs don't match */ -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 */ - - 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); - ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, - echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), - 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_REJECTED); - - test_ssl_memio_cleanup(&test_ctx); - - - /* 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; - - 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 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, - badPrivateName, (word16)XSTRLEN(badPrivateName)), 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); - /* 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); - - test_ssl_memio_cleanup(&test_ctx); - - - /* verify with double public SNI */ - - XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - - test_ctx.s_cb.method = wolfTLSv1_3_server_method; - test_ctx.c_cb.method = wolfTLSv1_3_client_method; - - test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; - test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; - - ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); - - /* set public SNI for private SNI on client */ - ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echCbTestConfigs, - echCbTestConfigsLen), WOLFSSL_SUCCESS); - ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, - echCbTestPublicName, (word16)XSTRLEN(echCbTestPublicName)), - WOLFSSL_SUCCESS); - - if (hrr) { - ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); - } - if (sniCb) { - wolfSSL_CTX_set_servername_callback(test_ctx.s_ctx, - test_ech_server_sni_callback); - } - - ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); - ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), - WOLFSSL_ECH_STATUS_REJECTED); - ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), - WOLFSSL_ECH_STATUS_ACCEPTED); - - test_ssl_memio_cleanup(&test_ctx); - - return EXPECT_RESULT(); -} - -static int test_wolfSSL_Tls13_ECH_bad_configs(void) -{ - EXPECT_DECLS; - - ExpectIntEQ(test_wolfSSL_Tls13_ECH_bad_configs_ex(0, 0), WOLFSSL_SUCCESS); - ExpectIntEQ(test_wolfSSL_Tls13_ECH_bad_configs_ex(0, 1), WOLFSSL_SUCCESS); - ExpectIntEQ(test_wolfSSL_Tls13_ECH_bad_configs_ex(1, 0), WOLFSSL_SUCCESS); - ExpectIntEQ(test_wolfSSL_Tls13_ECH_bad_configs_ex(1, 1), WOLFSSL_SUCCESS); - - return EXPECT_RESULT(); -} - /* Test retry configs are returned after ECH rejection and are usable */ static int test_wolfSSL_Tls13_ECH_retry_configs_ex(int hrr) { EXPECT_DECLS; test_ssl_memio_ctx test_ctx; - WOLFSSL_CTX* tempCtx = NULL; - byte badConfigs[256]; - word32 badConfigsLen = sizeof(badConfigs); - byte retryConfigs[256]; + byte retryConfigs[ECH_CONFIG_LEN]; word32 retryConfigsLen = sizeof(retryConfigs); WOLFSSL_CTX* savedSCtx; @@ -14764,19 +14871,11 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_ex(int hrr) ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); - /* throwaway ECH config the server won't recognise */ - 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, badConfigs, &badConfigsLen), - WOLFSSL_SUCCESS); - wolfSSL_CTX_free(tempCtx); - tempCtx = NULL; - - ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, badConfigs, - badConfigsLen), WOLFSSL_SUCCESS); + /* bad config derived from the server's real one to force ECH rejection */ + ExpectIntEQ(test_ech_set_bad_echconfigs(test_ctx.s_ctx, test_ctx.c_ssl), + TEST_SUCCESS); ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, - echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + echPrivateName, (word16)XSTRLEN(echPrivateName)), WOLFSSL_SUCCESS); if (hrr) @@ -14817,7 +14916,7 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_ex(int hrr) ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, retryConfigs, retryConfigsLen), WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, - echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + echPrivateName, (word16)XSTRLEN(echPrivateName)), WOLFSSL_SUCCESS); if (hrr) @@ -14855,7 +14954,7 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(int hrr) EXPECT_DECLS; test_ssl_memio_ctx test_ctx; WOLFSSL_CTX* tempCtx = NULL; - byte badConfigs[256]; + byte badConfigs[ECH_CONFIG_LEN]; word32 badConfigsLen = sizeof(badConfigs); word32 retryConfigsLen = sizeof(badConfigs); const char* badPublicName = "ech-public-name.com"; @@ -14880,7 +14979,7 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(int hrr) ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, badConfigs, badConfigsLen), WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, - echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + echPrivateName, (word16)XSTRLEN(echPrivateName)), WOLFSSL_SUCCESS); /* Do not require client cert on server so it does not send @@ -14974,9 +15073,9 @@ static int test_wolfSSL_Tls13_ECH_new_config(void) { EXPECT_DECLS; test_ssl_memio_ctx test_ctx; - byte altConfig[512]; + byte altConfig[ECH_CONFIG_LEN]; word32 altConfigLen = sizeof(altConfig); - byte combinedConfigs[512]; + byte combinedConfigs[ECH_CONFIG_LEN]; word32 combinedConfigsLen = sizeof(combinedConfigs); word16 firstConfigLen = 0; word16 secondConfigOffset = 0; @@ -14995,7 +15094,7 @@ static int test_wolfSSL_Tls13_ECH_new_config(void) /* generate a second ECH config for the server */ ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(test_ctx.s_ctx, - echCbTestPrivateName, 0, 0, 0), WOLFSSL_SUCCESS); + echPrivateName, 0, 0, 0), WOLFSSL_SUCCESS); ExpectNotNull(test_ctx.s_ctx->echConfigs->next); /* capture the second ECH config in the list for the client to use */ @@ -15034,7 +15133,7 @@ static int test_wolfSSL_Tls13_ECH_new_config(void) ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, altConfig, altConfigLen), WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, - echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + echPrivateName, (word16)XSTRLEN(echPrivateName)), WOLFSSL_SUCCESS); ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); @@ -15112,6 +15211,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); @@ -15154,7 +15257,7 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, - echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + echPrivateName, (word16)XSTRLEN(echPrivateName)), WOLFSSL_SUCCESS); /* verify ECH is enabled on the client and server */ @@ -15194,7 +15297,7 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, - echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + echPrivateName, (word16)XSTRLEN(echPrivateName)), WOLFSSL_SUCCESS); /* verify ECH is enabled on the client and server */ @@ -15224,83 +15327,41 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) return EXPECT_RESULT(); } -static int test_wolfSSL_Tls13_ECH_disable_conn_ex(int enableServer, - int enableClient) +/* the outer ClientHello in buff must carry exactly one SNI: the public name */ +static int test_ech_assert_wire_sni(byte* buff, const char* publicName) { 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; + word16 idx = RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ; + word16 extLen; + word16 listLen; + word16 nameLen; + word16 publicLen = (word16)XSTRLEN(publicName); - ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + ExpectIntEQ(ech_find_extension(buff, &idx, TLSXT_SERVER_NAME), 0); - /* 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(buff + idx + 2, &extLen); + ato16(buff + idx + 4, &listLen); + ExpectIntEQ(buff[idx + 6], WOLFSSL_SNI_HOST_NAME); + ato16(buff + idx + 7, &nameLen); - /* 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); + /* the single entry is the public name */ + ExpectIntEQ(nameLen, publicLen); + ExpectIntEQ(XMEMCMP(buff + idx + 9, publicName, publicLen), 0); + /* and it is the only entry: the list and extension hold nothing else */ + ExpectIntEQ(listLen, OPAQUE8_LEN + OPAQUE16_LEN + nameLen); + ExpectIntEQ(extLen, OPAQUE16_LEN + listLen); 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) +/* The public name must be visible in plaintext + * useCtx installs the inner SNI on the client ctx */ +static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int accept, int useCtx) { 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; + accept ? echPrivateName : echPublicName; void* sniName = NULL; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); @@ -15310,29 +15371,36 @@ static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int hrr, int accept) 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); + /* derive a bad config from the server's real one to reject ECH */ + ExpectIntEQ(test_ech_set_bad_echconfigs(test_ctx.s_ctx, test_ctx.c_ssl), + TEST_SUCCESS); + } + else { + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echCbTestConfigs, + echCbTestConfigsLen), WOLFSSL_SUCCESS); + /* 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); + } + + /* install the private (inner) SNI on the ctx or the per-connection ssl */ + if (useCtx) { + ExpectIntEQ(wolfSSL_CTX_UseSNI(test_ctx.c_ctx, WOLFSSL_SNI_HOST_NAME, + echPrivateName, (word16)XSTRLEN(echPrivateName)), WOLFSSL_SUCCESS); + } + else { ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, - echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), - WOLFSSL_SUCCESS); + echPrivateName, (word16)XSTRLEN(echPrivateName)), 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) { @@ -15345,45 +15413,53 @@ 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)); + /* check sent SNI is correct */ + ExpectIntEQ(test_ech_assert_wire_sni(test_ctx.s_buff, echPublicName), + TEST_SUCCESS); - 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); + /* 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 */ + ExpectIntEQ(test_ech_assert_wire_sni(test_ctx.s_buff, echPublicName), + TEST_SUCCESS); - /* 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); + /* an inner SNI installed on the ctx is never copied onto the ssl, so on + * accept it does not become the authoritative request on the client ssl */ + if (accept && useCtx) + ExpectNull(sniName); + else + ExpectStrEQ((const char*)sniName, expectedSni); sniName = NULL; wolfSSL_SNI_GetRequest(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, &sniName); ExpectStrEQ((const char*)sniName, expectedSni); @@ -15397,75 +15473,77 @@ 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(0, 1), 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 */ -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: @@ -15497,7 +15575,7 @@ static int test_wolfSSL_Tls13_ECH_HRR_rejection(void) * sends HRR without an ECH extension (confBuf stays NULL on the client) */ wolfSSL_SetEchEnable(test_ctx.s_ssl, 0); ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, - echCbTestPublicName, (word16)XSTRLEN(echCbTestPublicName)), + echPublicName, (word16)XSTRLEN(echPublicName)), WOLFSSL_SUCCESS); /* Force HRR so client receives HRR with no ECH extension, @@ -15653,7 +15731,7 @@ static int test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex(const char* publicName, { EXPECT_DECLS; test_ssl_memio_ctx test_ctx; - byte echConfigs[512]; + byte echConfigs[ECH_CONFIG_LEN]; word32 echConfigsLen = sizeof(echConfigs); XMEMSET(&test_ctx, 0, sizeof(test_ctx)); @@ -15736,7 +15814,7 @@ static int test_wolfSSL_Tls13_ECH_rejected_empty_client_cert(void) { EXPECT_DECLS; test_ssl_memio_ctx test_ctx; - byte echConfigs[512]; + byte echConfigs[ECH_CONFIG_LEN]; word32 echConfigsLen = sizeof(echConfigs); const char* publicName = "example.com"; @@ -15791,6 +15869,267 @@ static int test_wolfSSL_Tls13_ECH_rejected_empty_client_cert(void) return EXPECT_RESULT(); } + +/* Install ECH on the server and initialize the ECH AAD, then run TLSX_Parse. + * ECH acceptance will be determined during TLSX_Parse */ +static int ech_server_parse_outer_ch(WOLFSSL* ssl, byte* chRecord) +{ + byte* chBody = chRecord + RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ; + TLSX* echX; + word32 helloSz = 0; + word16 extLen = 0; + word16 extOff; + int ret; + + ret = TLSX_ServerECH_Use(&ssl->extensions, ssl->heap, ssl->ctx->echConfigs); + if (ret != 0) + return ret; + echX = TLSX_Find(ssl->extensions, TLSX_ECH); + if (echX == NULL) + return WOLFSSL_FATAL_ERROR; + + /* AAD is the outer ClientHello body (the handshake-length bytes) */ + ato24(chRecord + RECORD_HEADER_SZ + 1, &helloSz); + ((WOLFSSL_ECH*)echX->data)->aad = chBody; + ((WOLFSSL_ECH*)echX->data)->aadLen = helloSz; + + extOff = (word16)ech_seek_extensions(chBody, &extLen); + + return TLSX_Parse(ssl, chBody + extOff, extLen, client_hello, + (Suites*)WOLFSSL_SUITES(ssl)); +} + +/* If ECH is accepted, run the decrypted inner extensions through TLSX_Parse */ +static int ech_server_parse_inner_ch(WOLFSSL* ssl) +{ + TLSX* echX = TLSX_Find(ssl->extensions, TLSX_ECH); + WOLFSSL_ECH* ech; + byte* innerBody; + word16 extLen = 0; + word16 extOff; + int ret; + + if (echX == NULL || echX->data == NULL) + return WOLFSSL_FATAL_ERROR; + ech = (WOLFSSL_ECH*)echX->data; + if (ech->innerClientHello == NULL) + return WOLFSSL_FATAL_ERROR; + + innerBody = ech->innerClientHello + HANDSHAKE_HEADER_SZ; + extOff = (word16)ech_seek_extensions(innerBody, &extLen); + + ssl->options.echProcessingInner = 1; + ret = TLSX_Parse(ssl, innerBody + extOff, extLen, client_hello, + (Suites*)WOLFSSL_SUITES(ssl)); + ssl->options.echProcessingInner = 0; + + return ret; +} + +/* test vector for TLSX_Parse */ +typedef struct echSniVec { + const char* desc; /* human-readable label */ + const char* sSNI; /* server-side SNI */ + const char* innerName; /* client private SNI */ + const char* outerName; /* client public SNI */ + const char* authoritative; /* value expected from wolfSSL_SNI_GetRequest */ + int outerRet; /* expected return from parsing the outer CH */ + int innerRet; /* expected return from parsing the inner CH */ + int reject; /* setup client's ECH to be rejected */ + byte sniOpt; /* options for the server SNI */ +} echSniVec; + +#ifdef WOLFSSL_ALWAYS_KEEP_SNI + #define ECH_KEPT(name) (name) +#else + #define ECH_KEPT(name) NULL +#endif + +/* derive the expected SNI status */ +static byte ech_sni_expected_status(const echSniVec* v) +{ + if (v->authoritative == NULL) + return WOLFSSL_SNI_NO_MATCH; + if ((v->sniOpt & WOLFSSL_SNI_ANSWER_ON_MISMATCH) && + XSTRCMP(v->authoritative, echOtherName) == 0) + return WOLFSSL_SNI_FAKE_MATCH; + /* neither the server SNI nor the public name match, + * name kept only because of WOLFSSL_ALWAYS_KEEP_SNI */ + if ((v->sSNI == NULL || XSTRCMP(v->authoritative, v->sSNI) != 0) && + XSTRCMP(v->authoritative, echPublicName) != 0) + return WOLFSSL_SNI_FORCE_KEEP; + return WOLFSSL_SNI_REAL_MATCH; +} + +/* Generate the ClientHello and feed to TLSX_Parse + * On accept, rerun TLSX_Parse over ClientHelloInner + * Validate the authoritative SNI, its match state, and the return values */ +static int test_ech_sni_parse_vec(WOLFSSL_CTX* serverCtx, const echSniVec* v) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + void* sniName = NULL; + int doInner; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.s_ctx = serverCtx; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* server SNI */ + if (v->sSNI != NULL) { + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + v->sSNI, (word16)XSTRLEN(v->sSNI)), WOLFSSL_SUCCESS); + if (v->sniOpt != 0) + wolfSSL_SNI_SetOptions(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + v->sniOpt); + } + + if (v->reject) { + /* modify ECH config if rejection desired */ + ExpectIntEQ(test_ech_set_bad_echconfigs(test_ctx.s_ctx, test_ctx.c_ssl), + TEST_SUCCESS); + } + else { + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echCbTestConfigs, + echCbTestConfigsLen), WOLFSSL_SUCCESS); + } + + /* outerName overrides the public_name the client sends in the clear */ + if (v->outerName != NULL) { + /* tamper the public_name in place; same length keeps the '\0' */ + ExpectNotNull(test_ctx.c_ssl->echConfigs); + ExpectIntEQ((int)XSTRLEN(v->outerName), (int)XSTRLEN(echPublicName)); + if (EXPECT_SUCCESS()) + XMEMCPY(test_ctx.c_ssl->echConfigs->publicName, v->outerName, + XSTRLEN(v->outerName)); + } + + /* client inner SNI */ + if (v->innerName != NULL) + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + v->innerName, (word16)XSTRLEN(v->innerName)), WOLFSSL_SUCCESS); + + /* 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); + + ExpectIntEQ(ech_server_parse_outer_ch(test_ctx.s_ssl, test_ctx.s_buff), + v->outerRet); + + /* ECH acceptance must line up */ + ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, !v->reject); + + /* on accept the inner hello must be parsed to learn the private name */ + doInner = (!v->reject && v->outerRet == 0); + if (doInner) + ExpectIntEQ(ech_server_parse_inner_ch(test_ctx.s_ssl), v->innerRet); + + /* verify the authoritative SNI has the correct name and state */ + if (v->outerRet == 0 && (!doInner || v->innerRet == 0)) { + wolfSSL_SNI_GetRequest(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, &sniName); + if (v->authoritative != NULL) + ExpectStrEQ((const char*)sniName, v->authoritative); + else + ExpectNull(sniName); + ExpectIntEQ(wolfSSL_SNI_Status(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME), + ech_sni_expected_status(v)); + } + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + +/* Generate ClientHello's and feed them through TLSX_Parse to test ECH with a + * variety of SNI configurations */ +static int test_wolfSSL_Tls13_ECH_sni_parse(void) +{ + EXPECT_DECLS; + WOLFSSL_CTX* serverCtx = NULL; + size_t i; + const echSniVec vectors[] = { + /* --- ECH rejected: outer (public) name governs --- */ + { "reject: no sSNI, outer=public", + NULL, echPrivateName, echPublicName, + echPublicName, 0, 0, 1, 0 }, + /* "reject: sSNI=private, outer=public" is the generic ECH-reject + * scenario (server keeps its private SNI, client falls back to the + * public name); it is exercised by most ECH handshake tests. */ + { "reject: sSNI=public, outer=public", + echPublicName, echPrivateName, echPublicName, + echPublicName, 0, 0, 1, 0 }, + { "reject: no sSNI, outer=other", + NULL, echPrivateName, echOtherName, + ECH_KEPT(echOtherName), 0, 0, 1, 0 }, + { "reject: sSNI=private, outer=other", + echPrivateName, echPrivateName, echOtherName, + NULL, WC_NO_ERR_TRACE(UNKNOWN_SNI_HOST_NAME_E), 0, 1, 0 }, + { "reject: sSNI=private+continue, outer=other", + echPrivateName, echPrivateName, echOtherName, + NULL, 0, 0, 1, WOLFSSL_SNI_CONTINUE_ON_MISMATCH }, + { "reject: sSNI=private+answer, outer=other", + echPrivateName, echPrivateName, echOtherName, + echOtherName, 0, 0, 1, WOLFSSL_SNI_ANSWER_ON_MISMATCH }, + + /* --- ECH accepted: inner (private) name governs --- */ + { "accept: no sSNI, no inner", + NULL, NULL, echPublicName, + NULL, 0, 0, 0, 0 }, + { "accept: no sSNI, inner=private", + NULL, echPrivateName, echPublicName, + ECH_KEPT(echPrivateName), 0, 0, 0, 0 }, + /* "accept: sSNI=private, inner=private" is the generic ECH-accept + * scenario (private inner name matches the server SNI); it is + * exercised by most ECH handshake tests. */ + { "accept: sSNI=private+abort, no inner", + echPrivateName, NULL, echPublicName, + NULL, 0, WC_NO_ERR_TRACE(SNI_ABSENT_ERROR), 0, + WOLFSSL_SNI_ABORT_ON_ABSENCE }, + { "accept: sSNI=private, inner=other", + echPrivateName, echOtherName, echPublicName, + NULL, 0, WC_NO_ERR_TRACE(UNKNOWN_SNI_HOST_NAME_E), 0, 0 }, + { "accept: sSNI=private+continue, inner=other", + echPrivateName, echOtherName, echPublicName, + NULL, 0, 0, 0, WOLFSSL_SNI_CONTINUE_ON_MISMATCH }, + { "accept: sSNI=private+answer, inner=other", + echPrivateName, echOtherName, echPublicName, + echOtherName, 0, 0, 0, WOLFSSL_SNI_ANSWER_ON_MISMATCH }, + { "accept: sSNI=public, inner=public", + echPublicName, echPublicName, echPublicName, + echPublicName, 0, 0, 0, 0 }, + { "accept: sSNI=public, inner=private", + echPublicName, echPrivateName, echPublicName, + NULL, 0, WC_NO_ERR_TRACE(UNKNOWN_SNI_HOST_NAME_E), 0, 0 }, + + /* --- ECH accepted but outer SNI != public name --- */ + { "accept: outer=other -> mismatch abort", NULL, echPrivateName, + echOtherName, NULL, WC_NO_ERR_TRACE(INVALID_PARAMETER), + 0, 0, 0 }, + }; + + /* One server CTX, reused by every vector. test_ssl_memio_setup treats a + * pre-set s_ctx as shared, so cleanup leaves it for the next vector. */ + ExpectNotNull(serverCtx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_use_certificate_chain_file(serverCtx, svrCertFile), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(serverCtx, svrKeyFile, + CERT_FILETYPE), WOLFSSL_SUCCESS); + ExpectIntEQ(test_ech_server_ctx_ready(serverCtx), TEST_SUCCESS); + + for (i = 0; i < XELEM_CNT(vectors) && !EXPECT_FAIL(); i++) { + printf("\tTesting %s...\n", vectors[i].desc); + ExpectIntEQ(test_ech_sni_parse_vec(serverCtx, &vectors[i]), + TEST_SUCCESS); + } + + wolfSSL_CTX_free(serverCtx); + + return EXPECT_RESULT(); +} + +#undef ECH_KEPT #endif /* HAVE_SSL_MEMIO_TESTS_DEPENDENCIES */ /* verify that ECH can be enabled/disabled without issue */ @@ -15800,7 +16139,7 @@ static int test_wolfSSL_Tls13_ECH_enable_disable(void) #if !defined(NO_WOLFSSL_CLIENT) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; - byte echConfigs[128]; + byte echConfigs[ECH_CONFIG_LEN]; word32 echConfigsLen = sizeof(echConfigs); /* NULL ctx, NULL ssl should not crash */ @@ -35622,6 +35961,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), @@ -35633,7 +35973,6 @@ TEST_CASE testCases[] = { #if defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) TEST_DECL(test_wolfSSL_Tls13_ECH_all_algos), TEST_DECL(test_wolfSSL_Tls13_ECH_no_private_name), - TEST_DECL(test_wolfSSL_Tls13_ECH_bad_configs), TEST_DECL(test_wolfSSL_Tls13_ECH_retry_configs), TEST_DECL(test_wolfSSL_Tls13_ECH_retry_configs_bad), TEST_DECL(test_wolfSSL_Tls13_ECH_retry_configs_auth_fail), @@ -35647,6 +35986,7 @@ TEST_CASE testCases[] = { 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_sni_parse), #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 323090264d1..0d58a9da76f 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3182,6 +3182,17 @@ 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); + +#ifdef WOLFSSL_API_PREFIX_MAP + #define TLSX_ServerECH_Use wolfSSL_TLSX_ServerECH_Use +#endif +WOLFSSL_TEST_VIS int TLSX_ServerECH_Use(TLSX** extensions, void* heap, + WOLFSSL_EchConfig* configs); WOLFSSL_LOCAL int SetEchConfigsEx(WOLFSSL_EchConfig** outputConfigs, void* heap, const byte* echConfigs, word32 echConfigsLen);