Skip to content

Commit e77a5f2

Browse files
improve ECH tests even further
fix HPKE error propagation in CH2
1 parent 1051590 commit e77a5f2

5 files changed

Lines changed: 99 additions & 40 deletions

File tree

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
@@ -13998,7 +13998,7 @@ static const byte* TLSX_ECH_FindOuterExtension(const byte* outerCh,
1399813998
* newInnerCh The inner ClientHello buffer.
1399913999
* newInnerChLen Inner ClientHello length.
1400014000
* numOuterRefs Number of references described by OuterExtensions extension.
14001-
* numOuterTypes References described by OuterExtensions extension.
14001+
* OuterRefTypes References described by OuterExtensions extension.
1400214002
* returns 0 on success and otherwise failure.
1400314003
*/
1400414004
static int TLSX_ECH_CopyOuterExtensions(const byte* outerCh, word32 outerChLen,
@@ -14013,6 +14013,10 @@ static int TLSX_ECH_CopyOuterExtensions(const byte* outerCh, word32 outerChLen,
1401314013
word16 extsLen = 0;
1401414014
const byte* outerExtData;
1401514015

14016+
if (newInnerCh == NULL) {
14017+
*newInnerChLen = 0;
14018+
}
14019+
1401614020
while (numOuterRefs-- > 0) {
1401714021
ato16(outerRefTypes, &refType);
1401814022

@@ -14078,7 +14082,7 @@ static int TLSX_ECH_ExpandOuterExtensions(WOLFSSL* ssl, WOLFSSL_ECH* ech,
1407814082
int foundEchOuter = 0;
1407914083
word16 numOuterRefs = 0;
1408014084
const byte* outerRefTypes = NULL;
14081-
word32 extraSize = 0;
14085+
word32 extraSize;
1408214086
byte* newInnerCh = NULL;
1408314087
byte* newInnerChRef;
1408414088
word32 newInnerChLen;
@@ -14249,6 +14253,7 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig,
1424914253
{
1425014254
int ret = 0;
1425114255
int i;
14256+
int allocatedHpke = 0;
1425214257
word32 rawConfigLen = 0;
1425314258
byte* info = NULL;
1425414259
word32 infoLen = 0;
@@ -14269,6 +14274,7 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig,
1426914274
}
1427014275
/* check if hpke already exists, may if HelloRetryRequest */
1427114276
if (ech->hpke == NULL) {
14277+
allocatedHpke = 1;
1427214278
ech->hpke = (Hpke*)XMALLOC(sizeof(Hpke), heap, DYNAMIC_TYPE_TMP_BUFFER);
1427314279
if (ech->hpke == NULL)
1427414280
ret = MEMORY_E;
@@ -14317,8 +14323,9 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig,
1431714323
ech->outerClientPayload, ech->innerClientHelloLen,
1431814324
ech->innerClientHello + HANDSHAKE_HEADER_SZ);
1431914325
}
14320-
/* free the hpke and context on failure */
14321-
if (ret != 0) {
14326+
/* only free hpke/hpkeContext if allocated in this call; otherwise preserve
14327+
* them for clientHello2 */
14328+
if (ret != 0 && allocatedHpke) {
1432214329
XFREE(ech->hpke, heap, DYNAMIC_TYPE_TMP_BUFFER);
1432314330
ech->hpke = NULL;
1432414331
XFREE(ech->hpkeContext, heap, DYNAMIC_TYPE_TMP_BUFFER);
@@ -14378,7 +14385,7 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size,
1437814385

1437914386
/* retry configs may only be accepted at the point when ECH_REQUIRED is
1438014387
* sent */
14381-
ssl->echRetryConfigsAccepted = 0;
14388+
ssl->options.echRetryConfigsAccepted = 0;
1438214389
}
1438314390
/* HRR with special confirmation */
1438414391
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
@@ -14308,7 +14308,9 @@ int wolfSSL_connect_TLSv13(WOLFSSL* ssl)
1430814308
* send ech_required alert and abort before returning to the app */
1430914309
if (ssl->echConfigs != NULL && !ssl->options.disableECH &&
1431014310
!ssl->options.echAccepted) {
14311-
ssl->echRetryConfigsAccepted = 1;
14311+
if (ssl->echRetryConfigs != NULL) {
14312+
ssl->options.echRetryConfigsAccepted = 1;
14313+
}
1431214314
SendAlert(ssl, alert_fatal, ech_required);
1431314315
ssl->error = ECH_REQUIRED_E;
1431414316
WOLFSSL_ERROR_VERBOSE(ECH_REQUIRED_E);

tests/api.c

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

wolfssl/internal.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5184,8 +5184,9 @@ struct Options {
51845184
#endif /* WOLFSSL_DTLS_CID */
51855185
#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH)
51865186
word16 echAccepted:1;
5187-
word16 disableECH:1; /* Did the user disable ech */
5188-
word16 echProcessingInner:1; /* Processing the inner hello */
5187+
word16 disableECH:1; /* Did the user disable ech */
5188+
word16 echProcessingInner:1; /* Processing the inner hello */
5189+
word16 echRetryConfigsAccepted:1;
51895190
#endif
51905191
#ifdef WOLFSSL_SEND_HRR_COOKIE
51915192
word16 cookieGood:1;
@@ -6505,7 +6506,6 @@ struct WOLFSSL {
65056506
#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH)
65066507
WOLFSSL_EchConfig* echConfigs;
65076508
WOLFSSL_EchConfig* echRetryConfigs;
6508-
byte echRetryConfigsAccepted:1;
65096509
#endif
65106510
#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) && defined(WOLFSSL_TEST_ECH)
65116511
/* Test-only hook: called on the client before ECH encryption, after the

0 commit comments

Comments
 (0)