From ff2c46622f86a23a63c855c6d9f51d29a4230b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 16 Jul 2026 16:29:49 +0200 Subject: [PATCH 1/6] Add general_error alert for RFC 9846 compliance RFC 9846 section 6.2 defines a new generic alert, general_error(117), sent to indicate an error condition when no more specific alert is available or the sender wishes to conceal the specific error code. Define the alert in the AlertDescription enumeration and add its name to AlertTypeToString so it is recognized and logged. In TLS 1.3 it is handled as a fatal alert by the existing alert processing, which matches the specification. --- src/internal.c | 7 +++++++ wolfssl/ssl.h | 1 + 2 files changed, 8 insertions(+) diff --git a/src/internal.c b/src/internal.c index 0ea1b04f2b7..feebabf54fc 100644 --- a/src/internal.c +++ b/src/internal.c @@ -23002,6 +23002,13 @@ const char* AlertTypeToString(int type) return certificate_required_str; } + case general_error: + { + static const char general_error_str[] = + "general_error"; + return general_error_str; + } + case no_application_protocol: { static const char no_application_protocol_str[] = diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index b5a0fb4a07a..7ad5749c016 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -1010,6 +1010,7 @@ enum AlertDescription { bad_certificate_status_response = 113, /**< RFC 6066, section 8 */ unknown_psk_identity = 115, /**< RFC 4279, section 2 */ certificate_required = 116, /**< RFC 8446, section 8.2 */ + general_error = 117, /**< RFC 9846, section 6.2 */ no_application_protocol = 120, ech_required = 121 /**< RFC 9849, section 5 */ }; From 9e019c2b82daf1234070cbc88a386e415252ab7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 16 Jul 2026 16:33:49 +0200 Subject: [PATCH 2/6] Cap TLS 1.3 sender key updates at 2^48-1 for RFC 9846 RFC 9846 Section 4.7.3 upgrades the key update bound so that sending implementations MUST NOT allow the number of key updates to exceed 2^48-1, while receiving implementations MUST NOT enforce this on the peer. The DTLS 1.3 path already gated its sending epoch, but stream TLS 1.3 tracked no key update count. Add a keyUpdateCount field at the end of the Keys structure TLS 1.3 block, refuse to send a KeyUpdate in SendTls13KeyUpdate once the sender reaches the ceiling, and increment the count after deriving each new sending key. The receive path is left unchanged so it never enforces a limit on the peer. Add a test that exercises both sides of the boundary: seeded one below the ceiling, wolfSSL_update_keys succeeds and advances the sender count to exactly 2^48-1; at the ceiling the next KeyUpdate is refused with BAD_STATE_E and the count is left unchanged. --- src/tls13.c | 16 ++++++++++++++ tests/api/test_tls13.c | 49 ++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls13.h | 2 ++ wolfssl/internal.h | 7 ++++++ 4 files changed, 74 insertions(+) diff --git a/src/tls13.c b/src/tls13.c index b8c7dbbc912..ed37e9ebb43 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -12401,6 +12401,19 @@ int SendTls13KeyUpdate(WOLFSSL* ssl) } #endif /* WOLFSSL_DTLS13 */ + if (!ssl->options.dtls) { + /* RFC 9846 Section 4.7.3: a sending implementation MUST NOT allow its + * number of key updates to exceed 2^48-1. Receivers MUST NOT enforce + * this on the peer. */ + if (w64GTE(ssl->keys.keyUpdateCount, + w64From32(TLS13_KEY_UPDATE_MAX_HI32, + TLS13_KEY_UPDATE_MAX_LO32))) { + WOLFSSL_MSG("TLS 1.3 key update count at maximum; refusing " + "KeyUpdate"); + return BAD_STATE_E; + } + } + outputSz = OPAQUE8_LEN + MAX_MSG_EXTRA; /* Check buffers are big enough and grow if needed. */ if ((ret = CheckAvailableSize(ssl, outputSz)) != 0) @@ -12472,6 +12485,9 @@ int SendTls13KeyUpdate(WOLFSSL* ssl) return ret; if ((ret = SetKeysSide(ssl, ENCRYPT_SIDE_ONLY)) != 0) return ret; + + /* Count this key update against the RFC 9846 sender limit. */ + w64Increment(&ssl->keys.keyUpdateCount); } diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index 75c453a1211..b73461df3af 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -8652,3 +8652,52 @@ int test_tls13_AEAD_limit_KU_aes128_ccm_8_sha256(void) return EXPECT_RESULT(); } +/* RFC 9846 Section 4.7.3: a sending implementation MUST NOT allow its number of + * key updates to exceed 2^48-1. Establish a TLS 1.3 connection and exercise both + * sides of the boundary: one update seeded just below the ceiling must succeed + * and advance the sender count to exactly 2^48-1, and a further update once the + * count has reached the ceiling must be refused with BAD_STATE_E without + * advancing the count. The count is inspected directly because + * SendTls13KeyUpdate enforces the limit before touching the transport. */ +int test_tls13_KeyUpdate_sender_limit(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(NO_WOLFSSL_SERVER) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + if (EXPECT_SUCCESS() && ssl_c != NULL) { + w64wrapper ceiling; + + ceiling = w64From32(TLS13_KEY_UPDATE_MAX_HI32, TLS13_KEY_UPDATE_MAX_LO32); + + /* One below the ceiling: the update is allowed and must advance the + * sender count by one, reaching exactly 2^48-1. */ + ssl_c->keys.keyUpdateCount = w64From32(TLS13_KEY_UPDATE_MAX_HI32, + TLS13_KEY_UPDATE_MAX_LO32 - 1); + ExpectIntEQ(wolfSSL_update_keys(ssl_c), WOLFSSL_SUCCESS); + ExpectTrue(w64Equal(ssl_c->keys.keyUpdateCount, ceiling)); + + /* At the ceiling: a further update must be refused rather than + * advancing the count past 2^48-1. */ + ExpectIntEQ(wolfSSL_update_keys(ssl_c), WC_NO_ERR_TRACE(BAD_STATE_E)); + ExpectTrue(w64Equal(ssl_c->keys.keyUpdateCount, ceiling)); + } + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + diff --git a/tests/api/test_tls13.h b/tests/api/test_tls13.h index 08cb3408445..05f2be54f2d 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -102,6 +102,7 @@ int test_tls13_AEAD_limit_KU_aes128_gcm_sha256(void); int test_tls13_AEAD_limit_KU_aes256_gcm_sha384(void); int test_tls13_AEAD_limit_KU_aes128_ccm_sha256(void); int test_tls13_AEAD_limit_KU_aes128_ccm_8_sha256(void); +int test_tls13_KeyUpdate_sender_limit(void); int test_tls13_pqc_hybrid_async_server(void); #define TEST_TLS13_DECLS \ @@ -183,6 +184,7 @@ int test_tls13_pqc_hybrid_async_server(void); TEST_DECL_GROUP("tls13", test_tls13_AEAD_limit_KU_aes256_gcm_sha384), \ TEST_DECL_GROUP("tls13", test_tls13_AEAD_limit_KU_aes128_ccm_sha256), \ TEST_DECL_GROUP("tls13", test_tls13_AEAD_limit_KU_aes128_ccm_8_sha256), \ + TEST_DECL_GROUP("tls13", test_tls13_KeyUpdate_sender_limit), \ TEST_DECL_GROUP("tls13", test_tls13_pqc_hybrid_async_server) #endif /* WOLFCRYPT_TEST_TLS13_H */ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index d54496e0f9c..e7187223e22 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -2964,6 +2964,7 @@ typedef struct Keys { #ifdef WOLFSSL_TLS13 byte updateResponseReq; /* KeyUpdate response from peer required. */ byte keyUpdateRespond; /* KeyUpdate is to be responded to. */ + w64wrapper keyUpdateCount; /* Sending key updates performed (RFC 9846). */ #endif #ifdef WOLFSSL_RENESAS_TSIP_TLS @@ -2977,6 +2978,12 @@ typedef struct Keys { #endif } Keys; +/* RFC 9846 Section 4.7.3: a TLS 1.3 sender MUST NOT allow its number of key + * updates to exceed 2^48-1. Receivers MUST NOT enforce this. Expressed as the + * high and low 32-bit halves of a w64wrapper. */ +#define TLS13_KEY_UPDATE_MAX_HI32 0x0000FFFFU +#define TLS13_KEY_UPDATE_MAX_LO32 0xFFFFFFFFU + /* Forward declare opaque pointer to make available for func def */ typedef struct Options Options; From d0f5bce96b6f189897daf31817aa9884ea955160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 16 Jul 2026 16:38:56 +0200 Subject: [PATCH 3/6] Accept empty CertificateRequest extensions for RFC 9846 RFC 9846 Section 4.4.2 corrects the lower bound on CertificateRequest.extensions to 0, so a zero length extensions block is syntactically valid. The client rejected it early with INVALID_PARAMETER before parsing. Remove the early rejection so the extensions are parsed and an empty block is instead caught by the existing mandatory signature_algorithms check, which returns the correct missing_extension alert. TLSX_Parse handles a zero length input by parsing no extensions. --- src/tls13.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tls13.c b/src/tls13.c index ed37e9ebb43..9bf093269ef 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -6244,8 +6244,10 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input, *inOutIdx += OPAQUE16_LEN; if ((*inOutIdx - begin) + len > size) return BUFFER_ERROR; - if (len == 0) - return INVALID_PARAMETER; + /* RFC 9846 Section 4.4.2: CertificateRequest.extensions has a lower bound of + * 0, so an empty extensions block is parsed rather than rejected here. A + * request missing the mandatory signature_algorithms extension is caught by + * the check below. */ if ((ret = TLSX_Parse(ssl, input + *inOutIdx, len, certificate_request, &peerSuites))) { return ret; From bfdecbc64465fb6db19e144dd6dde46aac5dd6f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 16 Jul 2026 17:00:27 +0200 Subject: [PATCH 4/6] Update cert_with_extern_psk references from RFC 8773bis to RFC 9973 RFC 9973 "TLS 1.3 Extension for Using Certificates with an External Pre-Shared Key" is now published and obsoletes RFC 8773. The cert_with_extern_psk implementation was written against the 8773bis draft that became RFC 9973, so it is already compliant. Update the textual references in comments, the configure help comment, the extension codepoint comment, a test comment, and the Doxygen docs. This is a documentation only change. The WOLFSSL_CERT_WITH_EXTERN_PSK macro, the --enable-cert-with-extern-psk option, the public API names, and the extension codepoint 33 (0x0021) are all unchanged, and no logic is affected. Historical ChangeLog and README entries are left as they shipped. --- configure.ac | 2 +- doc/dox_comments/header_files/ssl.h | 4 ++-- src/tls.c | 10 +++++----- src/tls13.c | 10 +++++----- tests/api/test_tls13.c | 2 +- wolfssl/internal.h | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/configure.ac b/configure.ac index 537a51ff99c..4efba915b31 100644 --- a/configure.ac +++ b/configure.ac @@ -5490,7 +5490,7 @@ then AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_PSK_ONE_ID" fi -# Certificate Authentication with External PSK (RFC 8773bis) +# Certificate Authentication with External PSK (RFC 9973, obsoletes RFC 8773) AC_ARG_ENABLE([cert-with-extern-psk], [AS_HELP_STRING([--enable-cert-with-extern-psk],[Enable Certificate Authentication with External PSKs for TLS 1.3 (default: disabled)])], [ ENABLED_CERT_WITH_EXTERN_PSK=$enableval ], diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index 59f44c3a54f..17cf99ef626 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -15313,7 +15313,7 @@ void wolfSSL_set_psk_server_tls13_callback(WOLFSSL* ssl, \ingroup Setup \brief Enable or disable TLS 1.3 certificate authentication with external - PSK (RFC8773bis) on a context. + PSK (RFC 9973) on a context. When enabled, wolfSSL advertises and accepts the `tls_cert_with_extern_psk` extension for TLS 1.3 handshakes using external @@ -15348,7 +15348,7 @@ int wolfSSL_CTX_set_cert_with_extern_psk(WOLFSSL_CTX* ctx, int state); \ingroup Setup \brief Enable or disable TLS 1.3 certificate authentication with external - PSK (RFC8773bis) on a connection. + PSK (RFC 9973) on a connection. This call applies to a single WOLFSSL object. Any non-zero \p state value enables the feature and zero disables it. diff --git a/src/tls.c b/src/tls.c index 69c2e0a710b..6733a85cc55 100644 --- a/src/tls.c +++ b/src/tls.c @@ -16694,7 +16694,7 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) #endif #if defined(WOLFSSL_CERT_WITH_EXTERN_PSK) if (ssl->options.certWithExternPsk) { - /* RFC8773bis requires psk_dhe_ke with cert_with_extern_psk. */ + /* RFC 9973 requires psk_dhe_ke with cert_with_extern_psk. */ modes |= 1 << PSK_DHE_KE; } #endif @@ -18753,12 +18753,12 @@ WOLFSSL_TEST_VIS int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, if (msgType == client_hello && isRequest) { TLSX* pskm; - /* RFC8773bis: CH2 after HRR must keep CH1's extension set. */ + /* RFC 9973: CH2 after HRR must keep CH1's extension set. */ if (secondClientHello && !prevHasPskWithCert) { WOLFSSL_ERROR_VERBOSE(EXT_NOT_ALLOWED); return EXT_NOT_ALLOWED; } - /* RFC8773bis: cert_with_extern_psk depends on these extensions. */ + /* RFC 9973: cert_with_extern_psk depends on these extensions. */ if (!hasPsk || !hasPskModes || !hasKeyShare || !hasSg || !hasSigAlg) { WOLFSSL_ERROR_VERBOSE(EXT_MISSING); @@ -18776,7 +18776,7 @@ WOLFSSL_TEST_VIS int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, } #endif pskm = TLSX_Find(ssl->extensions, TLSX_PSK_KEY_EXCHANGE_MODES); - /* RFC8773bis requires client support for psk_dhe_ke mode. */ + /* RFC 9973 requires client support for psk_dhe_ke mode. */ if (pskm == NULL || (pskm->val & (1 << PSK_DHE_KE)) == 0) { WOLFSSL_ERROR_VERBOSE(EXT_NOT_ALLOWED); return EXT_NOT_ALLOWED; @@ -18792,7 +18792,7 @@ WOLFSSL_TEST_VIS int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, } else if (msgType == client_hello && isRequest && secondClientHello && prevHasPskWithCert) { - /* RFC8773bis: reject dropping the extension in CH2 after HRR. */ + /* RFC 9973: reject dropping the extension in CH2 after HRR. */ WOLFSSL_ERROR_VERBOSE(EXT_NOT_ALLOWED); return EXT_NOT_ALLOWED; } diff --git a/src/tls13.c b/src/tls13.c index 9bf093269ef..a59fd4a7203 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -6004,12 +6004,12 @@ int DoTls13ServerHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, #endif #ifdef WOLFSSL_CERT_WITH_EXTERN_PSK if (ssl->options.certWithExternPsk && psk->resumption) { - /* RFC8773bis mode requires external PSK, not ticket resumption. */ + /* RFC 9973 mode requires external PSK, not ticket resumption. */ WOLFSSL_ERROR_VERBOSE(PSK_KEY_ERROR); return PSK_KEY_ERROR; } if (ssl->options.certWithExternPsk && ssl->options.shSentKeyShare == 0) { - /* RFC8773bis Sec. 3: cert_with_extern_psk requires psk_dhe_ke; + /* RFC 9973 Sect. 3: cert_with_extern_psk requires psk_dhe_ke; * a ServerHello without a key_share confirms only psk_ke. */ WOLFSSL_MSG("cert_with_extern_psk: ServerHello missing key_share"); WOLFSSL_ERROR_VERBOSE(EXT_MISSING); @@ -6556,7 +6556,7 @@ static int DoPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 inputSz, } if (ret == WOLFSSL_TICKET_RET_OK) { #if defined(WOLFSSL_CERT_WITH_EXTERN_PSK) && defined(HAVE_SESSION_TICKET) - /* RFC 8773bis Sect. 5.1: all PSKs listed alongside + /* RFC 9973 Sect. 5.1: all PSKs listed alongside * tls_cert_with_extern_psk MUST be external PSKs. A successfully * decrypted session ticket identity is a resumption PSK, so the * server MUST abort with illegal_parameter regardless of whether @@ -6881,7 +6881,7 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz, extEarlyData = TLSX_Find(ssl->extensions, TLSX_EARLY_DATA); if (extEarlyData != NULL) { /* Check if accepting early data and first PSK. - * RFC 8773bis: early_data is not compatible with + * RFC 9973: early_data is not compatible with * cert_with_extern_psk, so skip key derivation in that case. */ if (ssl->earlyData != no_early_data && first && ssl->options.maxEarlyDataSz > 0 @@ -6951,7 +6951,7 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz, ssl->options.sendVerify = SEND_CERT; certExt->resp = 1; #ifdef WOLFSSL_EARLY_DATA - /* RFC 8773bis: early_data is not compatible with + /* RFC 9973: early_data is not compatible with * cert_with_extern_psk. TLSX_Parse already rejects the * combination in the ClientHello, but clear the response flag * here as a defense-in-depth measure. */ diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index b73461df3af..fe311bf548f 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -1206,7 +1206,7 @@ int test_tls13_cert_with_extern_psk_rejects_resumption(void) ssl_s = NULL; /* Step 2: attempt to resume while also offering cert_with_extern_psk. - * RFC 8773bis Sect. 5.1 requires all PSKs offered alongside + * RFC 9973 Sect. 5.1 requires all PSKs offered alongside * cert_with_extern_psk to be external PSKs. The client MUST therefore * suppress the resumption ticket identity from the pre_shared_key * extension. The handshake succeeds as a cert_with_extern_psk handshake diff --git a/wolfssl/internal.h b/wolfssl/internal.h index e7187223e22..60e2a2e425d 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3006,7 +3006,7 @@ typedef struct Options Options; #define TLSXT_SERVER_CERTIFICATE 0x0014 /* RFC8446 */ #define TLSXT_ENCRYPT_THEN_MAC 0x0016 /* RFC 7366 */ #define TLSXT_EXTENDED_MASTER_SECRET 0x0017 /* HELLO_EXT_EXTMS */ -#define TLSXT_CERT_WITH_EXTERN_PSK 0x0021 /* RFC 8773bis */ +#define TLSXT_CERT_WITH_EXTERN_PSK 0x0021 /* RFC 9973 */ #define TLSXT_SESSION_TICKET 0x0023 #define TLSXT_PRE_SHARED_KEY 0x0029 #define TLSXT_EARLY_DATA 0x002a From 34a2a2f389d375a35f5d7690ee0138b3dbaff615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 16 Jul 2026 17:21:16 +0200 Subject: [PATCH 5/6] Parse sniffer keylog file line by line for RFC 9850 The keylog reader used fscanf with three whitespace delimited tokens, which is not line aware. RFC 9850 Section 1 requires readers to ignore empty lines and lines whose first character is '#', and Section 2 recommends ignoring lines that do not conform to the format so secrets can still be recovered from corrupted files. A comment line such as "# note" was parsed as three fields spanning the line boundary, which desynchronized the rest of the file. Read one line at a time with fgets, skip empty and comment lines, and parse each line with sscanf, skipping any line that does not yield the three expected fields instead of aborting. Clear the field buffers each iteration so a short field cannot pick up stale bytes from a previous line, validate that the fixed length client random field is the exact expected length, and leave the secret length variable because it depends on the negotiated hash. Zero the new line buffer on every return path, matching the existing ForceZero handling of the secret buffers. Add a comment line, a blank line, and a malformed line to the TLS 1.3 keylog test data so the sniffer keylog test exercises the skip paths. --- scripts/sniffer-tls13-keylog.sslkeylog | 4 +++ src/sniffer.c | 40 ++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/scripts/sniffer-tls13-keylog.sslkeylog b/scripts/sniffer-tls13-keylog.sslkeylog index dde5ad96ac2..0a8aef80775 100644 --- a/scripts/sniffer-tls13-keylog.sslkeylog +++ b/scripts/sniffer-tls13-keylog.sslkeylog @@ -1,3 +1,7 @@ +# RFC 9850: reader must ignore comment lines, blank lines, and malformed +# lines such as the INCOMPLETE_KEYLOG_LINE entry below + +INCOMPLETE_KEYLOG_LINE deadbeef CLIENT_HANDSHAKE_TRAFFIC_SECRET b28ffa242be7ecc3ef669ea686dc842c0de9efe938a0f047c28cb359710991f8 cfe4b2e9ef157938936ed15627e65338e6b5f5b251b1718e28b028eabecf3633 CLIENT_HANDSHAKE_TRAFFIC_SECRET b28ffa242be7ecc3ef669ea686dc842c0de9efe938a0f047c28cb359710991f8 cfe4b2e9ef157938936ed15627e65338e6b5f5b251b1718e28b028eabecf3633 SERVER_HANDSHAKE_TRAFFIC_SECRET b28ffa242be7ecc3ef669ea686dc842c0de9efe938a0f047c28cb359710991f8 737fd782b2e50c4982011f0ec7249d3f91b7e9e25529631c4794cfd4a83ceaf1 diff --git a/src/sniffer.c b/src/sniffer.c index 7f0c4322552..a5bdf54d380 100644 --- a/src/sniffer.c +++ b/src/sniffer.c @@ -7582,7 +7582,10 @@ static int parseKeyLogFile(const char* fileName, char* error) /* 2 chars for Hexadecimal representation, plus null terminator */ char clientRandomHex[2 * CLIENT_RANDOM_LENGTH + 1] = {0}; char secretHex[2 * SECRET_LENGTH + 1] = {0}; - + /* One keylog record. RFC 9850 requires line based parsing so that comment + * and malformed lines cannot desynchronize the fields. Sized to hold the + * longest valid line plus separators and terminator. */ + char line[256]; file = fopen(fileName, "r"); if (file == NULL) { @@ -7591,10 +7594,27 @@ static int parseKeyLogFile(const char* fileName, char* error) return WOLFSSL_SNIFFER_ERROR; } - /* Format specifiers for each column should be: - * MAX_PREFIX_LENGTH, 2*CLIENT_RANDOM_LENGTH, and 2*SECRET_LENGTH */ - while (fscanf(file, "%31s %64s %96s", prefix, clientRandomHex, secretHex) - == 3) { + while (fgets(line, (int)sizeof(line), file) != NULL) { + /* RFC 9850 Section 1: ignore empty lines and lines whose first + * character is the octothorpe ('#') comment marker. */ + if (line[0] == '#' || line[0] == '\n' || line[0] == '\r' || + line[0] == '\0') { + continue; + } + + /* Clear the field buffers so that a short field on one line cannot + * pick up stale bytes left by a previous line. */ + XMEMSET(prefix, 0, sizeof(prefix)); + XMEMSET(clientRandomHex, 0, sizeof(clientRandomHex)); + XMEMSET(secretHex, 0, sizeof(secretHex)); + + /* RFC 9850 Section 2: silently ignore lines that do not conform to the + * "label SP random SP secret" format so that secrets can still be + * recovered from a corrupted file. */ + if (sscanf(line, "%31s %64s %96s", prefix, clientRandomHex, secretHex) + != 3) { + continue; + } if (XSTRCMP(prefix, "CLIENT_RANDOM") == 0) { type = SNIFFER_SECRET_TLS12_MASTER_SECRET; @@ -7621,6 +7641,14 @@ static int parseKeyLogFile(const char* fileName, char* error) continue; } + /* The client random is always CLIENT_RANDOM_LENGTH bytes. Reject a + * line whose random field is not the exact expected length. The secret + * length is left variable because it depends on the negotiated hash. */ + if (XSTRLEN(clientRandomHex) != (size_t)(2 * CLIENT_RANDOM_LENGTH)) { + fprintf(stderr, "malformed client random for prefix: %s\n", prefix); + continue; + } + hexToBin(clientRandomHex, clientRandom, CLIENT_RANDOM_LENGTH); hexToBin(secretHex, secret, SECRET_LENGTH); ret = addSecretNode(clientRandom, type, secret, error); @@ -7629,6 +7657,7 @@ static int parseKeyLogFile(const char* fileName, char* error) fclose(file); ForceZero(secret, SECRET_LENGTH); ForceZero(secretHex, sizeof(secretHex)); + ForceZero(line, sizeof(line)); return ret; } } @@ -7636,6 +7665,7 @@ static int parseKeyLogFile(const char* fileName, char* error) ForceZero(secret, SECRET_LENGTH); ForceZero(secretHex, sizeof(secretHex)); + ForceZero(line, sizeof(line)); return 0; } From 2214063a91c123469032469ba297fdaac93f64a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 16 Jul 2026 17:26:58 +0200 Subject: [PATCH 6/6] Honor SSLKEYLOGFILE environment variable for key log output RFC 9850 notes that implementations informally use an environment variable named SSLKEYLOGFILE to select the key log path, which lets tools such as curl, NSS and Wireshark share one file. The key log writers only used the compile-time WOLFSSL_SSLKEYLOGFILE_OUTPUT path. Add an opt-in WOLFSSL_SSLKEYLOGFILE_USE_ENV macro (default off) that makes both tls13ShowSecrets and tlsShowSecrets prefer XGETENV("SSLKEYLOGFILE") when it is set and non-empty, falling back to the compile-time path otherwise. Keeping it opt-in leaves the default behavior unchanged, so a build that exports the variable for other applications is unaffected, and no XGETENV reference is emitted unless the macro is defined (avoiding a dependency on environment access, for example under NO_STDIO_FILESYSTEM). XGETENV resolves to NULL on targets without environment support, so it falls back automatically. This stays behind the existing SHOW_SECRETS and WOLFSSL_SSLKEYLOGFILE compile-time guards, so production builds that do not define them cannot log secrets regardless of the environment, as required by the RFC security considerations. --- .wolfssl_known_macro_extras | 1 + src/ssl.c | 13 ++++++++++++- src/tls.c | 1 + src/tls13.c | 16 ++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index 707c68cb51d..1e0ccea9d62 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -997,6 +997,7 @@ WOLFSSL_SNIFFER_NO_RECOVERY WOLFSSL_SP_ARM32_UDIV WOLFSSL_SP_FAST_NCT_EXPTMOD WOLFSSL_SP_INT_SQR_VOLATILE +WOLFSSL_SSLKEYLOGFILE_USE_ENV WOLFSSL_STACK_CHECK WOLFSSL_STM32C5 WOLFSSL_STM32F3 diff --git a/src/ssl.c b/src/ssl.c index f5a309552c1..9e01a7c4ff8 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -3722,7 +3722,18 @@ int tlsShowSecrets(WOLFSSL* ssl, void* secret, int secretSz, #if !defined(NO_FILESYSTEM) && defined(WOLFSSL_SSLKEYLOGFILE) { - FILE* f = XFOPEN(WOLFSSL_SSLKEYLOGFILE_OUTPUT, "a"); + const char* keyLogFile = WOLFSSL_SSLKEYLOGFILE_OUTPUT; + FILE* f; + #ifdef WOLFSSL_SSLKEYLOGFILE_USE_ENV + /* RFC 9850: prefer the SSLKEYLOGFILE environment variable so other + * tools can share the path, else use the compile-time path. XGETENV is + * NULL where environment access is unavailable. Opt-in so a build with + * the variable exported for other applications is not affected. */ + const char* keyLogEnv = XGETENV("SSLKEYLOGFILE"); + if (keyLogEnv != NULL && keyLogEnv[0] != '\0') + keyLogFile = keyLogEnv; + #endif + f = XFOPEN(keyLogFile, "a"); if (f != XBADFILE) { XFWRITE(pmsBuf, 1, pmsPos, f); XFCLOSE(f); diff --git a/src/tls.c b/src/tls.c index 6733a85cc55..9db47bfeee3 100644 --- a/src/tls.c +++ b/src/tls.c @@ -102,6 +102,7 @@ * WOLFSSL_SNIFFER: Enable TLS packet sniffing support default: off * WOLFSSL_SNIFFER_KEYLOGFILE: Sniffer keylog file support default: off * WOLFSSL_SSLKEYLOGFILE: Enable SSL key log file output default: off + * WOLFSSL_SSLKEYLOGFILE_USE_ENV: Use SSLKEYLOGFILE env var path default: off * WOLFSSL_SRTP: Enable SRTP extension support default: off * WOLFSSL_DUAL_ALG_CERTS: Enable dual algorithm certificates default: off * WOLFSSL_HAVE_PRF: Enable TLS PRF function access default: off diff --git a/src/tls13.c b/src/tls13.c index a59fd4a7203..0edd6316dbf 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -72,6 +72,7 @@ * (no ciphersuite requires it currently) * WOLFSSL_ERROR_CODE_OPENSSL: Use OpenSSL-compatible error codes default: off * WOLFSSL_SSLKEYLOGFILE_OUTPUT: Set key log output file path default: off + * WOLFSSL_SSLKEYLOGFILE_USE_ENV: Use SSLKEYLOGFILE env var path default: off * WOLFSSL_RW_THREADED: Enable read/write threading support default: off * WOLFSSL_ASYNC_IO: Enable async I/O operations default: off * WOLFSSL_NONBLOCK_OCSP: Non-blocking OCSP processing default: off @@ -16437,10 +16438,25 @@ int tls13ShowSecrets(WOLFSSL* ssl, int id, const unsigned char* secret, byte clientRandom[RAN_LEN]; int clientRandomSz; XFILE fp; +#if defined(WOLFSSL_SSLKEYLOGFILE_OUTPUT) && defined(WOLFSSL_SSLKEYLOGFILE_USE_ENV) + const char* keyLogFile; +#endif (void) ctx; #ifdef WOLFSSL_SSLKEYLOGFILE_OUTPUT +#ifdef WOLFSSL_SSLKEYLOGFILE_USE_ENV + /* RFC 9850: prefer the SSLKEYLOGFILE environment variable so tools such as + * curl and Wireshark can share the path, else use the compile-time path. + * XGETENV resolves to NULL where environment access is unavailable. Opt-in + * so a build with the variable exported for other applications is not + * affected. */ + keyLogFile = XGETENV("SSLKEYLOGFILE"); + if (keyLogFile == NULL || keyLogFile[0] == '\0') + keyLogFile = WOLFSSL_SSLKEYLOGFILE_OUTPUT; + fp = XFOPEN(keyLogFile, "ab"); +#else fp = XFOPEN(WOLFSSL_SSLKEYLOGFILE_OUTPUT, "ab"); +#endif if (fp == XBADFILE) { return BAD_FUNC_ARG; }