@@ -15083,7 +15083,8 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(int hrr)
1508315083 WOLFSSL_CTX* tempCtx = NULL;
1508415084 byte badConfigs[256];
1508515085 word32 badConfigsLen = sizeof(badConfigs);
15086- word32 retryConfigsLen = sizeof(badConfigs); /* non-zero sentinel */
15086+ word32 retryConfigsLen = sizeof(badConfigs);
15087+ const char* badPublicName = "ech-public-name.com";
1508715088
1508815089 XMEMSET(&test_ctx, 0, sizeof(test_ctx));
1508915090 test_ctx.s_cb.method = wolfTLSv1_3_server_method;
@@ -15092,10 +15093,8 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(int hrr)
1509215093
1509315094 ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS);
1509415095
15095- /* wrong ECH config so server sends retry_configs in EncryptedExtensions;
15096- * use a public name that does NOT appear in the server cert so that the
15097- * outer handshake cert check fails with DOMAIN_NAME_MISMATCH */
15098- const char* badPublicName = "ech-public-name.com";
15096+ /* generate mismatched ECH configs so retry_configs are sent
15097+ * and use a bad public name so auth fails in outer hello */
1509915098 ExpectNotNull(tempCtx = wolfSSL_CTX_new(wolfTLSv1_3_server_method()));
1510015099 ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(tempCtx, badPublicName,
1510115100 0, 0, 0), WOLFSSL_SUCCESS);
@@ -15115,15 +15114,15 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(int hrr)
1511515114 wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_NONE, NULL);
1511615115 wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL);
1511715116
15118- /* outer handshake uses badPublicName, which doesn't match the server cert */
15117+ /* use badPublicName so ECH public name matches */
1511915118 ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME,
1512015119 badPublicName, (word16)XSTRLEN(badPublicName)),
1512115120 WOLFSSL_SUCCESS);
1512215121
1512315122 if (hrr)
1512415123 ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS);
1512515124
15126- /* handshake fails due to auth failure in outer handshake, not ech_required */
15125+ /* auth failure in outer handshake, not ech_required */
1512715126 ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS);
1512815127 ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0),
1512915128 WC_NO_ERR_TRACE(DOMAIN_NAME_MISMATCH));
@@ -15424,14 +15423,8 @@ static int test_wolfSSL_Tls13_ECH_long_SNI(void)
1542415423}
1542515424
1542615425/* Test the HRR ECH rejection fallback path:
15427- * client offers ECH, HRR is triggered, server sends HRR without ECH extension
15428- * (confBuf == NULL), client frees hsHashesEch and falls back to the outer
15429- * transcript, then aborts with ech_required.
15430- *
15431- * When disableECH is set on the server the ECH-aware SNI matching in
15432- * TLSX_SNI_Parse (which normally accepts the ECH public name) is bypassed, so
15433- * the server SNI must be set explicitly to avoid an unrecognized_name fatal
15434- * alert before HRR is even sent. */
15426+ * client offers ECH, HRR is triggered, server sends HRR without ECH extension,
15427+ * client falls back to the outer transcript, then aborts with ech_required. */
1543515428static int test_wolfSSL_Tls13_ECH_HRR_rejection(void)
1543615429{
1543715430 EXPECT_DECLS;
@@ -15442,9 +15435,7 @@ static int test_wolfSSL_Tls13_ECH_HRR_rejection(void)
1544215435 test_ctx.s_cb.method = wolfTLSv1_3_server_method;
1544315436 test_ctx.c_cb.method = wolfTLSv1_3_client_method;
1544415437
15445- /* Server generates ECH config (public name = echCbTestPublicName =
15446- * "example.com", which appears in the server cert SAN so cert
15447- * verification passes when ECH is rejected and the outer name is used) */
15438+ /* Server generates ECH config with good public name */
1544815439 test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready;
1544915440 /* Client sets the correct ECH config and private SNI */
1545015441 test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready;
@@ -15456,20 +15447,18 @@ static int test_wolfSSL_Tls13_ECH_HRR_rejection(void)
1545615447 wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL);
1545715448
1545815449 /* Disable ECH on the server SSL object: the server ignores ECH in CH1 and
15459- * sends HRR without an ECH extension (confBuf stays NULL on the client).
15460- * Because ECH is disabled, the SNI code's ECH-aware public-name fallback
15461- * is skipped; set the server SNI to the public name explicitly. */
15450+ * sends HRR without an ECH extension (confBuf stays NULL on the client) */
1546215451 wolfSSL_SetEchEnable(test_ctx.s_ssl, 0);
1546315452 ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME,
1546415453 echCbTestPublicName, (word16)XSTRLEN(echCbTestPublicName)),
1546515454 WOLFSSL_SUCCESS);
1546615455
15467- /* Force HRR so the code path at tls13.c:5717-5725 is exercised:
15468- * client receives HRR with no ECH extension, detects confBuf == NULL
15469- * and frees hsHashesEch, falling back to the outer transcript */
15456+ /* Force HRR so client receives HRR with no ECH extension,
15457+ * detects confBuf == NULL and frees hsHashesEch, falling back to the
15458+ * outer transcript */
1547015459 ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS);
1547115460
15472- /* Handshake must fail: client aborts with ech_required after Finished */
15461+ /* Handshake must fail: client aborts with ech_required */
1547315462 ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS);
1547415463 ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0);
1547515464 /* hsHashesEch must have been freed by the HRR rejection code path */
@@ -15482,9 +15471,9 @@ static int test_wolfSSL_Tls13_ECH_HRR_rejection(void)
1548215471 return EXPECT_RESULT();
1548315472}
1548415473
15485- /* RFC 9849 §6.1.5: server must abort if CH2 omits the ECH extension after the
15486- * server accepted ECH in the HRR round. */
15487- static int test_wolfSSL_Tls13_ECH_HRR_ch2_no_ech (void)
15474+ /* verify the server aborts if CH2 omits the ECH extension after the server
15475+ * accepted ECH in the HRR round */
15476+ static int test_wolfSSL_Tls13_ECH_ch2_no_ech (void)
1548815477{
1548915478 EXPECT_DECLS;
1549015479 test_ssl_memio_ctx test_ctx;
@@ -15514,7 +15503,7 @@ static int test_wolfSSL_Tls13_ECH_HRR_ch2_no_ech(void)
1551415503 /* disable ECH on the client so CH2 omits the ECH extension entirely */
1551515504 wolfSSL_SetEchEnable(test_ctx.c_ssl, 0);
1551615505
15517- /* rest of handshake must fail: server enforces RFC 9849 §6.1.5 */
15506+ /* rest of handshake must fail */
1551815507 ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS);
1551915508 ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0),
1552015509 WC_NO_ERR_TRACE(INCOMPLETE_DATA));
@@ -15524,6 +15513,65 @@ static int test_wolfSSL_Tls13_ECH_HRR_ch2_no_ech(void)
1552415513 return EXPECT_RESULT();
1552515514}
1552615515
15516+ /* verify that a decryption failure in CH2 is caught
15517+ * this also verifies that HPKE context is correctly reused */
15518+ static int test_wolfSSL_Tls13_ECH_ch2_decrypt_error(void)
15519+ {
15520+ EXPECT_DECLS;
15521+ test_ssl_memio_ctx test_ctx;
15522+ int i;
15523+
15524+ XMEMSET(&test_ctx, 0, sizeof(test_ctx));
15525+
15526+ test_ctx.s_cb.method = wolfTLSv1_3_server_method;
15527+ test_ctx.c_cb.method = wolfTLSv1_3_client_method;
15528+
15529+ test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready;
15530+ test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready;
15531+ test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready;
15532+
15533+ ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS);
15534+
15535+ /* Withhold key shares so the server is forced to send HRR */
15536+ ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS);
15537+
15538+ /* One round: client sends CH1, server processes it and sends HRR */
15539+ (void)test_ssl_memio_do_handshake(&test_ctx, 1, NULL);
15540+
15541+ ExpectIntEQ(test_ctx.s_ssl->options.serverState,
15542+ SERVER_HELLO_RETRY_REQUEST_COMPLETE);
15543+ ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, 1);
15544+
15545+ if (EXPECT_SUCCESS()) {
15546+ /* Client reads HRR and writes CH2 into s_buff */
15547+ (void)wolfSSL_connect(test_ctx.c_ssl);
15548+
15549+ /* Corrupt one byte of the ECH ciphertext in the CH2 record in s_buff.
15550+ * ECH outer extension layout after the 0xFE0D type marker:
15551+ * extLen(2) + outerType(1) + kdfId(2) + aeadId(2) + configId(1)
15552+ * + encLen(2, always 0 in CH2) + payloadLen(2) = 12 bytes, so the
15553+ * ciphertext starts 14 bytes past the first 0xFE byte. */
15554+ for (i = 0; i < test_ctx.s_len - 1; i++) {
15555+ if (test_ctx.s_buff[i] == 0xFE && test_ctx.s_buff[i + 1] == 0x0D) {
15556+ if (i + 14 < test_ctx.s_len)
15557+ test_ctx.s_buff[i + 14] ^= 0xFF;
15558+ break;
15559+ }
15560+ }
15561+
15562+ /* Server processes the corrupted CH2.
15563+ * hpkeContext is preserved, TLSX_ECH_Parse correctly identifies the CH2
15564+ * round and sends decrypt_error. */
15565+ (void)wolfSSL_accept(test_ctx.s_ssl);
15566+ ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0),
15567+ WC_NO_ERR_TRACE(DECRYPT_ERROR));
15568+ }
15569+
15570+ test_ssl_memio_cleanup(&test_ctx);
15571+
15572+ return EXPECT_RESULT();
15573+ }
15574+
1552715575/* when ECH is rejected the certificate must match the public name of the chosen
1552815576 * ech config
1552915577 * the cert check should pass and the client aborts with ech_required */
@@ -37012,7 +37060,8 @@ TEST_CASE testCases[] = {
3701237060 TEST_DECL(test_wolfSSL_Tls13_ECH_disable_conn),
3701337061 TEST_DECL(test_wolfSSL_Tls13_ECH_long_SNI),
3701437062 TEST_DECL(test_wolfSSL_Tls13_ECH_HRR_rejection),
37015- TEST_DECL(test_wolfSSL_Tls13_ECH_HRR_ch2_no_ech),
37063+ TEST_DECL(test_wolfSSL_Tls13_ECH_ch2_no_ech),
37064+ TEST_DECL(test_wolfSSL_Tls13_ECH_ch2_decrypt_error),
3701637065 TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_cert_valid),
3701737066 TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_empty_client_cert),
3701837067#endif
0 commit comments