c1481e17dde9cc1ac8e49d4bb7ea3eba6d913fc0
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7b34da7c78 |
feat(sdk): verify disclosures in Go and TypeScript against the same bytes
All three clients now verify a presentation the Local Vault really built through the real edge core. The fixture is checked in rather than written to satisfy the verifiers: three implementations agreeing with each other proves less than three agreeing with the producer. The fixture is generated once and not regenerated on every run — a disclosure carries fresh randomizers and a fresh signature, so comparing regenerated bytes would fail by design. Drift is caught the other way round: the Local Vault's own verifier checks the checked-in fixture, so a format change makes the producer reject its own past output. CI runs that. `bytesForSubtle` and `hexToBytes` move from private to exported in the TypeScript proofstream module rather than being duplicated. Two hex decoders that could disagree is a worse outcome than one shared internal helper. Drift testing found that **nothing tested inclusion at all**. Removing the two-hop check left every disclosure test passing in all three languages: a tampered value was caught by the commitment check, a tampered signature by the signature check, but a leaf belonging to an entirely different capsule would have been accepted. That is the one thing a disclosure is for. Each SDK now has a test that corrupts a sibling in the subtree path and another in the top path, leaving value and randomizer untouched so only the fold can catch it. Corpus coverage 26/26 and 12/12 in all three languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3aff9fa0fb |
feat(attesto3): pin the range statement and its width across all three SDKs
The statement is what gets folded into the proof transcript, and the width is derived from its bounds. A client that ordered the fields differently or picked a different width would produce proofs nobody else could verify — and the symptom would read as a broken proof rather than a divergent implementation. Both are pure arithmetic and canonical JSON, so every SDK can check them and now does. Each client gains `zk_range_width` and `validate_range_statement`. The field set is exact rather than a minimum: an extra field would bind to nothing and a missing one would change the challenges. A float bound is refused rather than truncated, which is the encoding registry's whole purpose one layer up. The width table is checked in as a vector and the Rust core asserts against that file directly rather than against a second copy of the table. Changing one now fails the other, which a duplicated constant would not have done. Corpus coverage: 17/17 provenance and 12/12 zk-range in Python, Go and TypeScript. The cross-SDK vector item is closed; Sprint 12 is down to three open items, all of which need something local work cannot supply — other architectures, a curve-library decision, and a UI. Python 107, Go ok, TypeScript 119, Local Vault 375, edge 117. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c31c1796ae |
feat(attesto3): let a verifier client say what it did not check
Sprint 12's own evidence named this the feature's largest risk: not a broken proof, but a correct proof rendered as "AI generated: true". The SDKs had no result surface at all — only cryptographic primitives — so nothing stopped a consumer from reading a predicate result however it liked. All three now carry `inspect_predicate_result`, and the rule that shapes it is that a client without ristretto255 arithmetic cannot verify a range proof and must say so. `verified_here.zk_predicate` is always `not_checked`; the issuer's own verification block is carried separately under `reported_by_issuer`. An SDK that merged the two would be passing the prover's word through as though it had confirmed it, which is the failure the whole construction exists to prevent. Inclusion, which is SHA-256, is reported as genuinely checked when the caller checked it. A result is refused outright if it omits one of the three non-claims or carries a field a consumer could render as a verdict — at any nesting depth, since `predicate.confidence` misleads exactly as well as a top-level one. The corpus is generated by calling the real evaluator, so the fixture cannot drift from the implementation, and a contract compares rather than regenerates. Extending the coverage contract to a second corpus surfaced a third one: `provenance-envelope-v0.1` matched nothing. It turned out to be guarded a different but equally strict way — its own contract pins an explicit inventory — so the contract now models both shapes. "Checked somewhere else" and "checked by nobody" can no longer look the same, and a new corpus fails until one model or the other covers it. Python 100, Go ok, TypeScript 115, Local Vault 375. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
496d622671 |
feat(attesto3): make every SDK check every vector it is able to check
Twelve of twenty-five provenance vectors were consumed by no SDK. From outside the repository that looked exactly like full coverage, which is the problem: a corpus proves nothing about an implementation that never loads it. Vectors now declare what they require. `sha256` vectors use SHA-256 and canonical JSON, which all three SDKs have, so an unconsumed one is a gap and fails a contract. `ristretto255` vectors need curve scalar arithmetic no SDK carries; those are a declared boundary with a stated reason rather than a silent skip, so the exemption cannot spread by habit. Coverage went from 13/17 reachable in each SDK to 17/17. Closing the four gaps surfaced a real verifier weakness: `capsule_root` receives digests, so by then a role is no longer visible, and a tree carrying `evidence_root` twice with `vault_identity_commitment` missing folds to a root all three SDKs accepted. Each gains `ordered_top_leaf_digests`, which requires each of the six roles exactly once, and the safe path is now the easy one. Two findings of my own drift: * The Go corpus-typing test accepted only `valid` and `invalid`, so it had been failing since the Sprint 1 recovery added vectors carrying `differs` and `rejected`. I updated Python's typing test then and not Go's, and no gate caught it because the SDK parity suites are not in the sprint gates. Fixed, and both Go and TypeScript now also require the capability declaration. * TypeScript's strict indexing caught that a missing randomizer would have reached the hash as the string "undefined". Both halves are now checked. Python 88, Go ok, TypeScript 107, Local Vault 375, edge 116, backend 1404. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f80b28c3b5 |
feat(attesto3): register the REVIEW-02 disclosure v2 and ZK range domains
Sprint 1 recovery, first item. §5.2 of REVIEW-02 adds three domains the registry did not carry: attesto.disclosure.v2, attesto.zk.range.v1.statement and attesto.zk.range.v1.transcript. They are not in the attesto.provenance.v1. namespace, and that is deliberate: disclosure v2 and the ZK range protocol are separate protocols with their own versions, so a preimage space is named after the protocol that owns it rather than the one it happens to travel with. That namespace difference meant the parity contract could not see them at all — its pattern matched attesto.provenance.v1.* only, so three new domains would have been silently unguarded. The pattern now names each protocol explicitly rather than loosening to a prefix wildcard: a looser first attempt also matched prose that mentions a namespace without a terminal segment and reported it as an unknown domain. All four registry locations updated together with the golden vector, and all three SDK parity suites plus the contract are green. The Go failure message was also corrected: it printed "rust=21 go=21" while failing on a third hardcoded expectation it never named. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9e6ae6277a |
feat(attesto3): Sprint 1 — pinned attesto-edge core + 3-language parity
Establishes the single normative cryptographic authority for the provenance lane, and freezes the boundary and Merkle semantics before any ingestion path exists to depend on them. New crate edge/ (attesto-edge) - domains.rs — the closed 18-domain v1 registry. Unknown domains are errors, never a fallback: a generic attesto.provenance.v1.commitment would let two unrelated objects share a preimage space, which is what domain separation exists to prevent. - canonical.rs — conforming ATTESTO-CANONICAL-JSON-001, not a second serializer. Floats and integers past 2^53-1 are refused with their JSON path. - commitment.rs — randomized, domain-separated commitments. Legacy Proofstream commitments stay deterministic; provenance values are low-entropy, so claim_type = "c2pa_manifest_valid" hashed deterministically is a dictionary lookup and a deterministic asset digest links a file across events. Debug for Randomizer prints <redacted>: it is C1 and Debug output reaches logs. - merkle.rs — the two-level capsule forest. A claim leaf cannot verify against evidence_root on two independent grounds: subtrees fold under different node domains, and the top leaf binds leaf_role. Odd nodes are promoted, never duplicated, matching the rule inclusion.json already pins for Proofstream. - boundary.rs — derives nothing. It shapes a request for attesto-nova, reusing the existing event-payload 16 KiB size class so Nova's closed boundary_max_len() allowlist needs no new entry. On-wire artifact is N10.R redacted. - main.rs — NDJSON surface (handshake, canonicalize, commit, capsule-root, boundary-derive, self-test), the transport the backend already speaks. Poseidon is deliberately absent. It stays in proofs/nova, reached through that crate's public boundary API, so there remains exactly one Poseidon authority. The only Nova change is making CIRCUIT_ID and PROVER_VERSION pub so the edge handshake can report the prover it wraps; its 40 tests are unchanged. Test-only randomizers are gated behind the `test-vectors` cargo feature and compiled out of release builds. A caller who can choose the randomizer can make production commitments deterministic — that is not a debug convenience, it is the vulnerability. A release build refuses one and reports accepts_caller_randomizers: false in its handshake. Conformance - golden-vectors/provenance-v0.1-dev/ — 14 Rust-authored vectors, 9 valid and 5 invalid. CI regenerates them and requires git diff --exit-code, so the committed corpus cannot drift from what the normative core produces. - Python (sdk/python/src/attesto/provenance.py, 19 tests) and Go (sdk/go/provenance.go, 14 tests) reproduce every valid vector and refuse every invalid one. Both reuse their existing canonical-JSON primitives rather than forking a second implementation. - provenance_domain_registry_contract.py pins Rust = spec = Python = Go = vector, and that no registry declares the forbidden fallback. It reads each declaration block rather than whole files, so the negative test cases that must name the fallback do not trip it. TypeScript parity is still owed and Sprint 1's DoD is not fully closed: the sdk/typescript build break recorded in the Sprint 0 baseline makes its whole suite unrunnable. Also fixes a Sprint 0 guard found by the guard itself: the naming lint scanned only tracked files, so new work read green until it was committed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |