February 25, 2019·5 min read·engineeringgotlsnetworkingmodules

Go 1.12 makes TLS 1.3 an explicit deployment choice

Go 1.12 adds TLS 1.3 behind GODEBUG. Its handshake semantics and early-data limitations matter as much as enabling the protocol.

Turning on a protocol can change when an application learns that authentication failed. Go 1.12's TLS 1.3 implementation makes this concrete. A client can finish its handshake call and discover a server's rejection of its certificate on the first read. Code that treats a successful handshake call as the last possible authentication failure needs to account for the protocol's message ordering.

Andrew Bonventre's February 25 announcement introduces opt-in TLS 1.3 alongside module improvements and platform work. The release notes explain the operational details. There are no language specification changes in this release, but a program can still observe different behavior in its network connections and runtime.

I like Go's separation between a small language and substantial standard libraries. It keeps much of the code needed to run a service within a common set of documented interfaces. This release also demonstrates why the behavior behind those interfaces deserves close reading when upgrading a toolchain.

Enabling negotiation

TLS 1.3 support is in crypto/tls, and Go identifies its protocol specification as RFC 8446. The release does not enable it by default. A process opts in by including tls13=1 in its GODEBUG environment variable. The notes also require that the application's TLS configuration not set an explicit MaxVersion when following the documented path to negotiate TLS 1.3.

For an existing executable that uses the Go 1.12 implementation, the environment setting can look like this in a POSIX shell.

GODEBUG=tls13=1 ./server

The setting permits the new protocol negotiation. It does not establish what every remote peer supports. A deployment therefore has two relevant pieces of configuration, the process environment and the TLS configuration in the application. Reading only the environment can miss an explicit version ceiling in code.

Go 1.12 does not allow applications to configure the TLS 1.3 cipher suites. The notes describe all supported suites as safe and explain that, when PreferServerCipherSuites is set, the preference order depends on available hardware. An existing cipher-suite policy should consequently not be assumed to control this protocol's suites in the same way as earlier TLS versions.

The implementation provides equivalents for the TLS 1.2 features except renegotiation and TLSUnique in ConnectionState. Those exceptions matter to programs that use TLS for more than ordinary HTTP transport. An application depending on the old channel-binding field needs to recognize that enabling a newer protocol does not preserve that particular facility.

A successful handshake call has a narrower meaning

The release notes explain that a TLS 1.3 client sends the last handshake message. A server error caused by that message therefore reaches the client on its first Read, rather than its Handshake call. Rejection of a client certificate is the example given by the Go team.

This is a change in the location where an error becomes observable. An application still has to handle read failures, but its logs or connection accounting may currently assign a special meaning to reaching the end of the handshake. With TLS 1.3, that point does not prove that a server has accepted everything the client sent at the end of the exchange.

Session tickets also arrive after the handshake. The client receives them on its first read. Code that opens a connection, completes a handshake and immediately stops reading should not assume that it has processed a session ticket merely because the handshake returned. Both behaviors follow from post-handshake traffic described in the release notes.

I prefer network APIs whose failure behavior is explicit, and these details help preserve that property at the application level. A useful connection state model needs to describe what the peer has actually confirmed. A local function returning successfully cannot establish more than the protocol exchange has established.

Early data constrains a mixed server pool

Go 1.12 supports neither client nor server early data, also called 0-RTT mode. More specifically, its server cannot skip unexpected early data sent by a client. That second limitation makes the surrounding deployment relevant even when the Go service itself never enables early data.

A client retains information about servers that support 0-RTT. The release notes therefore say that a Go 1.12 server cannot share a load-balancing pool with servers that support it. A client returning with stored state could reach a different server implementation from the one that issued its ticket. Configuration at the load balancer cannot be understood solely by inspecting the Go member of the pool.

The same restriction affects replacing an existing server. When moving a domain from a server that supported early data to Go 1.12, the documented procedure is to disable 0-RTT for at least the lifetime of previously issued session tickets before the switch. The relevant interval comes from the lifetime of that state, rather than an arbitrary maintenance window.

This is a specific migration constraint. It does not mean that every TLS 1.3 deployment needs early data, and the notes do not promise that enabling TLS 1.3 in Go provides it. Keeping protocol version support distinct from individual protocol features avoids an incorrect assumption about interoperability.

The toolchain also changes the operating environment

Module operations receive practical improvements in Go 1.12. Commands that download and extract modules can now run concurrently safely, provided the module cache lives on a filesystem with file-locking support. The cache location is GOPATH/pkg/mod. Parallel command execution and the properties of the filesystem now belong in the same explanation of cache correctness.

The go directive in go.mod indicates the language version used by the module's files. Go 1.12 writes its version when no version is present. The release notes identify a compatibility problem with Go 1.11 through Go 1.11.3 and give go mod edit -go=1.11 as the adjustment when those versions must remain usable. Go 1.11.4 and later handle the directive correctly.

On Linux, the runtime uses MADV_FREE to release unused memory. The notes warn that this can raise reported resident memory even though the kernel can reclaim the unused data when necessary. A change in the memory graph after a toolchain update therefore requires an explanation of the operating system accounting involved. It is not sufficient evidence by itself that the application retains more useful heap data.

These changes affect different parts of a service's lifecycle. The TLS switch controls a negotiated connection protocol, while module directives affect builds and memory release affects runtime reporting. Go 1.12 preserves the language specification while changing these documented behaviors, including exactly where a TLS client receives a server's final handshake error.