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}