code wiki / (root) / nx_evattest.nx

nx_evattest.nx source

↩ module page · 208 lines · 12478 B

1// nx_evattest.nx -- SIGNED EVIDENCE ATTESTATIONS, shared. Closes seq1367: the human leg of the evidence law 2// was a plain-text row, so anything that could write knowledge/status/ could mint a human sign-off and drive 3// a domain to PROVEN. The `require_human` clause exists precisely so an automated actor cannot certify its 4// own work; an unsigned carrier let exactly that happen through the back door. 5// 6// (Named nx_evattest, not nx_attest: runtime/nx_attest.nx already exists and is a different organ -- an 7// append-only Merkle log for privileged operations. Check before you name.) 8// 9// ★★WHAT SIGNING HONESTLY BUYS, AND WHAT IT DOES NOT. 10// It does NOT create an absolute barrier against an agent that can read the filesystem -- a private key on 11// disk is readable by anything with disk access, and pretending otherwise would be exactly the comfortable 12// fiction this workstream exists to delete. What it DOES buy, all of it real: 13// * ATTRIBUTION -- every attestation names the key that made it, verifiably. 14// * TAMPER-EVIDENCE -- a row cannot be edited afterwards without invalidating its signature. 15// * ROLE SEPARATION -- the structural one: a human-class row signed by a key REGISTERED AS A MACHINE KEY 16// is REFUSED. The automation signing a human attestation is not merely discouraged, it is mechanically 17// rejected, and forging one requires a key the automation is not supposed to hold. 18// * AUDITABILITY -- if a human row ever were signed by the wrong key, that is provable afterwards. 19// 20// This library contains NO key generation and NO signing path. It verifies, and it refuses. Minting a human 21// attestation is an act the operator performs with a key the operator holds -- that is the whole clause. 22// 23// ROW FORMAT (one attestation per line, knowledge/status/evclass_<domain>.conf): 24// class=human verdict=pass scope=<what was examined> signer=<who> epoch=<ts> pub=<64hex> sig=<128hex> 25// The signature covers the CLAIM PREFIX: every byte of the row up to (and excluding) " pub=". So class, 26// verdict, scope, signer and epoch are all immutable under the signature -- a signed attestation cannot be 27// re-pointed at a different class, claim, or time. The key and signature themselves sit outside the signed 28// region, so a signer need not predict the encoding of its own key. 29// 30// KEY REGISTRY (knowledge/attest_keys.conf): role=human|llm|machine pub=<64hex> name=<who> 31// An unregistered key verifies cryptographically but is still REFUSED: a valid signature from a stranger is 32// not an authorisation. license_tier: ORIGINAL 33import "nx_syscalls.nx" 34import "nx_ed25519_signature.nx" 35const AT_MAGIC_4000: i64 = 4000 36const AT_MAGIC_4096: i64 = 4096 37 38const AT_CLASS_NONE: i64 = 0 39const AT_CLASS_MECH: i64 = 1 40const AT_CLASS_LLM: i64 = 2 41const AT_CLASS_HUMAN: i64 = 4 42// ADDED 2026-07-31. MEASURED DEFECT: nx_sota_status.nx:220 sets EV_CLASS_ORACLE (8) and hands it to 43// at_verify_row, but this file knew only MECH|LLM|HUMAN -- so an oracle row was authorised by FALL-THROUGH 44// (the role rules below constrain HUMAN and LLM and never mentioned oracle). The loader and the verifier 45// disagreed about the vocabulary. PERMITTED BY OMISSION IS NOT PERMITTED BY DECLARATION: a future 46// default-deny would have silently killed the class, a future default-allow silently widened it. 47const AT_CLASS_ORACLE: i64 = 8 48// ADDED 2026-08-01, value MUST match EV_CLASS_EXPERIENTIAL in nx_evidence_verdict.nx. Declared here 49// EXPLICITLY rather than left to fall through the role rules -- that omission is precisely the defect 50// recorded above for ORACLE. PERMITTED BY OMISSION IS NOT PERMITTED BY DECLARATION. 51const AT_CLASS_EXPERIENTIAL: i64 = 16 52 53const AT_OK: i64 = 0 54const AT_ERR_NOSIG: i64 = 1 55const AT_ERR_BADSIG: i64 = 2 56const AT_ERR_UNKNOWNKEY: i64 = 3 57const AT_ERR_ROLE: i64 = 4 58// An oracle row that does not carry its own re-derivation recipe. Distinct from AT_ERR_ROLE on purpose: 59// the signer WAS entitled to make the claim; the CLAIM ITSELF was unverifiable. 60const AT_ERR_NOTDERIVED: i64 = 5 61 62func at_hexval(c: i64) -> i64 { 63 if c >= 48 { if c <= 57 { return c - 48 } } 64 if c >= 97 { if c <= 102 { return c - 87 } } 65 if c >= 65 { if c <= 70 { return c - 55 } } 66 return 0 - 1 67} 68func at_unhex(buf: *u8, off: i64, n: i64, out: *u8) -> i64 { 69 var i: i64 = 0 70 while i < n { 71 let hi: i64 = at_hexval(buf[off+i*2] as i64) 72 let lo: i64 = at_hexval(buf[off+i*2+1] as i64) 73 if hi < 0 { return 0 } 74 if lo < 0 { return 0 } 75 out[i] = ((hi << 4) | lo) as u8 76 i = i + 1 77 } 78 return 1 79} 80func at_find(buf: *u8, s: i64, e: i64, key: *u8, kl: i64) -> i64 { 81 var i: i64 = s 82 while i + kl <= e { 83 var j: i64 = 0 84 var m: i64 = 1 85 while j < kl { if buf[i+j] != key[j] { m = 0; j = kl } else { j = j + 1 } } 86 if m == 1 { return i } 87 i = i + 1 88 } 89 return 0 - 1 90} 91 92// Which role is this 32-byte pubkey registered under? AT_CLASS_NONE if unknown. 93// An unknown key is refused even with a mathematically valid signature. 94func at_key_role(conf: *u8, cn: i64, pub: *u8) -> i64 { 95 let got: *u8 = sys_mmap(32) 96 var ls: i64 = 0 97 var p: i64 = 0 98 while p <= cn { 99 var eol: i64 = 0 100 if p == cn { eol = 1 } else { if conf[p] == (10 as u8) { eol = 1 } } 101 if eol == 1 { 102 if p > ls { if conf[ls] != (35 as u8) { 103 let pk: i64 = at_find(conf, ls, p, "pub=" as *u8, 4) 104 if pk >= 0 { if pk + 4 + 64 <= p { 105 if at_unhex(conf, pk + 4, 32, got) == 1 { 106 var same: i64 = 1 107 var i: i64 = 0 108 while i < 32 { if got[i] != pub[i] { same = 0; i = 32 } else { i = i + 1 } } 109 if same == 1 { 110 if at_find(conf, ls, p, "role=human" as *u8, 10) >= 0 { return AT_CLASS_HUMAN } 111 if at_find(conf, ls, p, "role=llm" as *u8, 8) >= 0 { return AT_CLASS_LLM } 112 if at_find(conf, ls, p, "role=machine" as *u8, 12) >= 0 { return AT_CLASS_MECH } 113 return AT_CLASS_NONE 114 } 115 } 116 } } 117 } } 118 ls = p + 1 119 } 120 p = p + 1 121 } 122 return AT_CLASS_NONE 123} 124 125// Verify one attestation row. `claimed` is the class the row asserts (parsed by the caller). 126// AT_OK only if: a signature is present, it verifies over the canonical prefix, the key is REGISTERED, and 127// the key's role is permitted to make that claim. A machine key may never sign a human-class row. 128func at_verify_row(buf: *u8, s: i64, e: i64, claimed: i64, conf: *u8, cn: i64) -> i64 { 129 let sk: i64 = at_find(buf, s, e, " sig=" as *u8, 5) 130 if sk < 0 { return AT_ERR_NOSIG } 131 let pk: i64 = at_find(buf, s, e, " pub=" as *u8, 5) 132 if pk < 0 { return AT_ERR_NOSIG } 133 if pk + 5 + 64 > e { return AT_ERR_NOSIG } 134 if sk + 5 + 128 > e { return AT_ERR_NOSIG } 135 let pub: *u8 = sys_mmap(32) 136 let sig: *u8 = sys_mmap(64) 137 if at_unhex(buf, pk + 5, 32, pub) == 0 { return AT_ERR_NOSIG } 138 if at_unhex(buf, sk + 5, 64, sig) == 0 { return AT_ERR_NOSIG } 139 // CANONICAL MESSAGE = the CLAIM PREFIX: every byte up to (excluding) " pub=". Both the key and the 140 // signature sit outside it, which is what a signer naturally signs -- it does not have to predict the 141 // encoding of its own key. Getting this boundary wrong is silent: sign and verify simply disagree and 142 // every genuine attestation reads BAD-SIGNATURE while all the refusal paths still pass, so the gate 143 // looks 3/7 healthy. The claim (class, verdict, scope, signer, epoch) is fully covered either way. 144 let mlen: i64 = pk - s 145 if mlen <= 0 { return AT_ERR_NOSIG } 146 if mlen > AT_MAGIC_4000 { return AT_ERR_NOSIG } 147 let msg: *u8 = sys_mmap(AT_MAGIC_4096) 148 var i: i64 = 0 149 while i < mlen { msg[i] = buf[s+i]; i = i + 1 } 150 if ed25519_verify_full(pub, msg, mlen, sig) != 1 { return AT_ERR_BADSIG } 151 let role: i64 = at_key_role(conf, cn, pub) 152 if role == AT_CLASS_NONE { return AT_ERR_UNKNOWNKEY } 153 if claimed == AT_CLASS_HUMAN { if role != AT_CLASS_HUMAN { return AT_ERR_ROLE } } 154 if claimed == AT_CLASS_LLM { if role == AT_CLASS_MECH { return AT_ERR_ROLE } } 155 // ORACLE: DERIVED, NEVER DECLARED -- AND ENFORCED HERE, AT THE VERIFIER. 156 // A machine key MAY sign an oracle row: for this class independence comes from the REFERENCE, not the 157 // signer, so role separation is deliberately not applied. That is exactly why the ROW must be 158 // checkable. Enforcing the derived contract only in the producer (nx_evoracle_sweep) would be security 159 // theatre: the producer is one program among many, and anything holding a machine key could hand-write 160 // class=oracle ref=RFC7748 having compared nothing. The VERIFIER is the chokepoint every row passes. 161 // ref= the external authority, named -- it matched WHAT? is not a rhetorical question 162 // refdig= digest of the exact bytes compared -- prose can CLAIM RFC 7748; a digest COMMITS to bytes 163 // gate= the executable that performed the comparison -- the re-derivation recipe 164 // A reader who distrusts the signature can ignore it entirely and RE-RUN the claim. For a mechanical 165 // claim, reproducibility outranks trust. 166 if claimed == AT_CLASS_ORACLE { 167 if at_find(buf, s, e, " ref=" as *u8, 5) < 0 { return AT_ERR_NOTDERIVED } 168 if at_find(buf, s, e, " refdig=" as *u8, 8) < 0 { return AT_ERR_NOTDERIVED } 169 if at_find(buf, s, e, " gate=" as *u8, 6) < 0 { return AT_ERR_NOTDERIVED } 170 } 171 // EXPERIENTIAL: THE WORLD ANSWERED BACK -- and the row must SAY SO, checkably, at this same chokepoint. 172 // Machine-signable for the same reason as ORACLE (independence is in the RESPONDENT, not the signer), 173 // and therefore held to the same standard: a machine key must not be able to hand-write 174 // "class=experiential" over a gate that merely returned 0. THIS IS A HARDER BAR THAN A SIGNATURE, and 175 // the three fields are what make it harder: 176 // ran= the thing actually executed in the world (not the assertion about it) 177 // witness= WHO OR WHAT OUTSIDE THIS PROCESS RESPONDED -- remote peers, a place-and-route timing 178 // closure, a physical device. If the only witness is us, the class is MECH, not this. 179 // observed= the response as a MEASURED VALUE (acked=8, 125.45MHz, 2.4pct) -- "it worked" is not an 180 // observation, it is a summary of one. 181 // Deliberately NO role restriction beyond registration, and deliberately NOT satisfiable by prose: 182 // a reader who distrusts the signature re-runs `ran=` and asks the same witness. 183 if claimed == AT_CLASS_EXPERIENTIAL { 184 if at_find(buf, s, e, " ran=" as *u8, 5) < 0 { return AT_ERR_NOTDERIVED } 185 if at_find(buf, s, e, " witness=" as *u8, 9) < 0 { return AT_ERR_NOTDERIVED } 186 if at_find(buf, s, e, " observed=" as *u8, 10) < 0 { return AT_ERR_NOTDERIVED } 187 } 188 return AT_OK 189} 190 191func at_err_name(rc: i64) -> *u8 { 192 if rc == AT_OK { return "OK" as *u8 } 193 if rc == AT_ERR_NOSIG { return "UNSIGNED" as *u8 } 194 if rc == AT_ERR_BADSIG { return "BAD-SIGNATURE" as *u8 } 195 if rc == AT_ERR_UNKNOWNKEY { return "UNREGISTERED-KEY" as *u8 } 196 if rc == AT_ERR_ROLE { return "WRONG-ROLE-FOR-CLASS" as *u8 } 197 // A CATCH-ALL IN AN ERROR-NAME TABLE IS A MISDIAGNOSIS GENERATOR. This arm was missing, so 198 // AT_ERR_NOTDERIVED (an oracle row lacking ref=/refdig=/gate= -- the anti-fabrication refusal, 199 // the most important one in the plane) PRINTED AS "WRONG-ROLE-FOR-CLASS", sending a reader to 200 // hunt key roles while the real cause is an underived row. Caught 2026-08-01 by running 201 // nx_attest_ceremony's T5 on the NAS: it scored GREEN (it asserts the NUMERIC code) while 202 // printing the wrong NAME -- proof that asserting on a diagnostic string would have produced a 203 // false RED against a correct verifier, and eyeballing the output alone would have produced a 204 // false diagnosis. The laptop copy already had this arm: TREE DIVERGENCE, and the DEPLOYING 205 // copy was the degraded one (debt 1785622769). 206 if rc == AT_ERR_NOTDERIVED { return "NOT-DERIVED (oracle lacks ref=/refdig=/gate=, or experiential lacks ran=/witness=/observed=)" as *u8 } 207 return "UNKNOWN-ERROR-CODE" as *u8 208}