今度こそopensslコマンドを理解して使いたい (補足1) サンプルスクリプトのまとめ

# 今回の内容
以下の記事で、今度こそopensslコマンドを理解して使いたいと考えていた調査と作業を終了しました。

[今度こそ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) サーバー／クライアント証明書を一括生成する](https://qiita.com/3244/items/2618429ebe6dc16c074e)
[今度こそopensslコマンドを理解して使いたい (5) CRL（証明書失効リスト）を作成してOpenVPNに配布する](https://qiita.com/3244/items/21c2cb009de53db03f0e)

![openssl_1-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/a82f6b96-69a5-0a91-b143-c0bb848b414a.png)

今回はこれらの記事から調査の過程と公式ドキュメントの抜粋を省いて、サンプルスクリプトの部分だけを抜き出してまとめます。

## 要件

* ルートCA作成、サーバー／クライアントの証明書を発行／失効、CRL発行を行う
* 対話入力なしの自動処理
* 処理対象リストを読み込んで一括処理
* 複数のルートCAを作成／運用する
* 設定ファイル（openssl.cnf）は1つ
* CAの秘密鍵にパスフレーズを設定する
* CA以外の秘密鍵はパスフレーズなしにする
* 証明書ごとの有効期限や拡張設定を自由に設定できる

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

証明書を発行／失効する対象のソフトウェアは特に限定されませんが、当面OpenVPNを対象とします。
参考: OpenVPN コミュニティ Wiki: https://community.openvpn.net/openvpn

# スクリプトと使用するファイル
設定ファイルopenssl.cnfは標準ファイルの一部を変更します。その他のファイルは任意のディレクトリに作成します。

種類|ファイル名|説明|CA作成|証明書発行|証明書失効|CRL発行
:-:|:--|:--|:-:|:-:|:-:|:-:
設定ファイル|/etc/pki/tls/openssl.cnf|OpenSSLの設定ファイル|X|X|X|X
リスト|./ca-list|作成／運用するルートCAのリスト|X|||X
リスト|./target-list|証明書を発行する対象サーバー／クライアントのリスト||X||
リスト|./revoke-list|証明書を失効する対象サーバー／クライアントのリスト|||X|
テキストファイル|./.capass|ルートCAの秘密鍵に設定するパスフレーズ|X|X|X|X
シェルスクリプト|./newca.sh|ca-listのルートCAを作成する|X|||
シェルスクリプト|./make-crt.sh|target-listのサーバー／クライアントの証明書を発行する||X||
シェルスクリプト|./revoke.sh|revoke-listのサーバー／クライアントの証明書を失効する|||X|
シェルスクリプト|./update-crl.sh|ca-listのルートCAのCRL（証明書失効リスト）を発行する||||X

作成するファイルは以下のとおりです。スクリプトを実行すると、CAごとにサブディレクトリが生成されます。

```Shell:
.
├── .capass
├── ca-list
├── make-crt.sh
├── newca.sh
├── revoke-list
├── revoke.sh
├── target-list
└── update-crl.sh
```

## OpenSSLの設定ファイル
標準の`/etc/pki/tls/openssl.cnf`を以下のように変更します。

* デフォルトセクション／CA用セクション  
  * デフォルトセクション（最初の名前付きセクションの前）に`CA_DIR = /etc/pki/CA`を追加
  * `[ CA_default ]`セクション内の`dir`の定義を変更

```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.
```
参考: [第2回 「環境変数」](https://qiita.com/3244/items/8c7e6892efdb4e500be9#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0)

* CA自己署名に使用する`[ v3_ca ]`セクション内のコメント行`#keyUsage = cRLSign, keyCertSign`をアンコメントして、以下の4行が有効になるようにしてください。

```Ini:/etc/pki/tls/openssl.cnf
[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = CA:true
keyUsage = cRLSign, keyCertSign
```

* サーバー／クライアント証明書用の拡張セクションを追加する。以下は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
```

参考：
[第2回 「X509 V3 拡張設定」](https://qiita.com/3244/items/8c7e6892efdb4e500be9#x509-v3-%E6%8B%A1%E5%BC%B5%E8%A8%AD%E5%AE%9A)
[第4回 「サーバー／クライアント証明書の拡張設定を検証する」](https://qiita.com/3244/items/2618429ebe6dc16c074e#%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%81%AE%E6%8B%A1%E5%BC%B5%E8%A8%AD%E5%AE%9A%E3%82%92%E6%A4%9C%E8%A8%BC%E3%81%99%E3%82%8B)

## CAリスト
CA自体の作成およびCRL（証明書失効リスト）発行のためのCA名のリスト
1列目: 作成／運用するルートCAのコモンネーム

* 例:

```:ca-list
ca1.mydomain
ca2.mydomain
```

## 証明書発行対象リスト
1列目: 発行対象のサーバー／クライアントのコモンネーム
2列目: 署名するCAのコモンネーム（ca-listで定義したどれかと一致すること）
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
```

## 証明書失効対象リスト
1列目: 失効対象のサーバー／クライアントのコモンネーム
2列目: 証明書を発行した時に署名したCAのコモンネーム

* 例:

```:revoke-list
vpnclient_1  ca1.mydomain
vpnserver_2  ca2.mydomain
```


## CAパスフレーズファイル
1行目: CAの秘密鍵に設定するパスフレーズ

* 例:

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

## CA作成スクリプト
ca-listに従って新しいCAのディレクトリ、秘密鍵および証明書を作成します。作成済みのCAはスキップします。

```Bash:newca.sh
CATOP=$(dirname "${0}")

# 作成するCAのコモンネームをリストから1件ずつ読み込む
while read ca_name
do
  # CAのディレクトリ、証明書、秘密鍵のパス
  ca_dir="${CATOP}/${ca_name}"
  ca_cert="${ca_dir}/cacert.pem"
  ca_key="${ca_dir}/private/cakey.pem"

  # すでに証明書と秘密鍵が作成済みの場合はスキップ
  [[ -e "${ca_cert}" && -e "${ca_key}" ]] && continue

  # CA用のディレクトリを作成する
  mkdir -p "$(dirname "${ca_cert}")"
  mkdir -p "$(dirname "${ca_key}")"

  # 秘密鍵を生成して自己署名証明書を発行する
  # -new        新しい証明書要求を生成する
  # -keyout     指定のパスで秘密鍵を出力する
  # -out        指定のパスに出力する（-x509オプションを指定した場合は自己署名証明書）
  # -passout    出力する秘密鍵に設定するパスフレーズを指定する
  # -subj       識別名を"/type0=value0/type1=value1/type2=..."の形式で指定する
  # -x509       署名要求を出力する代わりに自己署名証明書を出力する
  # -days       証明書の有効日数を指定する
  # -extensions 設定ファイルの拡張セクション名を指定する
  openssl req \
      -new \
      -keyout   "${ca_key}" \
      -out      "${ca_cert}" \
      -passout  "file:${CATOP}/.capass" \
      -subj     "/C=JP/ST=Chiba/O=myorg/CN=${ca_name}" \
      -x509 -days 1095 -extensions v3_ca

  # 秘密鍵のパーミッションを設定する
  chmod 0400 "${ca_key}"
done <"${CATOP}/ca-list"
```

本来は`req`コマンドでCSR（証明書署名要求）を作成して`ca`コマンドで署名することで証明書ができるのですが、ルートCAの場合に限り`req`コマンドで自己署名をして証明書を出力することができます。

* 出力ファイル

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

* 証明書の内容を確認するコマンド

```Shell:
openssl x509 -in ca1.mydomain/cacert.pem -text -noout
```

* 証明書の用途を検証するコマンド

```Shell:
openssl x509 -in ca1.mydomain/cacert.pem -purpose -noout
```

* 秘密鍵の内容を確認するコマンド

```Shell:
openssl rsa -in ca1.mydomain/private/cakey.pem -text -noout
```

## 証明書一括発行スクリプト
target-listに従ってサーバー／クライアントの秘密鍵と証明書を作成します。署名するCA、署名に使用する拡張セクションおよび証明書の有効日数もリストで指定します。証明書に署名済みの対象はスキップします。

```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"
```
自己署名ではないので、`req`コマンドで作成したCSR（証明書署名要求）に`ca`コマンドで署名して証明書を発行します。中間認証局も同じ手順で作成することができます。

* 出力ファイル

```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         # 証明書のシリアル番号ファイル。初期値（16進数）が必要だが、インクリメントは自動
│   └── serial.old
```

* 証明書の内容を確認するコマンド

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

* 証明書の用途を検証するコマンド

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

* 秘密鍵の内容を確認するコマンド

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

## 証明書一括失効スクリプト
revoke-listに従って証明書の失効処理を行い、CAのデータベースインデックス（index.txt）に結果を記録します。すでに失効済みの対象はスキップします。

```Bash:revoke.sh
#!/bin/bash

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

# 失効対象のコモンネーム、署名するCA名を失効対象リストから1件ずつ読み込む
while read common_name ca_name
do
  [[ ${common_name} != "" && ${ca_name} != "" ]] || continue

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

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

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

  # caコマンドで証明書を失効する。
  #   -revoke     失効対象の証明書ファイルのパス
  #   -passin     CAの秘密鍵のパスフレーズ入力
  openssl ca \
      -revoke   "${ca_dir}/certs/${common_name}.crt.pem" \
      -passin   "file:${CATOP}/.capass"

done <"${CATOP}/revoke-list"
```

* 失効された対象を確認する方法

```Shell:
grep "^R" ca1.mydomain/index.txt
```

## CRL（証明書失効リスト）一括発行スクリプト
ca-listに従って、各CAのデータベースインデックス（index.txt）の情報を元に、全CAの最新のCRLを作成します。失効処理をしていないCRでは「失効数0件のCRL」が作成されます。

```Bash:update-crl.sh
#!/bin/bash

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

# CRLを作成するCAのコモンネームをCAリストから1件ずつ読み込む
while read ca_name
do
  [[ ${ca_name} != "" ]] || continue

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

  # CRLのサブディレクトリとファイルがなければ作成する。
  #   - crlnumberにはCRL番号の初期値を出力する。
  mkdir -p "${ca_dir}/crl"
  [[ -e "${ca_dir}/crlnumber" ]] || echo "00" >"${ca_dir}/crlnumber"

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

  # caコマンドでCRLを生成する。
  #   -gencrl     CRLを生成する
  #   -crldays    次回CRL更新までの日数
  #   -out        出力するCRLファイル
  #   -passin     CAの秘密鍵のパスフレーズ入力
  openssl ca \
      -gencrl \
      -crldays  365 \
      -out      "${ca_dir}/crl/crl.pem" \
      -passin   "file:${CATOP}/.capass"

done <"${CATOP}/ca-list"
```
注意: `-crldays`で次回CRL更新までの日数を指定します。**この期限を過ぎてCRLが更新されないとすべての証明書が認証されなくなってしまうので、CRLの運用が確立していない場合は多めの日数を指定します。**このオプションを指定しない場合は、設定ファイルの`default_crl_days`の値が使用されます。

* 出力ファイル

```Bash:
.
├── ca1.mydomain/
│   ├── certs/
│   │   ├── vpnclient_1.crt.pem
│   │   ├── vpnclient_1.key.pem
│   │   ├── vpnserver_1.crt.pem
│   │   └── vpnserver_1.key.pem
│   ├── crl/              # CRL出力用ディレクトリ
│   │   └── crl.pem       # ca1.mydomainのCRL
│   ├── newcerts/
│   ├── private
│   │   └── cakey.pem
│   ├── cacert.pem
│   ├── crlnumber         # 次回のCRL番号ファイル。初期値が必要だが、インクリメントは自動
│   ├── crlnumber.old
│   ├── index.txt
│   ├── index.txt.attr
│   ├── index.txt.attr.old
│   ├── index.txt.old
│   ├── serial
│   └── serial.old
```

* CRLの内容を確認するコマンド

```Shell:
openssl crl -in ca1.mydomain/crl/crl.pem -text -noout
```

# 今後の課題
OpenVPN 2.4.7の証明書はOpenSSL 1.0.2で問題なくできましたが、このバージョンのOpenSSLはTLS 1.3に対応していません。今後新しいセキュリティ規格に対応するには、OpenSSL 1.1.xを導入した上で設定を再検証する必要があります。

# 参考

* OpenSSLのコマンド（記事での使用順）
    * `req`コマンド manpage (1.0.2): https://www.openssl.org/docs/man1.0.2/man1/openssl-req.html
    * `x509`コマンド manpage (1.0.2): https://www.openssl.org/docs/man1.0.2/man1/openssl-x509.html
    * `rsa`コマンド manpage (1.0.2): https://www.openssl.org/docs/man1.0.2/man1/openssl-rsa.html
    * `genrsa`コマンド manpage (1.0.2): https://www.openssl.org/docs/man1.0.2/man1/openssl-genrsa.html
    * `ca`コマンド manpage (1.0.2): https://www.openssl.org/docs/man1.0.2/man1/openssl-ca.html
    * `crl`コマンド manpage (1.0.2): https://www.openssl.org/docs/man1.0.2/man1/openssl-crl.html
* OpenSSLの設定
    * 設定ファイル manpage（1.0.2）: https://www.openssl.org/docs/man1.0.2/man5/config.html
    * X509 V3 拡張設定 manpage（1.0.2）: https://www.openssl.org/docs/man1.0.2/man5/x509v3_config.html