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}