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}