Skip to content
Open
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
50 changes: 50 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,19 @@
configuration now sees neither the prototype nor the `client_cert_cb`
typedef instead of failing to build; no other configuration changes.

* **Behavioral change (`wolfSSL_write_early_data` and the AEAD key usage
limit)**: RFC 9846, Section 5.5 adds that "it is not possible to perform a
KeyUpdate for early data; therefore, implementations MUST NOT exceed the
limits when sending early data". Reaching the limit mid-early-data
previously drove the ordinary rekey path, which emitted a KeyUpdate while
the client was still in `CLIENT_HELLO_COMPLETE` - before the handshake had
finished, where a conforming peer must reject it. The write now fails
instead, returning `WOLFSSL_FATAL_ERROR` with `wolfSSL_get_error()`
reporting `TOO_MUCH_EARLY_DATA`. Callers that hit it should complete the
handshake and send the remainder with `wolfSSL_write()`, which rekeys
normally. Reaching the limit needs roughly 23.7 million early data records
on one connection, so no practical caller is affected.

## Fixes

* **Fix (certificate manager left pointing at a released store)**:
Expand Down Expand Up @@ -198,6 +211,43 @@
than withdrawing them, matching what a failure to allocate the `ecc_key`
already did. Only affects builds with `WOLFSSL_BLIND_PRIVATE_KEY`.

* **Fix (fatal-level `user_canceled` closed a TLS 1.3 connection)**: RFC 9846,
Section 6.1 states that this alert "generally has AlertLevel=warning" and
that "receiving implementations SHOULD continue to read data from the peer
until a 'close_notify' is received". wolfSSL already exempted
`user_canceled` from the TLS 1.3 rule that all error alerts are fatal, but
both `DoAlert()` and `DoProcessAlertRecord()` acted on the AlertLevel byte
before reaching those exemptions, so a peer sending the alert at fatal level
tore the connection down and invalidated the session. The level byte
carries no meaning in TLS 1.3, and the alert is now ignored whichever level
the peer used. TLS 1.2 and earlier are unchanged: a fatal-level alert
remains fatal there.

* **Fix (key update cap turned a peer's `update_requested` into a fatal
error)**: RFC 9846, Section 4.7.3 adds that a sender at the 2^48-1 key
update cap "MUST NOT send its own KeyUpdate ... and SHOULD instead ignore
the 'update_requested' flag". Responding to a peer's request went through
the ordinary send path, which refuses at the cap with `BAD_STATE_E`, and
that error propagated out and killed the connection. The request is now
dropped and the connection continues on its current keys until the Section
5.5 data limits force it closed. An application-initiated
`wolfSSL_update_keys()` at the cap still reports `BAD_STATE_E`; the rule
applies only to responding to a peer.

* **Fix (malformed extension aborted without sending `decode_error`)**: RFC
9846, Section 4.3 adds that trailing data in an extension is forbidden and
that "receivers MUST abort the handshake with a 'decode_error' alert if
there is data left over after parsing the structure". The extension parsers
detect malformed structures, but around a third of them report it as the
wolfCrypt `BUFFER_E` rather than `BUFFER_ERROR`, and only `BUFFER_ERROR` was
mapped to an alert. `TranslateErrorToAlert()` returned `invalid_alert` for
`BUFFER_E`, which every caller treats as "send nothing", so the handshake
aborted correctly but silently and the peer saw only a dropped connection.
Both codes now map to `decode_error`. This affects `pre_shared_key`,
`psk_key_exchange_modes`, `early_data`, `cookie`, `post_handshake_auth` and
the certificate type extensions, and more generally any malformed handshake
message reported with `BUFFER_E`.

# wolfSSL Release 5.9.2 (Jun 23, 2026)

Release 5.9.2 has been developed according to wolfSSL's development and QA
Expand Down
2 changes: 1 addition & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -6047,7 +6047,7 @@ fi

# OLD TLS
AC_ARG_ENABLE([oldtls],
[AS_HELP_STRING([--enable-oldtls],[Enable old TLS versions < 1.2 (default: disabled)])],
[AS_HELP_STRING([--enable-oldtls],[Enable old TLS versions < 1.2, which RFC 8996 and RFC 9846 forbid negotiating (default: disabled)])],
[ ENABLED_OLD_TLS=$enableval ],
[ ENABLED_OLD_TLS=no ]
)
Expand Down
22 changes: 20 additions & 2 deletions doc/dox_comments/header_files/ssl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2584,6 +2584,11 @@ int wolfSSL_shutdown(WOLFSSL* ssl);

\param ssl pointer to the SSL session, created with wolfSSL_new().

\note RFC 9846, Section 6.1 requires this alert to be followed by a
close notify, which is why the shutdown is part of this call. On the
receiving side the alert is not itself an error: a TLS 1.3 peer keeps
reading until the close notify arrives, whatever AlertLevel was used.

_Example_
\code
int ret = 0;
Expand Down Expand Up @@ -14770,6 +14775,13 @@ int wolfSSL_require_psk(WOLFSSL* ssl);

\return BAD_FUNC_ARG if ssl is NULL or not using TLS v1.3.
\return WANT_WRITE if the writing is not ready.
\return BAD_STATE_E if the connection has already performed the maximum
number of key updates. RFC 9846, Section 4.7.3 caps a TLS 1.3 sender at
2^48-1 key updates; beyond that the connection must be closed rather than
rekeyed. Note that a KeyUpdate arriving from the peer with
request_update set is ignored once this cap is reached, rather than
failing the connection, so only an application-initiated update reports
this error.
\return WOLFSSL_SUCCESS if successful.

_Example_
Expand Down Expand Up @@ -14800,7 +14812,8 @@ int wolfSSL_update_keys(WOLFSSL* ssl);
is received.

\param [in] ssl a pointer to a WOLFSSL structure, created using wolfSSL_new().
\param [out] required 0 when no key update response required. 1 when no key update response required.
\param [out] required 0 when no key update response is required. 1 when
a key update response from the peer is still outstanding.

\return 0 on successful.
\return BAD_FUNC_ARG if ssl is NULL or not using TLS v1.3.
Expand Down Expand Up @@ -15622,7 +15635,12 @@ int wolfSSL_set_max_early_data(WOLFSSL* ssl, unsigned int sz);
\return SIDE_ERROR if called with a server.
\return BAD_STATE_E if invoked without a valid session or without a valid
PSK cb
\return WOLFSSL_FATAL_ERROR if the connection is not made.
\return WOLFSSL_FATAL_ERROR if the connection is not made, or if the
AEAD key usage limit would be exceeded by this write, in which case
wolfSSL_get_error() reports TOO_MUCH_EARLY_DATA. A KeyUpdate cannot be
performed while sending early data (RFC 9846, Section 5.5), so no further
early data can be sent on this connection; complete the handshake with
wolfSSL_connect_TLSv13() and send the remainder with wolfSSL_write().
\return the amount of early data written in bytes if successful.

_Example_
Expand Down
10 changes: 10 additions & 0 deletions src/dtls.c
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ void DtlsResetState(WOLFSSL* ssl)
ssl->keys.dtls_sequence_number_hi = 0;
ssl->keys.dtls_sequence_number_lo = 0;

/* Forget any alert this object sent for the ClientHello being abandoned.
* DoClientHello() can send a fatal alert on the stateless path and then
* swallow the error (DtlsIgnoreError) so the object stays up waiting for
* the next ClientHello. A leftover alert_fatal in the history makes the
* "already sent a more specific fatal alert" guards suppress every later
* alert on this object, so a single malformed ClientHello from a spoofed
* address would mute alerts for every peer that follows. */
ssl->alert_history.last_tx.code = -1;
ssl->alert_history.last_tx.level = -1;

/* Reset states */
ssl->options.serverState = NULL_STATE;
ssl->options.clientState = NULL_STATE;
Expand Down
109 changes: 96 additions & 13 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@
* WOLFSSL_TLS13_NO_PEEK_HANDSHAKE_DONE:
* Disable peek returning WANT_READ for tickets default: off
* WOLFSSL_TLS13_IGNORE_AEAD_LIMITS:
* Ignore AEAD message limits from RFC 8446 default: off
* Ignore AEAD message limits from RFC 9846 5.5, which
* makes observing them a MUST default: off
* WOLFSSL_DTLS13_SEND_MOREACK_DEFAULT:
* Send more ACKs by default in DTLS 1.3 default: off
*
Expand Down Expand Up @@ -23958,10 +23959,30 @@ static void LogAlert(int type)
#endif /* DEBUG_WOLFSSL */
}

/* RFC 9846 Section 6.1 exempts "user_canceled" from tearing the connection
* down whatever AlertLevel the peer used, because the level byte carries no
* meaning in TLS 1.3.
*
* The exemption needs a connection that has actually negotiated TLS 1.3.
* ssl->version holds the highest version this side offered until the peer's
* choice is known, so testing it would also exempt a pre-1.3 peer's fatal
* alert sent before version negotiation and leave the caller waiting for a
* handshake that is never coming. options.tls1_3 is only set once the version
* is settled - including by wolfSSL_set_session() for a TLS 1.3 session.
*
* ssl The SSL/TLS object.
* code Alert description received.
* returns 1 when the alert must be ignored rather than acted on.
*/
static int AlertIsExemptUserCanceled(const WOLFSSL* ssl, int code)
{
return ssl->options.tls1_3 && (code == user_canceled);
}

/* 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
* fatal alert MUST be invalidated so it cannot be resumed. (TLS 1.3 RFC 9846
* 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
Expand All @@ -23975,7 +23996,7 @@ static void InvalidateSessionOnFatalAlert(WOLFSSL* ssl)
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
* ignored) by DoAlert, and the teardown alert routes back here. RFC 9846
* 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) &&
Expand Down Expand Up @@ -24039,10 +24060,16 @@ static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type)
{
ssl->alert_history.last_rx.code = code;
ssl->alert_history.last_rx.level = level;
if (level == alert_fatal) {
/* RFC 9846 Section 6.1: "user_canceled" only "generally" has
* AlertLevel=warning, and a receiver SHOULD keep reading until
* "close_notify" arrives. The level byte is meaningless in TLS 1.3,
* so do not let a peer that sends the alert at fatal level tear the
* connection down. */
if (level == alert_fatal &&
!AlertIsExemptUserCanceled(ssl, code)) {
ssl->options.isClosed = 1; /* Don't send close_notify */
}
/* RFC 8446 Section 6.2: In TLS 1.3, all error alerts are implicitly
/* RFC 9846 Section 6.2: In TLS 1.3, all error alerts are implicitly
* fatal regardless of the AlertLevel byte. */
if (IsAtLeastTLSv1_3(ssl->version) &&
code != close_notify && code != user_canceled) {
Expand Down Expand Up @@ -24094,12 +24121,17 @@ static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* 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)))
* (RFC 5246 7.2.2; in TLS 1.3 all error alerts are fatal, RFC 9846
* 6.2). "close_notify" is not an error, and "user_canceled" is exempt
* in TLS 1.3 at any AlertLevel (RFC 9846 6.1). */
if (IsAtLeastTLSv1_3(ssl->version)) {
if (*type != close_notify &&
!AlertIsExemptUserCanceled(ssl, *type))
InvalidateSessionOnFatalAlert(ssl);
}
else if (level == alert_fatal && *type != close_notify) {
InvalidateSessionOnFatalAlert(ssl);
}
#endif
}
return level;
Expand Down Expand Up @@ -24949,7 +24981,9 @@ static int DoProcessAlertRecord(WOLFSSL* ssl)
WOLFSSL_MSG("got ALERT!");
ret = DoAlert(ssl, ssl->buffers.inputBuffer.buffer,
&ssl->buffers.inputBuffer.idx, &type);
if (ret == alert_fatal)
/* RFC 9846 Section 6.1: keep reading past a TLS 1.3 "user_canceled" until
* "close_notify" arrives, whatever AlertLevel the peer used. */
if (ret == alert_fatal && !AlertIsExemptUserCanceled(ssl, type))
return FATAL_ERROR;
else if (ret < 0)
return ret;
Expand All @@ -24965,7 +24999,7 @@ static int DoProcessAlertRecord(WOLFSSL* ssl)
if (type == decrypt_error)
return FATAL_ERROR;

/* RFC 8446 Section 6.2: In TLS 1.3, all error alerts MUST
/* RFC 9846 Section 6.2: In TLS 1.3, all error alerts MUST
* be treated as fatal regardless of the AlertLevel byte.
* Only close_notify (handled above) and user_canceled
* are exempt. */
Expand Down Expand Up @@ -28594,7 +28628,7 @@ int IsSCR(WOLFSSL* ssl)
!defined(WOLFSSL_TLS13_IGNORE_AEAD_LIMITS)
/*
* Enforce limits specified in
* https://www.rfc-editor.org/rfc/rfc8446#section-5.5
* https://www.rfc-editor.org/rfc/rfc9846#section-5.5
*/
static int CheckTLS13AEADSendLimit(WOLFSSL* ssl)
{
Expand Down Expand Up @@ -28664,6 +28698,20 @@ static int CheckTLS13AEADSendLimit(WOLFSSL* ssl)
if (w64GTE(seq, limit)) { /* cppcheck-suppress uninitvar
* (false positive from cppcheck-2.13.0)
*/
#ifdef WOLFSSL_EARLY_DATA
/* RFC 9846 Section 5.5: a KeyUpdate cannot be performed for early
* data, so a sender MUST NOT exceed the limits while sending it.
* There is no way to rekey at this point - the handshake has not
* finished, so a KeyUpdate here would be out of order - and the write
* has to fail instead. */
if (ssl->options.side == WOLFSSL_CLIENT_END &&
ssl->earlyData != no_early_data &&
ssl->earlyData != done_early_data) {
WOLFSSL_MSG("AEAD limit reached while sending early data");
WOLFSSL_ERROR_VERBOSE(TOO_MUCH_EARLY_DATA);
return TOO_MUCH_EARLY_DATA;
Comment thread
SparkiDev marked this conversation as resolved.
}
#endif
return Tls13UpdateKeys(ssl); /* Need to generate new keys */
}

Expand Down Expand Up @@ -28926,6 +28974,34 @@ int SendData(WOLFSSL* ssl, const void* data, size_t sz)
#if defined(WOLFSSL_TLS13) && !defined(WOLFSSL_TLS13_IGNORE_AEAD_LIMITS)
if (IsAtLeastTLSv1_3(ssl->version)) {
ret = CheckTLS13AEADSendLimit(ssl);
#ifdef WOLFSSL_EARLY_DATA
/* Hitting the early data limit part way through a multi-record
* write must not throw away the records already emitted in this
* call: the caller is told to send the remainder over the
* completed handshake, and it can only work out the remainder
* from the returned count. Report the short write; the next call
* re-runs this check with nothing sent yet and fails properly. */
if ((ret == WC_NO_ERR_TRACE(TOO_MUCH_EARLY_DATA)) && (sent > 0)) {
Comment thread
SparkiDev marked this conversation as resolved.
Comment thread
SparkiDev marked this conversation as resolved.
Comment thread
SparkiDev marked this conversation as resolved.
/* This check runs at the top of every iteration, including
* the one after the last record, so it also fires when the
* final record happened to land exactly on the limit. That
* write is complete: break without recording an error, or the
* caller sees a full byte count from a call that
* wolfSSL_get_error() calls a failure and ReceiveData()
* refuses to read after.
*
* A genuinely short write does record the reason. Every other
* short return here is gated on partialWrite, so a bare short
* count would look like a complete one; wolfSSL_get_error(ssl,
* 0) reports TOO_MUCH_EARLY_DATA instead. The loop clears
* ssl->error only after a record goes out, so it survives to
* the caller. */
if (sent < (word32)sz) {
ssl->error = ret;
}
break;
}
#endif
if (ret != 0) {
ssl->error = ret;
return WOLFSSL_FATAL_ERROR;
Expand Down Expand Up @@ -38336,7 +38412,14 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx,
int TranslateErrorToAlert(int err)
{
switch (err) {
/* RFC 9846 Section 4.3 requires a "decode_error" alert when an
* extension has data left over after its structure is parsed, and
* Section 6.2 defines the alert for any field out of range or
* message of incorrect length. The extension parsers report those
* as either BUFFER_ERROR or the wolfCrypt BUFFER_E; both must map
* here, or the handshake aborts silently with no alert sent. */
case WC_NO_ERR_TRACE(BUFFER_ERROR):
case WC_NO_ERR_TRACE(BUFFER_E):
Comment thread
SparkiDev marked this conversation as resolved.
return decode_error;
case WC_NO_ERR_TRACE(EXT_NOT_ALLOWED):
case WC_NO_ERR_TRACE(PEER_KEY_ERROR):
Expand Down
16 changes: 15 additions & 1 deletion src/ssl_api_rw.c
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,21 @@ static int wolfssl_write_dup_do_tls13_work(WOLFSSL* ssl)
{
/* keyUpdateRespond is cleared in SendTls13KeyUpdate. */
if (ssl->keys.keyUpdateRespond) {
ret = Tls13UpdateKeys(ssl);
/* RFC 9846 Section 4.7.3: a sender that would exceed the
* key update limit "MUST NOT send its own KeyUpdate ...
* and SHOULD instead ignore the 'update_requested' flag".
* The read side delegated this response without seeing the
* cap - it never sends KeyUpdates, so its count is not the
* one that matters - so the check belongs here, on the
* side that actually sends and owns the counter. */
if (Tls13KeyUpdateLimitReached(ssl)) {
WOLFSSL_MSG("Key update limit reached; ignoring "
"delegated update_requested");
ssl->keys.keyUpdateRespond = 0;
}
else {
ret = Tls13UpdateKeys(ssl);
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/tls.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
* WOLFSSL_OLD_PRIME_CHECK: Use old DH prime checking method default: off
* WOLFSSL_STATIC_DH: Enable static DH cipher suites default: off
* WOLFSSL_STATIC_EPHEMERAL: Enable static ephemeral key loading default: off
* Reuses a key share across connections, which
* RFC 9846 4.3.8 forbids. Inspection/debug only.
*
* Post-Quantum:
* WOLFSSL_HAVE_MLKEM: Enable ML-KEM (Kyber) support default: off
Expand Down
Loading
Loading