サーバー証明書が、信頼するルートCAまで正しくたどれるかを確認したいときは、openssl verifyを使う。

$ openssl verify -CAfile root.crt -untrusted inter.crt leaf.crt
leaf.crt: OK

それぞれのオプションの意味は次のとおり。

オプション意味
-CAfile信頼するルートCAの証明書
-untrustedチェーンの途中に使う証明書(中間CAの証明書)
最後の引数検証するサーバー証明書

証明書は、サーバー証明書(leaf.crt)、中間CA(inter.crt)、ルートCA(root.crt)の3階層とする。実行例はUbuntu 24.04のOpenSSL 3.0.13のもの。

検証に使う証明書を用意する

実行例で使う3階層の証明書は、次のコマンドで作った。

# ルートCA
$ openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
    -keyout root.key -out root.crt -days 3650 -subj "/CN=Test Root CA" \
    -addext "basicConstraints=critical,CA:TRUE" \
    -addext "keyUsage=critical,keyCertSign,cRLSign"

# 中間CA
$ openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
    -keyout inter.key -out inter.csr -subj "/CN=Test Intermediate CA"
$ printf 'basicConstraints=critical,CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign\n' > inter.ext
$ openssl x509 -req -in inter.csr -CA root.crt -CAkey root.key -CAcreateserial \
    -out inter.crt -days 1825 -extfile inter.ext

# サーバー証明書
$ openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
    -keyout leaf.key -out leaf.csr -subj "/CN=leaf.example.test"
$ printf 'subjectAltName=DNS:leaf.example.test\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth\n' > leaf.ext
$ openssl x509 -req -in leaf.csr -CA inter.crt -CAkey inter.key -CAcreateserial \
    -out leaf.crt -days 365 -extfile leaf.ext

参考: 【openssl】自分用の認証局で証明書に署名する (x509 -req)

チェーンをたどれるか確認する

-show_chainを付けると、検証で使った証明書の階層を表示する。

$ openssl verify -show_chain -CAfile root.crt -untrusted inter.crt leaf.crt
leaf.crt: OK
Chain:
depth=0: CN = leaf.example.test (untrusted)
depth=1: CN = Test Intermediate CA (untrusted)
depth=2: CN = Test Root CA

depth=0が検証対象のサーバー証明書で、数字が大きいほど上位のCAである。(untrusted)は-untrustedで渡した証明書を表す。信頼の起点は-CAfileで渡したdepth=2だけである。

検証に失敗した場合は、終了コードが2になる。スクリプトで成否を判定できる。

よくあるエラー

unable to get local issuer certificate

中間CAの証明書を渡さないと、サーバー証明書の発行者が見つからず失敗する。error 20 at 0 depth lookupの0は、サーバー証明書で失敗したことを表す。

$ openssl verify -CAfile root.crt leaf.crt
CN = leaf.example.test
error 20 at 0 depth lookup: unable to get local issuer certificate
error leaf.crt: verification failed

-untrustedで中間CAの証明書を渡すと解決する。サーバーが中間CAの証明書を送らない構成では、ブラウザーでは接続できても、curlなどで失敗する場合がある。

同じエラーは、ルートCAの証明書を渡さない場合にも出る。depthの値が違う点で区別できる。

$ openssl verify -untrusted inter.crt leaf.crt
CN = Test Intermediate CA
error 20 at 1 depth lookup: unable to get local issuer certificate
error leaf.crt: verification failed

1 depthは、中間CAの発行者(ルートCA)が見つからないことを表す。

self-signed certificate

自己署名の証明書を、信頼するCAとして渡さないと失敗する。

$ openssl verify ss.crt
CN = ss
error 18 at 0 depth lookup: self-signed certificate
error ss.crt: verification failed

自己署名の証明書自身を-CAfileに渡すと成功する。

$ openssl verify -CAfile ss.crt ss.crt
ss.crt: OK

certificate has expired

有効期限が切れている証明書は、エラーになる。次の例は、2年後の時刻を仮定して実行した結果である。

$ openssl verify -CAfile root.crt -untrusted inter.crt leaf.crt
CN = leaf.example.test
error 10 at 0 depth lookup: certificate has expired
error leaf.crt: verification failed

-attimeに、UNIX時間(秒)で検証時刻を指定すると、期限切れの確認や、将来の期限切れを事前に検査できる。

$ openssl verify -attime $(date -d '+2 years' +%s) -CAfile root.crt -untrusted inter.crt leaf.crt
CN = leaf.example.test
error 10 at 0 depth lookup: certificate has expired
error leaf.crt: verification failed

date -dはGNU版のコマンドである。macOSではdate -v+2y +%sを使う。

有効期間の開始前の証明書は、certificate is not yet validになる。チェーンのどの階層で失敗したかは、at N depthで分かる。

参考: 【openssl】証明書ファイルの有効期限を確認する

unsuitable certificate purpose

-purposeで用途を指定して検証できる。サーバー証明書がserverAuthだけを許可している場合、クライアント証明書としての検証は失敗する。

$ openssl verify -CAfile root.crt -untrusted inter.crt -purpose sslclient leaf.crt
error 26 at 0 depth lookup: unsuitable certificate purpose
error leaf.crt: verification failed

サーバー証明書として検証するには-purpose sslserverを指定する。

$ openssl verify -CAfile root.crt -untrusted inter.crt -purpose sslserver leaf.crt
leaf.crt: OK

hostname mismatch

-verify_hostnameでホスト名を検証できる。SANに含まれていないホスト名は失敗する。

$ openssl verify -CAfile root.crt -untrusted inter.crt -verify_hostname leaf.example.test leaf.crt
leaf.crt: OK
$ openssl verify -CAfile root.crt -untrusted inter.crt -verify_hostname other.test leaf.crt
error 62 at 0 depth lookup: hostname mismatch
error leaf.crt: verification failed

path length constraint exceeded

中間CAのbasicConstraintsにpathlen:0があると、その中間CAの配下にさらにCAを作れない。下位のCAを作った場合は、検証に失敗する。

CN = Test Intermediate CA
error 25 at 2 depth lookup: path length constraint exceeded
error l2.crt: verification failed

中間CAの渡し方を選ぶ

中間CAの証明書は、-untrusted以外に、-CAfileへまとめて渡す方法もある。

$ cat root.crt inter.crt > bundle.pem
$ openssl verify -CAfile bundle.pem leaf.crt
leaf.crt: OK

この方法では、中間CAも信頼の起点として扱われる。検証の意図が変わるため、チェーンの構成を確認したい場合は-untrustedを使う。

サーバーから受け取った証明書が1つのファイルにまとまっている場合は、そのファイルを-untrustedに渡せる。

$ openssl verify -CAfile root.crt -untrusted leafchain.pem leaf.crt
leaf.crt: OK

中間CAを信頼の起点にする

-partial_chainを付けると、ルートCAではなく中間CAの証明書を信頼の起点にして検証できる。

$ openssl verify -CAfile inter.crt leaf.crt
CN = Test Intermediate CA
error 2 at 1 depth lookup: unable to get issuer certificate
error leaf.crt: verification failed
$ openssl verify -partial_chain -CAfile inter.crt leaf.crt
leaf.crt: OK

システムの信頼済みCAで検証する

-CAfileを省略すると、OSの信頼済みCAを使う。自分用のCAが含まれていない場合は、unable to get local issuer certificateになる。

$ openssl verify leaf.crt
error 20 at 0 depth lookup: unable to get local issuer certificate
error leaf.crt: verification failed

公開サーバーの証明書を検証する場合は、openssl s_clientで取得した証明書を渡す。

参考: 【openssl】指定したURLの証明書をコマンドで確認する

バージョンによる違い

OpenSSL 3.6でも、ここまでの例は同じように動く。

macOS標準の/usr/bin/openssl(LibreSSL 3.3.6)では、以下が異なる。

  • -show_chain、-partial_chain、-verify_hostnameに対応していない
  • エラー表示がerror 20 at 0 depth lookup:unable to get local issuer certificateのように、コロンの後に空白がない
  • 最後にleaf.crt: verification failed: 20 (unable to get local issuer certificate)の行が出る
  • 用途のエラーはunsupported certificate purposeになる

ホスト名の検証やチェーンの確認が必要な場合は、Homebrewなどでインストールしたopensslを使う。

参考

openssl-verify - OpenSSL Documentation