What is mTLS

It’s called mutual TLS or TLS mutual authentication. I read a well-organized article 1, so I’m not confident I can express it accurately, but I’ll take notes in my own words.

TLS is a protocol for encryption used when performing some kind of communication over a network. It’s used for web browsing, email, Voice over IP, etc. Especially in web browsing, it’s familiar as a lock icon displayed on the left side of the address bar.

TLS consists of PKI (Public Key Infrastructure) and X.509 certificates. X.509 is a standard certificate format adopted in TLS/SSL, which is the foundation of https. It’s also used offline for purposes such as digital signatures. X.509 consists of a public key and several identities (hostname, organization information, etc.), and is signed by itself or by a certificate authority.

By default, TLS is only used for clients to verify the server’s identity, so the mechanism for servers to verify clients needed to be implemented on the application side. Therefore, for business use with even higher security requirements than consumer web services, mTLS came to be used as a mechanism for mutual authentication between servers and clients.

Execution Example

Just to understand the operation, I’ll try it using curl (client) and Node.js (server).

First, create CAs for both client and server. -new -x509 corresponds to creating an X.509 certificate request for a self-signed root CA. Also, -nodes (No DES) corresponds to not setting a password on the private key.

Checking the contents of ca.crt, you can see that Subject and Issuer have the same value example-ca. In other words, this CA is self-signed. Since CA:True, it can sign other certificates.

# Create Certificate Authority
# Outputs are ca.key and ca.crt
# Both are in PEM format (base64-encoded private key and X.509 certificate)
$ openssl req -new -x509 -nodes -days 365 -subj '/CN=example-ca' -keyout ca.key -out ca.crt

# Check the certificate
$ openssl x509 -in ca.crt -text -noout
Issuer: CN=example-ca
Subject: CN=example-ca
CA:TRUE

Next, create the server’s private key. Since the certificate corresponding to this private key needs to be signed by the CA, create a Certificate Signing Request (CSR).

# Create private key for server
$ openssl genrsa -out server.key

# Create CSR
$ openssl req -new -key server.key -subj '/CN=localhost' -out server.csr

Based on the CSR, create a server certificate signed by the CA.

# Create server certificate server.crt
$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 365 -out server.crt

# Check its contents
$ openssl x509 -in server.crt -text -noout
Issuer: CN=example-ca
Subject: CN=localhost

Do the same procedure for the client.

# Similarly create certificate for client
$ openssl genrsa -out client.key
$ openssl req -new -key client.key -subj '/CN=localhost' -out client.csr
$ openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 365 -out client.crt
$ openssl x509 -in client.crt -text -noout

# List of CA, Server, and Client certificates
$ ls
ca.crt ca.key ca.srl client.crt client.csr client.key server.crt server.csr server.key

Now that the certificates are ready, let’s start a web server.

// index.js
const https = require('https');
const fs = require('fs');

const hostname = 'localhost';
const port = 3000;

const options = {
  ca: fs.readFileSync('ca.crt'), // CA certificate
  cert: fs.readFileSync('server.crt'), // Server certificate
  key: fs.readFileSync('server.key'), // Server private key
  rejectUnauthorized: true, // Reject if client authentication fails
  requestCert: true, // Perform client authentication
};

const server = https.createServer(options, (req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Try making a request with curl. When you specify the client’s private key and certificate, and the CA certificate, mutual authentication is performed via mTLS, and the request succeeds normally.

$ node index.js
Server running at http://localhost:3000/

$ curl --cacert ./ca.crt --key ./client.key --cert ./client.crt https://localhost:3000
Hello World

# Without specifying CA certificate, client cannot authenticate server
$ curl --key ./client.key --cert ./client.crt https://localhost:3000
curl: (60) Peer's certificate issuer has been marked as not trusted by the user.
More details here: http://curl.haxx.se/docs/sslcerts.html

# Without specifying client key/certificate, server cannot authenticate client
$ curl --cacert ./ca.crt https://localhost:3000
curl: (35) NSS: client certificate not found (nickname not specified) ```