| 1 | <!doctype html> |
| 2 | <html lang="en"> |
| 3 | <head> |
| 4 | <meta charset="utf-8"> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | <title>Featured Finding: Certificate path-length bypass | Zion Boggan</title> |
| 7 | <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."> |
| 8 | <meta name="robots" content="index, follow"> |
| 9 | <link rel="canonical" href="https://zionboggan.com/featured-finding/"> |
| 10 | <meta property="og:type" content="article"> |
| 11 | <meta property="og:site_name" content="Zion Boggan"> |
| 12 | <meta property="og:title" content="Featured Finding: Certificate path-length bypass (coordinated disclosure)"> |
| 13 | <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 after the fix ships."> |
| 14 | <meta property="og:url" content="https://zionboggan.com/featured-finding/"> |
| 15 | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> |
| 16 | <meta name="twitter:card" content="summary_large_image"> |
| 17 | <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"> |
| 18 | <style> |
| 19 | :root{ |
| 20 | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; |
| 21 | --line:#222936; --line2:#2c3543; |
| 22 | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; |
| 23 | --accent:#6cc7b8; --accent-dim:#274b47; --red:#ff6b6b; --redbg:#2a0d0d; |
| 24 | --maxw:1020px; |
| 25 | } |
| 26 | *{box-sizing:border-box;} |
| 27 | html{scroll-behavior:smooth;-webkit-text-size-adjust:100%;text-size-adjust:100%;} |
| 28 | body{margin:0;background:var(--bg);color:var(--ink);overflow-x:hidden; |
| 29 | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; |
| 30 | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased; |
| 31 | -webkit-tap-highlight-color:rgba(108,199,184,.18);} |
| 32 | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} |
| 33 | a{color:var(--accent);text-decoration:none;} |
| 34 | a:hover{color:#8fe0d2;} |
| 35 | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} |
| 36 | |
| 37 | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); |
| 38 | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} |
| 39 | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} |
| 40 | nav .brand{font-weight:600;letter-spacing:.2px;} |
| 41 | nav .brand .dot{color:var(--accent);} |
| 42 | nav .links{display:flex;gap:26px;font-size:13.5px;} |
| 43 | nav .links a{color:var(--muted);} |
| 44 | nav .links a:hover{color:var(--ink);} |
| 45 | @media(max-width:680px){nav .links{display:none;}} |
| 46 | |
| 47 | header.hero{padding:60px 0 44px;border-bottom:1px solid var(--line); |
| 48 | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} |
| 49 | .crumb{font-size:13px;color:var(--muted);margin-bottom:22px;} |
| 50 | .status{font-size:12px;letter-spacing:1.4px;text-transform:uppercase;color:var(--accent); |
| 51 | display:inline-flex;align-items:center;gap:9px;margin-bottom:18px; |
| 52 | border:1px solid var(--accent-dim);border-radius:999px;padding:6px 14px;background:#0e1a18;} |
| 53 | .status .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); |
| 54 | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} |
| 55 | @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)}} |
| 56 | h1{font-size:clamp(28px,5vw,42px);line-height:1.08;margin:0 0 12px;letter-spacing:-.6px;font-weight:680;} |
| 57 | .hero .lede{max-width:720px;color:var(--soft);font-size:17px;margin:0 0 22px;} |
| 58 | .hero .lede b{color:var(--ink);font-weight:600;} |
| 59 | .facts{display:flex;flex-wrap:wrap;gap:8px 10px;margin-top:8px;} |
| 60 | .facts span{font-size:12px;color:var(--soft);background:var(--panel);border:1px solid var(--line); |
| 61 | border-radius:6px;padding:5px 11px;} |
| 62 | .facts span b{color:var(--accent);font-weight:600;} |
| 63 | |
| 64 | section{padding:52px 0;border-bottom:1px solid var(--line);} |
| 65 | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:26px;} |
| 66 | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} |
| 67 | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} |
| 68 | .shead .rule{flex:1;height:1px;background:var(--line);} |
| 69 | p.body{color:var(--soft);max-width:760px;} |
| 70 | p.body b{color:var(--ink);font-weight:600;} |
| 71 | .callout{border-left:2px solid var(--accent-dim);padding:2px 0 2px 18px;margin:22px 0;color:var(--soft);max-width:740px;} |
| 72 | |
| 73 | /* sandbox */ |
| 74 | .lab{border:1px solid var(--line2);border-radius:12px;overflow:hidden;background:#0a0c10; |
| 75 | box-shadow:0 0 0 1px rgba(108,199,184,.08), 0 26px 64px -28px rgba(0,0,0,.75);} |
| 76 | .labbar{display:flex;align-items:center;gap:8px;padding:11px 14px;background:#11151b;border-bottom:1px solid var(--line);} |
| 77 | .labbar .d{width:11px;height:11px;border-radius:50%;} |
| 78 | .labbar .r{background:#ff5f57;}.labbar .y{background:#febc2e;}.labbar .g{background:#28c840;} |
| 79 | .labbar .dlabel{color:var(--faint);font-size:12.5px;margin-left:8px;} |
| 80 | .labbar .dbadge{margin-left:auto;color:#06231f;background:var(--accent);font-size:11px;font-weight:700; |
| 81 | padding:3px 9px;border-radius:5px;letter-spacing:.5px;} |
| 82 | .labgrid{display:grid;grid-template-columns:1fr 1fr;gap:0;} |
| 83 | @media(max-width:760px){.labgrid{grid-template-columns:1fr;}} |
| 84 | .labcol{padding:20px 22px;} |
| 85 | .labcol.left{border-right:1px solid var(--line);} |
| 86 | @media(max-width:760px){.labcol.left{border-right:none;border-bottom:1px solid var(--line);}} |
| 87 | .labcol h4{margin:0 0 4px;font-size:12px;letter-spacing:1.2px;text-transform:uppercase;color:var(--faint);} |
| 88 | .chain{margin:14px 0 6px;} |
| 89 | .node{border:1px solid var(--line);border-radius:9px;padding:11px 13px;margin:0 0 6px;background:var(--panel); |
| 90 | position:relative;transition:border-color .18s,background .18s;} |
| 91 | .node .cn{font-size:14px;font-weight:600;color:var(--ink);} |
| 92 | .node .ext{font-size:11.5px;color:var(--muted);margin-top:3px;font-family:ui-monospace,Menlo,monospace;} |
| 93 | .node.rogue{border-color:#3a2530;} |
| 94 | .node.flagged{border-color:var(--red);background:var(--redbg);box-shadow:0 0 0 1px rgba(255,107,107,.25);} |
| 95 | .node.okca{border-color:var(--accent-dim);} |
| 96 | .arrow{color:var(--faint);font-size:13px;text-align:center;margin:1px 0;} |
| 97 | .toggles{margin:6px 0 0;} |
| 98 | .tg{display:flex;align-items:center;gap:11px;padding:11px 0;border-top:1px solid var(--line);cursor:pointer;} |
| 99 | .tg:first-child{border-top:none;} |
| 100 | .sw{width:42px;height:24px;border-radius:999px;background:#2a313d;position:relative;flex:none;transition:background .16s;} |
| 101 | .sw::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%; |
| 102 | background:#cfd6df;transition:transform .16s;} |
| 103 | .tg.on .sw{background:var(--accent);} |
| 104 | .tg.on .sw::after{transform:translateX(18px);background:#06231f;} |
| 105 | .tg .tt{font-size:13.5px;color:var(--soft);} |
| 106 | .tg .tt b{color:var(--ink);} |
| 107 | .tg .tt small{display:block;color:var(--faint);font-size:11.5px;} |
| 108 | .runbtn{margin-top:16px;width:100%;display:inline-flex;align-items:center;justify-content:center;gap:8px; |
| 109 | padding:11px 18px;border-radius:8px;font-size:14.5px;font-weight:600;cursor:pointer; |
| 110 | background:var(--accent);color:#06231f;border:1px solid var(--accent);} |
| 111 | .runbtn:hover{background:#8fe0d2;} |
| 112 | .console{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;line-height:1.6;color:var(--soft); |
| 113 | background:#070809;border-radius:8px;padding:13px 14px;min-height:172px;white-space:pre-wrap; |
| 114 | border:1px solid var(--line);} |
| 115 | .console .ok{color:var(--accent);} |
| 116 | .console .bad{color:var(--red);} |
| 117 | .console .dim{color:var(--faint);} |
| 118 | .verdict{margin-top:13px;padding:12px 14px;border-radius:8px;font-size:13.5px;font-weight:600; |
| 119 | border:1px solid var(--line);} |
| 120 | .verdict.accept{background:var(--redbg);border-color:var(--red);color:#ffb4b4;} |
| 121 | .verdict.reject{background:#0e1a18;border-color:var(--accent-dim);color:#a9e7db;} |
| 122 | .verdict small{display:block;font-weight:400;color:var(--soft);margin-top:3px;font-size:12px;} |
| 123 | .labnote{margin-top:14px;font-size:11.5px;color:var(--faint);} |
| 124 | .vrow{display:flex;flex-direction:column;gap:12px;margin-top:14px;} |
| 125 | .vbox{border:1px solid var(--line);border-radius:10px;padding:13px 15px;background:var(--panel); |
| 126 | transition:border-color .18s,background .18s;} |
| 127 | .vbox .vlabel{font-size:11px;letter-spacing:.8px;text-transform:uppercase;color:var(--faint);margin-bottom:7px;} |
| 128 | .vstate{font-size:15.5px;font-weight:700;} |
| 129 | .vwhy{font-size:12.5px;color:var(--muted);margin-top:5px;line-height:1.5;} |
| 130 | .vbox.accept{border-color:var(--red);background:var(--redbg);} |
| 131 | .vbox.accept .vstate{color:#ffb4b4;} |
| 132 | .vbox.reject{border-color:var(--accent-dim);background:#0e1a18;} |
| 133 | .vbox.reject .vstate{color:#a9e7db;} |
| 134 | .vhint{margin-top:15px;font-size:13px;color:var(--soft);border-left:2px solid var(--accent-dim); |
| 135 | padding-left:13px;line-height:1.55;} |
| 136 | .vhint b{color:var(--ink);} |
| 137 | |
| 138 | .ruled{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:8px;} |
| 139 | @media(max-width:680px){.ruled{grid-template-columns:1fr;}} |
| 140 | .rcard{border:1px solid var(--line);border-radius:10px;padding:15px 16px;background:var(--panel);} |
| 141 | .rcard .h{display:flex;align-items:center;gap:9px;margin-bottom:6px;} |
| 142 | .rcard .neg{font-size:10.5px;font-weight:700;letter-spacing:.5px;color:#06231f;background:var(--faint); |
| 143 | padding:2px 7px;border-radius:4px;} |
| 144 | .rcard h3{margin:0;font-size:14.5px;} |
| 145 | .rcard p{margin:0;font-size:13px;color:var(--muted);} |
| 146 | |
| 147 | .timeline{list-style:none;margin:6px 0 0;padding:0;max-width:760px;} |
| 148 | .timeline li{display:grid;grid-template-columns:128px 1fr;gap:16px;padding:12px 0;border-top:1px solid var(--line);} |
| 149 | .timeline li:first-child{border-top:none;} |
| 150 | .timeline .when{font-size:12.5px;color:var(--accent);font-family:ui-monospace,Menlo,monospace;} |
| 151 | .timeline .what{font-size:14px;color:var(--soft);} |
| 152 | .timeline .what b{color:var(--ink);} |
| 153 | .pending{color:var(--faint);} |
| 154 | .embargo{border:1px dashed var(--line2);border-radius:10px;padding:16px 18px;color:var(--muted); |
| 155 | font-size:13.5px;max-width:760px;background:#0e1116;} |
| 156 | .embargo b{color:var(--soft);} |
| 157 | |
| 158 | footer{padding:42px 0 64px;} |
| 159 | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} |
| 160 | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} |
| 161 | footer .note{color:var(--faint);font-size:12.5px;max-width:560px;} |
| 162 | |
| 163 | /* ---- mobile (iPhone / iPad) ---- */ |
| 164 | @media(max-width:760px){ |
| 165 | .labbar{flex-wrap:wrap;row-gap:6px;} |
| 166 | .labbar .dbadge{margin-left:auto;} |
| 167 | } |
| 168 | @media(max-width:680px){ |
| 169 | .wrap{padding:0 18px;} |
| 170 | header.hero{padding:44px 0 34px;} |
| 171 | section{padding:40px 0;} |
| 172 | .dtop{display:none;} |
| 173 | h1{font-size:clamp(25px,7.5vw,34px);} |
| 174 | .timeline li{grid-template-columns:1fr;gap:2px;} |
| 175 | .timeline .when{color:var(--faint);} |
| 176 | .labcol{padding:17px 16px;} |
| 177 | .runbtn{padding:13px 18px;} /* comfortable tap target */ |
| 178 | .tg{padding:13px 0;} |
| 179 | .console{font-size:12px;min-height:150px;} |
| 180 | .node .ext{word-break:break-word;} |
| 181 | } |
| 182 | @media(max-width:380px){ |
| 183 | .facts span,.rcard p{font-size:11.5px;} |
| 184 | h1{font-size:23px;} |
| 185 | } |
| 186 | </style> |
| 187 | </head> |
| 188 | <body> |
| 189 | |
| 190 | <nav><div class="wrap"> |
| 191 | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> |
| 192 | <span class="links"> |
| 193 | <a href="/#proof">Demos</a> |
| 194 | <a href="/#research">Research</a> |
| 195 | <a href="/security-research-notebook/">Notebook</a> |
| 196 | <a href="https://github.com/zionboggan">GitHub</a> |
| 197 | </span> |
| 198 | </div></nav> |
| 199 | |
| 200 | <header class="hero"><div class="wrap"> |
| 201 | <div class="crumb mono"><a href="/">home</a> / featured finding</div> |
| 202 | <div class="status mono"><span class="pulse"></span>Coordinated disclosure in progress</div> |
| 203 | <h1>A certificate path-length limit that disappears<br class="dtop">when you remove an unrelated field</h1> |
| 204 | <p class="lede">A widely-deployed open-source cryptography library enforces an <b>RFC 5280 |
| 205 | path-length constraint</b> (the rule that stops a subordinate CA from minting further |
| 206 | CAs) <b>only when the certificate also carries a separate, unrelated extension</b>. Drop |
| 207 | that extension and the limit is silently skipped: a CA that was explicitly forbidden from |
| 208 | delegating can issue rogue sub-CAs that the library accepts as valid. I found it by |
| 209 | <b>variant-hunting a security patch the maintainers had just shipped</b>. The fix closed |
| 210 | one instance of the gating mistake and left this one behind.</p> |
| 211 | <div class="facts mono"> |
| 212 | <span>Class · <b>CWE-295</b> improper certificate validation</span> |
| 213 | <span>Standard · <b>RFC 5280</b> §6.1.4 / §4.2.1.9</span> |
| 214 | <span>Impact · <b>CA constraint bypass</b></span> |
| 215 | <span>Severity · <b>Medium</b> (~CVSS 6.0)</span> |
| 216 | <span>Confirmed on the <b>current shipped release</b></span> |
| 217 | </div> |
| 218 | </div></header> |
| 219 | |
| 220 | <section><div class="wrap"> |
| 221 | <div class="shead"><span class="idx mono">01</span><h2>See the bug in your browser</h2><span class="rule"></span></div> |
| 222 | <p class="body">This is a faithful, self-contained reproduction of the flaw's logic (no |
| 223 | library named yet; that waits for the fix). You're looking at a certificate chain where a |
| 224 | CA limited to <span class="mono">pathLenConstraint = 0</span>, meaning <i>"you may issue |
| 225 | end-entity certificates only, never another CA"</i>, has nonetheless issued a sub-CA. |
| 226 | A correct validator must reject this. <b>Toggle the <span class="mono">keyUsage</span> |
| 227 | extension</b> on that intermediate and watch the current library change its verdict on a chain that |
| 228 | should always be rejected, while the patched build stays put.</p> |
| 229 | |
| 230 | <div class="lab"> |
| 231 | <div class="labbar"> |
| 232 | <span class="d r"></span><span class="d y"></span><span class="d g"></span> |
| 233 | <span class="dlabel mono">minipki · verifyCertificateChain()</span> |
| 234 | </div> |
| 235 | <div class="labgrid"> |
| 236 | <div class="labcol left"> |
| 237 | <h4>Certificate chain (attacker-supplied)</h4> |
| 238 | <div class="chain" id="chain"></div> |
| 239 | <div class="toggles"> |
| 240 | <div class="tg" id="tgKU" onclick="toggle()"> |
| 241 | <div class="sw"></div> |
| 242 | <div class="tt"><b>keyUsage</b> extension on the constrained intermediate |
| 243 | <small>An unrelated field. Real CAs include it; minimal tooling often omits it.</small></div> |
| 244 | </div> |
| 245 | </div> |
| 246 | <p class="labnote mono">Both validators re-evaluate the instant you flip the switch.</p> |
| 247 | </div> |
| 248 | <div class="labcol"> |
| 249 | <h4>Same chain, two validators</h4> |
| 250 | <div class="vrow"> |
| 251 | <div class="vbox" id="box_vuln"> |
| 252 | <div class="vlabel">Current library</div> |
| 253 | <div class="vstate" id="st_vuln"></div> |
| 254 | <div class="vwhy" id="why_vuln"></div> |
| 255 | </div> |
| 256 | <div class="vbox" id="box_patch"> |
| 257 | <div class="vlabel">With my one-line fix</div> |
| 258 | <div class="vstate" id="st_patch"></div> |
| 259 | <div class="vwhy" id="why_patch"></div> |
| 260 | </div> |
| 261 | </div> |
| 262 | <div class="vhint" id="hint"></div> |
| 263 | </div> |
| 264 | </div> |
| 265 | </div> |
| 266 | <p class="callout">One toggle, two verdicts. The current library lets the presence of an |
| 267 | unrelated extension decide whether it enforces the path-length limit, so flipping |
| 268 | <span class="mono">keyUsage</span> flips its answer on a chain that should always be rejected. |
| 269 | The patched validator ignores that field and rejects the chain every time.</p> |
| 270 | </div></div></section> |
| 271 | |
| 272 | <section><div class="wrap"> |
| 273 | <div class="shead"><span class="idx mono">02</span><h2>Why it happens</h2><span class="rule"></span></div> |
| 274 | <p class="body">Certificate-chain validators decide whether each intermediate is allowed to |
| 275 | act as a CA. RFC 5280 makes two of those decisions <b>independent</b>: §4.2.1.9 says a |
| 276 | <span class="mono">basicConstraints</span> <span class="mono">pathLenConstraint</span> caps |
| 277 | how many CAs may follow in the path, and §6.1.4 applies that cap to <b>every</b> intermediate |
| 278 | unconditionally. The presence of a <span class="mono">keyUsage</span> extension is governed |
| 279 | separately by §4.2.1.3 and has nothing to do with path-length processing.</p> |
| 280 | <p class="body">The vulnerable code couples them anyway: the path-length check sits behind a |
| 281 | guard that only runs <span class="mono">if (keyUsage extension is present)</span>. The author's |
| 282 | reasoning was that having parsed <span class="mono">keyUsage</span> they "know" the basic |
| 283 | constraints are present too, but that conflates two orthogonal extensions. Remove |
| 284 | <span class="mono">keyUsage</span> and a real, signed, constraint-bearing CA escapes its own |
| 285 | limit. It's the same <b>"gate a mandatory check on an optional field"</b> anti-pattern that a |
| 286 | recently-published advisory fixed for a sibling check. This is the instance that fix didn't reach.</p> |
| 287 | <div class="embargo"> |
| 288 | <b>Held until the fix ships:</b> the exact library, file/line, the published advisory, and |
| 289 | the real end-to-end proof-of-concept against the live release. Those will |
| 290 | appear here once the maintainers release a patched version. Responsible disclosure first, |
| 291 | write-up second. <span class="pending">(Private report submitted to the maintainer.)</span> |
| 292 | </div> |
| 293 | </div></div></section> |
| 294 | |
| 295 | <section><div class="wrap"> |
| 296 | <div class="shead"><span class="idx mono">03</span><h2>What I ruled out</h2><span class="rule"></span></div> |
| 297 | <p class="body">Finding the bug was half the work; <b>not crying wolf on the others was the |
| 298 | rest</b>. I ran four more independent deep analyses against the same library's most |
| 299 | security-critical paths. Each produced a rigorous negative, and a negative I can defend is |
| 300 | worth more than a finding I can't. This is the discipline that keeps a report from getting |
| 301 | auto-rejected as noise.</p> |
| 302 | <div class="ruled"> |
| 303 | <div class="rcard"> |
| 304 | <div class="h"><span class="neg mono">RULED OUT</span><h3>ASN.1 validator desync</h3></div> |
| 305 | <p>A recently-rewritten schema validator does ignore trailing elements, but every |
| 306 | trust-bearing sink is guarded by an outer length check or sits under a real signature. |
| 307 | Not weaponizable.</p> |
| 308 | </div> |
| 309 | <div class="rcard"> |
| 310 | <div class="h"><span class="neg mono">RULED OUT</span><h3>RSA PKCS#1 v1.5 forgery</h3></div> |
| 311 | <p>Probed the Bleichenbacher / low-exponent class with real e=3 keys. Tight padding + |
| 312 | full-byte parsing + exact-digest compare pin the hash to the low-order bytes. No |
| 313 | no-private-key forgery.</p> |
| 314 | </div> |
| 315 | <div class="rcard"> |
| 316 | <div class="h"><span class="neg mono">RULED OUT</span><h3>Ed25519 malleability</h3></div> |
| 317 | <p>Differential-tested against OpenSSL across non-canonical, low-order and cofactor edge |
| 318 | cases. The S<L fix plus byte-exact R compare make the accepted-signature set a |
| 319 | singleton. No divergence.</p> |
| 320 | </div> |
| 321 | <div class="rcard"> |
| 322 | <div class="h"><span class="neg mono">RULED OUT</span><h3>X.509 chain confusion</h3></div> |
| 323 | <p>DN/issuer spoofing, trust-anchor confusion, critical-extension smuggling, algorithm |
| 324 | downgrade. Trust anchoring is byte-exact; no false-accept beyond the path-length issue |
| 325 | above.</p> |
| 326 | </div> |
| 327 | </div> |
| 328 | </div></div></section> |
| 329 | |
| 330 | <section><div class="wrap"> |
| 331 | <div class="shead"><span class="idx mono">04</span><h2>Disclosure timeline</h2><span class="rule"></span></div> |
| 332 | <ul class="timeline"> |
| 333 | <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> |
| 334 | <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> |
| 335 | <li><span class="when">pending</span><span class="what pending">Maintainer triage & fix.</span></li> |
| 336 | <li><span class="when">pending</span><span class="what pending">Maintainer publishes an advisory.</span></li> |
| 337 | <li><span class="when">pending</span><span class="what pending"><b>This page unlocks:</b> library named, file/line, advisory link, live PoC against the patched-vs-vulnerable release.</span></li> |
| 338 | </ul> |
| 339 | </div></div></section> |
| 340 | |
| 341 | <footer><div class="wrap"> |
| 342 | <div class="row"> |
| 343 | <div class="links"> |
| 344 | <a href="/">← Back to home</a> |
| 345 | <a href="/security-research-notebook/">Research notebook</a> |
| 346 | <a href="https://github.com/zionboggan">GitHub</a> |
| 347 | </div> |
| 348 | </div> |
| 349 | <p class="note">Research conducted against open-source code under coordinated disclosure. No |
| 350 | systems were attacked; all proofs run against local copies and lab reproductions. Full |
| 351 | technical detail is withheld until a fixed release is available.</p> |
| 352 | </div></footer> |
| 353 | |
| 354 | <script> |
| 355 | // disclosure-safe MiniPKI: two faithful validators over one attacker-supplied chain. |
| 356 | // No third-party code; models the logic with public RFC 5280 concepts. |
| 357 | // Chain (top to bottom): Demo Root CA (trust anchor) -> Constrained Intermediate |
| 358 | // (pathLenConstraint=0) -> Rogue Sub-CA -> leaf. A pathLen=0 CA must NOT issue another CA, |
| 359 | // so a correct validator always rejects this chain. |
| 360 | var keyUsage = false; // start in the divergent state so the difference is visible on load |
| 361 | |
| 362 | // returns true if the chain is ACCEPTED. enforceAlways=true models the patched build; |
| 363 | // the current build only enforces pathLen when a keyUsage extension is present (the bug). |
| 364 | function accepts(enforceAlways){ |
| 365 | var enforce = enforceAlways || keyUsage; |
| 366 | return enforce ? false : true; // one sub-CA follows pathLen=0 -> rejected when enforced |
| 367 | } |
| 368 | |
| 369 | function extLine(role){ |
| 370 | if(role==='leaf') return 'basicConstraints: cA=false'; |
| 371 | if(role==='inter') return 'basicConstraints: cA=true, pathLenConstraint=0' + (keyUsage ? ' +keyUsage' : ' (no keyUsage)'); |
| 372 | if(role==='sub') return 'basicConstraints: cA=true +keyUsage'; |
| 373 | return 'trust anchor · basicConstraints: cA=true'; |
| 374 | } |
| 375 | |
| 376 | function renderChain(flagged){ |
| 377 | var rows = [ |
| 378 | {cn:'Demo Root CA', role:'root', cls:'okca'}, |
| 379 | {cn:'Constrained Intermediate', role:'inter', cls:'okca'}, |
| 380 | {cn:'Rogue Sub-CA', role:'sub', cls: flagged ? 'flagged' : 'rogue'}, |
| 381 | {cn:'victim.example.com', role:'leaf', cls: flagged ? 'flagged' : ''} |
| 382 | ]; |
| 383 | var html=''; |
| 384 | rows.forEach(function(r,i){ |
| 385 | if(i) html += '<div class="arrow">│ issues ▼</div>'; |
| 386 | html += '<div class="node '+r.cls+'"><div class="cn">'+r.cn+'</div><div class="ext">'+extLine(r.role)+'</div></div>'; |
| 387 | }); |
| 388 | document.getElementById('chain').innerHTML = html; |
| 389 | } |
| 390 | |
| 391 | function setBox(id,accepted,whyAccept,whyReject){ |
| 392 | var box=document.getElementById('box_'+id); |
| 393 | box.className='vbox '+(accepted?'accept':'reject'); |
| 394 | document.getElementById('st_'+id).textContent = accepted ? '✗ Chain accepted' : '✓ Chain rejected'; |
| 395 | document.getElementById('why_'+id).textContent = accepted ? whyAccept : whyReject; |
| 396 | } |
| 397 | |
| 398 | function evaluate(){ |
| 399 | var vuln = accepts(false); // current library |
| 400 | setBox('vuln', vuln, |
| 401 | 'A pathLenConstraint=0 CA issued a sub-CA and it was trusted. keyUsage is absent, so the path-length check never ran.', |
| 402 | 'keyUsage happens to be present, so this build runs the path-length check and rejects the sub-CA.'); |
| 403 | setBox('patch', accepts(true), |
| 404 | '', |
| 405 | 'pathLenConstraint is enforced whenever cA and pathLen are set, regardless of keyUsage.'); |
| 406 | renderChain(vuln); // when the current library wrongly accepts, flag the certs it should not have trusted |
| 407 | document.getElementById('hint').innerHTML = keyUsage |
| 408 | ? 'Now switch <b>keyUsage</b> off: the current library will accept a chain it just rejected. The fix will not move.' |
| 409 | : '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.'; |
| 410 | } |
| 411 | |
| 412 | function toggle(){ |
| 413 | keyUsage = !keyUsage; |
| 414 | document.getElementById('tgKU').classList.toggle('on', keyUsage); |
| 415 | evaluate(); |
| 416 | } |
| 417 | |
| 418 | evaluate(); |
| 419 | </script> |
| 420 | </body> |
| 421 | </html> |