Zion Boggan zionboggan.com ↗

Redesign sandbox: one keyUsage toggle, two side-by-side validators (current vs patched)

65c390d   Zion Boggan committed on May 31, 2026 (3 weeks ago)
featured-finding/index.html +82 -92
@@ -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>