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}