topics: cfssl (posts)

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)
	}
}