| | @@ -120,6 +120,20 @@ |
| | .verdict.accept{background:var(--redbg);border-color:var(--red);color:#ffb4b4;} |
| | .verdict.reject{background:#0e1a18;border-color:var(--accent-dim);color:#a9e7db;} |
| | .verdict small{display:block;font-weight:400;color:var(--soft);margin-top:3px;font-size:12px;} |
| + | .labnote{margin-top:14px;font-size:11.5px;color:var(--faint);} |
| + | .vrow{display:flex;flex-direction:column;gap:12px;margin-top:14px;} |
| + | .vbox{border:1px solid var(--line);border-radius:10px;padding:13px 15px;background:var(--panel); |
| + | transition:border-color .18s,background .18s;} |
| + | .vbox .vlabel{font-size:11px;letter-spacing:.8px;text-transform:uppercase;color:var(--faint);margin-bottom:7px;} |
| + | .vstate{font-size:15.5px;font-weight:700;} |
| + | .vwhy{font-size:12.5px;color:var(--muted);margin-top:5px;line-height:1.5;} |
| + | .vbox.accept{border-color:var(--red);background:var(--redbg);} |
| + | .vbox.accept .vstate{color:#ffb4b4;} |
| + | .vbox.reject{border-color:var(--accent-dim);background:#0e1a18;} |
| + | .vbox.reject .vstate{color:#a9e7db;} |
| + | .vhint{margin-top:15px;font-size:13px;color:var(--soft);border-left:2px solid var(--accent-dim); |
| + | padding-left:13px;line-height:1.55;} |
| + | .vhint b{color:var(--ink);} |
| | |
| | .ruled{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:8px;} |
| | @media(max-width:680px){.ruled{grid-template-columns:1fr;}} |
| | @@ -211,44 +225,49 @@ |
| | CA limited to <span class="mono">pathLenConstraint = 0</span>, meaning <i>"you may issue |
| | end-entity certificates only, never another CA"</i>, has nonetheless issued a sub-CA. |
| | A correct validator must reject this. <b>Toggle the <span class="mono">keyUsage</span> |
| - | extension</b> on that intermediate and run the validator: watch the path-length check turn |
| - | itself off.</p> |
| + | extension</b> on that intermediate and watch the current library change its verdict on a chain that |
| + | should always be rejected, while the patched build stays put.</p> |
| | |
| | <div class="lab"> |
| | <div class="labbar"> |
| | <span class="d r"></span><span class="d y"></span><span class="d g"></span> |
| | <span class="dlabel mono">minipki · verifyCertificateChain()</span> |
| - | <span class="dbadge mono">LIVE · runs locally</span> |
| | </div> |
| | <div class="labgrid"> |
| | <div class="labcol left"> |
| | <h4>Certificate chain (attacker-supplied)</h4> |
| | <div class="chain" id="chain"></div> |
| | <div class="toggles"> |
| - | <div class="tg on" id="tgKU" onclick="toggle('KU')"> |
| + | <div class="tg" id="tgKU" onclick="toggle()"> |
| | <div class="sw"></div> |
| | <div class="tt"><b>keyUsage</b> extension on the constrained intermediate |
| - | <small>Real CAs include it; minimal tooling often omits it.</small></div> |
| - | </div> |
| - | <div class="tg" id="tgFix" onclick="toggle('Fix')"> |
| - | <div class="sw"></div> |
| - | <div class="tt"><b>Apply my patch</b>: enforce pathLen unconditionally |
| - | <small>Enforce the constraint whenever cA + pathLen are set, regardless of keyUsage.</small></div> |
| + | <small>An unrelated field. Real CAs include it; minimal tooling often omits it.</small></div> |
| | </div> |
| | </div> |
| - | <button class="runbtn mono" onclick="runVerify()">▶ verifyCertificateChain(chain)</button> |
| + | <p class="labnote mono">Both validators re-evaluate the instant you flip the switch.</p> |
| | </div> |
| | <div class="labcol"> |
| - | <h4>Validator output</h4> |
| - | <div class="console mono" id="con"></div> |
| - | <div class="verdict" id="verdict" style="display:none"></div> |
| + | <h4>Same chain, two validators</h4> |
| + | <div class="vrow"> |
| + | <div class="vbox" id="box_vuln"> |
| + | <div class="vlabel">Current library</div> |
| + | <div class="vstate" id="st_vuln"></div> |
| + | <div class="vwhy" id="why_vuln"></div> |
| + | </div> |
| + | <div class="vbox" id="box_patch"> |
| + | <div class="vlabel">With my one-line fix</div> |
| + | <div class="vstate" id="st_patch"></div> |
| + | <div class="vwhy" id="why_patch"></div> |
| + | </div> |
| + | </div> |
| + | <div class="vhint" id="hint"></div> |
| | </div> |
| | </div> |
| | </div> |
| - | <p class="callout">The two states are byte-identical except for one optional extension. Only |
| - | the chain <i>with</i> <span class="mono">keyUsage</span> is correctly rejected, which proves the |
| - | constraint's enforcement is gated on the wrong condition. Flip <b>Apply my patch</b> on and |
| - | the hole closes in both states.</p> |
| + | <p class="callout">One toggle, two verdicts. The current library lets the presence of an |
| + | unrelated extension decide whether it enforces the path-length limit, so flipping |
| + | <span class="mono">keyUsage</span> flips its answer on a chain that should always be rejected. |
| + | The patched validator ignores that field and rejects the chain every time.</p> |
| | </div></div></section> |
| | |
| | <!-- ============ HOW IT WORKS ============ --> |
| | @@ -337,99 +356,70 @@ |
| | </div></footer> |
| | |
| | <script> |
| - | // ---- disclosure-safe MiniPKI: a faithful reproduction of the gating flaw ---- |
| - | // No third-party code; this models the *logic* of the bug using public RFC 5280 concepts. |
| - | var state = { KU:true, Fix:false }; |
| + | // disclosure-safe MiniPKI: two faithful validators over one attacker-supplied chain. |
| + | // No third-party code; models the logic with public RFC 5280 concepts. |
| + | // Chain (top to bottom): Demo Root CA (trust anchor) -> Constrained Intermediate |
| + | // (pathLenConstraint=0) -> Rogue Sub-CA -> leaf. A pathLen=0 CA must NOT issue another CA, |
| + | // so a correct validator always rejects this chain. |
| + | var keyUsage = false; // start in the divergent state so the difference is visible on load |
| | |
| - | var chain = [ |
| - | { cn:'victim.example.com', role:'leaf', isCA:false, pathLen:null, depth:0 }, |
| - | { cn:'Rogue Sub-CA', role:'sub', isCA:true, pathLen:null, depth:1 }, |
| - | { cn:'Constrained Intermediate', role:'inter', isCA:true, pathLen:0, depth:2 }, // pathLen=0 => no sub-CAs |
| - | // Root CA lives in the trust store (depth 3) |
| - | ]; |
| + | // returns true if the chain is ACCEPTED. enforceAlways=true models the patched build; |
| + | // the current build only enforces pathLen when a keyUsage extension is present (the bug). |
| + | function accepts(enforceAlways){ |
| + | var enforce = enforceAlways || keyUsage; |
| + | return enforce ? false : true; // one sub-CA follows pathLen=0 -> rejected when enforced |
| + | } |
| | |
| - | function extLine(c){ |
| - | if(c.role==='leaf') return 'basicConstraints: cA=false'; |
| - | if(c.role==='inter'){ |
| - | var s = 'basicConstraints: cA=true, pathLenConstraint=0'; |
| - | s += state.KU ? ' +keyUsage(keyCertSign)' : ' (no keyUsage)'; |
| - | return s; |
| - | } |
| - | return 'basicConstraints: cA=true +keyUsage(keyCertSign)'; |
| + | function extLine(role){ |
| + | if(role==='leaf') return 'basicConstraints: cA=false'; |
| + | if(role==='inter') return 'basicConstraints: cA=true, pathLenConstraint=0' + (keyUsage ? ' +keyUsage' : ' (no keyUsage)'); |
| + | if(role==='sub') return 'basicConstraints: cA=true +keyUsage'; |
| + | return 'trust anchor · basicConstraints: cA=true'; |
| | } |
| | |
| | function renderChain(flagged){ |
| | var rows = [ |
| - | {cn:'Demo Root CA', ext:'trust anchor · basicConstraints: cA=true', cls:'okca'} |
| + | {cn:'Demo Root CA', role:'root', cls:'okca'}, |
| + | {cn:'Constrained Intermediate', role:'inter', cls:'okca'}, |
| + | {cn:'Rogue Sub-CA', role:'sub', cls: flagged ? 'flagged' : 'rogue'}, |
| + | {cn:'victim.example.com', role:'leaf', cls: flagged ? 'flagged' : ''} |
| | ]; |
| - | // render top-down: root -> inter -> sub -> leaf |
| - | var inter = chain[2], sub = chain[1], leaf = chain[0]; |
| - | rows.push({cn:inter.cn, ext:extLine(inter), cls:'okca'}); |
| - | rows.push({cn:sub.cn, ext:extLine(sub), cls: flagged ? 'flagged' : 'rogue'}); |
| - | rows.push({cn:leaf.cn, ext:extLine(leaf), cls: flagged ? 'flagged' : ''}); |
| | var html=''; |
| | rows.forEach(function(r,i){ |
| | if(i) html += '<div class="arrow">│ issues ▼</div>'; |
| - | html += '<div class="node '+r.cls+'"><div class="cn">'+r.cn+'</div><div class="ext">'+r.ext+'</div></div>'; |
| + | html += '<div class="node '+r.cls+'"><div class="cn">'+r.cn+'</div><div class="ext">'+extLine(r.role)+'</div></div>'; |
| | }); |
| | document.getElementById('chain').innerHTML = html; |
| | } |
| | |
| - | function toggle(k){ |
| - | state[k] = !state[k]; |
| - | document.getElementById('tg'+k).classList.toggle('on', state[k]); |
| - | renderChain(false); |
| - | document.getElementById('verdict').style.display='none'; |
| - | document.getElementById('con').innerHTML='<span class="dim">// chain changed, click verify to re-run</span>'; |
| + | function setBox(id,accepted,whyAccept,whyReject){ |
| + | var box=document.getElementById('box_'+id); |
| + | box.className='vbox '+(accepted?'accept':'reject'); |
| + | document.getElementById('st_'+id).textContent = accepted ? '✗ Chain accepted' : '✓ Chain rejected'; |
| + | document.getElementById('why_'+id).textContent = accepted ? whyAccept : whyReject; |
| | } |
| | |
| - | function line(t,cls){ return '<span class="'+(cls||'')+'">'+t+'</span>\n'; } |
| - | |
| - | function runVerify(){ |
| - | var con=document.getElementById('con'); var out=''; |
| - | var inter = chain[2]; |
| - | out += line('verifyCertificateChain(): 3 certs + 1 trust anchor','dim'); |
| - | out += line('depth 3 Demo Root CA ............ trusted anchor ✓'); |
| - | out += line('depth 2 Constrained Intermediate . signature ✓, cA=true ✓'); |
| - | |
| - | // The flaw: pathLen enforcement gated on keyUsage presence (unless patched). |
| - | var enforce = state.Fix || state.KU; |
| - | var pathLenForSub = inter.depth - 1; // intermediates between inter and leaf, excl. leaf = 1 (the sub-CA) |
| - | var violated=false; |
| - | if(inter.pathLen!==null){ |
| - | out += line(' pathLenConstraint=0 present on this CA'); |
| - | if(enforce){ |
| - | // pathLen=0 means: 0 intermediate CAs may follow. The Rogue Sub-CA is one => violation. |
| - | if(pathLenForSub > inter.pathLen){ |
| - | violated=true; |
| - | out += line(' enforce pathLen → '+pathLenForSub+' CA(s) follow > limit 0 → VIOLATION', 'bad'); |
| - | } |
| - | } else { |
| - | out += line(' keyUsage absent → pathLen check SKIPPED ← the bug', 'bad'); |
| - | } |
| - | } |
| - | out += line('depth 1 Rogue Sub-CA ............ signature ✓, cA=true ✓ '+(violated?'(should be rejected)':'')); |
| - | out += line('depth 0 victim.example.com ...... signature ✓'); |
| - | |
| - | var accepted = !violated; |
| - | con.innerHTML = out; |
| - | renderChain(accepted /* if accepted, the sub-CA+leaf should not have been trusted -> flag red */); |
| + | function evaluate(){ |
| + | var vuln = accepts(false); // current library |
| + | setBox('vuln', vuln, |
| + | 'A pathLenConstraint=0 CA issued a sub-CA and it was trusted. keyUsage is absent, so the path-length check never ran.', |
| + | 'keyUsage happens to be present, so this build runs the path-length check and rejects the sub-CA.'); |
| + | setBox('patch', accepts(true), |
| + | '', |
| + | 'pathLenConstraint is enforced whenever cA and pathLen are set, regardless of keyUsage.'); |
| + | renderChain(vuln); // when the current library wrongly accepts, flag the certs it should not have trusted |
| + | document.getElementById('hint').innerHTML = keyUsage |
| + | ? 'Now switch <b>keyUsage</b> off: the current library will accept a chain it just rejected. The fix will not move.' |
| + | : 'Flip <b>keyUsage</b> back on: the current library suddenly rejects the very same chain. Its decision hinges on a field that has nothing to do with path limits. The fix never let it.'; |
| + | } |
| | |
| - | var v=document.getElementById('verdict'); v.style.display='block'; |
| - | if(accepted){ |
| - | v.className='verdict accept'; |
| - | v.innerHTML='✗ CHAIN ACCEPTED, and it should not have been.'+ |
| - | '<small>A pathLenConstraint=0 CA just issued a sub-CA, and the validator trusted it. '+ |
| - | (state.Fix?'':'The keyUsage extension is absent, so the path-length check never ran.')+'</small>'; |
| - | } else { |
| - | v.className='verdict reject'; |
| - | v.innerHTML='✓ CHAIN REJECTED · pathLenConstraint enforced.'+ |
| - | '<small>'+(state.Fix?'The patch enforces the constraint regardless of keyUsage.':'keyUsage is present, so this validator does run the path-length check.')+'</small>'; |
| - | } |
| + | function toggle(){ |
| + | keyUsage = !keyUsage; |
| + | document.getElementById('tgKU').classList.toggle('on', keyUsage); |
| + | evaluate(); |
| | } |
| | |
| - | renderChain(false); |
| - | document.getElementById('con').innerHTML='<span class="dim">// click ▶ to run the validator on the chain at left</span>'; |
| + | evaluate(); |
| | </script> |
| | </body> |
| | </html> |