code wiki / _hdl_build / nx_provstmt_lib.nx

nx_provstmt_lib.nx source

↩ module page · 136 lines · 7004 B

1// nx_provstmt_lib.nx -- CR3 of /compare/capregistry: BUILD PROVENANCE AS AN in-toto STATEMENT, pure core. 2// 3// THE FINDING THIS SERIALISES, measured 2026-08-29: every /api/build response already returns src_sha256, 4// src_stable and closure_sha256, and /api/promote returns the live artifact sha256 after the rename. Those 5// fields ARE SLSA build provenance -- what source produced this artifact, the digest of its whole transitive 6// input closure, and the digest of what actually went live -- and every caller discards them at the end of 7// the HTTP response. A CAPABILITY THAT IS COMPUTED, RETURNED, AND DISCARDED BY EVERY CALLER IS DARK IN THE 8// MOST EXPENSIVE WAY: THE ESTATE PAYS FOR IT ON EVERY BUILD AND OWNS IT ON NONE. This rung is therefore a 9// SERIALISATION, not a measurement: nothing new is measured, only bound to a subject and written down. 10// 11// WHY THIS IS NOT AN EXTENSION OF nx_coe_intoto, read before deciding: that exporter's predicate is entirely 12// chain-of-evidence specific (frameHash, pillars, a Merkle inclusion proof) and its ed25519 publicKey is a 13// hardcoded literal. Extending it would fork one predicate across two unrelated subjects. Only the ITE-6 14// Statement SHAPE is shared, and a JSON scaffold is not a ruler. 15// 16// PURE BY CONSTRUCTION: no filesystem, no syscalls beyond the caller's buffers. The organ owns the 17// re-hash-and-refuse policy; this lib owns validation and serialisation, so the gate reaches every rule 18// in-process and a mutation cannot hide behind a fork boundary. 19 20import "nx_syscalls.nx" 21 22const PV_HEX_LEN: i64 = 64 // sha256 = 32 bytes = 64 hex chars, a DEFINITION not a tunable 23const PV_STMT_CAP: i64 = 4096 // the statement is ~700 B; 4096 leaves 5x headroom and pv_build REFUSES 24 // (returns 0) rather than truncating if it would ever fill -- no silent cap 25 26// Exactly PV_HEX_LEN lowercase hex chars, then NUL. Uppercase is REFUSED deliberately: every producer of 27// these digests (the build lane, the promote receipt, sha256 hex emitters estate-wide) emits lowercase, so 28// an uppercase digest is evidence of a hand-typed or foreign value and refusing it is 29// wrong-in-the-direction-of-refusing. 30func pv_hex_ok(s: *u8) -> i64 { 31 var i: i64 = 0 32 while i < PV_HEX_LEN { 33 let c: i64 = s[i] as i64 34 if c == 0 { return 0 } 35 var ok: i64 = 0 36 if c >= 48 { if c <= 57 { ok = 1 } } 37 if c >= 97 { if c <= 102 { ok = 1 } } 38 if ok == 0 { return 0 } 39 i = i + 1 40 } 41 if s[PV_HEX_LEN] != (0 as u8) { return 0 } 42 return 1 43} 44 45// 64-char digest equality. Both inputs are assumed pv_hex_ok; a caller comparing unvalidated strings gets a 46// correct answer for the first 64 chars regardless. 47func pv_hex_eq(a: *u8, b: *u8) -> i64 { 48 var i: i64 = 0 49 while i < PV_HEX_LEN { 50 if a[i] != b[i] { return 0 } 51 i = i + 1 52 } 53 return 1 54} 55 56// 32-byte digest -> 64 lowercase hex chars + NUL. The encoder lives in the lib so the gate can prove the 57// round-trip in-process: pv_hex_of output must satisfy pv_hex_ok, and a digest byte of 0x00 must encode as 58// "00" not collapse -- the classic leading-zero hex bug. 59func pv_hex_of(dig: *u8, out: *u8) -> i64 { 60 var i: i64 = 0 61 while i < 32 { 62 let b: i64 = dig[i] as i64 63 let hi: i64 = b / 16 64 let lo: i64 = b % 16 65 var c: i64 = hi + 48 66 if hi > 9 { c = hi + 87 } 67 out[i * 2] = c as u8 68 c = lo + 48 69 if lo > 9 { c = lo + 87 } 70 out[i * 2 + 1] = c as u8 71 i = i + 1 72 } 73 out[64] = 0 as u8 74 return 64 75} 76 77func pv_cat(d: *u8, o: i64, s: *u8) -> i64 { 78 var p: i64 = o 79 var i: i64 = 0 80 while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } 81 return p 82} 83 84func pv_q(d: *u8, o: i64) -> i64 { d[o] = 34 as u8; return o + 1 } 85 86func pv_cq(d: *u8, o: i64, s: *u8) -> i64 { 87 var p: i64 = pv_q(d, o) 88 p = pv_cat(d, p, s) 89 return pv_q(d, p) 90} 91 92// Build the ITE-6 Statement into J (caller-owned, at least PV_STMT_CAP bytes). 93// Returns the byte length, or 0 when any input fails validation or the output would overflow -- 94// a refused build writes NOTHING, so a partial statement can never be mistaken for a whole one. 95// The subject NAME is "<target>.elf" and the subject digest is the LIVE artifact sha the ORGAN verified by 96// re-hashing; this lib serialises what it is given and the organ owes the verification. 97func pv_build(J: *u8, target: *u8, art_sha: *u8, src_sha: *u8, closure_sha: *u8) -> i64 { 98 if pv_hex_ok(art_sha) == 0 { return 0 } 99 if pv_hex_ok(src_sha) == 0 { return 0 } 100 if pv_hex_ok(closure_sha) == 0 { return 0 } 101 var tl: i64 = 0 102 while target[tl] != (0 as u8) { tl = tl + 1 } 103 if tl <= 0 { return 0 } 104 // worst case: fixed scaffold (~500) + name + 3 digests; refuse before any write if it cannot fit 105 if 600 + tl + 3 * PV_HEX_LEN >= PV_STMT_CAP { return 0 } 106 var o: i64 = 0 107 o = pv_cat(J, o, "{" as *u8) 108 o = pv_cq(J, o, "_type" as *u8); o = pv_cat(J, o, ":" as *u8) 109 o = pv_cq(J, o, "https://in-toto.io/Statement/v1" as *u8); o = pv_cat(J, o, "," as *u8) 110 o = pv_cq(J, o, "subject" as *u8); o = pv_cat(J, o, ":[{" as *u8) 111 o = pv_cq(J, o, "name" as *u8); o = pv_cat(J, o, ":" as *u8) 112 o = pv_q(J, o); o = pv_cat(J, o, target); o = pv_cat(J, o, ".elf" as *u8); o = pv_q(J, o) 113 o = pv_cat(J, o, "," as *u8) 114 o = pv_cq(J, o, "digest" as *u8); o = pv_cat(J, o, ":{" as *u8) 115 o = pv_cq(J, o, "sha256" as *u8); o = pv_cat(J, o, ":" as *u8); o = pv_cq(J, o, art_sha) 116 o = pv_cat(J, o, "}}]," as *u8) 117 o = pv_cq(J, o, "predicateType" as *u8); o = pv_cat(J, o, ":" as *u8) 118 o = pv_cq(J, o, "https://nishi.sovereign/build-provenance/v1" as *u8); o = pv_cat(J, o, "," as *u8) 119 o = pv_cq(J, o, "predicate" as *u8); o = pv_cat(J, o, ":{" as *u8) 120 o = pv_cq(J, o, "srcSha256" as *u8); o = pv_cat(J, o, ":" as *u8); o = pv_cq(J, o, src_sha); o = pv_cat(J, o, "," as *u8) 121 o = pv_cq(J, o, "closureSha256" as *u8); o = pv_cat(J, o, ":" as *u8); o = pv_cq(J, o, closure_sha); o = pv_cat(J, o, "," as *u8) 122 o = pv_cq(J, o, "builder" as *u8); o = pv_cat(J, o, ":{" as *u8) 123 o = pv_cq(J, o, "id" as *u8); o = pv_cat(J, o, ":" as *u8) 124 o = pv_cq(J, o, "https://nishi.sovereign/api/build" as *u8); o = pv_cat(J, o, "," as *u8) 125 o = pv_cq(J, o, "sovereign" as *u8); o = pv_cat(J, o, ":true}," as *u8) 126 // HONESTY FIELD: this Statement is not yet wrapped in a signed DSSE envelope. in-toto separates the 127 // Statement (this document) from the envelope (the signature layer), so an unsigned Statement is 128 // legitimate -- but a reader must not assume a signature that does not exist, so the absence is DECLARED 129 // in-band rather than left to be inferred from a missing file. 130 o = pv_cq(J, o, "envelope" as *u8); o = pv_cat(J, o, ":" as *u8) 131 o = pv_cq(J, o, "UNSIGNED-STATEMENT-dsse-envelope-owed" as *u8) 132 o = pv_cat(J, o, "}}" as *u8) 133 J[o] = 10 as u8 134 o = o + 1 135 return o 136}