code wiki / (root) / nx_mtls_identity.nx

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}