If you are developing services which need to authenticate each other by mTLS you need a triplet of x509 certificate files: A root CA, client certificates and servers certificates and corresponding key files. One way to generate these files locally is to use openssl, but this is rather tedious as you must configure a lot to use this tool. A more elegant solution is to use the CloudFlare PKI/TLS toolkit.

In this post I am going to explain how to create all files you need to test a mTLS connection.

Setup

To install CFSSL you can either use brew or install it directly with the go tool chain. For the later you need a working go installation.

Use brew:

brew install cfssl

For installing CFSSL with go tool chain see the offical documentation.

Root CA

First you need a Root CA (Certificate Authority) from which all other certificates are derived from.
CFSSL supports configuring variables for certificates in a JSON format. So first we create a JSON file with variables for our CA:

Filename: configs/ca.json

{
  "CN": "www.radile.net",
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "C":  "DE",
      "L":  "Cologne",
      "O":  "Martin Radile",
      "OU": "cfssldemo",
      "ST": "NRW"
    }
  ]
}

To create the CA files run the following command:

mkdir -p certs
cfssl gencert -initca \
    configs/ca.json | cfssljson -bare certs/ca

This will create a new CA with the variables configured in configs/ca.json. To show the contents of the CA certificate run:

openssl x509 -in certs/ca.pem -text

The output will be somewhat like this:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            01:02:c7:ec:cc:d9:20:af:c5:f9:7c:34:79:eb:f6:6c:74:30:12:ca
    Signature Algorithm: sha512WithRSAEncryption
        Issuer: C=DE, ST=NRW, L=Cologne, O=Martin Radile, OU=cfssldemo, CN=www.radile.net
        Validity
            Not Before: Jan 31 20:42:00 2022 GMT
            Not After : Jan 30 20:42:00 2027 GMT
        Subject: C=DE, ST=NRW, L=Cologne, O=Martin Radile, OU=cfssldemo, CN=www.radile.net
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (4096 bit)
...

Profiles Configuration

Now that we have a new CA, we can create the certificates for our client and server applications. We create again a new configuration file to define profiles for certificate generation. These profiles are later referenced when creating the client and server files.

Filename: configs/ca-config.json

{
  "signing": {
    "profiles": {
      "client": {
        "usages": ["client auth"],
        "expiry": "8760h"
      },
      "server": {
        "usages": ["server auth"],
        "expiry": "8760h"
      }
    }
  }
}

The configuration contains two profiles named client and server. You can choose any name you like. The client profile tells CFSSL to create certificates which can be used for authentication as a client. The server profile lets CFSS generate certificates for a server application. Both profiles are configured to create certificates with of lifespan of one year.

We are now ready to create our server and client certificates.

Client Certificates

We need again a configuration file with parameters for our certificates:

Filename: configs/client.json

{
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "C":  "DE",
      "L":  "Cologne",
      "O":  "Martin Radile",
      "OU": "client cfssl demo",
      "ST": "NRW"
    }
  ]
}

To generate the client certificate run the following command:

cfssl gencert \
    -ca=certs/ca.pem \
    -ca-key=certs/ca-key.pem \
    -config=configs/ca-config.json \
    -profile=client \
    configs/client.json | cfssljson -bare certs/client

We reference the CA files and our previously created client profile. The last parameter for cfssljson certs/client is the output directory and the filename prefix for the certificates.

Server Certificates

The configuration for the server certificates is looking nearly identical. The only difference is the hosts array. Here you can add hosts for which the generated certificate will be valid.

If you enable strict host checking in your client application, and you do not specify the correct hosts here the connection will fail.

Filename: configs/server.json

{
  "hosts": [
    "server.cfssldemo.radile.net",
    "127.0.0.1",
    "localhost"
  ],
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "C":  "DE",
      "L":  "Cologne",
      "O":  "Martin Radile",
      "OU": "server cfssl demo",
      "ST": "NRW"
    }
  ]
}

To generate the server certificates run:

cfssl gencert \
    -ca=certs/ca.pem \
    -ca-key=certs/ca-key.pem \
    -config=configs/ca-config.json \
    -profile=server \
    configs/server.json | cfssljson -bare certs/server

The host names will be included in the SAN (Subject Alternative Name) field in the certificate:

openssl x509 -in certs/server.pem -text 
...
    X509v3 extensions:
        X509v3 Subject Alternative Name: 
            DNS:server.cfssldemo.radile.net, DNS:localhost, IP Address:127.0.0.1
...

Files overview

If you followed all steps you should have the following file structure:

├── certs
│   ├── ca-key.pem
│   ├── ca.csr
│   ├── ca.pem
│   ├── client-key.pem
│   ├── client.csr
│   ├── client.pem
│   ├── server-key.pem
│   ├── server.csr
│   └── server.pem
└── configs
    ├── ca-config.json
    ├── ca.json
    ├── client.json
    └── server.json

Distribute these files as follows to your client and server applications:

  • client
    • certs/ca.pem
    • certs/client.pem
    • certs/client-key.pem
  • server
    • certs/ca.pem
    • certs/server.pem
    • certs/server-key.pem

Full Code

You can find the full code with a Makefile here github.com/mradile/cfssl-mtls-demo.