| | @@ -0,0 +1,435 @@ |
| + | <!doctype html> |
| + | <html lang="en"> |
| + | <head> |
| + | <meta charset="utf-8"> |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| + | <title>Featured Finding: Certificate path-length bypass | Zion Boggan</title> |
| + | <meta name="description" content="A coordinated-disclosure vulnerability finding by Zion Boggan: an RFC 5280 certificate path-length constraint that a widely-deployed crypto library enforces only when an unrelated extension is present. Interactive in-browser demonstration of the bug class."> |
| + | <meta name="robots" content="index, follow"> |
| + | <link rel="canonical" href="https://zionboggan.com/featured-finding/"> |
| + | <meta property="og:type" content="article"> |
| + | <meta property="og:site_name" content="Zion Boggan"> |
| + | <meta property="og:title" content="Featured Finding: Certificate path-length bypass (coordinated disclosure)"> |
| + | <meta property="og:description" content="A logic-gating flaw in certificate-chain validation, found via variant analysis of a just-released security patch. Interactive demo of the bug class; full details and CVE after the fix ships."> |
| + | <meta property="og:url" content="https://zionboggan.com/featured-finding/"> |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> |
| + | <meta name="twitter:card" content="summary_large_image"> |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> |
| + | <style> |
| + | :root{ |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; |
| + | --line:#222936; --line2:#2c3543; |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; |
| + | --accent:#6cc7b8; --accent-dim:#274b47; --red:#ff6b6b; --redbg:#2a0d0d; |
| + | --maxw:1020px; |
| + | } |
| + | *{box-sizing:border-box;} |
| + | html{scroll-behavior:smooth;-webkit-text-size-adjust:100%;text-size-adjust:100%;} |
| + | body{margin:0;background:var(--bg);color:var(--ink);overflow-x:hidden; |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased; |
| + | -webkit-tap-highlight-color:rgba(108,199,184,.18);} |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} |
| + | a{color:var(--accent);text-decoration:none;} |
| + | a:hover{color:#8fe0d2;} |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} |
| + | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} |
| + | nav .brand .dot{color:var(--accent);} |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} |
| + | nav .links a{color:var(--muted);} |
| + | nav .links a:hover{color:var(--ink);} |
| + | @media(max-width:680px){nav .links{display:none;}} |
| + | |
| + | header.hero{padding:60px 0 44px;border-bottom:1px solid var(--line); |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} |
| + | .crumb{font-size:13px;color:var(--muted);margin-bottom:22px;} |
| + | .status{font-size:12px;letter-spacing:1.4px;text-transform:uppercase;color:var(--accent); |
| + | display:inline-flex;align-items:center;gap:9px;margin-bottom:18px; |
| + | border:1px solid var(--accent-dim);border-radius:999px;padding:6px 14px;background:#0e1a18;} |
| + | .status .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} |
| + | h1{font-size:clamp(28px,5vw,42px);line-height:1.08;margin:0 0 12px;letter-spacing:-.6px;font-weight:680;} |
| + | .hero .lede{max-width:720px;color:var(--soft);font-size:17px;margin:0 0 22px;} |
| + | .hero .lede b{color:var(--ink);font-weight:600;} |
| + | .facts{display:flex;flex-wrap:wrap;gap:8px 10px;margin-top:8px;} |
| + | .facts span{font-size:12px;color:var(--soft);background:var(--panel);border:1px solid var(--line); |
| + | border-radius:6px;padding:5px 11px;} |
| + | .facts span b{color:var(--accent);font-weight:600;} |
| + | |
| + | section{padding:52px 0;border-bottom:1px solid var(--line);} |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:26px;} |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} |
| + | p.body{color:var(--soft);max-width:760px;} |
| + | p.body b{color:var(--ink);font-weight:600;} |
| + | .callout{border-left:2px solid var(--accent-dim);padding:2px 0 2px 18px;margin:22px 0;color:var(--soft);max-width:740px;} |
| + | |
| + | /* sandbox */ |
| + | .lab{border:1px solid var(--line2);border-radius:12px;overflow:hidden;background:#0a0c10; |
| + | box-shadow:0 0 0 1px rgba(108,199,184,.08), 0 26px 64px -28px rgba(0,0,0,.75);} |
| + | .labbar{display:flex;align-items:center;gap:8px;padding:11px 14px;background:#11151b;border-bottom:1px solid var(--line);} |
| + | .labbar .d{width:11px;height:11px;border-radius:50%;} |
| + | .labbar .r{background:#ff5f57;}.labbar .y{background:#febc2e;}.labbar .g{background:#28c840;} |
| + | .labbar .dlabel{color:var(--faint);font-size:12.5px;margin-left:8px;} |
| + | .labbar .dbadge{margin-left:auto;color:#06231f;background:var(--accent);font-size:11px;font-weight:700; |
| + | padding:3px 9px;border-radius:5px;letter-spacing:.5px;} |
| + | .labgrid{display:grid;grid-template-columns:1fr 1fr;gap:0;} |
| + | @media(max-width:760px){.labgrid{grid-template-columns:1fr;}} |
| + | .labcol{padding:20px 22px;} |
| + | .labcol.left{border-right:1px solid var(--line);} |
| + | @media(max-width:760px){.labcol.left{border-right:none;border-bottom:1px solid var(--line);}} |
| + | .labcol h4{margin:0 0 4px;font-size:12px;letter-spacing:1.2px;text-transform:uppercase;color:var(--faint);} |
| + | .chain{margin:14px 0 6px;} |
| + | .node{border:1px solid var(--line);border-radius:9px;padding:11px 13px;margin:0 0 6px;background:var(--panel); |
| + | position:relative;transition:border-color .18s,background .18s;} |
| + | .node .cn{font-size:14px;font-weight:600;color:var(--ink);} |
| + | .node .ext{font-size:11.5px;color:var(--muted);margin-top:3px;font-family:ui-monospace,Menlo,monospace;} |
| + | .node.rogue{border-color:#3a2530;} |
| + | .node.flagged{border-color:var(--red);background:var(--redbg);box-shadow:0 0 0 1px rgba(255,107,107,.25);} |
| + | .node.okca{border-color:var(--accent-dim);} |
| + | .arrow{color:var(--faint);font-size:13px;text-align:center;margin:1px 0;} |
| + | .toggles{margin:6px 0 0;} |
| + | .tg{display:flex;align-items:center;gap:11px;padding:11px 0;border-top:1px solid var(--line);cursor:pointer;} |
| + | .tg:first-child{border-top:none;} |
| + | .sw{width:42px;height:24px;border-radius:999px;background:#2a313d;position:relative;flex:none;transition:background .16s;} |
| + | .sw::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%; |
| + | background:#cfd6df;transition:transform .16s;} |
| + | .tg.on .sw{background:var(--accent);} |
| + | .tg.on .sw::after{transform:translateX(18px);background:#06231f;} |
| + | .tg .tt{font-size:13.5px;color:var(--soft);} |
| + | .tg .tt b{color:var(--ink);} |
| + | .tg .tt small{display:block;color:var(--faint);font-size:11.5px;} |
| + | .runbtn{margin-top:16px;width:100%;display:inline-flex;align-items:center;justify-content:center;gap:8px; |
| + | padding:11px 18px;border-radius:8px;font-size:14.5px;font-weight:600;cursor:pointer; |
| + | background:var(--accent);color:#06231f;border:1px solid var(--accent);} |
| + | .runbtn:hover{background:#8fe0d2;} |
| + | .console{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;line-height:1.6;color:var(--soft); |
| + | background:#070809;border-radius:8px;padding:13px 14px;min-height:172px;white-space:pre-wrap; |
| + | border:1px solid var(--line);} |
| + | .console .ok{color:var(--accent);} |
| + | .console .bad{color:var(--red);} |
| + | .console .dim{color:var(--faint);} |
| + | .verdict{margin-top:13px;padding:12px 14px;border-radius:8px;font-size:13.5px;font-weight:600; |
| + | border:1px solid var(--line);} |
| + | .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;} |
| + | |
| + | .ruled{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:8px;} |
| + | @media(max-width:680px){.ruled{grid-template-columns:1fr;}} |
| + | .rcard{border:1px solid var(--line);border-radius:10px;padding:15px 16px;background:var(--panel);} |
| + | .rcard .h{display:flex;align-items:center;gap:9px;margin-bottom:6px;} |
| + | .rcard .neg{font-size:10.5px;font-weight:700;letter-spacing:.5px;color:#06231f;background:var(--faint); |
| + | padding:2px 7px;border-radius:4px;} |
| + | .rcard h3{margin:0;font-size:14.5px;} |
| + | .rcard p{margin:0;font-size:13px;color:var(--muted);} |
| + | |
| + | .timeline{list-style:none;margin:6px 0 0;padding:0;max-width:760px;} |
| + | .timeline li{display:grid;grid-template-columns:128px 1fr;gap:16px;padding:12px 0;border-top:1px solid var(--line);} |
| + | .timeline li:first-child{border-top:none;} |
| + | .timeline .when{font-size:12.5px;color:var(--accent);font-family:ui-monospace,Menlo,monospace;} |
| + | .timeline .what{font-size:14px;color:var(--soft);} |
| + | .timeline .what b{color:var(--ink);} |
| + | .pending{color:var(--faint);} |
| + | .embargo{border:1px dashed var(--line2);border-radius:10px;padding:16px 18px;color:var(--muted); |
| + | font-size:13.5px;max-width:760px;background:#0e1116;} |
| + | .embargo b{color:var(--soft);} |
| + | |
| + | footer{padding:42px 0 64px;} |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:560px;} |
| + | |
| + | /* ---- mobile (iPhone / iPad) ---- */ |
| + | @media(max-width:760px){ |
| + | .labbar{flex-wrap:wrap;row-gap:6px;} |
| + | .labbar .dbadge{margin-left:auto;} |
| + | } |
| + | @media(max-width:680px){ |
| + | .wrap{padding:0 18px;} |
| + | header.hero{padding:44px 0 34px;} |
| + | section{padding:40px 0;} |
| + | .dtop{display:none;} |
| + | h1{font-size:clamp(25px,7.5vw,34px);} |
| + | .timeline li{grid-template-columns:1fr;gap:2px;} |
| + | .timeline .when{color:var(--faint);} |
| + | .labcol{padding:17px 16px;} |
| + | .runbtn{padding:13px 18px;} /* comfortable tap target */ |
| + | .tg{padding:13px 0;} |
| + | .console{font-size:12px;min-height:150px;} |
| + | .node .ext{word-break:break-word;} |
| + | } |
| + | @media(max-width:380px){ |
| + | .facts span,.rcard p{font-size:11.5px;} |
| + | h1{font-size:23px;} |
| + | } |
| + | </style> |
| + | </head> |
| + | <body> |
| + | |
| + | <nav><div class="wrap"> |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> |
| + | <span class="links"> |
| + | <a href="/#proof">Demos</a> |
| + | <a href="/#research">Research</a> |
| + | <a href="/security-research-notebook/">Notebook</a> |
| + | <a href="https://github.com/zionboggan">GitHub</a> |
| + | </span> |
| + | </div></nav> |
| + | |
| + | <header class="hero"><div class="wrap"> |
| + | <div class="crumb mono"><a href="/">home</a> / featured finding</div> |
| + | <div class="status mono"><span class="pulse"></span>Coordinated disclosure in progress · CVE pending</div> |
| + | <h1>A certificate path-length limit that disappears<br class="dtop">when you remove an unrelated field</h1> |
| + | <p class="lede">A widely-deployed open-source cryptography library enforces an <b>RFC 5280 |
| + | path-length constraint</b> (the rule that stops a subordinate CA from minting further |
| + | CAs) <b>only when the certificate also carries a separate, unrelated extension</b>. Drop |
| + | that extension and the limit is silently skipped: a CA that was explicitly forbidden from |
| + | delegating can issue rogue sub-CAs that the library accepts as valid. I found it by |
| + | <b>variant-hunting a security patch the maintainers had just shipped</b>. The fix closed |
| + | one instance of the gating mistake and left this one behind.</p> |
| + | <div class="facts mono"> |
| + | <span>Class · <b>CWE-295</b> improper certificate validation</span> |
| + | <span>Standard · <b>RFC 5280</b> §6.1.4 / §4.2.1.9</span> |
| + | <span>Impact · <b>CA constraint bypass</b></span> |
| + | <span>Severity · <b>Medium</b> (~CVSS 6.0)</span> |
| + | <span>Confirmed on the <b>current shipped release</b></span> |
| + | </div> |
| + | </div></header> |
| + | |
| + | <!-- ============ INTERACTIVE SANDBOX ============ --> |
| + | <section><div class="wrap"> |
| + | <div class="shead"><span class="idx mono">01</span><h2>See the bug in your browser</h2><span class="rule"></span></div> |
| + | <p class="body">This is a faithful, self-contained reproduction of the flaw's logic (no |
| + | library named yet; that waits for the fix). You're looking at a certificate chain where a |
| + | 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> |
| + | |
| + | <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="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> |
| + | </div> |
| + | </div> |
| + | <button class="runbtn mono" onclick="runVerify()">▶ verifyCertificateChain(chain)</button> |
| + | </div> |
| + | <div class="labcol"> |
| + | <h4>Validator output</h4> |
| + | <div class="console mono" id="con"></div> |
| + | <div class="verdict" id="verdict" style="display:none"></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> |
| + | </div></div></section> |
| + | |
| + | <!-- ============ HOW IT WORKS ============ --> |
| + | <section><div class="wrap"> |
| + | <div class="shead"><span class="idx mono">02</span><h2>Why it happens</h2><span class="rule"></span></div> |
| + | <p class="body">Certificate-chain validators decide whether each intermediate is allowed to |
| + | act as a CA. RFC 5280 makes two of those decisions <b>independent</b>: §4.2.1.9 says a |
| + | <span class="mono">basicConstraints</span> <span class="mono">pathLenConstraint</span> caps |
| + | how many CAs may follow in the path, and §6.1.4 applies that cap to <b>every</b> intermediate |
| + | unconditionally. The presence of a <span class="mono">keyUsage</span> extension is governed |
| + | separately by §4.2.1.3 and has nothing to do with path-length processing.</p> |
| + | <p class="body">The vulnerable code couples them anyway: the path-length check sits behind a |
| + | guard that only runs <span class="mono">if (keyUsage extension is present)</span>. The author's |
| + | reasoning was that having parsed <span class="mono">keyUsage</span> they "know" the basic |
| + | constraints are present too, but that conflates two orthogonal extensions. Remove |
| + | <span class="mono">keyUsage</span> and a real, signed, constraint-bearing CA escapes its own |
| + | limit. It's the same <b>"gate a mandatory check on an optional field"</b> anti-pattern that a |
| + | recently-published advisory fixed for a sibling check. This is the instance that fix didn't reach.</p> |
| + | <div class="embargo"> |
| + | <b>Held until the fix ships:</b> the exact library, file/line, the published advisory, the |
| + | assigned CVE, and the real end-to-end proof-of-concept against the live release. Those will |
| + | appear here once the maintainers release a patched version. Responsible disclosure first, |
| + | write-up second. <span class="pending">(Private report submitted to the maintainer.)</span> |
| + | </div> |
| + | </div></div></section> |
| + | |
| + | <!-- ============ WHAT I RULED OUT ============ --> |
| + | <section><div class="wrap"> |
| + | <div class="shead"><span class="idx mono">03</span><h2>What I ruled out</h2><span class="rule"></span></div> |
| + | <p class="body">Finding the bug was half the work; <b>not crying wolf on the others was the |
| + | rest</b>. I ran four more independent deep analyses against the same library's most |
| + | security-critical paths. Each produced a rigorous negative, and a negative I can defend is |
| + | worth more than a finding I can't. This is the discipline that keeps a report from getting |
| + | auto-rejected as noise.</p> |
| + | <div class="ruled"> |
| + | <div class="rcard"> |
| + | <div class="h"><span class="neg mono">RULED OUT</span><h3>ASN.1 validator desync</h3></div> |
| + | <p>A recently-rewritten schema validator does ignore trailing elements, but every |
| + | trust-bearing sink is guarded by an outer length check or sits under a real signature. |
| + | Not weaponizable.</p> |
| + | </div> |
| + | <div class="rcard"> |
| + | <div class="h"><span class="neg mono">RULED OUT</span><h3>RSA PKCS#1 v1.5 forgery</h3></div> |
| + | <p>Probed the Bleichenbacher / low-exponent class with real e=3 keys. Tight padding + |
| + | full-byte parsing + exact-digest compare pin the hash to the low-order bytes. No |
| + | no-private-key forgery.</p> |
| + | </div> |
| + | <div class="rcard"> |
| + | <div class="h"><span class="neg mono">RULED OUT</span><h3>Ed25519 malleability</h3></div> |
| + | <p>Differential-tested against OpenSSL across non-canonical, low-order and cofactor edge |
| + | cases. The S<L fix plus byte-exact R compare make the accepted-signature set a |
| + | singleton. No divergence.</p> |
| + | </div> |
| + | <div class="rcard"> |
| + | <div class="h"><span class="neg mono">RULED OUT</span><h3>X.509 chain confusion</h3></div> |
| + | <p>DN/issuer spoofing, trust-anchor confusion, critical-extension smuggling, algorithm |
| + | downgrade. Trust anchoring is byte-exact; no false-accept beyond the path-length issue |
| + | above.</p> |
| + | </div> |
| + | </div> |
| + | </div></div></section> |
| + | |
| + | <!-- ============ TIMELINE ============ --> |
| + | <section><div class="wrap"> |
| + | <div class="shead"><span class="idx mono">04</span><h2>Disclosure timeline</h2><span class="rule"></span></div> |
| + | <ul class="timeline"> |
| + | <li><span class="when">2026-05-31</span><span class="what"><b>Discovered & confirmed.</b> Variant analysis of a just-released security patch; working PoC on the current shipped release.</span></li> |
| + | <li><span class="when">2026-05-31</span><span class="what"><b>Reported privately</b> to the maintainer through their security-advisory channel.</span></li> |
| + | <li><span class="when">pending</span><span class="what pending">Maintainer triage & fix.</span></li> |
| + | <li><span class="when">pending</span><span class="what pending">CVE assigned · advisory published.</span></li> |
| + | <li><span class="when">pending</span><span class="what pending"><b>This page unlocks:</b> library named, file/line, CVE link, live PoC against the patched-vs-vulnerable release.</span></li> |
| + | </ul> |
| + | </div></div></section> |
| + | |
| + | <footer><div class="wrap"> |
| + | <div class="row"> |
| + | <div class="links"> |
| + | <a href="/">← Back to home</a> |
| + | <a href="/security-research-notebook/">Research notebook</a> |
| + | <a href="https://github.com/zionboggan">GitHub</a> |
| + | </div> |
| + | </div> |
| + | <p class="note">Research conducted against open-source code under coordinated disclosure. No |
| + | systems were attacked; all proofs run against local copies and lab reproductions. Full |
| + | technical detail is withheld until a fixed release is available.</p> |
| + | </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 }; |
| + | |
| + | 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) |
| + | ]; |
| + | |
| + | 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 renderChain(flagged){ |
| + | var rows = [ |
| + | {cn:'Demo Root CA', ext:'trust anchor · basicConstraints: cA=true', cls:'okca'} |
| + | ]; |
| + | // 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>'; |
| + | }); |
| + | 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 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 */); |
| + | |
| + | 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>'; |
| + | } |
| + | } |
| + | |
| + | renderChain(false); |
| + | document.getElementById('con').innerHTML='<span class="dim">// click ▶ to run the validator on the chain at left</span>'; |
| + | </script> |
| + | </body> |
| + | </html> |