From 335714d6587d3c4c258af63cf1fbc539b12550f2 Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Mon, 11 May 2026 11:09:42 -0600 Subject: [PATCH] API to get ECH connection status --- src/ssl_ech.c | 56 +++++++++++++++++++++++++ src/tls.c | 11 ++--- tests/api.c | 113 +++++++++++++++++++++++++++++++++++++++----------- wolfssl/ssl.h | 6 +++ 4 files changed, 153 insertions(+), 33 deletions(-) diff --git a/src/ssl_ech.c b/src/ssl_ech.c index c085468238..3760501fdb 100644 --- a/src/ssl_ech.c +++ b/src/ssl_ech.c @@ -498,6 +498,62 @@ void wolfSSL_SetEchEnableTrialDecrypt(WOLFSSL* ssl, byte enable) ssl->options.enableEchTrialDecrypt = (enable != 0); } +/* Return the status of the ECH connection. Possible return values: + * WOLFSSL_ECH_STATUS_NOT_OFFERED: + * server - client did not send ECH or it is not setup + * client - ECH is not setup + * pending - connection has not been initiated + * + * WOLFSSL_ECH_STATUS_GREASE: + * client - GREASE ECH extension sent + * + * WOLFSSL_ECH_STATUS_REJECTED: + * server - ECH was not accepted (decryption of inner ClientHello failed) + * client - ECH was offered but the server rejected it + * + * In both cases the connection fell back to the outer transcript. + * + * WOLFSSL_ECH_STATUS_ACCEPTED: + * Decryption of the inner ClientHello was successful and the inner + * transcript was used. + * + * Returns BAD_FUNC_ARG if ssl is NULL. */ +int wolfSSL_GetEchStatus(const WOLFSSL* ssl) +{ + if (ssl == NULL) + return BAD_FUNC_ARG; + + if (ssl->options.disableECH) + return WOLFSSL_ECH_STATUS_NOT_OFFERED; + if (ssl->options.echAccepted) + return WOLFSSL_ECH_STATUS_ACCEPTED; + + if (ssl->options.side == WOLFSSL_SERVER_END) { + TLSX* echX = TLSX_Find(ssl->extensions, TLSX_ECH); + WOLFSSL_ECH* ech; + + if (echX == NULL || echX->data == NULL) + return WOLFSSL_ECH_STATUS_NOT_OFFERED; + + /* state stays at ECH_WRITE_NONE and innerClientHello stays NULL when + * the client did not send an ECH extension */ + ech = (WOLFSSL_ECH*)echX->data; + if (ech->state == ECH_WRITE_NONE && ech->innerClientHello == NULL) + return WOLFSSL_ECH_STATUS_NOT_OFFERED; + + return WOLFSSL_ECH_STATUS_REJECTED; + } + + /* client */ + if (ssl->options.connectState < CLIENT_HELLO_SENT) + return WOLFSSL_ECH_STATUS_NOT_OFFERED; + + if (ssl->echConfigs == NULL) + return WOLFSSL_ECH_STATUS_GREASE; + + return WOLFSSL_ECH_STATUS_REJECTED; +} + /* Walk the ECHConfigExtension list and check for mandatory extensions. * Returns: * 0 if all extensions are known/optional, diff --git a/src/tls.c b/src/tls.c index f1a14bac23..6792dcfd19 100644 --- a/src/tls.c +++ b/src/tls.c @@ -14551,20 +14551,15 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size, } } else { + WOLFSSL_MSG("ECH accepted"); + ssl->options.echAccepted = 1; + ret = TLSX_ECH_CheckInnerPadding(ssl, ech); if (ret == 0) { /* expand EchOuterExtensions if present. * Also, if it exists, copy sessionID from outer hello */ ret = TLSX_ECH_ExpandOuterExtensions(ssl, ech, ssl->heap); } - - if (ret == 0){ - WOLFSSL_MSG("ECH accepted"); - ssl->options.echAccepted = 1; - } - else { - WOLFSSL_MSG("ECH rejected"); - } } if (ret != 0) { XFREE(ech->innerClientHello, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); diff --git a/tests/api.c b/tests/api.c index c94f162e78..0d9440e7ad 100644 --- a/tests/api.c +++ b/tests/api.c @@ -14976,7 +14976,7 @@ static int test_wolfSSL_ECH_conn_ex(method_provider serverMeth, /* connect like normal */ ExpectIntEQ(wolfSSL_set_fd(ssl, sockfd), WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_connect(ssl), WOLFSSL_SUCCESS); - ExpectIntEQ(ssl->options.echAccepted, 1); + ExpectIntEQ(wolfSSL_GetEchStatus(ssl), WOLFSSL_ECH_STATUS_ACCEPTED); ExpectIntEQ(wolfSSL_write(ssl, privateName, privateNameLen), privateNameLen); ExpectIntGT((replyLen = wolfSSL_read(ssl, reply, sizeof(reply))), 0); @@ -15137,7 +15137,10 @@ static int test_wolfSSL_Tls13_ECH_all_algos_ex(void) ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); - ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 1); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); if (echCbTestKemID != 0 && echCbTestKdfID != 0 && echCbTestAeadID != 0) { TLSX* echX = TLSX_Find(test_ctx.c_ssl->extensions, TLSX_ECH); @@ -15250,7 +15253,10 @@ static int test_wolfSSL_Tls13_ECH_no_private_name(void) ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); - ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 1); + 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); @@ -15270,7 +15276,10 @@ static int test_wolfSSL_Tls13_ECH_no_private_name(void) echCbTestConfigsLen), WOLFSSL_SUCCESS); 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_NOT_OFFERED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_NOT_OFFERED); test_ssl_memio_cleanup(&test_ctx); @@ -15289,7 +15298,10 @@ static int test_wolfSSL_Tls13_ECH_no_private_name(void) echCbTestConfigsLen), WOLFSSL_SUCCESS); 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_NOT_OFFERED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_NOT_OFFERED); test_ssl_memio_cleanup(&test_ctx); @@ -15348,7 +15360,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_REJECTED); test_ssl_memio_cleanup(&test_ctx); @@ -15384,7 +15399,12 @@ 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); + /* 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); @@ -15445,7 +15465,10 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_ex(int hrr) /* ECH must fail and retry configs must be present */ 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_REJECTED); ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), WC_NO_ERR_TRACE(ECH_REQUIRED_E)); @@ -15483,7 +15506,10 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_ex(int hrr) ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); - ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 1); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); wolfSSL_CTX_free(test_ctx.s_ctx); test_ctx.s_ctx = NULL; @@ -15605,7 +15631,10 @@ static int test_wolfSSL_Tls13_ECH_retry_configs_bad(void) /* bad retry configs are discarded - failure must be ECH_REQUIRED_E, * not a retry-config parse error */ 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_REJECTED); ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), WC_NO_ERR_TRACE(ECH_REQUIRED_E)); @@ -15690,7 +15719,10 @@ static int test_wolfSSL_Tls13_ECH_new_config(void) WOLFSSL_SUCCESS); ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); - ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 1); + 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); @@ -15736,7 +15768,10 @@ static int test_wolfSSL_Tls13_ECH_trial_decrypt(void) } 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_REJECTED); ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), WC_NO_ERR_TRACE(ECH_REQUIRED_E)); @@ -15768,7 +15803,10 @@ static int test_wolfSSL_Tls13_ECH_trial_decrypt(void) } ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); - ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 1); + 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); @@ -15810,9 +15848,11 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) /* handshake should succeed - server ignores the GREASE ECH extension */ ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); - /* ECH should NOT be accepted since this was GREASE */ - ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, 0); - ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0); + /* server has no configs and client did not offer real ECH */ + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_NOT_OFFERED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_GREASE); /* verify no ECH configs are received */ ExpectNull(test_ctx.c_ssl->echConfigs); /* retry configs must not be saved */ @@ -15848,9 +15888,12 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) /* handshake should succeed - server responds to the GREASE ECH extension */ ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); - /* ECH should NOT be accepted since this was GREASE */ - ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, 0); - ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0); + /* server was unable to decrypt the ECH extension's payload + * client never offered real ECH */ + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_REJECTED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_GREASE); /* verify no ECH configs are received */ ExpectNull(test_ctx.c_ssl->echConfigs); /* retry configs must not be saved */ @@ -15895,6 +15938,8 @@ static int test_wolfSSL_Tls13_ECH_disable_conn_ex(int enableServer, * 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 @@ -15902,8 +15947,11 @@ static int test_wolfSSL_Tls13_ECH_disable_conn_ex(int enableServer, * 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(test_ctx.c_ssl->options.echAccepted, 0); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_NOT_OFFERED); test_ssl_memio_cleanup(&test_ctx); @@ -16008,7 +16056,10 @@ static int test_wolfSSL_Tls13_ECH_HRR_rejection(void) /* Handshake must fail: client aborts with ech_required */ 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_NOT_OFFERED); /* hsHashesEch must have been freed by the HRR rejection code path */ ExpectNull(test_ctx.c_ssl->hsHashesEch); ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), @@ -16046,7 +16097,10 @@ static int test_wolfSSL_Tls13_ECH_ch2_no_ech(void) /* server must have committed to ECH acceptance in the HRR */ ExpectIntEQ(test_ctx.s_ssl->options.serverState, SERVER_HELLO_RETRY_REQUEST_COMPLETE); - ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, 1); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_REJECTED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); /* disable ECH on the client so CH2 omits the ECH extension entirely */ wolfSSL_SetEchEnable(test_ctx.c_ssl, 0); @@ -16088,7 +16142,10 @@ static int test_wolfSSL_Tls13_ECH_ch2_decrypt_error(void) ExpectIntEQ(test_ctx.s_ssl->options.serverState, SERVER_HELLO_RETRY_REQUEST_COMPLETE); - ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, 1); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_REJECTED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); if (EXPECT_SUCCESS()) { /* Client reads HRR and writes CH2 into s_buff */ @@ -16166,7 +16223,10 @@ static int test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex(const char* publicName, /* client sends ECH but server can't process it, however it is possible to * fall back to the outer handshake */ 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_NOT_OFFERED); if (validName) { /* the server should see the handshake as successful @@ -16244,7 +16304,10 @@ static int test_wolfSSL_Tls13_ECH_rejected_empty_client_cert(void) publicName, (word16)XSTRLEN(publicName)), WOLFSSL_SUCCESS); 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_NOT_OFFERED); /* Server cert is valid for public_name, cert check passes, ech_required * is sent on the client side. */ diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 6e713d4bdd..7cf3b19eef 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -1261,6 +1261,12 @@ WOLFSSL_API int wolfSSL_GetEchRetryConfigs(WOLFSSL* ssl, byte* echConfigs, WOLFSSL_API void wolfSSL_SetEchEnable(WOLFSSL* ssl, byte enable); WOLFSSL_API void wolfSSL_SetEchEnableTrialDecrypt(WOLFSSL* ssl, byte enable); + +#define WOLFSSL_ECH_STATUS_NOT_OFFERED 0 +#define WOLFSSL_ECH_STATUS_GREASE 1 +#define WOLFSSL_ECH_STATUS_REJECTED 2 +#define WOLFSSL_ECH_STATUS_ACCEPTED 3 +WOLFSSL_API int wolfSSL_GetEchStatus(const WOLFSSL* ssl); #endif /* WOLFSSL_TLS13 && HAVE_ECH */ #ifdef HAVE_POLY1305