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}