環境を作って、mTLS通信の基礎を学んでみよう

# 1. はじめに

Mutual TLS (mTLS/相互TLS認証) は 主に B2B間でWebサービスを使用する際に使われる サーバ・クライアントが相互に認証をする仕組みのことです。（公式な仕様・定義はみつけられなかったのですが、広く使われている用語のようです。）

普段使われている TLSは ネットワーク上でなんらかの通信を行う際に用いられる暗号化のためのプロトコルです。主に、PKI（Public Key Infrastructure）の X509.証明書から構成されています。X509.証明書は 証明書のフォーマットで、https通信の基本となる TLSで採用されています。

デフォルトでは、TSLプロトコルは、X509.証明書を利用して、クライアントがサーバのアイデンティティを検証します。企業間（B2B）のWebアプリケーションのように、サーバだけでなく、クライアントのアイデンティティ検証が必要になる場面では、Mutual TLS (mTLS/相互TLS認証) が用いられています。

詳しくは以下のサイトを確認いただければと思います。

【図解】よく分かるデジタル証明書(SSL証明書)の仕組み 〜https通信フロー,発行手順,CSR,自己署名(オレオレ)証明書,ルート証明書,中間証明書の必要性や扱いについて〜  
https://milestone-of-se.nesuke.com/sv-advanced/digicert/digital-certification-summary/  

【図解】mutual-TLS (mTLS, 2way TLS),クライアント認証とトークンバインディング over http
https://milestone-of-se.nesuke.com/nw-basic/tls/mutual-tls-token-binding/


# 2. 試してみたこと

いまいちふんわり理解であった mTLS を、手を動かして、①サーバ証明書・クライアント証明書を用意し、②Webサーバを準備し、③実際にmTLS通信をすることで、理解を深めてみようと思います。

今後、同じように、実際に手を動かして理解しようとされる方々のために、手順やコマンドの参考になれば幸いです。


# 3. クライアント証明書の準備・確認

クライアントPC側でクライアント証明書を準備します。

## クライアント用 CA作成

```
$ openssl req -new -x509 -nodes -days 365 -subj 'CN=client-example-ca' -keyout clientca.key -out clientca.crt
Generating a 2048 bit RSA private key
(中略)
writing new private key to 'clientca.key'
-----
$ ls clientca*
clientca.crt   # <- 新規生成
clientca.key   # <- 新規生成
```

## クライアント秘密鍵作成

```
$ openssl genrsa -out client.key
Generating RSA private key, 2048 bit long modulus
(中略)
e is 65537 (0x10001)
$ ls client.key
client.key   # <- 新規生成
```

## クライアント秘密鍵用 Certificate Signing Request（CSR） 作成

```
$ openssl req -new -key client.key -subj '/CN=localclient' -out client.csr
(特に標準出力なし)
$ ls client.csr
client.csr   # <- 新規生成
```

## クライアント証明書作成

クライアントCSRに クライアントCAが署名したクライアント証明書を作成します。

```
$ openssl x509 -req -in client.csr -CA clientca.crt -CAkey clientca.key -CAcreateserial -days 365 -out client.crt
Signature ok
subject=/CN=localclient
Getting CA Private Key
$ ls client.*
client.crt   # <- 新規生成
client.csr   # <- 新規生成
client.key   # <- 新規生成
$ ls clientca.*
clientca.crt
clientca.key
clientca.srl   # <- 新規生成
```

clientca.srl は、CA が CSR に署名した際に発行されるシリアル番号が書かれています。このシリアル番号は個々のCAにおいて、署名済み証明書ごとに異なる番号である必要があります。  
上記の例では、`-CAcreateserial` を付与して自動的にシリアル番号を生成しています。任意のシリアル番号を指定するオプションもあります。


## クライアント証明書の確認

作成したクライアント証明書の確認をしてみます

クライアント証明書には
* client-example-ca が署名していること `Issuer: CN=client-example-ca`
* localclient の証明書であること `Subject: CN=localclient`
* localclient の 公開鍵が含まれていること
がわかります。

```
$ openssl x509 -in client.crt -text -noout
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number: 9959518786539174946 (0x8a3751918d7f3c22)
    Signature Algorithm: sha1WithRSAEncryption
        Issuer: CN=client-example-ca
        Validity
            Not Before: Aug  6 03:43:33 2021 GMT
            Not After : Aug  6 03:43:33 2022 GMT
        Subject: CN=localclient
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:af:03:11:0b:6b:6a:4a:6d:6a:30:65:89:ba:ce:
                    (中略)
                    bb:17
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha1WithRSAEncryption
         85:32:29:50:ae:a7:0a:ad:51:5e:dd:1a:cb:51:da:f2:cd:4f:
         (中略)
         ea:26:6f:e0
```

クライアント証明書関連のファイルは以下の通りです

```
$ ls client*
client.crt     … クライアント証明書
client.csr     … クライアント証明書を作るためのCSR
client.key     … クライアント秘密鍵（これを厳重保管する必要あり）
clientca.crt   … クライアント証明に署名した CAの証明書
clientca.key   … クライアント証明に署名した CAの秘密鍵
clientca.srl   … CAがCSRに署名した際のシリアル番号（証明書ごとに変える必要あり）
```

今回は、クライアントCSRに、自分で作成したCAで署名しクライアント証明書を作成しました。この場合、mTLS通信をするサーバ側に、予めCAの証明書を渡しておく必要があります。  
実際のケースでは、クライアントCSRを、mTLS通信したいサーバの管理者に送り、先方のCAで署名してもらったクライアント証明書をなんらかの方法で入手し、mTLS通信に使用するケースもあるようです。

# 4. サーバ証明書の準備・確認

Webサーバ側でサーバ証明書を準備します。

## サーバ証明書の作成

おおよそやってることはクライアント証明書の作成と同じです。  
今回は、自分で作成したCAでサーバCSRに署名していますが、ちゃんとやるときは（？）公的な認証局にSSL証明書を発行してもらってください（念の為）

```
# サーバ用 CA作成
$ openssl req -new -x509 -nodes -days 365 -subj '/CN=server-example-ca' -keyout serverca.key -out serverca.crt

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

# サーバ秘密鍵用 Certificate Signing Request（CSR） 作成
$ openssl req -new -key server.key -subj '/CN=www.kentarok.local' -out server.csr

# サーバ証明書作成
$ openssl x509 -req -in server.csr -CA serverca.crt -CAkey serverca.key -CAcreateserial -days 365 -out server.crt
```

## サーバ証明書の確認

作成したサーバ証明書の確認をしてみます

サーバ証明書には
* server-example-ca が署名していること `Issuer: CN = server-example-ca`
* www.kentarok.local の証明書であること `Subject: CN = www.kentarok.local`
* www.kentarok.local の 公開鍵が含まれていること
がわかります。

```
$ openssl x509 -in server.crt -text -noout
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            59:15:9a:ea:75:a8:04:fb:f6:4c:45:32:65:26:06:cc:ea:ae:0b:96
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = server-example-ca
        Validity
            Not Before: Aug  6 06:36:21 2021 GMT
            Not After : Aug  6 06:36:21 2022 GMT
        Subject: CN = www.kentarok.local
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:bd:64:4f:80:90:0b:2c:bc:94:e8:2f:ee:ce:58:
                    (中略)
                    a2:7d
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha256WithRSAEncryption
         42:25:85:70:87:c3:6d:4f:10:53:8e:ae:0d:5f:60:e9:80:81:
         (中略)
         ff:b9:51:3e
```


サーバ証明書関連のファイルは以下の通りです

```
$ ls server*
server.crt     … サーバ証明書
server.csr     … サーバ証明書を作るためのCSR
server.key     … サーバ秘密鍵（これを厳重保管する必要あり）
serverca.crt   … サーバ証明に署名した CAの証明書
serverca.key   … サーバ証明に署名した CAの秘密鍵
serverca.srl   … CAがCSRに署名した際のシリアル番号（証明書ごとに変える必要あり）
```


# 5. Webサーバの準備

では、作成したクライアント証明書・サーバ証明書を使って、実際にmTLS通信を試すため、Webサーバを準備していきます。
ここでは Node.js + Express でさくっとWebサーバを用意して試してみたいと思います

今回用意したWebサーバ用のサーバは以下の通りです。
```
$ cat /etc/centos-release
CentOS Linux release 8.3.2011
```

Node.jsを導入します。

```
yum install -y nodejs
npm install express
npm install log4js
```

必要に応じて、Webサーバで使う予定のポートも開けておきます。

```
$ firewall-cmd --zone=public --add-port 8443/tcp
$ firewall-cmd --zone=public --add-port 8443/tcp --permanent
$ firewall-cmd --reload
$ firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: cockpit dhcpv6-client ssh
  ports: 8443/tcp
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
```

サーバ証明書関連ファイルがおいてあるサーバにindex.js を用意します

```
// index.js
const express = require('express');
const http = require('http');
const https = require('https');
const fs = require('fs');
const options = {
  cert: fs.readFileSync('server.crt'), // サーバの証明書
  key: fs.readFileSync('server.key'), // サーバの秘密鍵
  ca: fs.readFileSync('clientca.crt'), // クライアント証明書に署名したCAの証明書
  rejectUnauthorized: true, // クライアント認証に失敗するとリジェクト
  requestCert: true, // クライアント認証を実施
};

var log4js = require('log4js');
log4js.configure({
  appenders: {
    system: {type: 'file',filename: 'system.log'},  // システムログ
    access: {type: 'file',filename: 'access.log'}   // アクセスログ
  },
  categories: {
    default: {appenders: ['system'],level: 'debug'},
    access: {appenders: ['access'],level: 'info'}
  }
});

var systemLogger = log4js.getLogger();
var accessLogger = log4js.getLogger('access');
const app = express();
app.use(log4js.connectLogger(accessLogger));
app.get('/test',(req, res) => {
  res.status(200).json({
    message: 'Hello mtls'
  });
});

https.createServer(options,app).listen(8443, () => {
  console.log('HTTPS listening on 8443....');
  systemLogger.info('HTTPS start');
});
```

* Webサーバ側には、クライアントから送られてくるクライアント証明書を確認するために、クライアント証明書を作る際に署名した CAの証明書（clientca.crt）を配置する必要があります。
* クライアント側には、サーバから送られてくるサーバ証明書を確認するために、サーバ証明書を作る際に署名したCAの証明書（serverca.crt）を配置する必要があります

なので、お互いにCAの証明書を交換しあってください。


# 6. mTLS通信テスト

curl で mutual TLS通信の接続テストをしてみます。  
（クライアントPC側は hostsファイルなどで、サーバ証明書に記載したドメイン名をサーバのIPに名前解決できるように設定しておいてください）

curl に クライアント証明書とその秘密鍵、そして、サーバから送られてくるであろうサーバ証明書のCA証明書を指定してあげると、mutualTLSによって相互認証が行われ、正常にリクエストが成功します

```
$ curl -v --cacert ./serverca.crt --key ./client.key --cert ./client.crt https://www.kentarok.local:8443/test

* Trying xxx.xxx.140.18...
* TCP_NODELAY set
* Connected to www.kentarok.local (xxx.xxx.140.18) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: ./serverca.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS handshake, CERT verify (15):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: CN=www.kentarok.local
*  start date: Aug  6 06:36:21 2021 GMT
*  expire date: Aug  6 06:36:21 2022 GMT
*  common name: www.kentarok.local (matched)
*  issuer: CN=server-example-ca
*  SSL certificate verify ok.
> GET /test HTTP/1.1
> Host: www.kentarok.local:8443
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 24
< ETag: W/"18-g/xxyjKO9ZXN0/7BZTmgDKWVQBE"
< Date: Fri, 06 Aug 2021 07:55:04 GMT
< Connection: keep-alive
<
* Connection #0 to host www.kentarok.local left intact
{"message":"Hello mtls"}* Closing connection 0
```

curl の 引数から サーバ証明書のCA証明書 を省いてあげると、以下の通りTLS handshakeの中のサーバ証明書の検証あたりでエラーとなります。

```
$ curl -v --key ./client.key --cert ./client.crt https://www.kentarok.local:8443/test

* Trying xxx.xxx.140.18...
* TCP_NODELAY set
* Connected to www.kentarok.local (xxx.xxx.140.18) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate
* Closing connection 0
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
```

curl の 引数から クライアント証明書と秘密鍵を省いてあげると、（クライアント証明書を求めてくる）サーバ側から切られる形のSSLエラーとなります。

```
$ curl -v --cacert ./serverca.crt https://www.kentarok.local:8443/test

*   Trying xxx.xxx.140.18...
* TCP_NODELAY set
* Connected to www.kentarok.local (xxx.xxx.140.18) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: ./serverca.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS alert, handshake failure (552):
* error:1401E410:SSL routines:CONNECT_CR_FINISHED:sslv3 alert handshake failure
* Closing connection 0
curl: (35) error:1401E410:SSL routines:CONNECT_CR_FINISHED:sslv3 alert handshake failure

```

# 7. おわりに

この記事では、実際に mTLS通信をためせるよう ①クライアント証明書の発行 ②サーバ証明書の発行 ③Webサーバの準備 ④mTLS通信時に実行するコマンド例 を記載しました。



# おまけ.Centos8の vi日本語文字化け対策

今回、CentOS8のviで日本語が正常に表示されない事案があったので対応策をメモしておきます。

```
$ cat ~/.virc
" vim の独自拡張機能を使用 (vi との互換性無し)
:set nocompatible
" 文字コードを指定
:set encoding=utf-8


" ファイルエンコードの指定
:set fileencodings=utf-8,iso-2022-jp,euc-jp,sjis


" 自動認識させる改行コードの指定
:set fileformats=unix,dos,mac

$ source ~/.bashrc
```

【CentOS8】Dockerコンテナでviやvimで日本語が文字化けするときの対処方法
https://genchan.net/it/server/10829/

# 参考にさせていただいたサイト

Mutual TLS Authentication (mTLS) De-Mystified
https://codeburst.io/mutual-tls-authentication-mtls-de-mystified-11fa2a52e9cf

mTLS（Mutual TLS）メモ
https://blog.bobuhiro11.net/2021/01-12-mtls.html


CIS（IBM Cloud Internet Service）でAuthenticated Origin Pull（認証済みOrigin Pull）を構成してみました
https://qiita.com/JoeToyoda/items/434a2ad9ede7bd3cb429#4%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB

mTLS（Mutual TLS）メモ
https://blog.bobuhiro11.net/2021/01-12-mtls.html  

OpenSSLで雑にCAを構築する
https://inaz2.hatenablog.com/entry/2015/01/28/230418