@@ -15069,7 +15069,8 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(int hrr)
1506915069 WOLFSSL_CTX* tempCtx = NULL;
1507015070 byte badConfigs[256];
1507115071 word32 badConfigsLen = sizeof(badConfigs);
15072- word32 retryConfigsLen = sizeof(badConfigs); /* non-zero sentinel */
15072+ word32 retryConfigsLen = sizeof(badConfigs);
15073+ const char* badPublicName = "ech-public-name.com";
1507315074
1507415075 XMEMSET(&test_ctx, 0, sizeof(test_ctx));
1507515076 test_ctx.s_cb.method = wolfTLSv1_3_server_method;
@@ -15078,10 +15079,8 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(int hrr)
1507815079
1507915080 ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS);
1508015081
15081- /* wrong ECH config so server sends retry_configs in EncryptedExtensions;
15082- * use a public name that does NOT appear in the server cert so that the
15083- * outer handshake cert check fails with DOMAIN_NAME_MISMATCH */
15084- const char* badPublicName = "ech-public-name.com";
15082+ /* generate mismatched ECH configs so retry_configs are sent
15083+ * and use a bad public name so auth fails in outer hello */
1508515084 ExpectNotNull(tempCtx = wolfSSL_CTX_new(wolfTLSv1_3_server_method()));
1508615085 ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(tempCtx, badPublicName,
1508715086 0, 0, 0), WOLFSSL_SUCCESS);
@@ -15101,15 +15100,15 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(int hrr)
1510115100 wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_NONE, NULL);
1510215101 wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL);
1510315102
15104- /* outer handshake uses badPublicName, which doesn't match the server cert */
15103+ /* use badPublicName so ECH public name matches */
1510515104 ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME,
1510615105 badPublicName, (word16)XSTRLEN(badPublicName)),
1510715106 WOLFSSL_SUCCESS);
1510815107
1510915108 if (hrr)
1511015109 ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS);
1511115110
15112- /* handshake fails due to auth failure in outer handshake, not ech_required */
15111+ /* auth failure in outer handshake, not ech_required */
1511315112 ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS);
1511415113 ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0),
1511515114 WC_NO_ERR_TRACE(DOMAIN_NAME_MISMATCH));
@@ -15410,14 +15409,8 @@ static int test_wolfSSL_Tls13_ECH_long_SNI(void)
1541015409}
1541115410
1541215411/* Test the HRR ECH rejection fallback path:
15413- * client offers ECH, HRR is triggered, server sends HRR without ECH extension
15414- * (confBuf == NULL), client frees hsHashesEch and falls back to the outer
15415- * transcript, then aborts with ech_required.
15416- *
15417- * When disableECH is set on the server the ECH-aware SNI matching in
15418- * TLSX_SNI_Parse (which normally accepts the ECH public name) is bypassed, so
15419- * the server SNI must be set explicitly to avoid an unrecognized_name fatal
15420- * alert before HRR is even sent. */
15412+ * client offers ECH, HRR is triggered, server sends HRR without ECH extension,
15413+ * client falls back to the outer transcript, then aborts with ech_required. */
1542115414static int test_wolfSSL_Tls13_ECH_HRR_rejection(void)
1542215415{
1542315416 EXPECT_DECLS;
@@ -15428,9 +15421,7 @@ static int test_wolfSSL_Tls13_ECH_HRR_rejection(void)
1542815421 test_ctx.s_cb.method = wolfTLSv1_3_server_method;
1542915422 test_ctx.c_cb.method = wolfTLSv1_3_client_method;
1543015423
15431- /* Server generates ECH config (public name = echCbTestPublicName =
15432- * "example.com", which appears in the server cert SAN so cert
15433- * verification passes when ECH is rejected and the outer name is used) */
15424+ /* Server generates ECH config with good public name */
1543415425 test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready;
1543515426 /* Client sets the correct ECH config and private SNI */
1543615427 test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready;
@@ -15442,20 +15433,18 @@ static int test_wolfSSL_Tls13_ECH_HRR_rejection(void)
1544215433 wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL);
1544315434
1544415435 /* Disable ECH on the server SSL object: the server ignores ECH in CH1 and
15445- * sends HRR without an ECH extension (confBuf stays NULL on the client).
15446- * Because ECH is disabled, the SNI code's ECH-aware public-name fallback
15447- * is skipped; set the server SNI to the public name explicitly. */
15436+ * sends HRR without an ECH extension (confBuf stays NULL on the client) */
1544815437 wolfSSL_SetEchEnable(test_ctx.s_ssl, 0);
1544915438 ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME,
1545015439 echCbTestPublicName, (word16)XSTRLEN(echCbTestPublicName)),
1545115440 WOLFSSL_SUCCESS);
1545215441
15453- /* Force HRR so the code path at tls13.c:5717-5725 is exercised:
15454- * client receives HRR with no ECH extension, detects confBuf == NULL
15455- * and frees hsHashesEch, falling back to the outer transcript */
15442+ /* Force HRR so client receives HRR with no ECH extension,
15443+ * detects confBuf == NULL and frees hsHashesEch, falling back to the
15444+ * outer transcript */
1545615445 ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS);
1545715446
15458- /* Handshake must fail: client aborts with ech_required after Finished */
15447+ /* Handshake must fail: client aborts with ech_required */
1545915448 ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS);
1546015449 ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0);
1546115450 /* hsHashesEch must have been freed by the HRR rejection code path */
@@ -15468,9 +15457,9 @@ static int test_wolfSSL_Tls13_ECH_HRR_rejection(void)
1546815457 return EXPECT_RESULT();
1546915458}
1547015459
15471- /* RFC 9849 §6.1.5: server must abort if CH2 omits the ECH extension after the
15472- * server accepted ECH in the HRR round. */
15473- static int test_wolfSSL_Tls13_ECH_HRR_ch2_no_ech (void)
15460+ /* verify the server aborts if CH2 omits the ECH extension after the server
15461+ * accepted ECH in the HRR round */
15462+ static int test_wolfSSL_Tls13_ECH_ch2_no_ech (void)
1547415463{
1547515464 EXPECT_DECLS;
1547615465 test_ssl_memio_ctx test_ctx;
@@ -15500,7 +15489,7 @@ static int test_wolfSSL_Tls13_ECH_HRR_ch2_no_ech(void)
1550015489 /* disable ECH on the client so CH2 omits the ECH extension entirely */
1550115490 wolfSSL_SetEchEnable(test_ctx.c_ssl, 0);
1550215491
15503- /* rest of handshake must fail: server enforces RFC 9849 §6.1.5 */
15492+ /* rest of handshake must fail */
1550415493 ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS);
1550515494 ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0),
1550615495 WC_NO_ERR_TRACE(INCOMPLETE_DATA));
@@ -15510,6 +15499,65 @@ static int test_wolfSSL_Tls13_ECH_HRR_ch2_no_ech(void)
1551015499 return EXPECT_RESULT();
1551115500}
1551215501
15502+ /* verify that a decryption failure in CH2 is caught
15503+ * this also verifies that HPKE context is correctly reused */
15504+ static int test_wolfSSL_Tls13_ECH_ch2_decrypt_error(void)
15505+ {
15506+ EXPECT_DECLS;
15507+ test_ssl_memio_ctx test_ctx;
15508+ int i;
15509+
15510+ XMEMSET(&test_ctx, 0, sizeof(test_ctx));
15511+
15512+ test_ctx.s_cb.method = wolfTLSv1_3_server_method;
15513+ test_ctx.c_cb.method = wolfTLSv1_3_client_method;
15514+
15515+ test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready;
15516+ test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready;
15517+ test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready;
15518+
15519+ ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS);
15520+
15521+ /* Withhold key shares so the server is forced to send HRR */
15522+ ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS);
15523+
15524+ /* One round: client sends CH1, server processes it and sends HRR */
15525+ (void)test_ssl_memio_do_handshake(&test_ctx, 1, NULL);
15526+
15527+ ExpectIntEQ(test_ctx.s_ssl->options.serverState,
15528+ SERVER_HELLO_RETRY_REQUEST_COMPLETE);
15529+ ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, 1);
15530+
15531+ if (EXPECT_SUCCESS()) {
15532+ /* Client reads HRR and writes CH2 into s_buff */
15533+ (void)wolfSSL_connect(test_ctx.c_ssl);
15534+
15535+ /* Corrupt one byte of the ECH ciphertext in the CH2 record in s_buff.
15536+ * ECH outer extension layout after the 0xFE0D type marker:
15537+ * extLen(2) + outerType(1) + kdfId(2) + aeadId(2) + configId(1)
15538+ * + encLen(2, always 0 in CH2) + payloadLen(2) = 12 bytes, so the
15539+ * ciphertext starts 14 bytes past the first 0xFE byte. */
15540+ for (i = 0; i < test_ctx.s_len - 1; i++) {
15541+ if (test_ctx.s_buff[i] == 0xFE && test_ctx.s_buff[i + 1] == 0x0D) {
15542+ if (i + 14 < test_ctx.s_len)
15543+ test_ctx.s_buff[i + 14] ^= 0xFF;
15544+ break;
15545+ }
15546+ }
15547+
15548+ /* Server processes the corrupted CH2.
15549+ * hpkeContext is preserved, TLSX_ECH_Parse correctly identifies the CH2
15550+ * round and sends decrypt_error. */
15551+ (void)wolfSSL_accept(test_ctx.s_ssl);
15552+ ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0),
15553+ WC_NO_ERR_TRACE(DECRYPT_ERROR));
15554+ }
15555+
15556+ test_ssl_memio_cleanup(&test_ctx);
15557+
15558+ return EXPECT_RESULT();
15559+ }
15560+
1551315561/* when ECH is rejected the certificate must match the public name of the chosen
1551415562 * ech config
1551515563 * the cert check should pass and the client aborts with ech_required */
@@ -36998,7 +37046,8 @@ TEST_CASE testCases[] = {
3699837046 TEST_DECL(test_wolfSSL_Tls13_ECH_disable_conn),
3699937047 TEST_DECL(test_wolfSSL_Tls13_ECH_long_SNI),
3700037048 TEST_DECL(test_wolfSSL_Tls13_ECH_HRR_rejection),
37001- TEST_DECL(test_wolfSSL_Tls13_ECH_HRR_ch2_no_ech),
37049+ TEST_DECL(test_wolfSSL_Tls13_ECH_ch2_no_ech),
37050+ TEST_DECL(test_wolfSSL_Tls13_ECH_ch2_decrypt_error),
3700237051 TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_cert_valid),
3700337052 TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_empty_client_cert),
3700437053#endif
0 commit comments