-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
633 lines (592 loc) · 32.7 KB
/
Copy pathindex.html
File metadata and controls
633 lines (592 loc) · 32.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Certificate Forge — A Certificate Issuance Playground</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=Spline+Sans+Mono:wght@400;500;600&family=Spline+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{
--ink:#1a1614;
--paper:#f4efe6;
--paper-2:#ece4d6;
--card:#fbf8f1;
--line:#d8cdb8;
--seal:#9a2d1f;
--seal-2:#c44a36;
--gold:#a87d35;
--green:#2f6b4f;
--green-soft:#e3efe7;
--red-soft:#f5e2dd;
--mute:#6b6256;
--shadow:rgba(40,30,20,.13);
--mono:'Spline Sans Mono',monospace;
--sans:'Spline Sans',sans-serif;
--serif:'Fraunces',serif;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{background:var(--paper);color:var(--ink);font-family:var(--sans);-webkit-font-smoothing:antialiased}
body{
background-image:
radial-gradient(circle at 12% 8%, rgba(168,125,53,.06), transparent 40%),
radial-gradient(circle at 88% 92%, rgba(154,45,31,.05), transparent 45%),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .035 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
min-height:100vh;
}
.wrap{max-width:1080px;margin:0 auto;padding:0 22px 120px}
/* Header */
header{padding:44px 0 30px;text-align:center;border-bottom:1.5px solid var(--line);position:relative}
.kicker{font-family:var(--mono);font-size:11px;letter-spacing:.32em;text-transform:uppercase;color:var(--seal);margin-bottom:14px}
h1{font-family:var(--serif);font-weight:600;font-size:clamp(34px,6vw,58px);line-height:.98;letter-spacing:-.02em}
h1 em{font-style:italic;color:var(--seal)}
.sub{margin-top:14px;color:var(--mute);font-size:15px;max-width:560px;margin-left:auto;margin-right:auto;line-height:1.5}
.seal-mark{position:absolute;top:30px;right:0;width:74px;height:74px;opacity:.9}
/* Progress rail */
.rail{display:flex;gap:0;margin:30px 0 36px;flex-wrap:wrap;justify-content:center}
.rail-step{display:flex;align-items:center;gap:9px;padding:7px 16px;font-size:12.5px;font-weight:500;color:var(--mute);position:relative}
.rail-step .dot{width:24px;height:24px;border-radius:50%;border:1.5px solid var(--line);display:grid;place-items:center;font-family:var(--mono);font-size:11px;background:var(--card);transition:.35s}
.rail-step.done .dot{background:var(--green);border-color:var(--green);color:#fff}
.rail-step.active .dot{background:var(--seal);border-color:var(--seal);color:#fff;box-shadow:0 0 0 4px var(--red-soft)}
.rail-step.active{color:var(--ink);font-weight:600}
.rail-step:not(:last-child)::after{content:"";width:18px;height:1.5px;background:var(--line);margin-left:7px}
/* Card */
.card{background:var(--card);border:1.5px solid var(--line);border-radius:4px;box-shadow:0 10px 34px -16px var(--shadow);overflow:hidden;animation:rise .5s cubic-bezier(.2,.7,.3,1)}
@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
.card-head{padding:24px 30px 20px;border-bottom:1px solid var(--line);background:linear-gradient(var(--card),var(--paper-2))}
.step-no{font-family:var(--mono);font-size:11px;letter-spacing:.2em;color:var(--seal);text-transform:uppercase}
.card-head h2{font-family:var(--serif);font-size:27px;font-weight:600;margin-top:6px;letter-spacing:-.01em}
.card-body{padding:28px 30px 30px}
/* Guide note */
.guide{display:flex;gap:13px;background:var(--paper-2);border-left:3px solid var(--gold);border-radius:0 4px 4px 0;padding:14px 16px;margin-bottom:24px;font-size:13.5px;line-height:1.55;color:#5b5347}
.guide b{color:var(--ink)}
.guide .ic{flex-shrink:0;font-size:16px;line-height:1.3}
label{display:block;font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--mute);margin-bottom:7px}
.hint{font-weight:400;text-transform:none;letter-spacing:0;color:var(--mute);font-size:12px;margin-left:6px}
input,select{width:100%;font-family:var(--sans);font-size:15px;padding:11px 13px;background:#fff;border:1.5px solid var(--line);border-radius:4px;color:var(--ink);transition:.2s}
input:focus,select:focus{outline:none;border-color:var(--seal);box-shadow:0 0 0 3px var(--red-soft)}
.field{margin-bottom:18px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
@media(max-width:620px){.grid2{grid-template-columns:1fr}}
/* Buttons */
.btn{font-family:var(--sans);font-weight:600;font-size:14.5px;padding:12px 26px;border-radius:4px;border:1.5px solid var(--seal);background:var(--seal);color:#fff;cursor:pointer;transition:.2s;letter-spacing:.01em}
.btn:hover{background:var(--seal-2);border-color:var(--seal-2)}
.btn:disabled{opacity:.4;cursor:not-allowed}
.btn.ghost{background:transparent;color:var(--seal)}
.btn.ghost:hover{background:var(--red-soft)}
.btn.green{background:var(--green);border-color:var(--green)}
.btn.green:hover{filter:brightness(1.1)}
.btn.dark{background:var(--ink);border-color:var(--ink)}
.btn.dark:hover{filter:brightness(1.4)}
.actions{display:flex;gap:12px;align-items:center;margin-top:26px;flex-wrap:wrap}
.spacer{flex:1}
/* Code / mono blocks */
.mono-block{font-family:var(--mono);font-size:12px;line-height:1.5;background:#211c19;color:#d7c9a8;border-radius:5px;padding:16px 18px;overflow:auto;white-space:pre;border:1px solid #3a322b;position:relative;max-height:340px}
.mono-block .b64{color:#9bbf9b}
.copy-tag{position:absolute;top:9px;right:10px;font-family:var(--sans);font-size:11px;background:#3a322b;color:#d7c9a8;border:none;padding:4px 10px;border-radius:3px;cursor:pointer}
.copy-tag:hover{background:var(--seal)}
/* Key cards */
.keypair{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:8px}
@media(max-width:620px){.keypair{grid-template-columns:1fr}}
.kcard{border:1.5px solid var(--line);border-radius:5px;padding:16px;background:#fff}
.kcard h4{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;margin-bottom:4px}
.kcard.pub h4{color:var(--green)}
.kcard.priv h4{color:var(--seal)}
.kcard p{font-size:12px;color:var(--mute);line-height:1.5}
.kcard .lock{font-size:20px;margin-bottom:6px}
/* Review table (CA officer) */
.review{border:1.5px solid var(--line);border-radius:5px;overflow:hidden;margin-top:6px}
.review-row{display:grid;grid-template-columns:200px 1fr;border-bottom:1px solid var(--line)}
.review-row:last-child{border-bottom:none}
.review-row:nth-child(odd){background:var(--paper-2)}
.review-row .k{padding:12px 16px;font-family:var(--mono);font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--mute);border-right:1px solid var(--line)}
.review-row .v{padding:12px 16px;font-size:14.5px;font-weight:500;word-break:break-word}
.review-row .v .empty{color:#b9ac96;font-style:italic;font-weight:400}
@media(max-width:620px){.review-row{grid-template-columns:1fr}.review-row .k{border-right:none;border-bottom:1px solid var(--line)}}
.officer-banner{display:flex;align-items:center;gap:12px;background:#211c19;color:#e8dcc2;padding:14px 18px;border-radius:5px;margin-bottom:22px}
.officer-banner .badge{font-family:var(--mono);font-size:10px;letter-spacing:.18em;background:var(--gold);color:#211c19;padding:3px 9px;border-radius:3px;font-weight:700}
.officer-banner span{font-size:13.5px;line-height:1.4}
.verdict{display:flex;gap:12px;margin-top:26px}
/* Certificate render */
.cert{position:relative;background:
linear-gradient(135deg,#fdfbf4,#f3ecdc);
border:2px solid var(--gold);border-radius:6px;padding:34px 38px;box-shadow:0 16px 44px -20px var(--shadow);overflow:hidden}
.cert::before{content:"";position:absolute;inset:9px;border:1px solid rgba(168,125,53,.45);border-radius:3px;pointer-events:none}
.cert-watermark{position:absolute;right:-20px;bottom:-30px;font-family:var(--serif);font-size:200px;color:rgba(168,125,53,.06);font-weight:700;pointer-events:none;line-height:1}
.cert-top{display:flex;justify-content:space-between;align-items:flex-start;position:relative}
.cert-top .issuer{font-family:var(--serif);font-size:13px;color:var(--gold);letter-spacing:.08em}
.cert h3{font-family:var(--serif);font-size:30px;font-weight:600;margin:18px 0 4px;position:relative}
.cert .tagline{font-size:12px;color:var(--mute);margin-bottom:24px;font-family:var(--mono);letter-spacing:.05em}
.cert-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 28px;position:relative}
.cert-field label{color:var(--gold);font-size:10px}
.cert-field .val{font-family:var(--mono);font-size:13px;color:var(--ink);word-break:break-all}
.cert-seal{width:84px;height:84px}
.cert-foot{margin-top:26px;padding-top:18px;border-top:1px solid rgba(168,125,53,.4);display:flex;justify-content:space-between;font-family:var(--mono);font-size:10.5px;color:var(--mute);position:relative}
.toast{position:fixed;bottom:26px;left:50%;transform:translateX(-50%) translateY(80px);background:var(--ink);color:#fff;padding:13px 22px;border-radius:5px;font-size:13.5px;box-shadow:0 8px 24px var(--shadow);transition:.4s;z-index:50;opacity:0}
.toast.show{transform:translateX(-50%);opacity:1}
.spin{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.4);border-top-color:#fff;border-radius:50%;animation:sp .7s linear infinite;vertical-align:-2px;margin-right:7px}
@keyframes sp{to{transform:rotate(360deg)}}
.fade{animation:rise .45s cubic-bezier(.2,.7,.3,1)}
.reject-box{border:1.5px solid var(--seal);background:var(--red-soft);border-radius:5px;padding:22px 24px;text-align:center}
.reject-box h3{font-family:var(--serif);font-size:22px;color:var(--seal);margin-bottom:8px}
.reject-box p{font-size:14px;color:#7a4338;line-height:1.5}
.chip{display:inline-block;font-family:var(--mono);font-size:11px;background:var(--green-soft);color:var(--green);padding:3px 9px;border-radius:20px;margin-left:8px;vertical-align:1px}
</style>
</head>
<body>
<div class="wrap">
<header>
<svg class="seal-mark" viewBox="0 0 100 100" fill="none">
<circle cx="50" cy="50" r="46" stroke="#a87d35" stroke-width="1.5"/>
<circle cx="50" cy="50" r="38" stroke="#a87d35" stroke-width=".8"/>
<path d="M50 22 L56 40 L75 40 L60 52 L66 70 L50 58 L34 70 L40 52 L25 40 L44 40 Z" fill="#9a2d1f" opacity=".85"/>
</svg>
<div class="kicker">Public Key Infrastructure · Interactive</div>
<h1>Certificate <em>Forge</em></h1>
<p class="sub">A hands-on playground for how a digital certificate is born — from key generation, to signing request, to a real Certificate Authority's stamp of approval. Every key and signature below is genuinely generated in your browser.</p>
</header>
<div class="rail" id="rail"></div>
<div id="stage"></div>
</div>
<div class="toast" id="toast"></div>
<script>
/* ============================================================
STATE
============================================================ */
const STEPS = [
{n:1, t:"Generate Keys"},
{n:2, t:"Identify Yourself"},
{n:3, t:"Build the CSR"},
{n:4, t:"Submit to CA"},
{n:5, t:"CA Review"},
{n:6, t:"Certificate"},
];
const S = {
step: 1,
keyPair: null,
pubPem: "",
subject: {CN:"", O:"", OU:"", L:"", ST:"", C:""},
csrPem: "",
certPem: "",
certInfo: null,
decision: null, // 'approved' | 'rejected'
rejectReason: "",
};
const ALG = { name:"RSASSA-PKCS1-v1_5", hash:"SHA-256", modulusLength:2048, publicExponent:new Uint8Array([1,0,1]) };
const webcrypto = self.crypto.subtle;
const rng = self.crypto;
/* ============================================================
DER / ASN.1 ENCODING (no external libraries)
Verified against OpenSSL: CSR self-signature + X.509 cert.
============================================================ */
function _len(n){
if(n<0x80) return [n];
const b=[]; let x=n;
while(x>0){ b.unshift(x&0xff); x>>=8; }
return [0x80|b.length, ...b];
}
function _tlv(tag,c){ return [tag,..._len(c.length),...c]; }
const _SEQ=(c)=>_tlv(0x30,c);
const _SET=(c)=>_tlv(0x31,c);
function _INT(bytes){ let b=[...bytes]; if(!b.length) b=[0]; if(b[0]&0x80) b=[0,...b]; return _tlv(0x02,b); }
const _INTn=(n)=>_INT([n]);
function _OID(str){
const p=str.split(".").map(Number);
const body=[40*p[0]+p[1]];
for(let i=2;i<p.length;i++){ let v=p[i]; const st=[v&0x7f]; v>>=7; while(v>0){ st.unshift((v&0x7f)|0x80); v>>=7; } body.push(...st); }
return _tlv(0x06,body);
}
const _NULL=()=>[0x05,0x00];
const _str=(tag,s)=>_tlv(tag,[...new TextEncoder().encode(s)]);
const _UTF8=(s)=>_str(0x0c,s);
const _PRINT=(s)=>_str(0x13,s);
const _BIT=(b)=>_tlv(0x03,[0x00,...b]);
const _CTX=(num,c)=>_tlv(0xa0|0x20|num,c);
function _UTCTime(d){
const p=(n)=>String(n).padStart(2,"0");
const s=p(d.getUTCFullYear()%100)+p(d.getUTCMonth()+1)+p(d.getUTCDate())+p(d.getUTCHours())+p(d.getUTCMinutes())+p(d.getUTCSeconds())+"Z";
return _str(0x17,s);
}
const _OIDS={CN:"2.5.4.3",O:"2.5.4.10",OU:"2.5.4.11",L:"2.5.4.7",ST:"2.5.4.8",C:"2.5.4.6"};
function _rdn(subj){
const rdns=[];
const push=(oid,enc)=>rdns.push(_SET(_SEQ([..._OID(oid),...enc])));
if(subj.C) push(_OIDS.C,_PRINT(subj.C));
if(subj.ST) push(_OIDS.ST,_UTF8(subj.ST));
if(subj.L) push(_OIDS.L,_UTF8(subj.L));
if(subj.O) push(_OIDS.O,_UTF8(subj.O));
if(subj.OU) push(_OIDS.OU,_UTF8(subj.OU));
if(subj.CN) push(_OIDS.CN,_UTF8(subj.CN));
return _SEQ(rdns.flat());
}
const _sha256RSA=()=>_SEQ([..._OID("1.2.840.113549.1.1.11"),..._NULL()]);
async function _spki(pub){ return [...new Uint8Array(await webcrypto.exportKey("spki",pub))]; }
async function makeCSR(keyPair,subj){
const cri=_SEQ([ ..._INTn(0), ..._rdn(subj), ...(await _spki(keyPair.publicKey)), ..._CTX(0,[]) ]);
const sig=await webcrypto.sign({name:"RSASSA-PKCS1-v1_5"},keyPair.privateKey,new Uint8Array(cri));
return new Uint8Array(_SEQ([...cri,..._sha256RSA(),..._BIT([...new Uint8Array(sig)])]));
}
async function makeCert(caKeyPair,issuer,subj,subjectPub,serial){
const spki=await _spki(subjectPub);
const now=new Date(); const end=new Date(); end.setUTCFullYear(end.getUTCFullYear()+1);
const tbs=_SEQ([
..._CTX(0,_INTn(2)),
..._INT([...serial]),
..._sha256RSA(),
..._rdn(issuer),
..._SEQ([..._UTCTime(now),..._UTCTime(end)]),
..._rdn(subj),
...spki
]);
const sig=await webcrypto.sign({name:"RSASSA-PKCS1-v1_5"},caKeyPair.privateKey,new Uint8Array(tbs));
const der=new Uint8Array(_SEQ([...tbs,..._sha256RSA(),..._BIT([...new Uint8Array(sig)])]));
return {der, notBefore:now, notAfter:end, serial};
}
/* ============================================================
HELPERS
============================================================ */
const $ = (id)=>document.getElementById(id);
const stage = ()=>$("stage");
function toast(msg){
const t=$("toast"); t.textContent=msg; t.classList.add("show");
clearTimeout(t._tm); t._tm=setTimeout(()=>t.classList.remove("show"),2200);
}
function ab2b64(buf){
let bin=""; const bytes=new Uint8Array(buf);
for(let i=0;i<bytes.length;i++) bin+=String.fromCharCode(bytes[i]);
return btoa(bin);
}
function pemWrap(b64, label){
const lines = b64.match(/.{1,64}/g).join("\n");
return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----`;
}
function download(filename, text){
const blob=new Blob([text],{type:"application/x-pem-file"});
const a=document.createElement("a");
a.href=URL.createObjectURL(blob); a.download=filename; a.click();
URL.revokeObjectURL(a.href);
}
function esc(s){return (s||"").replace(/[<>&]/g,c=>({"<":"<",">":">","&":"&"}[c]));}
function renderRail(){
$("rail").innerHTML = STEPS.map(s=>{
const cls = s.n < S.step ? "done" : s.n===S.step ? "active" : "";
const mark = s.n < S.step ? "✓" : s.n;
return `<div class="rail-step ${cls}"><span class="dot">${mark}</span>${s.t}</div>`;
}).join("");
}
function go(step){ S.step=step; renderRail(); render(); window.scrollTo({top:0,behavior:"smooth"}); }
/* ============================================================
STEP RENDERERS
============================================================ */
function render(){
const fns={1:stepKeys,2:stepIdentity,3:stepCSR,4:stepSubmit,5:stepReview,6:stepCert};
fns[S.step]();
}
/* ---- STEP 1: KEY GENERATION ---- */
function stepKeys(){
const have = !!S.keyPair;
stage().innerHTML = `
<div class="card">
<div class="card-head"><div class="step-no">Step 1 of 6</div><h2>Generate your key pair</h2></div>
<div class="card-body">
<div class="guide"><span class="ic">🔑</span><div>
Everything starts with <b>two mathematically linked keys</b>. The <b>private key</b> stays secret with you — it proves your identity. The <b>public key</b> can be shared freely. Anything encrypted to one can only be undone by the other. We'll generate a real <b>RSA 2048-bit</b> pair right here, in your browser.
</div></div>
${have ? keyView() : `
<p style="font-size:14.5px;color:var(--mute);line-height:1.6;margin-bottom:8px">Click below to create your pair. The private key never leaves this page.</p>
<div class="actions">
<button class="btn" id="genBtn">Generate RSA 2048 Key Pair</button>
<span class="spacer"></span>
</div>`}
${have ? `<div class="actions"><span class="spacer"></span><button class="btn" id="next1">Continue → Identify Yourself</button></div>` : ""}
</div>
</div>`;
if(!have){
$("genBtn").onclick = async (e)=>{
const b=e.target; b.disabled=true; b.innerHTML=`<span class="spin"></span>Generating…`;
try{
S.keyPair = await webcrypto.generateKey(ALG, true, ["sign","verify"]);
const spki = await webcrypto.exportKey("spki", S.keyPair.publicKey);
S.pubPem = pemWrap(ab2b64(spki), "PUBLIC KEY");
toast("Key pair generated ✓");
stepKeys();
}catch(err){ b.disabled=false; b.textContent="Generate RSA 2048 Key Pair"; toast("Error: "+err.message); }
};
} else {
$("next1").onclick=()=>go(2);
}
}
function keyView(){
return `
<div class="keypair">
<div class="kcard pub">
<div class="lock">🌍</div><h4>Public Key <span class="chip">shareable</span></h4>
<p>Goes inside your request and, later, your certificate. Anyone can hold it.</p>
</div>
<div class="kcard priv">
<div class="lock">🔒</div><h4>Private Key <span style="color:var(--seal);font-size:10px">never shared</span></h4>
<p>Held only in this page's memory. In real life you'd guard this like a password.</p>
</div>
</div>
<div style="margin-top:18px">
<label>Your public key <span class="hint">(PEM format — safe to show)</span></label>
<div class="mono-block"><button class="copy-tag" onclick="navigator.clipboard.writeText(document.getElementById('pubpem').textContent);toast('Copied')">Copy</button><span id="pubpem">${esc(S.pubPem)}</span></div>
</div>`;
}
/* ---- STEP 2: IDENTITY / SUBJECT ---- */
function stepIdentity(){
const f = S.subject;
stage().innerHTML=`
<div class="card">
<div class="card-head"><div class="step-no">Step 2 of 6</div><h2>Who is this certificate for?</h2></div>
<div class="card-body">
<div class="guide"><span class="ic">🪪</span><div>
A certificate vouches for <b>an identity</b>. These fields make up the <b>Subject</b> — the "name on the badge." For a website that's a domain name; for a person, their name. Only <b>Common Name</b> is required; the rest add context. Fill in what fits your scenario.
</div></div>
<div class="field">
<label>Common Name (CN) <span class="hint">required — the main identity, e.g. a domain or full name</span></label>
<input id="CN" value="${esc(f.CN)}" placeholder="example.go.ke or Jane Wanjiru">
</div>
<div class="grid2">
<div class="field"><label>Organization (O)<span class="hint">company / body</span></label><input id="O" value="${esc(f.O)}" placeholder="Ministry of Lands"></div>
<div class="field"><label>Organizational Unit (OU)<span class="hint">team / dept</span></label><input id="OU" value="${esc(f.OU)}" placeholder="ICT Directorate"></div>
</div>
<div class="grid2">
<div class="field"><label>City / Locality (L)</label><input id="L" value="${esc(f.L)}" placeholder="Nairobi"></div>
<div class="field"><label>State / County (ST)</label><input id="ST" value="${esc(f.ST)}" placeholder="Nairobi County"></div>
</div>
<div class="grid2">
<div class="field"><label>Country (C)<span class="hint">2-letter ISO code, e.g. KE</span></label><input id="C" value="${esc(f.C)}" maxlength="2" placeholder="KE"></div>
<div class="field"></div>
</div>
<div class="actions">
<button class="btn ghost" id="back2">← Back</button>
<span class="spacer"></span>
<button class="btn" id="next2">Continue → Build CSR</button>
</div>
</div>
</div>`;
$("back2").onclick=()=>go(1);
$("next2").onclick=()=>{
const get=id=>$(id).value.trim();
S.subject={CN:get("CN"),O:get("O"),OU:get("OU"),L:get("L"),ST:get("ST"),C:get("C").toUpperCase()};
if(!S.subject.CN){ toast("Common Name is required"); $("CN").focus(); return; }
if(S.subject.C && S.subject.C.length!==2){ toast("Country must be a 2-letter code (e.g. KE)"); return; }
go(3);
};
}
/* ---- STEP 3: BUILD CSR ---- */
function stepCSR(){
const built = !!S.csrPem;
stage().innerHTML=`
<div class="card">
<div class="card-head"><div class="step-no">Step 3 of 6</div><h2>Build your signing request (CSR)</h2></div>
<div class="card-body">
<div class="guide"><span class="ic">📝</span><div>
A <b>CSR (Certificate Signing Request)</b> bundles your <b>public key</b> + your <b>identity</b> together, then signs the bundle with your <b>private key</b>. That self-signature proves you actually hold the private key for the public key inside. You send the CSR to a CA — never your private key.
</div></div>
${built ? csrView() : `
<div class="review" style="margin-bottom:22px">
${reviewRows(S.subject)}
</div>
<div class="actions">
<button class="btn ghost" id="back3">← Back</button>
<span class="spacer"></span>
<button class="btn" id="buildBtn">Sign & Build CSR</button>
</div>`}
${built ? `<div class="actions"><button class="btn ghost" id="back3b">← Edit identity</button><span class="spacer"></span><button class="btn" id="next3">Continue → Submit to CA</button></div>`:""}
</div>
</div>`;
if(!built){
$("back3").onclick=()=>go(2);
$("buildBtn").onclick=async(e)=>{
const b=e.target;b.disabled=true;b.innerHTML=`<span class="spin"></span>Signing…`;
try{ await buildCSR(); toast("CSR signed & built ✓"); stepCSR(); }
catch(err){ b.disabled=false;b.textContent="Sign & Build CSR"; toast("Error: "+err.message); console.error(err);}
};
} else {
$("back3b").onclick=()=>{ S.csrPem=""; go(2); };
$("next3").onclick=()=>go(4);
}
}
function csrView(){
return `
<div class="review" style="margin-bottom:18px">${reviewRows(S.subject)}</div>
<label>Your CSR <span class="hint">— this is what you send to the CA</span></label>
<div class="mono-block"><button class="copy-tag" onclick="navigator.clipboard.writeText(document.getElementById('csrpem').textContent);toast('Copied')">Copy</button><span id="csrpem">${esc(S.csrPem)}</span></div>
<div class="actions" style="margin-top:14px">
<button class="btn dark" onclick="window._dl('csr')">⤓ Download .csr</button>
<button class="btn dark" onclick="window._dl('key')">⤓ Download private key</button>
</div>`;
}
function reviewRows(s){
const row=(k,v)=>`<div class="review-row"><div class="k">${k}</div><div class="v">${v?esc(v):'<span class="empty">— not provided —</span>'}</div></div>`;
return row("Common Name",s.CN)+row("Organization",s.O)+row("Org. Unit",s.OU)+
row("Locality",s.L)+row("State / County",s.ST)+row("Country",s.C);
}
async function buildCSR(){
const der = await makeCSR(S.keyPair, S.subject);
S.csrPem = pemWrap(ab2b64(der.buffer), "CERTIFICATE REQUEST");
}
/* ---- STEP 4: SUBMIT ---- */
function stepSubmit(){
stage().innerHTML=`
<div class="card">
<div class="card-head"><div class="step-no">Step 4 of 6</div><h2>Submit the request to the CA</h2></div>
<div class="card-body">
<div class="guide"><span class="ic">📮</span><div>
You now hand your CSR to a <b>Certificate Authority (CA)</b> — a trusted body that vouches for identities. The CA reads your public key and identity, but <b>cannot see your private key</b> (you kept it). Below is exactly what travels over the wire.
</div></div>
<label>Transmitting to <b style="color:var(--seal)">Forge Root CA</b></label>
<div class="mono-block" style="max-height:200px">${esc(S.csrPem)}</div>
<div class="actions">
<button class="btn ghost" id="back4">← Back</button>
<span class="spacer"></span>
<button class="btn" id="submitBtn">📤 Submit to Forge Root CA</button>
</div>
</div>
</div>`;
$("back4").onclick=()=>go(3);
$("submitBtn").onclick=(e)=>{
const b=e.target;b.disabled=true;b.innerHTML=`<span class="spin"></span>Sending…`;
setTimeout(()=>{toast("CSR received by CA ✓");go(5);},900);
};
}
/* ---- STEP 5: CA REVIEW (user is the officer) ---- */
function stepReview(){
stage().innerHTML=`
<div class="card">
<div class="card-head"><div class="step-no">Step 5 of 6</div><h2>Review the request</h2></div>
<div class="card-body">
<div class="officer-banner">
<span class="badge">ROLE SWITCH</span>
<span>You are now the <b>CA Registration Officer</b>. Your job: confirm the requester really is who they claim before the CA's trust is lent to them. In the real world you'd verify documents, domain ownership, or ID here.</span>
</div>
<div class="guide"><span class="ic">🔍</span><div>
The CSR's <b>self-signature has already been checked</b> ✓ — we've confirmed the requester holds the matching private key. What software <b>can't</b> judge is whether the <b>identity</b> below is legitimate. That's the human decision you're about to make.
</div></div>
<label>Request under review</label>
<div class="review">${reviewRows(S.subject)}
<div class="review-row"><div class="k">Key Type</div><div class="v">RSA 2048-bit <span class="chip">signature valid ✓</span></div></div>
</div>
<div class="verdict">
<button class="btn green" id="approveBtn" style="flex:1">✓ Approve & Issue Certificate</button>
<button class="btn ghost" id="rejectBtn" style="flex:1;border-color:var(--seal)">✕ Reject Request</button>
</div>
</div>
</div>`;
$("approveBtn").onclick=async(e)=>{
const b=e.target;b.disabled=true;$("rejectBtn").disabled=true;b.innerHTML=`<span class="spin"></span>Issuing…`;
try{ await issueCert(); S.decision="approved"; toast("Certificate issued ✓"); go(6);}
catch(err){ b.disabled=false;$("rejectBtn").disabled=false;b.textContent="✓ Approve & Issue Certificate"; toast("Error: "+err.message); console.error(err);}
};
$("rejectBtn").onclick=()=>{
const r=prompt("Reason for rejection (the requester will see this):","Identity could not be verified");
if(r===null) return;
S.decision="rejected"; S.rejectReason=r||"No reason given"; go(6);
};
}
/* ---- CA signs the cert ---- */
let CA_KEYS=null;
async function getCaKeys(){
if(CA_KEYS) return CA_KEYS;
CA_KEYS = await webcrypto.generateKey(ALG, true, ["sign","verify"]);
return CA_KEYS;
}
async function issueCert(){
const caKeys=await getCaKeys();
const issuer={CN:"Forge Root CA", O:"Certificate Forge Authority", C:"KE"};
const serial=new Uint8Array(16); rng.getRandomValues(serial); serial[0]&=0x7f;
const out=await makeCert(caKeys, issuer, S.subject, S.keyPair.publicKey, serial);
S.certInfo={serial:[...serial], notBefore:out.notBefore, notAfter:out.notAfter};
S.certPem=pemWrap(ab2b64(out.der.buffer),"CERTIFICATE");
}
/* ---- STEP 6: RESULT ---- */
function stepCert(){
if(S.decision==="rejected"){
stage().innerHTML=`
<div class="card"><div class="card-head"><div class="step-no">Step 6 of 6</div><h2>Request rejected</h2></div>
<div class="card-body">
<div class="reject-box">
<h3>✕ No certificate issued</h3>
<p>The CA officer declined this request.<br><b>Reason:</b> ${esc(S.rejectReason)}</p>
</div>
<div class="guide" style="margin-top:22px"><span class="ic">💡</span><div>
This is the CA's gatekeeping power in action. A certificate is only as trustworthy as the verification behind it — so a CA can and must refuse anything it can't vouch for. Adjust the identity and try again.
</div></div>
<div class="actions"><button class="btn ghost" id="redo">← Back to review</button><span class="spacer"></span><button class="btn" id="restart">Start over</button></div>
</div></div>`;
$("redo").onclick=()=>{S.decision=null;go(5);};
$("restart").onclick=resetAll;
return;
}
const s=S.subject;
const serialHex = bufHex(S.certInfo.serial).match(/.{1,2}/g).join(":").toUpperCase();
const nb=S.certInfo.notBefore, na=S.certInfo.notAfter;
const fmt=d=>d.toLocaleDateString(undefined,{year:"numeric",month:"short",day:"numeric"});
stage().innerHTML=`
<div class="card"><div class="card-head"><div class="step-no">Step 6 of 6</div><h2>Your certificate is issued 🎉</h2></div>
<div class="card-body">
<div class="guide"><span class="ic">📜</span><div>
The CA wrapped your public key + identity into a certificate and <b>signed it with the CA's own private key</b>. Now anyone who trusts the CA will trust this certificate. The signature is real — generated moments ago in your browser.
</div></div>
<div class="cert fade" id="certCanvas">
<div class="cert-watermark">✦</div>
<div class="cert-top">
<div class="issuer">FORGE ROOT CA · CERTIFICATE FORGE AUTHORITY · KE</div>
<svg class="cert-seal" viewBox="0 0 100 100" fill="none">
<circle cx="50" cy="50" r="46" stroke="#a87d35" stroke-width="1.5"/>
<circle cx="50" cy="50" r="37" stroke="#a87d35" stroke-width=".7"/>
<path d="M50 24 L55.5 40 L72 40 L59 50.5 L64 67 L50 56.5 L36 67 L41 50.5 L28 40 L44.5 40 Z" fill="#9a2d1f" opacity=".8"/>
<text x="50" y="92" text-anchor="middle" font-family="Spline Sans Mono" font-size="7" fill="#a87d35">SIGNED</text>
</svg>
</div>
<h3>${esc(s.CN)}</h3>
<div class="tagline">X.509 v3 · SHA-256 with RSA · this identity is hereby certified</div>
<div class="cert-grid">
<div class="cert-field"><label>Subject (Organization)</label><div class="val">${esc(s.O||"—")}</div></div>
<div class="cert-field"><label>Country</label><div class="val">${esc(s.C||"—")}</div></div>
<div class="cert-field"><label>Valid From</label><div class="val">${fmt(nb)}</div></div>
<div class="cert-field"><label>Valid Until</label><div class="val">${fmt(na)}</div></div>
<div class="cert-field" style="grid-column:1/-1"><label>Serial Number</label><div class="val">${serialHex}</div></div>
</div>
<div class="cert-foot">
<span>ISSUED ${fmt(nb)}</span>
<span>SIGNED BY FORGE ROOT CA</span>
</div>
</div>
<div style="margin-top:24px">
<label>Certificate in PEM format <span class="hint">— deployable to a server, wallet, or trust store</span></label>
<div class="mono-block"><button class="copy-tag" onclick="navigator.clipboard.writeText(document.getElementById('certpem').textContent);toast('Copied')">Copy</button><span id="certpem" class="b64">${esc(S.certPem)}</span></div>
</div>
<div class="actions">
<button class="btn dark" onclick="window._dl('cert')">⤓ Download .crt</button>
<button class="btn dark" onclick="window._dl('key')">⤓ Download private key</button>
<span class="spacer"></span>
<button class="btn green" id="restart2">↻ Run another identity</button>
</div>
</div></div>`;
$("restart2").onclick=resetAll;
}
function bufHex(view){ return Array.from(view).map(b=>b.toString(16).padStart(2,"0")).join(""); }
/* downloads */
window._dl=async(what)=>{
if(what==="csr") download((S.subject.CN||"request")+".csr", S.csrPem);
else if(what==="cert") download((S.subject.CN||"certificate")+".crt", S.certPem);
else if(what==="key"){
const pkcs8=await webcrypto.exportKey("pkcs8",S.keyPair.privateKey);
download((S.subject.CN||"private")+".key", pemWrap(ab2b64(pkcs8),"PRIVATE KEY"));
}
toast("Downloaded ✓");
};
function resetAll(){
S.step=1;S.keyPair=null;S.pubPem="";S.subject={CN:"",O:"",OU:"",L:"",ST:"",C:""};
S.csrPem="";S.certPem="";S.certInfo=null;S.decision=null;S.rejectReason="";
go(1);
}
/* boot */
renderRail(); render();
</script>
</body>
</html>