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}