Skip to content

Commit 29f14ed

Browse files
authored
Merge pull request #10582 from julek-wolfssl/fenrir-20260602
Fenrir 2026-06-02: TLS/DTLS correctness, resumption & renegotiation safety fixes
2 parents 74c3b50 + e68cc75 commit 29f14ed

12 files changed

Lines changed: 710 additions & 9 deletions

File tree

src/dtls13.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2225,6 +2225,7 @@ static int Dtls13InitChaChaCipher(RecordNumberCiphers* c, byte* key,
22252225

22262226
ret = wc_Chacha_SetKey(c->chacha, key, keySize);
22272227
if (ret != 0) {
2228+
ForceZero(c->chacha, sizeof(ChaCha));
22282229
XFREE(c->chacha, heap, DYNAMIC_TYPE_CIPHER);
22292230
c->chacha = NULL;
22302231
}

src/internal.c

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3343,6 +3343,10 @@ void FreeCiphers(WOLFSSL* ssl)
33433343
ssl->dtlsRecordNumberDecrypt.aes = NULL;
33443344
#endif /* BUILD_AES */
33453345
#ifdef HAVE_CHACHA
3346+
if (ssl->dtlsRecordNumberEncrypt.chacha != NULL)
3347+
ForceZero(ssl->dtlsRecordNumberEncrypt.chacha, sizeof(ChaCha));
3348+
if (ssl->dtlsRecordNumberDecrypt.chacha != NULL)
3349+
ForceZero(ssl->dtlsRecordNumberDecrypt.chacha, sizeof(ChaCha));
33463350
XFREE(ssl->dtlsRecordNumberEncrypt.chacha, ssl->heap, DYNAMIC_TYPE_CIPHER);
33473351
XFREE(ssl->dtlsRecordNumberDecrypt.chacha, ssl->heap, DYNAMIC_TYPE_CIPHER);
33483352
ssl->dtlsRecordNumberEncrypt.chacha = NULL;
@@ -18049,6 +18053,17 @@ static int DoHelloRequest(WOLFSSL* ssl, word32 size)
1804918053
}
1805018054
#ifdef HAVE_SECURE_RENEGOTIATION
1805118055
else if (ssl->secure_renegotiation && ssl->secure_renegotiation->enabled) {
18056+
/* WOLFSSL_OP_NO_RENEGOTIATION: caller opted into rejecting
18057+
* peer-initiated renegotiation. Respond with a no_renegotiation
18058+
* warning alert instead of starting a secure renegotiation. */
18059+
if (ssl->options.mask & WOLFSSL_OP_NO_RENEGOTIATION) {
18060+
int ret;
18061+
WOLFSSL_MSG("Rejecting HelloRequest: WOLFSSL_OP_NO_RENEGOTIATION");
18062+
ret = SendAlert(ssl, alert_warning, no_renegotiation);
18063+
WOLFSSL_LEAVE("DoHelloRequest", ret);
18064+
WOLFSSL_END(WC_FUNC_HELLO_REQUEST_DO);
18065+
return ret;
18066+
}
1805218067
ssl->secure_renegotiation->startScr = 1;
1805318068
WOLFSSL_LEAVE("DoHelloRequest", 0);
1805418069
WOLFSSL_END(WC_FUNC_HELLO_REQUEST_DO);
@@ -18781,6 +18796,17 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx,
1878118796
ssl->secure_renegotiation &&
1878218797
ssl->secure_renegotiation->enabled)
1878318798
{
18799+
/* WOLFSSL_OP_NO_RENEGOTIATION: caller opted into rejecting
18800+
* peer-initiated renegotiation. RFC 5246 7.2.2: no_renegotiation is a
18801+
* warning-level alert, so refuse the renegotiation but keep the
18802+
* established connection rather than aborting it. Skip the ClientHello
18803+
* body and leave handshake state untouched, mirroring the client-side
18804+
* HelloRequest refusal in DoHelloRequest(). */
18805+
if (ssl->options.mask & WOLFSSL_OP_NO_RENEGOTIATION) {
18806+
WOLFSSL_MSG("Refusing renegotiation: WOLFSSL_OP_NO_RENEGOTIATION");
18807+
*inOutIdx = expectedIdx;
18808+
return SendAlert(ssl, alert_warning, no_renegotiation);
18809+
}
1878418810
WOLFSSL_MSG("Reset handshake state");
1878518811
XMEMSET(&ssl->msgsReceived, 0, sizeof(MsgsReceived));
1878618812
ssl->options.serverState = NULL_STATE;
@@ -18789,6 +18815,8 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx,
1878918815
ssl->options.acceptState = ACCEPT_FIRST_REPLY_DONE;
1879018816
ssl->options.handShakeState = NULL_STATE;
1879118817
ssl->secure_renegotiation->cache_status = SCR_CACHE_NEEDED;
18818+
/* Reset for the renegotiation_info presence check below. */
18819+
ssl->secure_renegotiation->renegInfoSeen = 0;
1879218820

1879318821
ret = InitHandshakeHashes(ssl);
1879418822
if (ret != 0)
@@ -22487,6 +22515,32 @@ static void LogAlert(int type)
2248722515
}
2248822516

2248922517
/* process alert, return level */
22518+
#ifndef NO_SESSION_CACHE
22519+
/* RFC 5246 Section 7.2.2: a TLS 1.2 session whose connection is terminated by a
22520+
* fatal alert MUST be invalidated so it cannot be resumed. (TLS 1.3 RFC 8446
22521+
* Section 6.2 only requires closing the connection, but evicting here too is
22522+
* sound defense-in-depth.) Evict the cached session (which also drops any
22523+
* associated ticket). Acts on an established connection or an in-progress
22524+
* resumption - both reference a cached session; a brand-new full handshake has
22525+
* no cached session to remove. */
22526+
static void InvalidateSessionOnFatalAlert(WOLFSSL* ssl)
22527+
{
22528+
if (ssl == NULL || ssl->ctx == NULL || ssl->session == NULL)
22529+
return;
22530+
if (!ssl->options.handShakeDone && !ssl->options.resuming)
22531+
return;
22532+
/* Don't evict on an unauthenticated record: a TLS 1.3 plaintext alert
22533+
* received under encryption (current record not decrypted) is rejected (or
22534+
* ignored) by DoAlert, and the teardown alert routes back here. RFC 8446
22535+
* 6.2 doesn't require TLS 1.3 eviction; TLS 1.2 alerts are plaintext so are
22536+
* unaffected. */
22537+
if (IsAtLeastTLSv1_3(ssl->version) && IsEncryptionOn(ssl, 0) &&
22538+
!ssl->keys.decryptedCur)
22539+
return;
22540+
(void)wolfSSL_SSL_CTX_remove_session(ssl->ctx, ssl->session);
22541+
}
22542+
#endif /* !NO_SESSION_CACHE */
22543+
2249022544
static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type)
2249122545
{
2249222546
byte level;
@@ -22593,6 +22647,15 @@ static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type)
2259322647
*/
2259422648
WOLFSSL_ERROR(*type);
2259522649
}
22650+
#ifndef NO_SESSION_CACHE
22651+
/* Validated fatal alert: invalidate the session so it can't be resumed
22652+
* (RFC 5246 7.2.2; in TLS 1.3 all error alerts are fatal, RFC 8446
22653+
* 6.2). */
22654+
if (*type != close_notify &&
22655+
(level == alert_fatal ||
22656+
(IsAtLeastTLSv1_3(ssl->version) && *type != user_canceled)))
22657+
InvalidateSessionOnFatalAlert(ssl);
22658+
#endif
2259622659
}
2259722660
return level;
2259822661
}
@@ -23097,6 +23160,52 @@ static void DropAndRestartProcessReply(WOLFSSL* ssl)
2309723160
#endif /* WOLFSSL_DTLS_DROP_STATS */
2309823161
}
2309923162
#endif /* WOLFSSL_DTLS */
23163+
23164+
#ifndef WOLFSSL_NO_TLS12
23165+
/* On a confirmed TLS 1.2 / DTLS 1.2 client resumption, check the abbreviated
23166+
* ServerHello's EMS state (RFC 7627 5.3) and cipher suite (RFC 5246 7.4.1.3)
23167+
* match the resumed session. Called once resumption is confirmed - at a renewed
23168+
* NewSessionTicket (before SetupSession refreshes the cached values) or the
23169+
* server ChangeCipherSpec. Deferred from ServerHello because a declined ticket
23170+
* (RFC 5077 3.4) falls back to a full handshake that must not be rejected.
23171+
* Returns 0 if consistent, else sends a fatal alert and returns an error. */
23172+
static int CheckResumptionConsistency(WOLFSSL* ssl)
23173+
{
23174+
if (ssl->session == NULL) /* nothing to compare against */
23175+
return 0;
23176+
/* EMS must match (RFC 7627 5.3); skip EAP-FAST (session-secret callback). */
23177+
if (
23178+
#ifdef HAVE_SECRET_CALLBACK
23179+
!(ssl->sessionSecretCb != NULL
23180+
#ifdef HAVE_SESSION_TICKET
23181+
&& ssl->session->ticketLen > 0
23182+
#endif
23183+
) &&
23184+
#endif
23185+
ssl->session->haveEMS != ssl->options.haveEMS) {
23186+
WOLFSSL_MSG("Resumed session EMS state does not match "
23187+
"ServerHello EMS state");
23188+
SendAlert(ssl, alert_fatal, handshake_failure);
23189+
WOLFSSL_ERROR_VERBOSE(EXT_MASTER_SECRET_NEEDED_E);
23190+
return EXT_MASTER_SECRET_NEEDED_E;
23191+
}
23192+
#ifndef NO_RESUME_SUITE_CHECK
23193+
/* Suite must match (RFC 5246 7.4.1.3), tickets included. Skip when no suite
23194+
* was retained (both zero = TLS_NULL_WITH_NULL_NULL, e.g. EAP-FAST PAC). */
23195+
if ((ssl->session->cipherSuite0 != 0 || ssl->session->cipherSuite != 0) &&
23196+
(ssl->options.cipherSuite0 != ssl->session->cipherSuite0 ||
23197+
ssl->options.cipherSuite != ssl->session->cipherSuite)) {
23198+
WOLFSSL_MSG("Resumed session cipher suite does not match "
23199+
"ServerHello cipher suite");
23200+
SendAlert(ssl, alert_fatal, illegal_parameter);
23201+
WOLFSSL_ERROR_VERBOSE(MATCH_SUITE_ERROR);
23202+
return MATCH_SUITE_ERROR;
23203+
}
23204+
#endif /* NO_RESUME_SUITE_CHECK */
23205+
return 0;
23206+
}
23207+
#endif /* !WOLFSSL_NO_TLS12 */
23208+
2310023209
/* Process input requests. Return 0 is done, 1 is call again to complete, and
2310123210
negative number is error. If allowSocketErr is set, SOCKET_ERROR_E in
2310223211
ssl->error will be whitelisted. This is useful when the connection has been
@@ -23211,6 +23320,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr)
2321123320
/* see if sending SSLv2 client hello */
2321223321
if ( ssl->options.side == WOLFSSL_SERVER_END &&
2321323322
ssl->options.clientState == NULL_STATE &&
23323+
!ssl->options.handShakeDone &&
2321423324
ssl->buffers.inputBuffer.buffer[ssl->buffers.inputBuffer.idx]
2321523325
!= handshake &&
2321623326
/* change_cipher_spec here is an error but we want to handle
@@ -23926,6 +24036,15 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr)
2392624036
}
2392724037
}
2392824038

24039+
/* Server CCS confirms the abbreviated handshake: validate
24040+
* the resumed session before installing keys. */
24041+
if (ssl->options.side == WOLFSSL_CLIENT_END &&
24042+
ssl->options.resuming) {
24043+
ret = CheckResumptionConsistency(ssl);
24044+
if (ret != 0)
24045+
return ret;
24046+
}
24047+
2392924048
ssl->keys.encryptionOn = 1;
2393024049

2393124050
/* setup decrypt keys for following messages */
@@ -24660,6 +24779,19 @@ int BuildMessage(WOLFSSL* ssl, byte* output, int outSz, const byte* input,
2466024779
#endif
2466124780

2466224781
#ifndef WOLFSSL_NO_TLS12
24782+
/* RFC 5246 6.1: sequence numbers MUST NOT wrap. GetSEQIncrement post-
24783+
* increments, so refuse at hi == lo == 0xFFFFFFFF (2^64-1): that last legal
24784+
* value is deliberately sacrificed to avoid wrapping to 0 and reusing
24785+
* sequence number 0. The caller must renegotiate or close. DTLS sequence
24786+
* numbers are epoch-scoped and handled elsewhere. */
24787+
if (!sizeOnly && !ssl->options.dtls &&
24788+
ssl->keys.sequence_number_hi == 0xFFFFFFFFU &&
24789+
ssl->keys.sequence_number_lo == 0xFFFFFFFFU) {
24790+
WOLFSSL_MSG("TLS write sequence number would wrap");
24791+
WOLFSSL_ERROR_VERBOSE(SEQUENCE_NUMBER_E);
24792+
return SEQUENCE_NUMBER_E;
24793+
}
24794+
2466324795
#ifdef WOLFSSL_ASYNC_CRYPT
2466424796
ret = WC_NO_PENDING_E;
2466524797
if (asyncOkay) {
@@ -27552,6 +27684,17 @@ int SendAlert(WOLFSSL* ssl, int severity, int type)
2755227684
return BAD_FUNC_ARG;
2755327685
}
2755427686

27687+
/* InvalidateSessionOnFatalAlert() is defined in the !NO_TLS section, so the
27688+
* guard here must match (with NO_TLS there are no TLS sessions to evict). */
27689+
#if !defined(NO_SESSION_CACHE) && !defined(NO_TLS)
27690+
/* RFC 5246 Section 7.2.2: a fatal alert terminates the connection;
27691+
* invalidate the established session so it cannot be resumed. Do this as
27692+
* soon as the fatal alert is generated, before the pendingAlert/backpressure
27693+
* handling below which can return early without sending the alert now. */
27694+
if (severity == alert_fatal)
27695+
InvalidateSessionOnFatalAlert(ssl);
27696+
#endif
27697+
2755527698
if (ssl->pendingAlert.level != alert_none) {
2755627699
ret = RetrySendAlert(ssl);
2755727700
if (ret != 0) {
@@ -28234,6 +28377,9 @@ const char* wolfSSL_ERR_reason_error_string(unsigned long e)
2823428377

2823528378
case ECH_REQUIRED_E:
2823628379
return "ECH offered but rejected by server";
28380+
28381+
case SEQUENCE_NUMBER_E:
28382+
return "Record sequence number would wrap";
2823728383
}
2823828384

2823928385
return "unknown error number";
@@ -32403,6 +32549,9 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key)
3240332549
}
3240432550
else {
3240532551
if (DSH_CheckSessionId(ssl)) {
32552+
/* EMS/suite consistency is checked once resumption is confirmed
32553+
* (CheckResumptionConsistency), not here: a ticket the server
32554+
* declines (RFC 5077 3.4) must fall back to a full handshake. */
3240632555
if (SetCipherSpecs(ssl) == 0) {
3240732556
if (!HaveUniqueSessionObj(ssl)) {
3240832557
WOLFSSL_MSG("Unable to have unique session object");
@@ -35668,6 +35817,15 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx,
3566835817
return SESSION_TICKET_EXPECT_E;
3566935818
}
3567035819

35820+
/* A renewed ticket while resuming confirms resumption; check before the
35821+
* SetupSession() below refreshes the cached suite/EMS and masks a downgrade.
35822+
* (The ChangeCipherSpec check covers the no-renewal case.) */
35823+
if (ssl->options.resuming) {
35824+
ret = CheckResumptionConsistency(ssl);
35825+
if (ret != 0)
35826+
return ret;
35827+
}
35828+
3567135829
if (OPAQUE32_LEN > size)
3567235830
return BUFFER_ERROR;
3567335831

@@ -38793,6 +38951,17 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl)
3879338951
0) {
3879438952
TLSX* extension;
3879538953

38954+
#ifdef HAVE_SECURE_RENEGOTIATION
38955+
/* SCSV not allowed on a renegotiation ClientHello (RFC 5746 3.5). */
38956+
if (ssl->secure_renegotiation &&
38957+
ssl->secure_renegotiation->enabled &&
38958+
ssl->secure_renegotiation->verifySet) {
38959+
WOLFSSL_MSG("SCSV received on renegotiation ClientHello");
38960+
SendAlert(ssl, alert_fatal, handshake_failure);
38961+
ret = SECURE_RENEGOTIATION_E;
38962+
goto out;
38963+
}
38964+
#endif
3879638965
/* check for TLS_EMPTY_RENEGOTIATION_INFO_SCSV suite */
3879738966
ret = TLSX_AddEmptyRenegotiationInfo(&ssl->extensions, ssl->heap);
3879838967
if (ret != WOLFSSL_SUCCESS) {
@@ -39045,6 +39214,19 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl)
3904539214
*inOutIdx = begin + helloSz; /* skip extensions */
3904639215
}
3904739216

39217+
#ifdef HAVE_SECURE_RENEGOTIATION
39218+
/* renegotiation_info MUST be present on a renegotiation (RFC 5746 3.7). */
39219+
if (ssl->secure_renegotiation &&
39220+
ssl->secure_renegotiation->enabled &&
39221+
ssl->secure_renegotiation->verifySet &&
39222+
!ssl->secure_renegotiation->renegInfoSeen) {
39223+
WOLFSSL_MSG("Renegotiation ClientHello missing renegotiation_info");
39224+
SendAlert(ssl, alert_fatal, handshake_failure);
39225+
ret = SECURE_RENEGOTIATION_E;
39226+
goto out;
39227+
}
39228+
#endif /* HAVE_SECURE_RENEGOTIATION */
39229+
3904839230
#ifdef WOLFSSL_DTLS_CID
3904939231
if (ssl->options.useDtlsCID)
3905039232
DtlsCIDOnExtensionsParsed(ssl);

src/tls.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6277,6 +6277,9 @@ static int TLSX_SecureRenegotiation_Parse(WOLFSSL* ssl, const byte* input,
62776277
if (ret == WOLFSSL_SUCCESS)
62786278
ret = 0;
62796279
}
6280+
/* renegotiation_info seen (checked by DoClientHello, RFC 5746 3.7) */
6281+
if (ssl->secure_renegotiation != NULL)
6282+
ssl->secure_renegotiation->renegInfoSeen = 1;
62806283
if (ret != 0 && ret != WC_NO_ERR_TRACE(SECURE_RENEGOTIATION_E)) {
62816284
}
62826285
else if (ssl->secure_renegotiation == NULL) {

src/tls13.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6030,7 +6030,7 @@ static int DoTls13EncryptedExtensions(WOLFSSL* ssl, const byte* input,
60306030
i += OPAQUE16_LEN;
60316031

60326032
/* Extension data. */
6033-
if (i - begin + totalExtSz > totalSz)
6033+
if (i - begin + totalExtSz != totalSz)
60346034
return BUFFER_ERROR;
60356035
if ((ret = TLSX_Parse(ssl, input + i, totalExtSz, encrypted_extensions,
60366036
NULL))) {
@@ -6168,14 +6168,17 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input,
61686168
}
61696169
*inOutIdx += len;
61706170

6171+
/* No trailing bytes allowed (RFC 8446 4.3.2). */
6172+
if ((*inOutIdx - begin) != size)
6173+
return BUFFER_ERROR;
6174+
61716175
/* RFC 8446 Section 4.3.2: the signature_algorithms extension MUST be
61726176
* present in a CertificateRequest. */
61736177
if (peerSuites.hashSigAlgoSz == 0) {
61746178
SendAlert(ssl, alert_fatal, missing_extension);
61756179
WOLFSSL_ERROR_VERBOSE(INVALID_PARAMETER);
61766180
return INVALID_PARAMETER;
61776181
}
6178-
61796182
#ifdef WOLFSSL_CERT_SETUP_CB
61806183
if ((ret = CertSetupCbWrapper(ssl)) != 0)
61816184
return ret;

tests/api.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35193,9 +35193,12 @@ TEST_CASE testCases[] = {
3519335193
#endif
3519435194
TEST_DECL(test_tls_ems_downgrade),
3519535195
TEST_DECL(test_tls_ems_resumption_downgrade),
35196+
TEST_DECL(test_tls_ems_resumption_server_downgrade),
3519635197
TEST_DECL(test_tls12_chacha20_poly1305_bad_tag),
3519735198
TEST_DECL(test_tls13_null_cipher_bad_hmac),
3519835199
TEST_DECL(test_scr_verify_data_mismatch),
35200+
TEST_DECL(test_scr_no_renegotiation_option),
35201+
TEST_DECL(test_helloRequest_no_renegotiation_option),
3519935202
TEST_DECL(test_tls13_hrr_cipher_suite_mismatch),
3520035203
TEST_DECL(test_tls13_ticket_age_out_of_window),
3520135204
TEST_DECL(test_wolfSSL_DisableExtendedMasterSecret),

0 commit comments

Comments
 (0)