code wiki / (root) / nx_emitted_substrate.nx

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}