code wiki / (root) / nx_asset_provenance.nx

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}