Skip to content
Published on

TLS証明書完全ガイド: opensslコマンドで最後まで扱う

シェア
Authors

はじめに

証明書の問題はいつも急ぎです。更新を逃してサービスが止まったり、新しく設置したのに一部のクライアントだけ失敗したり、ファイル形式が合わずロードできなかったりします。そのとき必要なのはTLSハンドシェイクの理論ではなく、いまこのファイルが何で、何が欠けているのかを確認するコマンドです。

このブログにはSSL/TLS証明書完全ガイドがすでにあります。あの記事はLet's Encryptの発行、Nginxの設定、自動更新といった発行と運用の流れを扱います。この記事は別のことを目指します。openssl コマンドそのもののリファレンスです。証明書ファイルを目の前にして何を確認すべきか、どのコマンドがどの事実を教えてくれるかを目的別に整理します。発行方法ではなく診断方法がテーマです。

この記事のコマンドはすべて読み取りと診断が中心です。証明書を読み違えて生じる被害はありませんが、秘密鍵を扱うコマンドは違います。鍵ファイルを画面に出力したり誤ったパーミッションで保存したりすれば、その瞬間に漏洩です。そのためこの記事は可能な限り鍵の内容を出力せずダイジェストだけを比較する方式を基本とします。

基準はOpenSSL 3.xです。OpenSSL 1.1.1と3.xは一部のオプションとデフォルト動作が異なります。またmacOSの標準環境や一部のディストリビューションはLibreSSLを使っており、この場合オプションが異なることがあります。実行前にバージョンを確認してください。

openssl version -a

1. ファイル形式の見分け — 何を受け取ったかの確認から

証明書関連の事故の相当数は形式の取り違えから始まります。拡張子は参考にすぎず、内容を保証しません。

形式内容よくある拡張子
PEMBase64テキスト。ヘッダ行がある.pem .crt .cer .key
DERバイナリエンコーディング.der .cer
PKCS#12証明書と秘密鍵を1ファイルに.p12 .pfx
PKCS#7証明書の束(鍵なし).p7b .p7c
PKCS#8秘密鍵の標準形式.key .pem

まずファイルがテキストかバイナリかを見ます。

file server.crt
head -1 server.crt

PEMなら先頭行に -----BEGIN で始まる表示があります。その表示が何かがそのまま内容です。

  • BEGIN CERTIFICATE: 証明書
  • BEGIN CERTIFICATE REQUEST: CSR
  • BEGIN PRIVATE KEY: PKCS#8秘密鍵
  • BEGIN RSA PRIVATE KEY: 伝統的なRSA秘密鍵
  • BEGIN ENCRYPTED PRIVATE KEY: 暗号化された秘密鍵

形式変換は次のとおりです。

openssl x509 -in server.der -inform DER -out server.pem -outform PEM
openssl x509 -in server.pem -outform DER -out server.der
openssl pkcs12 -in bundle.pfx -nodes -out bundle.pem
openssl pkcs12 -export -inkey server.key -in server.crt -certfile chain.crt -out bundle.pfx
openssl pkcs7 -print_certs -in chain.p7b -out chain.pem

-inform-outform は入力・出力のエンコーディングを指定します。PKCS#12変換での -nodes は秘密鍵を暗号化せずに出力するという意味なので、結果ファイルのパーミッションを直ちに制限しなければなりません。

chmod 600 bundle.pem

形式の取り違えが実際にどう事故につながるか、1つだけ例を挙げるとこうです。Windowsサーバーからエクスポートした .pfx ファイルを受け取ってNginxにそのまま指定すると、サーバーが起動しません。NginxはPEM形式の証明書と鍵をそれぞれ要求するからです。逆にJavaアプリケーションにPEMのペアをそのまま渡すと、keystore形式ではないと拒否されます。ファイルを受け取ったら拡張子を信じず内容を先に確認することが30分を節約してくれます。

PKCS#12の束から必要な部分だけを取り出すこともできます。証明書だけ、あるいは鍵だけを分離しなければならない状況はよく起きます。

openssl pkcs12 -in bundle.pfx -clcerts -nokeys -out server.crt
openssl pkcs12 -in bundle.pfx -cacerts -nokeys -out chain.crt
openssl pkcs12 -in bundle.pfx -nocerts -nodes -out server.key

-clcerts はクライアント(エンドエンティティ)証明書だけ、-cacerts はCA証明書だけ、-nocerts は証明書を除いた鍵だけを出力します。


2. 証明書を読む — このファイルが何かを確認

最もよく使うコマンドから見ます。

openssl x509 -in server.crt -noout -text
openssl x509 -in server.crt -noout -subject -issuer -dates
openssl x509 -in server.crt -noout -serial -fingerprint -sha256
openssl x509 -in server.crt -noout -ext subjectAltName

オプションの意味はドキュメント基準で次のとおりです。

  • -noout はエンコードされた元の出力を抑制し、要求した項目だけを見せます。
  • -text は証明書全体を人が読める形で出力します。
  • -subject-issuer はサブジェクトと発行者、-dates は有効期間の開始・終了時刻を見せます。-startdate-enddate でそれぞれ見ることもできます。
  • -fingerprint はDERエンコード版のダイジェストを計算します。
  • -ext は指定したX.509拡張を出力します。

実務で最も頻繁に確認すべきものはSAN(Subject Alternative Name)です。現代のブラウザとライブラリはCNを見ずSANだけを見ます。CNにドメインがあってもSANになければ失敗します。

openssl x509 -in server.crt -noout -ext subjectAltName

期限切れの判定は秒単位で行えます。ドキュメントによれば -checkend は指定した秒以内に期限が切れるかを確認します。

openssl x509 -in server.crt -noout -checkend 0
openssl x509 -in server.crt -noout -checkend 2592000
echo "exit=$?"

終了コードが0なら、その期間内に期限が切れないという意味です。30日(2592000秒)を入れておけば、更新通知スクリプトをそのまま作れます。

証明書本文で実際に確認する価値のある項目は思ったより少ないです。有効期間とSANが8割で、残りは問題が起きたときだけ見ます。-text の出力で注目すべきものを挙げると次のとおりです。

  • Signature Algorithm: 署名アルゴリズムです。古いSHA-1署名は最新のクライアントが拒否します。
  • Public Key Algorithmと鍵長: RSA 1024ビットのような短い鍵は拒否されます。
  • Basic Constraints: CAかどうかです。サーバー証明書なのにCAと表示されていれば誤発行です。
  • Key UsageとExtended Key Usage: サーバー認証の用途が含まれていなければなりません。クライアント認証用に発行された証明書をサーバーに設置すると失敗します。
  • Authority Information Access: 発行者証明書とOCSPレスポンダのアドレスが入っています。

これらの項目は、新しいCAを導入したり発行要求のフォームを変えたときに一度は確認しておくとよいでしょう。

#!/usr/bin/env bash
set -uo pipefail
for CRT in /etc/pki/tls/certs/*.crt; do
  if ! openssl x509 -in "$CRT" -noout -checkend 2592000 >/dev/null 2>&1; then
    echo "EXPIRING SOON: $CRT"
    openssl x509 -in "$CRT" -noout -subject -enddate
  fi
done

3. 鍵と証明書がペアかを確認

「証明書を替えたらサーバーが起動しない」の半分は、鍵と証明書がペアでないことが原因です。判別方法は公開鍵部分を比較することです。

RSA鍵ならmodulusを比較します。

openssl x509 -in server.crt -noout -modulus | openssl sha256
openssl rsa -in server.key -noout -modulus | openssl sha256
openssl req -in server.csr -noout -modulus | openssl sha256

3つの値が同じなら、CSR、証明書、鍵はすべて一組です。異なればペアではありません。

RSAではない鍵(ECDSA、Ed25519)には -modulus がありません。この場合は公開鍵自体を取り出して比較します。

openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256

この方法は鍵の種類に関係なく動作するので、覚えるコマンドを1つだけ選ぶならこちらが優れています

秘密鍵自体の情報は次で見ます。

openssl pkey -in server.key -noout -text
openssl rsa -in server.key -check -noout

破壊的コマンドの警告の代わりに漏洩注意: 秘密鍵を出力するコマンドは、ターミナルの履歴と画面共有にそのまま残ります。鍵の内容全体を出力する代わりに、上のようにダイジェストだけを比較する習慣をつけてください。


4. CSRを作る — SANを必ず入れる

CSR生成で最もよくある間違いはSANを入れないことです。設定ファイルを使うほうが確実です。

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext

[dn]
C = KR
ST = Seoul
O = Example Corp
CN = www.example.com

[req_ext]
subjectAltName = @alt_names

[alt_names]
DNS.1 = www.example.com
DNS.2 = example.com
DNS.3 = api.example.com

このファイルを csr.cnf として保存したあと生成します。

openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr -config csr.cnf
openssl req -in server.csr -noout -text
openssl req -in server.csr -noout -verify

ECDSA鍵を使う場合です。

openssl ecparam -name prime256v1 -genkey -noout -out server-ec.key
openssl req -new -key server-ec.key -out server-ec.csr -config csr.cnf

生成後は必ずSANが入ったかを確認してください。ここで見逃すと発行をやり直すことになります。

openssl req -in server.csr -noout -text | grep -A3 'Subject Alternative Name'

内部テスト用のプライベートCAとサーバー証明書は次のように作ります。

openssl req -x509 -new -nodes -newkey rsa:4096 -sha256 -days 3650 \
  -subj '/C=KR/O=Example Internal/CN=Example Internal Root CA' \
  -keyout ca.key -out ca.crt

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt -days 397 -sha256 -extfile csr.cnf -extensions req_ext

-CAcreateserial はシリアル番号ファイルがなければ作成します。-extfile-extensions を指定しないと、SANが署名結果に含まれません。プライベートCAで作った証明書がブラウザで失敗する最もよくある原因です。


5. チェーン検証 — 中間証明書の欠落を捕まえる

「自分のブラウザでは通るのにサーバーからcurlでは通らない」の標準的な原因は中間証明書の欠落です。ブラウザはキャッシュやAIA情報で中間証明書を補いますが、コマンドラインツールやサーバー間の通信はそうではありません。

ローカルファイルで検証します。

openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile root.crt -untrusted intermediate.crt server.crt
openssl verify -show_chain -CAfile /etc/pki/tls/certs/ca-bundle.crt server.crt
  • -CAfile は信頼アンカー(ルート)を指定します。
  • -untrusted は中間証明書を提供します。
  • -show_chain は構成されたチェーンを見せます。

システム信頼ストアのパスはディストリビューションごとに異なります。RHEL系/etc/pki/tls/certs/ca-bundle.crtDebian/Ubuntu系/etc/ssl/certs/ca-certificates.crt です。

証明書ファイルの中に何枚入っているかを数えるのも有用です。

grep -c 'BEGIN CERTIFICATE' fullchain.pem

サーバーに設置するファイルは通常、サーバー証明書の次に中間証明書という順で連結します。ルートは含めないのが慣例です。順序が入れ替わると一部のクライアントが失敗します。

cat server.crt intermediate.crt > fullchain.pem
openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout

最後のコマンドは束ファイル内の各証明書のsubjectとissuerを列挙してくれます。前の証明書のissuerが次の証明書のsubjectと一致するかを確認すれば、順序が正しいかがわかります。

チェーン構成で混乱しやすい点を整理するとこうです。サーバーが送るべきものは自分の証明書と、ルートに到達するまでに必要な中間証明書すべてです。ルート証明書はクライアントがすでに信頼ストアに持っているので送る必要はなく、送ってもたいてい無視されます。ただし送るとハンドシェイクのたびに不要なバイトが行き来するので、慣例的に除外します。

中間証明書が複数枚の場合もあります。最近の商用CAは2段階の中間構造を使うことがあり、発行メールに添付された束をそのまま使わず自分で組み立てて1枚を落とす事故が起きます。組み立てたなら必ず検証まで行ってください。

openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt -untrusted intermediate.crt server.crt

このコマンドが OK を出力すれば、ローカル信頼ストアの基準でチェーンが完成しています。失敗すれば、どの段階で切れたかがエラーメッセージに出ます。


6. サーバーに接続して診断する — s_client

実際のサーバーが何を送るかを確認するコマンドです。

openssl s_client -connect example.com:443 -servername example.com
openssl s_client -connect example.com:443 -servername example.com -showcerts
openssl s_client -connect example.com:443 -servername example.com -brief
openssl s_client -connect example.com:443 -servername example.com -tls1_2
openssl s_client -connect example.com:443 -servername example.com -status
openssl s_client -connect smtp.example.com:587 -starttls smtp

オプションの意味はドキュメント基準です。

  • -connect は接続先です。
  • -servername はClientHelloにSNIを入れます。バーチャルホスティング環境でこれがないと見当違いのデフォルト証明書を受け取ります。診断するときは必ず入れてください。
  • -showcerts はサーバーが送った証明書のリストをそのまま見せます。ドキュメントが明示するとおり、これは検証済みのチェーンではなくサーバーが送ったそのままです。だから中間証明書の欠落を確認するのにぴったりです。
  • -status はOCSPステープリングの応答を要求します。
  • -brief は接続の要約だけを出力します。
  • -starttls はプロトコル別の切り替えメッセージを送ります。smtp、imap、pop3、ftp、postgres、mysql、ldapなどに対応します。

コマンドが入力を待って止まるので、スクリプトでは標準入力を閉じます。

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates

この1行がリモート証明書の期限確認の標準的な定型句です。バッチで複数ホストを点検するときもそのまま使います。

for H in www.example.com api.example.com admin.example.com; do
  printf '%s ' "$H"
  echo | openssl s_client -connect "$H:443" -servername "$H" 2>/dev/null \
    | openssl x509 -noout -enddate
done

チェーン検証の結果は出力の前半にあるverify関連の行で確認します。unable to get local issuer certificate が見えたら、中間証明書が抜けているか、ローカル信頼ストアにルートがないかのどちらかです。


7. よく遭遇するエラーと判別の順序

症状確認コマンド原因の候補
ブラウザは通りcurlは失敗s_client -showcerts中間証明書の欠落
特定のドメインだけ失敗x509 -ext subjectAltNameSANの欠落
サーバー起動失敗、鍵エラーmodulusまたはpubkeyのダイジェスト比較鍵と証明書の不一致
更新したのに古い証明書が見えるs_client -servername再読み込み漏れ、別のバーチャルホスト
古いクライアントだけ失敗s_client -tls1_2プロトコル・暗号スイートの不一致
プライベートCA証明書が拒否されるverify -CAfile信頼ストアにCA未登録
期限直前の通知が来ないx509 -checkend監視の不在

判別の順序はいつも同じです。第一に、ファイルが何かを確認します。第二に、鍵とペアかを確認します。第三に、チェーンが完全かを確認します。第四に、サーバーが実際に何を送るかを確認します。この順序を守れば、たいてい2番目か3番目で原因が出てきます。

プライベートCAをシステム信頼ストアに登録する方法もディストリビューションごとに異なります。

# RHEL系
sudo cp internal-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract

# Debian系
sudo cp internal-ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates

注意: ファイルの拡張子について、ディストリビューションごとに要求される形が異なります。Debian系は .crt 拡張子とPEM形式を要求します。登録後は実際に信頼されるかを確認してください。

openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt
curl -sSI https://internal.example.com | head -1

更新後に反映されない問題もよくあります。ファイルだけを替えてサービスを再読み込みしないと、プロセスがメモリに載せた古い証明書を使い続けます。ファイルの時刻と実際に提供中の証明書を突き合わせれば即座に判別できます。

ls -l --time-style=long-iso /etc/pki/tls/certs/server.crt
openssl x509 -in /etc/pki/tls/certs/server.crt -noout -enddate
echo | openssl s_client -connect localhost:443 -servername www.example.com 2>/dev/null | openssl x509 -noout -enddate

ファイルの有効期限とサーバーが実際に提供する証明書の有効期限が異なれば、再読み込みが漏れています。自動更新を設定しているなら、更新フックにサービスの再読み込みまで含まれているかを必ず確認してください。更新は成功したのに反映されず、期限切れで障害になる事例は実際に少なくありません。

もう1つ忘れられがちなのが、アプリケーションごとに信頼ストアが異なるという事実です。Javaは独自のkeystoreを、Pythonはcertifiバンドルを、Node.jsは内蔵リストを使います。システムにCAを登録したのに特定のアプリケーションだけ失敗するなら、そのランタイムの信頼ストアを別途確認しなければなりません。


クイズ: 理解度を確認しましょう

クイズ1: ブラウザでは正常なのにサーバー間のAPI呼び出しだけ証明書エラーになります。何を先に確認しますか?

正解: サーバーが中間証明書も一緒に送っているかを確認します

解説: ブラウザはキャッシュやAIA情報で中間証明書を補いますが、コマンドラインツールと大半の言語ランタイムはそうではありません。

echo | openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'

1が出ればサーバー証明書だけを送っているので、中間証明書が抜けています。サーバーに設置するファイルを、サーバー証明書の次に中間証明書という順で連結して設置し直してください。

クイズ2: 証明書と秘密鍵が一組かを確認する、鍵の種類に依存しない方法は?

正解: 両方から公開鍵を取り出してダイジェストを比較します

解説: -modulus の比較はRSAにしか通用しません。ECDSAやEd25519では次の方法を使います。

openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256

2つの値が同じなら一組です。この方法は鍵の種類を選ばないので、1つだけ覚えるならこちらです。秘密鍵の内容そのものを画面に出力しないという点でも安全です。

クイズ3: プライベートCAで作ったサーバー証明書が、ブラウザでドメイン不一致として拒否されます。何を落としたのでしょうか?

正解: 署名時にSAN拡張を含めませんでした

解説: CSRにSANを入れていても、openssl x509 -req で署名するときに拡張を明示しなければ、結果の証明書にSANは入りません。

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt -days 397 -sha256 -extfile csr.cnf -extensions req_ext

署名後は必ず確認してください。

openssl x509 -in server.crt -noout -ext subjectAltName

現代のクライアントはCNを見ずSANだけを見ます。

クイズ4: 複数のドメインが1つのIPにあります。s_clientで特定のドメインの証明書を確認するには?

正解: -servername でSNIを指定します

解説: SNIを与えないとサーバーがデフォルトのバーチャルホストの証明書を返すため、確認したかったドメインの証明書ではないものを見ることになります。

echo | openssl s_client -connect 203.0.113.10:443 -servername api.example.com 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName

ドキュメントによれば -servername を省略すると -connect のホスト名がデフォルトとして使われるので、IPで直接接続するときは必ず明示しなければなりません。

クイズ5: 30日以内に期限が切れる証明書を自動的に見つけ出すには?

正解: -checkend に秒単位の値を与え、終了コードで判定します

解説: ドキュメントによれば -checkend は指定した秒以内に証明書の期限が切れるかを確認します。30日は2592000秒です。

for CRT in /etc/pki/tls/certs/*.crt; do
  openssl x509 -in "$CRT" -noout -checkend 2592000 >/dev/null 2>&1 \
    || echo "EXPIRING: $CRT"
done

リモートサーバーも同じ方式で点検できます。

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 2592000
クイズ6: システム信頼ストアに社内CAを登録したのに、Javaアプリケーションだけ依然として失敗します。なぜでしょうか?

正解: アプリケーションランタイムが独自の信頼ストアを使うためです

解説: Javaは独自のkeystoreを、Pythonはcertifiバンドルを、Node.jsは内蔵CAリストを使用します。OSの信頼ストアを更新しても、そのランタイムには反映されません。

まずシステムレベルでは正常かを確認します。

openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt
curl -sSI https://internal.example.com | head -1

curl は通るのにアプリケーションだけ失敗するなら、原因が確定します。各ランタイムにCAを登録する正確な手順は、該当ランタイムのドキュメントで確認してください。


おわりに

証明書の診断は順序さえ守れば難しくありません。ファイルが何か、鍵とペアか、チェーンが完全か、サーバーが実際に何を送るか。この4つを順に確認すれば原因が明らかになります。

そして急ぎのときに思い出せないコマンドは役に立ちません。次の3行だけは覚えておいてください。

# 1. ローカルファイルの確認
openssl x509 -in server.crt -noout -text

# 2. リモートサーバーの確認
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates

# 3. 鍵と証明書のペア確認
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256

この3ブロックが証明書関連の作業の90パーセントをカバーします。

最後に、期限切れによる障害は100パーセント予防可能な事故です。-checkend の1行で作る点検スクリプトを今日中に仕掛けておいてください。


参考資料


関連記事