Merge pull request #10582 from julek-wolfssl/fenrir-20260602

Fenrir 2026-06-02: TLS/DTLS correctness, resumption & renegotiation safety fixes
This commit is contained in:
David Garske
2026-06-11 15:29:51 -07:00
committed by GitHub
12 changed files with 710 additions and 9 deletions
+1
View File
@@ -2225,6 +2225,7 @@ static int Dtls13InitChaChaCipher(RecordNumberCiphers* c, byte* key,
ret = wc_Chacha_SetKey(c->chacha, key, keySize);
if (ret != 0) {
ForceZero(c->chacha, sizeof(ChaCha));
XFREE(c->chacha, heap, DYNAMIC_TYPE_CIPHER);
c->chacha = NULL;
}
+182
View File
@@ -3343,6 +3343,10 @@ void FreeCiphers(WOLFSSL* ssl)
ssl->dtlsRecordNumberDecrypt.aes = NULL;
#endif /* BUILD_AES */
#ifdef HAVE_CHACHA
if (ssl->dtlsRecordNumberEncrypt.chacha != NULL)
ForceZero(ssl->dtlsRecordNumberEncrypt.chacha, sizeof(ChaCha));
if (ssl->dtlsRecordNumberDecrypt.chacha != NULL)
ForceZero(ssl->dtlsRecordNumberDecrypt.chacha, sizeof(ChaCha));
XFREE(ssl->dtlsRecordNumberEncrypt.chacha, ssl->heap, DYNAMIC_TYPE_CIPHER);
XFREE(ssl->dtlsRecordNumberDecrypt.chacha, ssl->heap, DYNAMIC_TYPE_CIPHER);
ssl->dtlsRecordNumberEncrypt.chacha = NULL;
@@ -18049,6 +18053,17 @@ static int DoHelloRequest(WOLFSSL* ssl, word32 size)
}
#ifdef HAVE_SECURE_RENEGOTIATION
else if (ssl->secure_renegotiation && ssl->secure_renegotiation->enabled) {
/* WOLFSSL_OP_NO_RENEGOTIATION: caller opted into rejecting
* peer-initiated renegotiation. Respond with a no_renegotiation
* warning alert instead of starting a secure renegotiation. */
if (ssl->options.mask & WOLFSSL_OP_NO_RENEGOTIATION) {
int ret;
WOLFSSL_MSG("Rejecting HelloRequest: WOLFSSL_OP_NO_RENEGOTIATION");
ret = SendAlert(ssl, alert_warning, no_renegotiation);
WOLFSSL_LEAVE("DoHelloRequest", ret);
WOLFSSL_END(WC_FUNC_HELLO_REQUEST_DO);
return ret;
}
ssl->secure_renegotiation->startScr = 1;
WOLFSSL_LEAVE("DoHelloRequest", 0);
WOLFSSL_END(WC_FUNC_HELLO_REQUEST_DO);
@@ -18781,6 +18796,17 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx,
ssl->secure_renegotiation &&
ssl->secure_renegotiation->enabled)
{
/* WOLFSSL_OP_NO_RENEGOTIATION: caller opted into rejecting
* peer-initiated renegotiation. RFC 5246 7.2.2: no_renegotiation is a
* warning-level alert, so refuse the renegotiation but keep the
* established connection rather than aborting it. Skip the ClientHello
* body and leave handshake state untouched, mirroring the client-side
* HelloRequest refusal in DoHelloRequest(). */
if (ssl->options.mask & WOLFSSL_OP_NO_RENEGOTIATION) {
WOLFSSL_MSG("Refusing renegotiation: WOLFSSL_OP_NO_RENEGOTIATION");
*inOutIdx = expectedIdx;
return SendAlert(ssl, alert_warning, no_renegotiation);
}
WOLFSSL_MSG("Reset handshake state");
XMEMSET(&ssl->msgsReceived, 0, sizeof(MsgsReceived));
ssl->options.serverState = NULL_STATE;
@@ -18789,6 +18815,8 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx,
ssl->options.acceptState = ACCEPT_FIRST_REPLY_DONE;
ssl->options.handShakeState = NULL_STATE;
ssl->secure_renegotiation->cache_status = SCR_CACHE_NEEDED;
/* Reset for the renegotiation_info presence check below. */
ssl->secure_renegotiation->renegInfoSeen = 0;
ret = InitHandshakeHashes(ssl);
if (ret != 0)
@@ -22487,6 +22515,32 @@ static void LogAlert(int type)
}
/* process alert, return level */
#ifndef NO_SESSION_CACHE
/* RFC 5246 Section 7.2.2: a TLS 1.2 session whose connection is terminated by a
* fatal alert MUST be invalidated so it cannot be resumed. (TLS 1.3 RFC 8446
* Section 6.2 only requires closing the connection, but evicting here too is
* sound defense-in-depth.) Evict the cached session (which also drops any
* associated ticket). Acts on an established connection or an in-progress
* resumption - both reference a cached session; a brand-new full handshake has
* no cached session to remove. */
static void InvalidateSessionOnFatalAlert(WOLFSSL* ssl)
{
if (ssl == NULL || ssl->ctx == NULL || ssl->session == NULL)
return;
if (!ssl->options.handShakeDone && !ssl->options.resuming)
return;
/* Don't evict on an unauthenticated record: a TLS 1.3 plaintext alert
* received under encryption (current record not decrypted) is rejected (or
* ignored) by DoAlert, and the teardown alert routes back here. RFC 8446
* 6.2 doesn't require TLS 1.3 eviction; TLS 1.2 alerts are plaintext so are
* unaffected. */
if (IsAtLeastTLSv1_3(ssl->version) && IsEncryptionOn(ssl, 0) &&
!ssl->keys.decryptedCur)
return;
(void)wolfSSL_SSL_CTX_remove_session(ssl->ctx, ssl->session);
}
#endif /* !NO_SESSION_CACHE */
static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type)
{
byte level;
@@ -22593,6 +22647,15 @@ static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type)
*/
WOLFSSL_ERROR(*type);
}
#ifndef NO_SESSION_CACHE
/* Validated fatal alert: invalidate the session so it can't be resumed
* (RFC 5246 7.2.2; in TLS 1.3 all error alerts are fatal, RFC 8446
* 6.2). */
if (*type != close_notify &&
(level == alert_fatal ||
(IsAtLeastTLSv1_3(ssl->version) && *type != user_canceled)))
InvalidateSessionOnFatalAlert(ssl);
#endif
}
return level;
}
@@ -23097,6 +23160,52 @@ static void DropAndRestartProcessReply(WOLFSSL* ssl)
#endif /* WOLFSSL_DTLS_DROP_STATS */
}
#endif /* WOLFSSL_DTLS */
#ifndef WOLFSSL_NO_TLS12
/* On a confirmed TLS 1.2 / DTLS 1.2 client resumption, check the abbreviated
* ServerHello's EMS state (RFC 7627 5.3) and cipher suite (RFC 5246 7.4.1.3)
* match the resumed session. Called once resumption is confirmed - at a renewed
* NewSessionTicket (before SetupSession refreshes the cached values) or the
* server ChangeCipherSpec. Deferred from ServerHello because a declined ticket
* (RFC 5077 3.4) falls back to a full handshake that must not be rejected.
* Returns 0 if consistent, else sends a fatal alert and returns an error. */
static int CheckResumptionConsistency(WOLFSSL* ssl)
{
if (ssl->session == NULL) /* nothing to compare against */
return 0;
/* EMS must match (RFC 7627 5.3); skip EAP-FAST (session-secret callback). */
if (
#ifdef HAVE_SECRET_CALLBACK
!(ssl->sessionSecretCb != NULL
#ifdef HAVE_SESSION_TICKET
&& ssl->session->ticketLen > 0
#endif
) &&
#endif
ssl->session->haveEMS != ssl->options.haveEMS) {
WOLFSSL_MSG("Resumed session EMS state does not match "
"ServerHello EMS state");
SendAlert(ssl, alert_fatal, handshake_failure);
WOLFSSL_ERROR_VERBOSE(EXT_MASTER_SECRET_NEEDED_E);
return EXT_MASTER_SECRET_NEEDED_E;
}
#ifndef NO_RESUME_SUITE_CHECK
/* Suite must match (RFC 5246 7.4.1.3), tickets included. Skip when no suite
* was retained (both zero = TLS_NULL_WITH_NULL_NULL, e.g. EAP-FAST PAC). */
if ((ssl->session->cipherSuite0 != 0 || ssl->session->cipherSuite != 0) &&
(ssl->options.cipherSuite0 != ssl->session->cipherSuite0 ||
ssl->options.cipherSuite != ssl->session->cipherSuite)) {
WOLFSSL_MSG("Resumed session cipher suite does not match "
"ServerHello cipher suite");
SendAlert(ssl, alert_fatal, illegal_parameter);
WOLFSSL_ERROR_VERBOSE(MATCH_SUITE_ERROR);
return MATCH_SUITE_ERROR;
}
#endif /* NO_RESUME_SUITE_CHECK */
return 0;
}
#endif /* !WOLFSSL_NO_TLS12 */
/* Process input requests. Return 0 is done, 1 is call again to complete, and
negative number is error. If allowSocketErr is set, SOCKET_ERROR_E in
ssl->error will be whitelisted. This is useful when the connection has been
@@ -23211,6 +23320,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr)
/* see if sending SSLv2 client hello */
if ( ssl->options.side == WOLFSSL_SERVER_END &&
ssl->options.clientState == NULL_STATE &&
!ssl->options.handShakeDone &&
ssl->buffers.inputBuffer.buffer[ssl->buffers.inputBuffer.idx]
!= handshake &&
/* change_cipher_spec here is an error but we want to handle
@@ -23926,6 +24036,15 @@ default:
}
}
/* Server CCS confirms the abbreviated handshake: validate
* the resumed session before installing keys. */
if (ssl->options.side == WOLFSSL_CLIENT_END &&
ssl->options.resuming) {
ret = CheckResumptionConsistency(ssl);
if (ret != 0)
return ret;
}
ssl->keys.encryptionOn = 1;
/* setup decrypt keys for following messages */
@@ -24660,6 +24779,19 @@ int BuildMessage(WOLFSSL* ssl, byte* output, int outSz, const byte* input,
#endif
#ifndef WOLFSSL_NO_TLS12
/* RFC 5246 6.1: sequence numbers MUST NOT wrap. GetSEQIncrement post-
* increments, so refuse at hi == lo == 0xFFFFFFFF (2^64-1): that last legal
* value is deliberately sacrificed to avoid wrapping to 0 and reusing
* sequence number 0. The caller must renegotiate or close. DTLS sequence
* numbers are epoch-scoped and handled elsewhere. */
if (!sizeOnly && !ssl->options.dtls &&
ssl->keys.sequence_number_hi == 0xFFFFFFFFU &&
ssl->keys.sequence_number_lo == 0xFFFFFFFFU) {
WOLFSSL_MSG("TLS write sequence number would wrap");
WOLFSSL_ERROR_VERBOSE(SEQUENCE_NUMBER_E);
return SEQUENCE_NUMBER_E;
}
#ifdef WOLFSSL_ASYNC_CRYPT
ret = WC_NO_PENDING_E;
if (asyncOkay) {
@@ -27552,6 +27684,17 @@ int SendAlert(WOLFSSL* ssl, int severity, int type)
return BAD_FUNC_ARG;
}
/* InvalidateSessionOnFatalAlert() is defined in the !NO_TLS section, so the
* guard here must match (with NO_TLS there are no TLS sessions to evict). */
#if !defined(NO_SESSION_CACHE) && !defined(NO_TLS)
/* RFC 5246 Section 7.2.2: a fatal alert terminates the connection;
* invalidate the established session so it cannot be resumed. Do this as
* soon as the fatal alert is generated, before the pendingAlert/backpressure
* handling below which can return early without sending the alert now. */
if (severity == alert_fatal)
InvalidateSessionOnFatalAlert(ssl);
#endif
if (ssl->pendingAlert.level != alert_none) {
ret = RetrySendAlert(ssl);
if (ret != 0) {
@@ -28234,6 +28377,9 @@ const char* wolfSSL_ERR_reason_error_string(unsigned long e)
case ECH_REQUIRED_E:
return "ECH offered but rejected by server";
case SEQUENCE_NUMBER_E:
return "Record sequence number would wrap";
}
return "unknown error number";
@@ -32403,6 +32549,9 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key)
}
else {
if (DSH_CheckSessionId(ssl)) {
/* EMS/suite consistency is checked once resumption is confirmed
* (CheckResumptionConsistency), not here: a ticket the server
* declines (RFC 5077 3.4) must fall back to a full handshake. */
if (SetCipherSpecs(ssl) == 0) {
if (!HaveUniqueSessionObj(ssl)) {
WOLFSSL_MSG("Unable to have unique session object");
@@ -35668,6 +35817,15 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx,
return SESSION_TICKET_EXPECT_E;
}
/* A renewed ticket while resuming confirms resumption; check before the
* SetupSession() below refreshes the cached suite/EMS and masks a downgrade.
* (The ChangeCipherSpec check covers the no-renewal case.) */
if (ssl->options.resuming) {
ret = CheckResumptionConsistency(ssl);
if (ret != 0)
return ret;
}
if (OPAQUE32_LEN > size)
return BUFFER_ERROR;
@@ -38793,6 +38951,17 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl)
0) {
TLSX* extension;
#ifdef HAVE_SECURE_RENEGOTIATION
/* SCSV not allowed on a renegotiation ClientHello (RFC 5746 3.5). */
if (ssl->secure_renegotiation &&
ssl->secure_renegotiation->enabled &&
ssl->secure_renegotiation->verifySet) {
WOLFSSL_MSG("SCSV received on renegotiation ClientHello");
SendAlert(ssl, alert_fatal, handshake_failure);
ret = SECURE_RENEGOTIATION_E;
goto out;
}
#endif
/* check for TLS_EMPTY_RENEGOTIATION_INFO_SCSV suite */
ret = TLSX_AddEmptyRenegotiationInfo(&ssl->extensions, ssl->heap);
if (ret != WOLFSSL_SUCCESS) {
@@ -39045,6 +39214,19 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl)
*inOutIdx = begin + helloSz; /* skip extensions */
}
#ifdef HAVE_SECURE_RENEGOTIATION
/* renegotiation_info MUST be present on a renegotiation (RFC 5746 3.7). */
if (ssl->secure_renegotiation &&
ssl->secure_renegotiation->enabled &&
ssl->secure_renegotiation->verifySet &&
!ssl->secure_renegotiation->renegInfoSeen) {
WOLFSSL_MSG("Renegotiation ClientHello missing renegotiation_info");
SendAlert(ssl, alert_fatal, handshake_failure);
ret = SECURE_RENEGOTIATION_E;
goto out;
}
#endif /* HAVE_SECURE_RENEGOTIATION */
#ifdef WOLFSSL_DTLS_CID
if (ssl->options.useDtlsCID)
DtlsCIDOnExtensionsParsed(ssl);
+3
View File
@@ -6277,6 +6277,9 @@ static int TLSX_SecureRenegotiation_Parse(WOLFSSL* ssl, const byte* input,
if (ret == WOLFSSL_SUCCESS)
ret = 0;
}
/* renegotiation_info seen (checked by DoClientHello, RFC 5746 3.7) */
if (ssl->secure_renegotiation != NULL)
ssl->secure_renegotiation->renegInfoSeen = 1;
if (ret != 0 && ret != WC_NO_ERR_TRACE(SECURE_RENEGOTIATION_E)) {
}
else if (ssl->secure_renegotiation == NULL) {
+5 -2
View File
@@ -6030,7 +6030,7 @@ static int DoTls13EncryptedExtensions(WOLFSSL* ssl, const byte* input,
i += OPAQUE16_LEN;
/* Extension data. */
if (i - begin + totalExtSz > totalSz)
if (i - begin + totalExtSz != totalSz)
return BUFFER_ERROR;
if ((ret = TLSX_Parse(ssl, input + i, totalExtSz, encrypted_extensions,
NULL))) {
@@ -6168,6 +6168,10 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input,
}
*inOutIdx += len;
/* No trailing bytes allowed (RFC 8446 4.3.2). */
if ((*inOutIdx - begin) != size)
return BUFFER_ERROR;
/* RFC 8446 Section 4.3.2: the signature_algorithms extension MUST be
* present in a CertificateRequest. */
if (peerSuites.hashSigAlgoSz == 0) {
@@ -6175,7 +6179,6 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input,
WOLFSSL_ERROR_VERBOSE(INVALID_PARAMETER);
return INVALID_PARAMETER;
}
#ifdef WOLFSSL_CERT_SETUP_CB
if ((ret = CertSetupCbWrapper(ssl)) != 0)
return ret;