Rustls Fixes a Two-Year-Old TLS 1.3 Spec Deviation That OpenSSL Never Had

Rustls Fixes a Two-Year-Old TLS 1.3 Spec Deviation That OpenSSL Never Had

Rustls 0.23.45 fixes a security issue that has been present since v0.23.13 in September 2024. OpenSSL, BoringSSL and the other established implementations were never affected.

What was wrong

Rustls accepted TLS 1.3 handshake messages sent at the wrong encryption level when they followed a key-changing message within the same record. That is a deviation from RFC 8446.

The project’s own description of the impact is appropriately measured:

“Rustls accepted TLS 1.3 handshake messages sent at the wrong encryption level when they followed a key-changing message in the same record. The handshake transcript is still authenticated, so a network-position attacker cannot use this to alter or complete a handshake; the practical effect is that a peer could send handshake messages that should be encrypted in plaintext without rustls rejecting the connection.”

That is the right way to describe a security issue. The handshake transcript remains authenticated, so an attacker on the network path cannot alter or complete a handshake using this. What they could do is send, in plaintext, handshake messages that TLS 1.3 requires to be encrypted, and Rustls would accept the connection rather than rejecting it.

A protocol violation that should be fatal was tolerated. Not a break, but a hole in an enforcement boundary that exists precisely so that deviations get rejected.

Notably, a very similar issue affected Go’s TLS implementation back in January. Two independent modern implementations, the same corner of the same RFC, the same mistake.

The uncomfortable point, stated fairly

Rustls exists substantially because TLS libraries written in C have produced a long history of memory-safety catastrophes. That argument is sound, and this bug does not undermine it.

It does illustrate the boundary of the claim. Memory safety prevents a category of bug. It does not prevent protocol logic errors, and a state machine that accepts a message it should have rejected is a logic error. Rust’s compiler has no opinion about whether your TLS state machine matches RFC 8446.

The mature framing is that a rewrite trades a known bug class for an unknown one. You eliminate use-after-free and buffer overflows, and you take on the risk that comes with a newer implementation of an intricate specification that the old code had two decades to get wrong and then fix.

That sits alongside two other items this month. Gzip 1.15 fixed a buffer overflow present since the beginning, which is the argument for rewriting in a memory-safe language. This is the argument for being patient about it. Both are true, and the honest position holds both.

The counterpoint in Rustls’s favour is that the failure mode here is considerably gentler. A protocol deviation that fails closed on the next check is not equivalent to remote code execution.

What to do

Update. The fix is in 0.23.45, and anything from 0.23.13 onwards is affected.

# what version is in your dependency tree
cargo tree -i rustls

# check for known advisories across all dependencies
cargo audit

Rustls sits underneath a growing amount of infrastructure, frequently without being visible. It is the TLS backend for a range of Rust HTTP clients and servers, so the relevant question is usually not “do I use Rustls” but “does anything in my dependency tree use it”. cargo tree -i answers that.

The exposure is low and the fix is trivial to take, which is the best combination a security advisory can offer.

Background reading

Explainers for the concepts behind this story.