code wiki / _hdl_build / nx_pe_authhash.nx
nx_pe_authhash.nx source
↩ module page · 307 lines · 14492 B
1// nx_pe_authhash.nx -- F103e RUNG 1: the Authenticode PE hash.
2//
3// WHY THIS EXISTS (measured 2026-08-08): real EDK2 with Microsoft keys enrolled REFUSES our unsigned
4// EFI application -- `BdsDxe: failed to load Boot0001 ... : Access Denied` -- while the SAME secboot
5// firmware in SETUP mode (no Platform Key) runs it and prints NISHI. So the refusal is the ENROLLED
6// KEY SET, not Secure Boot, and the remedy is to enroll our own key and SIGN the image. Debt
7// 1786237435. Signing starts here, because every later step (SpcIndirectDataContent -> PKCS#7
8// SignedData -> WIN_CERTIFICATE) carries THIS hash and is worthless if it is wrong.
9//
10// The Authenticode hash is NOT the hash of the file. Three ranges are excluded, because each is
11// written or rewritten AFTER signing and would otherwise invalidate the signature:
12// 1. the OptionalHeader CheckSum (4 bytes @ OPT+64)
13// 2. the Certificate Table data directory (8 bytes @ datadir[4]) -- it points AT the signature
14// 3. the attribute certificate table itself (the appended signature blob)
15// Everything else is hashed: headers up to SizeOfHeaders, then every section's raw data IN ASCENDING
16// PointerToRawData ORDER (file order, NOT section-table order -- they are allowed to differ), then
17// any trailing bytes that are not the certificate table.
18//
19// ★THE ANTI-VACUITY PROBLEM IS THE WHOLE POINT: a trivial "sha256 the whole file" implementation
20// passes every test that mutates .text and fails ONLY on the exclusion ranges. So T1/T2 below mutate
21// INSIDE the excluded fields and demand the hash NOT move -- those are the teeth a wrong
22// implementation cannot pass, and they are the reason this organ is not just a call to sha256_digest.
23//
24// There is no external Authenticode oracle on this host (sbsign/pesign/osslsigncode all MEASURED
25// MISSING), so the teeth ARE the specification, stated as properties rather than a golden vector.
26// ⚠That is a REAL limit and it is named, not hidden: this proves the exclusion semantics and
27// determinism; it does NOT prove byte-agreement with Microsoft's implementation. A golden vector
28// from a signed EFI binary is the next tooth to add when one is obtainable.
29//
30// Usage: nx_pe_authhash <file> -> "<64 hex> <path>" exit 0
31// nx_pe_authhash selftest -> property teeth exit 0 GREEN / 1 RED
32// Exit: 0 ok | 1 RED | 2 refused (not a PE we can hash) | 3 UNPROVEN (cannot read input)
33// Log -> knowledge/status/nishi_os.log, canonical verdict= LAST (positional anchor).
34// Sovereign: syscalls only, no openssl/sbsign. license_tier: ORIGINAL
35import "nx_syscalls.nx"
36import "nx_sha256.nx"
37const PAH_MAGIC_4096: i64 = 4096
38
39const PAH_MAXSEC: i64 = 96 // refuse absurd section counts rather than walk off the header
40const PAH_DOS_LFA: i64 = 0x3C
41const PAH_MAG_P32P: i64 = 0x20B
42const PAH_MAG_P32: i64 = 0x10B
43
44func ph_p(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
45func ph_fp(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 }
46func ph_fn(fd: i64, v: i64) -> i64 {
47 let bb: *u8 = sys_mmap(28); var m: i64 = v
48 if m < 0 { m = 0 - m }
49 let t: *u8 = sys_mmap(28); var k: i64 = 0
50 if m == 0 { t[0] = 48 as u8; k = 1 }
51 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
52 var i: i64 = 0
53 while i < k { bb[i] = t[k - 1 - i]; i = i + 1 }
54 sys_write(fd, bb, k); return 0
55}
56func ph_hex(fd: i64, d: *u8, n: i64) -> i64 {
57 let hx: *u8 = "0123456789abcdef" as *u8 // hoisted: indexing an inline-cast literal never matches
58 let o: *u8 = sys_mmap(n * 2 + 8)
59 var i: i64 = 0
60 while i < n {
61 o[i * 2] = hx[(d[i] as i64) >> 4]
62 o[i * 2 + 1] = hx[(d[i] as i64) & 0xf]
63 i = i + 1
64 }
65 sys_write(fd, o, n * 2); return 0
66}
67func ph_eq(a: *u8, b: *u8) -> i64 {
68 var i: i64 = 0
69 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
70 if b[i] != (0 as u8) { return 0 }
71 return 1
72}
73func ph_r16(b: *u8, o: i64) -> i64 { return (b[o] as i64) | ((b[o+1] as i64) << 8) }
74func ph_r32(b: *u8, o: i64) -> i64 {
75 return (b[o] as i64) | ((b[o+1] as i64) << 8) | ((b[o+2] as i64) << 16) | ((b[o+3] as i64) << 24)
76}
77
78// ---- the hash. out = 32 bytes. Returns 0 ok, negative = a NAMED refusal, never a silent zero.
79// -1 too short -2 no MZ -3 bad e_lfanew -4 no PE sig -5 bad opt magic
80// -6 insane section count -7 header/section geometry runs past the file
81func pe_authhash(f: *u8, flen: i64, out: *u8) -> i64 {
82 if flen < 64 { return 0 - 1 }
83 if (f[0] as i64) != 0x4D { return 0 - 2 } // 'M'
84 if (f[1] as i64) != 0x5A { return 0 - 2 } // 'Z'
85 let lfa: i64 = ph_r32(f, PAH_DOS_LFA)
86 if lfa < 0 { return 0 - 3 }
87 if (lfa + 24) > flen { return 0 - 3 }
88 if ph_r32(f, lfa) != 0x00004550 { return 0 - 4 } // 'PE\0\0'
89 let coff: i64 = lfa + 4
90 let opt: i64 = lfa + 24
91 if (opt + 4) > flen { return 0 - 3 }
92 let mag: i64 = ph_r16(f, opt)
93 var ddoff: i64 = 0
94 if mag == PAH_MAG_P32P { ddoff = opt + 112 } else {
95 if mag == PAH_MAG_P32 { ddoff = opt + 96 } else { return 0 - 5 } }
96
97 let nsec: i64 = ph_r16(f, coff + 2)
98 let optsz: i64 = ph_r16(f, coff + 16)
99 let sizehdr: i64 = ph_r32(f, opt + 60)
100 if nsec < 0 { return 0 - 6 }
101 if nsec > PAH_MAXSEC { return 0 - 6 }
102 if sizehdr < 0 { return 0 - 7 }
103 if sizehdr > flen { return 0 - 7 }
104
105 let cksum_off: i64 = opt + 64 // EXCLUDED range 1 (4 bytes)
106 let cert_off: i64 = ddoff + 4 * 8 // EXCLUDED range 2 (8 bytes)
107 if (cert_off + 8) > flen { return 0 - 7 }
108 let cert_sz: i64 = ph_r32(f, cert_off + 4) // size of the appended signature, 0 if unsigned
109 let sectab: i64 = opt + optsz
110 if (sectab + nsec * 40) > flen { return 0 - 7 }
111
112 let c: *Sha256 = sys_mmap(PAH_MAGIC_4096) as *Sha256
113 sha256_init(c)
114
115 // headers, with the two excluded fields skipped
116 sha256_update(c, f, cksum_off) // [0, CheckSum)
117 sha256_update(c, (f + cksum_off + 4) as *u8, cert_off - (cksum_off + 4)) // (CheckSum, certdir)
118 sha256_update(c, (f + cert_off + 8) as *u8, sizehdr - (cert_off + 8)) // (certdir, SizeOfHeaders)
119
120 // sections in ASCENDING PointerToRawData order. Selection sort over indices -- the section TABLE
121 // order is not required to match FILE order, and hashing in table order is a silent wrong answer
122 // on any linker that reorders. A separate `taken` flag ends each pick: a loop that exits by
123 // clobbering its own cursor destroys the position it was searching for.
124 let taken: *u8 = sys_mmap(PAH_MAXSEC + 8)
125 var z: i64 = 0
126 while z < nsec { taken[z] = 0 as u8; z = z + 1 }
127 var summed: i64 = sizehdr
128 var picked: i64 = 0
129 while picked < nsec {
130 var best: i64 = 0 - 1
131 var bestptr: i64 = 0
132 var i: i64 = 0
133 while i < nsec {
134 if (taken[i] as i64) == 0 {
135 let p: i64 = ph_r32(f, sectab + i * 40 + 20) // PointerToRawData
136 if best < 0 { best = i; bestptr = p } else { if p < bestptr { best = i; bestptr = p } }
137 }
138 i = i + 1
139 }
140 if best < 0 { return 0 - 7 }
141 taken[best] = 1 as u8
142 let praw: i64 = ph_r32(f, sectab + best * 40 + 20)
143 let sraw: i64 = ph_r32(f, sectab + best * 40 + 16) // SizeOfRawData
144 if sraw > 0 {
145 if praw < 0 { return 0 - 7 }
146 if (praw + sraw) > flen { return 0 - 7 }
147 sha256_update(c, (f + praw) as *u8, sraw)
148 summed = summed + sraw
149 }
150 picked = picked + 1
151 }
152
153 // trailing bytes that are NOT the certificate table (EXCLUDED range 3)
154 if flen > summed {
155 let tail: i64 = flen - summed - cert_sz
156 if tail > 0 { sha256_update(c, (f + summed) as *u8, tail) }
157 }
158
159 sha256_final(c, out)
160 return 0
161}
162
163// ---------------------------------------------------------------------------------------------
164// TEETH. T1/T2 are the ones a whole-file sha256 CANNOT pass -- they are the anti-vacuity core.
165func ph_same(a: *u8, b: *u8) -> i64 {
166 var i: i64 = 0
167 while i < 32 { if a[i] != b[i] { return 0 } i = i + 1 }
168 return 1
169}
170
171func ph_selftest(path: *u8) -> i64 {
172 let lp: *i64 = sys_mmap(16) as *i64
173 let orig: *u8 = sys_read_file(path, lp)
174 let n: i64 = lp[0]
175 if n <= 0 {
176 ph_p("PAH UNPROVEN: cannot read fixture " as *u8); ph_p(path); ph_p("\n" as *u8)
177 sys_exit(3); return 3
178 }
179 // scratch copy so the fixture on disk is never touched
180 let work: *u8 = sys_mmap(n + 64)
181 var i: i64 = 0
182 while i < n { work[i] = orig[i]; i = i + 1 }
183
184 let h0: *u8 = sys_mmap(64)
185 let h1: *u8 = sys_mmap(64)
186 var pass: i64 = 0
187 var teeth: i64 = 0
188
189 let r0: i64 = pe_authhash(work, n, h0)
190 if r0 != 0 {
191 ph_p("PAH RED: fixture did not parse, rc=" as *u8); ph_fn(1, r0); ph_p("\n" as *u8)
192 sys_exit(1); return 1
193 }
194 let lfa: i64 = ph_r32(work, PAH_DOS_LFA)
195 let opt: i64 = lfa + 24
196 let ddoff: i64 = opt + 112
197 let cksum_off: i64 = opt + 64
198 let cert_off: i64 = ddoff + 32
199
200 // T0 determinism: the same bytes must hash the same twice. Without this every other tooth is
201 // ambiguous between "the mutation mattered" and "the organ is nondeterministic".
202 teeth = teeth + 1
203 pe_authhash(work, n, h1)
204 if ph_same(h0, h1) == 1 { pass = pass + 1; ph_p("PAH-T0 deterministic GREEN\n" as *u8) }
205 else { ph_p("PAH-T0 RED\n" as *u8) }
206
207 // T1 ANTI-VACUITY: mutate INSIDE the CheckSum field -> hash MUST NOT move.
208 teeth = teeth + 1
209 let sv1: i64 = work[cksum_off + 1] as i64
210 work[cksum_off + 1] = ((sv1 ^ 0xff) & 0xff) as u8
211 var moved: i64 = 0
212 if (work[cksum_off + 1] as i64) != sv1 { moved = 1 } // assert the fixture REACHED the condition
213 pe_authhash(work, n, h1)
214 if moved == 1 { if ph_same(h0, h1) == 1 { pass = pass + 1; ph_p("PAH-T1 checksum-excluded GREEN\n" as *u8) }
215 else { ph_p("PAH-T1 RED (a whole-file hash fails exactly here)\n" as *u8) } }
216 else { ph_p("PAH-T1 RED [VACUOUS: fixture never changed]\n" as *u8) }
217 work[cksum_off + 1] = sv1 as u8
218
219 // T2 ANTI-VACUITY: mutate INSIDE the certificate data-directory entry -> hash MUST NOT move.
220 teeth = teeth + 1
221 let sv2: i64 = work[cert_off + 2] as i64
222 work[cert_off + 2] = ((sv2 ^ 0xff) & 0xff) as u8
223 moved = 0
224 if (work[cert_off + 2] as i64) != sv2 { moved = 1 }
225 pe_authhash(work, n, h1)
226 if moved == 1 { if ph_same(h0, h1) == 1 { pass = pass + 1; ph_p("PAH-T2 certdir-excluded GREEN\n" as *u8) }
227 else { ph_p("PAH-T2 RED\n" as *u8) } }
228 else { ph_p("PAH-T2 RED [VACUOUS]\n" as *u8) }
229 work[cert_off + 2] = sv2 as u8
230
231 // T3 POSITIVE CONTROL: mutate real code (.text) -> hash MUST move. Without this, an organ that
232 // returned a constant would pass T1 and T2 perfectly.
233 teeth = teeth + 1
234 let optsz: i64 = ph_r16(work, lfa + 4 + 16)
235 let sectab: i64 = opt + optsz
236 let praw: i64 = ph_r32(work, sectab + 20)
237 let sv3: i64 = work[praw] as i64
238 work[praw] = ((sv3 ^ 0xff) & 0xff) as u8
239 pe_authhash(work, n, h1)
240 if ph_same(h0, h1) == 0 { pass = pass + 1; ph_p("PAH-T3 text-mutation-detected GREEN\n" as *u8) }
241 else { ph_p("PAH-T3 RED [the hash ignored real code]\n" as *u8) }
242 work[praw] = sv3 as u8
243
244 // T4 a header byte OUTSIDE the excluded ranges must still be covered.
245 teeth = teeth + 1
246 let sv4: i64 = work[opt + 60] as i64 // SizeOfHeaders' neighbourhood is hashed
247 work[opt + 2] = ((work[opt + 2] as i64) ^ 0xff) as u8 // MajorLinkerVersion: hashed, not excluded
248 pe_authhash(work, n, h1)
249 if ph_same(h0, h1) == 0 { pass = pass + 1; ph_p("PAH-T4 header-covered GREEN\n" as *u8) }
250 else { ph_p("PAH-T4 RED\n" as *u8) }
251 work[opt + 2] = ((work[opt + 2] as i64) ^ 0xff) as u8
252 if (work[opt + 60] as i64) != sv4 { ph_p("PAH-T4 note: restore drift\n" as *u8) }
253
254 // T5 restore must be exact -- if it is not, every tooth after a mutation is measuring debris.
255 teeth = teeth + 1
256 pe_authhash(work, n, h1)
257 if ph_same(h0, h1) == 1 { pass = pass + 1; ph_p("PAH-T5 restore-exact GREEN\n" as *u8) }
258 else { ph_p("PAH-T5 RED [mutations leaked]\n" as *u8) }
259
260 // T6 a non-PE input must be REFUSED by name, not hashed into a confident wrong answer.
261 teeth = teeth + 1
262 let sv6: i64 = work[0] as i64
263 work[0] = 0x58 as u8 // 'X' -- no longer MZ
264 let r6: i64 = pe_authhash(work, n, h1)
265 if r6 == (0 - 2) { pass = pass + 1; ph_p("PAH-T6 non-pe-refused GREEN\n" as *u8) }
266 else { ph_p("PAH-T6 RED rc=" as *u8); ph_fn(1, r6); ph_p("\n" as *u8) }
267 work[0] = sv6 as u8
268
269 ph_p("PAH-SELFTEST " as *u8); ph_fn(1, pass); ph_p("/" as *u8); ph_fn(1, teeth); ph_p("\n" as *u8)
270 let lf: i64 = sys_openat_append("knowledge/status/nishi_os.log" as *u8, 0x1a4)
271 if lf >= 0 {
272 ph_fp(lf, "PEAUTH selftest teeth=" as *u8); ph_fn(lf, pass)
273 ph_fp(lf, "of" as *u8); ph_fn(lf, teeth)
274 ph_fp(lf, " oracle=property-only(no sbsign/pesign on host) verdict=" as *u8)
275 if pass == teeth { ph_fp(lf, "GREEN\n" as *u8) } else { ph_fp(lf, "RED\n" as *u8) }
276 sys_close(lf)
277 }
278 if pass == teeth { sys_exit(0); return 0 }
279 sys_exit(1); return 1
280}
281
282func main(argc: i64, argv: *i64) -> i64 {
283 if argc < 2 {
284 ph_p("usage: nx_pe_authhash <file.efi> | nx_pe_authhash selftest [fixture.efi]\n" as *u8)
285 sys_exit(2); return 2
286 }
287 if ph_eq(argv[1] as *u8, "selftest" as *u8) == 1 {
288 var fx: *u8 = "_offc/nx_boot_uefi.efi" as *u8
289 if argc >= 3 { fx = argv[2] as *u8 }
290 return ph_selftest(fx)
291 }
292 let path: *u8 = argv[1] as *u8
293 let lp: *i64 = sys_mmap(16) as *i64
294 let f: *u8 = sys_read_file(path, lp)
295 if lp[0] <= 0 {
296 ph_p("PAH UNPROVEN: cannot read " as *u8); ph_p(path); ph_p("\n" as *u8)
297 sys_exit(3); return 3
298 }
299 let out: *u8 = sys_mmap(64)
300 let r: i64 = pe_authhash(f, lp[0], out)
301 if r < 0 {
302 ph_p("PAH REFUSED: code " as *u8); ph_fn(1, r); ph_p(" on " as *u8); ph_p(path); ph_p("\n" as *u8)
303 sys_exit(2); return 2
304 }
305 ph_hex(1, out, 32); ph_p(" " as *u8); ph_p(path); ph_p("\n" as *u8)
306 sys_exit(0); return 0
307}