Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
133 changes: 117 additions & 16 deletions src/tls.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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 */
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TLSX_ExtractEch() logs the HPKE shared secret when ech->hpke->echSecret is set, but leaves echSecret populated afterward. Consider wiping/freeing ech->hpke->echSecret immediately after EchWriteKeyLog() succeeds (wc_HpkeFreeEchSecret) to reduce the amount of time key material stays resident in memory.

Suggested change
echConfigRaw, echConfigRawSz);
echConfigRaw, echConfigRawSz);
if (ret == 0) {
wc_HpkeFreeEchSecret(ech->hpke);
}

Copilot uses AI. Check for mistakes.
}
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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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 */
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After successfully emitting ECH_SECRET/ECH_CONFIG, the HPKE shared secret remains allocated in ech->hpke->echSecret until TLSX_ECH_Free(). Since this buffer contains key material, it should be wiped and freed immediately after logging to minimize secret lifetime (e.g., call the existing wc_HpkeFreeEchSecret(ech->hpke) once EchWriteKeyLog() succeeds).

Suggested change
ech->echConfig->raw, ech->echConfig->rawLen);
ech->echConfig->raw, ech->echConfig->rawLen);
if (ret == 0) {
wc_HpkeFreeEchSecret(ech->hpke);
}

Copilot uses AI. Check for mistakes.
}
#endif /* HAVE_SECRET_CALLBACK */

if (info != NULL)
XFREE(info, ech->hpke->heap, DYNAMIC_TYPE_TMP_BUFFER);
if (aadCopy != NULL)
Expand All @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion src/tls13.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)));

Expand Down Expand Up @@ -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);
Expand Down
39 changes: 36 additions & 3 deletions tests/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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
Comment on lines +14185 to +14189
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test now requires EXPORTER_SECRET to be present, but TLS 1.3 only derives/logs EXPORTER_SECRET when HAVE_KEYING_MATERIAL is enabled and saveArrays is set (e.g., wolfSSL_KeepArrays()). The test is currently guarded only by WOLFSSL_TLS13/OPENSSL_EXTRA/HAVE_SECRET_CALLBACK, so it can fail in builds where HAVE_KEYING_MATERIAL is off or arrays aren’t kept. Consider gating the expectation/label_count on HAVE_KEYING_MATERIAL and ensuring the client calls wolfSSL_KeepArrays() before the handshake if you want to assert on EXPORTER_SECRET.

Copilot uses AI. Check for mistakes.

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);
Expand All @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
Loading
Loading