code wiki / (root) / nx_mtls_serve_decision.nx

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}