はじめに
この記事は、「OpenSSLでプライベートCAを作る」シリーズから参照される用語解説をまとめたものである。
| 記事 | 内容 |
|---|---|
| OpenSSLでプライベートCAを作る(全15章) | 全体の目次。0〜1章、14〜15章 |
| mTLSとは何か ― TLSとの違いから証明書チェーンまで | 2章。前提となる概念 |
| OpenSSLでプライベートCAを作る ― 構築手順 | 3〜11章・13章。実際に手を動かす本編 |
| OpenSSLでプライベートCAを作る ― スクリプト全文 | 12章。手順をまとめたスクリプト |
まず読むなら目次記事から入るのがよい。
本記事だけでも読めるが、G01のような番号は本記事内の通し番号であり、各記事からのリンク先として使っている。
目次
2. 基本概念 より
- G01. mTLSの"m"とは
- G02. ハンドシェイク
- G03. ClientHelloとは
- G04. ServerHelloとは
- G05. EncryptedExtensionsとは
- G06. CertificateRequestとは
- G07. Certificateとは
- G08. CertificateVerifyとは
- G09. Finishedとは
- G10. 鍵配送問題
- G11. なぜRSA鍵輸送は廃止されたのか
- G12. 補足:「ハイブリッド」には別の意味もある
- G13. X.509について補足
- G14. SubjectとDN(識別名)
- G15. X.509拡張(extension)
- G16. 信頼ストアに証明書を追加するということの意味
- G17. パス検証で何を確かめているのか
- G18. なぜ中間CAを挟むのか
- G19. 失効はチェーンの各段で必要になる
- G20. 用語:PEM形式とDER形式
- G21. 用語:nginx(エンジンエックス)
- G22. 用語:OpenSSL
- G23. CSR(証明書署名要求)
- G24. 用語:curl / Docker
3. 作成するCAの構成 より
4. 事前準備 より
- G28. ターミナルに関数を貼ったときの挙動
- G29. Homebrewの導入先を確認する
- G30. なぜmacOS標準のopensslでは動かないのか
- G31. スクリプトで実装を判定する
- G32. なぜ台帳が必要なのか(openssl req -x509との違い)
- G33. index.txtが無いときのエラー
- G34. シリアル番号(serial)とは
- G35. : > fileとchmod 700の意味
- G36. 補足:openssl caは本番用のツールではない
- G37. cat > ファイル名 <<'CNF_EOF' という書き方
- G38. なぜdirが相対パスでよいのか
- G39. 設定ファイルの「節(section)」とは
- G40. policyとは
- G41. copy_extensionsの危険性
5. ルートCAを作成する より
7. サーバ証明書を作成する より
8. クライアント証明書を作成する より
- G47. なぜクライアント証明書ではCNが禁止されていないのか
- G48. 用語:ロールオーバー(rollover)
- G49. ⚠️ unique_subjectの設定が効かないことがある
- G50. 用語:末尾のZは何か
- G51. 用語:PKCS#12(.p12 / .pfx)
- G52. 用語:.p12のパスワードは何を守っているのか
9. 失効リスト(CRL)を作成する より
10. 生成物を検証する より
11. nginxで動作確認する より
用語解説
2. 基本概念 より
G01. mTLSの"m"とは
mutual = 相互の頭文字から。サーバとクライアントが互いに認証するため。
- 「双方向TLS」「クライアント証明書認証」「2-way TLS」なども同じものを指す
- TLSの規格そのものに「mTLS」という別プロトコルがあるわけではない
- TLS 1.3の仕様に元から含まれるクライアント認証の機能を使っているだけ
- つまりmTLSは新しい技術ではなく、TLSの使い方の名前
G02. ハンドシェイク
ハンドシェイク(handshake / 握手) は、TLSで実際のデータをやり取りする前におこなう準備の通信である。
ここで次の3つを決める。
- 使う暗号方式(どのアルゴリズムで暗号化するか)
- 通信相手が本物か(証明書の検証=認証)
- 実際の暗号化に使う鍵(両者だけが知る共通の鍵を作る)
mTLSの「クライアントを検証して弾く」はこのハンドシェイクの中で完結する。ゆえに HTTPのリクエストが始まる前に接続が切れる。
curlで失敗したときに、404でも403でもなく次のようなエラーになる。
curl: (56) OpenSSL SSL_read: error:0A00045C:SSL routines::tlsv13 alert certificate required
G03. ClientHelloとは
クライアントが最初に送る、接続の開始を告げるメッセージ。
主に次の情報を載せる。
| 内容 | 意味 |
|---|---|
| 対応するTLSのバージョン | TLS 1.3が使えるか |
| 対応する暗号方式の一覧(cipher suites) | 「これらの暗号が使えます」 |
key_share |
鍵交換用の公開値。TLS 1.3では最初から送る |
server_name(SNI) |
接続したいホスト名 |
signature_algorithms |
対応する署名アルゴリズムの一覧 |
ポイントは2つある。
-
key_shareを最初から送る- TLS 1.2では鍵交換に往復が余分に必要だった
- TLS 1.3は「たぶんこれで合意できるだろう」という公開値を先に送ることで、往復を1回減らしている
-
SNI(Server Name Indication)で接続先を伝える
- 1つのIPアドレスで複数のサイトを運用している場合、サーバはどの証明書を返せばよいか判断できない
- そこで接続したいホスト名を先に伝える
- nginxの
server_nameは、これを見て振り分けている
ClientHelloは暗号化されない。まだ鍵が決まっていないためである。
G04. ServerHelloとは
サーバがClientHelloに応える、合意内容を伝えるメッセージ。
| 内容 | 意味 |
|---|---|
| 選んだTLSのバージョン | 実際に使うバージョン |
| 選んだ暗号方式 | クライアントが提示した中から1つ選ぶ |
key_share |
サーバ側の鍵交換用の公開値 |
ClientHelloが「使えるものの一覧」を出すのに対し、ServerHelloはその中から1つを選んで返す。
この時点で、双方が互いのkey_shareを持つことになる。これで両者だけが計算できる共通の鍵が求まり、以降のメッセージは暗号化される。
mermaid図でServerHelloより下のメッセージがすべて暗号化されているのは、このためである。TLS 1.2では証明書が平文で流れていたが、TLS 1.3では証明書も暗号化される。
G05. EncryptedExtensionsとは
ServerHelloに載せきれなかったその他の合意事項を、暗号化して送るメッセージ。
- ServerHelloは暗号化前に送られるため、秘匿したい情報を載せられない
- そこで、鍵が確定した直後に「残りの設定」をまとめて暗号化して送る
- 例:ALPN(HTTP/2を使うかHTTP/1.1を使うかの合意)、最大フラグメント長など
TLS 1.3で新設されたメッセージであり、ハンドシェイク情報の秘匿を進めるために導入された。
証明書の話には直接関係しないが、メッセージの並びを理解するうえで位置を押さえておくとよい。
G06. CertificateRequestとは
サーバがクライアントに証明書を要求するメッセージ。これがmTLSの入口である。
このメッセージが送られなければ、クライアントは証明書を出さない。つまり通常のHTTPSとmTLSを分けているのは、サーバがこれを送るかどうかだけである。
nginxではssl_verify_client on;と書くことで、このメッセージが送られるようになる。
重要な中身が certificate_authorities拡張である。
- サーバが「このCAが発行した証明書なら受け付けます」という一覧を送る
- 中身はCAのDN(識別名)の並び
- クライアントはこれを見て、手元の証明書から候補を絞る
これが実際の使い勝手に直結する。
- macOSは、この一覧に合致する証明書だけを「証明書を選択してください」のダイアログに出す
- 一覧が広いと、ユーザーに無関係な証明書まで候補として並ぶ
- mTLS専用の中間CAを1つ用意すると、候補を意図した1枚に絞れる
nginxでは、ssl_client_certificateに指定した証明書のDNがこの一覧として送られる。公式ドキュメントに次のようにある。
The list of certificates will be sent to clients. If this is not desired, the
ssl_trusted_certificatedirective can be used.
(nginx ngx_http_ssl_module)
送りたくない場合はssl_trusted_certificateを使う。検証には使われるが、一覧には含まれない。
G07. Certificateとは
自分の証明書(とその上位のCA証明書)を相手に渡すメッセージ。サーバとクライアントの両方が送る。
| 送信者 | 中身 | この記事で対応するファイル |
|---|---|---|
| サーバ | サーバ証明書 + 中間CA | server-fullchain.pem |
| クライアント | クライアント証明書 + 中間CA |
.p12に同梱したもの |
ルートCAは含めないのが原則である。受け取る側は既にルートCAを信頼ストアに持っているはずで、持っていないなら送っても信頼されないためである。
注意点として、このメッセージは「証明書を持っている」ことしか示さない。証明書は公開情報なので、盗み見てコピーすれば誰でも同じものを送れてしまう。
「本当に本人か」は、次のCertificateVerifyで証明される。
G08. CertificateVerifyとは
送った証明書の秘密鍵を、自分が本当に持っていることを証明するメッセージ。サーバとクライアントの両方が送る。
ここまでのハンドシェイクの内容全体に対して、秘密鍵で署名したものを送る。
受け取った側の検証手順は次のとおり。
- 直前のCertificateメッセージから、相手の公開鍵を取り出す
- その公開鍵で署名を検証する
- 通れば「相手は対応する秘密鍵を持っている」と確定する
これが「秘密鍵を送らずに身元を証明する」の実体である。秘密鍵そのものは一度も通信路に出ていない。
Certificateとの役割分担を整理すると次のようになる。
| メッセージ | 示すこと |
|---|---|
| Certificate | 「この名前と公開鍵の組は、CAに保証されている」 |
| CertificateVerify | 「その公開鍵に対応する秘密鍵を、私が持っている」 |
両方そろって初めて認証が成立する。片方だけでは、証明書をコピーしたなりすましを防げない。
なお、署名の対象にハンドシェイク全体を含めている点も重要である。過去の署名を使い回す攻撃(リプレイ攻撃)を防ぐためである。
G09. Finishedとは
ハンドシェイクが改竄されていないことを確認するメッセージ。サーバとクライアントの両方が送る。
ここまでのハンドシェイク全体に対するMAC(改竄検知用の符号)を送り、相手も同じ値を計算して突き合わせる。
これがないと、次のような攻撃(ダウングレード攻撃)が成立してしまう。
- 攻撃者が経路の途中でClientHelloを書き換え、弱い暗号方式しか使えないように見せかける
- サーバは弱い暗号方式を選んでしまう
- 攻撃者が通信を解読する
Finishedでハンドシェイク全体を突き合わせれば、途中で1ビットでも書き換えられていれば値が一致せず、接続が切断される。
Finishedの交換が終わった時点でハンドシェイクは完了し、以降は実際のHTTP通信が始まる。
G10. 鍵配送問題
共通鍵暗号を使うには、通信の前に両者が同じ鍵を持っている必要がある。
ここで循環に陥る。
- 鍵を相手に送りたい
- しかし経路は盗聴されているかもしれない
- だから鍵を暗号化して送りたい
- その暗号化に使う鍵をどうやって送るのか → 1に戻る
これが鍵配送問題である。事前に会って手渡しできるなら解決するが、初めて接続するWebサイトが相手では不可能である。
公開鍵暗号は、この循環を断ち切るために生まれた。「公開鍵は盗聴されてもよい」という性質によって、事前の共有なしに鍵を用意できる。
G11. なぜRSA鍵輸送は廃止されたのか
TLS 1.2以前には、次の方式があった。
- クライアントが共通鍵の素を乱数で作る
- サーバの証明書に書かれた公開鍵で暗号化して送る
- サーバが自分の秘密鍵で復号する
これが「共通鍵を公開鍵で暗号化して送る」の実体である(RSA鍵輸送)。
この方式には致命的な弱点がある。前方秘匿性(forward secrecy)がないことである。
- サーバの秘密鍵が後から漏れたとする
- 攻撃者が過去の通信を記録していれば、それをすべて遡って復号できる
- 手順2で送られた暗号文が記録に残っているため
(EC)DHEではこれが起きない。鍵は毎回その場で生成され、通信が終われば捨てられる。証明書の秘密鍵が漏れても、過去の通信は復号できない。
RFC 8446が "all public-key based key exchange mechanisms now provide forward secrecy" と述べているのは、この点である。
G12. 補足:「ハイブリッド」には別の意味もある
近年のTLSの文脈で「ハイブリッド」と言うと、耐量子計算機暗号(PQC)との組み合わせを指すことが増えている。
- 従来の鍵交換(X25519)と、耐量子の鍵交換(ML-KEM)を同時に使う方式
-
X25519MLKEM768のような名前で呼ばれる - どちらか一方が破られても、もう一方が守るという考え方
本節で説明した「共通鍵暗号 + 公開鍵暗号」のハイブリッドとは別の話である。文脈で読み分ける必要がある。
G13. X.509について補足
X.509の中身は主に次のとおり。
- Subject(名前) — 誰の証明書か
- Public Key(公開鍵) — その人の公開鍵
- Validity(有効期間) — いつからいつまで有効か
- Issuer(発行者) — 誰が発行したか
- Signature(発行者の署名) — 上記の内容に対する、発行者の秘密鍵による署名
公開鍵だけが手元にあっても「これは誰の鍵か」が分からない。証明書は、その紐づけを第三者(CA)の署名で保証するための入れ物である。
「証明書ファイル」と言ったとき、秘密鍵は含まれない。秘密鍵は別ファイルである。
(例外的に、.p12形式は証明書と秘密鍵をまとめて1ファイルにしたものである)
G14. SubjectとDN(識別名)
Subject は「この証明書が誰のものか」を表す名前である。中身は DN(Distinguished Name / 識別名) という構造を持つ。
DNは属性の並びである。よく使うものを挙げる。
| 略号 | 正式名 | 意味 | 例 |
|---|---|---|---|
CN |
Common Name | 名前そのもの | device-001 |
O |
Organization | 組織名 | Example Corp |
OU |
Organizational Unit | 部署・分類 | Devices |
C |
Country | 国コード(2文字) | JP |
ST |
State/Province | 都道府県 | Tokyo |
L |
Locality | 市区町村 | Chiyoda |
OpenSSLのコマンドラインではスラッシュ区切りで書く。
-subj "/C=JP/O=Example Corp/OU=Devices/CN=device-001"
一方、nginxの$ssl_client_s_dnはカンマ区切りの逆順で出る(RFC 2253 形式)。
CN=device-001,OU=Devices,O=Example Corp,C=JP
書き方が違うだけで同じもである。
G15. X.509拡張(extension)
証明書の基本項目(名前・公開鍵・有効期間)だけでは足りない情報を追加するのが拡張である。この記事で使うものを挙げる。
| 拡張 | 役割 |
|---|---|
basicConstraints |
この証明書はCAか否か。CA:TRUE / CA:FALSE。pathlenで配下のCA段数も制限できる |
keyUsage |
鍵の用途。署名用か、鍵交換用か、証明書への署名用か |
extendedKeyUsage |
より具体的な用途。serverAuth(サーバ用)/ clientAuth(クライアント用) |
subjectAltName |
SAN。この証明書が有効なホスト名やIPの一覧 |
subjectKeyIdentifier |
この証明書の鍵の識別子 |
authorityKeyIdentifier |
発行者の鍵の識別子。チェーンをたどるときの手がかり |
拡張には critical(必須) の指定がある。
-
critical付き — 検証する側がこの拡張を理解できなければ、証明書を拒否しなければならない - 無し — 理解できなければ無視してよい
basicConstraints = critical, CA:trueのように書くのは、「CAかどうかの判定を無視されては困る」からである。
拡張が無い、あるいは間違っていると何が起きるか。
-
basicConstraintsが無い → CAとして扱われない(下位の証明書を発行しても検証が通らない) -
extendedKeyUsage = serverAuthが無い → クライアント証明書としては使えてもサーバ証明書としては拒否される -
subjectAltNameが無い → ホスト名の照合に失敗する
G16. 信頼ストアに証明書を追加するということの意味
信頼ストアへの追加は、そのCAに「どんな名前の証明書でも発行してよい」という権限を与える操作である。
たとえば自作のCAを信頼ストアに入れると、そのCAは次のような証明書を作れてしまう。
-
CN=[www.google.com](https://www.google.com)の証明書 -
CN=login.example-bank.co.jpの証明書
そして手元の端末は、それを正規のものとして受け入れる。名前の正しさを保証しているのはCAだけであり、CAを信頼すると決めたのは自分だからである。したがって自作CAの秘密鍵を厳重に管理しなければならない。
G17. パス検証で何を確かめているのか
各段でおこなう確認は、おおむね次の4つである。
| 確認項目 | 内容 |
|---|---|
| 署名 | 上位の公開鍵で署名を検証できるか |
| 有効期間 | 現在時刻がNot Before〜Not Afterの範囲にあるか |
| 失効 | CRLやOCSPに載っていないか |
| 拡張 |
basicConstraintsがCA:TRUEか、pathlenの段数を超えていないか |
これを末端から順に繰り返し、トラストアンカーに到達できれば検証成功となる。
G18. なぜ中間CAを挟むのか
ルートCAから直接発行しても技術的には動作するが、それでも中間CAを挟むのが標準的な構成である。理由は以下の通り。
- 理由1:ルートCAの秘密鍵をオフラインに置ける
- 日常の発行作業は中間CAがおこなう
- ルートCAの鍵は金庫やHSMに入れたまま、普段は取り出さない
- 理由2:被害を局所化できる
- 中間CAの鍵が漏れた場合、その中間CAを失効させて作り直せばよい
- ルートCAが漏れると、信頼ストアに入れた全端末をやり直すことになる
- 理由3:用途ごとに分けられる
- 「サーバ用」「mTLS用」のように中間CAを分けられる
- この記事でmTLS専用の中間CAを作るのはこのため(2.2.3の
certificate_authoritiesを参照)
公的なCAの世界では、ルートCAから末端の証明書を直接発行しない運用が標準になっている(CA/Browser Forum Baseline Requirements)。
G19. 失効はチェーンの各段で必要になる
失効の確認は、末端の証明書だけでは足りない。中間CAも失効し得るからである。
nginxはssl_crlを設定すると、チェーンの全段についてCRLを確認する。(ngx_http_ssl_module)したがって次の2つが要る。
| 確認する段 | 必要なCRL | 発行者 |
|---|---|---|
| クライアント証明書 | 中間CAのCRL | 中間CA |
| 中間CA | ルートCAのCRL | ルートCA |
ルートCAのCRLが無いと、中間CAの段で「CRLが見つからない」となり、失効していない証明書まで拒否される。実際の手順は本編の9.3.2. ルートCAのCRLも作るを参照。
なお、失効状態を示す責任は、その証明書を発行したCAが負う。(RFC 5280 §3.3)
したがって、中間CAが出した証明書は中間CAが、中間CA自身はルートCAが失効させることになる。ただし、CRLを発行する主体は必ずしもCA自身とは限らない。(RFC 5280 §3.3)
「責任を負うのは発行したCA」だが、「CRLに署名する主体は委任され得る」という二段構えになっている。この記事では委任はせず、中間CAが自分でCRLを発行する。
G20. 用語:PEM形式とDER形式
証明書や鍵のファイル形式である。
PEM — テキスト形式。-----BEGINと-----ENDで挟まれたBase64。
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIUBYBQy9Sz9nZgUitxUhFS0eF5TLEwDQYJKoZIhvcNAQEL
...
-----END CERTIFICATE-----
- テキストなので
catで連結できる - この記事でチェーンファイルを
catで作れるのはこのため - 拡張子は
.pem.crt.cer.keyなどバラバラ。拡張子は形式を保証しない
DER — バイナリ形式。同じ内容をエンコードしただけで、情報量は同じ。
変換はこうする。
openssl x509 -in cert.pem -outform DER -out cert.der
openssl x509 -in cert.der -inform DER -out cert.pem
BEGIN行のラベルで中身の種類が分かる。
| ラベル | 中身 |
|---|---|
CERTIFICATE |
証明書 |
CERTIFICATE REQUEST |
CSR |
PRIVATE KEY |
秘密鍵(暗号化なし) |
ENCRYPTED PRIVATE KEY |
秘密鍵(パスワード付き) |
X509 CRL |
失効リスト |
PRIVATE KEYと書かれたファイルは絶対に外に出さない、と覚えておけば事故が減る。
G21. 用語:nginx(エンジンエックス)
nginx は、オープンソースの Webサーバ / リバースプロキシ である。読み方は「エンジンエックス」。
役割を3つ挙げる。
- Webサーバ — HTTPのリクエストを受け取り、ファイルを返す
- リバースプロキシ — 受け取ったリクエストを、後ろにいる別のサーバ(アプリケーション)に転送する
- TLS終端(TLS termination) — HTTPSの暗号化・復号を引き受ける
この記事で使うのは3つ目の役割である。
TLS終端とは、TLSの通信相手がnginxになるということである。暗号化された通信はクライアントとnginxのあいだだけで成立し、その先へは平文のHTTPとして渡される。
nginxがTLSのハンドシェイクを担当するので、クライアント証明書の検証もnginxの仕事になる。検証はハンドシェイクの一部だからである(2.2.3. mTLSのハンドシェイク)。
アプリケーションが受け取るのは、復号が済んだ普通のHTTPリクエストである。そのため、次のものをアプリケーション側に書かなくてよい。
- TLSライブラリの初期化と設定
- サーバ証明書と秘密鍵の読み込み
- クライアント証明書の検証(チェーンをたどる、CRLを見る)
検証の結果が必要な場合は、nginxが$ssl_client_s_dnなどの変数で渡す。HTTPヘッダに入れてアプリケーションへ転送するのが一般的である。
mTLSの検証をnginxにやらせるのがこの構成の要点である。
- 不正な端末はnginxの段階で切断される。アプリケーションまで到達しない
- 「証明書が正しいか」はnginx、「その端末に権限があるか」はアプリケーション、と担当を分けられる
なお、nginxとアプリケーションのあいだは平文である。この区間は信頼できる経路であることが前提になる。同じホスト内やコンテナ間などに限る。
本編の11章では後ろにアプリケーションを置かず、nginxが$ssl_client_*の値をそのまま返す構成にしている。
設定はnginx.confなどのテキストファイルに書く。この記事で使う主な設定項目(ディレクティブ)は次のとおりである。
| ディレクティブ | 意味 |
|---|---|
ssl_certificate |
サーバ自身の証明書(+ 中間CA) |
ssl_certificate_key |
サーバ自身の秘密鍵 |
ssl_client_certificate |
クライアント証明書を検証するためのCA |
ssl_verify_client |
クライアント証明書の検証を有効にする |
ssl_crl |
失効リスト。設定するとチェーンの全段についてCRLを確認する(G19) |
同じ役割のソフトウェアにApache HTTP ServerやCaddy、Envoyなどがある。設定の書き方は違うが、必要な証明書ファイルの考え方は共通である。
G22. 用語:OpenSSL
OpenSSL は、TLSの実装と暗号処理を提供するオープンソースのライブラリ、およびそのコマンドラインツールである。
- ライブラリとしては、nginxやcurlなど多数のソフトウェアの内部で使われている
- コマンドラインツールとしては、鍵の生成・証明書の発行・検証・形式変換などを一通りこなす
この記事で使うのは後者、opensslコマンドのほうである。
1つの実行ファイルの中に多数の機能を持っていて、第1引数で使う機能(サブコマンド)を選ぶ。
| サブコマンド | 役割 |
|---|---|
openssl genrsa / genpkey
|
鍵を生成する |
openssl req |
CSRを作る。-x509を付けると自己署名証明書も作れる |
openssl x509 |
証明書を表示・変換する。署名もできるが台帳は持たない |
openssl ca |
CAとして証明書を発行・失効させる(台帳あり) |
openssl verify |
証明書チェーンを検証する |
openssl crl |
CRLを表示する |
openssl pkcs12 |
.p12を作る・展開する |
openssl s_client |
TLSクライアントとして接続して中身を見る |
紛らわしいのがopenssl x509 -req -CA ...で、これでも署名はできる。手軽であるが台帳が無いので、失効も連番管理もできない。この記事ではopenssl caを使う。
G23. CSR(証明書署名要求)
CSR(Certificate Signing Request) は、「この公開鍵とこの名前で証明書を発行してください」という申請書である。
作る側の手順は以下の通り。
- 秘密鍵と公開鍵のペアを生成する
- 公開鍵 + 名前(Subject)をまとめ、自分の秘密鍵で署名してCSRを作る
- CSRをCAに渡す
- CAが内容を確認し、CAの秘密鍵で署名して証明書を返す
秘密鍵はCSRに含まれない。ゆえにCSRをCAに渡しても秘密鍵は手元から出ない。
拡張子は.csrや.req。中身を見る方法は以下の通り。
openssl req -in server.csr.pem -noout -text
G24. 用語:curl / Docker
curl(カール) は、コマンドラインからHTTPなどのリクエストを送るツールである。ブラウザの代わりに使う。
この記事では、クライアント証明書を指定してmTLSの動作を確認するのに使う。
curl https://localhost:8443/ \
--cacert ルートCAの証明書 \
--cert クライアント証明書 \
--key クライアントの秘密鍵
Docker(ドッカー) は、アプリケーションを「コンテナ」という隔離された環境で動かす仕組みである。
- nginxをインストールせずに動かせる
- 試し終わったらコンテナごと消せる。手元の環境が汚れない
-
docker composeを使うと、設定をファイルに書いて1コマンドで起動できる
どちらも動作確認のために使うだけで、証明書の作成そのものには不要である。
3. 作成するCAの構成 より
G25. pathlen(パス長制約)
basicConstraintsに書ける制約で、そのCAの下にあと何段CAを作れるかを制限する。
| 設定 | 意味 |
|---|---|
pathlen:1 |
配下にあと1段だけCAを作れる |
pathlen:0 |
配下にCAを作れない。末端の証明書しか発行できない |
| 指定なし | 無制限 |
この記事の構成はこうなる。
- ルートCAに
pathlen:1→ 中間CAを作れる。その先のCAは作れない - 中間CAに
pathlen:0→ 中間CAはもうCAを発行できない
なぜ制限するのか。もし中間CAの鍵が漏れた場合、pathlenが無いと攻撃者はさらに下位のCAを無限に作れてしまう。段数を切っておけば、被害の範囲を末端の証明書だけに限定できる。
必要最小限の段数を明示しておくのが定石である。
G26. 用語:CA/Browser ForumとBaseline Requirements
CA/Browser Forum(CA/Bフォーラム)は、認証局とブラウザベンダーが集まって、公的な証明書のルールを決める業界団体である。
- 参加者は2種類
- Certificate Issuer — 認証局(DigiCert、Sectigo、Let's Encryptなど)
- Certificate Consumer — ブラウザ・OSベンダー(Apple、Google、Mozilla、Microsoft)
- 決定は投票でおこなわれる(Ballotと呼ばれる)
- 決まったルールがBaseline Requirements(BR)という文書になる
法的な強制力はない。では、なぜ守られるのか。
- ブラウザ・OSベンダーが「BRに従わないCAは信頼ストアから外す」という立場を取るため
- 信頼ストアから外されると、そのCAの証明書はどこでも使えなくなる
G27. 補足:公的CAの上限は47日へ向かっている
825日は、2018年3月〜2020年9月に公的CAに適用されていた上限である。その後398日に短縮された。
さらに2025年4月、CA/Browser Forumの投票(Ballot SC-081v3)で段階的な短縮が決まっている。
- 現行: 398日
- 2026年3月から2029年3月にかけて段階的に短縮
- 最終的に 47日
(CA/Browser Forum Ballot SC-081v3)
4. 事前準備 より
G28. ターミナルに関数を貼ったときの挙動
複数行をまとめて貼り付けると、閉じ括弧までを1つのまとまりとして受け取る。
$ init_ca_dir() {
> local dir="$1"
> mkdir -p "$dir/newcerts" "$dir/private"
> }
$
2行目以降の>は「入力の続きを待っている」という印であり、自分で打つ必要はない。閉じ括弧を入力した時点で定義が完了し、通常のプロンプトに戻る。
定義した関数は、そのあと普通のコマンドとして呼べる。
$ init_ca_dir certs/root
ただし、定義した関数はターミナルを閉じると消える。作業を再開するときは貼り直すか、Bのスクリプトを使う。
G29. Homebrewの導入先を確認する
openssl@3の実体がどこに入るかは、CPUアーキテクチャによって変わる。
| 環境 | Homebrewの導入先 |
openssl@3のパス |
|---|---|---|
| Apple Silicon(M1以降) | /opt/homebrew |
/opt/homebrew/opt/openssl@3/bin/openssl |
| Intel | /usr/local |
/usr/local/opt/openssl@3/bin/openssl |
自分の環境の導入先は次で確認できる。
$ brew --prefix
/usr/local
$ brew --prefix openssl@3
/usr/local/opt/openssl@3
brew --prefix openssl@3を使えば、どちらの環境でも正しいパスが得られる。
なおopt/の下は現在有効なバージョンへのシンボリックリンクである。実体はCellar/にあり、複数バージョンが同居し得る。
$ ls -l /usr/local/opt/openssl@3
/usr/local/opt/openssl@3 -> ../Cellar/openssl@3/3.6.3
$ ls /usr/local/Cellar/openssl@3/
3.6.2 3.6.3
バージョンを上げても追随するよう、opt/経由で指す。
G30. なぜmacOS標準のopensslでは動かないのか
/usr/bin/opensslの実体は LibreSSL である。
$ /usr/bin/openssl version
LibreSSL 3.3.6
LibreSSL は、OpenBSDプロジェクトがOpenSSL 1.0.1gをフォークして作った別のTLSライブラリである。
- きっかけは2014年のHeartbleed脆弱性
- OpenSSLのコードベースから機能を削ぎ落とし、安全性と可読性を優先した
- AppleはこれをmacOSに同梱している
フォーク元が OpenSSL 1.0.1g(2014年) であるため、その後OpenSSL側に入った機能の多くがLibreSSLには無く、コマンドラインのオプションも一致しない。
LibreSSL is not ABI compatible with any release of OpenSSL
(LibreSSL - Wikipedia)
HomebrewでOpenSSLを入れても/usr/bin/opensslは置き換わらない。これは keg-only という扱いで、意図的にそうなっている。macOSの一部がLibreSSLに依存しているためである。
フォーク(fork) とは、あるソフトウェアのソースコードを複製して別プロジェクトとして独立に開発を続けることである。名前もコマンド名も似たままなことが多い。
G31. スクリプトで実装を判定する
12章のスクリプトでは、冒頭で次の判定を入れている。
OPENSSL="${OPENSSL:-openssl}"
if ! "$OPENSSL" version | grep -q '^OpenSSL 3'; then
echo "エラー: OpenSSL 3.x が必要です(現在: $("$OPENSSL" version))" >&2
exit 1
fi
注意点は、バージョン番号だけでは判定できないことである。
- LibreSSLも
3.3.6のように3系のバージョンを名乗る -
grep '3\.'のような判定ではLibreSSLも通ってしまう - 先頭の
OpenSSLという文字列まで含めて判定する
"${OPENSSL:-openssl}"はシェルのパラメータ展開である。
- 環境変数
OPENSSLが設定されていれば、その値を使う - 未設定または空なら、既定値として
opensslを使う
これにより、実行時に外から実装を差し替えられる。
G32. なぜ台帳が必要なのか(openssl req -x509との違い)
自己署名証明書を1枚作るだけならopenssl req -x509で済む。しかしCAとして運用するには足りない。
openssl req -x509でできないことは次のとおりである。
- 発行済み証明書の管理(何をいつ誰に出したか)
- 失効(revoke)とCRLの生成
- 連番(シリアル番号)の採番
これらはopenssl caサブコマンドの担当であり、openssl caはファイルベースの台帳を前提に動く。公式ドキュメントにも「必須」と明記されている。
database: The text database file to use. Mandatory. This file must be present though initially it will be empty.
new_certs_dir: Mandatory. It specifies the directory where new certificates will be placed.
(openssl-ca)
G33. index.txtが無いときのエラー
エラーメッセージが原因を直接指してくれない。
Can't open ./demoCA/index.txt for reading, No such file or directory
demoCAという見覚えのないパスが出るのは、設定ファイルを読めていないときの既定値を見に行っているためである。-configの指定漏れか、設定内のdirが違うかを疑う。
newcerts/も同様で、ディレクトリがないと発行自体が失敗する。
G34. シリアル番号(serial)とは
証明書1枚ごとに振られる通し番号である。
- 同じCAの中で一意になる
- 証明書の中に記録されていて、後から変更できない
- 失効の管理はこのシリアル番号でおこなう(CRLに載るのはシリアル番号)
serialファイルには「次に使う番号」を16進数で書く。1000から始めるのが慣例である。発行するたびにOpenSSLが自動で1増やす。
$ cat certs/intermediate/serial
1006
発行した証明書のコピーは、newcerts/にシリアル番号のファイル名で置かれる。
$ ls certs/intermediate/newcerts
1000.pem 1001.pem 1002.pem 1003.pem 1004.pem 1005.pem
シリアル番号は、設定によって連番にもランダムにもできる。
| 設定 | 結果 |
|---|---|
rand_serialを書かない |
連番。serialファイルの値を使い、1つずつ増やす |
rand_serialの行がある |
ランダム。20バイトの乱数になり、serialファイルは更新されない |
rand_serialは値ではなく行の有無で判定される点に注意。 rand_serial = noと書いてもランダムになる。連番にしたい場合は、行ごと書かない。
- ランダム — 公的CAの要件。予測攻撃を防ぐため
- 連番 — 検証環境では追いやすい。どれが何枚目か分かる
この記事は検証用なので、rand_serialを書かずに連番にしている。
G35. : > fileとchmod 700の意味
: > file は、空ファイルを作る(または既存ファイルを空にする)シェルのイディオムである。
-
:は「何もしないで成功を返す」組み込みコマンド -
>はリダイレクト。出力先のファイルを空にして作る
touch fileとの違いは、既にファイルがある場合である。
-
touch— 中身はそのまま、更新日時だけ変わる -
: >— 中身が消えて空になる
「まっさらな台帳を作る」のが目的なので: >を使う。
chmod はファイルのアクセス権を設定するコマンドである。数字は3桁で、左から「所有者 / グループ / その他」の権限を表す。
| 数字 | 意味 |
|---|---|
| 7 | 読みr + 書きw + 実行x
|
| 6 | 読み + 書き |
| 0 | 権限なし |
-
chmod 700 dir— 所有者だけが入れるディレクトリ -
chmod 600 file— 所有者だけが読み書きできるファイル
秘密鍵は他のユーザーから読めてはいけない。読めてしまえば、その鍵の持ち主になりすませる。
G36. 補足:openssl caは本番用のツールではない
公式ドキュメントに、はっきりとこう書かれている。
This command was originally meant as an example of how to do things in a CA. Its code does not have production quality. It was not supposed to be used as a full blown CA itself, nevertheless some people are using it for this purpose at least internally.
(openssl-ca)
既知の制約として次が挙げられている。
- ファイルロックが無い — 同時に複数の発行を走らせてはいけない
- 台帳が壊れると修復が難しい
-
copy_extensionsの設定を誤ると危険
検証環境や社内用途では広く使われているが、規模が大きくなるなら step-ca や cfssl、あるいは商用のPKI製品を検討する場面である。
この記事は「まず仕組みを理解して動かす」が目的なので、素のopenssl caを使う。
G37. cat > ファイル名 <<'CNF_EOF' という書き方
ヒアドキュメントという、複数行のテキストをそのままファイルへ書き込むシェルの機能である。
cat > 保存先ファイル名 <<'CNF_EOF'
ここに書いた内容が
そのままファイルになる
CNF_EOF
-
cat > ファイル名— 受け取った内容をそのファイルに書き出す -
<<'CNF_EOF'— 「行頭にCNF_EOFだけが現れる行まで」を入力として受け取る - 区切り文字は
CNF_EOFでなくてもよい。中身と重複しない文字列であればよい
CNF_EOFをシングルクォートで囲んでいる点が重要である。
| 書き方 |
$dirの扱い |
|---|---|
<<CNF_EOF |
シェルが変数として展開してしまう(この場合は空文字になる) |
<<'CNF_EOF' |
そのままの文字列として書き込まれる |
設定ファイル内の$dirは、シェルではなく OpenSSL自身が解釈する変数である。OpenSSLの設定ファイルには、同じ節の値を$名前で参照する機能があるためである。
dir = certs/root
database = $dir/index.txt → certs/root/index.txt と解釈される
シェルに展開されては困るので、クォート付きの<<'CNF_EOF'を使う。
G38. なぜdirが相対パスでよいのか
openssl caは設定ファイル内の相対パスを、設定ファイルの位置ではなく、コマンドを実行したディレクトリを基準に解決する。
この記事は4.0で「以降のコマンドはすべて~/mtls-demoで実行する」と決めているため、certs/rootのような相対パスで問題なく動く。
逆に言えば、別のディレクトリに移動してから実行すると台帳を見失う。
$ cd ~/mtls-demo && $OPENSSL ca -config certs/root/openssl.cnf ... # 動く
$ cd /tmp && $OPENSSL ca -config ~/mtls-demo/certs/root/openssl.cnf ... # 失敗する
12章のスクリプトは、どこから実行されても動くよう、dirに絶対パスを書き込む形にしている。
G39. 設定ファイルの「節(section)」とは
OpenSSLの設定ファイルは INI形式である。[ 名前 ]で区切られたブロックを「節(section)」と呼ぶ。
[ ca ]
default_ca = CA_default ← 「CA_default という節を見ろ」という指定
[ CA_default ]
dir = certs/root
database = $dir/index.txt ← $変数 で同じ節の値を参照できる
特徴が2つある。
-
節は名前で参照される —
-extensions server_certは「[ server_cert ]という節を使え」の意味 - 同じ名前の節が複数のファイルにあり得る — どのファイルの節を指すのかが曖昧になる
この「どのファイルの節か」の取り違えが、7.1で出てくる落とし穴の正体である。
G40. policyとは
policyは、CSRのSubjectにどの項目が必須かを決める設定である。
| 値 | 意味 |
|---|---|
supplied |
必須。CSRに入っていなければ発行を拒否する |
optional |
任意。あってもなくてもよい |
match |
CA自身の証明書と同じ値でなければならない |
この記事ではcommonName = suppliedだけを必須にし、他はすべてoptionalにしている(policy_loose)。
厳しくすると、たとえばorganizationName = matchにして「自組織の証明書しか発行しない」という縛りをかけられる。検証用なので緩めにしてある。
G41. copy_extensionsの危険性
copy_extensionsは、CSRに書かれた拡張を証明書にコピーするかを決める設定である。
この記事では安全性の理由からnone(コピーしない)にしている。
copy_extensions = copyにすると、申請者がCSRに書いた拡張がそのまま証明書に入る。すると次のような攻撃が成立する。
- 申請者がCSRに
basicConstraints = CA:TRUEを書き込む - CAがそれをコピーして署名する
- 申請者がCAになってしまい、任意の証明書を発行できる
SANのようにCSRから引き継ぎたい項目がある場合は、copy_extensionsに頼らず、CA側で内容を確認したうえで明示的に指定するのが安全である。
5. ルートCAを作成する より
G42. なぜルートCAだけopenssl caを使わないのか
4章で用意した台帳はopenssl caが使うものだが、ルートCAの作成では使わない。
openssl caは「上位のCAが、下位のCSRに署名する」コマンドである。ルートCAには署名してくれる上位が存在しないため、このコマンドは使えない。
そこでopenssl req -x509を使い、生成した秘密鍵でそのまま自分に署名する。IssuerとSubjectが同じになるのはこのためである。
なおopenssl req -x509は台帳を使わないため、ルートCA自身のシリアル番号はランダムな値になる。certs/root/serialの1000が使われるのは、次章で中間CAに署名するときからである。
G43. -nodes(パスワードをかけない)について
-nodesは、生成した秘密鍵を暗号化せずそのまま保存するオプションである。付けない場合、鍵を使うたびにパスワードの入力を求められる。
本番のルートCAでは必ずパスワードをかける(-nodesを外す)。パスワード無しの秘密鍵は、ファイルを読める者なら誰でも使えてしまうためである。
この記事で外しているのは、手順を止めずに通しで実行できるようにするためである。2.4.2で触れたとおり、この鍵を持つ者は任意の名前の証明書を発行できるので、検証が終わったら削除する。
G44. なぜ鍵長が4096ビットなのか
RSAの鍵長は、長いほど解読が難しくなる代わりに処理が遅くなる。
| 用途 | この記事での鍵長 | 理由 |
|---|---|---|
| ルートCA・中間CA | 4096 | 有効期間が長く(5〜10年)、漏れたときの影響が大きい |
| サーバ・クライアント証明書 | 2048 | 2年で作り直す。ハンドシェイクのたびに使うので速度も効く |
CAの鍵を長くするのは、「長く使うものほど強くする」という考え方による。現在2048ビットは十分な強度とされているが、10年後も同じとは限らないためである。
7. サーバ証明書を作成する より
G45. 用語:SAN(subjectAltName / サブジェクト代替名)
SAN は、その証明書が有効なホスト名やIPアドレスを列挙する拡張である。
X509v3 Subject Alternative Name:
DNS:gw.example.internal, DNS:localhost, IP Address:192.0.2.10, IP Address:127.0.0.1
型が明示されているのがCNとの決定的な違いである。
-
DNS:— ホスト名として照合する -
IP:— IPアドレスとして照合する - ほかに
email:URI:などもある
CNは「ただの文字列」なので、そこに書かれた192.0.2.10がホスト名なのかIPなのか、規格上は判別できない。RFC 9525が "not strongly typed" と言っているのはこの点である。
複数の名前を1枚の証明書でカバーできるのもSANの利点である。CNは1つしか書けない。
DNS:とIP:は別物であり、IPでアクセスするならIP:エントリが要る。DNS:192.0.2.10と書いてもIPアクセスの照合には使われない。開発中は名前解決が怪しいことが多いので、両方入れておく。
なおDNS:*.example.comのように書くとワイルドカード証明書になるが、*が使えるのは最も左のラベル1つだけである。
G46. 用語:なぜCNでのホスト名照合が禁止されたのか
CNは元々「人や組織の名前」を入れる汎用のフィールドで、ホスト名専用ではなかった。それを流用したのが混乱の元である。
具体的な問題が2つある。
-
型が無い —
CN=Example CorpなのかCN=example.comなのか、文字列を見ても区別できない。実装ごとに解釈が割れる - 1つしか書けない — 複数ホスト名に対応できず、結局SANが必要になる
さらに、過去にはCNにNULL文字を仕込んで検証を騙す攻撃も報告された。
経緯としては次のとおり。
-
RFC 2818(2000年)— SANがあればSANを使い、無ければ
CNを使う、とされた -
RFC 6125(2011年)—
CNの使用を非推奨に - RFC 9525(2023年)— MUST NOT。RFC 6125を廃止
実装側の対応はもっと早く、Chromeは2017年にCNへのフォールバックを削除している。現在のブラウザ・macOS・curl・GoはいずれもSANしか見ない。
8. クライアント証明書を作成する より
G47. なぜクライアント証明書ではCNが禁止されていないのか
RFC 9525が対象にしているのは "Service Identity"、つまり「接続先のサーバが名乗ったホスト名と一致するか」の照合である。
クライアント証明書のCNは、これとは用途がまったく違う。
サーバ証明書のCN
|
クライアント証明書のCN
|
|
|---|---|---|
| 用途 | ホスト名の照合 | アプリケーションが決める識別子 |
| 照合する主体 | TLSライブラリ(自動) | アプリ/サーバの設定(手動) |
| 規格上の扱い | 禁止(RFC 9525) | 規定なし=自由に使ってよい |
クライアント証明書のCNに何を入れるか、それをどう使うかは、完全にシステム側の設計判断である。だから資産番号でも社員番号でもデバイスIDでも構わない。
「CNは使ってはいけない」という話を聞いたことがあると、ここで混乱しがちである。禁止されているのはサーバ側のホスト名照合だけと押さえておけば区別できる。
G48. 用語:ロールオーバー(rollover)
証明書を、切れ目なく新しいものに入れ替えることである。
単純に「期限が切れたら新しく発行する」だと、切り替えの瞬間にサービスが止まる。そこで、期限が切れる前に新しい証明書を発行し、一時的に新旧2枚が有効な状態を作る。
旧証明書 ├────────────────────┤
新証明書 ├────────────────────┤
└─ この期間は両方有効(重なり)
この「重なり」を作るには、同じSubjectの証明書が同時に2枚有効でなければならない。unique_subject = yesのままだとこれができない、というのがnoを推奨する理由である。
device-001とdevice-001-renewedは、まさにこの重なりの状態を再現したものである。
G49. ⚠️ unique_subjectの設定が効かないことがある
unique_subjectの値はindex.txt.attrという別ファイルに書き出され、そのファイルが既にあるとそちらが優先される。
つまり、一度unique_subject = yesの状態で発行したディレクトリを使い回すと、設定を変えても効かない。CAディレクトリごと作り直すのが確実である。
G50. 用語:末尾のZは何か
UTC(協定世界時)であることを示す記号である。Zulu timeのZ。
証明書の有効期間は、内部的にASN.1の時刻型で保持される。
-
YYMMDDHHMMSSZ— UTCTime(2桁年。2049年まで) -
YYYYMMDDHHMMSSZ— GeneralizedTime(4桁年)
どちらも末尾にZが要る。証明書の時刻は常にUTC で、タイムゾーンの概念がない。
openssl x509 -textで表示される日時もUTCである。日本時間で見る場合は9時間足して読む。
G51. 用語:PKCS#12(.p12 / .pfx)
証明書と秘密鍵(と必要ならCAチェーン)を1つのファイルにまとめた形式である。仕様は RFC 7292。
- 拡張子は
.p12または.pfx(中身は同じ。.pfxはWindows系でよく使われる呼び方) - 必ずパスワードで保護される。秘密鍵を含むため
- 端末に「身元(identity)」を渡すための、事実上の標準フォーマット
なぜPEMのままではいけないのか、を整理する。
| PEMファイル群 | .p12 |
|
|---|---|---|
| ファイル数 | 3つ(鍵 / 証明書 / CAチェーン) | 1つ |
| 秘密鍵の保護 | 平文のまま(-nodesの場合) |
パスワードで暗号化される |
| OSへの取り込み | 対応していないOSもある | macOS / Windows / iOS / Androidすべて対応 |
| 鍵と証明書の対応 | 人が正しく組み合わせる必要がある | ファイル内で対応づけ済み |
macOSではダブルクリックするとキーチェーンに取り込まれ、パスワードを聞かれる。
.p12は秘密鍵を含む。 メールやチャットで送る、リポジトリに置く、といった扱いをしてはいけない。端末に入れたら元ファイルは消す。
G52. 用語:.p12のパスワードは何を守っているのか
.p12はパスワード1つで守られているが、内部では3つの部品が働いている。
1. 鍵導出(KDF)— パスワードから暗号鍵を作る
パスワードをそのまま暗号鍵には使えない(短すぎる、偏りがある)。計算を何千回も繰り返して固定長の鍵を作る。OpenSSL 3の既定は PBKDF2。
2. 暗号化 — 中身を暗号化する
OpenSSL 3の既定は AES-256-CBC である。
The default encryption algorithm is AES-256-CBC with PBKDF2 for key derivation.
(openssl-pkcs12)
3. MAC — 改竄検知とパスワード確認
MAC(Message Authentication Code) は、中身が書き換えられていないかを検証する符号である。パスワードが正しいかの判定も兼ねている。既定はSHA-256。
パスワードを間違えたときにMAC verification failedというエラーが出るのは、この3番目で落ちているためである。
守っている対象は「端末へ渡すまでの間のファイル」であって、TLS通信そのものの強度とは別の話である。
9. 失効リスト(CRL)を作成する より
G53. 用語:失効理由(crl_reason)
CRLには「なぜ失効させたか」の理由を記録できる。openssl caで指定できる値は次のとおり。
| 値 | 意味 |
|---|---|
unspecified |
理由を明示しない |
keyCompromise |
秘密鍵が漏れた |
CACompromise |
CAの秘密鍵が漏れた |
affiliationChanged |
所属が変わった(退職・部署異動など) |
superseded |
新しい証明書に置き換えた |
cessationOfOperation |
運用を終了した |
certificateHold |
一時停止。後で解除できる |
removeFromCRL |
一時停止を解除する |
実務上の使い分けの目安は次のとおり。
- 端末を紛失した →
keyCompromise - 端末を廃棄・返却した →
cessationOfOperation - 証明書を更新して旧証明書を止める →
superseded - 調査中で一旦止めたい →
certificateHold
なお、理由を指定するとCRLがv2形式になる。
Setting any revocation reason will make the CRL v2.
(openssl-ca)
G54. 用語:-crlextsとCRLのバージョン
-crlextsは、CRLに含める拡張の節を指定するオプションである。
If no CRL extension section is present then a V1 CRL is created, if the CRL extension section is present (even if it is empty) then a V2 CRL is created.
(openssl-ca)
- v1 CRL — 拡張が無い古い形式
- v2 CRL — 拡張を持てる。失効理由やCRL番号を記録できる
この記事では[ crl_ext ]にauthorityKeyIdentifierを入れてv2にしている。節が空でも v2になる、という点が独特である。「拡張セクションが存在するか」で判定される。
G55. なぜCRL自体に有効期限があるのか
一見おかしな仕様に見える。「失効リストが期限切れになったら、失効が解除されるのか」と。
答えは逆で、期限切れのCRLは検証エラーになる(失効が解除されるのではなく、検証そのものが通らなくなる)。
理由はリプレイ攻撃の防止である。
CRLに期限が無いと、次の攻撃が成立する。
- 攻撃者が「まだ何も失効していない」時点の古いCRLを保存しておく
- その後、攻撃者の証明書が失効させられる
- 検証する側に、保存しておいた古いCRLを掴ませる
- 「失効していない」と判定される
CRLに「この日まで有効」と書いておけば、古いCRLは期限切れとして弾かれる。CRLの鮮度を強制するための仕組みである。
運用上は、default_crl_daysより短い間隔でCRLを再生成し、配置し直す必要がある。cronなどで自動化するのが一般的である。
10. 生成物を検証する より
G56. 用語:-CAfileと-untrustedの違い
どちらも「検証に使う証明書」を渡すオプションであるが、意味がまったく違う。
| オプション | 渡すもの | 意味 |
|---|---|---|
-CAfile |
ルートCA | トラストアンカー。これを無条件に信頼する |
-untrusted |
中間CA | チェーンをつなぐための材料。それ自体は信頼しない |
-untrusted(信頼しない)という名前が分かりにくいところである。「信頼の起点にはしないが、チェーンを組み立てる材料としては使う」という意味である。
実際の検証はこう進む。
- サーバ証明書のIssuerを見る → 「中間CAが発行した」と分かる
-
-untrustedの中から中間CAを探す → 見つかる - 中間CAのIssuerを見る → 「ルートCAが発行した」と分かる
-
-CAfileの中からルートCAを探す → 見つかる。信頼の起点に到達 - 各段の署名を検証する → すべて正しければOK
もし中間CAを-CAfileに入れてしまうと、「中間CAを信頼の起点とみなす」ことになり、ルートCAまで遡らなくても検証が通ってしまう。本来検証したいことが検証できない。
G57. 用語:よく見るopenssl verifyのエラー番号
エラー番号と原因の対応は次のとおり。切り分けに使える。
| 番号 | メッセージ | 原因 |
|---|---|---|
| 2 | unable to get issuer certificate | 発行者の証明書が見つからない |
| 10 | certificate has expired | 有効期限切れ |
| 18 | self-signed certificate | 自己署名証明書を検証しようとした |
| 19 | self-signed certificate in certificate chain | チェーンの途中に、信頼していない自己署名証明書がある |
| 20 | unable to get local issuer certificate | チェーンがたどれない(中間CAが渡されていない等) |
| 21 | unable to verify the first certificate | 同上。チェーンが不完全 |
| 23 | certificate revoked | 失効している |
| 24 | invalid CA certificate | CAとして使えない(basicConstraintsが無い等) |
at 0 depthの0は、どの段で失敗したかを示す。0なら末端の証明書、1なら中間CAである。
11. nginxで動作確認する より
G58. 用語:ssl_verify_depthの「深さ」の数え方
ssl_verify_depthは、証明書チェーンをどこまで遡ってよいかの上限である。nginxの既定値は 1。
数え方に注意が要る。OpenSSLの公式ドキュメントには次のようにある。
Limit the certificate chain to num intermediate CA certificates. A maximal depth chain can have up to num+2 certificates, since neither the end-entity certificate nor the trust-anchor certificate count against the -verify_depth limit.
(openssl-verification-options)
数えるのは中間CAの枚数だけである。末端の証明書とルートCAは数に入らない。
クライアント証明書 ← 数えない(end-entity)
↑
中間CA ← これが1枚 → depth 1 で足りる
↑
ルートCA ← 数えない(trust anchor)
したがって、この記事の構成(中間CA 1枚)では既定の1で足りる。設定を書く必要はない。2が必要になるのは中間CAが2段になったときである。
G59. 用語:nginxの変数($ssl_client_*)
nginxは、TLSの検証結果を変数としてアプリケーションに渡せる。主なものは次のとおり。
| 変数 | 内容 |
|---|---|
$ssl_client_verify |
検証結果。SUCCESS / FAILED:理由 / NONE
|
$ssl_client_s_dn |
クライアント証明書のSubject(RFC 2253形式) |
$ssl_client_i_dn |
発行者(Issuer)のDN |
$ssl_client_serial |
シリアル番号 |
$ssl_client_fingerprint |
証明書のSHA1フィンガープリント |
$ssl_client_v_end |
有効期限の終了日 |
$ssl_client_v_remain |
期限までの残り日数 |
$ssl_client_escaped_cert |
証明書そのもの(URLエンコード済みPEM) |
実際の構成では、これらをヘッダとしてアプリに転送する。
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-Client-Serial $ssl_client_serial;
こうすると、アプリ側は TLSの処理を一切書かずに「どの端末から来たか」を知ることができる。