これまでの反省
OpenSSLを使ってオレオレ証明書を作った経験は何度かあるのですが、先人がネットで紹介されていた手順のとおりに操作しただけで、各サブコマンドの機能や設定、オプションの意味など何も理解していませんでした。
今回、多数のプライベートCAとクライアント証明書をスクリプトで自動作成することになったので、これを機会にopensslコマンドの勉強を始めました。
なぜopensslコマンドが難しいのか
個人的に難しいと思う点をまとめたらこんなにありました…
-
コマンド(サブコマンド)の数が多く、しかも機能が重複している
OpenSSLコマンド(1.0.2): https://www.openssl.org/docs/man1.0.2/man1/ -
同じ目的のための方法がいろいろある
例えば証明書発行を [秘密鍵を作成] → [署名要求を作成] → [署名して証明書を発行] と3手順で行う方法もあれば、1手順で全部を行う方法もあります。 -
設定ファイルが難解
構造は一般的なINI形式ですが、どのセクションの設定がどの場合に使われるかが分かりにくいです。 -
コマンドのオプションが難解
コマンド(サブコマンド)ごとに定義されているオプションが多く、また設定ファイル内の設定値との関係も複雑です。 -
そもそも公開鍵暗号技術が難解
こればかりは勉強するしかありません。
設定ファイル(openssl.cnf)のセクションに関する誤解
恥ずかしながら公式ドキュメントを読んで、設定ファイル内のセクションの意味を初めて知りました。
例えば以下の部分を見て、default_ca = CA_default
が下の[ CA_default ]
セクションを指定していることは分かります。
しかし最初の[ ca ]
セクションはどこからも指定されていません。
[ ca ]
default_ca = CA_default # The default ca section
[ CA_default ]
私はこれを誤解して、[ ca ]
セクションは CAを作る時に自動的に参照されると思っていました。
しかし実際には**[ ca ]
セクションはca
コマンドが参照するセクション**であって、CAを作ることとは関係ありません。
同様に、[ req ]
セクションはreq
コマンドが参照するセクションです。
例えばデフォルトのopenssl.cnf
でca
コマンドを実行すると、各セクションは以下のように参照されます。
-
openssl ca
コマンドを実行すると - セクション
[ ca ]
が参照される
この中でdefault_ca = CA_default
と指定されているので - セクション
[ CA_default ]
が参照される
この中でpolicy = policy_match
とx509_extensions = usr_cert
が指定されているので - セクション
[ policy_match ]
と拡張セクション[ usr_cert ]
が参照される
こんなことも知らずに使っていたとは…
ルートCAを作る
まずは ルートCA(認証局)を作るところから始めます。
要件
少しハードルが高くなりますが、以下の要件で進めたいと思います。
-
自動処理
opensslコマンドを普通に使用するとパスフレーズや識別名が対話入力になるが、これをスクリプトで自動化したい -
設定ファイルは1つ
CA用/サーバー用などで設定ファイルを作り分けると設定値の管理が煩雑になるので、設定ファイルを1つにしたい -
CAの秘密鍵にパスフレーズを設定する
パスフレーズを削除する例が多いが、署名時のパスフレーズ入力操作も含めて自動化したい
環境とバージョン
CentOS 7.6を使用するので、OpenSSLのバージョンは1.0.2を使用します。
- OpenSSL Manpages for 1.0.2: https://www.openssl.org/docs/man1.0.2/
英語のドキュメントを読むのが嫌いな人は、よろしければこちらもご一読ください。
'CA'スクリプトのCA作成処理
まずCAの作り方のお手本として、公式スクリプトの'CA'
(CentOSの場合は/etc/pki/tls/misc/CA
)を参考にします。新規CAを作成する時には-newca
オプションを指定するので、実行されるコードは以下の部分になります。
-newca)
# if explicitly asked for or it doesn't exist then setup the directory
# structure that Eric likes to manage things
NEW="1"
if [ "$NEW" -o ! -f ${CATOP}/serial ]; then
# create the directory hierarchy
mkdir -p ${CATOP}
mkdir -p ${CATOP}/certs
mkdir -p ${CATOP}/crl
mkdir -p ${CATOP}/newcerts
mkdir -p ${CATOP}/private
touch ${CATOP}/index.txt
fi
if [ ! -f ${CATOP}/private/$CAKEY ]; then
echo "CA certificate filename (or enter to create)"
read FILE
# ask user for existing CA certificate
if [ "$FILE" ]; then
cp_pem $FILE ${CATOP}/private/$CAKEY PRIVATE
cp_pem $FILE ${CATOP}/$CACERT CERTIFICATE
RET=$?
if [ ! -f "${CATOP}/serial" ]; then
$X509 -in ${CATOP}/$CACERT -noout -next_serial \
-out ${CATOP}/serial
fi
else
echo "Making CA certificate ..."
$REQ -new -keyout ${CATOP}/private/$CAKEY \
-out ${CATOP}/$CAREQ
$CA -create_serial -out ${CATOP}/$CACERT $CADAYS -batch \
-keyfile ${CATOP}/private/$CAKEY -selfsign \
-extensions v3_ca \
-infiles ${CATOP}/$CAREQ
RET=$?
fi
fi
;;
既存の証明書を処理する部分を除いて変数を展開し、新しいスクリプトとして保存します。
CATOP=/etc/pki/CA
# 秘密鍵の生成から署名要求の作成までを行う
openssl req -new -keyout ${CATOP}/private/cakey.pem \
-out ${CATOP}/careq.pem
# 上の署名要求を受け取って証明書を発行する
openssl ca -create_serial -out ${CATOP}/cacert.pem -days 1095 -batch \
-keyfile ${CATOP}/private/cakey.pem -selfsign \
-extensions v3_ca \
-infiles ${CATOP}/careq.pem
処理の内容は以下の2つです。
-
req
コマンドを使って、秘密鍵の生成から署名要求の作成までを行う- このままでは、秘密鍵のパスフレーズを対話形式で入力する必要があります
- 署名要求の識別名(国、組織、コモンネームなど)も対話形式で入力する必要があります
-
ca
コマンドを使って署名要求を受け取り、署名して証明書を発行する-
-days
オプションで証明書の有効期限を指定しているので、openssl.cnf
のdefault_days
の設定値は無視されます -
-extensions
オプションでopenssl.cnf
内の拡張セクション[ v3_ca ]
を参照することが明示的に指定されています - 拡張セクション
[ v3_ca ]
内でbasicConstraints = CA:true
が設定されているので、発行された証明書はCAで署名用として使用できるようになります
-
公式ドキュメントをよく読む
-
req
コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-req.html
ページの前半にコマンドオプション、後半に設定ファイルのオプション、識別名と属性セクションのフォーマット、サンプル、注記、エラーの診断方法、環境変数の説明などが丁寧に記載されています。
秘密鍵のパスフレーズ入力を自動化する
対話入力のパスフレーズをスクリプトで自動入力する方法を調べます。
req
コマンドのドキュメントを読むと、出力用のパスフレーズを指定する-passout
オプションが見つかりました。
-passout arg
出力ファイルのパスワード入力。引数arg
の詳細は、openssl
コマンドのPASS PHRASE ARGUMENTS
セクションを参照のこと。
指示どおりにopensslコマンドのページを参照すると、PASS PHRASE ARGUMENTS
セクションで以下のように説明されています。
openssl
コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl.html
いくつかのコマンドは、入出力パスワードの引数を
-passin
と-passout
で、以下の入力元とフォーマットのどれか1つを受け取ることができる。
pass:password
password
で実際のパスワードを指定する。この形式はps
コマンドなどでパスワードが見られてしまうので、セキュリティが重要でない場合でのみ使用すること。
env:var
環境変数からパスワードを取得する。ps
コマンドなどで他のプロセスの環境変数が読み取られるので、この方法は注意して使用すること。
file:pathname
pathname
の1行目でパスワードを指定する。同じpathname
が-passin
と-passout
で指定された場合は、1行目が入力用、2行目が出力用として使用される。pathname
は通常のファイル以外に、デバイスや名前付きパイプを参照することもできる。
fd:number
ファイルディスクリプタからパスワードを読み取る。例えばパイプを介してデータを送るために使用できる。
stdin
標準入力からパスワードを読み取る。
今回はfile:pathname
の方法を採用し、パスフレーズを1行目に記述したファイルを作成します。例として/etc/pki/CA/.capass
を作成し、パスフレーズを'mycap@ss'とします。
mycap@ss
このファイルを秘密鍵の出力用パスフレーズとして-passout
オプションで指定すると、req
コマンドのオプションは以下のようになります。
# 秘密鍵の生成から署名要求の作成までを行う
openss req -new \
-keyout ${CATOP}/private/cakey.pem \
-out ${CATOP}/careq.pem \
-passout file:${CATOP}/.capass # 出力用パスフレーズを記述したファイル
識別名の入力を自動化する
このままでは、識別名の各フィールド(国、都道府県、組織、コモンネームなど)が対話入力のままです。
またコモンネームを設定ファイルで指定してしまうと、証明書を発行するサーバやクライアントの数だけ設定ファイルが必要になってしまうという問題もあります。
これらの問題を解決するために、識別名をコマンドラインオプションで指定する方法を調べます。
req
コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-req.html
-subj arg
署名要求のサブジェクトのフィールドを指定データで置換して出力する。arg
は/type0=value0/type1=value1/type2=...
の形式にしなければならない。\
で文字をエスケープすることができ、スペースはスキップされない。
また同じページのサンプルで、対話形式でない場合の識別名の各フィールドが以下のように記載されています。
[ req_distinguished_name ]
C = 2文字の国名
ST = 都道府県
L = 地域
O = 組織名
OU = 組織単位名
CN = コモンネーム
emailAddress = test@email.address
コモンネーム以外は必須ではないので省略できるようですが、今回は一部を省略して以下のようにします。
/C=JP/ST=Chiba/O=myorg/CN=ca1.mydomain
また他の組織の証明書を取り扱うことはないので/C=JP/ST=Chiba/O=myorg
までを固定にして、コモンネームだけを署名の対象ごとに変更できるようにします。
コモンネームを変数common_name
で指定すると、req
コマンドのオプションは以下のようになります。
# 秘密鍵の生成から署名要求の作成までを行う
openss req -new \
-keyout ${CATOP}/private/cakey.pem \
-out ${CATOP}/careq.pem \
-passout file:${CATOP}/.capass \
-subj "/C=JP/ST=Chiba/O=myorg/CN=${common_name}" # 識別名を指定する
今回は複数のCAを連続して生成したいので、CAのシリアル番号とコモンネームの一覧を記述したファイル/etc/pki/CA/ca-list
(任意)を作成します。
1 ca1.mydomain
2 ca2.mydomain
このリストを読み込んでループの中でreq
コマンドとca
コマンドを実行するように変更します。またそれぞれのCA用に、秘密鍵と証明書を出力するためのサブディレクトリも作成します。
CATOP=/etc/pki/CA
while read serial_num common_name
do
# CAごとに出力用のサブディレクトリを作成する
mkdir -p ${CATOP}/${common_name}/private
# 秘密鍵の生成から署名要求の作成までを行う
openss req -new \
-keyout ${CATOP}/${common_name}/private/cakey.pem \
-out ${CATOP}/${common_name}/careq.pem \
-passout file:${CATOP}/.capass \
-subj "/C=JP/ST=Chiba/O=myorg/CN=${common_name}" # 識別名を指定する
# 上の署名要求を受け取って証明書を発行する
openssl ca -create_serial -out ${CATOP}/${common_name}/cacert.pem -days 1095 -batch \
-keyfile ${CATOP}/${common_name}/private/cakey.pem -selfsign \
-extensions v3_ca \
-infiles ${CATOP}/${common_name}/careq.pem
done <${CATOP}/ca-list # CAのシリアル番号とコモンネームの一覧を読み込む
証明書の自己署名を同時に行う
今回はルートCAとして証明書を自己署名するので、そもそも署名要求の生成と署名の処理を分ける必要がありません。そのためにreq
コマンドには、署名要求の代わりに自己署名済みの証明書を出力する機能があることが分かりました。
この機能を使えば、req
コマンドに続くca
コマンドの処理をなくすことができそうです。
req
コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-req.html
-x509
このオプションは署名要求の代わりに自己署名された証明書を出力する。通常これはテストまたは自己署名のルートCAに使用される。設定ファイルで指定されていれば拡張属性が証明書に付与される。set_serial
オプションが使用されなければ、大きな乱数がシリアル番号として使用される。
既存の署名要求が-in
オプションで指定されたら自己署名証明書に変換され、なければ新規の署名要求が作成される。
-days n
-x509
オプションが使用される場合に、証明書が有効な日数を指定する。デフォルトは30日。
-set_serial n
自己署名証明書を出力するときに使用するシリアル番号。 10進数または0x
で始まる16進数を指定できる。マイナス値は使用できるが推奨されない。
以下のようにこれらのオプションをreq
コマンドに追加することで、ca
コマンドが行っていた署名と証明書出力の処理を置き換えることができそうです。
CATOP=/etc/pki/CA
# CAのシリアル番号とコモンネームを1件ずつ読み込む
while read serial_num common_name
do
# CAごとに出力用のサブディレクトリを作成する
mkdir -p ${CATOP}/${common_name}/private
# 秘密鍵を生成して自己署名証明書を発行する
openss req -new \
-keyout ${CATOP}/${common_name}/private/cakey.pem \
-out ${CATOP}/${common_name}/cacert.pem \
-passout file:${CATOP}/.capass \
-subj "/C=JP/ST=Chiba/O=myorg/CN=${common_name}" \
-x509 -days 1095 -set_serial ${serial_num} # 自己署名を行い証明書を出力する
done <${CATOP}/ca-list
-
-out
オプションで指定するファイル名を、署名要求から証明書のファイル名に変更 -
-days
オプションで証明書の有効日数を指定する
拡張セクションを指定する
元の公式'CA'
スクリプトの中で、ca
コマンドのオプションとして-extensions
で拡張セクションが指定されています。req
コマンドでも同様に、以下のオプションを使うことで拡張セクションを指定できることができると書かれています。
req
コマンド manpage: https://www.openssl.org/docs/man1.0.2/man1/openssl-req.html
-extensions section
-reqexts section
これらのオプションは、証明書の拡張設定(-x509
が与えられた場合)または署名要求に含める代替のセクションを指定する。これによって、さまざまな目的に応じて、同じ設定ファイル内の異るセクションを使用することができる。
設定ファイルを用途別に作り分ける方法がよく紹介されていますが、その理由の多くは拡張セクションのオプションが用途によって異なるためです。
しかし上記オプションの説明に書かれているように、1つのファイル内に用途別の拡張セクションをいくつか用意して指定すれば、設定ファイルを1つにすることができそうです。
今回はデフォルトの設定ファイルにCA用として用意されている[ v3_ca ]
セクションを-extensions
オプションで指定します。この中でbasicConstraints = CA:true
の拡張設定が含まれているので、CA用として署名に使用できる証明書になるはずです。
CATOP=/etc/pki/CA
# CAのシリアル番号とコモンネームを1件ずつ読み込む
while read serial_num common_name
do
# CAごとに出力用のサブディレクトリを作成する
mkdir -p ${CATOP}/${common_name}/private
# 秘密鍵を生成して自己署名証明書を発行する
openss req -new \
-keyout ${CATOP}/${common_name}/private/cakey.pem \
-out ${CATOP}/${common_name}/cacert.pem \
-passout file:${CATOP}/.capass \
-subj "/C=JP/ST=Chiba/O=myorg/CN=${common_name}" \
-x509 -days 1095 -set_serial ${serial_num} \
-extensions v3_ca # 拡張セクションを指定する
# 秘密鍵のパーミッションを設定する
chmod 0400 ${CATOP}/${common_name}/private/cakey.pem
done <${CATOP}/ca-list
ついでに秘密鍵のパーミッション設定処理も追加しました。
作成したスクリプトでCAを作成する
スクリプトを実行して、秘密鍵の生成から自己署名証明書の発行までを行います。
bash newca.sh
Generating a 2048 bit RSA private key
.............................+++
.................................................................+++
writing new private key to '/etc/pki/CA/ca1.mydomain/private/cakey.pem'
-----
スクリプトが正常に終了すると、以下のようにサブディレクトリごとに秘密鍵と証明書のファイルが出力されます。
/etc/pki/CA/
├── ca1.mydomain
│ ├── cacert.pem
│ └── private
│ └── cakey.pem
├── ca2.mydomain
│ ├── cacert.pem
│ └── private
│ └── cakey.pem
証明書の内容を確認する
以下のようにx509
コマンドを使って証明書の内容を確認します。
openssl x509 -in /etc/pki/CA/ca1.mydomain/cacert.pem -text
問題がなければ以下のように表示されます。
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1 (0x1)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=JP, ST=Chiba, O=myorg, CN=ca1.mydomain
Validity
Not Before: Aug 3 07:25:17 2019 GMT
Not After : Aug 2 07:25:17 2022 GMT
Subject: C=JP, ST=Chiba, O=myorg, CN=ca1.mydomain
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
(略)
X509v3 extensions:
(略)
X509v3 Basic Constraints:
CA:TRUE
(略)
以下の設定値が期待したとおりであることが確認できました。
-
Issuer:
とSubject:
の両方が、-subj
オプションで指定した識別名/コモンネームになっている -
Not After :
(有効期限)が、-days
オプションで指定した日数経過後の日付になっている -
X509v3 Basic Constraints:
が、-extensions
オプションで指定した設定ファイルのセクション内のbasicConstraints = CA:true
で指定されたとおりになっている -
Signature Algorithm:
のハッシュアルゴリズムが、設定ファイルの[ req ]
セクション内のdefault_md = sha256
で指定されたとおりになっている -
Public-Key:
の鍵長が、設定ファイルの[ req ]
セクション内のdefault_bits = 2048
で指定されたとおりになっている
秘密鍵の内容を確認する
次に、生成された秘密鍵の内容をrsa
コマンドを使って確認します。
openssl rsa -in /etc/pki/CA/ca1.mydomain/private/cakey.pem -text
以下のようにパスフレーズの入力を求められたら、設定したパスフレーズを入力します。今回の例では、ファイル/etc/pki/CA/.capass
で設定したmycap@ss
を入力します。
Enter pass phrase for /etc/pki/CA/ca1.mydomain/private/cakey.pem:
これで秘密鍵の内容が表示されれば、パスフレーズが意図したとおりに設定されています。
Private-Key: (2048 bit)
modulus:
(略)
publicExponent: 65537 (0x10001)
privateExponent:
(略)
prime1:
(略)
prime2:
(略)
exponent1:
(略)
exponent2:
(略)
coefficient:
(略)
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
(略)
-----END RSA PRIVATE KEY-----
またPrivate-Key: (2048 bit)
から、鍵長が設定ファイルの[ req ]
セクション内のdefault_bits = 2048
で指定されたとおりになっていることが確認できます。
今後の予定
-
設定ファイルの各オプションを精査する
- 設定ファイル 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
-
作成したCAでサーバー/クライアント証明書を発行し、OpenVPNでの動作を確認する
- OpenVPN コミュニティ Wiki: https://community.openvpn.net/openvpn
記事一覧
今度こそopensslコマンドを理解して使いたい (1) ルートCAをスクリプトで作成する
今度こそopensslコマンドを理解して使いたい (2) 設定ファイル(openssl.cnf)を理解する
今度こそopensslコマンドを理解して使いたい (3) CA証明書の拡張設定を検証する
今度こそopensslコマンドを理解して使いたい (4) サーバー/クライアント証明書を一括生成する
今度こそopensslコマンドを理解して使いたい (5) CRL(証明書失効リスト)を作成してOpenVPNに配布する
今度こそopensslコマンドを理解して使いたい (補足1) サンプルスクリプトのまとめ