nx_mtls_serve_decision.nx source
↩ module page · 118 lines · 7934 B
1// nx_mtls_serve_decision.nx -- R7 ENFORCEMENT: turn an mTLS handshake result into a doc-serving decision.
2//
3// This is the glue the doc-serving wall (wiki_gw / the OPAQUE wall that today bounces a top-level nav to login)
4// calls AFTER the mTLS run loop returns. The run loop hands back out_auth {1=verified, 0=no-cert, -1=verify-failed}
5// + the presented client cert; this rung composes R6 (nx_mtls_cert_identity + nx_mtls_authz_level, deny-by-default)
6// into exactly one of four decisions. Building + gating it HERE means the live daemon wiring is a trivial call to
7// proven logic, not ad-hoc branching -- and the never-lockout / request-not-require property is baked in + tested:
8//
9// out_auth == -1 -> REJECT a cert was presented but FAILED to prove key-possession. NOT a fallback --
10// silently falling back would be a downgrade. 403/close.
11// out_auth == 0 -> FALLBACK NO cert presented. Serve via the EXISTING header-token path exactly as today
12// (a browser with no cert still works -> never-lockout, request-not-require).
13// out_auth == 1 -> (identity) a VERIFIED cert. Extract the Subject CN identity:
14// REJECT verified key but no extractable identity (malformed) -> close.
15// FORBIDDEN verified identity but NOT enrolled in the roster -> 403 (deny-by-default).
16// BY_IDENTITY verified + enrolled -> serve; out_uidhex + out_level set.
17//
18// The roster (uidhex -> level) is the live HR/uid index the daemon supplies (olgd_idx_lookup + HR level), same as
19// R6. This rung is the DECISION; that is the DATA. Composes nx_mtls_authz (R6). license_tier: ORIGINAL expect_exit: 0
20import "nx_syscalls.nx"
21import "nx_mtls_authz.nx"
22const NX_MAGIC_4096: i64 = 4096
23
24const NX_SERVE_REJECT: i64 = 0 // cert presented but verify-failed (out_auth<0) OR verified-but-no-identity
25const NX_SERVE_FALLBACK: i64 = 1 // no cert (out_auth==0) -> existing header-token path; never-lockout
26const NX_SERVE_FORBIDDEN: i64 = 2 // verified identity but unenrolled -> 403 (deny-by-default)
27const NX_SERVE_BY_IDENTITY: i64 = 3 // verified + enrolled -> serve; out_uidhex + out_level set
28
29// Decide what the doc wall serves for a connection whose mTLS run loop returned `out_auth` + `client_cert_der`.
30// Returns one NX_SERVE_* code. On BY_IDENTITY, out_uidhex holds hex(user_id_hash) + out_level the granted level.
31func nx_mtls_serve_decide(
32 out_auth: i64,
33 client_cert_der: *u8, cert_len: i64,
34 roster: *i64, roster_lens: *i64, levels: *i64, roster_n: i64,
35 out_uidhex: *u8, out_uidhex_cap: i64, out_level: *i64
36) -> i64 {
37 out_level[0] = NX_AZ_DENY
38 if out_auth < 0 { return NX_SERVE_REJECT } // presented-but-bad cert: never downgrade to fallback
39 if out_auth == 0 { return NX_SERVE_FALLBACK } // no cert: graceful header-token fallback (never-lockout)
40 // out_auth == 1: a cryptographically VERIFIED client cert.
41 let idn: i64 = nx_mtls_cert_identity(client_cert_der, cert_len, out_uidhex, out_uidhex_cap)
42 if idn <= 0 { return NX_SERVE_REJECT } // verified key but no extractable identity -> malformed
43 let lvl: i64 = nx_mtls_authz_level(1, out_uidhex, idn, roster, roster_lens, levels, roster_n)
44 if lvl == NX_AZ_DENY { return NX_SERVE_FORBIDDEN }
45 out_level[0] = lvl
46 return NX_SERVE_BY_IDENTITY
47}
48
49// ===== in-process gate: the 4 decisions over real minted certs + a real roster (composes R6 az_mint/az_hex) =====
50func sd_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
51func sd_row(name: *u8, ok: i64) -> i64 { if ok == 1 { sd_w(" PASS " as *u8) } else { sd_w(" FAIL " as *u8) } sd_w(name); sd_w("\n" as *u8); return ok }
52
53func main() -> i64 {
54 sd_w("nx_mtls serve-decision gate (mTLS out_auth -> serve/fallback/forbidden/reject; R7 enforcement, deny-by-default)\n" as *u8)
55
56 // identities: X enrolled lvl 2, Y enrolled lvl 1, Z verified-but-unenrolled
57 let uidX: *u8 = sys_mmap(32); let uidY: *u8 = sys_mmap(32); let uidZ: *u8 = sys_mmap(32)
58 var i: i64 = 0
59 while i < 32 { uidX[i] = (0x10 + i) as u8; uidY[i] = (0x90 ^ i) as u8; uidZ[i] = (0xEE - i) as u8; i = i + 1 }
60 let hexX: *u8 = sys_mmap(72); az_hex(uidX, 32, hexX)
61 let hexY: *u8 = sys_mmap(72); az_hex(uidY, 32, hexY)
62 let certX: *u8 = sys_mmap(NX_MAGIC_4096); let lenX: i64 = az_mint(uidX, certX, NX_MAGIC_4096)
63 let certY: *u8 = sys_mmap(NX_MAGIC_4096); let lenY: i64 = az_mint(uidY, certY, NX_MAGIC_4096)
64 let certZ: *u8 = sys_mmap(NX_MAGIC_4096); let lenZ: i64 = az_mint(uidZ, certZ, NX_MAGIC_4096)
65
66 // roster the live daemon supplies from the HR/uid index: X->2, Y->1 (Z absent)
67 let roster: *i64 = sys_mmap(8 * 4) as *i64; let rlens: *i64 = sys_mmap(8 * 4) as *i64; let levels: *i64 = sys_mmap(8 * 4) as *i64
68 roster[0] = hexX as i64; rlens[0] = 64; levels[0] = 2
69 roster[1] = hexY as i64; rlens[1] = 64; levels[1] = 1
70 let roster_n: i64 = 2
71
72 let uidbuf: *u8 = sys_mmap(128); let lvlout: *i64 = sys_mmap(8) as *i64
73 var pass: i64 = 0
74
75 // T1: verified + enrolled X -> SERVE_BY_IDENTITY, level 2, identity == hex(uidX)
76 let d1: i64 = nx_mtls_serve_decide(1, certX, lenX, roster, rlens, levels, roster_n, uidbuf, 128, lvlout)
77 var t1: i64 = 0
78 if d1 == NX_SERVE_BY_IDENTITY { if lvlout[0] == 2 { if az_streq(uidbuf, 64, hexX, 64) == 1 { t1 = 1 } } }
79 pass = pass + sd_row("T1 verified+enrolled -> SERVE_BY_IDENTITY (level 2, identity recovered)" as *u8, t1)
80
81 // T2: NO cert (out_auth==0) -> FALLBACK (a browser with no cert still served via the header path; never-lockout)
82 let d2: i64 = nx_mtls_serve_decide(0, certX, lenX, roster, rlens, levels, roster_n, uidbuf, 128, lvlout)
83 var t2: i64 = 0
84 if d2 == NX_SERVE_FALLBACK { if lvlout[0] == NX_AZ_DENY { t2 = 1 } }
85 pass = pass + sd_row("T2 no cert presented -> FALLBACK to header path (never-lockout, request-not-require)" as *u8, t2)
86
87 // T3: verify-FAILED (out_auth==-1) -> REJECT, NOT fallback (a bad cert must not silently downgrade)
88 let d3: i64 = nx_mtls_serve_decide(0 - 1, certX, lenX, roster, rlens, levels, roster_n, uidbuf, 128, lvlout)
89 var t3: i64 = 0
90 if d3 == NX_SERVE_REJECT { t3 = 1 }
91 pass = pass + sd_row("T3 presented-but-bad cert -> REJECT, never silently downgrades to fallback" as *u8, t3)
92
93 // T4: verified identity Z NOT enrolled -> FORBIDDEN (deny-by-default; a real key is not authorization)
94 let d4: i64 = nx_mtls_serve_decide(1, certZ, lenZ, roster, rlens, levels, roster_n, uidbuf, 128, lvlout)
95 var t4: i64 = 0
96 if d4 == NX_SERVE_FORBIDDEN { if lvlout[0] == NX_AZ_DENY { t4 = 1 } }
97 pass = pass + sd_row("T4 verified but unenrolled -> FORBIDDEN (deny-by-default, no fail-open)" as *u8, t4)
98
99 // T5: verified + enrolled Y -> SERVE_BY_IDENTITY, level 1 (roster lookup is per-identity, not constant)
100 let d5: i64 = nx_mtls_serve_decide(1, certY, lenY, roster, rlens, levels, roster_n, uidbuf, 128, lvlout)
101 var t5: i64 = 0
102 if d5 == NX_SERVE_BY_IDENTITY { if lvlout[0] == 1 { if az_streq(uidbuf, 64, hexY, 64) == 1 { t5 = 1 } } }
103 pass = pass + sd_row("T5 a different enrolled identity -> its OWN level (1), not X's" as *u8, t5)
104
105 // T6: verified but malformed cert (truncated -> no identity) -> REJECT
106 let d6: i64 = nx_mtls_serve_decide(1, certX, 20, roster, rlens, levels, roster_n, uidbuf, 128, lvlout)
107 var t6: i64 = 0
108 if d6 == NX_SERVE_REJECT { t6 = 1 }
109 pass = pass + sd_row("T6 verified key but malformed cert (no identity) -> REJECT" as *u8, t6)
110
111 if pass == 6 {
112 sd_w("NX-MTLS-SERVE-DECISION GATE GREEN 6/6 (serve-by-identity | header-fallback | forbidden | reject; ready to drop into wiki_gw)\n" as *u8)
113 sys_exit(0)
114 }
115 sd_w("NX-MTLS-SERVE-DECISION GATE RED\n" as *u8)
116 sys_exit(1)
117 return 1
118}