This is the first article I wrote for the Go blog (!!) about how TLS cipher suites configuration got so complicated, and how we've made it way easier in Go 1.17.
The Go standard library provides
a robust implementation of Transport Layer Security (TLS),
the most important security protocol on the Internet,
and the fundamental component of HTTPS.
In Go 1.17 we made its configuration easier, more secure,
and more efficient by automating the priority order of cipher suites.
How cipher suites work
Cipher suites date back to TLS’s predecessor Secure Socket Layer (SSL),
which called them “cipher kinds”.
They are the intimidating-looking identifiers like
that spell out the algorithms used to exchange keys,
authenticate certificates, and encrypt records in a TLS connection.
Cipher suites are negotiated during the TLS handshake: the client sends the list of cipher suites it supports in its first message, the Client Hello, and the server picks one from that list, communicating its choice to the client. The client sends the list of supported cipher suites in its own preference order, and the server is free to pick from it however it wants. Most commonly, the server will pick the first mutually supported cipher suite either in client preference order or in server preference order, based on its configuration.
Cipher suites are really only one of many negotiated parameters—supported curves/groups and signature algorithms are additionally negotiated through their own extensions—but are the most complex and famous ones, and the only ones that developers and administrators were trained over the years to have opinions on.
In TLS 1.0–1.2, all these parameters interact in a complex web of inter-dependencies: for example supported certificates depend on supported signature algorithms, supported curves, and supported cipher suites. In TLS 1.3 this was all drastically simplified: cipher suites only specify symmetric encryption algorithms, while supported curves/groups govern the key exchange and supported signature algorithms apply to the certificate.
A complex choice abdicated to developers
Most HTTPS and TLS servers delegate the choice of cipher suites and preference order to the server operator or the applications developer. This is a complex choice that requires up-to-date and specialized knowledge for many reasons.
Some older cipher suites have insecure components, some require extremely careful and complex implementations to be secure, and some are only secure if the client applies certain mitigations or even has certain hardware. Beyond the security of the individual components, different ciphersuites can provide drastically different security properties for the whole connection, as cipher suites without ECDHE or DHE don’t provide forward secrecy—the property that connections can’t be retroactively or passively decrypted with the certificate’s key. Finally, the choice of supported cipher suites impacts compatibility and performance, and making changes without an up-to-date understanding of the ecosystem can lead to breaking connections from legacy clients, increasing the resources consumed by the server, or draining the batteries of mobile clients.
This choice is so arcane and delicate that there are dedicated tools to guide operators, such as the excellent Mozilla SSL Configuration Generator.
How did we get here and why is it like this?
To start, individual cryptographic components used to break much more often. In 2011, when the BEAST attack broke CBC cipher suites in such a way that only clients could mitigate the attack, servers moved to preferring RC4, which was unaffected. In 2013, when it became clear that RC4 was broken, servers went back to CBC. When Lucky Thirteen made it clear it was extremely hard to implement CBC cipher suites due to their backwards MAC-then-encrypt design… Well, there wasn’t anything else on the table so implementations had to carefully jump through hoops to implement CBC and kept failing at that daunting task for years. Configurable cipher suites and cryptographic agility used to provide some reassurance that when a component broke it could be replaced on the fly.
Modern cryptography is significantly different. Protocols can still break from time to time, but it’s rarely an individual abstracted component that fails. None of the AEAD-based ciphersuites introduced starting with TLS 1.2 in 2008 have been broken. These days cryptographic agility is a liability: it introduces complexity that can lead to weaknesses or downgrades, and it is only necessary for performance and compliance reasons.
Patching used to be different, too. Today we acknowledge that promptly applying software patches for disclosed vulnerabilities is the cornerstone of secure software deployments, but ten years ago it was not standard practice. Changing configuration was seen as a much more rapid option to respond to vulnerable cipher suites, so the operator, through configuration, was put fully in charge of them. We now have the opposite problem: there are fully patched and updated servers that still behave weirdly, suboptimally, or insecurely, because their configurations haven’t been touched in years.
Finally, it was understood that servers tended to update more slowly than clients, and therefore were less reliable judges of the best choice of cipher suite. However, it’s servers who have the last word on cipher suite selection, so the default became to make servers defer to the client preference order, instead of having strong opinions. This is still partially true: browsers managed to make automatic updates happen and are much more up-to-date than the average server. On the other hand, a number of legacy devices are now out of support and are stuck on old TLS client configurations, which often makes an up-to-date server better equipped to choose than some of its clients.
Regardless of how we got here, it’s a failure of cryptography engineering to require application developers and server operators to become experts in the nuances of cipher suite selection, and to stay up-to-date on the latest developments to keep their configs up-to-date. If they are deploying our security patches, that should be enough.
The Mozilla SSL Configuration Generator is great, and it should not exist.
Is this getting any better?
There is good news and bad news for how things are trending in the past few years. The bad news is that ordering is getting even more nuanced, because there are sets of cipher suites that have equivalent security properties. The best choice within such a set depends on the available hardware and is hard to express in a config file. In other systems, what started as a simple list of cipher suites now depends on more complex syntax or additional flags like SSL_OP_PRIORITIZE_CHACHA.
The good news is that TLS 1.3 drastically simplified cipher suites,
and it uses a disjoint set from TLS 1.0–1.2.
All TLS 1.3 cipher suites are secure, so application developers and server
operators shouldn’t have to worry about them at all.
Indeed, some TLS libraries like BoringSSL and Go’s
allow configuring them at all.
Go’s crypto/tls and cipher suites
Go does allow configuring cipher suites in TLS 1.0–1.2.
Applications have always been able to set the enabled cipher suites and
preference order with
Servers prioritize the client’s preference order by default,
Config.PreferServerCipherSuites is set.
When we implemented TLS 1.3 in Go 1.12, we didn’t make TLS 1.3 cipher suites configurable,
because they are a disjoint set from the TLS 1.0–1.2 ones and most importantly
they are all secure,
so there is no need to delegate a choice to the application.
Config.PreferServerCipherSuites still controls which side’s preference order is used,
and the local side’s preferences depend on the available hardware.
In Go 1.14 we exposed supported cipher suites, but explicitly chose to return them in a neutral order (sorted by their ID), so that we wouldn’t end up tied to representing our priority logic in terms of a static sort order.
In Go 1.16, we started actively preferring ChaCha20Poly1305 cipher suites over AES-GCM on the server when we detect that either the client or the server lacks hardware support for AES-GCM. This is because AES-GCM is hard to implement efficiently and securely without dedicated hardware support (such as the AES-NI and CLMUL instruction sets).
Go 1.17, recently released, takes over cipher suite preference ordering for all Go users.
Config.CipherSuites still controls which TLS 1.0–1.2 cipher suites are enabled,
it is not used for ordering, and
Config.PreferServerCipherSuites is now ignored.
crypto/tls makes all ordering decisions,
based on the available cipher suites, the local hardware,
and the inferred remote hardware capabilities.
The current TLS 1.0–1.2 ordering logic follows the following rules:
ECDHE is preferred over the static RSA key exchange.
The most important property of a cipher suite is enabling forward secrecy. We don’t implement “classic” finite-field Diffie-Hellman, because it’s complex, slower, weaker, and subtly broken in TLS 1.0–1.2, so that means prioritizing the Elliptic Curve Diffie-Hellman key exchange over the legacy static RSA key exchange. (The latter simply encrypts the connection’s secret using the certificate’s public key, making it possible to decrypt if the certificate is compromised in the future.)
AEAD modes are preferred over CBC for encryption.
Even if we do implement partial countermeasures for Lucky13 (my first contribution to the Go standard library, back in 2015!), the CBC suites are a nightmare to get right, so all other more important things being equal, we pick AES-GCM and ChaCha20Poly1305 instead.
3DES, CBC-SHA256, and RC4 are only used if nothing else is available, in that preference order.
3DES has 64-bit blocks, which makes it fundamentally vulnerable to birthday attacks given enough traffic. 3DES is listed under
InsecureCipherSuites, but it’s enabled by default for compatibility. (An additional benefit of controlling preference orders is that we can afford to keep less secure cipher suites enabled by default without worrying about applications or clients selecting them except as a last resort. This is safe because there are no downgrade attacks that rely on the availability of a weaker cipher suite to attack peers that support better alternatives.)
The CBC cipher suites are vulnerable to Lucky13-style side channel attacks and we only partially implement the complex countermeasures discussed above for the SHA-1 hash, not for SHA-256. CBC-SHA1 suites have compatibility value, justifying the extra complexity, while the CBC-SHA256 ones don’t, so they are disabled by default.
RC4 has practically exploitable biases that can lead to plaintext recovery without side channels. It doesn’t get any worse than this, so RC4 is disabled by default.
ChaCha20Poly1305 is preferred over AES-GCM for encryption, unless both sides have hardware support for the latter.
As we discussed above, AES-GCM is hard to implement efficiently and securely without hardware support. If we detect that there isn’t local hardware support or (on the server) that the client has not prioritized AES-GCM, we pick ChaCha20Poly1305 instead.
AES-128 is preferred over AES-256 for encryption.
AES-256 has a larger key than AES-128, which is usually good, but it also performs more rounds of the core encryption function, making it slower. (The extra rounds in AES-256 are independent of the key size change; they are an attempt to provide a wider margin against cryptanalysis.) The larger key is only useful in multi-user and post-quantum settings, which are not relevant to TLS, which generates sufficiently random IVs and has no post-quantum key exchange support. Since the larger key doesn’t have any benefit, we prefer AES-128 for its speed.
TLS 1.3’s ordering logic needs only the last two rules, because TLS 1.3 eliminated the problematic algorithms the first three rules are guarding against.
What if a cipher suite turns out to be broken? Just like any other vulnerability, it will be fixed in a security release for all supported Go versions. All applications need to be prepared to apply security fixes to operate securely. Historically, broken cipher suites are increasingly rare.
Why leave enabled TLS 1.0–1.2 cipher suites configurable? There is a meaningful tradeoff between baseline security and legacy compatibility to make in choosing which cipher suites to enable, and that’s a choice we can’t make ourselves without either cutting out an unacceptable slice of the ecosystem, or reducing the security guarantees of modern users.
Why not make TLS 1.3 cipher suites configurable? Conversely, there is no tradeoff to make with TLS 1.3, as all its cipher suites provide strong security. This lets us leave them all enabled and pick the fastest based on the specifics of the connection without requiring the developer’s involvement.
Starting in Go 1.17,
crypto/tls is taking over the order in which available
cipher suites are selected.
With a regularly updated Go version, this is safer than letting potentially
outdated clients pick the order,
lets us optimize performance, and it lifts significant complexity from Go developers.
This is consistent with our general philosophy of making cryptographic decisions whenever we can, instead of delegating them to developers, and with our cryptography principles. Hopefully other TLS libraries will adopt similar changes, making delicate cipher suite configuration a thing of the past.
Just for the Cryptography Dispatches edition, here's a picture of a bit of the New York skyline from my old apartment.