code wiki / (root) / nx_tls13_server_clientauth.nx

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}