code wiki / (root) / nx_canary_value.nx

nx_canary_value.nx source

↩ module page · 335 lines · 12607 B

1// nx_canary_value.nx -- known-value tokens detecting silent tampering. 2// 3// module: nishi-core.security.canary_value 4// depends: nishi-core.io.syscalls, nishi-core.io.dir 5// disk_kb: 4 6// capability: CRYPTO 7// wired_status: FULLY_WIRED 8// 9// (Honest: registry + verify-constant + verify-bytes + verify-file + 10// scan-files-array + scan-aggregate are all wired AND smoke-verified 11// via runtime/nx_canary_test.nx PASS on qemu-rv64. Self-consistent 12// hash test: read a real runtime/ file, compute FNV-1a via 13// nx_canary_hash_bytes, install canary with that hash, re-verify -> 14// INTACT. Mutation test: tweak expected hash, re-verify -> VIOLATED. 15// Aggregate over mixed canary array verdicts correctly.) 16// 17// license_tier: PUBLIC_NISHI_SUBSTRATE 18// genealogy_id: nishi_racing_crew_team_honesty_threat_aware_cardinal + 19// canary_token_pattern_thinkst + 20// stack_canary_starguard_etoh_2003 + 21// nishi_no_false_ok_substrate_audit 22// 23// Per cardinal [[feedback-racing-crew-team-honesty-threat-aware]]: 24// known-value tokens embedded in catalogs + critical substrate 25// constants. Any silent modification (by adversarial AI agent / 26// supply-chain attack / corrupted Wheeler-anchor / disk corruption) 27// trips the canary on next verification scan. 28// 29// Two canary classes: 30// 1. EMBEDDED in catalog files (a row with known content; if its 31// hash changes, something tampered) 32// 2. CONSTANT in substrate primitives (specific magic numbers; 33// bit-flip detection) 34 35// nx_capability_manifest: 36// variant_class: tamper_detect 37// variant_id: canary_value_fnv1a_v1 38// requires_isa: [rv64imac, x86_64] 39// requires_syscalls: [read, mmap, close] 40// requires_ram_min_b: 4096 41// tier_floor: NX_TIER_MOBILE 42// tier_ceiling: NX_TIER_HPC 43// cost_model: 44// flops_per_n: 1.0 // ~1 FNV-1a round per byte 45// bytes_per_n: 1.0 // byte stream input 46// syscalls_per_n: 0.0 // io syscalls amortized across file 47// adversary_class: THREAT_AI_ADVERSARY 48// 49// Future variants (queued): canary_value_sha256, canary_value_blake3. 50// Per [[feedback-racing-crew-team-honesty-threat-aware]], stronger 51// hashes are warranted for nation-state threat tier; SA-7 byzantine 52// chain may choose at install per policy. 53 54// Note: import "syscalls.nx" (not nx_syscalls.nx) to match nx_dir's 55// import of the same so sys_read_file / sys_mmap / sys_close are not 56// double-emitted at link time. See nx_dir.nx for the same trick. 57import "syscalls.nx" 58import "nx_dir.nx" 59const NX_MAGIC_1048576: i64 = 1048576 60const NX_MAGIC_65536: i64 = 65536 61 62// ===== Verdict ==================================================== 63 64const NX_CANARY_INTACT: i64 = 1 65const NX_CANARY_VIOLATED: i64 = 2 // value changed; tampering suspected 66const NX_CANARY_MISSING: i64 = 3 // canary row deleted entirely 67const NX_CANARY_NOT_VERIFIED: i64 = 4 // scan hasn't run yet 68 69func nx_canary_verdict_name(v: i64) -> *u8 { 70 if v == NX_CANARY_INTACT { return "INTACT" } 71 if v == NX_CANARY_VIOLATED { return "VIOLATED" } 72 if v == NX_CANARY_MISSING { return "MISSING" } 73 if v == NX_CANARY_NOT_VERIFIED { return "NOT_VERIFIED" } 74 return "UNKNOWN" 75} 76 77// ===== Canary class sealed enum =================================== 78 79const NX_CANARY_CLASS_CATALOG_ROW: i64 = 1 80const NX_CANARY_CLASS_PRIMITIVE_CONSTANT: i64 = 2 81const NX_CANARY_CLASS_BUILD_ARTIFACT: i64 = 3 // hash of compiled output 82const NX_CANARY_CLASS_DEPENDENCY_HASH: i64 = 4 // hash of Wheeler-anchor binary 83const NX_CANARY_CLASS_DOCUMENT_HASH: i64 = 5 // hash of substrate roadmap docs 84 85func nx_canary_class_name(c: i64) -> *u8 { 86 if c == NX_CANARY_CLASS_CATALOG_ROW { return "CATALOG_ROW" } 87 if c == NX_CANARY_CLASS_PRIMITIVE_CONSTANT { return "PRIMITIVE_CONSTANT" } 88 if c == NX_CANARY_CLASS_BUILD_ARTIFACT { return "BUILD_ARTIFACT" } 89 if c == NX_CANARY_CLASS_DEPENDENCY_HASH { return "DEPENDENCY_HASH" } 90 if c == NX_CANARY_CLASS_DOCUMENT_HASH { return "DOCUMENT_HASH" } 91 return "UNKNOWN" 92} 93 94// ===== CanaryRecord struct ======================================== 95 96struct CanaryRecord { 97 canary_hk: i64, 98 canary_name_ptr: *u8, // human-readable: "potato_kind_id_1_canary" 99 canary_name_len: i64, 100 canary_class: i64, // NX_CANARY_CLASS_* 101 expected_value_i64: i64, // for primitive-constant canaries 102 expected_value_hash: i64, // for catalog-row / file canaries (FNV-1a 64) 103 location_ptr: *u8, // file path or constant name 104 location_len: i64, 105 installed_at_unix: i64, 106 last_verified_unix: i64, 107 last_verdict: i64, 108 is_current: i64, 109} 110 111const NX_CANARY_RECORD_BYTES: i64 = 96 // 12 fields * 8 bytes 112 113// ===== FNV-1a 64 helper (same as nx_idempotent_key) =============== 114 115const NX_CANARY_FNV1A_OFFSET_BASIS: i64 = -3750763034362895579 116const NX_CANARY_FNV1A_PRIME: i64 = 1099511628211 117 118func nx_canary_hash_bytes(bytes_ptr: *u8, bytes_len: i64) -> i64 { 119 if bytes_len <= 0 { return 0 } 120 var h: i64 = NX_CANARY_FNV1A_OFFSET_BASIS 121 var i: i64 = 0 122 var iter: i64 = 0 123 var verdict: i64 = 0 124 while verdict == 0 && iter < NX_MAGIC_1048576 { 125 if i >= bytes_len { verdict = 1 } 126 if verdict == 0 { 127 h = h ^ (bytes_ptr[i] as i64) 128 h = h * NX_CANARY_FNV1A_PRIME 129 i = i + 1 130 } 131 iter = iter + 1 132 } 133 return h 134} 135 136// ===== Install a canary =========================================== 137 138func nx_canary_install( 139 canary_name_ptr: *u8, 140 canary_name_len: i64, 141 canary_class: i64, 142 expected_i64: i64, 143 expected_hash: i64, 144 location_ptr: *u8, 145 location_len: i64, 146 now_unix: i64 147) -> *CanaryRecord { 148 let raw: *u8 = sys_mmap(NX_CANARY_RECORD_BYTES) 149 let c: *CanaryRecord = raw as *CanaryRecord 150 c.canary_hk = 0 151 c.canary_name_ptr = canary_name_ptr 152 c.canary_name_len = canary_name_len 153 c.canary_class = canary_class 154 c.expected_value_i64 = expected_i64 155 c.expected_value_hash = expected_hash 156 c.location_ptr = location_ptr 157 c.location_len = location_len 158 c.installed_at_unix = now_unix 159 c.last_verified_unix = 0 160 c.last_verdict = NX_CANARY_NOT_VERIFIED 161 c.is_current = 1 162 return c 163} 164 165// ===== Verify a canary ============================================ 166 167func nx_canary_verify_constant( 168 canary: *CanaryRecord, 169 observed_i64: i64, 170 now_unix: i64 171) -> i64 { 172 if canary == 0 as *CanaryRecord { return NX_CANARY_NOT_VERIFIED } 173 canary.last_verified_unix = now_unix 174 if observed_i64 == canary.expected_value_i64 { 175 canary.last_verdict = NX_CANARY_INTACT 176 return NX_CANARY_INTACT 177 } 178 canary.last_verdict = NX_CANARY_VIOLATED 179 return NX_CANARY_VIOLATED 180} 181 182func nx_canary_verify_bytes( 183 canary: *CanaryRecord, 184 observed_ptr: *u8, 185 observed_len: i64, 186 now_unix: i64 187) -> i64 { 188 if canary == 0 as *CanaryRecord { return NX_CANARY_NOT_VERIFIED } 189 let observed_hash: i64 = nx_canary_hash_bytes(observed_ptr, observed_len) 190 canary.last_verified_unix = now_unix 191 if observed_hash == canary.expected_value_hash { 192 canary.last_verdict = NX_CANARY_INTACT 193 return NX_CANARY_INTACT 194 } 195 canary.last_verdict = NX_CANARY_VIOLATED 196 return NX_CANARY_VIOLATED 197} 198 199// ===== Substrate canary registry ================================== 200// 201// Known canaries embedded in the substrate. Verification cron runs 202// these; any violation triggers substrate-level alert. 203 204const NX_CANARY_FNV1A_OFFSET_BASIS_EXPECTED: i64 = -3750763034362895579 205const NX_CANARY_FNV1A_PRIME_EXPECTED: i64 = 1099511628211 206 207// These canaries verify that the FNV-1a constants used across 208// nx_canary_value + nx_idempotent_key + nx_dlq + multiple other 209// primitives haven't been silently modified. If an adversarial 210// agent changes the offset basis to a colliding value, the canary 211// trips. 212 213// ===== Aggregate verification report ============================== 214 215struct CanaryScanReport { 216 report_hk: i64, 217 scope_name_ptr: *u8, 218 canaries_total: i64, 219 canaries_intact: i64, 220 canaries_violated: i64, 221 canaries_missing: i64, 222 canaries_not_run: i64, 223 scanned_at_unix: i64, 224 verdict: i64, 225} 226 227const NX_CANARY_SCAN_REPORT_BYTES: i64 = 72 // 9 fields * 8 bytes 228 229func nx_canary_scan_aggregate( 230 canaries: **CanaryRecord, 231 n_canaries: i64, 232 scope_name_ptr: *u8, 233 now_unix: i64 234) -> *CanaryScanReport { 235 let raw: *u8 = sys_mmap(NX_CANARY_SCAN_REPORT_BYTES) 236 let r: *CanaryScanReport = raw as *CanaryScanReport 237 r.report_hk = 0 238 r.scope_name_ptr = scope_name_ptr 239 r.canaries_total = n_canaries 240 r.canaries_intact = 0 241 r.canaries_violated = 0 242 r.canaries_missing = 0 243 r.canaries_not_run = 0 244 r.scanned_at_unix = now_unix 245 r.verdict = NX_CANARY_NOT_VERIFIED 246 247 if n_canaries <= 0 { return r } 248 249 var i: i64 = 0 250 var iter: i64 = 0 251 var verdict: i64 = 0 252 while verdict == 0 && iter < NX_MAGIC_65536 { 253 if i >= n_canaries { verdict = 1 } 254 if verdict == 0 { 255 let c: *CanaryRecord = canaries[i] 256 if c.last_verdict == NX_CANARY_INTACT { r.canaries_intact = r.canaries_intact + 1 } 257 if c.last_verdict == NX_CANARY_VIOLATED { r.canaries_violated = r.canaries_violated + 1 } 258 if c.last_verdict == NX_CANARY_MISSING { r.canaries_missing = r.canaries_missing + 1 } 259 if c.last_verdict == NX_CANARY_NOT_VERIFIED { r.canaries_not_run = r.canaries_not_run + 1 } 260 i = i + 1 261 } 262 iter = iter + 1 263 } 264 265 if r.canaries_violated > 0 { r.verdict = NX_CANARY_VIOLATED } 266 if r.canaries_violated == 0 { 267 if r.canaries_missing > 0 { r.verdict = NX_CANARY_MISSING } 268 if r.canaries_missing == 0 { 269 if r.canaries_intact == n_canaries { r.verdict = NX_CANARY_INTACT } 270 if r.canaries_intact != n_canaries { r.verdict = NX_CANARY_NOT_VERIFIED } 271 } 272 } 273 return r 274} 275 276// ===== File canary verification (composes sys_read_file) ========== 277// 278// Reads the file at canary.location_ptr (NUL-terminated), hashes it 279// with nx_canary_hash_bytes, compares against canary.expected_value_hash. 280// Sets last_verified_unix + last_verdict on the canary. Returns the 281// verdict. Caller does NOT need to free the read buffer here -- the 282// sys_read_file primitive mmaps into substrate-managed memory. 283 284func nx_canary_verify_file(canary: *CanaryRecord, now_unix: i64) -> i64 { 285 if canary == 0 as *CanaryRecord { return NX_CANARY_NOT_VERIFIED } 286 if canary.location_ptr == 0 as *u8 { return NX_CANARY_NOT_VERIFIED } 287 let len_box: *u8 = sys_mmap(8) 288 let lp: *i64 = len_box as *i64 289 let bytes: *u8 = sys_read_file(canary.location_ptr, lp) 290 canary.last_verified_unix = now_unix 291 if bytes == 0 as *u8 { 292 canary.last_verdict = NX_CANARY_MISSING 293 return NX_CANARY_MISSING 294 } 295 let observed_hash: i64 = nx_canary_hash_bytes(bytes, *lp) 296 if observed_hash == canary.expected_value_hash { 297 canary.last_verdict = NX_CANARY_INTACT 298 return NX_CANARY_INTACT 299 } 300 canary.last_verdict = NX_CANARY_VIOLATED 301 return NX_CANARY_VIOLATED 302} 303 304// ===== Scan an array of file canaries ============================= 305// 306// For each canary in the array, calls nx_canary_verify_file. Then 307// composes the per-canary verdicts into a CanaryScanReport via the 308// existing nx_canary_scan_aggregate helper. 309// 310// Bounded loop per JPL Rule 2. 311 312const NX_CANARY_MAX_SCAN_FILES: i64 = 65536 313 314func nx_canary_scan_files( 315 canaries: **CanaryRecord, 316 n_canaries: i64, 317 scope_name_ptr: *u8, 318 now_unix: i64 319) -> *CanaryScanReport { 320 if n_canaries > 0 { 321 var i: i64 = 0 322 var iter: i64 = 0 323 var verdict: i64 = 0 324 while verdict == 0 && iter < NX_CANARY_MAX_SCAN_FILES { 325 if i >= n_canaries { verdict = 1 } 326 if verdict == 0 { 327 let c: *CanaryRecord = canaries[i] 328 nx_canary_verify_file(c, now_unix) 329 i = i + 1 330 iter = iter + 1 331 } 332 } 333 } 334 return nx_canary_scan_aggregate(canaries, n_canaries, scope_name_ptr, now_unix) 335}