code wiki / _hdl_build / nx_vecfetch.nx

nx_vecfetch.nx source

↩ module page · 247 lines · 13818 B

1// nx_vecfetch.nx -- ACQUIRE EXTERNAL ANSWERS WITH THE AGENT OUT OF THE BYTE PATH. 2// 3// THE HOLE THIS CLOSES (measured 2026-07-31): every clause in the evidence apparatus verifies the 4// COMPARISON and none verifies the PROVENANCE OF THE ANSWERS. An agent can write a gate against 5// "RFC 8032 test vectors" by typing them from its own recall -- it compiles, runs GREEN, and yields a row 6// with a real ref=, a real refdig= and a real execution. refdig= commits to the bytes we compared against; 7// it CANNOT prove those bytes came from the IETF rather than from the model that wrote the file. 8// 9// ============================ v2, 2026-07-31: TWO DEFECTS FOUND AND FIXED ============================ 10// 11// ⚠DEFECT 1 -- I HASHED THE ENVELOPE, NOT THE DOCUMENT. v1 hashed whatever nx_https_get returned. By 12// contract that is the ENTIRE HTTP RESPONSE: status line, headers, and chunked-transfer framing. So all 14 13// pinned "authority documents" were HTTP WIRE TRANSCRIPTS -- rfc2202.txt carried 707 bytes of Cloudflare 14// headers and a `2ea6` chunk-size line before the RFC even began. nx_extvec_authentic_gate now reports 15// 14/14 CONTAMINATED against the v1 corpus. Verified against a stack sharing no code (.NET WebClient): 16// chunk size 0x2ea6 = 11942 = the true document length exactly. 17// 18// ★★★★★ THE LAW v1 GOT WRONG, CORRECTED: 19// v1 said "hash the bytes before anything touches them". That is too crude -- it hashes the ENVELOPE. 20// THE RULE IS: HASH THE OBJECT YOU ARE MAKING A CLAIM ABOUT, AT THE EARLIEST POINT IT EXISTS. 21// The document does not exist until the transport framing is removed. Hashing before that point is not 22// "hashing earlier and therefore safer" -- it is hashing A DIFFERENT OBJECT. 23// The anti-tamper property v1 was protecting is PRESERVED: the decode below is a pure, deterministic, 24// in-process transform. No agent, no second tool, and no disk sits between the socket and the digest. 25// 26// ⚠DEFECT 2 -- THE DECODER ALREADY EXISTED AND I DID NOT ADOPT IT. nx_fetch_unit.nx has had fu_body_off / 27// fu_status / fu_find_ci / fu_dechunk, and a fully staged nx_fetch_staged, the whole time. v1 called the 28// RAW primitive underneath them. ★THE ADOPTION GAP, NOT A MISSING PRIMITIVE -- the same class already in 29// memory. The fix is to ADOPT the proven decoder, not to write a ninth private copy of one. (`nx_body_off` 30// is currently duplicated across at least 9 files -- that Rule-15 extraction is filed separately; this 31// organ deliberately adds no new copy.) 32// 33// ============================ CORROBORATION: THE JULY-2026 BAR ============================ 34// A digest pin proves INTEGRITY-SINCE-FETCH. It cannot prove AUTHENTICITY-AT-SOURCE, because a corrupted 35// or re-framed fetch pins exactly as cleanly as a good one -- the pin is self-consistent by construction. 36// The state of the art does not solve this with a better hash; it solves it with ANOTHER PARTY: 37// - CA/Browser Forum MPIC (mandatory for TLS/S-MIME issuance): validate from MULTIPLE INDEPENDENT 38// NETWORK PERSPECTIVES, and if the perspectives DISAGREE, THE PROCESS HALTS. It does not pick one. 39// - Sigstore/Rekor: a transparency log exists so verification need not trust a single party. 40// So this organ takes an OPTIONAL third argument: a digest obtained by an INDEPENDENT STACK. If supplied 41// and it disagrees, the fetch is REFUSED -- halt, never reconcile, never prefer our own. If not supplied, 42// the output is explicitly stamped UNCORROBORATED so a single-perspective acquisition can never be 43// mistaken for a corroborated one downstream. 44// ★TWO FETCHES THROUGH THE SAME CODE ARE NOT TWO PERSPECTIVES. The corroborating digest MUST come from a 45// stack that shares no parser with this one, or it only re-measures our own bug. 46// 47// ORDERING, AND IT IS NOT REARRANGEABLE: 48// 1 fetch into a buffer 2 verify status, strip headers, de-chunk -> THE DOCUMENT 49// 3 sha256 THE DOCUMENT, in this process, before it touches disk 50// 4 compare against the independent perspective (halt on disagreement) 51// 5 only then write 6 print refsrc= / refsrcdig= / bytes= / corroboration state 52// 53// usage: nx_vecfetch <url> <destpath> [independent-sha256-hex] 54// license_tier: ORIGINAL expect_exit: 0 55import "nx_syscalls.nx" 56import "nx_csprng.nx" 57import "nx_x509_trust_store.nx" 58import "nx_trust_store_load_from_certdata.nx" 59import "nx_https_get.nx" 60import "nx_fetch_unit.nx" 61import "nx_sha256_wasm.nx" 62import "nx_inflate.nx" 63 64const INF_RC_NOT_GZIP: i64 = 0 - 7 // inf_gunzip's rc for a body that is not a gzip container (identity) 65 66func vf_put(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 67func vf_wb(b: *u8, n: i64) -> i64 { sys_write(1, b, n); return 0 } 68 69func vf_num(v: i64) -> i64 { 70 var m: i64 = v 71 if m < 0 { vf_put("-" as *u8); m = 0 - m } 72 let t: *u8 = sys_mmap(32) 73 var k: i64 = 0 74 if m == 0 { t[0] = 48 as u8; k = 1 } 75 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 76 let b: *u8 = sys_mmap(32) 77 var j: i64 = 0 78 while j < k { b[j] = t[k - 1 - j]; j = j + 1 } 79 sys_write(1, b, k) 80 return 0 81} 82 83func vf_hexnib(v: i64) -> i64 { if v < 10 { return 48 + v } return 87 + v } 84func vf_lc(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c } 85 86func main(argc: i64, argv: *i64) -> i64 { 87 if argc < 3 { vf_put("usage: nx_vecfetch <url> <destpath> [independent-sha256-hex]\n" as *u8); return 2 } 88 let url: *u8 = argv[1] as *u8 89 let dest: *u8 = argv[2] as *u8 90 var corro: *u8 = 0 as *u8 91 if argc >= 4 { corro = argv[3] as *u8 } 92 let now: i64 = sys_now_realtime_sec() 93 94 let r: i64 = nx_trust_store_load_from_certdata("data/mozilla_certdata.txt\x00" as *u8, 512, 4194304) 95 if r <= 0 { vf_put("ERROR: trust-store load failed (need data/mozilla_certdata.txt on CWD)\n" as *u8); return 3 } 96 let store: *TrustStore = r as *TrustStore 97 98 let cr: *u8 = sys_mmap(32) 99 nx_csprng_fill(cr, 32) 100 let priv: *u8 = sys_mmap(32) 101 nx_csprng_fill(priv, 32) 102 103 // ---- STEP 1: FETCH (returns the WHOLE response: status line + headers + framed body) ---- 104 let cap: i64 = 8388608 105 let buf: *u8 = sys_mmap(cap) 106 let v: i64 = nx_https_get(url, cr, priv, store, now, buf, cap) 107 vf_put("fetch_rc=" as *u8); vf_num(v); vf_put("\n" as *u8) 108 // ★MY FIRST GUARD ASKED THE WRONG QUESTION. nx_https_get_verdict_is_valid(v) is a RANGE CHECK ON THE 109 // VERDICT ENUM, not "did the fetch succeed": a success returns the RESPONSE LENGTH, far above the enum 110 // bound. Narrowed to exactly the success shape -- widening to `v >= 0` would let every failure verdict 111 // through as a "body" and manufacture provenance for bytes never received. 112 if v < NX_HTTPS_GET_VERDICT_N { 113 vf_put("ERROR: fetch returned a FAILURE VERDICT -- refusing to write or hash anything.\n" as *u8) 114 return 3 115 } 116 if v > cap { vf_put("ERROR: reported length exceeds the buffer -- truncation suspected, refusing.\n" as *u8); return 3 } 117 118 // ---- STEP 2: DECODE THE TRANSPORT ENVELOPE. THE DOCUMENT DOES NOT EXIST UNTIL THIS COMPLETES. ---- 119 // 2a. Status. A 404 body or a redirect stub pins just as cleanly as the real document; only 200 may be 120 // treated as the authority's answer. v1 never looked at the status line at all. 121 let st: i64 = fu_status(buf, v) 122 vf_put("http_status=" as *u8); vf_num(st); vf_put("\n" as *u8) 123 if st != 200 { 124 vf_put("ERROR: HTTP status is not 200 -- refusing to pin a non-answer as an external answer.\n" as *u8) 125 return 3 126 } 127 128 // 2b. Header/body split. 129 let bo: i64 = fu_body_off(buf, v) 130 if bo <= 0 { 131 vf_put("ERROR: no CRLFCRLF header/body break found -- cannot identify the document.\n" as *u8) 132 return 3 133 } 134 135 // 2c. Chunked? Header names are case-insensitive (RFC 9110), and the probe is bounded to the header 136 // block so a body that DISCUSSES chunked encoding cannot flip the decoder. 137 var chunked: i64 = 0 138 let te: i64 = fu_find_ci(buf, bo, "\ntransfer-encoding:" as *u8) 139 if te >= 0 { 140 if fu_find_ci(((buf as i64) + te) as *u8, 96, "chunked" as *u8) >= 0 { chunked = 1 } 141 } 142 143 let doc: *u8 = sys_mmap(cap) 144 var L: i64 = 0 145 if chunked == 1 { 146 L = fu_dechunk(buf, bo, v, doc, cap) 147 vf_put("framing=chunked decoded_bytes=" as *u8); vf_num(L); vf_put("\n" as *u8) 148 } else { 149 var i: i64 = 0 150 while bo + i < v { doc[i] = buf[bo + i]; i = i + 1 } 151 L = i 152 vf_put("framing=identity body_bytes=" as *u8); vf_num(L); vf_put("\n" as *u8) 153 } 154 if L <= 0 { vf_put("ERROR: empty document after decode\n" as *u8); return 3 } 155 156 // 2d. SELF-CHECK: the decoded document must be strictly smaller than the transcript, and must not still 157 // begin with an HTTP status line. A decoder that silently no-ops is the exact failure that produced 158 // the v1 corpus, and it must be LOUD rather than merely absent. 159 if L >= v { vf_put("ERROR: decode produced no reduction -- envelope not removed, refusing.\n" as *u8); return 3 } 160 var looks: i64 = 1 161 let sig: *u8 = "HTTP/" as *u8 162 var s: i64 = 0 163 while s < 5 { if doc[s] != sig[s] { looks = 0 } s = s + 1 } 164 if looks == 1 { vf_put("ERROR: decoded document still begins 'HTTP/' -- refusing.\n" as *u8); return 3 } 165 166 // ---- STEP 2e: CONTENT-ENCODING. The framing decode above strips CHUNKING, not Content-Encoding. 167 // rfc-editor.org (and most origins) gzip the body regardless of what we asked, so `doc` may still 168 // be a gzip stream -- pinning it would commit the digest to COMPRESSED bytes and every downstream 169 // KAT that parses the document as ASCII fails on the pin or the parse. We do NOT re-derive what a 170 // gzip container looks like (that ruler lives in inf_gunzip): attempt to inflate, and if the body 171 // is not a gzip container inf_gunzip returns INF_RC_NOT_GZIP and we keep the identity body as-is. 172 // Any OTHER negative rc is a CORRUPT gzip stream and fails LOUD -- inf_gunzip verifies CRC32 and 173 // ISIZE, so we never pin bytes we could not fully and correctly decompress. Reuses the existing 174 // `cap` buffer and copies back into `doc` so the hash and write steps below are unchanged. 175 let plain: *u8 = sys_mmap(cap) 176 let PL: i64 = inf_gunzip(doc, L, plain, cap) 177 if PL >= 0 { 178 var ci: i64 = 0 179 while ci < PL { doc[ci] = plain[ci]; ci = ci + 1 } 180 L = PL 181 vf_put("content_encoding=gzip inflated_bytes=" as *u8); vf_num(PL); vf_put("\n" as *u8) 182 } else { 183 if PL != INF_RC_NOT_GZIP { vf_put("ERROR: Content-Encoding gzip inflate failed rc=" as *u8); vf_num(PL); vf_put(" -- refusing to pin a body we cannot decompress.\n" as *u8); return 3 } 184 vf_put("content_encoding=identity\n" as *u8) 185 } 186 187 // ---- STEP 3: HASH THE DOCUMENT, HERE, BEFORE IT TOUCHES DISK OR ANY OTHER PROCESS ---- 188 let ctx: *u8 = sys_mmap(1024) 189 let dig: *u8 = sys_mmap(64) 190 nx_sha256_one_shot(doc, L, ctx, dig) 191 let hx: *u8 = sys_mmap(80) 192 var i2: i64 = 0 193 while i2 < 32 { 194 hx[i2 * 2] = vf_hexnib(((dig[i2] as i64) / 16) & 15) as u8 195 hx[i2 * 2 + 1] = vf_hexnib((dig[i2] as i64) & 15) as u8 196 i2 = i2 + 1 197 } 198 199 // ---- STEP 4: CORROBORATION. HALT ON DISAGREEMENT -- NEVER RECONCILE, NEVER PREFER OUR OWN. ---- 200 var corro_state: i64 = 0 // 0 = none supplied, 1 = agreed 201 if (corro as i64) != 0 { 202 var cl: i64 = 0 203 while corro[cl] != (0 as u8) { cl = cl + 1 } 204 if cl != 64 { 205 vf_put("ERROR: independent digest must be 64 hex chars, got " as *u8); vf_num(cl); vf_put("\n" as *u8) 206 return 3 207 } 208 var agree: i64 = 1 209 var k: i64 = 0 210 while k < 64 { if vf_lc(corro[k] as i64) != (hx[k] as i64) { agree = 0 } k = k + 1 } 211 if agree == 0 { 212 vf_put("\nHALT: PERSPECTIVES DISAGREE -- REFUSING TO WRITE.\n" as *u8) 213 vf_put(" ours(sovereign) = " as *u8); vf_wb(hx, 64); vf_put("\n" as *u8) 214 vf_put(" independent = " as *u8); vf_put(corro); vf_put("\n" as *u8) 215 vf_put(" Per CA/Browser Forum MPIC: on disagreement the process HALTS. Picking either side\n" as *u8) 216 vf_put(" would be choosing which party to trust, which is the thing corroboration exists to\n" as *u8) 217 vf_put(" avoid. Resolve the disagreement, do not average it.\n" as *u8) 218 return 4 219 } 220 corro_state = 1 221 } 222 223 // ---- STEP 5: only now, write ---- 224 let fd: i64 = sys_openat_wr(dest, 0x1a4) 225 if fd < 0 { vf_put("ERROR: cannot open destpath for write\n" as *u8); return 3 } 226 var off: i64 = 0 227 while off < L { 228 let wn: i64 = sys_write(fd, ((doc as i64) + off) as *u8, L - off) 229 if wn <= 0 { sys_close(fd); vf_put("ERROR: short write -- destination NOT trustworthy\n" as *u8); return 3 } 230 off = off + wn 231 } 232 sys_close(fd) 233 234 // ---- STEP 6: report ---- 235 vf_put("\n refsrc=" as *u8); vf_put(url); vf_put("\n" as *u8) 236 vf_put(" refsrcdig=" as *u8); vf_wb(hx, 64); vf_put("\n" as *u8) 237 vf_put(" bytes=" as *u8); vf_num(L); vf_put("\n" as *u8) 238 vf_put(" transcript_bytes=" as *u8); vf_num(v); vf_put(" document_bytes=" as *u8); vf_num(L); vf_put("\n" as *u8) 239 if corro_state == 1 { 240 vf_put(" corroboration=AGREED (independent stack confirms this digest)\n" as *u8) 241 } else { 242 vf_put(" corroboration=UNCORROBORATED -- ONE PERSPECTIVE ONLY.\n" as *u8) 243 vf_put(" This digest proves integrity-since-fetch, NOT authenticity-at-source. Supply a digest\n" as *u8) 244 vf_put(" from a stack sharing no code with this one to raise it.\n" as *u8) 245 } 246 return 0 247}