Validating Client Certificate SANs in Go
Tags: go, tls, san, hostname
Published: 2019-01-24 17:17:05

---
title: Validating Client Certificate SANs in Go
published: true
description: How to validate client certificate SANs in Go using callbacks on tls.Config
tags: go, tls, san, hostname
---

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](https://golang.org/pkg/crypto/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:

```go
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: 

```go
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](https://golang.org/pkg/crypto/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:

```go
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:

```go
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 `getClientValidator`method. 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.

```go
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 `RemoteAddr`port 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](https://gist.github.com/LivingInSyn/b2634518f654c0e877224bb03e2ed3bb)