今度こそopensslコマンドを理解して使いたい (4) サーバー／クライアント証明書を一括生成する

前回まで:
第1回: [今度こそopensslコマンドを理解して使いたい (1) ルートCAをスクリプトで作成する](https://qiita.com/3244/items/780469306a3c3051c9fe)
第2回: [今度こそopensslコマンドを理解して使いたい (2) 設定ファイル（openssl.cnf）を理解する](https://qiita.com/3244/items/8c7e6892efdb4e500be9)
第3回: [今度こそopensslコマンドを理解して使いたい (3) CA証明書の拡張設定を検証する](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9)

前回までに調べた知識を使って、ターゲット（サーバー／クライアント）用の証明書をスクリプトで作成します。

![ca-sign.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/522b6efe-6e83-8639-c9ac-7df6dec06a52.png)

# ターゲットの証明書をスクリプトで一括生成する
以下の処理をスクリプトで自動化します。

1. ターゲット用の鍵とCSR（証明書署名要求）を生成する
2. 前回作成したルートCAを使って証明書に署名する

## 要件

* 自動処理
    * opensslコマンドのパスフレーズや識別名の入力を自動化する
    * 鍵とCSRの生成から、署名して証明書を出力するまでのフローを自動化する
* 一括処理
    * ターゲットリストを読み込んで複数の証明書を一括生成する
    * 証明書発行済みのターゲットをスキップして、新規ターゲットのみ処理する
* サーバー証明書とクライアント証明書の処理を共通化する
    * ターゲットリストも、サーバー／クライアントを混在可能にする
* どのCAで署名するかをターゲットリストで個別に指定する
    * [前回のスクリプトで作成したCA](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E5%A4%89%E6%9B%B4)を使って証明書に署名する
    * どのCAで署名するかを、ターゲットリストで個別に指定できるようにする
* 設定ファイルは1つ
    * 設定値の管理が煩雑にならないように、設定ファイルを1つにする
    * ただし拡張セクションは必要に応じて用途ごとに作成する
* ターゲットの秘密鍵にパスフレーズを設定しない
    * 運用時の入力を避けるため、ターゲットの秘密鍵にはパスフレーズを設定しない

## 環境とバージョン
CentOS 7.6を使用するので、OpenSSLのバージョンは1.0.2を使用します。
参考: OpenSSL Manpages for 1.0.2: https://www.openssl.org/docs/man1.0.2/

## ターゲットリスト
以下の例のように、タブまたは空白区切りのテキストファイルで複数のターゲットを指定できるようにします。サーバー／クライアントは混在可能です。

1列目: 対象のサーバー／クライアントのコモンネーム
2列目: 署名するCAのコモンネーム
3列目: 設定ファイルの拡張セクション名（署名用）
4列目: 証明書の有効日数

```:target-list
vpnserver_1  ca1.mydomain  v3_vpnserver  1095
vpnserver_2  ca2.mydomain  v3_vpnserver  1095
vpnclient_1  ca1.mydomain  v3_vpnclient  365
```

2列目: 署名するCAは、[前回のスクリプトで作成したCA](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E5%A4%89%E6%9B%B4)であることが前提です。以下のように、CAごとのサブディレクトリと証明書／秘密鍵が生成されているはずです。

```Bash:
.
├── ca1.mydomain/
│   ├── private/
│   │   └── cakey.pem  # CA「ca1.mydomain」の秘密鍵
│   └── cacert.pem     # CA「ca1.mydomain」の証明書
├── ca2.mydomain/
│   ├── private/
│   │   └── cakey.pem  # CA「ca2.mydomain」の秘密鍵
│   └── cacert.pem     # CA「ca2.mydomain」の証明書
```

3列目: 設定ファイル`openssl.cnf`内の拡張セクション名（署名用）を指定します。必要に応じて、用途別の拡張セクションを作成します。
以下はOpenVPNのサーバー用とクライアント用の例です。

```Ini:/etc/pki/tls/openssl.cnf
[ v3_vpnserver ]
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[ v3_vpnclient ]
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyAgreement
extendedKeyUsage = clientAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
```

参考: OpenVPN.JP How To 「クライアントが接続先のサーバー証明書を検証しない場合に発生する可能性のある「Man-in-the-Middle」攻撃について」
https://www.openvpn.jp/document/how-to/#Man-in-the-Middle

## 設定ファイルの変更
上記拡張セクションの追加に加えて、CAのディレクトリを動的にスクリプトから指定するために`openssl.cnf`を以下のように変更します。

第2回で説明したように、CAのディレクトリを環境変数で変更できるようにします。
参考: [第2回 「環境変数」](https://qiita.com/3244/items/8c7e6892efdb4e500be9#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0)

```Bash:/etc/pki/tls/openssl.cnf
CA_DIR    = /etc/pki/CA     # 全体の保存場所（環境変数がない場合のデフォルト値）

[ CA_default ]
dir       = $ENV::CA_DIR    # 全体の保存場所（環境変数）
# 以下変更なし
certs     = $dir/certs      # Where the issued certs are kept
crl_dir   = $dir/crl        # Where the issued crl are kept
database  = $dir/index.txt  # database index file.
```
## 署名に用いるCAのパスフレーズ
今回は証明書に署名するため、CAの秘密鍵のパスフレーズを入力する必要があります。前回CAを作成した時にパスフレーズを指定するために使用したファイル.capassを、パスフレーズ入力用としてそのまま使用します。

```:.capass
mycap@ss
```

## CAのサブディレクトリとファイル
各CAのディレクトリには、以下のサブディレクトリとファイルがデフォルトの名前で自動的に生成されます。

```Bash:
.
├── ca1.mydomain/
│   ├── certs/         # 証明書の出力用ディレクトリ。今回はCSRと秘密鍵もここに出力する
│   ├── newcerts/      # 証明書が自動的に「シリアル番号.pem」のファイル名で出力される
│   ├── private/
│   │   └── cakey.pem
│   ├── cacert.pem
│   ├── index.txt      # 署名した証明書のデーターベースインデックスファイル
│   └── serial         # 証明書のシリアル番号ファイル。初期値（16進数）が必要だが、インクリメントは自動
```

## 証明書一括作成用スクリプト
以上の仕様でスクリプトを作成します。エラー処理などは省略しています。

```Bash:make-crt.sh
#!/bin/bash

CATOP="$(dirname "${0}")"

# ターゲットのコモンネーム、署名するCA名、署名に使用する拡張セクション名および
# 証明書の有効日数をターゲットリストから1件ずつ読み込む
while read common_name ca_name ex_section expire_days
do
  [[ ${common_name} != "" && ${ca_name} != "" && ${ex_section } != "" ]] || continue

  # CAのディレクトリ
  ca_dir="${CATOP}/${ca_name}"

  # CAのサブディレクトリとファイルがなければ作成する。
  #   - serialにはシリアル番号の初期値（16進数）を出力する。
  #   - index.txt（データーベースインデックス）は空のファイルを作成する。
  mkdir -p "${ca_dir}/certs"
  mkdir -p "${ca_dir}/newcerts"
  [[ -e "${ca_dir}/serial"    ]] || echo 1000 >"${ca_dir}/serial"
  [[ -e "${ca_dir}/index.txt" ]] || touch "${ca_dir}/index.txt"

  # データーベースインデックスのレコードを見て、現在のターゲットが処理済み
  # ならスキップする。
  grep "/CN=${common_name//./\\.}$" "${ca_dir}/index.txt" && continue

  # ターゲットの秘密鍵、CSR（証明書署名要求）、証明書ファイルのパス。
  new_key="${ca_dir}/certs/${common_name}.key.pem"
  new_csr="${ca_dir}/certs/${common_name}.csr.pem"
  new_crt="${ca_dir}/certs/${common_name}.crt.pem"

  # genrsaコマンドでパスフレーズなしの秘密鍵を生成する。
  #   -out  出力する秘密鍵ファイル
  #   2048  秘密鍵のサイズ（ビット数）
  openssl genrsa -out "${new_key}" 2048
  chmod 0400 "${new_key}"

  # reqコマンドでCSR（証明書署名要求）を出力する。
  #   -key  上で生成した秘密鍵ファイル
  #   -out  出力するCSRファイル
  #   -subj ターゲットの識別名
  openssl req -new \
      -key        "${new_key}" \
      -out        "${new_csr}" \
      -subj       "/C=JP/ST=Chiba/O=myorg/CN=${common_name}"

  # CAのディレクトリを環境変数に出力する。
  export CA_DIR="${ca_dir}"

  # caコマンドで署名して証明書を出力する。
  #   -batch      対話入力なしで自動処理を行う
  #   -extensions 設定ファイル内の拡張セクション名
  #   -out        出力する証明書ファイル
  #   -days       証明書を認証する日数
  #   -passin     CAの秘密鍵のパスフレーズ入力
  #   -infiles    上で生成したCSRファイル
  openssl ca \
      -batch \
      -extensions ${ex_section} \
      -out        "${new_crt}" \
      -days       ${expire_days} \
      -passin     "file:${CATOP}/.capass" \
      -infiles    "${new_csr}"

  # 不要になったファイルを削除する。
  rm -f "${new_csr}" "${ca_dir}/newcerts"/*.pem
done <"${CATOP}/target-list"
```
参考:
`genrsa`コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-genrsa.html
`req`コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-req.html
`ca`コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-ca.html

スクリプトを実行すると、ターゲットリストの指定にしたがって以下のファイルが生成されます。

```Bash:
.
├── ca1.mydomain/
│   ├── certs/
│   │   ├── vpnclient_1.crt.pem  # 「vpnclient_1」のクライアント証明書
│   │   ├── vpnclient_1.key.pem  # 「vpnclient_1」の秘密鍵
│   │   ├── vpnserver_1.crt.pem  # 「vpnserver_1」のサーバー証明書
│   │   └── vpnserver_1.key.pem  # 「vpnserver_1」の秘密鍵
│   ├── newcerts/
│   ├── private/
│   │   └── cakey.pem
│   ├── cacert.pem
│   ├── index.txt
│   ├── index.txt.attr
│   ├── index.txt.attr.old
│   ├── index.txt.old
│   ├── serial
│   └── serial.old
├── ca2.mydomain/
│   ├── certs/
│   │   ├── vpnserver_2.crt.pem  # 「vpnserver_2」のサーバー証明書
│   │   └── vpnserver_2.key.pem  # 「vpnserver_2」の秘密鍵
│   ├── newcerts/
│   ├── private/
│   │   └── cakey.pem
│   ├── cacert.pem
│   ├── index.txt
│   ├── index.txt.attr
│   ├── index.txt.attr.old
│   ├── index.txt.old
│   ├── serial
│   └── serial.old
├── .capass
├── ca-list
├── make-crt.sh
├── newca.sh
└── target-list
```

## 証明書の内容を確認する
最初に作成したサーバー証明書を、以下のコマンドを使って内容を確認します。

```Shell:
openssl x509 -in ca1.mydomain/certs/vpnserver_1.crt.pem -text
```

```Shell:
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 4096 (0x1000)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=JP, ST=Chiba, O=myorg, CN=ca1.mydomain
        Validity
            Not Before: Aug 22 11:04:14 2019 GMT
            Not After : Aug 21 11:04:14 2022 GMT
        Subject: C=JP, ST=Chiba, O=myorg, CN=vpnserver_1
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
（略）
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:FALSE
            X509v3 Key Usage: 
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication
            X509v3 Subject Key Identifier: 
                B2:12:4C:93:34:CE:DF:BF:C5:18:1E:12:AD:20:FE:BF:42:80:EA:ED
            X509v3 Authority Key Identifier: 
                keyid:50:7E:07:3B:E8:41:0C:6B:73:1D:67:06:D7:D6:52:02:56:92:F5:9D
（略）
```

* [前回作成したCA証明書](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9#%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%81%AE%E5%86%85%E5%AE%B9%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B)は自己署名なので`Issuer:`と`Subject:`の識別名が同じでしたが、今回は`Issuer:`がCA証明書から引用されたCAの識別名で、`Subject:`がCSR（署名要求）作成時に指定した対象サーバーになっています。

* また拡張オプションを多数指定したので、`X509v3 extensions:`の内容が前回のCA証明書より多くなっています。

* シリアル番号は`Serial Number: 4096 (0x1000)`となっており、スクリプトで`echo 1000 >"${ca_dir}/serial"`と初期化したとおりに1000（16進数）になっています。

念のため以下のように、同じCAで2番目に署名された証明書のシリアル番号を確認すると1001になっているので、正しくインクリメントされたことが確認できます。

```Shell:
openssl x509 -in ca1.mydomain/certs/vpnclient_1.crt.pem -serial -noout

serial=1001
```

シリアル番号ファイルの内容は、次回署名する証明書に与える予定のシリアル番号です。1001まで署名したので、以下のように1002になっています。

```Shell:
cat ca1.mydomain/serial

1002
```

データベースインデックスファイルには、このCAで署名された証明書の情報が正しく記録されています。

```Shell:
cat ca1.mydomain/index.txt

V	220821110732Z		1000	unknown	/C=JP/ST=Chiba/O=myorg/CN=vpnserver_1
V	200821110732Z		1001	unknown	/C=JP/ST=Chiba/O=myorg/CN=vpnclient_1
```

## 秘密鍵を確認する
以下のコマンドで秘密鍵の内容を確認する時にパスフレーズの入力が要求されなければ、パスフレーズなしの秘密鍵が正しく作成されています。

```Shell:
rsa -in ca1.mydomain/certs/vpnserver_1.key.pem -text
```

# サーバー／クライアント証明書の拡張設定を検証する
今回はCA証明書よりも多数の拡張設定（keyUsage、extendedKeyUsage）を指定したので、その結果としてサーバー／クライアント証明書の用途がどのように設定されたかを検証します。

## 作成したサーバー証明書の用途
証明書の署名に使用した拡張設定:

```Ini:/etc/pki/tls/openssl.cnf
[ v3_vpnserver ]
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
```

結果:
SSLサーバー以外のほとんどの用途が制限されました。

```Shell:
openssl x509 -in ca1.mydomain/certs/vpnserver_1.crt.pem -purpose -noout

Certificate purposes:
SSL client : No
SSL client CA : No
SSL server : Yes
SSL server CA : No
Netscape SSL server : Yes
Netscape SSL server CA : No
S/MIME signing : No
S/MIME signing CA : No
S/MIME encryption : No
S/MIME encryption CA : No
CRL signing : No
CRL signing CA : No
Any Purpose : Yes
Any Purpose CA : Yes
OCSP helper : Yes
OCSP helper CA : No
Time Stamp signing : No
Time Stamp signing CA : No
```

## 作成したクライアント証明書の用途
証明書の署名に使用した拡張設定:

```Ini:/etc/pki/tls/openssl.cnf
[ v3_vpnclient ]
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyAgreement
extendedKeyUsage = clientAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
```

結果:
SSLクライアント以外のほとんどの用途が制限されました。

```Shell:
openssl x509 -in ca1.mydomain/certs/vpnclient_1.crt.pem -purpose -noout

Certificate purposes:
SSL client : Yes
SSL client CA : No
SSL server : No
SSL server CA : No
Netscape SSL server : No
Netscape SSL server CA : No
S/MIME signing : No
S/MIME signing CA : No
S/MIME encryption : No
S/MIME encryption CA : No
CRL signing : No
CRL signing CA : No
Any Purpose : Yes
Any Purpose CA : Yes
OCSP helper : Yes
OCSP helper CA : No
Time Stamp signing : No
Time Stamp signing CA : No
```

## 拡張設定と証明書の用途の関係
前回は基本制約を`CA:TRUE`としてCA証明書を作成した時に、`keyUsage`の有無でCA証明書の用途がどのように変わるかを検証しました。
参考: [第３回「作成した証明書の用途をコマンドで検証する」](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9#%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%81%AE%E7%94%A8%E9%80%94%E3%82%92%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%A7%E6%A4%9C%E8%A8%BC%E3%81%99%E3%82%8B)

今回は基本制約を`CA:FALSE`とした上で、`keyUsage`と`extendedKeyUsage`の有無による変化を検証するために、以下の5つの拡張セクションを作成して、それぞれの設定を使用して証明書に署名しました。

```Ini:/etc/pki/tls/openssl.cnf
[ ex1 ]
# 共通設定のみ
basicConstraints=CA:FALSE
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[ ex2 ]
# 共通設定＋keyUsage
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[ ex3 ]
# 共通設定＋extendedKeyUsage
basicConstraints=CA:FALSE
extendedKeyUsage = serverAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[ ex4 ]
# 共通設定＋keyUsage＋extendedKeyUsage
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[ ex5 ]
# 共通設定＋keyUsage＋extendedKeyUsage（クライアント）
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyAgreement
extendedKeyUsage = clientAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
```

[ ex1 ]は全部に共通の設定で、[ ex2 ]には`keyUsage`だけを、[ ex3 ]には`extendedKeyUsage`だけを追加、[ ex4 ]には両方を追加しました。[ ex5 ]は全部入りのクライアント版です。

結果は下表のとおりで、やはり拡張設定を追加するほど証明書の用途が限定されることが分かります。

| | ex1 | ex2 | ex3 | ex4 | ex5 |
:--|:-:|:-:|:-:|:-:|:-:
basicConstraints | CA:FALSE | CA:FALSE | CA:FALSE | CA:FALSE | CA:FALSE
keyUsage | なし | digitalSignature,<br/>keyEncipherment | なし | digitalSignature,<br/>keyEncipherment | digitalSignature,<br/>keyAgreement
extendedKeyUsage | なし | なし | serverAuth | serverAuth | clientAuth
SSL client | Yes | Yes | No | No | Yes
SSL client CA | No | No | No | No | No
SSL server | Yes | Yes | Yes | Yes | No
SSL server CA | No | No | No | No | No
Netscape SSL server | Yes | Yes | Yes | Yes | No
Netscape SSL server CA | No | No | No | No | No
S/MIME signing | Yes | Yes | No | No | No
S/MIME signing CA | No | No | No | No | No
S/MIME encryption | Yes | Yes | No | No | No
S/MIME encryption CA | No | No | No | No | No
CRL signing | Yes | No | Yes | No | No
CRL signing CA | No | No | No | No | No
Any Purpose | Yes | Yes | Yes | Yes | Yes
Any Purpose CA | Yes | Yes | Yes | Yes | Yes
OCSP helper | Yes | Yes | Yes | Yes | Yes
OCSP helper CA | No | No | No | No | No
Time Stamp signing | No | No | No | No | No
Time Stamp signing CA | No | No | No | No | No

# OpenVPNでの動作確認
詳細は省略しますが、サーバー証明書／秘密鍵、CA証明書の3つをOpenVPNサーバーに、クライアント証明書／秘密鍵、CA証明書の3つをOpenVPNクライアントにコピーして、正しくVPN接続ができることを確認しました。

環境：
OpenVPN 2.4.7
サーバー: Amazon Linux 2
クライアント: CentOS 7.6

第1回からopensslコマンドの勉強を始めたそもそもの目的が、OpenVPN用に多数のCAとクライアント証明書の作成を自動化することだったので、当初の目的は達成することができました。

# 今後の予定

* OpenVPNクライアント証明書の失効処理

# 記事一覧
[今度こそopensslコマンドを理解して使いたい (1) ルートCAをスクリプトで作成する](https://qiita.com/3244/items/780469306a3c3051c9fe)
[今度こそopensslコマンドを理解して使いたい (2) 設定ファイル（openssl.cnf）を理解する](https://qiita.com/3244/items/8c7e6892efdb4e500be9)
[今度こそopensslコマンドを理解して使いたい (3) CA証明書の拡張設定を検証する](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9)
今度こそopensslコマンドを理解して使いたい (4) サーバー／クライアント証明書を一括生成する
[今度こそopensslコマンドを理解して使いたい (5) CRL（証明書失効リスト）を作成してOpenVPNに配布する](https://qiita.com/3244/items/21c2cb009de53db03f0e)
[今度こそopensslコマンドを理解して使いたい (補足1) サンプルスクリプトのまとめ](https://qiita.com/3244/items/65dd3926fab76b9dd4aa)