code wiki / (root) / nx_install_hash.nx

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}