nx_emitted_substrate.nx source
↩ module page · 472 lines · 18663 B
1// nx_emitted_substrate.nx -- Phase 5 (Reproduce) substrate emission.
2//
3// Closes the ecosystem evolution lifecycle: a mature substrate
4// (Phase 4 seed-ready) emits a SIGNED expected-blob-set that
5// downstream devices consume in Phase 2 (Network) via
6// nx_substrate_manifest_ingest.
7//
8// Per NISHI_ECOSYSTEM_EVOLUTION_ROADMAP.md Phase 5:
9// Mature -> nx_emitted_substrate_sign(record, operator_priv)
10// -> transport (sneakernet / USB / serial / HTTPS / spore)
11// -> Downstream receiver verifies sig + populates its
12// nx_substrate_manifest from the record
13// -> Ingests blobs via nx_substrate_manifest_ingest until
14// is_complete
15//
16// The record is the DECLARATION; the bytes-on-wire transport is
17// transport-agnostic (the same record content goes over any
18// medium). Bytes-identical canonical serialization means the
19// signature works across any transport.
20//
21// Defends against:
22// - Adversary in the middle who replaces an expected hash ->
23// signature verification fails (downstream refuses ingest)
24// - Adversary who substitutes the emitter's identity ->
25// install_hash mismatch caught when downstream cross-checks
26// against the operator's known good install_hashes
27// - Stale spore replay -> ts_emit_us in canonical bytes; if
28// paired with a freshness window, replays caught
29//
30// V1 scope:
31// - Up to NX_EMIT_MAX_EXPECTED = 16 expected hashes inline
32// - Single Ed25519 signature (threshold queued for SA-7)
33// - Canonical serialization same shape as nx_install_plan
34// (magic tag + LE i64 fields + length-prefixed hash array)
35// - Round-trip from record into a fresh NxSubstrateManifest
36//
37// Deferred:
38// - Larger N>16 (composes with Merkle root rather than flat set)
39// - Multi-signature (threshold / N-of-M peers must co-sign)
40// - Post-quantum migration (Ed25519 -> ML-DSA queued)
41// - Bootstrap-code payload (the actual Stage0 hex0 seed bytes
42// are out-of-band for V1; SA-9 wires them in)
43//
44// genealogy_id: in_toto_link_metadata_2018 + slsa_provenance_2021 +
45// rfc_5280_x509_signed_data + bittorrent_torrent_file_2001 +
46// cardinal_2026-05-20_ecosystem_evolution
47// lineage_id: substrate_emitted_substrate_v1
48//
49// nx_capability_manifest:
50// variant_class: emitted_substrate
51// variant_id: emitted_substrate_v1_ed25519_flat_set
52// requires_isa: [rv64imac, x86_64, cortex_m, armv7a, aarch64]
53// requires_syscalls: [mmap, clock_gettime_mono]
54// requires_ram_min_b: 4096
55// tier_floor: NX_TIER_INF_MOBILE
56// tier_ceiling: NX_TIER_INF_HPC
57// cost_model:
58// flops_per_n: 50000.0 // Ed25519 sign cost dominates
59// bytes_per_n: 128.0 // record header + per-hash 32 B
60// syscalls_per_n: 1.0 // one clock_gettime
61// adversary_class: THREAT_AI_ADVERSARY
62//
63// nx_safety_envelope:
64// intended_use: "Phase 5 (Reproduce) substrate emission;
65// signed expected-blob-set for downstream
66// consumption"
67// sil_target: SIL3
68// evidence: [canary_bracketed, canonical_serialization,
69// ed25519_signed, tamper_detected,
70// transport_agnostic]
71// verdict: NOT_YET_EVALUATED
72
73import "nx_syscalls.nx"
74import "nx_sha256.nx"
75import "nx_ed25519_signature.nx"
76import "nx_blob_store.nx"
77import "nx_install_hash.nx"
78import "nx_substrate_manifest.nx"
79const NX_MAGIC_1000000: i64 = 1000000
80const NX_MAGIC_4096: i64 = 4096
81
82// ===== Constants =================================================
83const NX_EMIT_MAX_EXPECTED: i64 = 16
84const NX_EMIT_SCHEMA_VERSION: i64 = 1
85const NX_EMIT_HEADER_BYTES: i64 = 80 // pre-hashes header (10 i64s)
86const NX_EMIT_SIG_BYTES: i64 = 64
87const NX_EMIT_PRIV_BYTES: i64 = 32
88const NX_EMIT_PUB_BYTES: i64 = 32
89
90// Verdicts.
91const NX_EMIT_OK: i64 = 0
92const NX_EMIT_BAD_INPUT: i64 = 1
93const NX_EMIT_FULL: i64 = 2
94const NX_EMIT_TAMPER: i64 = 3
95const NX_EMIT_BAD_SIG: i64 = 4
96const NX_EMIT_DUPLICATE: i64 = 5
97const NX_EMIT_N: i64 = 6
98
99func nx_emit_verdict_is_valid(v: i64) -> i64 {
100 if v < 0 { return 0 }
101 if v >= NX_EMIT_N { return 0 }
102 return 1
103}
104
105// Canaries distinct from all prior primitives.
106const NX_EMIT_CANARY_PRE: i64 = 0x4E58454D49545000 // "NXEMITP\0"
107const NX_EMIT_CANARY_POST: i64 = 0x4E58454D4954454E // "NXEMITEN"
108
109// ===== NxEmittedSubstrate =======================================
110//
111// emitter_install_hash inlined as 4 i64 (32 bytes LE-packed).
112// target_tier + target_isa carried separately so receiver can
113// reject records emitted for incompatible targets BEFORE signature
114// verification (cheaper rejection path).
115//
116// expected_hashes_ptrs is *i64 array of NxBlobHash pointers; each
117// pointed-at hash is deep-copied when added so the record survives
118// caller-side hash deallocation.
119//
120// sig_w0..w7 holds the Ed25519 signature (64 bytes LE-packed across
121// 8 i64). Zero until sign() runs.
122
123struct NxEmittedSubstrate {
124 canary_pre: i64,
125 schema_version: i64,
126 target_tier: i64,
127 target_isa: i64,
128 ts_emit_us: i64,
129 emitter_install_hash_w0: i64,
130 emitter_install_hash_w1: i64,
131 emitter_install_hash_w2: i64,
132 emitter_install_hash_w3: i64,
133 n_expected: i64,
134 max_expected: i64,
135 expected_hashes_ptrs: *i64, // *i64 array of NxBlobHash pointers
136 sig_w0: i64,
137 sig_w1: i64,
138 sig_w2: i64,
139 sig_w3: i64,
140 sig_w4: i64,
141 sig_w5: i64,
142 sig_w6: i64,
143 sig_w7: i64,
144 is_signed: i64, // 0 / 1
145 canary_post: i64,
146}
147
148// ===== Little-endian helpers ====================================
149func _emit_store_i64_le(buf: *u8, off: i64, v: i64) -> i64 {
150 buf[off + 0] = (v & 255) as u8
151 buf[off + 1] = ((v >> 8) & 255) as u8
152 buf[off + 2] = ((v >> 16) & 255) as u8
153 buf[off + 3] = ((v >> 24) & 255) as u8
154 buf[off + 4] = ((v >> 32) & 255) as u8
155 buf[off + 5] = ((v >> 40) & 255) as u8
156 buf[off + 6] = ((v >> 48) & 255) as u8
157 buf[off + 7] = ((v >> 56) & 255) as u8
158 return off + 8
159}
160
161// Magic tag "NXEMTSUB" emitted at the start of canonical bytes.
162func _emit_store_magic(buf: *u8, off: i64) -> i64 {
163 buf[off + 0] = 78 as u8 // 'N'
164 buf[off + 1] = 88 as u8 // 'X'
165 buf[off + 2] = 69 as u8 // 'E'
166 buf[off + 3] = 77 as u8 // 'M'
167 buf[off + 4] = 84 as u8 // 'T'
168 buf[off + 5] = 83 as u8 // 'S'
169 buf[off + 6] = 85 as u8 // 'U'
170 buf[off + 7] = 66 as u8 // 'B'
171 return off + 8
172}
173
174// ===== Construction =============================================
175func nx_emitted_substrate_new(
176 emitter_install_hash: *u8, // 32-byte digest (LE-packed source)
177 target_tier: i64,
178 target_isa: i64
179) -> *NxEmittedSubstrate {
180 if (emitter_install_hash as i64) == 0 { return (0 as i64) as *NxEmittedSubstrate }
181 let r: *NxEmittedSubstrate = (sys_mmap(192)) as *NxEmittedSubstrate
182 r.canary_pre = NX_EMIT_CANARY_PRE
183 r.canary_post = NX_EMIT_CANARY_POST
184 r.schema_version = NX_EMIT_SCHEMA_VERSION
185 r.target_tier = target_tier
186 r.target_isa = target_isa
187 // Timestamp.
188 let ts: *i64 = (sys_mmap(16)) as *i64
189 let rc_ts: i64 = sys_clock_gettime_mono(ts)
190 if rc_ts == 0 {
191 r.ts_emit_us = (ts[0] * NX_MAGIC_1000000) + (ts[1] / 1000)
192 } else {
193 r.ts_emit_us = 0
194 }
195 // Read emitter_install_hash as 4 i64 LE.
196 let b0: i64 = (emitter_install_hash[0] as i64) & 255
197 let b1: i64 = (emitter_install_hash[1] as i64) & 255
198 let b2: i64 = (emitter_install_hash[2] as i64) & 255
199 let b3: i64 = (emitter_install_hash[3] as i64) & 255
200 let b4: i64 = (emitter_install_hash[4] as i64) & 255
201 let b5: i64 = (emitter_install_hash[5] as i64) & 255
202 let b6: i64 = (emitter_install_hash[6] as i64) & 255
203 let b7: i64 = (emitter_install_hash[7] as i64) & 255
204 r.emitter_install_hash_w0 = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
205 | (b4 << 32) | (b5 << 40) | (b6 << 48) | (b7 << 56)
206 let c0: i64 = (emitter_install_hash[8] as i64) & 255
207 let c1: i64 = (emitter_install_hash[9] as i64) & 255
208 let c2: i64 = (emitter_install_hash[10] as i64) & 255
209 let c3: i64 = (emitter_install_hash[11] as i64) & 255
210 let c4: i64 = (emitter_install_hash[12] as i64) & 255
211 let c5: i64 = (emitter_install_hash[13] as i64) & 255
212 let c6: i64 = (emitter_install_hash[14] as i64) & 255
213 let c7: i64 = (emitter_install_hash[15] as i64) & 255
214 r.emitter_install_hash_w1 = c0 | (c1 << 8) | (c2 << 16) | (c3 << 24)
215 | (c4 << 32) | (c5 << 40) | (c6 << 48) | (c7 << 56)
216 let d0: i64 = (emitter_install_hash[16] as i64) & 255
217 let d1: i64 = (emitter_install_hash[17] as i64) & 255
218 let d2: i64 = (emitter_install_hash[18] as i64) & 255
219 let d3: i64 = (emitter_install_hash[19] as i64) & 255
220 let d4: i64 = (emitter_install_hash[20] as i64) & 255
221 let d5: i64 = (emitter_install_hash[21] as i64) & 255
222 let d6: i64 = (emitter_install_hash[22] as i64) & 255
223 let d7: i64 = (emitter_install_hash[23] as i64) & 255
224 r.emitter_install_hash_w2 = d0 | (d1 << 8) | (d2 << 16) | (d3 << 24)
225 | (d4 << 32) | (d5 << 40) | (d6 << 48) | (d7 << 56)
226 let e0: i64 = (emitter_install_hash[24] as i64) & 255
227 let e1: i64 = (emitter_install_hash[25] as i64) & 255
228 let e2: i64 = (emitter_install_hash[26] as i64) & 255
229 let e3: i64 = (emitter_install_hash[27] as i64) & 255
230 let e4: i64 = (emitter_install_hash[28] as i64) & 255
231 let e5: i64 = (emitter_install_hash[29] as i64) & 255
232 let e6: i64 = (emitter_install_hash[30] as i64) & 255
233 let e7: i64 = (emitter_install_hash[31] as i64) & 255
234 r.emitter_install_hash_w3 = e0 | (e1 << 8) | (e2 << 16) | (e3 << 24)
235 | (e4 << 32) | (e5 << 40) | (e6 << 48) | (e7 << 56)
236 r.n_expected = 0
237 r.max_expected = NX_EMIT_MAX_EXPECTED
238 r.expected_hashes_ptrs = (sys_mmap(NX_EMIT_MAX_EXPECTED * 8)) as *i64
239 var k: i64 = 0
240 while k < NX_EMIT_MAX_EXPECTED {
241 r.expected_hashes_ptrs[k] = 0
242 k = k + 1
243 }
244 r.sig_w0 = 0
245 r.sig_w1 = 0
246 r.sig_w2 = 0
247 r.sig_w3 = 0
248 r.sig_w4 = 0
249 r.sig_w5 = 0
250 r.sig_w6 = 0
251 r.sig_w7 = 0
252 r.is_signed = 0
253 return r
254}
255
256// ===== Validity gate ============================================
257func nx_emitted_substrate_is_valid(r: *NxEmittedSubstrate) -> i64 {
258 if (r as i64) == 0 { return 0 }
259 if r.canary_pre != NX_EMIT_CANARY_PRE { return 0 }
260 if r.canary_post != NX_EMIT_CANARY_POST { return 0 }
261 if r.schema_version != NX_EMIT_SCHEMA_VERSION { return 0 }
262 if r.n_expected < 0 { return 0 }
263 if r.n_expected > r.max_expected { return 0 }
264 return 1
265}
266
267// ===== Add an expected hash =====================================
268func nx_emitted_substrate_add_expected(
269 r: *NxEmittedSubstrate,
270 hash: *NxBlobHash
271) -> i64 {
272 if nx_emitted_substrate_is_valid(r) != 1 { return NX_EMIT_TAMPER }
273 if (hash as i64) == 0 { return NX_EMIT_BAD_INPUT }
274 if r.n_expected >= r.max_expected { return NX_EMIT_FULL }
275 // Once signed, the record is FROZEN; adding more would
276 // invalidate the signature. Caller must build all expected
277 // hashes BEFORE signing.
278 if r.is_signed != 0 { return NX_EMIT_TAMPER }
279
280 // Refuse duplicates.
281 var i: i64 = 0
282 while i < r.n_expected {
283 let h_addr: i64 = r.expected_hashes_ptrs[i]
284 if h_addr != 0 {
285 let h: *NxBlobHash = h_addr as *NxBlobHash
286 if nx_blob_hash_eq(h, hash) == 1 { return NX_EMIT_DUPLICATE }
287 }
288 i = i + 1
289 }
290 // Append (deep-copy).
291 let copy: *NxBlobHash = nx_blob_hash_new()
292 copy.w0 = hash.w0
293 copy.w1 = hash.w1
294 copy.w2 = hash.w2
295 copy.w3 = hash.w3
296 r.expected_hashes_ptrs[r.n_expected] = copy as i64
297 r.n_expected = r.n_expected + 1
298 return NX_EMIT_OK
299}
300
301// ===== Canonical serialization for signing =====================
302// Layout:
303// 0..7 magic "NXEMTSUB"
304// 8..15 schema_version
305// 16..23 target_tier
306// 24..31 target_isa
307// 32..39 ts_emit_us
308// 40..71 emitter_install_hash (4 i64 LE)
309// 72..79 n_expected
310// 80.. expected_hashes (n_expected * 32 bytes LE)
311//
312// Returns bytes-written or -1 on tamper / oversized buf.
313func nx_emitted_substrate_canonicalize(
314 r: *NxEmittedSubstrate,
315 buf: *u8, buf_max: i64
316) -> i64 {
317 if nx_emitted_substrate_is_valid(r) != 1 { return -1 }
318 if (buf as i64) == 0 { return -1 }
319 let need: i64 = NX_EMIT_HEADER_BYTES + (r.n_expected * 32)
320 if buf_max < need { return -1 }
321
322 var off: i64 = 0
323 off = _emit_store_magic(buf, off)
324 off = _emit_store_i64_le(buf, off, r.schema_version)
325 off = _emit_store_i64_le(buf, off, r.target_tier)
326 off = _emit_store_i64_le(buf, off, r.target_isa)
327 off = _emit_store_i64_le(buf, off, r.ts_emit_us)
328 off = _emit_store_i64_le(buf, off, r.emitter_install_hash_w0)
329 off = _emit_store_i64_le(buf, off, r.emitter_install_hash_w1)
330 off = _emit_store_i64_le(buf, off, r.emitter_install_hash_w2)
331 off = _emit_store_i64_le(buf, off, r.emitter_install_hash_w3)
332 off = _emit_store_i64_le(buf, off, r.n_expected)
333 // expected_hashes inline.
334 var i: i64 = 0
335 while i < r.n_expected {
336 let h: *NxBlobHash = (r.expected_hashes_ptrs[i]) as *NxBlobHash
337 off = _emit_store_i64_le(buf, off, h.w0)
338 off = _emit_store_i64_le(buf, off, h.w1)
339 off = _emit_store_i64_le(buf, off, h.w2)
340 off = _emit_store_i64_le(buf, off, h.w3)
341 i = i + 1
342 }
343 return off
344}
345
346// ===== Sign =====================================================
347// Canonicalize + Ed25519-sign + populate sig_w0..w7. Once signed,
348// is_signed=1 freezes the record (no more add_expected).
349func nx_emitted_substrate_sign(
350 r: *NxEmittedSubstrate,
351 priv_32: *u8
352) -> i64 {
353 if nx_emitted_substrate_is_valid(r) != 1 { return NX_EMIT_TAMPER }
354 if (priv_32 as i64) == 0 { return NX_EMIT_BAD_INPUT }
355 if r.n_expected == 0 { return NX_EMIT_BAD_INPUT } // empty record won't sign
356
357 let buf: *u8 = sys_mmap(NX_MAGIC_4096)
358 let n: i64 = nx_emitted_substrate_canonicalize(r, buf, NX_MAGIC_4096)
359 if n < 0 { return NX_EMIT_TAMPER }
360
361 let sig: *u8 = sys_mmap(64)
362 let rc: i64 = ed25519_sign_full(priv_32, buf, n, sig)
363 if rc != 0 { return NX_EMIT_BAD_SIG }
364 // Pack 64 bytes into 8 i64 LE.
365 var i: i64 = 0
366 while i < 8 {
367 let off: i64 = i * 8
368 let b0: i64 = (sig[off + 0] as i64) & 255
369 let b1: i64 = (sig[off + 1] as i64) & 255
370 let b2: i64 = (sig[off + 2] as i64) & 255
371 let b3: i64 = (sig[off + 3] as i64) & 255
372 let b4: i64 = (sig[off + 4] as i64) & 255
373 let b5: i64 = (sig[off + 5] as i64) & 255
374 let b6: i64 = (sig[off + 6] as i64) & 255
375 let b7: i64 = (sig[off + 7] as i64) & 255
376 let w: i64 = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
377 | (b4 << 32) | (b5 << 40) | (b6 << 48) | (b7 << 56)
378 if i == 0 { r.sig_w0 = w }
379 if i == 1 { r.sig_w1 = w }
380 if i == 2 { r.sig_w2 = w }
381 if i == 3 { r.sig_w3 = w }
382 if i == 4 { r.sig_w4 = w }
383 if i == 5 { r.sig_w5 = w }
384 if i == 6 { r.sig_w6 = w }
385 if i == 7 { r.sig_w7 = w }
386 i = i + 1
387 }
388 r.is_signed = 1
389 return NX_EMIT_OK
390}
391
392// ===== Verify ===================================================
393// Re-canonicalize + verify the sig against pub. Returns NX_EMIT_OK
394// on valid, NX_EMIT_BAD_SIG on tampered/wrong-key.
395func nx_emitted_substrate_verify_sig(
396 r: *NxEmittedSubstrate,
397 pub_32: *u8
398) -> i64 {
399 if nx_emitted_substrate_is_valid(r) != 1 { return NX_EMIT_TAMPER }
400 if (pub_32 as i64) == 0 { return NX_EMIT_BAD_INPUT }
401 if r.is_signed == 0 { return NX_EMIT_BAD_SIG }
402
403 let buf: *u8 = sys_mmap(NX_MAGIC_4096)
404 let n: i64 = nx_emitted_substrate_canonicalize(r, buf, NX_MAGIC_4096)
405 if n < 0 { return NX_EMIT_TAMPER }
406
407 // Reconstruct the 64-byte sig from sig_w0..w7.
408 let sig: *u8 = sys_mmap(64)
409 var i: i64 = 0
410 while i < 8 {
411 var w: i64 = 0
412 if i == 0 { w = r.sig_w0 }
413 if i == 1 { w = r.sig_w1 }
414 if i == 2 { w = r.sig_w2 }
415 if i == 3 { w = r.sig_w3 }
416 if i == 4 { w = r.sig_w4 }
417 if i == 5 { w = r.sig_w5 }
418 if i == 6 { w = r.sig_w6 }
419 if i == 7 { w = r.sig_w7 }
420 let off: i64 = i * 8
421 sig[off + 0] = (w & 255) as u8
422 sig[off + 1] = ((w >> 8) & 255) as u8
423 sig[off + 2] = ((w >> 16) & 255) as u8
424 sig[off + 3] = ((w >> 24) & 255) as u8
425 sig[off + 4] = ((w >> 32) & 255) as u8
426 sig[off + 5] = ((w >> 40) & 255) as u8
427 sig[off + 6] = ((w >> 48) & 255) as u8
428 sig[off + 7] = ((w >> 56) & 255) as u8
429 i = i + 1
430 }
431
432 let rc: i64 = ed25519_verify_full(pub_32, buf, n, sig)
433 if rc == NX_ED25519_SIG_OK { return NX_EMIT_OK }
434 return NX_EMIT_BAD_SIG
435}
436
437// ===== Populate a NxSubstrateManifest from this record =========
438// After verifier confirms signature, receiver calls this to copy
439// every expected hash from the record into a fresh manifest.
440// Manifest is then ready for nx_substrate_manifest_ingest as
441// blobs arrive.
442func nx_emitted_substrate_to_manifest(
443 r: *NxEmittedSubstrate,
444 manifest: *NxSubstrateManifest
445) -> i64 {
446 if nx_emitted_substrate_is_valid(r) != 1 { return NX_EMIT_TAMPER }
447 if nx_substrate_manifest_is_valid(manifest) != 1 { return NX_EMIT_BAD_INPUT }
448 var i: i64 = 0
449 while i < r.n_expected {
450 let h_addr: i64 = r.expected_hashes_ptrs[i]
451 if h_addr != 0 {
452 let h: *NxBlobHash = h_addr as *NxBlobHash
453 let rc: i64 = nx_substrate_manifest_add_expected(manifest, h)
454 if rc != NX_SM_INGESTED {
455 if rc != NX_SM_ALREADY_RECEIVED { return NX_EMIT_BAD_INPUT }
456 }
457 }
458 i = i + 1
459 }
460 return NX_EMIT_OK
461}
462
463// ===== Audit accessor ===========================================
464func nx_emitted_substrate_n_expected(r: *NxEmittedSubstrate) -> i64 {
465 if nx_emitted_substrate_is_valid(r) != 1 { return -1 }
466 return r.n_expected
467}
468
469func nx_emitted_substrate_is_signed(r: *NxEmittedSubstrate) -> i64 {
470 if nx_emitted_substrate_is_valid(r) != 1 { return 0 }
471 return r.is_signed
472}