自分で発行した証明書を置いたサイトをブラウザで開くと、ページの代わりに警告が出ます。通信は暗号化されているのに、拒まれます。
前回からの持ち越しも、これと地続きです。署名は完璧に検証できるのに、検証に使う公開鍵が本当にアリスのものかは確かめようがない。錠前は配れるようになったのに、配られた錠前の持ち主を保証するものがありません。どちらの場面でも足りないのは同じで、鍵の持ち主を保証する仕組みです。今日はそれを作ります。
OpenSSL で手を動かして学ぶ暗号の基礎、第5回は証明書と PKI です。環境の準備は第0回、持ち越した問題の経緯は第4回からどうぞ。
コマンドを打つ前に、証明書の正体を2コマにしておきます。
公開鍵と持ち主を1枚に束ねて、第三者(認証局)が署名する。それが証明書。
形式は正しい。ただ、保証人を誰も知らない。
証明書は、公開鍵に付けた身分証
答えの構造は単純です。公開鍵と持ち主の情報をひとつのファイルに束ね、そこに信頼できる第三者が署名する。署名は前回作った道具そのままです。このファイルを証明書と呼び、署名する第三者を認証局(CA)と呼びます。
「この公開鍵は localhost のものです。以上を保証します。認証局より」という一枚の書類。今日はまずこれを自分で発行します。保証人も自分で務めます。その歪みがどう露見するかまで含めて、ハンズオンです。
openssl req で、1行で発行する
openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt \
-days 365 -nodes -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"
req は証明書まわりの要求を作るコマンドで、-x509 を付けると、要求の代わりに証明書そのものを出力します。-newkey rsa:2048 で第3回と同じ RSA 鍵ペアも同時に作り、-nodes は秘密鍵をパスフレーズなしで保存する指定です(サーバーが無人で再起動できるように。そのぶん保管は厳重に)。-subj が持ち主の名乗り、-addext の SAN は後述します。-days 365 で有効期間は1年です。
server.key(秘密鍵)と server.crt(証明書)ができました。
openssl x509 -text で中身を読む
証明書はただのファイルなので、開いて読めます。
openssl x509 -in server.crt -text -noout
# Certificate:
# Data:
# Version: 3 (0x2)
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: CN=localhost
# Validity
# Not Before: Aug 18 16:39:26 2026 GMT
# Not After : Aug 18 16:39:26 2027 GMT
# Subject: CN=localhost
# Subject Public Key Info:
# Public Key Algorithm: rsaEncryption
# Public-Key: (2048 bit)
# Modulus:
# 00:a7:4f:84:3b:dd:fb:b3:69:ff:ef:51:37:a9:11:
# (中略)
# X509v3 extensions:
# X509v3 Subject Alternative Name:
# DNS:localhost
指差し確認をします。Subject が持ち主(localhost)、Validity が有効期間、Subject Public Key Info の中に第3回で見たのと同じ形の公開鍵が丸ごと入っています。末尾には Signature Algorithm と署名本体が付いていて、これが前回の dgst -sign と同じ仕組みで作られた保証印です。
問題は Issuer、つまり保証人の欄です。Issuer も CN=localhost。持ち主と保証人が同一人物です。自分で書いた身分証に自分で判を押した、いわゆる自己署名証明書(オレオレ証明書)です。
自己署名だと、ブラウザに警告される
この証明書で HTTPS サーバーを立ててみます。openssl には検証用の簡易サーバーが入っています。
openssl s_server -key server.key -cert server.crt -www -accept 4443
ブラウザで https://localhost:4443 を開くと、ページの代わりに全画面の警告が出ます。「この接続ではプライバシーが保護されません」といった文言で、続行には小さなリンクを探して踏む必要があります。curl も同じ判断をします。
curl https://localhost:4443
# curl: (60) SSL certificate problem: self signed certificate
暗号化そのものは正常に動いています。それでも拒まれるのは、ブラウザや curl が持っている信頼できる保証人の名簿に CN=localhost がいないからです。身分証の形式は正しい。ただ、保証人を誰も知らない。警告が言っているのは「相手が名乗りどおりだと確認できない」であって、「通信が平文だ」ではありません。
警告の意味は「通信が平文だ」ではなく「相手が名乗りどおりだと確認できない」。
本物のサイトは、認証局の鎖で保証されている
比較のために、実在のサイトの証明書を覗きます。相手は当ブログです。
echo | openssl s_client -connect torigoedesign.com:443 2>/dev/null | head -12
# Certificate chain
# 0 s:CN=www.torigoedesign.com
# i:C=US, O=Let's Encrypt, CN=YR2
# v:NotBefore: Aug 4 21:19:56 2026 GMT; NotAfter: Nov 2 21:19:55 2026 GMT
# 1 s:C=US, O=Let's Encrypt, CN=YR2
# i:C=US, O=ISRG, CN=Root YR
s が Subject(持ち主)、i が Issuer(保証人)です。今度は別人になっています。このサイトの証明書は Let’s Encrypt という認証局が保証し、その Let’s Encrypt の証明書はさらに上位の ISRG Root が保証しています。身分証の保証人にも身分証があり、たどると根元(ルート CA)に着く。この鎖が PKI(公開鍵基盤)です。
保証人にも身分証があり、たどると根元(ルート CA)に着く。根元が名簿にいれば、鍵マーク。
では根元は誰が保証するのか。誰もしません。ルート CA の証明書は自己署名です。代わりに、OS とブラウザが厳格な審査を経たルート証明書を最初から同梱しています。ブラウザが警告を出さないのは、鎖をたどった先が同梱の名簿にいるときだけです。オレオレ証明書は、この名簿への登録を「自分を信じてくれ」で済ませようとしたから拒まれました。
echo | openssl s_client -connect torigoedesign.com:443 2>/dev/null | grep "Verify return"
# Verify return code: 0 (ok)
注意点
SAN を省くと、正しくても拒まれる
発行コマンドの -addext "subjectAltName=DNS:localhost" は飾りではありません。現代のブラウザは持ち主の確認に Subject の CN を見ず、SAN(Subject Alternative Name)拡張だけを見ます。SAN のない証明書は、他が完璧でも別の警告で拒まれます。古い手順書には SAN なしの一行が今も残っているので、写すときは気をつけてください。
自己署名の正しい使い道
オレオレ証明書そのものが悪いわけではありません。用途が限られているだけです。ローカル開発、閉じた検証環境、機器の内部通信。信頼の名簿を自分で管理できる場所では現役です。公開サーバーで使ってはいけない理由は、訪問者に「警告を無視して続行する」操作を覚えさせてしまうからです。その習慣は、本物の攻撃のときに牙をむきます。
有効期限は思ったより短い
さきほどの本物の証明書は、有効期間が8月4日から11月2日、ちょうど90日でした。Let’s Encrypt の標準です。期限を短くすると、鍵が漏れたときの被害期間も短くなり、失効の仕組みに頼りきらずに済みます。実務では自動更新が前提で、業界全体もさらに短くする方向に動いています。1年で切った今日の証明書も、来年の8月18日を過ぎれば警告の種に変わります。
server.key の扱いは第3回と同じ
証明書は公開してよいファイルですが、対になる server.key は秘密鍵です。これが漏れると、証明書ごと相手になりすませます。chmod 600 と、置き場所への注意はここでも変わりません。
公開鍵は証明書という身分証になり、身分証は保証人の鎖でルートまでつながり、鎖の根元は OS とブラウザに同梱されている。アドレスバーの警告と鍵マークの分かれ目は、この鎖がつながるかどうかです。
今日作った server.key と server.crt は消さずに取っておいてください。次回は最終回です。この2つのファイルで手元に HTTPS サーバーを立て、ハッシュ、共通鍵、公開鍵、署名、証明書。連載の部品を全部、1本の通信に組み立てます。
