自分で発行した証明書を置いたサイトをブラウザで開くと、ページの代わりに警告が出ます。通信は暗号化されているのに、拒まれます。

前回からの持ち越しも、これと地続きです。署名は完璧に検証できるのに、検証に使う公開鍵が本当にアリスのものかは確かめようがない。錠前は配れるようになったのに、配られた錠前の持ち主を保証するものがありません。どちらの場面でも足りないのは同じで、鍵の持ち主を保証する仕組みです。今日はそれを作ります。

OpenSSL で手を動かして学ぶ暗号の基礎、第5回は証明書と PKI です。環境の準備は第0回、持ち越した問題の経緯は第4回からどうぞ。

コマンドを打つ前に、証明書の正体を2コマにしておきます。

錠前に、身分証を付ける。
公開鍵 + 持ち主 localhost 保証人 持ち主の名乗り 保証人が署名 持ち主 localhost 保証人 認証局 署名 証明書 公開鍵 + 持ち主 localhost 保証人 持ち主の名乗り 保証人が署名 持ち主 localhost 保証人 認証局 署名 証明書

公開鍵と持ち主を1枚に束ねて、第三者(認証局)が署名する。それが証明書。

自分で自分を、保証する。
持ち主 localhost 保証人 localhost 署名 自己署名証明書(オレオレ) 持ち主 = 保証人 自分の身分証に、自分で判を押した 持ち主 localhost 保証人 localhost 署名 自己署名証明書(オレオレ) 持ち主 = 保証人 自分の身分証に、自分で判を押した

形式は正しい。ただ、保証人を誰も知らない。

証明書は、公開鍵に付けた身分証

答えの構造は単純です。公開鍵と持ち主の情報をひとつのファイルに束ね、そこに信頼できる第三者が署名する。署名は前回作った道具そのままです。このファイルを証明書と呼び、署名する第三者を認証局(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 がいないからです。身分証の形式は正しい。ただ、保証人を誰も知らない。警告が言っているのは「相手が名乗りどおりだと確認できない」であって、「通信が平文だ」ではありません。

ブラウザは、名簿を持っている。
ブラウザの名簿 ISRG Root ✓ DigiCert ✓ GlobalSign ✓ … ✕ 名簿にいない 持ち主 localhost 保証人 localhost 署名 警告 ブラウザの名簿 ISRG Root ✓ DigiCert ✓ GlobalSign ✓ … ✕ 名簿にいない 持ち主 localhost 保証人 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(公開鍵基盤)です。

本物は、鎖になっている。
持ち主 www.torigoedesign.com 保証人 Let's Encrypt 署名 このサイト 保証人は 持ち主 Let's Encrypt 保証人 ISRG Root 署名 中間の認証局 保証人は 持ち主 ISRG Root 保証人 自分(根元) 署名 名簿にいる ✓ 持ち主 www.torigoedesign.com 保証人 Let's Encrypt 署名 このサイト 保証人は 持ち主 Let's Encrypt 保証人 ISRG Root 署名 中間の認証局 保証人は 持ち主 ISRG Root 保証人 自分(根元) 署名 名簿にいる ✓

保証人にも身分証があり、たどると根元(ルート 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本の通信に組み立てます。