前回まで:
第1回: 今度こそopensslコマンドを理解して使いたい (1) ルートCAをスクリプトで作成する
第2回: 今度こそopensslコマンドを理解して使いたい (2) 設定ファイル(openssl.cnf)を理解する
第3回: 今度こそopensslコマンドを理解して使いたい (3) CA証明書の拡張設定を検証する
前回までに調べた知識を使って、ターゲット(サーバー/クライアント)用の証明書をスクリプトで作成します。
ターゲットの証明書をスクリプトで一括生成する
以下の処理をスクリプトで自動化します。
- ターゲット用の鍵とCSR(証明書署名要求)を生成する
- 前回作成したルートCAを使って証明書に署名する
要件
- 自動処理
- opensslコマンドのパスフレーズや識別名の入力を自動化する
- 鍵とCSRの生成から、署名して証明書を出力するまでのフローを自動化する
- 一括処理
- ターゲットリストを読み込んで複数の証明書を一括生成する
- 証明書発行済みのターゲットをスキップして、新規ターゲットのみ処理する
- サーバー証明書とクライアント証明書の処理を共通化する
- ターゲットリストも、サーバー/クライアントを混在可能にする
- どのCAで署名するかをターゲットリストで個別に指定する
- 前回のスクリプトで作成したCAを使って証明書に署名する
- どの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列目: 証明書の有効日数
vpnserver_1 ca1.mydomain v3_vpnserver 1095
vpnserver_2 ca2.mydomain v3_vpnserver 1095
vpnclient_1 ca1.mydomain v3_vpnclient 365
2列目: 署名するCAは、前回のスクリプトで作成したCAであることが前提です。以下のように、CAごとのサブディレクトリと証明書/秘密鍵が生成されているはずです。
.
├── 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のサーバー用とクライアント用の例です。
[ 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回 「環境変数」
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を、パスフレーズ入力用としてそのまま使用します。
mycap@ss
CAのサブディレクトリとファイル
各CAのディレクトリには、以下のサブディレクトリとファイルがデフォルトの名前で自動的に生成されます。
.
├── ca1.mydomain/
│ ├── certs/ # 証明書の出力用ディレクトリ。今回はCSRと秘密鍵もここに出力する
│ ├── newcerts/ # 証明書が自動的に「シリアル番号.pem」のファイル名で出力される
│ ├── private/
│ │ └── cakey.pem
│ ├── cacert.pem
│ ├── index.txt # 署名した証明書のデーターベースインデックスファイル
│ └── serial # 証明書のシリアル番号ファイル。初期値(16進数)が必要だが、インクリメントは自動
証明書一括作成用スクリプト
以上の仕様でスクリプトを作成します。エラー処理などは省略しています。
#!/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
スクリプトを実行すると、ターゲットリストの指定にしたがって以下のファイルが生成されます。
.
├── 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
証明書の内容を確認する
最初に作成したサーバー証明書を、以下のコマンドを使って内容を確認します。
openssl x509 -in ca1.mydomain/certs/vpnserver_1.crt.pem -text
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証明書は自己署名なので
Issuer:
とSubject:
の識別名が同じでしたが、今回はIssuer:
がCA証明書から引用されたCAの識別名で、Subject:
がCSR(署名要求)作成時に指定した対象サーバーになっています。 -
また拡張オプションを多数指定したので、
X509v3 extensions:
の内容が前回のCA証明書より多くなっています。 -
シリアル番号は
Serial Number: 4096 (0x1000)
となっており、スクリプトでecho 1000 >"${ca_dir}/serial"
と初期化したとおりに1000(16進数)になっています。
念のため以下のように、同じCAで2番目に署名された証明書のシリアル番号を確認すると1001になっているので、正しくインクリメントされたことが確認できます。
openssl x509 -in ca1.mydomain/certs/vpnclient_1.crt.pem -serial -noout
serial=1001
シリアル番号ファイルの内容は、次回署名する証明書に与える予定のシリアル番号です。1001まで署名したので、以下のように1002になっています。
cat ca1.mydomain/serial
1002
データベースインデックスファイルには、このCAで署名された証明書の情報が正しく記録されています。
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
秘密鍵を確認する
以下のコマンドで秘密鍵の内容を確認する時にパスフレーズの入力が要求されなければ、パスフレーズなしの秘密鍵が正しく作成されています。
rsa -in ca1.mydomain/certs/vpnserver_1.key.pem -text
サーバー/クライアント証明書の拡張設定を検証する
今回はCA証明書よりも多数の拡張設定(keyUsage、extendedKeyUsage)を指定したので、その結果としてサーバー/クライアント証明書の用途がどのように設定されたかを検証します。
作成したサーバー証明書の用途
証明書の署名に使用した拡張設定:
[ v3_vpnserver ]
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
結果:
SSLサーバー以外のほとんどの用途が制限されました。
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
作成したクライアント証明書の用途
証明書の署名に使用した拡張設定:
[ v3_vpnclient ]
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyAgreement
extendedKeyUsage = clientAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
結果:
SSLクライアント以外のほとんどの用途が制限されました。
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証明書の用途がどのように変わるかを検証しました。
参考: 第3回「作成した証明書の用途をコマンドで検証する」
今回は基本制約をCA:FALSE
とした上で、keyUsage
とextendedKeyUsage
の有無による変化を検証するために、以下の5つの拡張セクションを作成して、それぞれの設定を使用して証明書に署名しました。
[ 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,
keyEncipherment | なし | digitalSignature,
keyEncipherment | digitalSignature,
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をスクリプトで作成する
今度こそopensslコマンドを理解して使いたい (2) 設定ファイル(openssl.cnf)を理解する
今度こそopensslコマンドを理解して使いたい (3) CA証明書の拡張設定を検証する
今度こそopensslコマンドを理解して使いたい (4) サーバー/クライアント証明書を一括生成する
今度こそopensslコマンドを理解して使いたい (5) CRL(証明書失効リスト)を作成してOpenVPNに配布する
今度こそopensslコマンドを理解して使いたい (補足1) サンプルスクリプトのまとめ