nx_provstmt.nx source
↩ module page · 141 lines · 6273 B
1// nx_provstmt.nx -- CR3 of /compare/capregistry: ATTEST THE PROMOTED ARTIFACT, NOT THE EVIDENCE FRAME.
2//
3// nx_provstmt <target> <artifact_sha256> <src_sha256> <closure_sha256> [out.json]
4//
5// WHERE THE ARGUMENTS COME FROM, because they are the whole point: /api/build's response carries src_sha256
6// and closure_sha256, and /api/promote's response carries the live artifact sha256 after the rename. The
7// estate computes real SLSA build provenance on every build and every caller discards it at the end of the
8// HTTP response. This organ is the missing last step: bind those already-computed digests to the artifact
9// they describe and write the in-toto ITE-6 Statement down.
10//
11// THE POLICY THAT MAKES IT SAFE TO RUN LATE: the organ RE-HASHES THE LIVE ARTIFACT ITSELF and refuses when
12// the computed digest differs from the receipt's. Without that check, a stale receipt -- from before a
13// sibling's re-promote, or from a build that never went live -- would mint a Statement binding provenance to
14// bytes that are NOT being served: a false attestation with a valid shape, which is worse than none.
15// The in-toto spec matches subjects PURELY BY DIGEST, so the digest being right is the entire contract.
16//
17// EXITS: 0 statement written, receipt printed | 1 ATTEST-REFUSED, live artifact hash differs from the
18// receipt (both digests printed; NOTHING written) | 2 usage / malformed digest (names WHICH argument) |
19// 3 UNPROVEN, live artifact unreadable -- an absent artifact must abstain, never attest.
20
21import "nx_syscalls.nx"
22import "nx_sha256.nx"
23import "_hdl_build/nx_provstmt_lib.nx"
24
25const PVO_NAME_CAP: i64 = 128
26const PVO_PATH_CAP: i64 = 512
27
28func pvo_puts(s: *u8) -> i64 {
29 var n: i64 = 0
30 while s[n] != (0 as u8) { n = n + 1 }
31 sys_write(1, s, n)
32 return 0
33}
34
35func pvo_len(s: *u8) -> i64 {
36 var n: i64 = 0
37 while s[n] != (0 as u8) { n = n + 1 }
38 return n
39}
40
41func pvo_mk(dst: *u8, a: *u8, b: *u8) -> i64 {
42 var p: i64 = 0
43 var i: i64 = 0
44 while a[i] != (0 as u8) { dst[p] = a[i]; p = p + 1; i = i + 1 }
45 i = 0
46 while b[i] != (0 as u8) { dst[p] = b[i]; p = p + 1; i = i + 1 }
47 dst[p] = 0 as u8
48 return p
49}
50
51func main(argc: i64, argv: *i64) -> i64 {
52 if argc < 5 {
53 pvo_puts("usage: nx_provstmt <target> <artifact_sha256> <src_sha256> <closure_sha256> [out.json]\n digests come from the /api/build and /api/promote receipts of the SAME ship -- this organ re-hashes\n the live artifact and refuses when the receipt does not match what is actually being served\n" as *u8)
54 return 2
55 }
56 let target: *u8 = argv[1] as *u8
57 let art_sha: *u8 = argv[2] as *u8
58 let src_sha: *u8 = argv[3] as *u8
59 let clo_sha: *u8 = argv[4] as *u8
60 if pvo_len(target) >= PVO_NAME_CAP {
61 pvo_puts("PROVSTMT usage: target name exceeds the name cap\n" as *u8)
62 return 2
63 }
64 // NAME WHICH ARGUMENT: three digests, three separate refusals, or a caller who swapped two of them
65 // spends the investigation on the wrong one.
66 if pv_hex_ok(art_sha) == 0 { pvo_puts("PROVSTMT usage: artifact_sha256 is not 64 lowercase hex chars\n" as *u8); return 2 }
67 if pv_hex_ok(src_sha) == 0 { pvo_puts("PROVSTMT usage: src_sha256 is not 64 lowercase hex chars\n" as *u8); return 2 }
68 if pv_hex_ok(clo_sha) == 0 { pvo_puts("PROVSTMT usage: closure_sha256 is not 64 lowercase hex chars\n" as *u8); return 2 }
69
70 // ---- re-hash the LIVE artifact: the subject is what is being served, never what a receipt remembers ----
71 let path: *u8 = sys_mmap(PVO_PATH_CAP)
72 pvo_mk(path, target, ".elf" as *u8)
73 let lp: *i64 = sys_mmap(16) as *i64
74 lp[0] = 0
75 let body: *u8 = sys_read_file(path, lp)
76 let bn: i64 = lp[0]
77 if bn <= 0 {
78 pvo_puts("PROVSTMT UNPROVEN -- live artifact unreadable: " as *u8)
79 pvo_puts(path)
80 pvo_puts("\n An artifact that cannot be read cannot be attested; refusing is not the same as failing.\n" as *u8)
81 return 3
82 }
83 let dig: *u8 = sys_mmap(40)
84 sha256_digest(body, bn, dig)
85 sys_free_file(body, bn)
86 let live_hex: *u8 = sys_mmap(72)
87 pv_hex_of(dig, live_hex)
88
89 if pv_hex_eq(live_hex, art_sha) == 0 {
90 pvo_puts("PROVSTMT ATTEST-REFUSED -- the live artifact is not the one the receipt describes\n receipt " as *u8)
91 pvo_puts(art_sha)
92 pvo_puts("\n live " as *u8)
93 pvo_puts(live_hex)
94 pvo_puts("\n Binding provenance to bytes that are not being served would be a false attestation with a\n valid shape, which is worse than none. Re-ship, or pass the receipt from the ship that is live.\n NOTHING was written.\n" as *u8)
95 return 1
96 }
97
98 // ---- serialise ----
99 let J: *u8 = sys_mmap(PV_STMT_CAP)
100 let jn: i64 = pv_build(J, target, art_sha, src_sha, clo_sha)
101 if jn <= 0 {
102 pvo_puts("PROVSTMT usage: statement build refused its inputs\n" as *u8)
103 return 2
104 }
105 var opath: *u8 = sys_mmap(PVO_PATH_CAP)
106 if argc > 5 {
107 opath = argv[5] as *u8
108 } else {
109 let pfx: *u8 = sys_mmap(PVO_PATH_CAP)
110 pvo_mk(pfx, "knowledge/status/provstmt_" as *u8, target)
111 pvo_mk(opath, pfx, ".intoto.json" as *u8)
112 }
113 let fd: i64 = sys_openat_wr(opath, MODE_0644)
114 var wrote: i64 = 0
115 if fd >= 0 {
116 wrote = sys_write(fd, J, jn)
117 sys_close(fd)
118 }
119 if wrote != jn {
120 pvo_puts("PROVSTMT UNPROVEN -- statement could not be written whole (wrote/expected differ); a partial\n statement on disk would be worse than none, but truncate-open means the partial may exist: re-run.\n" as *u8)
121 return 3
122 }
123
124 pvo_puts("PROVSTMT OK subject=" as *u8)
125 pvo_puts(target)
126 pvo_puts(".elf digest-verified-against-live=1 bytes=" as *u8)
127 let nb: *u8 = sys_mmap(32)
128 var x: i64 = jn
129 var nn: i64 = 0
130 if x == 0 { nb[0] = 48 as u8; nn = 1 }
131 while x > 0 { nb[nn] = ((x % 10) + 48) as u8; x = x / 10; nn = nn + 1 }
132 var y: i64 = nn
133 let ob: *u8 = sys_mmap(32)
134 var oo: i64 = 0
135 while y > 0 { y = y - 1; ob[oo] = nb[y]; oo = oo + 1 }
136 sys_write(1, ob, oo)
137 pvo_puts(" of=" as *u8)
138 pvo_puts(opath)
139 pvo_puts("\n UNSIGNED STATEMENT: the DSSE envelope is owed and its absence is declared inside the JSON too.\n" as *u8)
140 return 0
141}