Edited at

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


今回の内容

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

今度こそopensslコマンドを理解して使いたい (1) ルートCAをスクリプトで作成する

今度こそopensslコマンドを理解して使いたい (2) 設定ファイル(openssl.cnf)を理解する

今度こそopensslコマンドを理解して使いたい (3) CA証明書の拡張設定を検証する

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

今度こそopensslコマンドを理解して使いたい (5) CRL(証明書失効リスト)を作成してOpenVPNに配布する

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


要件


  • ルート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ごとにサブディレクトリが生成されます。

.

├── .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の定義を変更




/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回 「環境変数」


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


/etc/pki/tls/openssl.cnf

[ v3_ca ]

subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = CA:true
keyUsage = cRLSign, keyCertSign


  • サーバー/クライアント証明書用の拡張セクションを追加する。以下はOpenVPNのサーバー/クライアント用に最適化したサンプルなので、それぞれの用途に合わせた拡張セクションを作成してください。


/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 拡張設定」

第4回 「サーバー/クライアント証明書の拡張設定を検証する」


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はスキップします。


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コマンドで自己署名をして証明書を出力することができます。


  • 出力ファイル

.

├── 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」の証明書


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

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


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

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


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

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


証明書一括発行スクリプト

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


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コマンドで署名して証明書を発行します。中間認証局も同じ手順で作成することができます。


  • 出力ファイル

.

├── 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


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

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


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

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


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

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


証明書一括失効スクリプト

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


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"



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

grep "^R" ca1.mydomain/index.txt


CRL(証明書失効リスト)一括発行スクリプト

ca-listに従って、各CAのデータベースインデックス(index.txt)の情報を元に、全CAの最新のCRLを作成します。失効処理をしていないCRでは「失効数0件のCRL」が作成されます。


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の値が使用されます。


  • 出力ファイル

.

├── 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の内容を確認するコマンド

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を導入した上で設定を再検証する必要があります。


参考