loading...

Validating Client Certificate SANs in Go

living_syn profile image Jeremy Mill ・3 min read

Go has one of the best TLS libraries available in any programming language, for it's my language of choice for doing networking tasks. So I was a bit surprised to learn that, by default, when you set tls.RequireAndVerifyClientCert on a tls.Config object, it doesn't verify the SAN/CN field on that client cert. The only thing it will verify is that it is signed by the configured root CA.

Setting go up to perform this validation was not quite as intuitive as I first believed it would be and I hope that I can help you with it if you have the same need as myself.

Starting Point

Lets say you have a starting point of a generic tls config like this:

serverConf := &tls.Config{
    Certificates: []tls.Certificate{cer},
    MinVersion:   tls.VersionTLS12,
    ClientAuth:   tls.RequireAndVerifyClientCert,
    ClientCAs:    rootCAs,
}

Here we've defined a server certificate, a minimum TLS version, the root CA's to use and that we need to verify client certificates. Right now, client certificates would be validated as signed by a CA in the rootCA's CertPool, but nothing else.

Custom Client Cert Validation

The tls.Config object has a callback that looks very promising called VerifyPeerCertificate that takes in a method with this signature:

func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error

The problem is that this doesn't have any of the client connection information passed it it for us to validate the connecting host! Luckily for us, there's another callback called GetConfigForClient. GetConfigForClient is another callback on the tls.Config object which gives us the tls.ClientHelloInfo as an argument. and returns a per-client tls.Config (or nil for no-change) object.

The answer is to use GetConfigForClient to call a function which returns a closure that matches the VerifyPeerCertificate signature but makes the ClientHelloInfo available to it.

Our server tls.Config now looks like:

serverConf := &tls.Config{
    Certificates: []tls.Certificate{cer},
    GetConfigForClient: func(hi *tls.ClientHelloInfo) (*tls.Config, error) {
        serverConf := &tls.Config{
            Certificates: []tls.Certificate{cer},
            MinVersion:            tls.VersionTLS12,
            ClientAuth:            tls.RequireAndVerifyClientCert,
            ClientCAs:             rootCAs,
            VerifyPeerCertificate: getClientValidator(hi),
        }
        return serverConf, nil
    },
}

and our stubbed out getClientValidator function looks like:

func getClientValidator(helloInfo *tls.ClientHelloInfo) func([][]byte, [][]*x509.Certificate) error {
    return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
        return nil
    }
}

At this point there has been essentially no change to the functionality from where we started. Clients still connect, and their certificates are validated, but their SAN's are not.

Validating SANs

To validate the SAN's on the client certificate we need to modify the getClientValidatormethod. In order to avoid writing our own validation methods, we can utilize the same validator that's used by default when we specify tls.RequireAndVerifyClientCert on our config object. All we need to do is add some additional options to its configuration object.

func getClientValidator(helloInfo *tls.ClientHelloInfo) func([][]byte, [][]*x509.Certificate) error {
    return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
        //copied from the default options in src/crypto/tls/handshake_server.go, 680 (go 1.11)
        //but added DNSName
        opts := x509.VerifyOptions{
            Roots:         rootCAs,
            CurrentTime:   time.Now(),
            Intermediates: x509.NewCertPool(),
            KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
            DNSName:       strings.Split(helloInfo.Conn.RemoteAddr().String(), ":")[0],
        }
        _, err := verifiedChains[0][0].Verify(opts)
        return err
    }
}

This now validates the client's certificate against the root CA's we specified at our starting point, the client's certificate key usage, and the DNSName of the client connecting. The strings.Split saves us from including the RemoteAddrport number in the DNSName field, and won't screw anything up if it's an actual DNS name.

There it is, I hope you find this useful! You can find all of the code in a single, small server.go example here

Posted on by:

living_syn profile

Jeremy Mill

@living_syn

Veteran, Security Engineer, prior dev

Discussion

pic
Editor guide
 

I was wondering if we couldn't use tls.VerifyHostname for that check also: golang.org/src/crypto/tls/conn.go?...

Edit: By looking into go code it looks that tls.Verify is broader than tls.VerifyHostname (it actually can call VerifyHostname).
Thanks a lot for such great post!

 

Note: Huge thanks to Filippo Valsorda (github.com/FiloSottile) for his help pointing me in the right direction on how to do this here