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}