LoginSignup
11
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-24

前回まで:
第1回: 今度こそopensslコマンドを理解して使いたい (1) ルートCAをスクリプトで作成する
第2回: 今度こそopensslコマンドを理解して使いたい (2) 設定ファイル(openssl.cnf)を理解する
第3回: 今度こそopensslコマンドを理解して使いたい (3) CA証明書の拡張設定を検証する

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

ca-sign.png

ターゲットの証明書をスクリプトで一括生成する

以下の処理をスクリプトで自動化します。

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

要件

  • 自動処理
    • opensslコマンドのパスフレーズや識別名の入力を自動化する
    • 鍵とCSRの生成から、署名して証明書を出力するまでのフローを自動化する
  • 一括処理
    • ターゲットリストを読み込んで複数の証明書を一括生成する
    • 証明書発行済みのターゲットをスキップして、新規ターゲットのみ処理する
  • サーバー証明書とクライアント証明書の処理を共通化する
    • ターゲットリストも、サーバー/クライアントを混在可能にする
  • どの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であることが前提です。以下のように、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のサーバー用とクライアント用の例です。

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

/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のディレクトリには、以下のサブディレクトリとファイルがデフォルトの名前で自動的に生成されます。

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

証明書一括作成用スクリプト

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

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

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

.
├── 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)を指定したので、その結果としてサーバー/クライアント証明書の用途がどのように設定されたかを検証します。

作成したサーバー証明書の用途

証明書の署名に使用した拡張設定:

/etc/pki/tls/openssl.cnf
[ 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

作成したクライアント証明書の用途

証明書の署名に使用した拡張設定:

/etc/pki/tls/openssl.cnf
[ 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とした上で、keyUsageextendedKeyUsageの有無による変化を検証するために、以下の5つの拡張セクションを作成して、それぞれの設定を使用して証明書に署名しました。

/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,
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) サンプルスクリプトのまとめ

11
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
9