code wiki / _hdl_build / nx_machine_key.nx

nx_machine_key.nx source

↩ module page · 198 lines · 9704 B

1// nx_machine_key.nx -- MACHINE-BOUND key derivation for the sovereign vault (operator's 2// own-cybersecurity choice: option 2, unattended, hardened as far as software allows). Derives the 3// vault passphrase from machine-LOCAL identity (/etc/machine-id + hostname) mixed with a repo-stored 4// PEPPER, via heavily-iterated SHA-256. Writes it to /tmp/nxpass for the gated nx_vault to consume, 5// then the Warden shreds it. No human prompt; the key never exists on disk -- it is RECONSTRUCTED 6// each run from machine state. 7// 8// HONEST THREAT MODEL (recorded so nobody overclaims): 9// DEFEATS: repo/backup leaks, file theft, cloud sync exposure, a copied vault file alone. 10// DOES NOT DEFEAT a nation-state with PHYSICAL ACCESS to this exact machine + the binary: a 11// machine-bound key is, by construction, re-derivable from the machine. True resistance to that 12// requires a HARDWARE ROOT OF TRUST (TPM2 / the NAS secure element) holding a key that never 13// leaves silicon -- that is the FLAGGED hardware build, not deliverable in software. argon2id 14// (memory-hard, already in the team) replacing the SHA-256 stretch is the next software rung. 15// license_tier: ORIGINAL 16import "nx_syscalls.nx" 17import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 18import "nx_sha256.nx" 19import "nx_argon2id.nx" 20const MK_MAGIC_1024: i64 = 1024 21const MK_MAGIC_2048: i64 = 2048 22 23// KDF v1: 500k SHA-256 iterations (the original deployed derivation; kept for migration -- run 24// with argv[1]="v1" to derive the legacy passphrase). 25// KDF v2 (DEFAULT since 2026-06-10): the v1 digest is additionally stretched MEMORY-HARD through 26// the team's own argon2id (RFC 9106). v2 strictly contains v1's work, so it is never weaker. 27// The 2026-06-10 "argon2id integration segfault" was ROOT-CAUSED as the vault-lane compiler's 28// >6-arg tail-call stack-arg drop (sibcall guard predated the lane binary) -- NOT an argon2id or 29// integration bug. Compiler blessed same day; gates: _ar2_minrepro rc=0 twice, argon2id property 30// + xvalidate tests, blake2b KAT, all via nx_sov_build_run. 31const MK_ITERS: i64 = 500000 32// argon2id params MEASURED on this box by _ar2_perf_probe (2026-06-10, sovereign lane): 33// m=1MiB:58ms m=8MiB:412ms m=64MiB:3644ms at t=3 -> 32MiB ~= 1.8s = largest m inside the 1-2s 34// unattended budget (vault opens are per-deploy/per-pulse, not per-request). 35const MK2_M_KIB: i64 = 32768 36const MK2_T: i64 = 3 37const MK2_P: i64 = 1 38 39func _p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 40// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 41// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 42// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 43// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 44func _pn(v: i64) -> i64 { nxi_out(v); return 0 } 45func mk_read(path: *u8, out: *u8, cap: i64, lenbox: *i64) -> i64 { 46 let fd: i64 = sys_openat_rd(path) 47 if fd < 0 { lenbox[0] = 0; return 0 - 1 } 48 var total: i64 = 0 49 var go: i64 = 1 50 while go == 1 { 51 let base: i64 = out as i64 52 let n: i64 = sys_read(fd, (base + total) as *u8, cap - total) 53 if n <= 0 { go = 0 } else { total = total + n } 54 if total >= cap { go = 0 } 55 } 56 sys_close(fd) 57 lenbox[0] = total 58 return total 59} 60func mk_chomp(buf: *u8, len: i64) -> i64 { 61 var l: i64 = len 62 while l > 0 { if buf[l-1] == (10 as u8) { l = l - 1 } else { if buf[l-1] == (13 as u8) { l = l - 1 } else { break } } } 63 return l 64} 65// emit 32 hex chars of dig to out 66func mk_hex(dig: *u8, out: *u8) -> i64 { 67 let hexd: *u8 = "0123456789abcdef" as *u8 68 var i: i64 = 0 69 while i < 16 { 70 out[i*2] = hexd[(dig[i] as i64 >> 4) & 15] 71 out[i*2+1] = hexd[dig[i] as i64 & 15] 72 i = i + 1 73 } 74 return 32 75} 76// argon2id ctx bundle (caller-owns-memory pattern; same alloc shape as nx_argon2id_test) 77func mk2_ctx(m_kib: i64) -> *NxArgon2idCtx { 78 let raw: *u8 = sys_mmap(NX_ARGON2ID_CTX_BYTES) 79 let ctx: *NxArgon2idCtx = raw as *NxArgon2idCtx 80 ctx.memory_blocks = sys_mmap(m_kib * MK_MAGIC_1024) 81 ctx.h0_buf = sys_mmap(64) 82 ctx.prepend_buf = sys_mmap(MK_MAGIC_2048) 83 ctx.prev_buf = sys_mmap(64) 84 ctx.curr_buf = sys_mmap(64) 85 ctx.zero_block = sys_mmap(MK_MAGIC_1024) 86 ctx.z_buf = sys_mmap(MK_MAGIC_1024) 87 ctx.tmp_block = sys_mmap(MK_MAGIC_1024) 88 ctx.addr_block = sys_mmap(MK_MAGIC_1024) 89 ctx.final_block = sys_mmap(MK_MAGIC_1024) 90 ctx.h0_input = sys_mmap(MK_MAGIC_2048) 91 ctx.b2b_ctx = sys_mmap(NX_BLAKE2B_CTX_BYTES) as *NxBlake2b 92 ctx.b2b_buf = sys_mmap(128) 93 ctx.b2b_sv = sys_mmap(128) as *i64 94 ctx.b2b_sm = sys_mmap(128) as *i64 95 ctx.g_r = sys_mmap(MK_MAGIC_1024) as *i64 96 ctx.g_rs = sys_mmap(MK_MAGIC_1024) as *i64 97 ctx.g_col = sys_mmap(128) as *i64 98 return ctx 99} 100func mk_streq(a: *u8, b: *u8) -> i64 { 101 var i: i64 = 0 102 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 103 if b[i] != (0 as u8) { return 0 } 104 return 1 105} 106// "/tmp/nxpass_<uid>" into out (NUL-terminated). getuid = x86_64 syscall 102. The derived key is 107// machine-bound (uid-INDEPENDENT), so a uid-namespaced file holds the SAME passphrase -- it only avoids 108// a cross-uid ownership COLLISION on the shared legacy path. 109func mk_uidpath(out: *u8) -> i64 { 110 let uid: i64 = __syscall(102, 0, 0, 0, 0, 0, 0) 111 let pfx: *u8 = "/tmp/nxpass_" as *u8 112 var o: i64 = 0 113 while pfx[o] != (0 as u8) { out[o] = pfx[o]; o = o + 1 } 114 let t: *u8 = sys_mmap(24); var m: i64 = uid; var k: i64 = 0 115 if m == 0 { t[0] = 48 as u8; k = 1 } 116 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 117 var q: i64 = k - 1 118 while q >= 0 { out[o] = t[q]; o = o + 1; q = q - 1 } 119 out[o] = 0 as u8 120 return o 121} 122func main(argc: i64, argv: *i64) -> i64 { 123 _p("=== MACHINE-BOUND KEY: deriving the vault passphrase from machine state (unattended) ===\n" as *u8) 124 var want_v1: i64 = 0 125 if argc > 1 { if mk_streq(argv[1] as *u8, "v1" as *u8) == 1 { want_v1 = 1 } } 126 let lenbox: *i64 = sys_mmap(16) as *i64 127 let mid: *u8 = sys_mmap(256) 128 mk_read("/etc/machine-id" as *u8, mid, 256, lenbox) 129 let midlen: i64 = mk_chomp(mid, lenbox[0]) 130 let host: *u8 = sys_mmap(256) 131 mk_read("/proc/sys/kernel/hostname" as *u8, host, 256, lenbox) 132 let hostlen: i64 = mk_chomp(host, lenbox[0]) 133 // PEPPER: a repo-stored constant -- raises the bar (attacker needs the repo AND the machine), 134 // and lets you ROTATE the derived key by changing it without touching the machine. 135 let pepper: *u8 = "nishi-vault-pepper-2026-06-rotate-me" as *u8 136 var plen: i64 = 0 137 while pepper[plen] != (0 as u8) { plen = plen + 1 } 138 if midlen < 8 { _p(" WARN: machine-id short/absent -- binding weak on this host\n" as *u8) } 139 let dig: *u8 = sys_mmap(32) 140 var c: Sha256 141 sha256_init(&c) 142 sha256_update(&c, "nishi-mkey-v1" as *u8, 13) 143 sha256_update(&c, mid, midlen) 144 sha256_update(&c, host, hostlen) 145 sha256_update(&c, pepper, plen) 146 sha256_final(&c, dig) 147 var it: i64 = 1 148 while it < MK_ITERS { 149 sha256_init(&c) 150 sha256_update(&c, dig, 32) 151 sha256_final(&c, dig) 152 it = it + 1 153 } 154 let hex: *u8 = sys_mmap(64) 155 if want_v1 == 1 { 156 mk_hex(dig, hex) 157 _p(" KDF v1 (legacy, migration only): 500k SHA-256\n" as *u8) 158 } else { 159 // v2: memory-hard stage. password = the v1 digest; salt = independent machine-bound 160 // 16 bytes (domain-separated hash of machine-id + hostname). 161 let saltd: *u8 = sys_mmap(32) 162 var sc: Sha256 163 sha256_init(&sc) 164 sha256_update(&sc, "nishi-mkey-v2-salt" as *u8, 18) 165 sha256_update(&sc, mid, midlen) 166 sha256_update(&sc, host, hostlen) 167 sha256_final(&sc, saltd) 168 let ctx: *NxArgon2idCtx = mk2_ctx(MK2_M_KIB) 169 let tag: *u8 = sys_mmap(64) 170 let rc2: i64 = nx_argon2id_hash(ctx, dig, 32, saltd, 16, MK2_P, 32, MK2_M_KIB, MK2_T, tag) 171 if rc2 != 0 { 172 _p(" argon2id stage FAILED rc -- refusing to emit a weaker key (fail closed)\n" as *u8) 173 sys_exit(1); return 1 174 } 175 mk_hex(tag, hex) 176 _p(" KDF v2: 500k SHA-256 -> argon2id(m=32MiB,t=3,p=1) memory-hard (params measured 2026-06-10)\n" as *u8) 177 } 178 let ofd: i64 = sys_openat_wr("/tmp/nxpass" as *u8, 0x180) // 0600 179 if ofd < 0 { 180 // A stale /tmp/nxpass owned by a DIFFERENT uid (e.g. a root recovery/pulse run that didn't shred) 181 // blocks this 0600 write -> the historical "/tmp/nxpass write failed" that intermittently broke 182 // EVERY sovereign vault op. Fall back to a uid-namespaced path this uid always owns; nx_vault reads 183 // the same fallback when the legacy path is unreadable. Same machine-bound passphrase either way. 184 let upath: *u8 = sys_mmap(64); mk_uidpath(upath) 185 let ofd2: i64 = sys_openat_wr(upath, 0x180) 186 if ofd2 < 0 { _p(" nxpass write failed (legacy + uid-fallback both blocked)\n" as *u8); sys_exit(1); return 1 } 187 sys_write(ofd2, hex, 32); sys_close(ofd2) 188 _p(" derived -> uid-fallback nxpass (legacy /tmp/nxpass blocked by a stale cross-uid file)\n" as *u8) 189 sys_exit(0); return 0 190 } 191 sys_write(ofd, hex, 32) 192 sys_close(ofd) 193 _p(" derived 256-bit machine-bound passphrase -> /tmp/nxpass (0600; Warden shreds after use)\n" as *u8) 194 _p(" bound to: machine-id(" as *u8); if midlen >= 8 { _p("present" as *u8) } else { _p("WEAK" as *u8) } 195 _p(") + hostname + repo pepper\n" as *u8) 196 sys_exit(0) 197 return 0 198}