Skip to content

Commit 6b0dc35

Browse files
improve ECH tests even further
fix HPKE error propagation in CH2
1 parent c309f5e commit 6b0dc35

File tree

5 files changed

+99
-40
lines changed

5 files changed

+99
-40
lines changed

src/ssl_ech.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,13 +454,14 @@ int wolfSSL_GetEchConfigs(WOLFSSL* ssl, byte* output, word32* outputLen)
454454
}
455455

456456
/* wrapper function to get retry configs
457+
* a client should only call this after the 'wolfSSL_connect()' call fails
457458
* returns error if retry configs were not received or were malformed */
458459
int wolfSSL_GetEchRetryConfigs(WOLFSSL* ssl, byte* output, word32* outputLen)
459460
{
460461
if (ssl == NULL || outputLen == NULL)
461462
return BAD_FUNC_ARG;
462463

463-
if (ssl->echRetryConfigs == NULL || !ssl->echRetryConfigsAccepted) {
464+
if (ssl->echRetryConfigs == NULL || !ssl->options.echRetryConfigsAccepted) {
464465
return WOLFSSL_FATAL_ERROR;
465466
}
466467

src/tls.c

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13915,7 +13915,7 @@ static const byte* TLSX_ECH_FindOuterExtension(const byte* outerCh,
1391513915
* newInnerCh The inner ClientHello buffer.
1391613916
* newInnerChLen Inner ClientHello length.
1391713917
* numOuterRefs Number of references described by OuterExtensions extension.
13918-
* numOuterTypes References described by OuterExtensions extension.
13918+
* OuterRefTypes References described by OuterExtensions extension.
1391913919
* returns 0 on success and otherwise failure.
1392013920
*/
1392113921
static int TLSX_ECH_CopyOuterExtensions(const byte* outerCh, word32 outerChLen,
@@ -13930,6 +13930,10 @@ static int TLSX_ECH_CopyOuterExtensions(const byte* outerCh, word32 outerChLen,
1393013930
word16 extsLen = 0;
1393113931
const byte* outerExtData;
1393213932

13933+
if (newInnerCh == NULL) {
13934+
*newInnerChLen = 0;
13935+
}
13936+
1393313937
while (numOuterRefs-- > 0) {
1393413938
ato16(outerRefTypes, &refType);
1393513939

@@ -13995,7 +13999,7 @@ static int TLSX_ECH_ExpandOuterExtensions(WOLFSSL* ssl, WOLFSSL_ECH* ech,
1399513999
int foundEchOuter = 0;
1399614000
word16 numOuterRefs = 0;
1399714001
const byte* outerRefTypes = NULL;
13998-
word32 extraSize = 0;
14002+
word32 extraSize;
1399914003
byte* newInnerCh = NULL;
1400014004
byte* newInnerChRef;
1400114005
word32 newInnerChLen;
@@ -14166,6 +14170,7 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig,
1416614170
{
1416714171
int ret = 0;
1416814172
int i;
14173+
int allocatedHpke = 0;
1416914174
word32 rawConfigLen = 0;
1417014175
byte* info = NULL;
1417114176
word32 infoLen = 0;
@@ -14186,6 +14191,7 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig,
1418614191
}
1418714192
/* check if hpke already exists, may if HelloRetryRequest */
1418814193
if (ech->hpke == NULL) {
14194+
allocatedHpke = 1;
1418914195
ech->hpke = (Hpke*)XMALLOC(sizeof(Hpke), heap, DYNAMIC_TYPE_TMP_BUFFER);
1419014196
if (ech->hpke == NULL)
1419114197
ret = MEMORY_E;
@@ -14234,8 +14240,9 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig,
1423414240
ech->outerClientPayload, ech->innerClientHelloLen,
1423514241
ech->innerClientHello + HANDSHAKE_HEADER_SZ);
1423614242
}
14237-
/* free the hpke and context on failure */
14238-
if (ret != 0) {
14243+
/* only free hpke/hpkeContext if allocated in this call; otherwise preserve
14244+
* them for clientHello2 */
14245+
if (ret != 0 && allocatedHpke) {
1423914246
XFREE(ech->hpke, heap, DYNAMIC_TYPE_TMP_BUFFER);
1424014247
ech->hpke = NULL;
1424114248
XFREE(ech->hpkeContext, heap, DYNAMIC_TYPE_TMP_BUFFER);
@@ -14295,7 +14302,7 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size,
1429514302

1429614303
/* retry configs may only be accepted at the point when ECH_REQUIRED is
1429714304
* sent */
14298-
ssl->echRetryConfigsAccepted = 0;
14305+
ssl->options.echRetryConfigsAccepted = 0;
1429914306
}
1430014307
/* HRR with special confirmation */
1430114308
else if (msgType == hello_retry_request && ssl->echConfigs != NULL) {

src/tls13.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14131,7 +14131,9 @@ int wolfSSL_connect_TLSv13(WOLFSSL* ssl)
1413114131
* send ech_required alert and abort before returning to the app */
1413214132
if (ssl->echConfigs != NULL && !ssl->options.disableECH &&
1413314133
!ssl->options.echAccepted) {
14134-
ssl->echRetryConfigsAccepted = 1;
14134+
if (ssl->echRetryConfigs != NULL) {
14135+
ssl->options.echRetryConfigsAccepted = 1;
14136+
}
1413514137
SendAlert(ssl, alert_fatal, ech_required);
1413614138
ssl->error = ECH_REQUIRED_E;
1413714139
WOLFSSL_ERROR_VERBOSE(ECH_REQUIRED_E);

tests/api.c

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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. */
1542115414
static 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

wolfssl/internal.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5163,8 +5163,9 @@ struct Options {
51635163
#endif /* WOLFSSL_DTLS_CID */
51645164
#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH)
51655165
word16 echAccepted:1;
5166-
word16 disableECH:1; /* Did the user disable ech */
5167-
word16 echProcessingInner:1; /* Processing the inner hello */
5166+
word16 disableECH:1; /* Did the user disable ech */
5167+
word16 echProcessingInner:1; /* Processing the inner hello */
5168+
word16 echRetryConfigsAccepted:1;
51685169
#endif
51695170
#ifdef WOLFSSL_SEND_HRR_COOKIE
51705171
word16 cookieGood:1;
@@ -6484,7 +6485,6 @@ struct WOLFSSL {
64846485
#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH)
64856486
WOLFSSL_EchConfig* echConfigs;
64866487
WOLFSSL_EchConfig* echRetryConfigs;
6487-
byte echRetryConfigsAccepted:1;
64886488
#endif
64896489
#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) && defined(WOLFSSL_TEST_ECH)
64906490
/* Test-only hook: called on the client before ECH encryption, after the

0 commit comments

Comments
 (0)