nx_install_hash.nx source
↩ module page · 210 lines · 8467 B
1// nx_install_hash.nx -- compute install_hash from NxInstallPlan via
2// canonical serialization + SHA-256.
3//
4// SA-7 partial milestone of NISHI_SELF_ASSEMBLY_ROADMAP.md. The
5// install_hash is the racing-line "ProvenanceLink" per
6// [[feedback-end-to-end-bit-traceability-architecture]]: every
7// install attestation carries a hash that future audits can
8// reproduce by replaying the selector against the recorded inputs.
9//
10// install_hash = SHA-256(canonical_serialize(plan))
11//
12// Canonical serialization rules:
13// 1. Magic tag "NXIPLN01" (8 bytes) so plans of different schema
14// versions cannot collide
15// 2. Every fixed i64 field written little-endian, in declared
16// struct order
17// 3. EXCLUDED: canary_pre + canary_post (tamper-detect, not
18// content); ts_us (timestamp varies across runs but inputs same)
19// 4. selected_variant_ids array written after n_selected (which
20// acts as the length prefix), each i64 little-endian
21//
22// Reproducibility gate (the SA-7 verification): same inputs
23// (probe + calibration + policy + selection) produce bit-equal
24// install_hash. Differences in ts_us or canaries do NOT affect
25// the hash (excluded by design).
26//
27// Sensitivity gate: any change to a content field (probe, calibration,
28// policy, selection) produces a different install_hash. This makes
29// the install_hash a sensitive integrity check, not just a
30// deterministic value.
31//
32// Refuses to hash a tampered plan: nx_install_plan_is_valid must
33// return 1 before serialization; otherwise returns
34// NX_HASH_BAD_PLAN. Canary tamper is a substrate-honesty signal,
35// not something to silently hash over.
36//
37// Deferred per roadmap:
38// - in-toto link metadata serialization with the hash chained
39// to source.nx file hashes (SA-7 remainder)
40// - Rekor-class append-only audit log local-by-default (SA-7
41// remainder)
42// - Multi-substrate co-residence via install_hash as primary key
43// (SA-7 remainder)
44// - Replay-from-plan verifier that re-runs the selector against
45// recorded inputs + asserts the new install_hash matches the
46// attested one (SA-7 remainder)
47//
48// genealogy_id: fips_180_4_sha256 + reproducible_builds_2013 +
49// in_toto_2018 + slsa_2021 + cardinal_2026-05-19_self_assembly
50// lineage_id: substrate_install_hash_v1
51//
52// nx_capability_manifest:
53// variant_class: install_hash_compute
54// variant_id: install_hash_sha256_v1
55// requires_isa: [rv64imac, x86_64]
56// requires_syscalls: [mmap]
57// requires_ram_min_b: 8192
58// tier_floor: NX_TIER_MOBILE
59// tier_ceiling: NX_TIER_HPC
60// cost_model:
61// flops_per_n: 50.0 // SHA-256 round cost per 64-byte block
62// bytes_per_n: 256.0 // serialized plan size dominates
63// syscalls_per_n: 0.0
64// adversary_class: THREAT_AI_ADVERSARY
65//
66// nx_safety_envelope:
67// intended_use: "Content-addressed install_hash for SA-7
68// attestation; canary-gated; reproducible by
69// construction"
70// sil_target: SIL2
71// evidence: [canary_gated, deterministic_serialization,
72// ts_excluded_for_replay, sensitivity_per_field]
73// verdict: NOT_YET_EVALUATED
74
75import "nx_syscalls.nx"
76import "nx_install_plan.nx"
77import "nx_sha256.nx"
78
79// ===== Verdict sealed enum =======================================
80const NX_HASH_OK: i64 = 0
81const NX_HASH_BAD_PLAN: i64 = 1
82const NX_HASH_BAD_OUT: i64 = 2
83
84// ===== Constants =================================================
85const NX_INSTALL_HASH_BYTES: i64 = 32 // SHA-256 digest size
86const NX_INSTALL_HASH_BUF_SIZE: i64 = 4096 // serialization scratch
87
88// Magic tag "NXIPLN01" emitted at the start of the serialized form.
89// Bumping the trailing version digit invalidates prior hashes.
90
91// ===== Little-endian i64 store ===================================
92// Writes 8 bytes of v in little-endian order starting at buf[off].
93// Returns the next offset (off + 8).
94func _store_i64_le(buf: *u8, off: i64, v: i64) -> i64 {
95 buf[off + 0] = (v & 255) as u8
96 buf[off + 1] = ((v >> 8) & 255) as u8
97 buf[off + 2] = ((v >> 16) & 255) as u8
98 buf[off + 3] = ((v >> 24) & 255) as u8
99 buf[off + 4] = ((v >> 32) & 255) as u8
100 buf[off + 5] = ((v >> 40) & 255) as u8
101 buf[off + 6] = ((v >> 48) & 255) as u8
102 buf[off + 7] = ((v >> 56) & 255) as u8
103 return off + 8
104}
105
106// Write the 8-byte magic tag "NXIPLN01".
107func _store_magic_tag(buf: *u8, off: i64) -> i64 {
108 buf[off + 0] = 78 as u8 // 'N'
109 buf[off + 1] = 88 as u8 // 'X'
110 buf[off + 2] = 73 as u8 // 'I'
111 buf[off + 3] = 80 as u8 // 'P'
112 buf[off + 4] = 76 as u8 // 'L'
113 buf[off + 5] = 78 as u8 // 'N'
114 buf[off + 6] = 48 as u8 // '0'
115 buf[off + 7] = 49 as u8 // '1'
116 return off + 8
117}
118
119// ===== Canonical serialization ==================================
120// Returns the byte length written to buf, or -1 on bad input.
121// EXCLUDED fields: canary_pre, canary_post, ts_us.
122
123func nx_install_plan_serialize(plan: *NxInstallPlan, buf: *u8) -> i64 {
124 if (plan as i64) == 0 { return -1 }
125 if (buf as i64) == 0 { return -1 }
126 if nx_install_plan_is_valid(plan) != 1 { return -1 }
127
128 var off: i64 = 0
129 off = _store_magic_tag(buf, off)
130 off = _store_i64_le(buf, off, plan.schema_version)
131
132 // Probe snapshot (7 fields).
133 off = _store_i64_le(buf, off, plan.probe_isa_family)
134 off = _store_i64_le(buf, off, plan.probe_endianness)
135 off = _store_i64_le(buf, off, plan.probe_pointer_width_bits)
136 off = _store_i64_le(buf, off, plan.probe_page_size_bytes)
137 off = _store_i64_le(buf, off, plan.probe_mmap_works)
138 off = _store_i64_le(buf, off, plan.probe_write_works)
139 off = _store_i64_le(buf, off, plan.probe_mono_clock_works)
140
141 // Calibration snapshot (7 fields).
142 off = _store_i64_le(buf, off, plan.calib_int_alu_ps_per_op)
143 off = _store_i64_le(buf, off, plan.calib_mem_ns_l1)
144 off = _store_i64_le(buf, off, plan.calib_mem_ns_l2)
145 off = _store_i64_le(buf, off, plan.calib_mem_ns_ram)
146 off = _store_i64_le(buf, off, plan.calib_mem_bw_mib_per_s)
147 off = _store_i64_le(buf, off, plan.calib_syscall_ns_gettime)
148 off = _store_i64_le(buf, off, plan.calib_inferred_tier)
149
150 // Policy snapshot (5 fields).
151 off = _store_i64_le(buf, off, plan.policy_weight_throughput)
152 off = _store_i64_le(buf, off, plan.policy_weight_bytes)
153 off = _store_i64_le(buf, off, plan.policy_weight_syscalls)
154 off = _store_i64_le(buf, off, plan.policy_weight_energy)
155 off = _store_i64_le(buf, off, plan.policy_weight_ram)
156
157 // Selection result (3 fields).
158 off = _store_i64_le(buf, off, plan.n_selected)
159 off = _store_i64_le(buf, off, plan.total_cost_q10)
160 off = _store_i64_le(buf, off, plan.selection_verdict)
161
162 // Variant IDs array (length-prefixed by n_selected above).
163 if plan.n_selected > 0 {
164 if (plan.selected_variant_ids as i64) == 0 { return -1 }
165 var i: i64 = 0
166 while i < plan.n_selected {
167 off = _store_i64_le(buf, off, plan.selected_variant_ids[i])
168 i = i + 1
169 }
170 }
171
172 return off
173}
174
175// ===== install_hash compute ======================================
176// Writes 32 bytes (SHA-256 digest) into out_digest. Returns
177// NX_HASH_OK on success; refuses to hash a tampered plan.
178
179func nx_install_hash_compute(plan: *NxInstallPlan, out_digest: *u8) -> i64 {
180 if (out_digest as i64) == 0 { return NX_HASH_BAD_OUT }
181 if (plan as i64) == 0 { return NX_HASH_BAD_PLAN }
182 if nx_install_plan_is_valid(plan) != 1 { return NX_HASH_BAD_PLAN }
183
184 let buf: *u8 = sys_mmap(NX_INSTALL_HASH_BUF_SIZE)
185 let n: i64 = nx_install_plan_serialize(plan, buf)
186 if n < 0 { return NX_HASH_BAD_PLAN }
187
188 sha256_digest(buf, n, out_digest)
189 return NX_HASH_OK
190}
191
192// ===== Digest comparison =========================================
193// Returns 1 if the two 32-byte digests are bit-equal; 0 otherwise.
194// Constant-time comparison NOT required for content-addressing
195// (no secrets here) but still uses unconditional iteration.
196
197func nx_install_hash_eq(a: *u8, b: *u8) -> i64 {
198 if (a as i64) == 0 { return 0 }
199 if (b as i64) == 0 { return 0 }
200 var diff: i64 = 0
201 var i: i64 = 0
202 while i < NX_INSTALL_HASH_BYTES {
203 let da: i64 = (a[i] as i64) & 255
204 let db: i64 = (b[i] as i64) & 255
205 diff = diff | (da ^ db)
206 i = i + 1
207 }
208 if diff == 0 { return 1 }
209 return 0
210}