Zion Boggan
repos/Security Portfolio/featured-finding/index.html
zionboggan.com ↗
421 lines · html
History for this file →
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&nbsp;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&nbsp;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&lt;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 &amp; 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 &amp; 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>