code wiki / _hdl_build / nx_dev_api_gate.nx
nx_dev_api_gate.nx source
↩ module page · 144 lines · 8515 B
1// nx_dev_api_gate.nx -- in-process gate for the sovereign DEV/CI API (nx_dev_api). Drives da_handle with
2// byte requests (NO socket, NO real fork). Proves routing + the TWO boundaries that make it safe to expose:
3// AUTH (object-capability): mutating routes need an X-Nishi-Cap granting "dev_build"; no-cap/forged -> 401.
4// INJECTION: with a VALID cap, the organ name is still charset-validated -> traversal/injection -> 400
5// BEFORE any exec (fixed-argv). Defence in depth: authority AND input both checked.
6// T1 GET index 200 T2 GET status 200 T3 unknown 404
7// T4 build NO cap -> 401 T5 build FORGED cap -> 401 T6 build valid-cap + ../etc/passwd -> 400
8// T7 build valid-cap + empty -> 400 T8 da_valid_name unit T9 da_form_field unit T10 injection charset
9// expect_exit: 0 license_tier: ORIGINAL
10import "nx_dev_api.nx"
11
12func gp(s: *u8) -> i64 { sys_write(1, s, da_slen(s)); return 0 }
13func gpn(v: i64) -> i64 { let b: *u8=sys_mmap(24); let n: i64=da_catn(b,0,v); sys_write(1,b,n); return 0 }
14func gcontains(hay: *u8, n: i64, needle: *u8) -> i64 {
15 let nl: i64=da_slen(needle); if nl==0 {return 1}
16 var i: i64=0
17 while i+nl<=n { var j: i64=0; var ok: i64=1; while j<nl { if hay[i+j]!=needle[j]{ok=0;j=nl} else {j=j+1} } if ok==1{return 1} i=i+1 }
18 return 0 }
19func chk(name: *u8, ok: i64, pass: *i64) -> i64 {
20 gp(" " as *u8)
21 if ok==1 { gp("PASS " as *u8); pass[0]=pass[0]+1 } else { gp("FAIL " as *u8) }
22 gp(name); gp("\n" as *u8)
23 return 0 }
24
25// assemble "POST /api/dev/build HTTP/1.1\r\n[X-Nishi-Cap: <cap>\r\n]Content-Length: <bl>\r\n\r\n<body>"
26func mkreq_path(out: *u8, path: *u8, cap: *u8, capl: i64, body: *u8) -> i64 {
27 var o: i64 = da_cat(out, 0, "POST " as *u8)
28 o = da_cat(out, o, path)
29 o = da_cat(out, o, " HTTP/1.1\r\nHost: x\r\n" as *u8)
30 if capl > 0 { o = da_cat(out, o, "X-Nishi-Cap: " as *u8); var i: i64=0; while i<capl { out[o]=cap[i]; o=o+1; i=i+1 } o = da_cat(out, o, "\r\n" as *u8) }
31 let bl: i64 = da_slen(body)
32 o = da_cat(out, o, "Content-Length: " as *u8); o = da_catn(out, o, bl); o = da_cat(out, o, "\r\n\r\n" as *u8)
33 o = da_cat(out, o, body)
34 return o
35}
36func mkreq(out: *u8, cap: *u8, capl: i64, body: *u8) -> i64 { return mkreq_path(out, "/api/dev/build" as *u8, cap, capl, body) }
37
38func main() -> i64 {
39 gp("=== nx_dev_api_gate: dev/CI API router + AUTH(ocap) + injection boundary ===\n" as *u8)
40 let pass: *i64 = sys_mmap(16) as *i64
41 pass[0]=0
42 let out: *u8 = sys_mmap(DA_OUTCAP + 16)
43 let nm: *u8 = sys_mmap(128)
44 let req: *u8 = sys_mmap(8192)
45
46 // mint a VALID dev_build cap with the SAME secret da_authz uses: keyfile if provisioned, else the const
47 // fallback. Fixes the self-verify fragility -- once dev_cap_secret.key exists in the tree, a const-signed
48 // gate cap fails HMAC (401) and the valid-cap tests (T6/T7) falsely RED though nothing regressed.
49 let gksz: *i64 = sys_mmap(16) as *i64; gksz[0]=0
50 let gkf: *u8 = sys_read_file(DA_CAP_KEYFILE, gksz)
51 var gsec: *u8 = DA_CAP_SECRET
52 var gsecl: i64 = da_slen(DA_CAP_SECRET)
53 if (gkf as i64) != 0 { if gksz[0] > 0 { gsec = gkf; gsecl = gksz[0] } }
54 let vcap: *u8 = sys_mmap(1024)
55 let exp: i64 = sys_now_realtime_sec() + 3600
56 let vlen: i64 = capt_issue(gsec, gsecl, "dev_build" as *u8, 9, exp, 42, vcap, 1024)
57 // a FORGED cap = valid shape, WRONG secret
58 let fcap: *u8 = sys_mmap(1024)
59 let flen: i64 = capt_issue("wrong-secret-attacker" as *u8, 21, "dev_build" as *u8, 9, exp, 42, fcap, 1024)
60
61 // T1 index
62 let r1: *u8 = "GET /api/dev HTTP/1.1\r\n\r\n"
63 let n1: i64 = da_handle(r1, da_slen(r1), out)
64 chk("T1 GET /api/dev -> 200 index " as *u8, 0 + (gcontains(out,n1,"nishi-dev" as *u8)==1), pass)
65
66 // T2 status
67 let r2: *u8 = "GET /api/dev/status HTTP/1.1\r\n\r\n"
68 let n2: i64 = da_handle(r2, da_slen(r2), out)
69 chk("T2 GET /api/dev/status -> 200 " as *u8, 0 + (gcontains(out,n2,"\"status\"" as *u8)==1), pass)
70
71 // T3 unknown
72 let r3: *u8 = "GET /api/dev/nope HTTP/1.1\r\n\r\n"
73 let n3: i64 = da_handle(r3, da_slen(r3), out)
74 chk("T3 unknown route -> 404 " as *u8, 0 + (gcontains(out,n3,"404 Not Found" as *u8)==1), pass)
75
76 // T4 build with NO cap -> 401
77 let e0: *u8 = sys_mmap(4); e0[0]=0 as u8
78 let q4: i64 = mkreq(req, e0, 0, "organ=nx_bytes_eq" as *u8)
79 let n4: i64 = da_handle(req, q4, out)
80 chk("T4 build NO cap -> 401 (auth required) " as *u8, 0 + (gcontains(out,n4,"401 Unauthorized" as *u8)==1), pass)
81
82 // T5 build with FORGED cap -> 401
83 let q5: i64 = mkreq(req, fcap, flen, "organ=nx_bytes_eq" as *u8)
84 let n5: i64 = da_handle(req, q5, out)
85 chk("T5 build FORGED cap -> 401 (HMAC fail-closed) " as *u8, 0 + (gcontains(out,n5,"401 Unauthorized" as *u8)==1), pass)
86
87 // T6 valid cap + path traversal -> 400 (auth passes, name rejected, NO fork)
88 let q6: i64 = mkreq(req, vcap, vlen, "organ=../etc/passwd" as *u8)
89 let n6: i64 = da_handle(req, q6, out)
90 chk("T6 valid-cap + ../etc/passwd -> 400 (traversal) " as *u8, 0 + (gcontains(out,n6,"400 Bad Request" as *u8)==1), pass)
91
92 // T7 valid cap + empty -> 400
93 let q7: i64 = mkreq(req, vcap, vlen, "organ=" as *u8)
94 let n7: i64 = da_handle(req, q7, out)
95 chk("T7 valid-cap + empty organ -> 400 " as *u8, 0 + (gcontains(out,n7,"400 Bad Request" as *u8)==1), pass)
96
97 // T8 da_valid_name unit
98 var t8: i64 = 1
99 if da_valid_name("nx_bytes_eq" as *u8, 11, nm) != 11 { t8=0 }
100 if da_valid_name("a/b" as *u8, 3, nm) != (0-1) { t8=0 }
101 if da_valid_name(".." as *u8, 2, nm) != (0-1) { t8=0 }
102 chk("T8 da_valid_name: valid ok, traversal NO " as *u8, t8, pass)
103
104 // T9 da_form_field unit
105 let got: *u8 = sys_mmap(128)
106 let gl: i64 = da_form_field("organ=nx_foo_gate" as *u8, 0, 17, "organ" as *u8, got)
107 chk("T9 da_form_field extracts organ value " as *u8, 0 + (gl==11), pass)
108
109 // T10 injection charset rejected
110 var t10: i64 = 1
111 if da_valid_name("nx_x;rm" as *u8, 7, nm) != (0-1) { t10=0 }
112 if da_valid_name("nx_x foo" as *u8, 8, nm) != (0-1) { t10=0 }
113 if da_valid_name("nx_x`id`" as *u8, 8, nm) != (0-1) { t10=0 }
114 chk("T10 NEG injection metachars rejected " as *u8, t10, pass)
115
116 // T11 run NO cap -> 401
117 let q11: i64 = mkreq_path(req, "/api/dev/run" as *u8, e0, 0, "organ=nx_exec_engine" as *u8)
118 let n11: i64 = da_handle(req, q11, out)
119 chk("T11 run NO cap -> 401 " as *u8, 0 + (gcontains(out,n11,"401 Unauthorized" as *u8)==1), pass)
120
121 // T12 run valid-cap + NOT-allowlisted organ -> 403 (allowlist fail-closed = no privilege-escalation)
122 let q12: i64 = mkreq_path(req, "/api/dev/run" as *u8, vcap, vlen, "organ=nx_aw_send" as *u8)
123 let n12: i64 = da_handle(req, q12, out)
124 chk("T12 run valid-cap + non-allowlisted -> 403 " as *u8, 0 + (gcontains(out,n12,"403 Forbidden" as *u8)==1), pass)
125
126 // T13 run valid-cap + allowlisted organ + BAD arg -> 400 (arg charset boundary, before any exec)
127 let q13: i64 = mkreq_path(req, "/api/dev/run" as *u8, vcap, vlen, "organ=nx_exec_engine&arg1=bad;arg" as *u8)
128 let n13: i64 = da_handle(req, q13, out)
129 chk("T13 run valid-cap + bad arg -> 400 " as *u8, 0 + (gcontains(out,n13,"400 Bad Request" as *u8)==1), pass)
130
131 // T14 run valid-cap + allowlisted organ (nx_safetensors, installed + fast) -> 200 + exit:0 = the real exec path
132 // T14 EXECUTION in the MULTI-CALL context (the 14th da_handle call). Root cause of the earlier fork-fail was
133 // NOT nx_cc: sys_read_file mmaps 4GiB/call and never munmaps -> ~13 prior per-request reads (keyfile/conf)
134 // exhausted address space -> fork()=-1. FIXED (da_read_small/lseek-tail, bounded, fd-closed). Now the 14th call
135 // must EXECUTE the organ (exit:0) = proves the run route runs AND the leak fix holds under repeated requests.
136 let q14: i64 = mkreq_path(req, "/api/dev/run" as *u8, vcap, vlen, "organ=nx_safetensors" as *u8)
137 let n14: i64 = da_handle(req, q14, out)
138 chk("T14 run (14th call) valid-cap+allowlisted -> exit:0" as *u8, 0 + (gcontains(out,n14,"\"exit\":0" as *u8)==1), pass)
139
140 gp("DEV-API-GATE pass=" as *u8); gpn(pass[0]); gp("/14" as *u8)
141 if pass[0]==14 { gp(" verdict=GREEN -- routing+auth+injection+allowlist + EXECUTION under repeated requests (leak fixed)\n" as *u8); return 0 }
142 gp(" verdict=RED\n" as *u8)
143 return 1
144}