nx_mtls_identity.nx source
↩ module page · 164 lines · 7816 B
1// nx_mtls_identity.nx -- OPAQUE-bound client identity: derive the mTLS keypair + cert from export_key.
2//
3// mTLS rung R5 of the NO-COOKIE session carrier -- the OPAQUE BINDING. After OPAQUE login, both client and
4// server hold the 32-byte export_key (RFC 9807) derived from the password proof. We HKDF-Expand it into a
5// deterministic Ed25519 identity keypair and mint a cert whose Subject CN = hex(user_id_hash). Consequences:
6// * the private key is NEVER stored server-side -- the client re-derives it from its own password proof
7// (sovereign, data-minimal: PRIV2 "no server-side correlation" preserved -- nothing new to store/leak);
8// * the identity is the SAME stable user_id_hash the OPAQUE/HR stack already keys on (SEC4 realm isolation);
9// * R6 maps the verified cert identity -> HR entitlements; the PKCS#12 packaging for browser import is R5b.
10//
11// LIVE-CAPTURE NOTE: nx_modern_auth_login / olg_login currently DISCARD the export_key. R5b/R7 surfaces it
12// from the OPAQUE flow (additive out-param) so the daemon can mint + hand back the identity on login.
13//
14// Composes nx_hkdf (export->key) + nx_x509_leaf_emit (R4 mint) + nx_ed25519_signature. license_tier: ORIGINAL expect_exit: 0
15import "nx_syscalls.nx"
16import "nx_hkdf.nx"
17import "nx_ed25519_signature.nx"
18import "nx_x509.nx"
19import "nx_x509_validate.nx"
20import "nx_x509_leaf_emit.nx"
21const NX_MAGIC_5869: i64 = 5869
22const NX_MAGIC_4096: i64 = 4096
23
24const NX_MID_OK: i64 = 0
25const NX_MID_ERR: i64 = 1
26
27// HKDF-Expand(export_key, "NishiClientIdentityKeyV1", 32) -> Ed25519 seed; derive the public key.
28// export_key is already a uniform 32-byte secret (an HKDF PRK), so Expand alone is correct + deterministic.
29func nx_mtls_derive_identity_keypair(export_key_32: *u8, out_seed_32: *u8, out_pub_32: *u8) -> i64 {
30 if (export_key_32 as i64) == 0 { return NX_MID_ERR }
31 let info: *u8 = "NishiClientIdentityKeyV1" as *u8 // domain separation (RFC NX_MAGIC_5869 ยง3.2)
32 if hkdf_expand(export_key_32, info, 24, 32, out_seed_32) != 0 { return NX_MID_ERR }
33 ed25519_pub_from_priv(out_seed_32, out_pub_32)
34 return NX_MID_OK
35}
36
37// hex-encode n bytes -> out[0..2n] (lowercase). Returns 2n.
38func nx_mid_hex(src: *u8, n: i64, out: *u8) -> i64 {
39 let hx: *u8 = "0123456789abcdef" as *u8
40 var i: i64 = 0
41 while i < n {
42 let c: i64 = src[i] as i64
43 out[i*2] = hx[(c >> 4) & 15]
44 out[i*2+1] = hx[c & 15]
45 i = i + 1
46 }
47 return n * 2
48}
49
50// Derive the keypair from export_key + mint the bound identity cert (Subject CN = hex(user_id_hash)).
51// Also returns the derived seed + pubkey (the caller never persists the seed server-side).
52func nx_mtls_mint_identity_cert(
53 export_key_32: *u8, uid_hash_32: *u8,
54 out_cert: *u8, cert_cap: i64, out_cert_n: *i64,
55 out_seed_32: *u8, out_pub_32: *u8
56) -> i64 {
57 if nx_mtls_derive_identity_keypair(export_key_32, out_seed_32, out_pub_32) != NX_MID_OK { return NX_MID_ERR }
58 let cn: *u8 = sys_mmap(72)
59 let cn_n: i64 = nx_mid_hex(uid_hash_32, 32, cn) // 64 hex chars
60 // serial = first 8 bytes of uid_hash, high bit cleared (positive DER INTEGER), stable per identity
61 let serial: *u8 = sys_mmap(8)
62 var i: i64 = 0
63 while i < 8 { serial[i] = uid_hash_32[i]; i = i + 1 }
64 serial[0] = (serial[0] & 0x7f) as u8
65 if (serial[0] as i64) == 0 { serial[0] = 0x01 as u8 }
66 if nx_x509_leaf_emit_ed25519(out_seed_32, out_pub_32, cn, cn_n, serial, 8,
67 "260101000000Z" as *u8, "360101000000Z" as *u8, out_cert, cert_cap, out_cert_n) != 0 { return NX_MID_ERR }
68 return NX_MID_OK
69}
70
71// ===== in-process gate =====
72func mid_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
73func mid_row(name: *u8, ok: i64) -> i64 { if ok == 1 { mid_w(" PASS " as *u8) } else { mid_w(" FAIL " as *u8) } mid_w(name); mid_w("\n" as *u8); return ok }
74func mid_eq32(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while i < 32 { if a[i] != b[i] { return 0 } i = i + 1 } return 1 }
75func mid_find(hay: *u8, hn: i64, needle: *u8, nn: i64) -> i64 {
76 var i: i64 = 0
77 while i + nn <= hn {
78 var k: i64 = 0; var ok: i64 = 1
79 while k < nn { if hay[i+k] != needle[k] { ok = 0; k = nn } else { k = k + 1 } }
80 if ok == 1 { return i }
81 i = i + 1
82 }
83 return 0 - 1
84}
85
86func main() -> i64 {
87 mid_w("nx_mtls identity gate (OPAQUE export_key -> deterministic Ed25519 identity + bound cert; rung R5)\n" as *u8)
88
89 // stand-in OPAQUE export_key + user_id_hash (R5b/R7 surfaces the real export_key from the OPAQUE flow)
90 let ek: *u8 = sys_mmap(32)
91 let uid: *u8 = sys_mmap(32)
92 var i: i64 = 0
93 while i < 32 { ek[i] = (0x5a + i) as u8; uid[i] = (0xC3 ^ i) as u8; i = i + 1 }
94
95 let seed1: *u8 = sys_mmap(32)
96 let pub1: *u8 = sys_mmap(32)
97 let cert: *u8 = sys_mmap(NX_MAGIC_4096)
98 let clen: *i64 = sys_mmap(8) as *i64
99 let rc: i64 = nx_mtls_mint_identity_cert(ek, uid, cert, NX_MAGIC_4096, clen, seed1, pub1)
100 var pass: i64 = 0
101
102 var t1: i64 = 0
103 if rc == NX_MID_OK { if clen[0] > 64 { t1 = 1 } }
104 pass = pass + mid_row("T1 export_key -> identity keypair + bound cert minted" as *u8, t1)
105
106 // determinism: re-derive from the SAME export_key -> identical keypair (no server-stored key needed)
107 let seed2: *u8 = sys_mmap(32)
108 let pub2: *u8 = sys_mmap(32)
109 nx_mtls_derive_identity_keypair(ek, seed2, pub2)
110 var t2: i64 = 0
111 if mid_eq32(seed1, seed2) == 1 { if mid_eq32(pub1, pub2) == 1 { t2 = 1 } }
112 pass = pass + mid_row("T2 deterministic: same export_key re-derives the SAME key (re-derivable, not stored)" as *u8, t2)
113
114 // independence: a different export_key -> a different identity key
115 let ek2: *u8 = sys_mmap(32)
116 var a: i64 = 0
117 while a < 32 { ek2[a] = ek[a]; a = a + 1 }
118 ek2[0] = (ek2[0] ^ 1) as u8
119 let seed3: *u8 = sys_mmap(32)
120 let pub3: *u8 = sys_mmap(32)
121 nx_mtls_derive_identity_keypair(ek2, seed3, pub3)
122 var t3: i64 = 0
123 if mid_eq32(pub1, pub3) == 0 { t3 = 1 }
124 pass = pass + mid_row("T3 independence: a different export_key yields a different identity key" as *u8, t3)
125
126 // the minted cert is valid on our own X.509 stack + embeds the derived identity pubkey
127 let xc: *X509Cert = sys_mmap(256) as *X509Cert
128 var t4: i64 = 0
129 if x509_parse(cert, clen[0], xc) == 0 {
130 if x509_verify_signature_ed25519(cert, xc, pub1) == NX_X509_VAL_OK {
131 if xc.pubkey_len == 32 {
132 t4 = 1
133 var j: i64 = 0
134 while j < 32 { if cert[xc.pubkey_off + j] != pub1[j] { t4 = 0; j = 32 } else { j = j + 1 } }
135 }
136 }
137 }
138 pass = pass + mid_row("T4 minted cert verifies on our X.509 stack + embeds the derived pubkey" as *u8, t4)
139
140 // the cert binds the OPAQUE identity: Subject CN == hex(user_id_hash)
141 let hexuid: *u8 = sys_mmap(72)
142 nx_mid_hex(uid, 32, hexuid)
143 var t5: i64 = 0
144 if mid_find(cert, clen[0], hexuid, 64) >= 0 { t5 = 1 }
145 pass = pass + mid_row("T5 cert binds the OPAQUE identity (Subject CN = hex(user_id_hash))" as *u8, t5)
146
147 // the derived key is usable for the CertificateVerify in R3 (signs + verifies)
148 let msg: *u8 = sys_mmap(16)
149 var m: i64 = 0
150 while m < 16 { msg[m] = (m + 1) as u8; m = m + 1 }
151 let sig: *u8 = sys_mmap(64)
152 ed25519_sign_full(seed1, msg, 16, sig)
153 var t6: i64 = 0
154 if ed25519_verify_full(pub1, msg, 16, sig) == NX_ED25519_SIG_OK { t6 = 1 }
155 pass = pass + mid_row("T6 derived key signs + verifies (usable for the TLS CertificateVerify)" as *u8, t6)
156
157 if pass == 6 {
158 mid_w("NX-MTLS-IDENTITY GATE GREEN 6/6 (sovereign OPAQUE-bound client identity; no server-stored private key)\n" as *u8)
159 sys_exit(0)
160 }
161 mid_w("NX-MTLS-IDENTITY GATE RED\n" as *u8)
162 sys_exit(1)
163 return 1
164}