Mutual TLS authentication using CFSSL and Go
Creating a Certificate Authority
cfssl print-defaults csr > csr.json
This creates a set of default values, e.g.:
{
"CN": "example.net",
"hosts": [
"example.net",
"www.example.net"
],
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "US",
"ST": "CA",
"L": "San Francisco"
}
]
}
cfssl genkey -initca csr.json | cfssljson -bare ca
We now have 3 files, a private key, a certificate request and a CA certificate.
ca-key.pem
ca.csr
ca.pem
From the private key, CA certificate and signing request default config generated previously we can create certificates:
mkdir -p client server \
&& cfssl gencert -ca ca.pem -ca-key ca-key.pem -hostname=localhost csr.json | cfssljson -bare \
mv cert.pem cert-key.pem client/ \
&& cfssl gencert -ca ca.pem -ca-key ca-key.pem -hostname=localhost csr.json | cfssljson -bare \
&& mv cert.pem cert-key.pem server/
$ find .
.
./ca-key.pem
./cert.csr
./server
./server/cert.pem
./server/cert-key.pem
./csr.json
./ca.pem
./ca.csr
./client
./client/cert.pem
./client/cert-key.pem
Writing some Go(lang)
The Go standard library supports MTLS (as you might expect). Getting a server and client implementation up and running is therefore quite straight forward.
The Server
./server/server.go
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
// read the CA Certificate
ca, err := ioutil.ReadFile("../ca.pem")
if err != nil {
log.Fatalln(err)
}
// add to a Cert Pool
// caPool defines the set of root certificate authorities
// that servers use when verifying client certificates.
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(ca)
// ServeMux is an HTTP request multiplexer.
handler := http.NewServeMux()
// HandleFunc registers the handler function for the given pattern.
handler.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
log.Println("request received")
if _, err := fmt.Fprint(writer, "request received"); err != nil {
log.Fatalln(err)
}
})
server := http.Server{
Addr: ":8080",
Handler: handler,
TLSConfig: &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caPool,
},
}
if err := server.ListenAndServeTLS("./cert.pem", "./cert-key.pem"); err != nil {
log.Fatalf("error listening to port: %v", err)
}
}
The Client
./server/client.go
package main
import (
"crypto/tls"
"crypto/x509"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"time"
)
func main() {
// read the CA Certificate
ca, err := ioutil.ReadFile("../ca.pem")
if err != nil {
log.Fatalln(err)
}
// add to a Cert Pool
// caPool defines the set of root certificate authorities
// that clients use when verifying server certificates.
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(ca)
// read the Client Certificate key pair
certificate, err := tls.LoadX509KeyPair("cert.pem", "cert-key.pem")
if err != nil {
log.Fatalln(err)
}
client := http.Client{
Timeout: time.Second * 10,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caPool,
Certificates: []tls.Certificate{
certificate,
},
},
},
}
res, err := client.Get("https://localhost:8080")
if err != nil {
log.Fatalln(err)
}
if _, err := io.Copy(os.Stdout, res.Body); err != nil {
log.Fatalln(err)
}
}