code wiki / (root) / nx_mtls_authz.nx

nx_mtls_authz.nx source

↩ module page · 165 lines · 8148 B

1// nx_mtls_authz.nx -- bind a VERIFIED client-cert identity to an access decision. 2// 3// mTLS rung R6 of the NO-COOKIE session carrier. After R3 proves key-possession, this rung answers 4// "who is this, and what may they do." Two pieces, both the security floor: 5// 1. nx_mtls_cert_identity: extract the Subject CN (= hex(user_id_hash), the SAME stable id the OPAQUE/HR 6// stack keys on) from the presented cert -- the cert->identity bridge. 7// 2. nx_mtls_authz_level: DENY-BY-DEFAULT (mirrors Consul Connect mc_intention). Access requires BOTH 8// (a) cryptographic verification by R3 AND (b) enrollment in the roster the live HR/uid index supplies. 9// A valid-but-unenrolled cert, or an unverified cert, gets level 0 (nothing). No fail-open path. 10// 11// The live daemon (R7) feeds the roster from olgd_idx_lookup (uidhex->handle) + the HR level, and emits the 12// per-user entitlement LINKS via the proven he_emit_super (nx_hr_entitle) -- this rung is the DECISION, that 13// is the DATA. Composes nx_x509 (parse). license_tier: ORIGINAL expect_exit: 0 14import "nx_syscalls.nx" 15import "nx_x509.nx" 16import "nx_ed25519_signature.nx" 17import "nx_x509_leaf_emit.nx" // gate only: mint identity certs to extract from 18const NX_MAGIC_4096: i64 = 4096 19 20const NX_AZ_DENY: i64 = 0 // deny-by-default access level 21 22// Extract the Subject CN (our identity certs carry exactly one CN UTF8String = hex(user_id_hash)). 23// Scans the Subject Name DER for the first UTF8String (tag 0x0c, short-form length) and copies its value. 24// Returns the identity length (0 = not found / malformed). 25func nx_mtls_cert_identity(cert_der: *u8, cert_len: i64, out: *u8, cap: i64) -> i64 { 26 if (cert_der as i64) == 0 { return 0 } 27 let xc: *X509Cert = sys_mmap(256) as *X509Cert 28 if x509_parse(cert_der, cert_len, xc) != 0 { return 0 } 29 var i: i64 = xc.subject_off 30 let end: i64 = xc.subject_off + xc.subject_len 31 while i + 2 <= end { 32 if (cert_der[i] & 0xff) == 0x0c { 33 let l: i64 = cert_der[i+1] & 0xff 34 if l < 128 { if l > 0 { if i + 2 + l <= end { 35 var k: i64 = 0 36 while k < l { if k < cap - 1 { out[k] = cert_der[i+2+k] } k = k + 1 } 37 out[l] = 0 as u8 38 return l 39 } } } 40 } 41 i = i + 1 42 } 43 return 0 44} 45 46// DENY-BY-DEFAULT authz decision. verified=1 ONLY when R3 proved key-possession this handshake. roster = 47// array of uidhex string pointers (as i64), roster_lens = parallel lengths, levels = parallel granted levels. 48// Returns the granted level, or NX_AZ_DENY on any failure (unverified OR unenrolled). 49func nx_mtls_authz_level( 50 verified: i64, 51 uidhex: *u8, uidhex_n: i64, 52 roster: *i64, roster_lens: *i64, levels: *i64, roster_n: i64 53) -> i64 { 54 if verified != 1 { return NX_AZ_DENY } 55 if uidhex_n <= 0 { return NX_AZ_DENY } 56 var i: i64 = 0 57 while i < roster_n { 58 if roster_lens[i] == uidhex_n { 59 let rp: *u8 = roster[i] as *u8 60 var k: i64 = 0; var hit: i64 = 1 61 while k < uidhex_n { if rp[k] != uidhex[k] { hit = 0; k = uidhex_n } else { k = k + 1 } } 62 if hit == 1 { return levels[i] } 63 } 64 i = i + 1 65 } 66 return NX_AZ_DENY 67} 68 69// ===== in-process gate: mint identity certs (R4) -> extract identity -> deny-by-default decision ===== 70func az_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 71func az_row(name: *u8, ok: i64) -> i64 { if ok == 1 { az_w(" PASS " as *u8) } else { az_w(" FAIL " as *u8) } az_w(name); az_w("\n" as *u8); return ok } 72func az_hex(src: *u8, n: i64, out: *u8) -> i64 { 73 let hx: *u8 = "0123456789abcdef" as *u8 74 var i: i64 = 0 75 while i < n { let c: i64 = src[i] as i64; out[i*2] = hx[(c >> 4) & 15]; out[i*2+1] = hx[c & 15]; i = i + 1 } 76 return n * 2 77} 78func az_streq(a: *u8, an: i64, b: *u8, bn: i64) -> i64 { 79 if an != bn { return 0 } 80 var i: i64 = 0 81 while i < an { if a[i] != b[i] { return 0 } i = i + 1 } 82 return 1 83} 84// mint a self-signed identity cert whose CN = hex(uid). Returns cert len. 85func az_mint(uid: *u8, cert: *u8, cap: i64) -> i64 { 86 let seed: *u8 = sys_mmap(32) 87 var i: i64 = 0 88 while i < 32 { seed[i] = (uid[i] ^ 0x5c) as u8; i = i + 1 } // any seed; identity is the CN 89 let pub: *u8 = sys_mmap(32); ed25519_pub_from_priv(seed, pub) 90 let cn: *u8 = sys_mmap(72); let cn_n: i64 = az_hex(uid, 32, cn) 91 let serial: *u8 = sys_mmap(8) 92 serial[0] = 0x33 as u8; serial[1] = 0x71 as u8; serial[2] = 0x09 as u8; serial[3] = 0x12 as u8 93 serial[4] = 0x44 as u8; serial[5] = 0x55 as u8; serial[6] = 0x66 as u8; serial[7] = 0x77 as u8 94 let clen: *i64 = sys_mmap(8) as *i64 95 if nx_x509_leaf_emit_ed25519(seed, pub, cn, cn_n, serial, 8, 96 "260101000000Z" as *u8, "360101000000Z" as *u8, cert, cap, clen) != 0 { return 0 } 97 return clen[0] 98} 99 100func main() -> i64 { 101 az_w("nx_mtls authz gate (verified client cert -> identity -> deny-by-default access; rung R6)\n" as *u8) 102 103 // two identities X (enrolled lvl 2), Y (enrolled lvl 1); Z is NOT enrolled 104 let uidX: *u8 = sys_mmap(32) 105 let uidY: *u8 = sys_mmap(32) 106 let uidZ: *u8 = sys_mmap(32) 107 var i: i64 = 0 108 while i < 32 { uidX[i] = (0x10 + i) as u8; uidY[i] = (0x90 ^ i) as u8; uidZ[i] = (0xEE - i) as u8; i = i + 1 } 109 let hexX: *u8 = sys_mmap(72); az_hex(uidX, 32, hexX) 110 let hexY: *u8 = sys_mmap(72); az_hex(uidY, 32, hexY) 111 let hexZ: *u8 = sys_mmap(72); az_hex(uidZ, 32, hexZ) 112 113 let certX: *u8 = sys_mmap(NX_MAGIC_4096); let lenX: i64 = az_mint(uidX, certX, NX_MAGIC_4096) 114 115 // roster (the live daemon fills this from the HR/uid index): X->2, Y->1 116 let roster: *i64 = sys_mmap(8 * 4) as *i64 117 let rlens: *i64 = sys_mmap(8 * 4) as *i64 118 let levels: *i64 = sys_mmap(8 * 4) as *i64 119 roster[0] = hexX as i64; rlens[0] = 64; levels[0] = 2 120 roster[1] = hexY as i64; rlens[1] = 64; levels[1] = 1 121 let roster_n: i64 = 2 122 123 var pass: i64 = 0 124 125 // T1: extract the identity from the cert == hex(uid) 126 let idbuf: *u8 = sys_mmap(128) 127 let idn: i64 = nx_mtls_cert_identity(certX, lenX, idbuf, 128) 128 var t1: i64 = 0 129 if az_streq(idbuf, idn, hexX, 64) == 1 { t1 = 1 } 130 pass = pass + az_row("T1 extract Subject CN identity from the cert == hex(user_id_hash)" as *u8, t1) 131 132 // T2: verified + enrolled -> the granted level 133 var t2: i64 = 0 134 if nx_mtls_authz_level(1, idbuf, idn, roster, rlens, levels, roster_n) == 2 { t2 = 1 } 135 pass = pass + az_row("T2 verified + enrolled identity -> granted level (2)" as *u8, t2) 136 137 // T3: verified but NOT enrolled -> deny (the key was real, the user is not provisioned) 138 var t3: i64 = 0 139 if nx_mtls_authz_level(1, hexZ, 64, roster, rlens, levels, roster_n) == NX_AZ_DENY { t3 = 1 } 140 pass = pass + az_row("T3 verified but unenrolled -> DENY (deny-by-default)" as *u8, t3) 141 142 // T4: enrolled but NOT verified -> deny (an unverified cert never grants access) 143 var t4: i64 = 0 144 if nx_mtls_authz_level(0, idbuf, idn, roster, rlens, levels, roster_n) == NX_AZ_DENY { t4 = 1 } 145 pass = pass + az_row("T4 enrolled but R3-unverified -> DENY (no fail-open)" as *u8, t4) 146 147 // T5: a malformed cert yields no identity -> deny 148 var t5: i64 = 0 149 let bad: i64 = nx_mtls_cert_identity(certX, 20, idbuf, 128) // truncated -> parse fails 150 if bad == 0 { if nx_mtls_authz_level(1, idbuf, 0, roster, rlens, levels, roster_n) == NX_AZ_DENY { t5 = 1 } } 151 pass = pass + az_row("T5 malformed cert -> no identity -> DENY" as *u8, t5) 152 153 // T6: the second enrolled identity resolves to its own level (roster lookup is correct, not constant) 154 var t6: i64 = 0 155 if nx_mtls_authz_level(1, hexY, 64, roster, rlens, levels, roster_n) == 1 { t6 = 1 } 156 pass = pass + az_row("T6 a different enrolled identity -> its own level (1), not X's" as *u8, t6) 157 158 if pass == 6 { 159 az_w("NX-MTLS-AUTHZ GATE GREEN 6/6 (cert identity -> deny-by-default access; entitlement links compose he_emit_super)\n" as *u8) 160 sys_exit(0) 161 } 162 az_w("NX-MTLS-AUTHZ GATE RED\n" as *u8) 163 sys_exit(1) 164 return 1 165}