Skip to content

The history of TLS and SSL: from SSL 2.0 to TLS 1.3, 1994-2018

· 23 min read
Copyright: MIT
The wordmark TLS 1.3 in large monospace type with a single orange underline bar and a small grey RFC 8446 subtitle

Open a TCP connection to any web server on port 443 and the first thing you send is a record that has not changed shape in twenty-five years. The version field at the front of it still reads 0x0301, the number that meant TLS 1.0 in 1999, even when the connection that follows is TLS 1.3. The protocol carries its own history in its bytes, and that history is mostly a list of things that went wrong. Each version after the first was written because someone found a way to read the plaintext, and each new version is the patch.

This is the story of how a layer of encryption built in a hurry at Netscape in 1994, to make people comfortable typing a credit card number into a browser, became the thing that protects most of the traffic on the internet. It is also a story about how that layer kept breaking. The interesting parts are not the clean version numbers but the attacks in between them, the ones with names like BEAST and POODLE and Heartbleed that each forced a generation of servers to change.

The road map is roughly chronological. We start with the SSL versions Netscape shipped and why two of the three were broken on arrival. Then TLS 1.0 through 1.2, the slow IETF standardization, and the long middle period where the protocol looked stable but the implementations and cipher modes underneath it were quietly rotting. Then the run of named attacks from 2011 to 2015 that made TLS front-page news. And finally TLS 1.3, four years in committee, which threw out most of the negotiable parts of the protocol because negotiation was where the bodies kept turning up.

Netscape and the three versions of SSL

The Secure Sockets Layer started inside Netscape Communications in 1994. The browser company needed a way to encrypt the connection between a browser and a server so that commerce could happen over the web, and it needed it fast, because the whole pitch of the early web was that you could do something on it. Taher Elgamal, Netscape’s chief scientist, led the design and is usually called the father of SSL. He had already given his name to the ElGamal encryption and signature scheme, the second of which fed into the NIST Digital Signature Algorithm, so the person writing the company’s crypto knew the field.

SSL 1.0 never shipped. It had serious flaws in the protocol, the kind you find when you show a draft to cryptographers outside the building, and Netscape never released it to the public. The first version anyone outside the company used was SSL 2.0, in February 1995.

SSL 2.0 was broken in ways that are easy to describe and were hard to live with. It used the same key material for encryption and for message authentication, which couples two things that should be independent. Its integrity check was a MAC built from MD5 with a secret prefix, a construction vulnerable to length-extension. There was no protection on the handshake itself, so an attacker sitting in the middle could strip the list of cipher suites down to the weakest one both sides would accept, and neither side would notice the negotiation had been tampered with. Within a year it was clear SSL 2.0 needed replacing rather than patching.

SSL 3.0 arrived in 1996 and was a full redesign, not a revision. Paul Kocher, who would later be one of the people behind the Spectre and Meltdown disclosures, worked with Netscape engineers Phil Karlton and Alan Freier on the protocol, with a reference implementation from Christopher Allen and Tim Dierks. SSL 3.0 separated the keys for encryption and authentication, switched to a better MAC, and added the framing that the rest of the family would inherit. It was good enough that the protocol design from 1996 is recognizably the protocol we still use. It was also, as we will see, still carrying one padding flaw that took eighteen years to weaponize.

SSL 1.0never shipped SSL 2.01995 SSL 3.01996 TLS 1.01999 TLS 1.12006 TLS 1.22008 TLS 1.32018 Transport encryption, 1994–2018 Netscape owned the first three. The IETF owned the rest. *The version line. The names change from SSL to TLS at the 1999 handoff to the IETF, but the wire-format lineage is continuous, which is why TLS 1.0 is `0x0301` on the wire (SSL 3.0 being 3.0).*

The handoff to the IETF, and TLS 1.0

By the late 1990s a security protocol owned by a single browser vendor was an awkward thing for the rest of the industry to build on. The work moved to the IETF, which published TLS 1.0 as RFC 2246 in January 1999. The document was written by Christopher Allen and Tim Dierks, the same pair from the SSL 3.0 reference implementation, so the continuity was real. TLS 1.0 is close enough to SSL 3.0 that it was sometimes called SSL 3.1, and the version byte on the wire reflects exactly that: 0x0301.

The renaming had a political reason as much as a technical one. Calling it Transport Layer Security rather than Secure Sockets Layer let the standard belong to everyone instead of to Netscape. The differences from SSL 3.0 were modest. TLS used a standardized HMAC for its record MAC and a different pseudorandom function for key derivation. It was an incremental cleanup of a protocol that already worked.

Then nothing much happened for seven years. TLS 1.1 did not arrive until April 2006, as RFC 4346. The headline change was a direct response to a class of attacks on CBC-mode encryption that had been theorized but not yet weaponized in the browser. TLS 1.0 derived the initialization vector for each record from the last ciphertext block of the previous record, which meant the IV for record N was predictable to anyone watching record N minus one. TLS 1.1 made the IV explicit and random per record. That single change is what later made TLS 1.1 immune to the attack that would eventually be called BEAST, even though nobody had demonstrated BEAST yet.

TLS 1.2 followed in August 2008 as RFC 5246, and it is the version that carried most of the web for the next decade. Its important move was to stop hardcoding MD5 and SHA-1 into the protocol’s own constructions. Earlier versions baked specific hash functions into the PRF and the digitally-signed elements. TLS 1.2 made the hash negotiable and introduced authenticated encryption with associated data, the AEAD cipher modes such as AES-GCM that combine encryption and integrity in one primitive. AEAD is the thing that, used correctly, makes a whole family of padding and MAC-ordering attacks impossible. The catch was the phrase “used correctly.” TLS 1.2 still permitted CBC modes, still permitted RC4, still permitted compression, and still permitted RSA key transport. Everything that broke over the next seven years was something TLS 1.2 allowed but did not require.

The long middle: where the rot was

It is tempting to read the version timeline as steady progress. It was not. Between 2008 and 2018, the negotiated version of TLS most of the web actually used barely moved, while the attacks moved constantly. The vulnerabilities of this period share a shape. They almost never break the core cryptography. They break the seams: the padding, the compression, the version negotiation, the fallback logic, the implementations. The math was mostly fine. The engineering around the math was the soft target.

A useful way to hold the next several attacks in your head is to sort them by which seam they exploit.

The attacks broke the seams, not the math BEAST · 2011CBC IV prediction POODLE · 2014SSLv3 CBC padding CRIME · 2012TLS compression BREACH · 2013HTTP gzip FREAK · 2015export RSA downgrade Logjam · 2015export DH downgrade Heartbleed · 2014not a protocol flaw — an OpenSSL memory bug in the heartbeat extension padding / IV compression side-channel negotiation downgrade *Five of these six are seam attacks: predictable padding, leaky compression, or weak-cipher negotiation. Heartbleed is the odd one out, a plain memory-safety bug in one library that happened to sit under most of the web.*

BEAST, 2011

On 23 September 2011, Thai Duong and Juliano Rizzo demonstrated a working attack against TLS 1.0 at the Ekoparty conference. They called it BEAST, for Browser Exploit Against SSL/TLS, and it carried CVE-2011-3389. The underlying weakness was old. Phillip Rogaway had pointed out in 2002 that TLS 1.0’s chained, predictable initialization vectors made CBC mode vulnerable to a chosen-plaintext attack. What Duong and Rizzo added was a practical way to run it from inside a browser.

The mechanism, at the level that is safe to describe, works like this. In TLS 1.0 the IV for a record is the last ciphertext block of the previous record, so an attacker who can see the wire knows it in advance. CBC encryption of a block XORs the plaintext with the previous ciphertext block before the cipher runs. If you can predict that previous block and you can control where the secret you are guessing falls inside a block, you can test a guess: arrange for your guessed plaintext to XOR against the known IV in a way that, if your guess is right, produces a ciphertext block you have seen before. The attack recovers the secret one byte at a time by aligning an unknown byte against known bytes and brute-forcing 256 possibilities per position. To run it in a browser you need to inject chosen plaintext into the same TLS connection that carries the victim’s session cookie, which Duong and Rizzo did with a Java applet that violated same-origin assumptions.

TLS 1.1 was already immune because of its explicit random IVs. But in 2011 almost nobody negotiated TLS 1.1. The practical fix that actually shipped was a hack in the implementations: the 1/n-1 record split, where the client sends the first byte of application data in its own record so the IV for the rest is no longer predictable from anything the attacker arranged. Browsers shipped that, and the long migration toward TLS 1.1 and 1.2 finally got a reason to move.

CRIME and BREACH, 2012 to 2013

The same two researchers came back a year later with a different seam. CRIME, presented in September 2012 and tracked as CVE-2012-4929, attacks compression rather than encryption. Adam Langley had floated the idea in 2011; Rizzo and Duong built it.

The principle is that compression leaks information about plaintext through ciphertext length, an observation the cryptographer John Kelsey wrote up back in 2002. If a request contains both an attacker-controlled value and a secret, and the whole request is compressed before encryption, then a guess that matches part of the secret compresses slightly better, and the encrypted record is a byte or two shorter. The attacker cannot read the ciphertext, but they can measure its length. By injecting guesses and watching the compressed size, they recover a session cookie character by character. CRIME targeted TLS-level compression and the compression in SPDY. The fix was blunt: turn TLS compression off. Browsers and servers did, and it has been off ever since.

BREACH, presented in August 2013 by Yoel Gluck, Angelo Prado, and Neal Harris and tracked as CVE-2013-3587, is the same idea aimed at HTTP-level gzip compression instead of TLS compression. Disabling TLS compression did nothing to stop it, because the compression it abuses lives in the web server’s response handling. BREACH was harder to mitigate cleanly, since gzipping HTTP responses is genuinely useful, and the defenses ended up being application-level: separating secrets from reflected user input, adding random length padding, or masking CSRF tokens per request.

Heartbleed, 2014

Heartbleed is the one your non-technical relatives heard about, and it is the odd member of the group because it is not a flaw in TLS at all. It is a memory-safety bug in OpenSSL, the library that happened to terminate TLS for most of the internet. It was disclosed on 7 April 2014 as CVE-2014-0160, found independently by Neel Mehta of Google’s security team and by the Finnish firm Codenomicon within days of each other.

The bug lived in OpenSSL’s implementation of the TLS heartbeat extension, defined in RFC 6520. A heartbeat is a keepalive: one side sends a small message with a payload and a stated payload length, and the other side echoes the payload back. The OpenSSL code read the attacker-supplied length field, allocated a response buffer of that size, and copied that many bytes out of the incoming message with memcpy, without checking that the message actually contained that many bytes of payload. Claim a payload of one byte but a length of 65535, and the server copies your one byte plus roughly 64KB of whatever was sitting next to it in heap memory, then encrypts it and sends it back. That adjacent memory could hold session cookies, decrypted requests from other users, or the server’s own private key.

A heartbeat request that lies about its length type len=65535 "A" ← payload claims 65535 bytes, sends 1 server response: memcpy(buf, payload, 65535) "A" ...65534 bytes of adjacent heap: keys, cookies, other users' plaintext... *The bug was a missing bounds check: the code trusted the attacker's length field instead of measuring the actual payload. RFC 6520 specifies the field; OpenSSL failed to validate it against reality.*

The flawed code was committed on 31 December 2011 by Robin Seggelmann, then a PhD student contributing to OpenSSL, and shipped in OpenSSL 1.0.1 on 14 March 2012. It sat in production for two years. Versions 1.0.1 through 1.0.1f were vulnerable; 1.0.1g, released the day of disclosure, added the bounds check. Because the bug leaked private keys, the correct response was not just to patch but to reissue every certificate and revoke the old ones, which is why Heartbleed was expensive far out of proportion to the size of the code change. Other TLS stacks, GnuTLS and Mozilla’s NSS and Microsoft’s SChannel among them, were untouched, because the defect was in OpenSSL’s code rather than in the protocol. Heartbleed is the clearest argument in this whole history that the protocol is only as safe as the single library most of the world runs it through. If you have read our history of web scraping, the same lesson recurs: a monoculture of implementations means a single bug has internet-scale blast radius.

POODLE, 2014

POODLE, disclosed on 14 October 2014 as CVE-2014-3566, finally cashed in the padding flaw that had been sitting in SSL 3.0 since 1996. The name stands for Padding Oracle On Downgraded Legacy Encryption, and both halves of that phrase matter.

The padding-oracle half: SSL 3.0’s CBC padding does not specify the contents of the padding bytes, only that the last byte gives the padding length. A receiver checks the MAC and the length but cannot check the padding bytes themselves, because they are undefined. That gap lets an attacker who can manipulate ciphertext distinguish, by whether the connection survives, a correct padding guess from an incorrect one, and a padding oracle that leaks one bit per query is enough to recover plaintext a byte at a time given enough queries. As with BEAST, the attack needs the victim’s browser to make many requests with a secret in a controllable position, which JavaScript on an attacker page can arrange.

The downgrade half is what made it relevant in 2014, years after everyone had TLS. Browsers, in the name of compatibility with broken servers, implemented a fallback dance: if a TLS handshake failed, retry with a lower version, and keep retrying down the ladder until something worked, all the way to SSL 3.0. An active attacker could force those failures by interfering with the handshakes, pushing a perfectly modern client and server down to SSL 3.0 against the wishes of both. POODLE married a fourteen-year-old protocol flaw to a fallback mechanism that existed only to be polite to old software.

The fix had two parts. The clean part was to stop using SSL 3.0, and the disclosure is what finally killed it. The mechanism part was TLS_FALLBACK_SCSV, a special signaling cipher-suite value the client includes when it is retrying after a failure; a server that sees it and knows it supports a higher version can recognize that the downgrade is not legitimate and refuse the connection. SSL 3.0 was formally deprecated in June 2015 by RFC 7568. SSL 2.0 had already been prohibited outright by RFC 6176 in 2011.

FREAK and Logjam, 2015

The last two attacks of the run both dug up something from the 1990s: U.S. export restrictions on cryptography. For years American law treated strong crypto as a munition and limited what could be exported, so SSL implementations shipped deliberately weakened “export-grade” cipher suites with key sizes small enough to satisfy the regulators. The export rules were gone by the late 1990s. The export cipher suites were not. They lingered in code for fifteen years, supported but unused, until 2015 when researchers showed they could be reached by force.

FREAK, for Factoring RSA Export Keys and tracked as CVE-2015-0204, was disclosed on 3 March 2015. A flaw in several TLS implementations let an active attacker ask a server to use an export-grade RSA key exchange even when the client had requested a strong one, and then accept it on the client side. Export RSA keys were capped at 512 bits. At disclosure, factoring a 512-bit RSA modulus cost about $100 of cloud GPU time and ran in roughly half a day, after which the attacker could decrypt the session. The weak key the standard left lying around did the rest.

Logjam, from the same broad group of researchers and tracked as CVE-2015-4000, is the Diffie-Hellman analogue. It downgrades a connection to export-grade DH, capped at 512-bit groups, which can be broken in the same range. Logjam had a sharper edge: many servers reused the same handful of standard 512-bit DH groups, and the expensive part of breaking DH, the precomputation, can be done once per group and then amortized across every connection that uses it. The Logjam authors argued that precomputation against a small number of common 1024-bit groups was within reach of a nation-state budget, which reframed even “strong” 1024-bit DH as suspect and pushed the web toward 2048-bit groups and elliptic-curve key exchange.

The common thread of FREAK and Logjam is that a protocol which negotiates its own parameters can be talked into negotiating badly, and that dead code left in for compatibility is still attack surface. Both are downgrade attacks. Both exploit a feature the standard never removed. The cleanest defense is not to support the weak option at all, which is exactly the conclusion TLS 1.3 reached about almost everything.

TLS 1.3: deleting the negotiable parts

By the mid-2010s the lesson was hard to miss. Most TLS attacks lived in the protocol’s flexibility, in the old cipher modes and weak key exchanges and version-fallback logic that stayed reachable for the sake of some server that had not been touched since 2003. TLS 1.3, published as RFC 8446 in August 2018, is the protocol that took the lesson seriously. The design instinct shifted from “negotiate the best available option” to “delete the options that keep getting us killed.”

The list of removals is the heart of it. Static RSA key exchange is gone, which means every TLS 1.3 connection has forward secrecy: an attacker who records traffic now and steals the server’s private key later still cannot decrypt the recording, because the session keys came from an ephemeral Diffie-Hellman exchange that left no trace on disk. CBC-mode ciphers are gone. RC4 is gone. TLS-level compression is gone, which closes CRIME at the protocol level. Renegotiation is gone. Custom and weak Diffie-Hellman groups are gone, replaced by a fixed set of vetted groups, which closes Logjam by construction. The DSA signature algorithm is gone. What remains is AEAD cipher suites only and a short list of strong key-exchange groups. There is much less to negotiate, so there is much less to negotiate badly.

The handshake itself was restructured, and the visible payoff is speed. A full TLS 1.2 handshake costs two round trips before any application data flows. TLS 1.3 collapses that to one. The client guesses the key-exchange parameters the server will accept and sends its Diffie-Hellman key share in the very first message, so that by the time the server replies it can already derive keys and start encrypting. Most of the handshake after the first server message is itself encrypted, hidden inside an EncryptedExtensions message, where in TLS 1.2 it travelled in the clear. And because the whole handshake is signed by the server, the downgrade tricks behind FREAK and POODLE no longer work: an attacker who tampers with the negotiation breaks the signature.

One round trip instead of two TLS 1.2 clientserver ClientHello ServerHello, cert, key exch, Finished Finished data (2-RTT) TLS 1.3 clientserver ClientHello + key share ServerHello + key share, EncryptedExtensions, Finished data (1-RTT) *The client sends its key share with the first message instead of waiting for the server to pick parameters. Everything after ServerHello is encrypted, including the certificate and the extensions.*

There is one genuinely new risk in TLS 1.3, and it is worth stating plainly because it is the price of the speed. The protocol added a 0-RTT resumption mode where a returning client can send application data in its very first flight, before the handshake completes, using a key established in a previous session. That saves a full round trip on repeat visits. But 0-RTT data has no protection against replay: an attacker who captures the encrypted early data can send it to the server again, and the server cannot tell the difference between the original and the copy from inside the handshake alone. The specification is explicit that 0-RTT must only carry requests that are safe to replay, idempotent reads rather than anything that changes state. It is a sharp edge the protocol hands to application developers, and getting it wrong reintroduces a class of bug the rest of TLS 1.3 worked hard to remove.

Why it took four years and why the bytes lie

TLS 1.3 took roughly four years from the first drafts in 2014 to publication in 2018, and a large share of that time went not into cryptography but into getting the new protocol past the existing internet. The obstacle had a name in the working group: ossification. Middleboxes, the firewalls and proxies and “security” appliances sitting between clients and servers, had been written to parse TLS 1.2 handshakes and to assume the handshake would always look the way it looked in 2012. Many of them broke when they saw a handshake they did not recognize, dropping connections rather than passing through a version they had never been taught about. Early TLS 1.3 deployment experiments found double-digit percentages of connections failing because of middleboxes that could not cope with a new version number.

The working group’s response was a kind of camouflage. TLS 1.3 was made to look, on the wire, a lot like TLS 1.2 session resumption, so that middleboxes expecting the old shape would wave it through. The version negotiation moved out of the legacy version field, which now permanently reads 0x0303 (TLS 1.2) for compatibility, and into a supported_versions extension where the real version lives. This is why the bytes lie. The number at the front of a modern handshake names a version from 2008 on purpose, so that hardware which stopped learning a decade ago does not panic. The fix for a brittle ecosystem was to make the new thing impersonate the old thing.

Ossification also produced GREASE, a mechanism in which clients deliberately advertise reserved, meaningless values in extension and version fields so that servers and middleboxes are forced to ignore unknown values rather than choke on them, keeping those code paths exercised and tolerant for the next real extension. That same handshake camouflage is exactly the surface that TLS fingerprinting reads to tell one client from another, and the GREASE values are part of what a fingerprint has to account for. If you want the frame-by-frame view of the modern handshake, our TLS 1.3 handshake walkthrough takes it apart message by message.

What the record kept, and where it went next

Look back over the whole arc and a pattern holds. Almost none of these attacks broke the underlying ciphers. AES was fine. RSA the algorithm was fine; it was 512-bit export RSA, left in for backward compatibility, that fell. The breaks happened at the joints: predictable padding, leaky compression, fallback logic written to be forgiving, a single library’s missing bounds check, weak parameters that nobody removed because removing things breaks somebody. TLS 1.3 is best read as the protocol that finally accepted this and started deleting joints, accepting that some old client somewhere would be cut off, because the cost of keeping the joint open had been demonstrated six times in seven years.

The deprecations followed the same logic on a delay. SSL 2.0 prohibited in 2011, SSL 3.0 in 2015 after POODLE, and finally TLS 1.0 and 1.1 formally deprecated together by RFC 8996 in March 2021, two decades after TLS 1.0 shipped. The long tail is always the hard part. A protocol can be declared dead years before the last server stops speaking it, and the version byte that still reads 0x0301 on the wire is a small monument to how long backward compatibility outlives the thing it was compatible with. The next chapter is already being written in the same place, the key-exchange groups, as post-quantum hybrids like X25519MLKEM768 move into the ClientHello to protect today’s traffic against a decryption that has not been invented yet.


Sources & further reading

Further reading