mTLSとは

mutual TLSやTLS相互認証と呼ばれているもの。 きれいにまとまっている記事 1 を読んだので、正確な言い回しができるか自信がないけれど、自分の言葉でメモしておく。

そもそもTLSとは、ネットワーク上で何らかの通信を行う際に用いられる暗号化のためのプロトコルである。 ウェブラウジング、電子メール、Voice over IPなどで利用される。 特にウェブブラウジングにおいては、アドレスバーの左側に鍵マークが表されるので馴染みがある。

TLSは、PKI(Public Key Infrastructure)とX.509証明書から構成される。 X.509は証明書のフォーマットの標準で、httpsの基幹となるTLS/SSLで採用されている。 オンラインだけでなく、オフラインでも電子署名などの用途で使われることもある。 X.509は、公開鍵といくつかのアイデンティティ(ホスト名、組織情報など)から構成され、自分自身あるいは認証局によって署名される。

デフォルトではTLSはクライアントがサーバの身元を検証するだけに使われるため、サーバがクライアントを検証するしくみはアプリケーション側で実装する必要があった。 そこで、コンシューマ向けウェブサービスよりもさらに高いセキュア要件のあるビジネス用途において、サーバ・クライアントが相互に認証できる仕組みとして mTLS が使われることになった。

実行例

単純に動作を知りたいだけなので、curl(クライアント)とNode.js(サーバ)を使って、動作を試してみる。

まず、クライアントとサーバ双方の認証局 CA を作る。 -new -x509が、自身で署名したルートCA用X.509証明書の作成リクエストに対応する。 また、-nodes (No DES)が秘密鍵にパスワードの設定しないことに対応する。

ca.crt の内容を確認すると、SubjectとIssuerが同じ値 example-ca を持っていることがわかる。 つまり、このCAは自身で署名されたということである。 CA:Trueとなっているため、他の証明書に署名することができる。

# 認証局の作成
# 成果物は ca.key と ca.crt
# どちらもPEMフォーマット(base64化された秘密鍵とX.509証明書)

$ openssl req -new -x509 -nodes -days 365 -subj '/CN=example-ca' -keyout ca.key -out ca.crt

# 証明書の確認
$ openssl x509 -in ca.crt -text -noout
Issuer: CN=example-ca
Subject: CN=example-ca
CA:TRUE

続いて、サーバの秘密鍵を作成する。 この秘密鍵に対応する証明書はCAから署名される必要があるので、Certificate Signing Request(CSR) を作成する。

# サーバ用の秘密鍵を作成
$ openssl genrsa -out server.key

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

csrをもとに、CAが署名したサーバ用途の証明書を作成する。

# サーバ用途の証明書 server.crt を作成
$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 365 -out server.crt

# その中身を確認
$ openssl x509 -in server.crt -text -noout
Issuer: CN=example-ca
Subject: CN=localhost

同様の手順をクライアントに対しても実施する。

# クライアントに対しても同様に署名書を作成
$ 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

# CA、Server、Clientそれぞれの証明書一覧
$ ls
ca.crt ca.key ca.srl client.crt client.csr client.key server.crt server.csr server.key

証明書が揃ったので、ウェブサーバを立ち上げてみる。

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

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

const options = {
  ca: fs.readFileSync('ca.crt'), // 認証局の証明書
  cert: fs.readFileSync('server.crt'), // サーバの証明書
  key: fs.readFileSync('server.key'), // サーバの秘密鍵
  rejectUnauthorized: true, // クライアント認証に失敗するとリジェクト
  requestCert: true, // クライアント認証を実施
};

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}/`);
});

curlでリクエストを投げてみる。 クライアントの秘密鍵と証明書、そして認証局の証明書を指定すると、 mTLSによって相互認証が行われ、正常にリクエストが成功する。

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

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

# CAの証明書を指定しないと、クライアント側でサーバ認証ができない
$ 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

# クライアントの鍵・証明書を指定しないと、サーバ側でクライアント認証ができない
$ curl --cacert ./ca.crt https://localhost:3000
curl: (35) NSS: client certificate not found (nickname not specified)
```