From 8a72319d0bf434744f9b84d825059d31f080f96f Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Fri, 17 Apr 2026 15:04:13 -0600 Subject: [PATCH] TLS ECH keylogging --- src/internal.c | 18 ++++++ src/tls.c | 133 ++++++++++++++++++++++++++++++++++----- src/tls13.c | 8 ++- tests/api.c | 39 +++++++++++- wolfcrypt/src/hpke.c | 32 ++++++++++ wolfssl/internal.h | 3 +- wolfssl/ssl.h | 6 +- wolfssl/wolfcrypt/hpke.h | 8 +++ 8 files changed, 225 insertions(+), 22 deletions(-) diff --git a/src/internal.c b/src/internal.c index 2ba6cabc157..335a1eb6b80 100644 --- a/src/internal.c +++ b/src/internal.c @@ -535,6 +535,12 @@ void wolfssl_priv_der_unblind_free(DerBuffer* key) #define SSC_TLS13_EES "EARLY_EXPORTER_SECRET" /* Label string for exporter secret. */ #define SSC_TLS13_ES "EXPORTER_SECRET" +#ifdef HAVE_ECH + /* Label string for ECH KEM shared secret. */ + #define SSC_TLS13_ECH_S "ECH_SECRET" + /* Label string for ECHConfig used to construct ECH. */ + #define SSC_TLS13_ECH_C "ECH_CONFIG" +#endif /* * This function builds up string for key-logging then call user's @@ -594,6 +600,18 @@ void wolfssl_priv_der_unblind_free(DerBuffer* key) label = SSC_TLS13_ES; break; +#ifdef HAVE_ECH + case ECH_SECRET: + labelSz = sizeof(SSC_TLS13_ECH_S); + label = SSC_TLS13_ECH_S; + break; + + case ECH_CONFIG: + labelSz = sizeof(SSC_TLS13_ECH_C); + label = SSC_TLS13_ECH_C; + break; +#endif + default: return BAD_FUNC_ARG; } diff --git a/src/tls.c b/src/tls.c index f8ba67db705..7daa3c5b214 100644 --- a/src/tls.c +++ b/src/tls.c @@ -13845,6 +13845,42 @@ static int TLSX_ECH_GetSize(WOLFSSL_ECH* ech, byte msgType) return (int)size; } +#ifdef HAVE_SECRET_CALLBACK +/* log ECH_SECRET and ECH_CONFIG + * returns 0 on success, TLS13_SECRET_CB_E otherwise */ +static int EchWriteKeyLog(WOLFSSL* ssl, const byte* secret, word32 secretSz, + const byte* config, word32 configSz) +{ + int ret = 0; + if (ssl->tls13SecretCb != NULL) { + ret = ssl->tls13SecretCb(ssl, ECH_SECRET, secret, (int)secretSz, + ssl->tls13SecretCtx); + if (ret == 0) { + ret = ssl->tls13SecretCb(ssl, ECH_CONFIG, config, (int)configSz, + ssl->tls13SecretCtx); + } + if (ret != 0) { + WOLFSSL_ERROR_VERBOSE(TLS13_SECRET_CB_E); + ret = TLS13_SECRET_CB_E; + } + } +#ifdef OPENSSL_EXTRA + if (ret == 0 && ssl->tls13KeyLogCb != NULL) { + ret = ssl->tls13KeyLogCb(ssl, ECH_SECRET, secret, (int)secretSz, NULL); + if (ret == 0) { + ret = ssl->tls13KeyLogCb(ssl, ECH_CONFIG, config, (int)configSz, + NULL); + } + if (ret != 0) { + WOLFSSL_ERROR_VERBOSE(TLS13_SECRET_CB_E); + ret = TLS13_SECRET_CB_E; + } + } +#endif /* OPENSSL_EXTRA */ + return ret; +} +#endif /* HAVE_SECRET_CALLBACK */ + /* rough check that inner hello fields do not exceed length of decrypted * information. Additionally, this function will check that all padding bytes * are zero and decrease the innerHelloLen accordingly if so. @@ -14259,15 +14295,15 @@ static int TLSX_ECH_ExpandOuterExtensions(WOLFSSL* ssl, WOLFSSL_ECH* ech, /* return status after attempting to open the hpke encrypted ech extension, if * successful the inner client hello will be stored in * ech->innerClientHelloLen */ -static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig, - byte* aad, word32 aadLen, void* heap) +static int TLSX_ExtractEch(WOLFSSL* ssl, WOLFSSL_ECH* ech, + WOLFSSL_EchConfig* echConfig, byte* aad, word32 aadLen) { int ret = 0; int i; word32 rawConfigLen = 0; byte* info = NULL; word32 infoLen = 0; - if (ech == NULL || echConfig == NULL || aad == NULL) + if (ssl == NULL || ech == NULL || echConfig == NULL || aad == NULL) return BAD_FUNC_ARG; /* verify the kem and key len */ if (wc_HpkeKemGetEncLen(echConfig->kemId) != ech->encLen) @@ -14284,13 +14320,14 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig, } /* check if hpke already exists, may if HelloRetryRequest */ if (ech->hpke == NULL) { - ech->hpke = (Hpke*)XMALLOC(sizeof(Hpke), heap, DYNAMIC_TYPE_TMP_BUFFER); + ech->hpke = (Hpke*)XMALLOC(sizeof(Hpke), ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); if (ech->hpke == NULL) ret = MEMORY_E; /* init the hpke struct */ if (ret == 0) { ret = wc_HpkeInit(ech->hpke, echConfig->kemId, - ech->cipherSuite.kdfId, ech->cipherSuite.aeadId, heap); + ech->cipherSuite.kdfId, ech->cipherSuite.aeadId, ssl->heap); } if (ret == 0) { /* allocate hpkeContext */ @@ -14308,7 +14345,7 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig, /* create info */ if (ret == 0) { infoLen = TLS_INFO_CONST_STRING_SZ + 1 + rawConfigLen; - info = (byte*)XMALLOC(infoLen, heap, DYNAMIC_TYPE_TMP_BUFFER); + info = (byte*)XMALLOC(infoLen, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); if (info == NULL) ret = MEMORY_E; @@ -14319,6 +14356,16 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig, TLS_INFO_CONST_STRING_SZ + 1, &rawConfigLen); } } +#ifdef HAVE_SECRET_CALLBACK + /* allocate secret buffer for wc_HpkeInitOpenContext to copy into */ + if (ret == 0 && (ssl->tls13SecretCb != NULL +#ifdef OPENSSL_EXTRA + || ssl->tls13KeyLogCb != NULL +#endif + )) { + ret = wc_HpkeInitEchSecret(ech->hpke); + } +#endif /* HAVE_SECRET_CALLBACK */ /* init the context for opening */ if (ret == 0) { ret = wc_HpkeInitOpenContext(ech->hpke, ech->hpkeContext, @@ -14332,16 +14379,44 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig, ech->outerClientPayload, ech->innerClientHelloLen, ech->innerClientHello + HANDSHAKE_HEADER_SZ); } + +#ifdef HAVE_SECRET_CALLBACK + if (ret == 0 && ech->hpke->echSecret != NULL) { + /* server does not store raw configs, so it needs to be built here */ + byte* echConfigRaw = NULL; + word32 echConfigRawSz = 0; + ret = GetEchConfig(echConfig, NULL, &echConfigRawSz); + if (ret == WC_NO_ERR_TRACE(LENGTH_ONLY_E)) + ret = 0; + if (ret == 0) { + echConfigRaw = (byte*)XMALLOC(echConfigRawSz, ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (echConfigRaw == NULL) + ret = MEMORY_E; + } + if (ret == 0) + ret = GetEchConfig(echConfig, echConfigRaw, &echConfigRawSz); + if (ret == 0) { + ret = EchWriteKeyLog(ssl, ech->hpke->echSecret, ech->hpke->Nsecret, + echConfigRaw, echConfigRawSz); + } + XFREE(echConfigRaw, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + } +#endif /* HAVE_SECRET_CALLBACK */ + /* free the hpke and context on failure */ if (ret != 0) { - XFREE(ech->hpke, heap, DYNAMIC_TYPE_TMP_BUFFER); +#ifdef HAVE_SECRET_CALLBACK + wc_HpkeFreeEchSecret(ech->hpke); +#endif + XFREE(ech->hpke, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); ech->hpke = NULL; - XFREE(ech->hpkeContext, heap, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(ech->hpkeContext, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); ech->hpkeContext = NULL; } if (info != NULL) - XFREE(info, heap, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(info, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); return ret; } @@ -14512,8 +14587,8 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size, echConfig = ssl->ctx->echConfigs; while (echConfig != NULL) { if (echConfig->configId == ech->configId) { - ret = TLSX_ExtractEch(ech, echConfig, aadCopy, ech->aadLen, - ssl->heap); + ret = TLSX_ExtractEch(ssl, ech, echConfig, aadCopy, + ech->aadLen); break; } echConfig = echConfig->next; @@ -14522,8 +14597,8 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size, if (echConfig == NULL || ret != 0) { echConfig = ssl->ctx->echConfigs; while (echConfig != NULL) { - ret = TLSX_ExtractEch(ech, echConfig, aadCopy, ech->aadLen, - ssl->heap); + ret = TLSX_ExtractEch(ssl, ech, echConfig, aadCopy, + ech->aadLen); if (ret == 0) break; echConfig = echConfig->next; @@ -14558,10 +14633,14 @@ static void TLSX_ECH_Free(WOLFSSL_ECH* ech, void* heap) if (ech->ephemeralKey != NULL) wc_HpkeFreeKey(ech->hpke, ech->hpke->kem, ech->ephemeralKey, ech->hpke->heap); +#ifdef HAVE_SECRET_CALLBACK + wc_HpkeFreeEchSecret(ech->hpke); +#endif XFREE(ech->hpke, heap, DYNAMIC_TYPE_TMP_BUFFER); } - if (ech->hpkeContext != NULL) + if (ech->hpkeContext != NULL) { XFREE(ech->hpkeContext, heap, DYNAMIC_TYPE_TMP_BUFFER); + } if (ech->privateName != NULL) XFREE((char*)ech->privateName, heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -14571,13 +14650,15 @@ static void TLSX_ECH_Free(WOLFSSL_ECH* ech, void* heap) /* encrypt the client hello and store it in ech->outerClientPayload, return * status */ -int TLSX_FinalizeEch(WOLFSSL_ECH* ech, byte* aad, word32 aadLen) +int TLSX_FinalizeEch(WOLFSSL* ssl, WOLFSSL_ECH* ech, byte* aad, word32 aadLen) { int ret = 0; void* receiverPubkey = NULL; byte* info = NULL; int infoLen = 0; byte* aadCopy = NULL; + if (ssl == NULL || ech == NULL || aad == NULL) + return BAD_FUNC_ARG; /* setup hpke context to seal, should be done at most once per connection */ if (ech->hpkeContext == NULL) { /* import the server public key */ @@ -14605,6 +14686,18 @@ int TLSX_FinalizeEch(WOLFSSL_ECH* ech, byte* aad, word32 aadLen) TLS_INFO_CONST_STRING_SZ + 1); XMEMCPY(info + TLS_INFO_CONST_STRING_SZ + 1, ech->echConfig->raw, ech->echConfig->rawLen); + } +#ifdef HAVE_SECRET_CALLBACK + /* allocate secret buffer for wc_HpkeInitSealContext to copy into */ + if (ret == 0 && (ssl->tls13SecretCb != NULL +#ifdef OPENSSL_EXTRA + || ssl->tls13KeyLogCb != NULL +#endif + )) { + ret = wc_HpkeInitEchSecret(ech->hpke); + } +#endif /* HAVE_SECRET_CALLBACK */ + if (ret == 0) { /* init the context for seal with info and keys */ ret = wc_HpkeInitSealContext(ech->hpke, ech->hpkeContext, ech->ephemeralKey, receiverPubkey, info, infoLen); @@ -14625,6 +14718,14 @@ int TLSX_FinalizeEch(WOLFSSL_ECH* ech, byte* aad, word32 aadLen) aadLen, ech->innerClientHello, ech->innerClientHelloLen - ech->hpke->Nt, ech->outerClientPayload); } + +#ifdef HAVE_SECRET_CALLBACK + if (ret == 0 && ech->hpke->echSecret != NULL) { + ret = EchWriteKeyLog(ssl, ech->hpke->echSecret, ech->hpke->Nsecret, + ech->echConfig->raw, ech->echConfig->rawLen); + } +#endif /* HAVE_SECRET_CALLBACK */ + if (info != NULL) XFREE(info, ech->hpke->heap, DYNAMIC_TYPE_TMP_BUFFER); if (aadCopy != NULL) @@ -14643,7 +14744,7 @@ int TLSX_FinalizeEch(WOLFSSL_ECH* ech, byte* aad, word32 aadLen) #define ECH_PARSE TLSX_ECH_Parse #define ECH_FREE TLSX_ECH_Free -#endif +#endif /* WOLFSSL_TLS13 && HAVE_ECH */ /** Releases all extensions in the provided list. */ void TLSX_FreeAll(TLSX* list, void* heap) diff --git a/src/tls13.c b/src/tls13.c index fa7f46bdba4..cfdce4673b4 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -4965,7 +4965,7 @@ int SendTls13ClientHello(WOLFSSL* ssl) /* encrypt and pack the ech innerClientHello */ if (ssl->echConfigs != NULL && !ssl->options.disableECH && (ssl->options.echAccepted || args->ech->innerCount == 0)) { - ret = TLSX_FinalizeEch(args->ech, + ret = TLSX_FinalizeEch(ssl, args->ech, args->output + RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ, (word32)(args->sendSz - (RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ))); @@ -15809,6 +15809,12 @@ int tls13ShowSecrets(WOLFSSL* ssl, int id, const unsigned char* secret, str = "SERVER_TRAFFIC_SECRET_0"; break; case EXPORTER_SECRET: str = "EXPORTER_SECRET"; break; +#ifdef HAVE_ECH + case ECH_SECRET: + str = "ECH_SECRET"; break; + case ECH_CONFIG: + str = "ECH_CONFIG"; break; +#endif default: #ifdef WOLFSSL_SSLKEYLOGFILE_OUTPUT XFCLOSE(fp); diff --git a/tests/api.c b/tests/api.c index 7cde2a6bdc5..a98395ea3d6 100644 --- a/tests/api.c +++ b/tests/api.c @@ -14157,6 +14157,12 @@ static int test_wolfSSL_Tls12_Key_Logging_test(void) #if defined(WOLFSSL_TLS13) && defined(OPENSSL_EXTRA) && \ defined(HAVE_SECRET_CALLBACK) +#ifdef HAVE_ECH +static int test_ech_server_ctx_ready(WOLFSSL_CTX* ctx); +static int test_ech_server_ssl_ready(WOLFSSL* ssl); +static int test_ech_client_ssl_ready(WOLFSSL* ssl); +#endif + static int test_wolfSSL_Tls13_Key_Logging_client_ctx_ready(WOLFSSL_CTX* ctx) { /* set keylog callback */ @@ -14176,11 +14182,21 @@ static int test_wolfSSL_Tls13_Key_Logging_test(void) test_ssl_cbf server_cbf; test_ssl_cbf client_cbf; XFILE fp = XBADFILE; +#ifdef HAVE_ECH + const int label_count = 7; +#else + const int label_count = 5; +#endif XMEMSET(&server_cbf, 0, sizeof(test_ssl_cbf)); XMEMSET(&client_cbf, 0, sizeof(test_ssl_cbf)); server_cbf.method = wolfTLSv1_3_server_method; /* TLS1.3 */ client_cbf.ctx_ready = &test_wolfSSL_Tls13_Key_Logging_client_ctx_ready; +#ifdef HAVE_ECH + server_cbf.ctx_ready = &test_ech_server_ctx_ready; + server_cbf.ssl_ready = &test_ech_server_ssl_ready; + client_cbf.ssl_ready = &test_ech_client_ssl_ready; +#endif /* clean up keylog file */ ExpectTrue((fp = XFOPEN("./MyKeyLog.txt", "w")) != XBADFILE); @@ -14195,7 +14211,7 @@ static int test_wolfSSL_Tls13_Key_Logging_test(void) /* check if the keylog file exists */ { char buff[300] = {0}; - int found[4] = {0}; + int found[7] = {0}; int numfnd = 0; int i; @@ -14223,14 +14239,31 @@ static int test_wolfSSL_Tls13_Key_Logging_test(void) found[3] = 1; continue; } + else if (0 == strncmp(buff, "EXPORTER_SECRET ", + sizeof("EXPORTER_SECRET ")-1)) { + found[4] = 1; + continue; + } +#ifdef HAVE_ECH + else if (0 == strncmp(buff, "ECH_SECRET ", + sizeof("ECH_SECRET ")-1)) { + found[5] = 1; + continue; + } + else if (0 == strncmp(buff, "ECH_CONFIG ", + sizeof("ECH_CONFIG ")-1)) { + found[6] = 1; + continue; + } +#endif } if (fp != XBADFILE) XFCLOSE(fp); - for (i = 0; i < 4; i++) { + for (i = 0; i < label_count; i++) { if (found[i] != 0) numfnd++; } - ExpectIntEQ(numfnd, 4); + ExpectIntEQ(numfnd, label_count); } #endif /* OPENSSL_EXTRA && HAVE_SECRET_CALLBACK && WOLFSSL_TLS13 */ return EXPECT_RESULT(); diff --git a/wolfcrypt/src/hpke.c b/wolfcrypt/src/hpke.c index 8f16f66f700..07814f14233 100644 --- a/wolfcrypt/src/hpke.c +++ b/wolfcrypt/src/hpke.c @@ -906,6 +906,11 @@ static int wc_HpkeSetupBaseSender(Hpke* hpke, HpkeBaseContext* context, infoSz); } +#if defined(HAVE_SECRET_CALLBACK) && defined(HAVE_ECH) + if (ret == 0 && hpke->echSecret != NULL) { + XMEMCPY(hpke->echSecret, sharedSecret, hpke->Nsecret); + } +#endif ForceZero(sharedSecret, hpke->Nsecret); WC_FREE_VAR_EX(sharedSecret, hpke->heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -1148,6 +1153,11 @@ static int wc_HpkeSetupBaseReceiver(Hpke* hpke, HpkeBaseContext* context, infoSz); } +#if defined(HAVE_SECRET_CALLBACK) && defined(HAVE_ECH) + if (ret == 0 && hpke->echSecret != NULL) { + XMEMCPY(hpke->echSecret, sharedSecret, hpke->Nsecret); + } +#endif ForceZero(sharedSecret, hpke->Nsecret); WC_FREE_VAR_EX(sharedSecret, hpke->heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -1344,4 +1354,26 @@ WOLFSSL_LOCAL int wc_HpkeAeadIsSupported(word16 aeadId) } } +#if defined(HAVE_SECRET_CALLBACK) && defined(HAVE_ECH) +WOLFSSL_LOCAL int wc_HpkeInitEchSecret(Hpke* hpke) +{ + if (hpke == NULL) + return BAD_FUNC_ARG; + hpke->echSecret = (byte*)XMALLOC(hpke->Nsecret, hpke->heap, + DYNAMIC_TYPE_SECRET); + if (hpke->echSecret == NULL) + return MEMORY_E; + return 0; +} + +WOLFSSL_LOCAL void wc_HpkeFreeEchSecret(Hpke* hpke) +{ + if (hpke == NULL || hpke->echSecret == NULL) + return; + ForceZero(hpke->echSecret, hpke->Nsecret); + XFREE(hpke->echSecret, hpke->heap, DYNAMIC_TYPE_SECRET); + hpke->echSecret = NULL; +} +#endif /* HAVE_SECRET_CALLBACK && HAVE_ECH */ + #endif /* HAVE_HPKE && (HAVE_ECC || HAVE_CURVE25519) && HAVE_AESGCM */ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 7ef18d6c7fc..e51b3c90ad5 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3156,7 +3156,8 @@ typedef struct WOLFSSL_ECH { WOLFSSL_LOCAL int EchConfigGetSupportedCipherSuite(WOLFSSL_EchConfig* config); -WOLFSSL_LOCAL int TLSX_FinalizeEch(WOLFSSL_ECH* ech, byte* aad, word32 aadLen); +WOLFSSL_LOCAL int TLSX_FinalizeEch(WOLFSSL* ssl, WOLFSSL_ECH* ech, byte* aad, + word32 aadLen); WOLFSSL_LOCAL int SetEchConfigsEx(WOLFSSL_EchConfig** outputConfigs, void* heap, diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 236515157b4..63c7503fee6 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -1051,7 +1051,11 @@ enum Tls13Secret { CLIENT_TRAFFIC_SECRET, SERVER_TRAFFIC_SECRET, EARLY_EXPORTER_SECRET, - EXPORTER_SECRET + EXPORTER_SECRET, +#if defined(HAVE_ECH) + ECH_SECRET, + ECH_CONFIG, +#endif }; #endif diff --git a/wolfssl/wolfcrypt/hpke.h b/wolfssl/wolfcrypt/hpke.h index c71619ccf79..3b5ab599f8c 100644 --- a/wolfssl/wolfcrypt/hpke.h +++ b/wolfssl/wolfcrypt/hpke.h @@ -96,6 +96,9 @@ typedef struct { word16 aead; byte kem_suite_id[KEM_SUITE_ID_LEN]; byte hpke_suite_id[HPKE_SUITE_ID_LEN]; +#if defined(HAVE_SECRET_CALLBACK) && defined(HAVE_ECH) + byte* echSecret; +#endif } Hpke; typedef struct { @@ -106,6 +109,11 @@ typedef struct { } HpkeBaseContext; +#if defined(HAVE_SECRET_CALLBACK) && defined(HAVE_ECH) +WOLFSSL_LOCAL int wc_HpkeInitEchSecret(Hpke* hpke); +WOLFSSL_LOCAL void wc_HpkeFreeEchSecret(Hpke* hpke); +#endif + WOLFSSL_API int wc_HpkeInit(Hpke* hpke, int kem, int kdf, int aead, void* heap); WOLFSSL_API int wc_HpkeGenerateKeyPair(Hpke* hpke, void** keypair, WC_RNG* rng); WOLFSSL_API int wc_HpkeSerializePublicKey(Hpke* hpke, void* key, byte* out,