SSLv2 redux: patching Go crypto/tls to work with IE6

tl;dr: Go crypto/tls servers can't understand a sad SSLv2-flavored compatibility trick IE6 and JDK 5/6 do, I updated a patch, don't use it.

While doing large scale TLS measurement with a Go crypto/tls server for CloudFlare, I started seeing this error score pretty high in my charts:

tls: unsupported SSLv2 handshake received

Since that measurement focuses on legacy clients, I was not happy. Googling led pretty quickly to issue #3930, which offers plenty of background and discussion.

To make it short, we are dealing with 14 years old software (IE 6) speaking a 20 years old protocol (SSLv2), to be compatible with servers that don't support the 19 years old SSLv3. One of those days.

The long story: SSLv2 leaves no space for forward-compatibility, so if a client wants a chance to work with a SSLv2-only server, the first packet it sends MUST be a valid SSLv2 ClientHello. Even if it is willing to talk SSLv3 or TLS 1.0 after that.

And that's exactly what IE6 does. SSL Labs calls this client trait "SSL 2 handshake compatibility". It's not only IE6, JDK 5/6 calls this "SSLv2Hello" and enable it by default, for example.

RFC 6101 (the historic SSLv3 spec) Appendix E documents this compatibility measure and details how to construct such a SSLv2 ClientHello, in which clients will then specify a max version of 3.0 or higher. IE6 for example will happily advertise TLS 1.0 (version number 3.1, because everything is terrible) over SSLv2 handshakes.

(The spec even includes an encrypted block padding-based rollback canary to let v2+v3 servers know securely that the client can speak v3, not just v2, even if a MitM tried to downgrade. Not that we care, since no one actually implements v2 anymore.)

Anyway, Go doesn't support SSLv2, and neither does any other server, since it's ridiculously broken and there are no clients incapable of at least SSLv3. However, if it could parse SSLv2 ClientHello packets and detect SSLv3 or TLS 1.0 support inside them, the rest of the code would work just fine. AGL isn't enthusiastic about adding such support, so not to make crypto/tls look like a production ready stack—something he responsibly says about anything he wrote—and I don't blame him.

Still, there's a patch by user erwan.legrand linked to the ticket, ported to 1.4 by user @elimisteve. Instead of erroring out it parses the SSLv2 ClientHello, converts it to a modern one, and keeps the original data around for the Finished hash. When I applied it, after clearing a couple merge conflicts, it blew up my CPUs with ridiculous load, but it sort of worked (even if only with TLS 1.0 turned on).

I ported it to 1.5.2, fixed what profiling showed to be a spinning loop (maybe introduced by one of the rebases), added proper error handling, relaxed it to work with SSLv3, optimized it and polished it a bit.

The result below (in git am format) ran smoothly at pretty high loads, but FOR THE LOVE OF ALL THAT'S SACRED DON'T USE IT IN PRODUCTION.


Update 2016-02-06: I published a new version fixing an easy to trigger crash (and a silly upload mistake causing a syntax error).

Windows XP with IE 6, in SSLv2+SSLv3 connected to my Go instance

This is how legacy persists on the Internet: a protocol design overlooks giving clients a way to negotiate down from future versions, clients keep talking the old protocol first to be compatible and servers 20 years later have to implement part of the original protocol they don't support anymore just to work with the clients operating in compatibility mode.

For more occasional protocol sadness follow me on Twitter.