nx_asset_provenance.nx source
↩ module page · 203 lines · 10560 B
1// nx_asset_provenance.nx -- UNIVERSAL ORGANIZATION TOOLING arc, R2.
2//
3// CRYPTOGRAPHICALLY SIGNED, TAMPER-EVIDENT PROVENANCE for asset records, so we can TRULY KNOW
4// whether an artifact is a generated image, a downloaded video, or human-authored -- not by a
5// trusting a mutable metadata field, but by a signature only the platform/author key could make.
6//
7// THE MODEL (cited to knowledge/registry/org_research.tsv CONFIRMED claims):
8// * c2pa-content-credentials: a provenance CREDENTIAL is a C2PA-style SIGNED, tamper-evident
9// assertion that travels with the asset -- who/what made it (incl. generative-AI).
10// * prov-model: the assertion is shaped as the W3C PROV graph ENTITY <- ACTIVITY <- AGENT.
11// The ENTITY is the asset's RECORD CID -- this BINDS the credential to that exact content
12// (change one byte of the asset/record -> its CID changes -> the credential no longer matches).
13// * provenance-class-three: the assertion states the class human|machine|downloaded, the agent,
14// and (machine) tool+model / (downloaded) source URL+fetch date.
15//
16// THE BACKBONE NEEDS ZERO NEW CRYPTO -- this organ is pure COMPOSITION (Rule 15, DRY):
17// * canonical assertion bytes -> nx_canon_cid (canon_encode: key-sorted, byte-deterministic,
18// so the SAME assertion is the SAME bytes everywhere -> the
19// signer and verifier agree on exactly what was signed)
20// * ed25519 sign / verify -> nx_ed25519_signature (ed25519_sign_full / ed25519_verify_full /
21// ed25519_pub_from_priv) -- the SAME RFC-8032, KAT-verified
22// ed25519 that nx_device_cert and nx_fw_capsule use. NO rolled
23// crypto: forgery (wrong-key signing) is what plain hashing
24// CANNOT catch, and what ed25519 verification DOES catch.
25//
26// CREDENTIAL FORMAT (C2PA "claim || signature", self-framed so verify recovers the exact signed
27// bytes -- the same shape as nx_fw_capsule's payload||sig, here magic + len + assertion + sig):
28// magic "NXPC1" (5) | u32be assertion_len | assertion bytes (canon_encode'd) | ed25519 sig (64)
29// The signature covers magic || len || assertion (everything before the sig), so flipping ANY byte
30// of the framing or the assertion breaks verification.
31//
32// VERIFY is DENY-BY-DEFAULT (Rule 12, fail-closed): a credential ALLOWs (returns 1) ONLY IF
33// (1) the framing is intact (magic + length within bounds), AND
34// (2) the ed25519 signature verifies against the TRUSTED public key over the exact assertion, AND
35// (3) the assertion's `entity` field == the expect_record_cid the caller demands (binding holds).
36// Any failure -> 0 (REJECT). Tampering the asset (different CID), tampering the assertion (sig
37// fails), forging the signature (wrong key fails), or re-binding a credential to a different
38// record (entity mismatch) are each detected.
39//
40// No hardware/persistent-firmware writes (Rule 26). license_tier: ORIGINAL
41import "nx_syscalls.nx"
42import "nx_canon_cid.nx"
43import "nx_uxf_decode.nx"
44import "nx_ed25519_signature.nx"
45
46// ---- small string helpers (no libc; null-terminated bytes) ----
47func ap_len(s: *u8) -> i64 {
48 var n: i64 = 0
49 while s[n] != (0 as u8) { n = n + 1 }
50 return n
51}
52
53func ap_streq(a: *u8, b: *u8) -> i64 {
54 var i: i64 = 0
55 while 1 == 1 {
56 if a[i] != b[i] { return 0 }
57 if a[i] == (0 as u8) { return 1 }
58 i = i + 1
59 }
60 return 1
61}
62
63// field value present? (non-null pointer AND non-empty string) -- mirrors ar_present from R0.
64func ap_present(v: *u8) -> i64 {
65 if (v as i64) == 0 { return 0 }
66 if v[0] == (0 as u8) { return 0 }
67 return 1
68}
69
70// append one (key,value) field into the parallel keys[]/vals[] arrays IFF the value is present.
71// kv[0] = current field count (mutated). One field, conditionally (matches R0's ar_addf idiom).
72func ap_addf(keys: *i64, vals: *i64, kv: *i64, key: *u8, val: *u8) -> i64 {
73 if ap_present(val) == 0 { return kv[0] }
74 let n: i64 = kv[0]
75 keys[n] = key as i64
76 vals[n] = val as i64
77 kv[0] = n + 1
78 return kv[0]
79}
80
81// ---- big-endian u32 read/write into the credential framing ----
82func ap_w32(p: *u8, off: i64, v: i64) -> i64 {
83 p[off + 0] = ((v >> 24) & 0xff) as u8
84 p[off + 1] = ((v >> 16) & 0xff) as u8
85 p[off + 2] = ((v >> 8) & 0xff) as u8
86 p[off + 3] = (v & 0xff) as u8
87 return off + 4
88}
89func ap_r32(p: *u8, off: i64) -> i64 {
90 var v: i64 = (p[off + 0] as i64) & 0xff
91 v = (v << 8) | ((p[off + 1] as i64) & 0xff)
92 v = (v << 8) | ((p[off + 2] as i64) & 0xff)
93 v = (v << 8) | ((p[off + 3] as i64) & 0xff)
94 return v
95}
96
97// ---- CLASS HELPERS (consistent with R0's ar_prov_* string vocabulary) ----
98// These return the canonical class STRING so a caller stays spelling-consistent with the record.
99// The three provenance classes the operator named: generated image / downloaded video /
100// human-authored, on the W3C PROV entity<-activity<-agent shape.
101func prov_class_human() -> *u8 { return "human\x00" as *u8 }
102func prov_class_machine() -> *u8 { return "machine\x00" as *u8 }
103func prov_class_downloaded() -> *u8 { return "downloaded\x00" as *u8 }
104
105// ---- BUILD the canonical PROV assertion bytes; entity = record_cid (the binding) ----
106// prov_make(record_cid, class, agent, tool, model, source, date, out) -> n (byte length).
107// The assertion is a canonical record (canon_encode key-sorts, so it is byte-deterministic and the
108// verifier reconstructs the SAME bytes for the SAME field set). Empty optional fields are omitted
109// (a machine assertion carries tool+model and no source; a downloaded one carries source+date and
110// no tool/model; a human one carries neither). `out` must be caller-mmap'd (callers use 4096+).
111func prov_make(record_cid: *u8, class: *u8, agent: *u8, tool: *u8, model: *u8,
112 source: *u8, date: *u8, out: *u8) -> i64 {
113 let keys: *i64 = sys_mmap(8 * 16) as *i64
114 let vals: *i64 = sys_mmap(8 * 16) as *i64
115 let kv: *i64 = sys_mmap(16) as *i64
116 kv[0] = 0
117 // entity = the asset RECORD CID: this is the cryptographic BINDING to that exact content.
118 ap_addf(keys, vals, kv, "entity\x00" as *u8, record_cid)
119 ap_addf(keys, vals, kv, "prov_class\x00" as *u8, class)
120 ap_addf(keys, vals, kv, "prov_agent\x00" as *u8, agent)
121 ap_addf(keys, vals, kv, "prov_tool\x00" as *u8, tool)
122 ap_addf(keys, vals, kv, "prov_model\x00" as *u8, model)
123 ap_addf(keys, vals, kv, "prov_source\x00" as *u8, source)
124 ap_addf(keys, vals, kv, "prov_date\x00" as *u8, date)
125 return canon_encode(keys, vals, kv[0], out)
126}
127
128// ---- SIGN the assertion -> a C2PA-style credential = framing || assertion || ed25519 signature ----
129// prov_sign(assertion, n, privkey, out_cred) -> clen (credential byte length).
130// Frames magic "NXPC1" + u32be(n) + assertion, then ed25519-signs that framed prefix (so the
131// signature covers the length + the assertion -- a length-extension or re-frame breaks it), and
132// appends the 64-byte signature. Reuses ed25519_sign_full (RFC 8032, KAT-verified). `out_cred` must
133// be caller-mmap'd (>= PROV_HDR + n + 64). privkey is the 32-byte secret seed.
134const PROV_MAGIC0: i64 = 78 // 'N'
135const PROV_MAGIC1: i64 = 88 // 'X'
136const PROV_MAGIC2: i64 = 80 // 'P'
137const PROV_MAGIC3: i64 = 67 // 'C'
138const PROV_MAGIC4: i64 = 49 // '1'
139const PROV_HDR: i64 = 9 // magic(5) + u32be assertion_len(4)
140const PROV_SIG: i64 = 64 // ed25519 signature
141
142func prov_sign(assertion: *u8, n: i64, privkey: *u8, out_cred: *u8) -> i64 {
143 out_cred[0] = PROV_MAGIC0 as u8
144 out_cred[1] = PROV_MAGIC1 as u8
145 out_cred[2] = PROV_MAGIC2 as u8
146 out_cred[3] = PROV_MAGIC3 as u8
147 out_cred[4] = PROV_MAGIC4 as u8
148 ap_w32(out_cred, 5, n)
149 var i: i64 = 0
150 while i < n { out_cred[PROV_HDR + i] = assertion[i]; i = i + 1 }
151 // sign magic || len || assertion (the framed prefix, PROV_HDR + n bytes)
152 let signed_len: i64 = PROV_HDR + n
153 let sig: *u8 = sys_mmap(PROV_SIG)
154 ed25519_sign_full(privkey, out_cred, signed_len, sig)
155 i = 0
156 while i < PROV_SIG { out_cred[signed_len + i] = sig[i]; i = i + 1 }
157 return signed_len + PROV_SIG
158}
159
160// ---- VERIFY: signature valid under trusted key AND entity bound to the demanded record CID ----
161// prov_verify(cred, clen, trusted_pubkey, expect_record_cid) -> 1 ALLOW / 0 REJECT.
162// DENY-BY-DEFAULT: returns 1 ONLY IF framing intact AND ed25519 verifies over the exact assertion
163// AND the assertion's `entity` == expect_record_cid. trusted_pubkey is the 32-byte public key.
164func prov_verify(cred: *u8, clen: i64, trusted_pubkey: *u8, expect_record_cid: *u8) -> i64 {
165 // (1) structural / framing checks (defensive at the boundary, Rule 12)
166 if clen < (PROV_HDR + PROV_SIG) { return 0 }
167 if cred[0] != PROV_MAGIC0 as u8 { return 0 }
168 if cred[1] != PROV_MAGIC1 as u8 { return 0 }
169 if cred[2] != PROV_MAGIC2 as u8 { return 0 }
170 if cred[3] != PROV_MAGIC3 as u8 { return 0 }
171 if cred[4] != PROV_MAGIC4 as u8 { return 0 }
172 let n: i64 = ap_r32(cred, 5)
173 if n < 0 { return 0 }
174 let signed_len: i64 = PROV_HDR + n
175 if (signed_len + PROV_SIG) > clen { return 0 } // assertion_len lies past the buffer -> reject
176
177 // (2) ed25519 signature over the exact framed prefix (forgery / tamper defense). The sig sits
178 // immediately after the signed prefix; verify covers magic||len||assertion byte-for-byte.
179 let sig: *u8 = ((cred as i64) + signed_len) as *u8
180 if ed25519_verify_full(trusted_pubkey, cred, signed_len, sig) != NX_ED25519_SIG_OK { return 0 }
181
182 // (3) BINDING: decode the assertion and require entity == the caller's demanded record CID.
183 // Even a perfectly-signed credential is REJECTED if it was minted for a different asset.
184 let assertion: *u8 = ((cred as i64) + PROV_HDR) as *u8
185 let dk: *i64 = sys_mmap(8 * 16) as *i64
186 let dv: *i64 = sys_mmap(8 * 16) as *i64
187 let nf: i64 = canon_decode(assertion, n, dk, dv, 14)
188 if nf < 0 { return 0 }
189 let ent: *u8 = prov_get(dk, dv, nf, "entity\x00" as *u8)
190 if ap_present(ent) == 0 { return 0 }
191 if ap_streq(ent, expect_record_cid) == 0 { return 0 }
192 return 1
193}
194
195// linear-find a decoded field's value by key name; null pointer if absent (mirrors R0's ar_get).
196func prov_get(keys: *i64, vals: *i64, nf: i64, key: *u8) -> *u8 {
197 var i: i64 = 0
198 while i < nf {
199 if ap_streq((keys[i]) as *u8, key) == 1 { return (vals[i]) as *u8 }
200 i = i + 1
201 }
202 return 0 as *u8
203}