TLS 1.2, for contrast — and a padding oracle that actually works
TLS 1.3 is best understood as a list of deletions, and every deletion is a wound. This page executes the parts of TLS 1.2 that TLS 1.3 removed: the RFC 5246 PRF and the two-PRF key schedule, RSA key transport with no forward secrecy, and CBC with MAC-then-encrypt — where a server that says only “padding ok” or “padding error” hands an attacker your plaintext, one byte at a time. The padding oracle below is not an illustration: it runs here, queries the same kind of oracle a real server leaks, and the assertion that its output equals what was encrypted is checked before you see this page’s verification badge.
The padding oracle attack, in one card
A CBC decryption of one block is P = D(C) XOR prev. An
attacker does not know D(C), but they can choose prev and observe a server that
only tells them whether the resulting plaintext ends in valid PKCS#7 padding. Guessing the last byte of
D(C) and watching for “padding ok” recovers it; then the whole block falls, then every earlier block.
Here the oracle is a real AES-CBC decryption that reports only valid or invalid padding — the exact leak a 2010-era TLS stack produced when decrypting a CBC record and checking the padding before the MAC.
Downgrade protection: what 1.2 lacked and 1.3 added
| bytes | meaning | where |
|---|---|---|
| 44 4F 57 4E 47 52 44 01 | “DOWNGRD\x01” — a TLS 1.3-capable server that negotiated TLS 1.2 | last 8 bytes of ServerHello.random (RFC 8446 §4.1.3) |
| 44 4F 57 4E 47 52 44 00 | “DOWNGRD\x00” — negotiated TLS 1.1 or below | last 8 bytes of ServerHello.random |
| 0x0303 in the ClientHello version field | frozen legacy value; the real list lives in the supported_versions extension | ClientHello (RFC 8446 §4.1.2) |
The randomness in a server hello is covered by the handshake transcript, so the sentinel cannot be stripped without breaking the Finished MAC — which is exactly what makes it a usable tripwire. TLS 1.2 had no such tripwire: its only downgrade defence was the Finished MAC at the very end, which is what Logjam and FREAK defeated.
TLS 1.2 → TLS 1.3, field by field
| aspect | TLS 1.2 (RFC 5246) | TLS 1.3 (RFC 8446) |
|---|---|---|
| key exchange | RSA key transport (client picks the premaster), or (EC)DHE, or PSK — server chooses | (EC)DHE and/or PSK, with the key share sent in the ClientHello; static RSA removed |
| key schedule | one PRF (P_SHA256, or MD5/SHA-1 before 1.2) expanding directly to master_secret, then key_block | HKDF-Extract/Expand on a staged schedule: early → derived → handshake → derived → master, with Derive-Secret at each label |
| ciphers | AEAD, CBC and stream; a separate MAC algorithm per suite; compression | AEAD only; no CBC, no RC4, no stream ciphers, no compression |
| record protection | CBC with MAC-then-encrypt (padding + MAC), or AEAD | AEAD only — confidentiality and integrity in one operation, no separate padding oracle |
| forward secrecy | only if (EC)DHE was chosen; RSA key transport never has it | mandatory for the certificate flow; ephemeral keys deleted after the handshake |
| round trips | 2 (client offers, server chooses parameters, client sends keys/certificate) | 1 for a fresh handshake, 0 for 0-RTT resumption (replayable) |
| authentication | signature only over ServerKeyExchange (does not cover version or cipher suite) | signature over the full handshake transcript (CertificateVerify) |
| version negotiation | the ClientHello.version field itself | frozen legacy field + supported_versions extension, with a downgrade sentinel |
| removed | — | renegotiation, static RSA, CBC / MAC-then-encrypt, RC4, compression (CRIME), custom DH groups (Logjam), export ciphers (FREAK), the Hello-Request rehandshake |
Notation
| PRF(secret, label, seed) | RFC 5246 §5. P_<hash>(secret, label ‖ seed), where P_hash concatenates HMAC(secret, A(i) ‖ seed) blocks and A(i) = HMAC(secret, A(i−1)), A(0) = seed. In TLS 1.2 the hash is fixed by the cipher suite (P_SHA256 for everything defined in RFC 5246); TLS 1.0/1.1 instead used a split MD5/SHA-1 PRF. |
| pre_master_secret | 48 bytes. With RSA key transport the client invents it and sends it encrypted to the server’s RSA key; with (EC)DHE both sides derive it. |
| master_secret | PRF(pre_master_secret, "master secret", client_random ‖ server_random)[0..47] — 48 bytes, the root of every key. |
| key_block | PRF(master_secret, "key expansion", server_random ‖ client_random) — note the reversed random order — sliced into the MAC keys, encryption keys and IVs for both directions. |
| MAC-then-encrypt | The record is protected as Encrypt( fragment ‖ MAC ‖ padding ). The MAC is computed over seq_num ‖ type ‖ version ‖ length ‖ fragment. This ordering is what creates a padding oracle, because the receiver must look at the padding before it can verify the MAC. |
| PKCS#7 padding | Append n bytes each of value n so the total is a multiple of the block size (16 for AES); a full block of 0x10 is added when the input is already aligned. |
| padding oracle | Any observable difference — an alert type, an error string, or a timing difference — that tells the attacker whether decryption produced valid padding. In real TLS the receiver is supposed to return the same failure for bad padding and bad MAC, and to do it in constant time. |
| Finished | The handshake MAC: PRF(master_secret, "client finished"|"server finished", Hash(handshake_messages)) — 12 bytes by default. It proves both ends derived the same master secret over the same transcript. |
Glossary
Why the PRF is a single monolithic function, and why that was a mistake
The TLS 1.2 PRF takes a secret, a label and a seed and expands them to any length, and everything — the master secret, every key, the Finished MAC — comes out of that one construction with only the label to separate the uses. It works, but it carries no proof of key separation, and the label is an arbitrary ASCII string concatenated with no structure at all. TLS 1.3 replaced it with HKDF, a two-stage Extract-then-Expand design with a structured info field, and spends a whole section of RFC 8446 deriving each secret from a value that already mixes in the previous stage.
Bleichenbacher, and what happened to RSA key transport
Daniel Bleichenbacher’s 1998 attack on RSA PKCS#1 v1.5 encryption (the “million message attack”) applies directly to the TLS RSA key exchange: the client sends the premaster secret RSA-encrypted with PKCS#1 v1.5 padding, and a server that reported why a decryption failed leaked one bit about the plaintext. Repeated adaptively, that bit recovers the premaster secret and therefore the whole session. The TLS fix is to always proceed as if the decryption had succeeded — substituting a random premaster when it did not — and to keep the error path constant-time. The 2017 ROBOT work showed how many deployments had quietly reverted to a distinguishable error path. TLS 1.3’s answer was structural: delete the RSA key exchange entirely, so there is no PKCS#1 v1.5 ciphertext to attack.
BEAST, POODLE, Lucky 13, DROWN and Raccoon: five ways CBC or RSA broke
BEAST (2011) exploited TLS 1.0’s implicit IV — the IV is the previous record’s last ciphertext block — to mount a chosen-plaintext attack; TLS 1.1 fixed it with an explicit per-record IV. POODLE (2014) used SSL 3.0’s padding as an oracle to decrypt a byte at a time. Lucky 13 (2013) turned the CBC MAC-then-encrypt construction’s timing into an oracle: removing the padding and checking the MAC take time that depends on the padding. DROWN (2016) used an SSLv2 server with export ciphers to break RSA key exchange even on a modern TLS 1.2 endpoint. Raccoon (2020) is not a padding bug at all: it exploits a timing difference between HMAC computations over Diffie–Hellman premaster secrets of different lengths, leaking the leading bits of the shared secret. All five share one lesson — the moment a decryption routine’s behaviour depends on secret data, the protocol leaks it — which is why TLS 1.3 permits only AEAD records and constant-time key derivation.
Which padding-oracle variant this page implements
This page implements the classic padding-only setting: the record is PKCS#7-pad(fragment)
and then CBC-encrypted, and the oracle returns only “padding ok” or “padding invalid” — no MAC is involved. That
is the cleanest way to watch recovery happen byte by byte, and it is exactly the oracle POODLE exploited. Real
TLS 1.2 instead uses MAC-then-encrypt, so the padding sits after a 32-byte HMAC-SHA256 and the same
attack recovers that MAC along the way; this page also builds that record, to show its shape. In real TLS the
receiver, having removed the padding, then checks the MAC and returns a single uniform bad_record_mac
whether the padding or the MAC was wrong — and, since Lucky 13, in constant time. TLS 1.3 removes the whole class
by using AEAD records, where one tag covers ciphertext, header and length and there is no padding to leak at
all.
What this page does not do
It does not implement record-layer framing, alerts, retransmission or renegotiation, and the RSA key transport demonstration uses a deliberately small 512-bit key so the arithmetic is visible — it is labelled as the shape of the mechanism, not a usable key. The PRF, the key schedule, the CBC encryption and the padding oracle are real and complete, and the oracle attack is asserted against the plaintext it was given. The PRF’s output is additionally cross-checked against an independent implementation of RFC 5246 §5 outside the browser; the page reports the result of its own checks in the badge.