nx_tls13_server_clientauth.nx source
↩ module page · 174 lines · 9167 B
1// nx_tls13_server_clientauth.nx -- TLS 1.3 server-side CLIENT authentication: verify a presented
2// client certificate's CertificateVerify over the handshake transcript (RFC 8446 §4.4.3).
3//
4// mTLS rung R3 of the NO-COOKIE session carrier. This is the cryptographic heart: it proves that whoever
5// opened the TLS connection HOLDS THE PRIVATE KEY of the presented client identity cert -- so the browser
6// authenticates itself at the TLS layer on EVERY connection (top-level navs, refresh, new tab) with zero
7// cookie, zero header, zero auth-carrier JS. R7 wires this into the live terminator's record loop (send
8// CertificateRequest after EncryptedExtensions, then receive+decrypt the client's Certificate +
9// CertificateVerify before its Finished). R6 turns the verified identity pubkey/Subject into HR entitlements.
10//
11// The signed content mirrors the server's build_cv_context (nx_tls13_server_session_emit_cv) but with the
12// CLIENT context label (RFC 8446 §4.4.3): 64*0x20 || "TLS 1.3, client CertificateVerify" || 0x00 || hash.
13// Ed25519 signs/verifies the 130-byte content directly. Composes nx_x509 (parse) + nx_ed25519_signature.
14// license_tier: ORIGINAL expect_exit: 0
15import "nx_syscalls.nx"
16import "nx_x509.nx"
17import "nx_ed25519_signature.nx"
18import "nx_x509_leaf_emit.nx" // gate only: mint R4 client identity certs to verify against
19const NX_MAGIC_4096: i64 = 4096
20
21const NX_CA_OK: i64 = 0
22const NX_CA_BAD_CERT: i64 = 1
23const NX_CA_BAD_SIG: i64 = 2
24const NX_CA_UNSUPPORTED_SCHEME: i64 = 3
25const NX_CA_CV_CONTEXT_LEN: i64 = 130
26const NX_CA_SS_ED25519: i64 = 0x0807 // SignatureScheme ed25519
27
28// Build the 130-byte CLIENT CertificateVerify signed content (RFC 8446 §4.4.3). Returns 130.
29func tls13_build_client_cv_content(transcript_hash_32: *u8, out_130: *u8) -> i64 {
30 var i: i64 = 0
31 while i < 64 { out_130[i] = 0x20 as u8; i = i + 1 } // 64 spaces
32 let lbl: *u8 = "TLS 1.3, client CertificateVerify" // 33 bytes
33 var j: i64 = 0
34 while j < 33 { out_130[64 + j] = lbl[j]; j = j + 1 }
35 out_130[97] = 0 as u8 // separator
36 var k: i64 = 0
37 while k < 32 { out_130[98 + k] = transcript_hash_32[k]; k = k + 1 }
38 return 130
39}
40
41// Verify a presented client cert's CertificateVerify (Ed25519, scheme 0x0807) over transcript_hash_32.
42// Proves possession of the cert's private key. On NX_CA_OK, copies the 32-byte identity pubkey to
43// out_pubkey_32 (the stable handle the authz rung resolves). This is AUTHENTICATION, not enrollment:
44// whether this identity is registered + entitled is the R6 authz decision.
45func tls13_verify_client_auth_ed25519(
46 client_cert_der: *u8, cert_len: i64,
47 scheme: i64,
48 cv_sig: *u8, cv_sig_len: i64,
49 transcript_hash_32: *u8,
50 out_pubkey_32: *u8
51) -> i64 {
52 if scheme != NX_CA_SS_ED25519 { return NX_CA_UNSUPPORTED_SCHEME }
53 if cv_sig_len != 64 { return NX_CA_BAD_SIG }
54 if (client_cert_der as i64) == 0 { return NX_CA_BAD_CERT }
55
56 let xc: *X509Cert = sys_mmap(256) as *X509Cert
57 if x509_parse(client_cert_der, cert_len, xc) != 0 { return NX_CA_BAD_CERT }
58 if xc.pubkey_len != 32 { return NX_CA_BAD_CERT }
59 let pub: *u8 = sys_mmap(32)
60 var i: i64 = 0
61 while i < 32 { pub[i] = client_cert_der[xc.pubkey_off + i]; i = i + 1 }
62
63 let content: *u8 = sys_mmap(NX_CA_CV_CONTEXT_LEN)
64 tls13_build_client_cv_content(transcript_hash_32, content)
65 if ed25519_verify_full(pub, content, NX_CA_CV_CONTEXT_LEN, cv_sig) != NX_ED25519_SIG_OK {
66 return NX_CA_BAD_SIG
67 }
68 var j: i64 = 0
69 while j < 32 { out_pubkey_32[j] = pub[j]; j = j + 1 }
70 return NX_CA_OK
71}
72
73// ===== in-process gate: mint a client cert (R4) -> sign a real CertificateVerify -> verify + negatives =====
74func ca_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
75func ca_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
76func ca_row(name: *u8, ok: i64) -> i64 { if ok == 1 { ca_w(" PASS " as *u8) } else { ca_w(" FAIL " as *u8) } ca_w(name); ca_w("\n" as *u8); return ok }
77
78// helper: mint a self-signed identity cert for a fresh seed -> cert bytes + len; returns pub (32) too.
79func ca_mint(seed: *u8, pub: *u8, cn: *u8, cn_n: i64, cert: *u8, cap: i64, clen: *i64) -> i64 {
80 ed25519_pub_from_priv(seed, pub)
81 let serial: *u8 = sys_mmap(8)
82 serial[0] = 0x55 as u8; serial[1] = 0x21 as u8; serial[2] = 0x09 as u8; serial[3] = 0x0a as u8
83 serial[4] = 0x0b as u8; serial[5] = 0x0c as u8; serial[6] = 0x0d as u8; serial[7] = 0x0e as u8
84 return nx_x509_leaf_emit_ed25519(seed, pub, cn, cn_n, serial, 8,
85 "260101000000Z" as *u8, "360101000000Z" as *u8, cert, cap, clen)
86}
87
88func main() -> i64 {
89 ca_w("nx_tls13 client-auth verify gate (RFC 8446 4.4.3; mTLS rung R3 -- the no-cookie carrier proof)\n" as *u8)
90
91 // --- client identity keypair + minted cert (what the browser would present) ---
92 let seed: *u8 = sys_mmap(32)
93 var i: i64 = 0
94 while i < 32 { seed[i] = (0x31 + i) as u8; i = i + 1 }
95 let pub: *u8 = sys_mmap(32)
96 let cn: *u8 = "nishi-uid-aabbccddeeff00112233445566" as *u8
97 let cert: *u8 = sys_mmap(NX_MAGIC_4096)
98 let clen: *i64 = sys_mmap(8) as *i64
99 if ca_mint(seed, pub, cn, ca_len(cn), cert, NX_MAGIC_4096, clen) != 0 { ca_w("MINT FAIL\n" as *u8); sys_exit(1) }
100
101 // --- the handshake transcript hash the client signs over (any 32 bytes for the gate) ---
102 let th: *u8 = sys_mmap(32)
103 var t: i64 = 0
104 while t < 32 { th[t] = (0xA0 ^ t) as u8; t = t + 1 }
105
106 // --- the CLIENT builds the CV content + signs it with its identity key = the CertificateVerify sig ---
107 let content: *u8 = sys_mmap(130)
108 tls13_build_client_cv_content(th, content)
109 let cv_sig: *u8 = sys_mmap(64)
110 if ed25519_sign_full(seed, content, 130, cv_sig) != 0 { ca_w("SIGN FAIL\n" as *u8); sys_exit(1) }
111
112 var pass: i64 = 0
113 let idpub: *u8 = sys_mmap(32)
114
115 // T1: a genuine client-auth verifies AND yields the identity pubkey
116 var t1: i64 = 0
117 if tls13_verify_client_auth_ed25519(cert, clen[0], NX_CA_SS_ED25519, cv_sig, 64, th, idpub) == NX_CA_OK {
118 t1 = 1
119 var j: i64 = 0
120 while j < 32 { if idpub[j] != pub[j] { t1 = 0; j = 32 } else { j = j + 1 } }
121 }
122 pass = pass + ca_row("T1 genuine client-auth verifies + returns the identity pubkey" as *u8, t1)
123
124 // T2: a different transcript hash (e.g. a replayed sig on another handshake) is rejected
125 let th2: *u8 = sys_mmap(32)
126 var a: i64 = 0
127 while a < 32 { th2[a] = th[a]; a = a + 1 }
128 th2[0] = (th2[0] ^ 1) as u8
129 var t2: i64 = 0
130 if tls13_verify_client_auth_ed25519(cert, clen[0], NX_CA_SS_ED25519, cv_sig, 64, th2, idpub) != NX_CA_OK { t2 = 1 }
131 pass = pass + ca_row("T2 signature bound to THIS transcript (replay on another handshake rejected)" as *u8, t2)
132
133 // T3: a tampered signature is rejected
134 let badsig: *u8 = sys_mmap(64)
135 var b: i64 = 0
136 while b < 64 { badsig[b] = cv_sig[b]; b = b + 1 }
137 badsig[5] = (badsig[5] ^ 0x20) as u8
138 var t3: i64 = 0
139 if tls13_verify_client_auth_ed25519(cert, clen[0], NX_CA_SS_ED25519, badsig, 64, th, idpub) != NX_CA_OK { t3 = 1 }
140 pass = pass + ca_row("T3 tampered CertificateVerify signature rejected" as *u8, t3)
141
142 // T4: presenting ANOTHER identity's cert with this signature fails (can't steal an identity)
143 let seed2: *u8 = sys_mmap(32)
144 var c: i64 = 0
145 while c < 32 { seed2[c] = (0x77 + c) as u8; c = c + 1 }
146 let pub2: *u8 = sys_mmap(32)
147 let cert2: *u8 = sys_mmap(NX_MAGIC_4096)
148 let clen2: *i64 = sys_mmap(8) as *i64
149 ca_mint(seed2, pub2, "nishi-uid-deadbeefdeadbeefdeadbeef00" as *u8, 36, cert2, NX_MAGIC_4096, clen2)
150 var t4: i64 = 0
151 if tls13_verify_client_auth_ed25519(cert2, clen2[0], NX_CA_SS_ED25519, cv_sig, 64, th, idpub) != NX_CA_OK { t4 = 1 }
152 pass = pass + ca_row("T4 another identity's cert cannot ride this signature (no impersonation)" as *u8, t4)
153
154 // T5: unsupported scheme returns an honest verdict (not a false accept)
155 var t5: i64 = 0
156 if tls13_verify_client_auth_ed25519(cert, clen[0], 0x0403, cv_sig, 64, th, idpub) == NX_CA_UNSUPPORTED_SCHEME { t5 = 1 }
157 pass = pass + ca_row("T5 unsupported scheme (ECDSA-P256 0x0403) honest-rejected, no false accept" as *u8, t5)
158
159 // T6: the signed content is RFC-shaped with the CLIENT label (byte-distinct from the server context)
160 var t6: i64 = 1
161 if content[0] != (0x20 as u8) { t6 = 0 }
162 if content[64] != (84 as u8) { t6 = 0 } // 'T' of the label (byte 64+0)
163 if content[73] != (99 as u8) { t6 = 0 } // 'c' of "client" (label offset 9 -> byte 64+9)
164 if content[97] != (0 as u8) { t6 = 0 } // separator after the 33-byte label
165 pass = pass + ca_row("T6 signed content is RFC 8446 4.4.3 client-CertificateVerify shaped" as *u8, t6)
166
167 if pass == 6 {
168 ca_w("NX-TLS13-CLIENTAUTH GATE GREEN 6/6 (browser proves key-possession at the TLS layer; no cookie/header/JS)\n" as *u8)
169 sys_exit(0)
170 }
171 ca_w("NX-TLS13-CLIENTAUTH GATE RED\n" as *u8)
172 sys_exit(1)
173 return 1
174}