自分だけの認証局を作って理解するHTTPS――PKI Labで証明書の発行・失効・HTTPキャッシュを実験する

自分だけの認証局を作って理解するHTTPS――PKI Labで証明書の発行・失効・HTTPキャッシュを実験する
目次





この記事の要点

  • 証明書の発行と、その証明書を信頼する判断は別です。 PKI Labでは、ルートCA・中間CA・利用者端末を分けて学びます。
  • 証明書を失効させても、新しい失効情報を確認するまでは接続できる場合があります。 HTTPキャッシュによる反映待ちを、実際のTLS接続で再現します。
  • 「失効済み」と「失効状態を確認できない」は別の結果です。 本ラボでは、確認不能を成功扱いにしません。
  • 3D教材は説明用、ラボの実行ログは観測結果です。 映像を再生しただけで、鍵生成や証明書の検証が実行されたことにはしません。

上記は、本稿で扱うRound 5の学習目標と実験範囲です。

HTTPSの証明書エラーを、検索で見つけたコマンドだけで解消していないでしょうか。

証明書を作れた。HTTPSにつながった。証明書を失効させた。この3つの操作は、それぞれ違う確認を必要とします。

本稿では、学習用の私設認証局 PKI Lab と、Three.jsによる3D教材 「信頼のアトリエ」 を使います。発行から接続、失効、HTTPキャッシュ、監査、復旧までを一続きで試します。

目指すのは、コマンドを暗記することではありません。「誰が、何を根拠に、この接続を許可したのか」を説明できることです。

対象版:Round 5配布版/確認日:2026年10月9日

本稿のHTTP機能は、ca-round5-clarity.zipに含まれる版を対象にしています。確認時点のGitHub PR #1は00bcdb2であり、Round 5は未反映です。GitHubをcloneするだけでは、本稿のHTTP用コマンドを実行できません。[^pr] [^round5]

1. 何を作ったのか――「発行できる」から「なぜ拒否されるか分かる」へ

PKI Labは、OpenSSLによる署名・検証を、Python製のコマンドで操作する学習用ラボです。「信頼のアトリエ」は、同じ概念を空間とアニメーションで説明します。

機能実際に試せること
CAの構築ルートCAと中間CAを作る
発行サーバー側の鍵生成、申請、審査、承認、署名、配置
検証信頼するCA、接続先名、期限、用途、失効情報を確認する
HTTP配布CRLの取得、200/304、キャッシュ、配布停止、更新反映を比較する
運用監査ログと台帳の照合、暗号化バックアップ、復旧保留と再開
3D教材180秒・18場面、証明書の比較9条件、HTTP比較7条件、理解確認3問

構成と機能の範囲は、配布版の実装・手順書に基づきます。[^pr] [^round5]

この記事で分かることは、証明書を発行する手順、失効確認の結果を読む方法、結果が期待と違ったときの調べ方です。まず一括デモで全体を確認し、その後、手動操作で各工程を追います。

2. 自分に関係するか――対象読者と前提環境

対象は、HTTPSを設定する開発者、証明書更新を担当する運用者、PKIを基礎から学び直したいエンジニアです。

本稿はLinuxまたはWSL2のBashを前提にします。PythonからOpenSSLを呼び出すため、両方が必要です。WindowsのネイティブPythonでの実行手順ではありません。

項目本稿の前提
実行環境Linux、またはWindows上のWSL2
Python記事執筆時の再実行環境は3.13.5
OpenSSL記事執筆時の再実行環境は3.5.5
Node.js教材ロジックのテストに使用。再実行環境は22.16.0
Three.js配布版にr169を同梱。教材表示のためのCDN取得は不要
証明書の対象名localhostと127.0.0.1
管理範囲自分のPC内の学習環境

これらのバージョンは再現環境の記録であり、最新版を示す一覧ではありません。配布版の動作前提・制約も確認してください。[^round5] [^pr]

先にサンプルを用意する

Round 5のサンプル一式を入手する

ZIPを展開し、lab/とviz/が入っている CA/ディレクトリ を開きます。以降、特記しないコマンドは、このCA/で実行します。

展開したフォルダー/
├── START-HERE.md
├── CA/                  ← コマンドの実行場所
│   ├── lab/
│   │   ├── pkilab.py
│   │   ├── crl_http.py
│   │   ├── config/
│   │   └── scripts/http_demo.py
│   ├── viz/
│   └── docs/
└── validation/

既存のCA作業領域へ上書きしないでください。 まず配布ZIPを別の場所へ展開して試します。既存Gitリポジトリへのパッチ適用は、同梱のSTART-HERE.mdに分けています。適用基点は00bcdb2です。[^round5]

このラボでは、OS全体の信頼設定を変更しない

自作ルートCAをOSへ登録する操作や、ブラウザの警告を無視する操作は行いません。検証用コマンドだけに、信頼するCAを指定します。

また、秘密鍵、鍵を開けるためのパスフレーズ、実行時の作業領域を、ブログやGitへ公開しないでください。同一PCでの役割切替は、学習のための論理分離です。別の管理者や別の機械による隔離を実現するものではありません。[^operations] ^quality

3. 用語と全体像――秘密鍵は残し、申請書と証明書を渡す

3.1 まず、登場人物を整理する

用語たとえと正式な意味
CA証明書へ署名する発行元です。Certification Authority、認証局を指します
ルートCA信頼関係の出発点として採用するCAです。採用するかは検証側が決めます
中間CAルートCAなどから証明書を発行され、配下の証明書を発行するCAです
RA申請の受付・審査窓口に相当します。Registration Authority、登録局です
秘密鍵・公開鍵署名するための鍵と、その署名を確かめるための対応する鍵です。本稿の署名の場面では、この役割で考えます
CSR署名付きの申込書です。公開鍵や申請情報を含む証明書署名要求です
SAN証明書に記載する接続先名の欄です。Subject Alternative Nameで、DNS名とIPアドレスは別の型です
EKU証明書の用途欄です。Extended Key Usageで、serverAuthはTLSサーバー認証用途を表します
信頼ストア検証側が採用した信頼の起点を保持する場所です
CRL発行元が署名した失効一覧です。Certificate Revocation Listを指します

役割と証明書拡張はRFC・OpenSSLの定義に沿っています。CSRについてはRFC 2986、証明書の検証についてはOpenSSLの検証仕様も参照できます。[^rfc2986][^openssl-verify][^x509-config]

3.2 発行する側と、信頼する側は別

【発行側】
ルートCAの秘密鍵
    └─ 中間CA証明書へ署名
            └─ 中間CAの秘密鍵でサーバー証明書へ署名

【サーバー側】
サーバー秘密鍵 ── 自分の場所に保持
    └─ CSRを作成して署名 ── RAの審査 ── 中間CAへ

【利用側】
事前に選んだルートCAを信頼
    + サーバーが提示する証明書と中間CA証明書
    + 検証に必要な失効情報
    → 証明書と接続先を検証
    → TLSハンドシェイクで相手の認証を確認

CSRにサーバー秘密鍵は入れません。 ただし、CSRには名前などの申請情報が含まれます。「秘密鍵ではないから、内容を確認せず公開してよい」とは考えないでください。[^rfc2986]

CSRの署名が正しいことと、申請者がその名前を使ってよいことも別です。本ラボでは、対象名を限定し、RA役が申請を承認します。公的な本人確認を実装しているわけではありません。^quality

サーバーから届いたルート証明書を、その場で自動的に信頼登録する設計でもありません。TLSでは、信頼の起点を受信側が独立に持つため、送信チェーンからルート証明書を省略できます。[^rfc8446]

3.3 「信頼のアトリエ」の各部屋が表すもの

空間見るポイント
ルートCA室中間CAへの委任。金色の秘密鍵は移動しない
中間CA室承認された申請への署名
RA申請窓口CSRの検査と、発行する内容の承認
サーバー区画秘密鍵の保持と、公開証明書の提示
利用者端末・信頼ストア利用側が信頼の起点を選ぶ
検証ゲート経路、SAN、期限、用途、失効、TLS認証の違い
CRL・OCSP区画失効情報の配布。OCSPは比較展示であり実レスポンダーではない

この配置は理解のための比喩です。ゲートはネットワーク上の別サービスではなく、利用側の確認を拡大した展示です。順番も、実ブラウザ内部の固定された実行順ではありません。ルートCA室の見た目が隔離されていても、実装の秘密鍵は同じPC上にあります。^quality

4. なぜ失効させても接続できるのか――「公開」と「利用側への反映」は別

失効は、証明書ファイルを削除する操作ではありません。 発行元の失効情報へ記録し、利用側がその情報を使って判定します。CRLの仕様はRFC 5280に定義されています。[^rfc5280]

本ラボでは、次の対応になります。

失効を確認する対象参照するCRL
サーバー証明書中間CAが署名したCRL
中間CA証明書ルートCAが署名したCRL
信頼の起点として採用したルート利用側での信頼設定の見直し。CRLだけで自動削除されるとは扱わない

さらにHTTPキャッシュが加わると、配布元と利用側で持っている版が異なる場面ができます。[^httpguide]

1. 利用側がCRL #100を取得する。対象証明書は未失効。
2. 発行元が対象証明書を失効させ、CRL #101を公開する。
3. 利用側は、再利用条件を満たす#100を使う。
   → 新しい失効をまだ知らず、接続できる場合がある。
4. 利用側が配布元へ再確認し、#101を取得する。
   → 次の新規接続を拒否する。

これは本ラボで再現する条件です。一般のブラウザすべてが、同じ方式・同じ秒数で失効確認するという説明ではありません。[^round5]

HTTPキャッシュとCRLには、別々の期限がある

期限・情報意味
HTTPのmax-ageなど保存したHTTP応答を、配布元へ再確認せずに再利用できる条件
CRLのthisUpdateCRLの発行時刻
CRLのnextUpdate次のCRLが発行される予定の上限時刻。本ラボでは再利用の期限としても確認
CRL番号同じ発行元について、既に確認した版より後退していないか調べる情報

HTTPの再利用期限が残っていても、CRL自身の署名付き期限を過ぎれば採用しません。逆に、期限内で署名が正しいCRLでも、配布元にさらに新しい版が存在する場合があります。[^rfc9111][^rfc5280]

HTTP 200は本文を受信できたという結果です。304は、条件付き要求に対して本文の再送を省略する応答です。どちらも、CRLの署名や証明書全体の安全性を保証するものではありません。 特に304を受けても、署名済みのnextUpdateは書き換わりません。[^rfc9110][^openssl-crl]

5. 実行前の確認――版、実行場所、秘密情報の置き場をそろえる

5.1 実行環境を確認する

目的・場所: CA/で、Python・OpenSSL・Node.jsとHTTP対応版のCLIを確認します。Node.jsの確認はテストも実行する場合に必要です。

python3 --version
openssl version
node --version
python3 lab/pkilab.py verify --help

正常例: 各バージョンが表示され、ヘルプに--crl-source {file,http}と--crl-policy {revalidate,cache}があります。

異常例と判断: command not foundは実行環境の不足です。HTTP用オプションがなければ対象版が違います。証明書の問題として調べ始めず、環境と配布版を先に確認してください。HTTPモードの仕様は付属手順書にも記載しています。[^httpguide]

5.2 実験を2種類に分ける

本稿では、作業領域を混ぜません。

一括HTTPデモは一時CAを作り、終了時に削除します。手動操作は$HOME/pki-lab-blogへCAを作り、終了後も状態を残します。

手動操作をやり直す場合は、既存のディレクトリを削除せず、別の名前の空の場所を使ってください。秘密鍵だけを残して台帳を初期化する操作は行いません。[^httpguide] [^operations]

6. まず一括デモ――HTTP取得から、キャッシュによる失効の反映待ちまで試す

目的・場所: CA/で、一時CAを使った実HTTP・実TLSの比較を実行します。既存CAやOSの信頼ストアは変更しません。

python3 lab/scripts/http_demo.py --events-out http-events.json

正常例: JSONに次の8段階が出力され、コマンド全体は終了値0になります。http-events.jsonには教材へ読み込むイベントが保存されます。

実験段階期待する結果分かること
HTTP_200_SIGNED_CRLOKCRLを取得・検査したうえで、証明書検証に成功
HTTP_304_SAME_CRLOK、HTTP 304同じCRLの本文を再利用して検証
NEW_TLS_ACCEPTOK、TLSv1.3実際の新規TLS接続に成功
OLD_FRESH_CACHE_CAN_STILL_ACCEPTOK失効公開後も、古い有効キャッシュで接続できる
REFRESH_NEW_CRL_NEW_TLS_REJECTREVOKED新しいCRLを取得した後の新規接続は拒否
OUTAGE_FRESH_CACHE_NO_HTTP_CONTACTOK配布停止でも有効キャッシュを明示的に使える
OUTAGE_REVALIDATION_REQUIREDCRL_HTTP_UNAVAILABLE配布元への再確認が必要なら、停止時は判定不能
OUTAGE_HTTP_CACHE_EXPIREDCRL_HTTP_UNAVAILABLEキャッシュ期限が過ぎた後も、停止を無視して許可しない

配布停止の最後の3ケースは、別に発行した未失効の証明書のファイル検証で比較します。直前に失効させた証明書が、再び有効になったわけではありません。実装の取得方式・期待結果は、配布手順と観測記録に基づきます。[^round5]

異常例と判断: 期待する終了値や結果コードに一致しなければ、デモは例外・非ゼロ終了になります。途中のREVOKEDは期待する拒否ですが、任意のエラーをデモ成功と数えるわけではありません。強制終了などで一時作業領域が残った場合は、残存した鍵も秘密情報として扱います。

最後のキャッシュ期限切れだけは、検証者に与える時刻を60秒進めています。OSの時計を変えた実験でも、実時間で60秒待つ実験でもありません。[^httpguide]

JSONは「結果→取得方法→観測範囲」の順で読む

例えば、古い有効キャッシュを使う場合は、次の組合せに注目します。以下は表示を絞った例です。

{
  "result": "ACCEPT",
  "code": "OK",
  "transport": {
    "policy": "cache",
    "latest_guaranteed": false,
    "crls": [
      {
        "which": "intermediate",
        "source": "cache_fresh",
        "publisher_contacted": false
      }
    ]
  }
}

transport.crlsには各CRLの記録が入り、上の例は中間CAの項目だけを抜粋しています。publisher_contacted:falseなら、その取得では配布元へ問い合わせていません。配布元が動いていると確認した結果でも、最新版だと証明した結果でもありません。 [^observations]

7. 3D教材の使い方――映像で全体をつかみ、ログで事実を確かめる

7.1 教材だけをローカル配信する

目的・場所: CA/の別ターミナルで、静的な教材ファイルだけを配信します。

python3 -m http.server 8765 --bind 127.0.0.1 --directory viz

ブラウザでhttp://127.0.0.1:8765/?autoplay=0を開きます。

正常例: 配信開始メッセージが表示され、教材の3D空間と操作パネルが開きます。

異常例と判断: ポート使用中なら競合を解消します。WebGLのエラー表示が出る場合は、配信成功と3D描画成功を分けて調べます。静止画や文章だけを表示できても、3D動作を確認したことにはしません。

--directory vizは省略しないでください。 CAの作業領域を教材と一緒に配信する必要はありません。外部へ公開するために待受を変更する手順も、本稿には含めません。

WSLで動かしたアプリをWindows側から開く場合のlocalhost接続は、MicrosoftのWSLネットワーク資料も参照できます。[^wsl]

7.2 最初は「本編」を一度見る

「条件を切り替える」で本編を選び、再生します。最初は全体を見て、次にRA、信頼ストア、失効確認の場面へ戻ると、判断の違いを追いやすくなります。

操作使いどころ
再生・一時停止移動している物がCSRか証明書かを確認する
再生速度0.5倍説明を読みながら追う
場面一覧・シークバー確認したい工程へ戻る
「詳しく」短い説明から、条件や制約を掘り下げる
カメラの自動切替を外す一つの区画を固定して観察する
正常・名前違い・失効などの条件同じ確認で、何が変わると止まるか比較する

7.3 HTTPの7条件を、3D本編とは分けて比較する

「HTTP配布・キャッシュを比べる」を開きます。「次へ」で、取得、キャッシュ、CRL、接続判断の関係を順に確認できます。

おすすめの順番は、通常取得→更新の反映待ち→配布停止→期限切れCRLへの304です。その後、番号の後退と署名不正を確認します。

最後に「3問で理解を確認する」で答え合わせをします。正答を覚えるより、「200は何を保証しないか」「配布停止から何を断定できないか」「304で何が変わらないか」を説明してみてください。これらの比較は合成データであり、パネル操作でHTTP通信する機能ではありません。^quality

7.4 自分が実行したイベントを読み込む

「実測イベント(PKI Lab)」から、先ほどのhttp-events.jsonを選びます。手動ラボのイベントを使う場合は、後述するexport-eventsで出力します。

イベントを選んだときに移る場面は、関連する説明箇所です。実際の暗号処理を再実行したり、ハンドシェイク内部の順序を忠実に再生したりするものではありません。 画面は、ファイルの作成者や真正性を独立に認証する機能でもありません。^quality

7.5 動作の重さは、同じ構図で測る

「この端末の描画を測る」で、今の構図を対象に測定します。準備1秒の後に10秒記録し、結果JSONを保存できます。測定中にタブ移動や画面操作をした場合は、結果を無効として扱います。^quality

FPSとフレーム間隔は、ブラウザで画面更新できた間隔です。GPUだけの実行時間ではありません。Three.jsのgeometriesとtexturesも個数であり、VRAMの使用MBではありません。vramBytes:nullは、0MBではなく未取得を意味します。[^three-renderer]

8. 手動で発行する――各コマンドで状態の変化を見る

一括デモとは別に、状態を残すラボを作ります。各ターミナルの実行場所はCA/です。作業領域は一貫して$HOME/pki-lab-blogを使います。

ターミナル役割
A初期化、申請、発行、検証、失効などの操作
BCRLのHTTP配布サーバーを起動したままにする
CHTTPSサーバーを起動したままにする

$REQは申請IDを入れるBash変数です。別ターミナルには自動で引き継がれないため、必要なターミナルで同じIDを入力します。

8.1 ルートCA・中間CA・初期CRLを作る

目的・場所: ターミナルAで、新しい学習用CAを初期化します。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" init

正常例: init_root、init_issuer、sign_intermediate、crl_root、crl_issuerの結果が返ります。

異常例と判断: ALREADY_INITIALIZEDなら既存のCAです。削除して通すのではなく、既存状態を使うか、別の空の作業領域へ切り替えます。

初期化では、失効0件のCRLも作ります。「失効した証明書がまだない」ことと、「確認するCRLが不要」なことは別です。[^operations]

8.2 サーバーの鍵とCSRを作り、申請IDを控える

目的・場所: ターミナルAで、サーバー側の鍵と申請を作ります。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" request
read -r -p '表示されたrequestの値を貼り付けてください: ' REQ

正常例: requestにREQ-日付-識別子形式のID、csrに申請ファイルのパスが返ります。入力するのはIDだけで、引用符は含めません。

異常例と判断: ファイルの書込みエラーやIDの入力誤りでは、後続の処理を進めません。IDが違う場合はUNKNOWN_REQUESTなどになります。

8.3 審査・承認した後、中間CAで発行する

目的・場所: ターミナルAで、RAの承認と中間CAの署名を別々に実行します。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" approve "$REQ" \
  --asset-owner lab-https-01

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" issue "$REQ"

正常例: 承認ではapproval_idが返り、発行ではserial、cert、fullchainが返ります。

異常例と判断: 許可外の名前はSAN_NOT_ALLOWED、承認前の発行はNOT_APPROVEDなどで止まります。承認後にCSRを変更して通らなくなった場合も、検査を外さず申請から見直します。

--asset-ownerは、この学習ラボの資産IDとの照合です。実組織の所有権を外部へ問い合わせて認証する機能ではありません。また、コマンドごとの役割名は学習用で、同一ユーザーのOS権限を分割するものではありません。

本ラボはCSRの拡張をそのままコピーせず、CA側の発行プロファイルを使います。OpenSSL公式文書も、CSRの拡張を不用意にコピーする危険を説明しています。[^openssl-ca]

8.4 証明書の中身を表示する

目的・場所: ターミナルAで、発行された公開証明書の名前・用途・期限を読みます。秘密鍵は表示しません。

openssl x509 \
  -in "$HOME/pki-lab-blog/server/certs/$REQ.cert.pem" \
  -noout -subject -issuer -dates \
  -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage

正常例: SANにDNS:localhostとIP Address:127.0.0.1、基本制約にCA:FALSE、用途にサーバー認証が表示されます。

異常例と判断: ファイルを開けない場合は申請IDと配置先を確認します。期待した拡張がなければ、配置した証明書と発行設定を確認します。表示できたことは、署名や接続の検証成功ではありません。 拡張の意味はOpenSSLのX.509設定資料で確認できます。[^x509-config]

8.5 証明書ファイルを検証し、名前違いも確認する

目的・場所: ターミナルAで、正しい名前と誤った名前を比較します。この段階ではCRLをファイルから読みます。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" verify --request "$REQ" \
  --trust "$HOME/pki-lab-blog/public/certs/root.cert.pem"

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" verify --request "$REQ" \
  --host example.com

正常例: 1本目はACCEPT / OK、2本目はREJECT / SAN_MISMATCHです。2本目の終了値1は、比較実験として期待する拒否です。

異常例と判断: 1本目も失敗する場合は、名前以外にCA、期限、CRLの状態を確認します。2本目が受理されるなら、名前検証の条件を調べる必要があります。

--trustは、この検証で使う信頼の起点を指定します。省略時も、ラボは作業領域のルート証明書を使います。OSの信頼ストアを変更する操作ではありません。

verify --host example.comは、証明書ファイルと照合する名前を変える操作です。example.comへアクセスするコマンドではありません。

DNS名とIPアドレスは型を分けて検査します。「SANに何か入っていればよい」「CNが合えばよい」という条件にはしません。TLSの接続先識別はRFC 9525を参照してください。[^rfc9525]

9. HTTPとTLSを接続する――取得の成功と接続の成功を分ける

9.1 CRL配布サーバーを起動する

目的・場所: ターミナルBで、許可した公開情報だけをHTTP配布します。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" serve-public \
  --port 8000 --max-age 5

正常例: PKI Lab public: http://127.0.0.1:8000/と表示され、起動状態を維持します。

異常例と判断: ポート競合、公開領域の不足、不正な設定なら起動できません。外部公開のために待受先を変更しないでください。

HTTPで配るCRLはDER形式です。内部の.crl.pemを、そのままHTTP用のバイナリCRLだと扱いません。本ラボは完全・直接CRLを対象とし、未対応の範囲やcritical拡張は拒否します。[^httpguide]

9.2 HTTPから取得したCRLで証明書を検証する

目的・場所: ターミナルAで、HTTP取得を明示して検証します。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" verify --request "$REQ" \
  --crl-source http --crl-base-url http://127.0.0.1:8000/

正常例: ACCEPT / OK、transport.mode:httpが返ります。初回はhttp_200、同じCRLで再実行するとhttp_304になることがあります。

異常例と判断: INDETERMINATEなら、codeとstopped_atを確認します。HTTPモードが失敗しても、CA内のローカルCRLへ自動的に切り替えて成功させません。[^httpguide]

この実装が取得するのは、明示されたhttp://127.0.0.1:<port>/だけです。証明書内の任意URLを自動的にたどる汎用CRLクライアントではありません。[^httpguide]

9.3 HTTPSサーバーを起動する

目的・場所: ターミナルCで、先ほど発行した証明書とサーバー秘密鍵を使います。

read -r -p '発行済みの申請IDを貼り付けてください: ' REQ
python3 lab/pkilab.py --home "$HOME/pki-lab-blog" serve-https "$REQ" --port 8443

正常例: PKI Lab HTTPS: https://localhost:8443/と表示されます。

異常例と判断: 未配置ならNOT_PUBLISHED、ポート競合や鍵・証明書の読込み失敗なら起動しません。検証クライアントを調べる前に、サーバーの起動結果を確認してください。

9.4 新規TLS接続を確認する

目的・場所: ターミナルAで、HTTP取得したCRLを使うTLSクライアントを実行します。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" client --port 8443 \
  --crl-source http --crl-base-url http://127.0.0.1:8000/

正常例: ACCEPT / OK、tls_version:TLSv1.3が返ります。

異常例と判断: CRL_HTTP_UNAVAILABLEなら失効情報の取得側、接続拒否ならHTTPSサーバー側、証明書拒否ならcodeに対応する確認を調べます。HTTP取得で止まった場合は、TLS接続自体を開始していません。[^httpguide]

証明書はコピーできます。TLS 1.3の証明書ベース認証では、CertificateVerifyなどにより、そのハンドシェイクで相手が対応する秘密鍵を利用できることを確認します。証明書ファイルの検証だけで、通信相手の認証まで完了したとは扱いません。 [^rfc8446]

10. 失効を試す――確認済みの失効と、通信障害を区別する

10.1 学習用証明書を失効させる

注意:この操作は状態を変更します。 以下は、この記事で新しく作った学習用証明書だけを対象にします。既存業務の証明書へ実行しないでください。状態を残したい場合は、先に第13節のバックアップを行います。サーバー秘密鍵が実際に漏えいしたという意味ではなく、keyCompromiseという失効理由を使った演習です。

目的・場所: ターミナルAで、失効要求からCRL公開までを進めます。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" revoke "$REQ" \
  --reason keyCompromise

正常例: revoked、reason、crlが返ります。

異常例と判断: REVOCATION_PENDINGなどで止まった場合は、公開完了ではありません。原因を解消し、未完了処理を再試行します。台帳に失効が載ったことだけで、利用側に反映済みとは判断しません。[^operations]

10.2 新しいCRLで拒否されることを確認する

目的・場所: ターミナルAで、配布元へ再確認し、ファイル検証と新規TLS接続の両方を確認します。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" verify --request "$REQ" \
  --crl-source http --crl-base-url http://127.0.0.1:8000/ --crl-refresh

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" client --port 8443 \
  --crl-source http --crl-base-url http://127.0.0.1:8000/ --crl-refresh

正常例: verifyはREJECT / LEAF_REVOKED、clientはREJECT / REVOKEDです。どちらも期待する終了値は1です。

異常例と判断: 受理された場合は、対象証明書、CRL番号、取得方式、確認の無効化を調べます。配布障害で判定不能になった場合は、失効を確認できたことにはしません。

verifyとclientでコードの粒度が違うのは、TLSの例外から同じ接続で取得できる情報に違いがあるためです。REVOKEDを、根拠なしに葉または中間CAの失効へ言い換えません。 [^httpguide]

10.3 キャッシュの再利用と、強制再確認を使い分ける

既定のrevalidateは毎回再確認します。cacheは、再利用条件を満たすキャッシュがある場合に、配布元へ問い合わせずに使います。

目的・場所: ターミナルAで、同じ証明書に対する取得方法を比較します。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" verify --request "$REQ" \
  --crl-source http --crl-policy cache

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" verify --request "$REQ" \
  --crl-source http --crl-policy cache --crl-refresh

正常例: 有効なキャッシュがあれば1本目はcache_fresh、2本目は配布元へ再確認します。上から順に操作した場合は、既に失効を載せたCRLを取得しているため、どちらも失効を検出して構いません。

異常例と判断: 手動操作中にキャッシュ期限が過ぎれば、1本目でもHTTP取得へ進みます。古い有効キャッシュで接続できる場面を確実に追いたい場合は、第6節の一括デモを使ってください。再利用の上限はラボ固有の設計値であり、一般のブラウザに適用される秒数ではありません。[^httpguide]

10.4 配布サーバーを停止すると、失効とは別の結果になる

ターミナルBの配布サーバーだけをCtrl+Cで停止します。HTTPSサーバーは起動したままにします。

目的・場所: ターミナルAで、配布元への再確認を必須にして、通信障害時の判断を確認します。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" verify --request "$REQ" \
  --crl-source http --crl-refresh

正常例: INDETERMINATE / CRL_HTTP_UNAVAILABLE、終了値1になります。stopped_at:crl_httpなら、HTTP段階で止まっています。

異常例と判断: 成功する場合は、別の配布サーバーが同じポートで動いていないか、HTTP取得を指定しているかを確認します。エラーを消すために、確認を無効化しないでください。

この証明書を直前に失効させたことを実験者が知っていても、今回の取得に失敗したクライアントの観測は「確認不能」です。 復旧後に再取得して、失効を確認する必要があります。[^observations]

11. 困ったときの確認方法と対処――結果コードから調べる場所を絞る

11.1 終了値だけでなく、意味を読む

終了値本ラボでの意味
0操作成功、または指定した条件での受理
1拒否、判定不能、照合異常、復元・再開の未完了など
2申請状態や権限、ポリシーなどの業務上の拒否
3ファイル・通信・実行環境などの想定外エラー

失効の拒否を試す操作では、終了値1が期待結果です。しかし、ポート競合などの環境エラーまで「拒否できたから成功」とは扱いません。結果コードと停止位置を確認します。[^operations]

結果コード最初に確認するもの対処の考え方
SAN_MISMATCH接続先名とSANの型・内容名前を訂正するか、正しい名前で再申請する
UNTRUSTED_ANCHOR指定した信頼の起点期待したCAか確認する。受信したCAを無条件登録しない
CERT_EXPIRED証明書の期限、検証者の時計正しい時刻を保ち、証明書を更新する
CRL_HTTP_UNAVAILABLE配布プロセス、ポート、取得URL配布を復旧して再取得する
CRL_EXPIREDCRLのnextUpdate対応するCAでCRLを更新する
CRL_BAD_SIGNATURE配布物と事前に用意したCA証明書採用せず、取り違え・改変・配布設定を調べる
CRL_ROLLBACK既知番号と受信番号番号記録を削除して回避しない
LEAF_REVOKED/REVOKED失効対象と理由新しい鍵・証明書への移行を検討する
AUDIT_FROZEN監査ログと基準ハッシュ証跡を保全し、通常処理を続けない
RECOVERY_HOLD復元・再開の判定項目不足条件を確認し、保留を勝手に削除しない

HTTP関連の判定と対処は、付属のHTTP CRL手順に対応しています。[^httpguide]

11.2 CRL更新後は、検証をもう一度行う

目的・場所: ターミナルAで、配布やストレージの障害を解消した後、CRLと未完了失効を処理します。対象は本稿の学習用CAです。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" crl-root
python3 lab/pkilab.py --home "$HOME/pki-lab-blog" crl-issuer
python3 lab/pkilab.py --home "$HOME/pki-lab-blog" check

正常例: CRL処理の結果と、checkのok:trueが返ります。

異常例と判断: REVOCATION_PENDINGやCAの停止状態が残る場合は、未完了です。失効済み中間CAの鍵をそのまま使い続けるための手順ではありません。更新後はHTTP配布を再起動し、verify/client --crl-source http --crl-refreshで結果を確認します。[^operations]

11.3 not_observedは、検証の失敗を意味しない

revocation_observationは、どこまで個別に観測したかを表します。

値読み方
not_requested失効確認を要求していない
not_executed名前照合などで先に止まり、後段の検証を実行していない
reported失効またはCRLに関する結果を観測した
not_observed内部の失効確認段階への到達を、個別には記録していない

最終結果がACCEPTでも、内部の全工程を個別に計測したことにはなりません。逆に、not_observedだけを見て「失効確認を無効にしていた」と推測するのも誤りです。要求した設定と最終結果を一緒に読みます。[^operations]

12. 動作確認の結果――合格した試験と、測っていないこと

記事執筆時には、配布ZIPの実ソースを使って次を再実行しました。

確認対象結果
CA全テスト125件成功
教材・説明ロジック23件成功
HTTP/実TLSデモ8段階が期待結果に一致
手動操作相当の確認発行、名前不一致、HTTP検証、新規TLS、失効、バックアップ、復元・再開を確認
復元後の失効確認復元前に失効した証明書を、復元後も拒否することを確認

実行ログは記事用の検証記録にまとめています。CAの125件には既存機能と境界条件の試験が含まれ、125件すべてがHTTPやTLSの試験という意味ではありません。

目的・場所: 読者の環境でも、CA/で同じ試験を実行できます。テストは専用の一時CAを使います。

python3 -m unittest discover -s lab/tests -v
(cd viz && npm test)

正常例: PythonはRan 125 testsとOK、Node.jsはtests 23とfail 0です。件数は本稿の配布版に対応します。

異常例と判断: FAILED、例外、非ゼロ終了を確認します。期待値の誤りか実装不具合かを切り分け、検査の削除や失効確認の無効化だけで合格にしないでください。

一方、Round 5のWebGL統合描画、読者の実GPUでのFPS・VRAM、初心者対象の理解度改善率は、今回の合格項目には含めていません。配布時のDOM操作確認にはレンダラーの代替実装が使われており、実描画の証拠ではありません。新しいGitHub CIも未実行です。[^round5] ^quality

13. 再発防止――監査、バックアップ、復旧の意味を取り違えない

13.1 監査ログと、CA全体の照合を分ける

目的・場所: ターミナルAで、監査の連鎖とCAの状態を確認し、教材イベントを保存します。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" audit-verify
python3 lab/pkilab.py --home "$HOME/pki-lab-blog" check
python3 lab/pkilab.py --home "$HOME/pki-lab-blog" export-events

正常例: 監査検証はok:true、照合はintegrity_ok:trueとok:trueです。ExporterはイベントJSONのパスと件数を返します。

異常例と判断: 監査の不一致、problems、修復待ちのactionsを確認します。warningsも読み飛ばさないでください。例えば、CAバックアップに含めないサーバー秘密鍵の不足は、CA操作とHTTPSサービス復旧を分けて判断する必要があります。

check.ok:trueは、任意の通信先へ安全に接続できる保証ではありません。 CAの状態照合と、個々の接続判断は別です。[^operations]

13.2 バックアップは、秘密鍵だけでは足りない

目的・場所: ターミナルAで、発行台帳、承認、失効情報、監査などを含めて保存します。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" backup

正常例: 暗号化アーカイブのbackupと、認証情報を持つmanifestのパスが返ります。

異常例と判断: 監査異常や保存エラーがあれば、復旧可能なバックアップが完成したとは扱いません。アーカイブとmanifestを対で保全します。

このラボでは、secrets/とserver/private/をCAバックアップに含めません。バックアップ用パスワード、CA鍵を開ける情報、サーバー秘密鍵は別に保管します。同じPCの別フォルダーに置くだけでは、PC全体の故障への備えになりません。 [^operations]

13.3 復旧演習は、最後に別ディレクトリで行う

ここからは通常の発行・HTTP実験とは別の、任意の復旧演習です。ターミナルB・CのサーバーをCtrl+Cで止め、元の学習用CAで更新操作を行わない状態にします。

目的・場所: ターミナルAで、直前のバックアップを元領域の外へ復元します。復元先は未使用または空にしてください。

read -r -p 'backupに表示されたアーカイブの絶対パス: ' BACKUP

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" restore \
  "$BACKUP" "$HOME/pki-lab-blog-restored" \
  --pass-file "$HOME/pki-lab-blog/secrets/backup.pass" \
  --root-pass-file "$HOME/pki-lab-blog/secrets/root.pass" \
  --issuer-pass-file "$HOME/pki-lab-blog/secrets/issuer.pass"

上のパスは、既定の設定で作った学習用環境の例です。既存の別保管情報を使う環境では、その実際の場所を指定します。

正常例: 条件がそろえば、次の値になります。以下は主要項目の抜粋です。

{
  "archive_integrity_ok": true,
  "state_consistent": true,
  "freshness_confirmed": true,
  "key_access_ready": true,
  "resume_authorized": false,
  "ready": true
}

異常例と判断: ready:falseなら、失敗した判定とactions_after_resumeを確認します。HMAC不一致、元状態との差分、鍵の不足を、例外フラグで一括して消さないでください。

ready:trueでも再開承認は済んでいません。復元可能な状態と、運用を再開した状態は別です。[^operations]

13.4 再開直前に、元CAの最新状態を再確認する

注意:次の操作は元の作業領域を置き換え済みとして扱い、以降の通常更新を止めます。 単なる動作確認ではありません。本稿で作った学習用環境だけを対象にしてください。

目的・場所: ターミナルAで、復元先の整合性・鍵・新しさを再確認して切り替えます。

python3 lab/pkilab.py --home "$HOME/pki-lab-blog-restored" resume --confirm \
  --root-pass-file "$HOME/pki-lab-blog/secrets/root.pass" \
  --issuer-pass-file "$HOME/pki-lab-blog/secrets/issuer.pass"

python3 lab/pkilab.py --home "$HOME/pki-lab-blog" status
python3 lab/pkilab.py --home "$HOME/pki-lab-blog-restored" check

正常例: 再開結果はaction:resumed、source_fenced:trueです。元領域のstatusではsuperseded:trueになります。

異常例と判断: heldなら再開していません。元環境の識別違い、鮮度の不一致、鍵の不足を調べます。再開後の修復待ち作業があれば、表示された指示に従って処理します。[^operations]

これはCA管理機能の切替です。 サーバー秘密鍵は別保管なので、HTTPSサービスの復旧まで自動完了したとは判断しません。また、CLIの更新停止は、PCの電源断や管理者権限の剝奪ではありません。

14. 注意点――避けたい3つの運用判断

14.1 「エラーが出るから失効確認を外す」は、修正ではない

--no-crlは比較実験用です。CRL取得に失敗した原因を直す代わりに使うと、確認すべき状態を確認しないまま受理することになります。

本ラボの目的はエラーを消すことではなく、拒否すべき条件を正しく拒否し、原因を説明することです。PythonのTLS設定でも、CRL確認は証明書・名前の検証とは別の設定になっています。[^python-ssl]

14.2 「同じ暗号方式だから公開CAと同じ」は、評価の飛躍

このラボでは、HSMや物理的なオフラインルートを完成条件にはしません。しかし、同一ユーザーが秘密鍵・解除情報・台帳へアクセスできる限界も消えません。

学習用としての正確さと、実運用の保証は分けて評価します。 公開CAへの参加や独立監査を実施済みとは書きません。^quality

14.3 「復旧フラグを付ければ履歴も戻る」は、誤り

古い状態を受け入れる例外操作は、失われた失効履歴を再生成する機能ではありません。

本稿の通常手順では、--accept-staleや監査の再アンカーを、エラー回避のために使いません。失効情報を確定できない場合は、旧CAの信頼を外し、新しい鍵のCAへ移行する判断も必要です。^quality

15. まとめ――理解すべきなのは、鍵の形ではなく判断の根拠

PKI Labでは、サーバー側で鍵とCSRを作り、RAが承認し、中間CAが証明書へ署名します。利用側は、事前に採用した信頼の起点と必要な失効情報を使って判断します。

HTTP実験では、新しいCRLを公開しても、利用側が古い有効キャッシュを使えば、まだ接続できる場面を確認できます。配布停止は失効の証拠ではなく、必要な情報が得られなければ判定不能として扱います。[^round5]

筆者の考察: この教材の価値は、複雑な処理を一つの「安全」という表示へまとめないことにあります。「受信できた」「署名を確認した」「名前が一致した」「失効を検出した」「確認できなかった」を分けると、実務でも次に調べる場所が見えてきます。

3Dで全体の関係をつかみ、コマンドで状態を変え、ログで根拠を確かめる。この往復を、証明書トラブルを理解するための練習にしてください。

FAQ

Q1. CAを作るためにNode.jsやThree.jsは必要ですか?

CA本体はPythonとOpenSSLで動きます。Node.jsは教材ロジックのテストなどに使います。3D表示用のThree.jsは配布版に含まれるため、表示だけならNode.jsでビルドする手順は不要です。[^pr]

Q2. HTTPSをブラウザで直接開くと警告が出るのは、実験の失敗ですか?

必ずしも失敗ではありません。本稿ではOSやブラウザへ自作ルートCAを登録していないため、専用クライアントとブラウザで信頼設定が異なります。警告を消す操作ではなく、信頼するCAを明示したラボのクライアントで確認します。[^openssl-verify]

Q3. CRLをHTTPで配るのは危険ではありませんか?

CRL本文は発行者が署名し、利用側がその署名を検証します。ただしHTTP応答を受け取れたこと、配布元が最新版を返したこと、通信妨害を受けていないことまで、その署名で証明できるわけではありません。本ラボは取得先をloopbackに限定し、未知の外部サーバーへ自動取得する用途を対象にしていません。[^httpguide]

Q4. OCSPも試せますか?

今回の実通信による失効確認はCRLです。OCSPは比較展示にとどまります。OCSPのgoodという応答だけで、名前・期限・信頼を含む証明書全体が有効だと判断する説明にはしません。[^vizdesign] [^rfc6960]

Q5. このラボをそのまま公開認証局として使えますか?

使えるとは評価していません。発行対象や利用者を限定した学習用です。OpenSSL公式も、openssl caを本格的な本番CA製品として扱うべきではないことを注意しています。学習成果を本番設計へ生かすことと、実装をそのまま流用することは別です。[^openssl-ca]

参考情報

プロジェクトと実装資料

PKI LabのGitHubリポジトリ/対象PR #1。本稿のHTTP機能はRound 5配布版を参照します。

Round 5サンプル一式/HTTP CRL手順書/学習品質と安全境界。

仕様・公式文書

資料確認できる内容
RFC 2986:PKCS #10CSRの構造と署名
RFC 5280:X.509証明書とCRL証明書、信頼経路、失効一覧、CRL配布
RFC 9525:TLSのサービス識別接続先名と証明書の照合
RFC 8446:TLS 1.3証明書の提示とハンドシェイク認証
RFC 9110:HTTP Semantics304 Not Modified
RFC 9111:HTTP Cachingキャッシュの鮮度と再確認
RFC 6960:OCSPOCSP応答の意味
OpenSSL:証明書検証のオプション信頼の起点、名前、用途、CRL確認
OpenSSL:X.509拡張設定SAN、Key Usage、EKUなど
OpenSSL:CRLコマンドCRLの形式変換と署名検証
OpenSSL:CAコマンド発行・失効管理と実装上の注意
Python 3.13:sslTLS接続、名前検証、CRL検証の設定
Three.js:WebGLRenderer描画統計とメモリ情報の意味
Microsoft:WSLのネットワークWindowsとWSL間のlocalhost接続

コピー用URL:

https://github.com/kurumonn/CA
https://github.com/kurumonn/CA/pull/1
https://www.rfc-editor.org/rfc/rfc2986.html
https://www.rfc-editor.org/rfc/rfc5280.html
https://www.rfc-editor.org/rfc/rfc9525.html
https://www.rfc-editor.org/rfc/rfc8446.html
https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5
https://www.rfc-editor.org/rfc/rfc9111.html
https://www.rfc-editor.org/rfc/rfc6960.html
https://docs.openssl.org/3.5/man1/openssl-verification-options/
https://docs.openssl.org/3.5/man5/x509v3_config/
https://docs.openssl.org/3.5/man1/openssl-crl/
https://docs.openssl.org/3.5/man1/openssl-ca/
https://docs.python.org/3.13/library/ssl.html
https://threejs.org/docs/pages/WebGLRenderer.html
https://learn.microsoft.com/ja-jp/windows/wsl/networking

本文の出典注

[^pr]: GitHub PR #1。2026-10-09確認時のHEADは 00bcdb2eaee833c75cd5cf5371f9ff348f8e3a28。 [^round5]: Round 5実装・検証報告。 [^operations]: 運用・復旧手順。 [^rfc2986]: RFC 2986。 [^openssl-verify]: OpenSSL 3.5:検証オプション。 [^x509-config]: OpenSSL 3.5:X.509拡張設定。 [^rfc8446]: RFC 8446。 [^rfc5280]: RFC 5280。 [^httpguide]: HTTP CRL操作手順。 [^rfc9111]: RFC 9111。 [^rfc9110]: RFC 9110。 [^openssl-crl]: OpenSSL 3.5:openssl-crl。 [^observations]: 配布時のHTTP観測記録。記事執筆時の再実行はこちら。 [^wsl]: Microsoft:WSLのネットワーク。 [^three-renderer]: Three.js:WebGLRenderer。 [^openssl-ca]: OpenSSL 3.5:openssl-ca。 [^rfc9525]: RFC 9525。 [^python-ssl]: Python 3.13:ssl。 [^vizdesign]: 3D教材の基点版設計。外観品質に関する完成条件はRound 5で変更。 [^rfc6960]: RFC 6960。


読んだ内容を10問練習と実技で確認

記事で理解した用語を、StudyQuestの演習とクラウド実技ラボで定着させます。

10問練習 実技ラボ

コメント(0件)

まだコメントはありません。最初のコメントを投稿してください!

コメントを投稿