From 4cd57a206a2c4da307bd8c51059409c3d4d49551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 15:58:49 +0200 Subject: [PATCH 1/8] Document ALLOW_INVALID_CERTSIGN and IGNORE_KEY_EXTENSIONS as intentional opt-in knobs These compile-time flags are off by default and RFC-non-conformant when enabled. Document their scope and intent at the asn.c macro list and each enforcement site (ParseCertRelative, VerifyCRL_Signature, AddCA, ProcessPeerCerts) so their opt-in nature is clear. No behavior change. Fixes F-6984 and F-6985. --- src/internal.c | 5 +++++ src/ssl_certman.c | 4 ++++ wolfcrypt/src/asn.c | 27 ++++++++++++++++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/internal.c b/src/internal.c index 5914aa4203..0fd282a423 100644 --- a/src/internal.c +++ b/src/internal.c @@ -18037,6 +18037,11 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } #endif /* KEEP_PEER_CERT */ + /* Enforced by default (RFC 5280 4.2.1.3 / RFC 8446 4.4.2.4: + * keyEncipherment/digitalSignature and serverAuth/clientAuth EKU + * on TLS certs). IGNORE_KEY_EXTENSIONS is a deliberate, + * RFC-non-conformant opt-out; see the macro list at the top of + * wolfcrypt/src/asn.c. */ #ifndef IGNORE_KEY_EXTENSIONS #if defined(OPENSSL_EXTRA) /* when compatibility layer is turned on and no verify is diff --git a/src/ssl_certman.c b/src/ssl_certman.c index d712e3b459..28dd7b1c85 100644 --- a/src/ssl_certman.c +++ b/src/ssl_certman.c @@ -3213,6 +3213,10 @@ int AddCA(WOLFSSL_CERT_MANAGER* cm, DerBuffer** pDer, int type, int verify) WOLFSSL_MSG("\tCan't add as CA if not actually one"); ret = NOT_CA_ERROR; } + /* Enforced by default. ALLOW_INVALID_CERTSIGN is a deliberate, + * RFC-non-conformant opt-out for interop with deployed certs that carry + * malformed keyUsage; see the macro list at the top of + * wolfcrypt/src/asn.c. */ #ifndef ALLOW_INVALID_CERTSIGN else if (ret == 0 && cert->isCA == 1 && type != WOLFSSL_USER_CA && !cert->selfSigned && cert->extKeyUsageSet && diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 525f5f562c..a66c5d4bbf 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -133,8 +133,17 @@ ASN Options: * NO_STRICT_ECDSA_LEN: Allow non-strict ECDSA signature length * NO_WOLFSSL_CM_VERIFY: Disable cert manager verify callback * NO_WOLFSSL_SKIP_TRAILING_PAD: Don't skip trailing padding + * ALLOW_INVALID_CERTSIGN: Opt-in, RFC 5280 non-conformant. Accept a + * certificate that asserts keyCertSign without the + * cA basic-constraint (RFC 5280 4.2.1.9), and a CA + * whose present keyUsage extension omits keyCertSign + * (RFC 5280 6.1.4). Off by default; enforcement is + * active in a stock build. Intended for interop with + * deployed certs that carry malformed keyUsage + * (e.g. the Mosquitto integration). * ALLOW_SELFSIGNED_INVALID_CERTSIGN: Allow self-signed certs - * without keyCertSign in keyUsage + * without keyCertSign in keyUsage. Narrower opt-in + * than ALLOW_INVALID_CERTSIGN (self-signed only). * ALLOW_V1_EXTENSIONS: Allow extensions in v1 certificates * USE_WOLF_VALIDDATE: Use wolfSSL date validation * WC_ASN_RUNTIME_DATE_CHECK_CONTROL: Runtime control of date checking @@ -150,7 +159,13 @@ ASN Options: * WOLFSSL_SEP: Enable SubjectEntryPoint extension * WOLFSSL_EKU_OID: Enable Extended Key Usage OID support * WOLFSSL_ACERT: Enable attribute certificate support - * IGNORE_KEY_EXTENSIONS: Ignore key usage extensions + * IGNORE_KEY_EXTENSIONS: Opt-in, RFC non-conformant. Suppress all key-usage + * and extended-key-usage enforcement: the TLS + * keyEncipherment/digitalSignature checks and the + * serverAuth/clientAuth EKU checks (ProcessPeerCerts, + * src/internal.c), and the cRLSign requirement on a + * CRL-signing CA (VerifyCRL_Signature, below). Off by + * default; enforcement is active in a stock build. * IGNORE_NETSCAPE_CERT_TYPE: Ignore Netscape cert type extension * WOLFSSL_ALLOW_CRIT_AIA: Allow critical Authority Info Access * WOLFSSL_ALLOW_CRIT_AKID: Allow critical Auth Key Identifier @@ -24326,7 +24341,10 @@ int ParseCertRelative(DecodedCert* cert, int type, int verify, void* cm, #endif #ifndef ALLOW_INVALID_CERTSIGN - /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 + /* Enforced by default. ALLOW_INVALID_CERTSIGN is a deliberate, + * RFC-non-conformant opt-out for interop with deployed certs that + * carry malformed keyUsage; see the macro list at the top of file. + * https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 * If the cA boolean is not asserted, then the keyCertSign bit in the * key usage extension MUST NOT be asserted. */ if (!cert->isCA && cert->extKeyUsageSet && @@ -37032,6 +37050,9 @@ int VerifyCRL_Signature(SignatureCtx* sigCtx, const byte* toBeSigned, int sigParamsSz, Signer *ca, void* heap) { /* try to confirm/verify signature */ + /* Enforced by default (RFC 5280 4.2.1.3 / 5.2: a CRL issuer MUST assert + * cRLSign). IGNORE_KEY_EXTENSIONS is a deliberate, RFC-non-conformant + * opt-out; see the macro list at the top of file. */ #ifndef IGNORE_KEY_EXTENSIONS if ((ca->keyUsage & KEYUSE_CRL_SIGN) == 0) { WOLFSSL_MSG("CA cannot sign CRLs"); From ce21bd67d18f1a3050bd371174902d2c062171c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 16:30:49 +0200 Subject: [PATCH 2/8] Return failure when ECDSA signature encoding fails in verify In the WOLF_CRYPTO_CB_ONLY_ECC path of wolfSSL_ECDSA_do_verify, a non-positive length from i2d_ECDSA_SIG (an encoding failure) skipped the verify block and left ret at its initial value of 1, reporting a valid signature although no verification was performed. Map the encoding failure to WOLFSSL_FATAL_ERROR so it is not treated as a successful verification. Fixes F-6981. --- src/pk_ec.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pk_ec.c b/src/pk_ec.c index d71f0d22d6..778478344f 100644 --- a/src/pk_ec.c +++ b/src/pk_ec.c @@ -5349,6 +5349,10 @@ int wolfSSL_ECDSA_do_verify(const unsigned char *dgst, int dLen, ret = 0; } } + else { + WOLFSSL_MSG("i2d_ECDSA_SIG failed"); + ret = WOLFSSL_FATAL_ERROR; + } #endif /* WOLF_CRYPTO_CB_ONLY_ECC */ } From d3509142311a2d2702ee8dcb933f00c6b4a8ae9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 16:56:45 +0200 Subject: [PATCH 3/8] Enforce attribute certificate validity period in VerifyX509Acert VerifyX509Acert parsed the acert and checked the signature but never validated the notBefore and notAfter dates, so wolfSSL_X509_ACERT_verify and wc_VerifyX509Acert accepted expired or not-yet-valid attribute certificates whenever the signature was good. Call CheckDate for both validity bounds before signature verification. CheckDate returns the proper date error and honors the runtime skip-date control. Also correct ParseX509Acert to report ASN_AFTER_DATE_E instead of ASN_BEFORE_DATE_E when the notAfter date check fails. Fixes F-6986. --- tests/api/test_ossl_x509_acert.c | 27 +++++++++++++++++++++++++++ wolfcrypt/src/asn.c | 12 +++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/api/test_ossl_x509_acert.c b/tests/api/test_ossl_x509_acert.c index e468aa0a6f..d1c133055c 100644 --- a/tests/api/test_ossl_x509_acert.c +++ b/tests/api/test_ossl_x509_acert.c @@ -104,6 +104,22 @@ static int do_acert_verify_test(const char * acert_file, return 0; } + +#if !defined(NO_ASN_TIME) && !defined(NO_ASN_TIME_CHECK) +/* Return a fixed time after the notAfter date of certs/acert/acert.pem + * (2028-01-01), used to exercise attribute certificate validity period + * enforcement without depending on the real system clock. */ +static time_t acert_expired_time_cb(time_t* t) +{ + time_t expired = (time_t)1893456000; /* 2030-01-01 00:00:00 UTC */ + + if (t != NULL) { + *t = expired; + } + + return expired; +} +#endif /* !NO_ASN_TIME && !NO_ASN_TIME_CHECK */ #endif int test_wolfSSL_X509_ACERT_verify(void) @@ -140,6 +156,17 @@ int test_wolfSSL_X509_ACERT_verify(void) } ExpectIntEQ(rc, 0); + +#if !defined(NO_ASN_TIME) && !defined(NO_ASN_TIME_CHECK) + /* Override the current time to be past the acert's notAfter date. A + * cert with a valid signature must now fail verification because its + * validity period no longer includes the current time (RFC 5755). */ + ExpectIntEQ(wc_SetTimeCb(acert_expired_time_cb), 0); + ExpectIntEQ(do_acert_verify_test(acerts[0], pkeys[0], 0), 0); + /* Always restore the real time source, even if the check above failed, + * so subsequent tests are unaffected. */ + wc_SetTimeCb(NULL); +#endif /* !NO_ASN_TIME && !NO_ASN_TIME_CHECK */ #endif return EXPECT_RESULT(); } diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index a66c5d4bbf..6646bf5fc0 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -40309,7 +40309,7 @@ int ParseX509Acert(DecodedAcert* acert, int verify) if ((verify != NO_VERIFY) && (verify != VERIFY_SKIP_DATE) && (! AsnSkipDateCheck)) { - badDate = ASN_BEFORE_DATE_E; + badDate = ASN_AFTER_DATE_E; } } @@ -40610,6 +40610,16 @@ int VerifyX509Acert(const byte* der, word32 derSz, } #endif + /* Enforce the attribute certificate validity period (RFC 5755 section + * 5.4). CheckDate returns the appropriate date error, or 0, and honors + * the runtime skip-date control internally. */ + if (ret == 0) { + ret = CheckDate(&dataASN[ACERT_IDX_ACINFO_VALIDITY_NOTB_GT], ASN_BEFORE); + } + if (ret == 0) { + ret = CheckDate(&dataASN[ACERT_IDX_ACINFO_VALIDITY_NOTA_GT], ASN_AFTER); + } + if (ret == 0) { /* Finally, do the verification. */ ret = acert_sig_verify(acinfo, acinfoSz, From 299c80a8a45a9c600572d14c81a5358ffbca5ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 17:22:34 +0200 Subject: [PATCH 4/8] Lock sniffer session tables when thread-local storage is disabled The sniffer elided all session, server, and keylog secret list locking whenever HAVE_C___ATOMIC was defined, assuming those tables are thread-local and need no mutex. Thread-locality is actually governed by THREAD_LS_T, which is only a real qualifier when HAVE_THREAD_LS is set and NO_THREAD_LS is not. A build with HAVE_C___ATOMIC and NO_THREAD_LS left the tables as process-shared globals with no synchronization, so concurrent packet processing raced on the list pointers and could free a session another thread was still using. Gate the lock elision on both the atomic fast path and real thread-local storage, and take the mutexes whenever the tables are process-shared. The stats counter path is unchanged because it uses a genuine atomic add. Fixes F-6983. --- src/sniffer.c | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/sniffer.c b/src/sniffer.c index a5bdf54d38..f952241a52 100644 --- a/src/sniffer.c +++ b/src/sniffer.c @@ -564,15 +564,27 @@ typedef struct SnifferSession { } SnifferSession; +/* The session, server, and keylog secret tables below are qualified + * THREAD_LS_T. When thread-local storage is genuinely available each thread + * owns a private copy of these tables, so their lookups and list mutations + * need no mutex. THREAD_LS_T is only a real thread-local qualifier when + * HAVE_THREAD_LS is defined and NO_THREAD_LS is not; otherwise the tables are + * process-shared globals that must be locked. HAVE_C___ATOMIC by itself does + * not make the tables thread-local, so the lock elision is gated on both. */ +#if defined(HAVE_C___ATOMIC) && defined(HAVE_THREAD_LS) && \ + !defined(NO_THREAD_LS) + #define SNIFFER_LOCKLESS_TABLES +#endif + /* Sniffer Server List and mutex */ static THREAD_LS_T SnifferServer* ServerList = NULL; -#ifndef HAVE_C___ATOMIC +#ifndef SNIFFER_LOCKLESS_TABLES static WC_THREADSHARED wolfSSL_Mutex ServerListMutex WOLFSSL_MUTEX_INITIALIZER_CLAUSE(ServerListMutex); #endif /* Session Hash Table, mutex, and count */ static THREAD_LS_T SnifferSession* SessionTable[HASH_SIZE]; -#ifndef HAVE_C___ATOMIC +#ifndef SNIFFER_LOCKLESS_TABLES static WC_THREADSHARED wolfSSL_Mutex SessionMutex WOLFSSL_MUTEX_INITIALIZER_CLAUSE(SessionMutex); #endif static THREAD_LS_T int SessionCount = 0; @@ -641,7 +653,7 @@ static void UpdateMissedDataSessions(void) NOLOCK_INC_STAT(x); UNLOCK_STAT(); } while (0) #endif /* WOLFSSL_SNIFFER_STATS */ -#ifdef HAVE_C___ATOMIC +#ifdef SNIFFER_LOCKLESS_TABLES #define LOCK_SESSION() WC_DO_NOTHING #define UNLOCK_SESSION() WC_DO_NOTHING #define LOCK_SERVER_LIST() WC_DO_NOTHING @@ -682,7 +694,7 @@ void ssl_InitSniffer_ex(int devId) { wolfSSL_Init(); #ifndef WOLFSSL_MUTEX_INITIALIZER -#ifndef HAVE_C___ATOMIC +#ifndef SNIFFER_LOCKLESS_TABLES wc_InitMutex(&ServerListMutex); wc_InitMutex(&SessionMutex); #endif @@ -908,7 +920,7 @@ void ssl_FreeSniffer(void) #ifndef WOLFSSL_SNIFFER_NO_RECOVERY wc_FreeMutex(&RecoveryMutex); #endif -#ifndef HAVE_C___ATOMIC +#ifndef SNIFFER_LOCKLESS_TABLES wc_FreeMutex(&SessionMutex); wc_FreeMutex(&ServerListMutex); #endif @@ -5222,14 +5234,14 @@ static void RemoveSession(SnifferSession* session, IpInfo* ipInfo, SnifferSession* previous = 0; SnifferSession* current; word32 row = rowHint; -#ifndef HAVE_C___ATOMIC +#ifndef SNIFFER_LOCKLESS_TABLES int haveLock = 0; #endif Trace(REMOVE_SESSION_STR); if (ipInfo && tcpInfo) row = SessionHash(ipInfo, tcpInfo); -#ifndef HAVE_C___ATOMIC +#ifndef SNIFFER_LOCKLESS_TABLES else haveLock = 1; #endif @@ -5237,7 +5249,7 @@ static void RemoveSession(SnifferSession* session, IpInfo* ipInfo, if (row >= HASH_SIZE) return; -#ifndef HAVE_C___ATOMIC +#ifndef SNIFFER_LOCKLESS_TABLES if (!haveLock) { LOCK_SESSION(); } @@ -5259,7 +5271,7 @@ static void RemoveSession(SnifferSession* session, IpInfo* ipInfo, current = current->next; } -#ifndef HAVE_C___ATOMIC +#ifndef SNIFFER_LOCKLESS_TABLES if (!haveLock) { UNLOCK_SESSION(); } @@ -7439,13 +7451,13 @@ typedef struct SecretNode { static THREAD_LS_T SecretNode* secretHashTable[WOLFSSL_SNIFFER_KEYLOGFILE_HASH_TABLE_SIZE] = {NULL}; -#ifndef HAVE_C___ATOMIC +#ifndef SNIFFER_LOCKLESS_TABLES static WC_THREADSHARED wolfSSL_Mutex secretListMutex WOLFSSL_MUTEX_INITIALIZER_CLAUSE(secretListMutex); #endif static unsigned int secretHashFunction(unsigned char* clientRandom); -#ifdef HAVE_C___ATOMIC +#ifdef SNIFFER_LOCKLESS_TABLES #define LOCK_SECRET_LIST() WC_DO_NOTHING #define UNLOCK_SECRET_LIST() WC_DO_NOTHING #else From 03cb14a30ea4e4f4da0d459ed010f88553218d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 22 Jul 2026 10:17:14 +0200 Subject: [PATCH 5/8] Stop offering SHA-1 signature schemes for TLS 1.2 by default InitSuitesHashSigAlgo added the ecdsa_sha1 and rsa_pkcs1_sha1 signature schemes to the signature_algorithms list based only on the build flags, ignoring the negotiated protocol version. Because that list is also the set a peer's signatures are validated against, any build with old TLS compiled in advertised and accepted SHA-1 handshake and certificate signatures for TLS 1.2, which RFC 9155 deprecates. Gate the SHA-1 schemes on the negotiated version so they are offered only for TLS 1.0 and 1.1 handshakes, unless WOLFSSL_ALLOW_TLS_SHA1 is defined to opt back in. The same gate excludes them for TLS 1.3, as required by RFC 8446. Fixes F-6991. --- src/internal.c | 19 +++++++++++++++++-- tests/api.c | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/internal.c b/src/internal.c index 0fd282a423..2249875276 100644 --- a/src/internal.c +++ b/src/internal.c @@ -3519,6 +3519,16 @@ void InitSuitesHashSigAlgo(byte* hashSigAlgo, int haveSig, int tls1_2, int tls1_3, int keySz, word16* len) { word16 idx = 0; +#if !defined(NO_SHA) && (!defined(NO_OLD_TLS) || defined(WOLFSSL_ALLOW_TLS_SHA1)) + /* RFC 9155 deprecates SHA-1 signatures for TLS 1.2. Offer them only for a + * TLS 1.0/1.1 handshake unless the operator opts in with + * WOLFSSL_ALLOW_TLS_SHA1. */ + #ifdef WOLFSSL_ALLOW_TLS_SHA1 + int offerSha1Sig = 1; + #else + int offerSha1Sig = !tls1_2; + #endif +#endif (void)tls1_2; (void)tls1_3; @@ -3559,7 +3569,10 @@ void InitSuitesHashSigAlgo(byte* hashSigAlgo, int haveSig, int tls1_2, #endif #if !defined(NO_SHA) && (!defined(NO_OLD_TLS) || \ defined(WOLFSSL_ALLOW_TLS_SHA1)) - AddSuiteHashSigAlgo(hashSigAlgo, sha_mac, ecc_dsa_sa_algo, keySz, &idx); + if (offerSha1Sig) { + AddSuiteHashSigAlgo(hashSigAlgo, sha_mac, ecc_dsa_sa_algo, keySz, + &idx); + } #endif #endif #ifdef HAVE_ED25519 @@ -3625,7 +3638,9 @@ void InitSuitesHashSigAlgo(byte* hashSigAlgo, int haveSig, int tls1_2, #endif #if !defined(NO_SHA) && (!defined(NO_OLD_TLS) || \ defined(WOLFSSL_ALLOW_TLS_SHA1)) - AddSuiteHashSigAlgo(hashSigAlgo, sha_mac, rsa_sa_algo, keySz, &idx); + if (offerSha1Sig) { + AddSuiteHashSigAlgo(hashSigAlgo, sha_mac, rsa_sa_algo, keySz, &idx); + } #endif } diff --git a/tests/api.c b/tests/api.c index 7cf9225e49..0eb785a475 100644 --- a/tests/api.c +++ b/tests/api.c @@ -19470,6 +19470,12 @@ static int test_wolfSSL_sigalg_info(void) word16 len = 0; word16 idx = 0; int allSigAlgs = SIG_ECDSA | SIG_RSA | SIG_SM2 | SIG_FALCON | SIG_MLDSA; +#if !defined(NO_SHA) && (!defined(NO_OLD_TLS) || defined(WOLFSSL_ALLOW_TLS_SHA1)) + int sawSha1 = 0; +#endif +#ifndef WOLFSSL_ALLOW_TLS_SHA1 + int tls13 = 0; +#endif InitSuitesHashSigAlgo(hashSigAlgo, allSigAlgs, 1, 1, 0xFFFFFFFF, &len); for (idx = 0; idx < len; idx += 2) { @@ -19495,6 +19501,37 @@ static int test_wolfSSL_sigalg_info(void) ExpectIntNE(hashAlgo, 0); } + /* RFC 9155 deprecates SHA-1 signatures for TLS 1.2, and RFC 8446 forbids + * them for TLS 1.3. When the negotiated version is TLS 1.2 or higher + * (tls1_2 argument set, whether or not tls1_3 is also set) the SHA-1 + * schemes must not be offered unless the operator opts in with + * WOLFSSL_ALLOW_TLS_SHA1. */ +#ifndef WOLFSSL_ALLOW_TLS_SHA1 + for (tls13 = 0; tls13 <= 1; tls13++) { + InitSuitesHashSigAlgo(hashSigAlgo, allSigAlgs, 1, tls13, 0xFFFFFFFF, + &len); + for (idx = 0; idx < len; idx += 2) { + ExpectFalse((hashSigAlgo[idx] == sha_mac) && + ((hashSigAlgo[idx + 1] == rsa_sa_algo) || + (hashSigAlgo[idx + 1] == ecc_dsa_sa_algo))); + } + } +#endif + + /* For a TLS 1.0/1.1 handshake the SHA-1 schemes remain available when + * they are compiled in. */ +#if !defined(NO_SHA) && (!defined(NO_OLD_TLS) || defined(WOLFSSL_ALLOW_TLS_SHA1)) + InitSuitesHashSigAlgo(hashSigAlgo, allSigAlgs, 0, 0, 0xFFFFFFFF, &len); + for (idx = 0; idx < len; idx += 2) { + if ((hashSigAlgo[idx] == sha_mac) && + ((hashSigAlgo[idx + 1] == rsa_sa_algo) || + (hashSigAlgo[idx + 1] == ecc_dsa_sa_algo))) { + sawSha1 = 1; + } + } + ExpectIntEQ(sawSha1, 1); +#endif + #endif return EXPECT_RESULT(); } From a347bb6a5314e6c74d32d53f61a2a77893891a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 22 Jul 2026 12:00:48 +0200 Subject: [PATCH 6/8] Zeroize secure renegotiation key copy before freeing it The SecureRenegotiation extension embeds a Keys tmp_keys copy of the session cipher and MAC keys, which are the keys used for the renegotiated epoch. SCR_FREE_ALL freed that struct with a bare XFREE, leaving a full set of session keys intact in freed heap memory. Wipe the struct with ForceZero before freeing it, matching the ForceZero of ssl->keys on connection teardown. Fixes F-7008. --- src/tls.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/tls.c b/src/tls.c index 661410b8e4..eeebc13d99 100644 --- a/src/tls.c +++ b/src/tls.c @@ -6401,6 +6401,18 @@ static int TLSX_SecureRenegotiation_Parse(WOLFSSL* ssl, const byte* input, return ret; } +/* tmp_keys holds a copy of the session cipher and MAC keys, so wipe the + * struct before freeing it, matching the ForceZero of ssl->keys on connection + * teardown. */ +static void TLSX_SecureRenegotiation_Free(SecureRenegotiation* data, void* heap) +{ + if (data != NULL) { + ForceZero(data, sizeof(SecureRenegotiation)); + } + XFREE(data, heap, DYNAMIC_TYPE_TLSX); + (void)heap; +} + int TLSX_UseSecureRenegotiation(TLSX** extensions, void* heap) { int ret = 0; @@ -6446,7 +6458,7 @@ int TLSX_AddEmptyRenegotiationInfo(TLSX** extensions, void* heap) #endif /* HAVE_SERVER_RENEGOTIATION_INFO */ -#define SCR_FREE_ALL(data, heap) XFREE(data, (heap), DYNAMIC_TYPE_TLSX) +#define SCR_FREE_ALL TLSX_SecureRenegotiation_Free #define SCR_GET_SIZE TLSX_SecureRenegotiation_GetSize #define SCR_WRITE TLSX_SecureRenegotiation_Write #define SCR_PARSE TLSX_SecureRenegotiation_Parse @@ -15192,7 +15204,7 @@ void TLSX_FreeAll(TLSX* list, void* heap) case TLSX_RENEGOTIATION_INFO: WOLFSSL_MSG("Secure Renegotiation extension free"); - SCR_FREE_ALL(extension->data, heap); + SCR_FREE_ALL((SecureRenegotiation*)extension->data, heap); break; case TLSX_SESSION_TICKET: From 8b12255d569ae4d8f4d873407503153916b57cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 22 Jul 2026 12:16:36 +0200 Subject: [PATCH 7/8] Document ALPN continue-on-mismatch RFC 7301 non-compliance WOLFSSL_ALPN_CONTINUE_ON_MISMATCH makes a server continue the handshake without an agreed protocol when no ALPN protocol matches, like OpenSSL, instead of sending the fatal no_application_protocol alert that RFC 7301 section 3.2 requires. This is an explicit, caller-selected opt-in: wolfSSL_UseALPN rejects a call that sets neither mismatch option, and the library never enables it implicitly. Document the RFC non-compliance at the option enum, the wolfSSL_UseALPN options parameter, and the mismatch handling in ALPN_find_match so the trade-off is clear at every point a user or maintainer encounters it. No behavior change. Fixes F-7003. --- src/ssl_api_ext.c | 10 +++++++++- src/tls.c | 6 ++++-- wolfssl/ssl.h | 7 +++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/ssl_api_ext.c b/src/ssl_api_ext.c index 2e4a8952f1..a1d9843508 100644 --- a/src/ssl_api_ext.c +++ b/src/ssl_api_ext.c @@ -554,7 +554,15 @@ int wolfSSL_set1_groups(WOLFSSL* ssl, int* groups, int count) * @param [in] ssl SSL/TLS object. * @param [in] protocol_name_list Comma-separated list of protocol names. * @param [in] protocol_name_listSz Length of the list in bytes. - * @param [in] options Bitmask of ALPN options. + * @param [in] options Bitmask of ALPN options. A mismatch + * behavior must be set or BAD_FUNC_ARG is returned. + * WOLFSSL_ALPN_FAILED_ON_MISMATCH sends the fatal + * no_application_protocol alert and fails the handshake when no + * protocol matches, per RFC 7301 section 3.2. + * WOLFSSL_ALPN_CONTINUE_ON_MISMATCH instead continues without an + * agreed protocol like OpenSSL. That does not send the alert and is + * therefore not RFC 7301 compliant, so use it only for OpenSSL + * interop. * @return WOLFSSL_SUCCESS on success. * @return BAD_FUNC_ARG when an argument is NULL, the list is too long or * options are unsupported. diff --git a/src/tls.c b/src/tls.c index eeebc13d99..1f19c46035 100644 --- a/src/tls.c +++ b/src/tls.c @@ -1957,8 +1957,10 @@ static int ALPN_find_match(WOLFSSL *ssl, TLSX **pextension, if (sel == NULL) { WOLFSSL_MSG("No ALPN protocol match"); - /* do nothing if no protocol match between client and server and option - is set to continue (like OpenSSL) */ + /* The caller explicitly opted out of failing on mismatch by passing + * WOLFSSL_ALPN_CONTINUE_ON_MISMATCH, so continue without an agreed + * protocol like OpenSSL. This deliberately skips the RFC 7301 section + * 3.2 fatal no_application_protocol alert. */ if (list->options & WOLFSSL_ALPN_CONTINUE_ON_MISMATCH) { WOLFSSL_MSG("Continue on mismatch"); } diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index b71810d318..a80839b012 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -4708,7 +4708,14 @@ WOLFSSL_API int wolfSSL_UseTrustedCA(WOLFSSL* ssl, unsigned char type, enum { WOLFSSL_ALPN_NO_MATCH = 0, WOLFSSL_ALPN_MATCH = 1, + /* On mismatch, continue the handshake without an agreed protocol, like + * OpenSSL. This intentionally does NOT send the RFC 7301 section 3.2 fatal + * no_application_protocol alert, so it is not compliant with that SHALL + * requirement. Select this only when OpenSSL-compatible behavior is + * needed. */ WOLFSSL_ALPN_CONTINUE_ON_MISMATCH = 2, + /* On mismatch, send the fatal no_application_protocol alert and fail the + * handshake, as required by RFC 7301 section 3.2. */ WOLFSSL_ALPN_FAILED_ON_MISMATCH = 4 }; From 64271d24ec31b52bec7322e0f0629a8c022da196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 22 Jul 2026 13:06:11 +0200 Subject: [PATCH 8/8] Send unexpected_message alert on EndOfEarlyData in DTLS 1.3 RFC 9147 section 5.6.1 states that EndOfEarlyData is not used in DTLS 1.3 and that a receiver must terminate the connection with an unexpected_message alert. Dtls13CheckEpoch grouped end_of_early_data into the default case that returns SANITY_MSG_E without sending any alert, and the DTLS 1.3 handshake dispatch in DoProcessReplyEx did not send a fatal alert on error the way the DTLS 1.2 path does, so the connection was dropped silently. Add an explicit end_of_early_data case that sends the unexpected_message alert, and mirror the DTLS 1.2 SendFatalAlertOnly handling in the DTLS 1.3 dispatch so other handshake errors are also reported rather than dropped silently. Fixes F-6987. --- src/dtls13.c | 7 +++++++ src/internal.c | 6 ++++++ tests/api/test_dtls13.c | 6 ++++++ 3 files changed, 19 insertions(+) diff --git a/src/dtls13.c b/src/dtls13.c index 070fc2e9f2..5a367be879 100644 --- a/src/dtls13.c +++ b/src/dtls13.c @@ -1823,6 +1823,13 @@ int Dtls13CheckEpoch(WOLFSSL* ssl, enum HandShakeType type) } break; case end_of_early_data: + /* RFC 9147 5.6.1: EndOfEarlyData is not used in DTLS 1.3. Its + * receipt must terminate the connection with an + * unexpected_message alert. */ + WOLFSSL_MSG("EndOfEarlyData not valid in DTLS 1.3"); + SendAlert(ssl, alert_fatal, unexpected_message); + WOLFSSL_ERROR_VERBOSE(SANITY_MSG_E); + return SANITY_MSG_E; case message_hash: case no_shake: default: diff --git a/src/internal.c b/src/internal.c index 2249875276..790bbf5acd 100644 --- a/src/internal.c +++ b/src/internal.c @@ -24732,6 +24732,12 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) * DTLS handshake message */ ssl->dtls_timeout = ssl->dtls_timeout_init; } + else { + if (SendFatalAlertOnly(ssl, ret) + == WC_NO_ERR_TRACE(SOCKET_ERROR_E)) { + ret = SOCKET_ERROR_E; + } + } } #endif /* WOLFSSL_DTLS13 */ } diff --git a/tests/api/test_dtls13.c b/tests/api/test_dtls13.c index 7752fe739c..a46a6a0c91 100644 --- a/tests/api/test_dtls13.c +++ b/tests/api/test_dtls13.c @@ -1073,6 +1073,7 @@ int test_dtls13_epochs(void) { WOLFSSL* ssl = NULL; byte input[20]; word32 inOutIdx = 0; + WOLFSSL_ALERT_HISTORY alertHistory; XMEMSET(input, 0, sizeof(input)); @@ -1107,6 +1108,11 @@ int test_dtls13_epochs(void) { ExpectIntEQ(Dtls13CheckEpoch(ssl, key_update), SANITY_MSG_E); ExpectIntEQ(Dtls13CheckEpoch(ssl, session_ticket), SANITY_MSG_E); ExpectIntEQ(Dtls13CheckEpoch(ssl, end_of_early_data), SANITY_MSG_E); + /* RFC 9147 5.6.1: EndOfEarlyData is not used in DTLS 1.3 and its receipt + * must terminate the connection with an unexpected_message alert. */ + ExpectIntEQ(wolfSSL_get_alert_history(ssl, &alertHistory), WOLFSSL_SUCCESS); + ExpectIntEQ(alertHistory.last_tx.level, alert_fatal); + ExpectIntEQ(alertHistory.last_tx.code, unexpected_message); ExpectIntEQ(Dtls13CheckEpoch(ssl, message_hash), SANITY_MSG_E); ExpectIntEQ(Dtls13CheckEpoch(ssl, no_shake), SANITY_MSG_E);