証明書は、公開鍵をアイデンティティ、用途、有効期限と結びつけ、発行者によって署名します。クライアントは証明書を受け入れる際に、信頼できるパスとアプリケーションによるアイデンティティ検証が必要です。さらに、TLSハンドシェイクによって、相手が対応する秘密鍵を保持していることを証明します。証明書を読み出せたり、自己署名を検証できたりしたからといって、相手の正体が分かっているわけではありません。
https://api.example.com:8443/orders を通して例を示します:接続先のアドレスは解決によって得られ、参照アイデンティティはアプリケーションがアクセスするターゲットから来て、証明書は検証可能なアイデンティティの宣言を提供します。DNSがどのIPを返したか、サーバーがどの名前を送信したかによって、クライアントが誰を信頼すべきかが決まるわけではありません。
| 解決すべき問題 | 参照箇所 |
| SAN、CN、ポート、ワイルドカードのマッチング方法 | サービスアイデンティティ |
| ルート証明書があるのにチェーンが不完全と報告される理由 | 信頼パス |
| DNS-01で実際にどのようなTXTレコードを書くべきか | ACMEの3種類チャレンジ |
| 証明書が期限切れでないのに拒否すべき理由 | 失効、用途、クライアントポリシー |
| CTが誤った発行を阻止できるか | 公開監査 |
| 更新は成功したが、オンラインではまだ期限切れ | 公開の閉ループ |
4つのオブジェクトをまず区別する
| オブジェクト | 含めるもの | 代替できないもの |
| 秘密鍵 | 署名やその他の秘密鍵演算に必要な秘密情報 | 証明書と共に公開配布することはできない |
| 証明書 | 公開鍵、アイデンティティ、制約、発行者の署名 | 現在の相手が秘密鍵を保持していることを証明しない |
| CSR | 署名申請情報と鍵申請の署名 | CAによって承認された証明書ではない |
| 信頼ストア | クライアントが承認するアンカーと関連ポリシー | サーバーが随意に提供する証明書の集合ではない |
証明書は通常DERバイナリエンコーディングされ、PEMは境界マーカ付きのテキストラッパーです。ファイル名が .pem であっても、中に証明書、秘密鍵、あるいは複数の証明書が含まれているかは判断できません。実際のオブジェクトを読み取る必要があります。証明書自体の署名アルゴリズムは発行者によって使用され、SubjectPublicKeyInfo は保持者の公開鍵アルゴリズムを記述します。これら2つは必ずしも同じである必要はありません。
サービスアイデンティティ:まずアクセス先を確定し、SANを比較する
本稿のHTTPSシナリオでは、SANの dNSName または iPAddress にマッチングします。Subject CNを使ってDNSアイデンティティを偽装してフォールバックしないでください。ドメイン名は適用ルールに従って大文字小文字と国際化名前を処理します。ワイルドカードは左端の完全なラベルのみを占め、1レベルのみをカバーします。
| 証明書 SAN | アクセスターゲット | 名前結果 |
| DNS:api.example.com | https://api.example.com:8443/ | マッチ;ポートはDNS SANに書き込まれない |
| DNS:*.example.com | https://api.example.com/ | マッチ |
| DNS:*.example.com | https://example.com/ | マッチせず(裸のドメイン) |
| DNS:*.example.com | https://v2.api.example.com/ | マッチせず(マルチレベル) |
| DNS:api.example.com | https://192.0.2.10/ | マッチせず;適切なIP SANが必要 |
SNIはサーバーが仮想ホストの証明書を選択するために使用しますが、クライアントによる名前検証を代行するものではありません。DNS CNAMEがホスティングプロバイダーを指していても、ホスティングプロバイダーの任意の名前で元のサービスアイデンティティを代替できるわけではありません。ここでは名前マッチングの結果を示していますが、最終的な認証はチェーンとその他のチェックに依存します。RFC 9525:サービスアイデンティティ
信頼パス:署名の有効性は条件の一つに過ぎない
典型的なサーバーはリーフ証明書と必要な中間証明書を送信し、通常はルートを送信する必要はありません。クライアントはローカルの信頼材料を使って候補パスを構築し、署名、有効期限、クリティカル拡張、制約を検証します。Issuerテキストが別の証明書のSubjectと等しいからといって、署名検証をスキップすることはできません。AKI/SKIは候補の選択に役立ちますが、それ自体が独立した信頼源ではありません。
| フィールドまたは拡張 | 読むべき重点 |
| serialNumber | 発行者と組み合わせて証明書を識別する。インターネット全体で一意のIDではない |
| BasicConstraints | CA資格;pathLenConstraintは、パス内の自己署名でない中間CAの数を制限する |
| KeyUsage | digitalSignature、keyCertSignなど。名称で随意に汎用化できない |
| ExtendedKeyUsage | serverAuth、clientAuthなど。KUと同時に存在する場合は両方を満たす必要がある |
| NameConstraints | 下位が使用できる名前の範囲を制限する |
| critical 拡張 | 処理できないクリティカルな制約は直接無視できない |
中間CAの pathLenConstraint=0 でもリーフ証明書を発行できます。これは「どの証明書も署名できない」という意味ではありません。ルート証明書は自己署名されることが多いですが、信頼は外部設定から来ており、自分の署名を循環検証することから来るものではありません。クロス署名によりパス構築に複数の候補が生じるため、あるクライアントでは動作し、別のクライアントでは失敗することがあります。これはアンカー、パス構築能力、またはポリシーの違いに起因する可能性があります。RFC 5280:証明書とパス
例えばサーバーがリーフ証明書のみを送信する場合:ブラウザは既に中間証明書をキャッシュしており接続に成功するかもしれませんが、新しいコンテナでは失敗します。サービスチェーンを補完する必要があり、「このブラウザなら開ける」ということをチェーンが完全である証拠としてはいけません。公開Web PKIと自社構築企業PKIの信頼範囲も異なります。企業ルートをインストールすると、そのクライアントが受け入れる発行者の範囲が広がります。
チェーンは繋がっているのに、なぜ拒否するのか?
まず裸のドメイン example.com やマルチレベル v2.api.example.com を入力し、ワイルドカードがそれらをカバーできないことを観察します。名前を復元すると、pathLen=0 でも直接リーフ証明書の署名を許可します。CAを1つ挿入して初めてこの制約がトリガーされます。他のチェックは独立して残ります。
図を準備しています
チェーンは繋がっているのに、なぜ拒否するのか?パス構築で候補関係が見つかったからといって、パス検証が成功するわけではない。名前マッチ、用途、時間、CA制約は同時に満たす必要のある条件であり、1つの有効な署名で代替できるものではない。
{"id":"pki-path-constraints","title":"チェーンは繋がっているのに、なぜ拒否するのか?","summary":"パス構築で候補関係が見つかったからといって、パス検証が成功するわけではない。名前マッチ、用途、時間、CA制約は同時に満たす必要のある条件であり、1つの有効な署名で代替できるものではない。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"form-field\"><span data-i18n=\"name\"></span><input type=\"text\" id=\"name\" value=\"api.example.com\" maxlength=\"40\" autocomplete=\"off\" spellcheck=\"false\"></label><label class=\"form-field\"><span data-i18n=\"purpose\"></span><select id=\"purpose\"><option value=\"server\">serverAuth</option><option value=\"client\">clientAuth</option></select></label><label class=\"check\"><input type=\"checkbox\" id=\"trust\" checked><span data-i18n=\"trust\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"extra\"><span data-i18n=\"extra\"></span></label><div class=\"control\"><label for=\"limit\" data-i18n=\"limit\"></label><output for=\"limit\" id=\"limit-value\"></output><input id=\"limit\" type=\"range\" min=\"0\" max=\"2\" value=\"0\" step=\"1\"></div><div class=\"control\"><label for=\"time\" data-i18n=\"time\"></label><output for=\"time\" id=\"time-value\"></output><input id=\"time\" type=\"range\" min=\"0\" max=\"120\" value=\"50\" step=\"1\"></div><section class=\"panel\"><h3 data-i18n=\"checks\"></h3><div id=\"checks\"></div></section><div class=\"metrics\"><div><span data-i18n=\"result\"></span><strong id=\"result\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#checks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const name=$('#name').value.toLowerCase(),checks={anchor:$('#trust').checked,SAN:/^[a-z0-9-]+\\.example\\.com$/.test(name),EKU:$('#purpose').value==='server',pathLen:Number($('#extra').checked)<=+$('#limit').value,time:+$('#time').value<=100};set('limit-value',$('#limit').value);set('time-value',$('#time').value);cells('checks',Object.entries(checks).map(([k,v])=>({text:k+' '+(v?'✓':'✗'),cls:v?'on':'error'})));const ok=Object.values(checks).every(Boolean);set('result',ok?'ACCEPT':'REJECT');$('#result').dataset.checks=JSON.stringify(checks);}document.querySelectorAll('input,select').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"checks":"独立チェック","extra":"中間CAの下にCAを1つ挿入","heading":"チェーンは繋がっているのに、なぜ拒否するのか?","intro":"参照名前、用途、信頼アンカー、中間CAの制約を変更し、項目ごとにチェックしてから集約する。","limit":"中間CA pathLenConstraint","name":"参照名前を適用","purpose":"検証用途","reset":"リセット","result":"最終検証","scope":"ルート→中間CA→リーフ証明書は固定され、すべての署名は数学的に有効;SAN=*.example.com、EKU=serverAuth、証明書時間[0,100]。中間CAの下に自己署名でないCAを追加可能;完全なPKIXや失効は実装しない。","scopeLabel":"モデル範囲と規約","time":"検証時間","trust":"ローカル信頼ルートアンカー"}}
ACME:ドメイン制御権を証明し、申請公開鍵の署名を取得する
ACMEアカウント鍵はアカウントリクエストの認証に使用され、証明書鍵はサービスハンドシェイクに使用されます。これらは異なる役割を担います。注文は識別子を記述し、authorization/challenge記録は制御権の検証を行い、CSRは署名待ちの公開鍵を提出します。注文がready状態になるとfinalizeされ、処理が完了してvalid状態になってから証明書をダウンロードできます。1回のPOST成功がすべてのステップの終了を意味するわけではありません。
keyAuthorization = token + "." + accountKeyThumbprint とし、ここでthumbprintはアカウント公開鍵JWKの正規化されたフィンガープリントです。2つの基本チャレンジの格納は以下の通りです:
| チャレンジ | CAがチェックする場所 | 配置する値 |
| HTTP-01 | 対象ドメインのHTTP 80ポート、規定のchallengeパス | keyAuthorizationの原文 |
| DNS-01 | _acme-challenge.<ドメイン> のTXT | base64url(SHA256(keyAuthorization))、パディングなし |
*.example.com を申請する場合、DNS検証では _acme-challenge.example.com が使用されます。同じ名前で複数のチャレンジTXTが存在する可能性があるため、自動化は各自の値に従って維持し、並列注文が互いに上書きしないようにする必要があります。公開後は権威応答と検証の可視性を確認する必要があり、ローカル再帰キャッシュのヒットをグローバルな準備完了と見なしてはいけません。RFC 8555:注文とチャレンジ
TLS-ALPN-01は443ポートを使用し、acme-tls/1 をネゴシエーションします。証明材料は専用自己署名証明書のクリティカルな acmeIdentifier 拡張に格納され、keyAuthorizationのSHA256ダイジェストです。ALPN文字列自体はチャレンジ値を運びません。この専用証明書には対応する単一のDNS SANが必要であり、通常のサービス証明書で直接代替することはできません。RFC 8737
実行可能な例:TXTを推測するのではなくチャレンジバイト列を確認する
以下では、教学用トークンと与えられたフィンガープリントを使用して、3種類の格納の違いを示します。JWKの正規化、署名付きリクエスト、または実際のCA検証は実装しません。
import base64
import hashlib
def b64url(data):
return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii')
token = 'teaching-token'
thumbprint = b64url(hashlib.sha256(b'teaching-account-key').digest())
key_auth = (token + '.' + thumbprint).encode('ascii')
digest = hashlib.sha256(key_auth).digest()
dns_txt = b64url(digest)
acme_identifier_der = bytes([0x04, 0x20]) + digest
assert len(digest) == 32 and len(dns_txt) == 43
assert base64.urlsafe_b64decode(dns_txt + '=') == digest
assert acme_identifier_der[:2] == bytes([4, 32])
assert dns_txt != key_auth.decode('ascii')
assert hashlib.sha256(key_auth + b' ').digest() != digest
print('HTTP 原文、DNS ダイジェストエンコーディング、TLS 拡張内層長さ:', len(key_auth), len(dns_txt), len(acme_identifier_der))
有効期限、失効、ステータスの鮮度
証明書が期限切れでないからといって、秘密鍵が漏洩していないことを意味しません。失効は発行後に承認を取り消すことを表します。クライアントがステータスをどのように取得し、強制執行するかは、プロトコルの能力と製品ポリシーによって共同で決定されます。
| メカニズム | 取得・検証するもの | 留意すべき境界 |
| CRL | 署名付きリストとその範囲、更新時間 | パーティション化されているか、インクリメンタルである場合があり、必ずしもCA単一の巨大テーブルではない |
| OCSP | 指定された発行者/シリアル番号に対する署名ステータス | good は完全な証明書有効性の証明ではない |
| OCSP Stapling | サーバーがキャッシュされたOCSP応答を添付 | 署名、証明書関連性、鮮度の検証が必要 |
| 短期有効期限 | 更新されない証明書の生存ウィンドウを短縮 | 漏洩の瞬間に既存の資格情報を無効化しない |
OCSPの good/revoked/unknown は「安全/不安全/ネットワーク異常」に単純化できません。good は少なくとも失効されていないことを示しますが、名前、用途、チェーンがすべて有効であることを保証するものではありません。キャッシュされた応答にも時間的な境界があります。仮に thisUpdate が 10:00、nextUpdate が 12:00 で、11:00 にそれを受け取った場合、ステータスが11:00に再生成されたことを意味しません。12:30 に受け取ったからといって、まだ鮮度がある肯定証拠にはなりません。RFC 6960
TLS 1.2 の stapling は CertificateStatus メッセージに一般的に見られます。TLS 1.3 では CertificateEntry の拡張を使用し、同じメッセージ位置にそのまま持ち込むことはできません。Must-Staple は TLS Feature 拡張の使用であり、コード署名用のCTではありません。その実際の制約効果はクライアントのサポートに依存します。RFC 7633、TLS 1.3 証明書メッセージ
2026年9月現在、Let's Encryptの旧OCSPアドレスを一般的なデプロイメント例として使用することはできません。そのOCSPサービスは2025年8月に終了し、CRL公開に移行しました。特定のCAがどのステータスメカニズムをサポートしているかは、現在の証明書拡張とそのポリシーを読む必要があります。Let's Encrypt 公告
CT:発行を監査可能にし、ドメイン検証に代えない
ログはSCTを返し、規定の期間内に提出項目をログに組み込むことを約束します。SCTを受け取ったからといって、inclusion proof をすでに取得したわけではありません。Merkle inclusion proof は、ある項目が特定のツリーヘッドに属していることを証明し、consistency proof はログの追従整合性を証明するために使用されます。監視者はこれに基づいて異常な発行を発見し、調査、失効、または信頼ポリシー処理に進みます。ログ自体がドメイン所有者に発行を認可するものではありません。RFC 9162:CT v2
プロトコルバージョンとブラウザのデプロイメントポリシーは別々に確認する必要があります。「すべての証明書にSCTを2つ固定する」とは書かないでください。例えば、現在のChromeの埋め込みSCTパスでは、180日以内のライフサイクルでは少なくとも2つの異なるログが必要で、180日を超える場合は3つ必要であり、運用者の多様性とログの状態条件があります。TLS伝送パスには別の条件があります。企業プライベート信頼と公開Web PKIに同じ結論を直接適用することはできません。Chrome CT Policy
CTログは公開情報源であり、申請内の内部命名が公開証明書に入ると、可視記録が形成されます。発見可能性は向上しますが、ドメインを隠すことはできず、悪意のある証明書が一時的に使用されるのを単独で阻止することはできません。
更新は公開の閉ループである
有効期限はX.509の永久に固定された定数ではなく、あるCAのある製品の期間をプロトコルの上限として書くことはできません。2026年9月現在、公開TLS Subscriber Certificateのベースライン要件は、2026-03-15から2027-03-15の間に発行された証明書に対して最大200日を規定しています。今後のフェーズではさらに緊縮される計画があり、特定の製品ではこれより短くなる可能性があります。自社構築PKIはこの数値を無差別に適用すべきではありません。CA/B Forum:6.3.2
教学用の証明書が残り20日であると仮定し、自動タスクが24時間ごとに実行され、正常な配布に2時間かかるとします。利用可能な回復ウィンドウは「20回確実に成功する更新」ではありません。チャレンジシステム、発行レートリミット、DNS権限、配布サービスは障害を共有する可能性があります。実際のサービス証明書の残り時間、最近の成功した公開、各エントリの整合性を監視し、処理失敗に必要な余裕を同時に保持する必要があります。
材料の検証が完了したら、ファイルをアトミックに置き換え、ユーザーをリロードし、各エントリから実際に送信された証明書を確認します。証明書ファイルの更新がプロセスの読み込みを意味するわけではなく、1つのノードの更新がクラスター全体の完了を意味するわけでもなく、既存の接続が自動的に再接続ハンドシェイクを行うわけでもありません。証明書の更新と秘密鍵のローテーションは別々に記録し、新しい証明書が必ずしも新しい鍵を意味すると誤解しないようにしてください。
さらに参照する