TLS Mutual Auth in GoLang

Golang takes no prisoners.

Don’t use a variable? Your code isn’t going to run!

Import something you don’t use? Nope, no chance.

Realistically I should have expected their attitude to security was just as tough. So implementing TLS mutual auth was never going to be simple. Simply put go does all the checks it quite rightfully should do, but those checks make it a nightmare when you just want to test some TLS work locally.

Strict TLS

There are a few points that trip you up locally, especially since go 1.3 where things got even more strict. When you hit any https endpoint online the certificate is usually backed to a domain name, however when working locally often you use localhost and 127.0.0.1 interchangeably, when doing mutual auth you can’t help but bump into this issue. Since TLS is designed primarily around domains you need to add IP SAN’s. Secondly x.509 certificates have an extension to define the usage of keys in terms of server or client auth, which of course go checks.

Older versions of go used to have options to disable certain checks, but those have long since gone. Its secure, or nothing!

All this boils down to it being a real nightmare to generate the right keys so you can get up and running. However now that I’ve been through that pain, the solution is quite simple.

First up you need to generate your keys with IP SAN’s, to do this get hold of this file which does a great job of making your life easier.

Generate your certificates

To generate the server cert (assuming localhost) run the program with the options:

  • Common Name: localhost
  • DNS or IP Address 1: 127.0.0.1
  • Number of days: 365 (or whatever you like)

To create the client cert there’s one thing missing. You need the extension for client certs, find the line that reads:

ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},

Change this to:

ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},

If you are being lazy you can also just permanently change it to:

ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},

Copy your previously generated server certs somewhere and run the program again to generate client certs.

This will give you 4 files, a cert and key for the server, the same for the client. The code to use them in mutual auth is pretty simple at this point.

I created 2 projects called secure-server and secure-client, the code looks like this:

Secure Server

Secure Client

Run it

First run the server, which will load its own cert and key, along with adding a client cert for the client application (you can add more if you need to) and then run the client, which loads its own cert and key, along with creating a new root ca pool with just that one server.

The great thing about this setup is once you have gone through the pain of creating your certificates and the code is somewhat trivial, after you have it all figured out.

Hopefully this should save people a bit of time in getting this working.

10 thoughts on “TLS Mutual Auth in GoLang

  1. I found this very useful. Thanks, Chris.

    Anyone know how I would put a local cert in Chrome for testing with a browser locally before I turn it over to devops guy to put in a real authority cert?

    1. Depends if you want client auth or just server auth, but check this page for how to accept or import a trusted certificate into chrome:

    2. http://stackoverflow.com/questions/7580508/getting-chrome-to-accept-self-signed-localhost-certificate
    3. This page

    4. http://www.binarytides.com/client-side-ssl-certificates-firefox-chrome/
    5. seems to suggest importing the client certificate as well will do the trick.

      The client side code above should be proof you have it working, just toggle the options on the server config in terms of tls.RequireAndVerifyClientCert, if it works with that enabled you have mutual auth.

  2. Very interesting and helpful article article. I only wonder if there is a way to apply the client authentication on a particular route instead of the whole server.

  3. Very nice, thanks for posting this.
    One thing to note: You’re putting your self-signed client certificates in a x509.CertPool and passing it as the ClientCAs field of tls.Config. This works, but there are a few side effects that people should know about.

    Per section 7.4.4 of https://www.ietf.org/rfc/rfc5246.txt, the distinguished names of all of the client CAs will be sent to the client in the Certificate Request message. This message is sent in the clear, exposing the distinguished names. Not a big deal, but it should be known.
    Also, this technique does not scale well when you have a lot of client certificates, because the distinguished names of all of them will be sent in the Certificate Request message. This means that the message will get larger the number of certificates in your CertPool. I hit a limit at about 440 client certificates when the message got too large for the TLS library to handle (this will vary with the length of the distinguished names in the certificates, of course) .
    I’m currently testing a solution where I don’t set the ClientCAs field of tls.Config, and I set ClientAuth to tls.RequireAnyClientCert (instead of tls.RequireAndCheckClientCert). This means that the TLS library will accept any client certificate, and I need to check the values in http.Request.TLS.PeerCertificates to make sure the certificate used is one that I trust.

    I’m not fully resolved on this issue, so anyone with other ideas please chime in.

      1. Karsten,
        tls.RequireAnyClientCert has worked quite well for me for the above use-case. However, since I need one of my routes to not require a certificate (it’s a health status route for AWS), I have moved to using tls.RequestClientCert.

        If Golang receives a certificate in this manner, it authenticates it against the client before sending it to my handler, so I don’t have to authenticate the certificate myself. If it doesn’t receive a client certificate, it sends the request to the handler, but request.TLS.PeerCertificates is empty. Thus I can use request.TLS.PeerCertificates to determine if the received certificate is one that I trust (for the routes that require it), and I can still go forward for the routes that don’t require a certificate and don’t receive one.

    1. After some more thinking and testing I came to the conclusion that the right way for me is to use tls.RequireAndVerifyClientCert in the server. However, instead of loading all the various client certificates into the x509.CertPool I only load the single root ca certificate of their issuing certificate authority.

      This has the nice benefit, that req.TLS.VerifiedChains contains all the complete and verified certificate chains of the client certificates including their intermediate ca certificates.

      In the self-signed example above req.TLS.VerifiedChains only contains the client certificate i.e. a single entry certificate chain.

      YMMV.

  4. Line 39 of the secure client should be log.Fatal() and not fmt.Printlin(). Otherwise the code will crash in line 41 if there was an error. Don’t ask why I know this. 😉

Leave a Reply