Mutual TLS authentication in Go

Go has an amazing TLS library. While it can be used in conjunction with the ‘net/http’ package to provide TLS protected transport for HTTP traffic (and now http2), people are starting to use it for authentication as well by way of mutual TLS.

Below we are going to explore how to safely implement mutual TLS authentication and some common pitfalls that can occur.

If you would just like to look at code, here is a straightforward example: https://github.com/levigross/go-mutual-tls

The code for the client and the server is straightforward.

Generating the certificate

I have modified the generate_cert.go program to generate TLS client certificates: The new code can be found here: generate_client_cert.go

The easiest way to generate a client+server TLS certificate is by running the following command:

go run generate_client_cert.go -email-address=a@a.com -ca -ecdsa-curve=P521

This command will generate two files:

  • key.pem
  • cert.pem

Which can be used by the TLS server and client to authenticate.

Configuring the TLS server

It is really important to understand and implement this step properly.

Let’s start off with a list of requirements:

  1. We need to ensure that our TLS server only uses the appropriate CA, and no other CAs.
  2. We need to set the ClientAuth portion within tls.Config to tls.RequireAndVerifyClientCert

For fun we can ensure that the server requires:

  1. TLS version is 1.2
  2. The cipher provides forward secrecy
tlsConfig := &tls.Config{
	// Reject any TLS certificate that cannot be validated
	ClientAuth: tls.RequireAndVerifyClientCert,
	// Ensure that we only use our "CA" to validate certificates
	ClientCAs: clientCertPool,
	// PFS because we can but this will reject client with RSA certificates
	CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384},
	// Force it server side
	PreferServerCipherSuites: true,
	// TLS 1.2 because we can
	MinVersion: tls.VersionTLS12,
}

tlsConfig.BuildNameToCertificate()

The first two items are the most imporant! Without them there is a potential authentication bypass vulnerability.

Setting up the server

Now that we have our custom &tls.Config{} struct, we can create the server:

http.HandleFunc("/", HelloUser)

httpServer := &http.Server{
	Addr:      ":8080",
	TLSConfig: tlsConfig,
}

log.Println(httpServer.ListenAndServeTLS("../cert.pem", "../key.pem"))

Now that we have our server, we need to build a view that returns a greeting to our user:

// HelloUser is a view that greets a user
func HelloUser(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(w, "Hello %v! \n", req.TLS.PeerCertificates[0].EmailAddresses[0])
}

It is important to remember that while the TLS server does authenticate and validate the user’s certificate, we need to know who this user is.

Building the client

To build our client, we need to:

  1. Lock ourselves into our custom CA
  2. Ensure that our client uses our TLS client certificate
// Load our TLS key pair to use for authentication
cert, err := tls.LoadX509KeyPair("../cert.pem", "../key.pem")
if err != nil {
	log.Fatalln("Unable to load cert", err)
}

// Load our CA certificate
clientCACert, err := ioutil.ReadFile("../cert.pem")
if err != nil {
	log.Fatal("Unable to open cert", err)
}

clientCertPool := x509.NewCertPool()
clientCertPool.AppendCertsFromPEM(clientCACert)

tlsConfig := &tls.Config{
	Certificates: []tls.Certificate{cert},
	RootCAs:      clientCertPool,
}

tlsConfig.BuildNameToCertificate()

Once we have a valid tls.Config, we can build a custom http.Client that handles the request. I will use GRequests to handle that:

ro := &grequests.RequestOptions{
	HTTPClient: &http.Client{
		Transport: &http.Transport{TLSClientConfig: tlsConfig},
	},
}
resp, err := grequests.Get("https://localhost:8080", ro)
if err != nil {
	log.Println("Unable to speak to our server", err)
}

// Lets print the message
log.Println(resp.String())

Final Thoughts

As you see, it is easy to implement mutual TLS authentication in Go. It is important to remember

  • Authentication is not authorization and you need to go through the TLS peer certificates to find your user.
  • You must set a custom CA. If you don’t, Go will use the CA pool that comes with your server.
// An empty list of certificateAuthorities signals to
// the client that it may send any certificate in response
// to our request. When we know the CAs we trust, then
// we can send them down, so that the client can choose
// an appropriate certificate to give to us.
if config.ClientCAs != nil {
	certReq.certificateAuthorities = config.ClientCAs.Subjects()
}

SRC: https://github.com/golang/go/blob/release-branch.go1.5/src/crypto/tls/handshake_server.go#L395-L402