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>
This commit is contained in:
Codex
2026-08-18 13:58:00 +02:00
co-authored by Claude Fable 5
parent 5030782a22
commit 9e6ae6277a
2 changed files with 783 additions and 0 deletions
+395
View File
@@ -0,0 +1,395 @@
package attesto
// Attesto 3 provenance verification (ATTESTO-PROVENANCE-001).
//
// Rust (edge/) is normative: it constructs commitments and capsule roots. This
// file is a verification client — it re-derives what the edge core produced and
// checks it. It cannot generate a randomizer, because outside the Local Vault
// there is nothing legitimate to commit.
//
// Canonicalization and domain hashing come from proofstream.go rather than a
// second implementation: the provenance lane hashes bytes under the same frozen
// ATTESTO-CANONICAL-JSON-001 rules as the rest of Attesto.
//
// Conformance is defined by golden-vectors/provenance-v0.1-dev/.
import (
"crypto/subtle"
"encoding/hex"
"fmt"
"sort"
)
const (
ProvenanceProtocol = "ATTESTO-PROVENANCE-001"
ProvenanceProtocolVersion = "0.1"
provenanceRandomizerBytes = 32
)
// ProvenanceDomains is the closed v1 registry. There is deliberately no generic
// attesto.provenance.v1.commitment fallback: every semantic object has its own
// domain, and an object without one is a protocol-registry change.
var ProvenanceDomains = map[string]struct{}{
"attesto.provenance.v1.asset": {},
"attesto.provenance.v1.claim": {},
"attesto.provenance.v1.evidence": {},
"attesto.provenance.v1.edge": {},
"attesto.provenance.v1.capsule_leaf": {},
"attesto.provenance.v1.capsule_node": {},
"attesto.provenance.v1.capsule_root": {},
"attesto.provenance.v1.envelope": {},
"attesto.provenance.v1.provider_result": {},
"attesto.provenance.v1.policy_result": {},
"attesto.provenance.v1.disclosure": {},
"attesto.provenance.v1.migration": {},
"attesto.provenance.v1.vault_identity": {},
"attesto.provenance.v1.attesto_mark": {},
"attesto.provenance.v1.claims_tree": {},
"attesto.provenance.v1.evidence_tree": {},
"attesto.provenance.v1.policy_tree": {},
"attesto.provenance.v1.attestation": {},
}
// TopLeafRoles is the canonical order of the six typed top-tree leaves. No
// other leaf exists in v1.
var TopLeafRoles = [6]string{
"subject_commitment",
"claims_root",
"evidence_root",
"policy_results_root",
"attestation_commitment",
"vault_identity_commitment",
}
var subtreeTreeDomain = map[string]string{
"claims": "attesto.provenance.v1.claims_tree",
"evidence": "attesto.provenance.v1.evidence_tree",
"policy_results": "attesto.provenance.v1.policy_tree",
}
var subtreeTopRole = map[string]string{
"claims": "claims_root",
"evidence": "evidence_root",
"policy_results": "policy_results_root",
}
func assertProvenanceDomain(domain string) error {
if _, ok := ProvenanceDomains[domain]; !ok {
return fmt.Errorf("unknown provenance domain: %q; there is no fallback domain", domain)
}
return nil
}
func assertRandomizer(randomizer string) error {
if len(randomizer) != provenanceRandomizerBytes*2 {
return fmt.Errorf("randomizer must be exactly %d bytes", provenanceRandomizerBytes)
}
for _, char := range randomizer {
if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f')) {
return fmt.Errorf("randomizer must be lowercase hex")
}
}
if _, err := hex.DecodeString(randomizer); err != nil {
return fmt.Errorf("randomizer must be lowercase hex")
}
return nil
}
func assertProvenanceDigest(field, digest string) error {
if len(digest) != 64 {
return fmt.Errorf("malformed digest in %s: expected 64 lowercase hex characters", field)
}
for _, char := range digest {
if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f')) {
return fmt.Errorf("malformed digest in %s: expected 64 lowercase hex characters", field)
}
}
return nil
}
// ProvenanceCommitmentDigest re-derives a randomized commitment. Verification
// only — the randomizer must already be known, which means the holder was given
// the opening.
func ProvenanceCommitmentDigest(domain string, value any, randomizer string) (string, error) {
if err := assertProvenanceDomain(domain); err != nil {
return "", err
}
if err := assertRandomizer(randomizer); err != nil {
return "", err
}
if err := AssertCommitmentSafeNumbers(value, "$"); err != nil {
return "", err
}
return DomainHashHex(domain, map[string]any{
"protocol": ProvenanceProtocol,
"protocol_version": ProvenanceProtocolVersion,
"randomizer": randomizer,
"value": value,
})
}
// VerifyProvenanceCommitment checks in constant time that (value, randomizer)
// opens expectedDigest.
func VerifyProvenanceCommitment(domain string, value any, randomizer, expectedDigest string) (bool, error) {
digest, err := ProvenanceCommitmentDigest(domain, value, randomizer)
if err != nil {
return false, err
}
return subtle.ConstantTimeCompare([]byte(digest), []byte(expectedDigest)) == 1, nil
}
func provenanceNode(domain, left, right string) (string, error) {
return DomainHashHex(domain, map[string]any{
"kind": "node",
"left": left,
"right": right,
})
}
func provenanceFold(domain string, level []string) (string, error) {
current := append([]string(nil), level...)
for len(current) > 1 {
next := make([]string, 0, (len(current)+1)/2)
for index := 0; index < len(current); index += 2 {
if index+1 >= len(current) {
next = append(next, current[index]) // promote odd node, never duplicate
continue
}
node, err := provenanceNode(domain, current[index], current[index+1])
if err != nil {
return "", err
}
next = append(next, node)
}
current = next
}
return current[0], nil
}
// SubtreeMerkleRoot folds a subtree's ordered leaves into its bare Merkle root.
func SubtreeMerkleRoot(subtree string, orderedLeaves []string) (string, error) {
domain, ok := subtreeTreeDomain[subtree]
if !ok {
return "", fmt.Errorf("unknown subtree: %q", subtree)
}
if len(orderedLeaves) == 0 {
return "", fmt.Errorf("cannot build an empty %s tree", subtree)
}
for index, leaf := range orderedLeaves {
if err := assertProvenanceDigest(fmt.Sprintf("orderedLeaves[%d]", index), leaf); err != nil {
return "", err
}
}
return provenanceFold(domain, orderedLeaves)
}
// SubtreeRoot wraps a bare Merkle root in its typed subtree root.
func SubtreeRoot(subtree, merkleRoot string, leafCount int) (string, error) {
domain, ok := subtreeTreeDomain[subtree]
if !ok {
return "", fmt.Errorf("unknown subtree: %q", subtree)
}
if err := assertProvenanceDigest("merkleRoot", merkleRoot); err != nil {
return "", err
}
return DomainHashHex(domain, map[string]any{
"kind": "root",
"tree": subtree,
"leaf_count": leafCount,
"merkle_root": merkleRoot,
})
}
// SubtreeLeafInput is one leaf awaiting canonical ordering.
type SubtreeLeafInput struct {
LeafRole string `json:"leaf_role"`
LeafID string `json:"leaf_id"`
Commitment string `json:"commitment"`
}
// OrderSubtreeLeaves orders leaves by (leaf_role, leaf_id), the frozen rule, so
// two vaults that assembled the same facts in different orders agree.
func OrderSubtreeLeaves(leaves []SubtreeLeafInput) ([]string, error) {
ordered := append([]SubtreeLeafInput(nil), leaves...)
sort.SliceStable(ordered, func(left, right int) bool {
if ordered[left].LeafRole != ordered[right].LeafRole {
return ordered[left].LeafRole < ordered[right].LeafRole
}
return ordered[left].LeafID < ordered[right].LeafID
})
seen := make(map[string]struct{}, len(ordered))
digests := make([]string, 0, len(ordered))
for _, leaf := range ordered {
key := leaf.LeafRole + "\x00" + leaf.LeafID
if _, duplicate := seen[key]; duplicate {
return nil, fmt.Errorf("duplicate leaf id %s", leaf.LeafID)
}
seen[key] = struct{}{}
if err := assertProvenanceDigest("leaf.commitment", leaf.Commitment); err != nil {
return nil, err
}
digests = append(digests, leaf.Commitment)
}
return digests, nil
}
// TopLeafDigest builds a blinded top-tree leaf. The randomizer keeps the top
// tree from leaking which subtrees are empty or shared between capsules.
func TopLeafDigest(leafRole, commitment, randomizer string) (string, error) {
known := false
for _, role := range TopLeafRoles {
if role == leafRole {
known = true
break
}
}
if !known {
return "", fmt.Errorf("unknown top leaf role: %q", leafRole)
}
if err := assertProvenanceDigest("commitment", commitment); err != nil {
return "", err
}
if err := assertRandomizer(randomizer); err != nil {
return "", err
}
return DomainHashHex("attesto.provenance.v1.capsule_leaf", map[string]any{
"leaf_role": leafRole,
"commitment": commitment,
"randomizer": randomizer,
})
}
// CapsuleRoot folds the six typed top leaves into the capsule root.
func CapsuleRoot(orderedTopLeafDigests []string) (string, error) {
if len(orderedTopLeafDigests) != len(TopLeafRoles) {
return "", fmt.Errorf("capsule tree must carry exactly %d leaves", len(TopLeafRoles))
}
merkleRoot, err := provenanceFold("attesto.provenance.v1.capsule_node", orderedTopLeafDigests)
if err != nil {
return "", err
}
return DomainHashHex("attesto.provenance.v1.capsule_root", map[string]any{
"kind": "root",
"leaf_count": len(orderedTopLeafDigests),
"merkle_root": merkleRoot,
})
}
// ProvenanceProofStep is one sibling hop in an inclusion proof.
type ProvenanceProofStep struct {
Side string `json:"side"`
Sibling string `json:"sibling"`
}
// TwoHopProof is a complete disclosure: one subtree leaf, proven to the capsule
// root.
type TwoHopProof struct {
Subtree string `json:"subtree"`
Leaf string `json:"leaf"`
SubtreeSteps []ProvenanceProofStep `json:"subtree_steps"`
SubtreeLeafCount int `json:"subtree_leaf_count"`
SubtreeRoot string `json:"subtree_root"`
TopLeafRole string `json:"top_leaf_role"`
TopLeafRandomizer string `json:"top_leaf_randomizer"`
TopSteps []ProvenanceProofStep `json:"top_steps"`
CapsuleRoot string `json:"capsule_root"`
}
func replayProvenanceProof(domain, leaf string, steps []ProvenanceProofStep) (string, error) {
current := leaf
for _, step := range steps {
if err := assertProvenanceDigest("proof.sibling", step.Sibling); err != nil {
return "", err
}
var err error
switch step.Side {
case "right":
current, err = provenanceNode(domain, current, step.Sibling)
case "left":
current, err = provenanceNode(domain, step.Sibling, current)
default:
return "", fmt.Errorf("unknown proof side: %q", step.Side)
}
if err != nil {
return "", err
}
}
return current, nil
}
// VerifyTwoHop verifies a disclosure: leaf -> subtree root -> capsule root.
//
// It returns (false, nil) for a cryptographic failure and an error for a
// malformed object, so a caller can tell "this proof does not hold" from "this
// object is not a proof".
//
// The cross-tree attack this refuses: presenting a claim leaf against
// evidence_root. It fails on two independent grounds — the subtree folds under
// a different node domain, and the top leaf binds leaf_role.
func VerifyTwoHop(proof TwoHopProof) (bool, error) {
treeDomain, ok := subtreeTreeDomain[proof.Subtree]
if !ok {
return false, fmt.Errorf("unknown subtree: %q", proof.Subtree)
}
for field, digest := range map[string]string{
"leaf": proof.Leaf,
"subtree_root": proof.SubtreeRoot,
"capsule_root": proof.CapsuleRoot,
} {
if err := assertProvenanceDigest(field, digest); err != nil {
return false, err
}
}
if proof.TopLeafRole != subtreeTopRole[proof.Subtree] {
return false, nil
}
merkleRoot, err := replayProvenanceProof(treeDomain, proof.Leaf, proof.SubtreeSteps)
if err != nil {
return false, err
}
derivedSubtreeRoot, err := SubtreeRoot(proof.Subtree, merkleRoot, proof.SubtreeLeafCount)
if err != nil {
return false, err
}
if derivedSubtreeRoot != proof.SubtreeRoot {
return false, nil
}
leaf, err := TopLeafDigest(proof.TopLeafRole, proof.SubtreeRoot, proof.TopLeafRandomizer)
if err != nil {
return false, err
}
topMerkle, err := replayProvenanceProof("attesto.provenance.v1.capsule_node", leaf, proof.TopSteps)
if err != nil {
return false, err
}
derived, err := DomainHashHex("attesto.provenance.v1.capsule_root", map[string]any{
"kind": "root",
"leaf_count": len(TopLeafRoles),
"merkle_root": topMerkle,
})
if err != nil {
return false, err
}
return subtle.ConstantTimeCompare([]byte(derived), []byte(proof.CapsuleRoot)) == 1, nil
}
// EnvelopeCoreCanonicalBytes returns the canonical bytes of the §8.3 envelope
// core: the egress envelope with signature and boundary removed, because both
// bind it and neither can be part of what they bind.
func EnvelopeCoreCanonicalBytes(envelope map[string]any) ([]byte, error) {
core := make(map[string]any, len(envelope))
for key, value := range envelope {
if key == "signature" || key == "boundary" {
continue
}
core[key] = value
}
if err := AssertCommitmentSafeNumbers(core, "$"); err != nil {
return nil, err
}
return CanonicalJSON(core)
}
+388
View File
@@ -0,0 +1,388 @@
package attesto
// Go parity against the Rust-normative provenance corpus.
//
// Rust (edge/) produced golden-vectors/provenance-v0.1-dev/. Go conforms iff it
// reproduces every "valid" vector byte-for-byte and refuses every "invalid" one.
// A corpus of only positive cases would prove the implementations can agree,
// not that either can refuse.
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func provenanceVectorDir(t *testing.T) string {
t.Helper()
dir := filepath.Join("..", "..", "golden-vectors", "provenance-v0.1-dev")
if _, err := os.Stat(dir); err != nil {
t.Fatalf("provenance vectors missing at %s: %v", dir, err)
}
return dir
}
func loadProvenanceVector(t *testing.T, name string) map[string]any {
t.Helper()
path := filepath.Join(provenanceVectorDir(t), name+".json")
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
var vector map[string]any
if err := json.Unmarshal(raw, &vector); err != nil {
t.Fatalf("parse %s: %v", path, err)
}
return vector
}
func TestProvenanceCorpusIsPresentAndTyped(t *testing.T) {
entries, err := filepath.Glob(filepath.Join(provenanceVectorDir(t), "*.json"))
if err != nil || len(entries) == 0 {
t.Fatalf("no provenance vectors: %v", err)
}
invalid := 0
for _, entry := range entries {
raw, err := os.ReadFile(entry)
if err != nil {
t.Fatalf("read %s: %v", entry, err)
}
var vector map[string]any
if err := json.Unmarshal(raw, &vector); err != nil {
t.Fatalf("parse %s: %v", entry, err)
}
if vector["protocol"] != ProvenanceProtocol {
t.Fatalf("%s: wrong protocol %v", entry, vector["protocol"])
}
switch vector["expectation"] {
case "valid":
case "invalid":
invalid++
default:
t.Fatalf("%s: bad expectation %v", entry, vector["expectation"])
}
}
if invalid < 5 {
t.Fatalf("corpus carries only %d negative vectors", invalid)
}
}
func TestProvenancePinnedRandomizerReproducesRustDigest(t *testing.T) {
vector := loadProvenanceVector(t, "provenance-commitment-valid")
digest, err := ProvenanceCommitmentDigest(
vector["domain"].(string),
vector["value"],
vector["randomizer"].(string),
)
if err != nil {
t.Fatalf("commit: %v", err)
}
if digest != vector["expected_digest"].(string) {
t.Fatalf("digest mismatch:\n got %s\nwant %s", digest, vector["expected_digest"])
}
ok, err := VerifyProvenanceCommitment(
vector["domain"].(string),
vector["value"],
vector["randomizer"].(string),
vector["expected_digest"].(string),
)
if err != nil || !ok {
t.Fatalf("verify failed: ok=%v err=%v", ok, err)
}
}
func TestProvenanceSameValueDifferentRandomizerIsUnlinkable(t *testing.T) {
vector := loadProvenanceVector(t, "provenance-commitment-randomizer-diff")
seen := map[string]bool{}
for _, entry := range vector["cases"].([]any) {
testCase := entry.(map[string]any)
digest, err := ProvenanceCommitmentDigest(
vector["domain"].(string), vector["value"], testCase["randomizer"].(string),
)
if err != nil {
t.Fatalf("commit: %v", err)
}
if digest != testCase["expected_digest"].(string) {
t.Fatalf("digest mismatch: got %s want %s", digest, testCase["expected_digest"])
}
if seen[digest] {
t.Fatal("two randomizers produced the same commitment")
}
seen[digest] = true
}
}
func TestProvenanceSameValueAcrossDomainsDoesNotCollide(t *testing.T) {
vector := loadProvenanceVector(t, "provenance-commitment-cross-domain")
seen := map[string]bool{}
for _, entry := range vector["cases"].([]any) {
testCase := entry.(map[string]any)
digest, err := ProvenanceCommitmentDigest(
testCase["domain"].(string), vector["value"], vector["randomizer"].(string),
)
if err != nil {
t.Fatalf("commit: %v", err)
}
if digest != testCase["expected_digest"].(string) {
t.Fatalf("digest mismatch: got %s want %s", digest, testCase["expected_digest"])
}
if seen[digest] {
t.Fatal("two domains produced the same commitment")
}
seen[digest] = true
}
}
func TestProvenanceUnknownDomainIsRefused(t *testing.T) {
vector := loadProvenanceVector(t, "provenance-commitment-invalid-domain")
if _, err := ProvenanceCommitmentDigest(
vector["domain"].(string), vector["value"], vector["randomizer"].(string),
); err == nil {
t.Fatal("an unknown domain must not resolve to a fallback")
}
}
func TestProvenanceMalformedRandomizersAreRefused(t *testing.T) {
vector := loadProvenanceVector(t, "provenance-commitment-invalid-randomizer")
for _, entry := range vector["cases"].([]any) {
testCase := entry.(map[string]any)
if _, err := ProvenanceCommitmentDigest(
vector["domain"].(string), vector["value"], testCase["randomizer"].(string),
); err == nil {
t.Fatalf("randomizer %q must be refused", testCase["randomizer"])
}
}
}
func TestProvenanceDomainRegistryMatchesRust(t *testing.T) {
vector := loadProvenanceVector(t, "provenance-domain-registry")
domains := vector["domains"].([]any)
if len(domains) != len(ProvenanceDomains) || len(domains) != 18 {
t.Fatalf("registry size mismatch: rust=%d go=%d", len(domains), len(ProvenanceDomains))
}
for _, domain := range domains {
if _, ok := ProvenanceDomains[domain.(string)]; !ok {
t.Fatalf("missing domain %s", domain)
}
}
for _, rejected := range vector["rejected"].([]any) {
if _, ok := ProvenanceDomains[rejected.(string)]; ok {
t.Fatalf("domain %s must not be in the registry", rejected)
}
}
}
func leafInputs(t *testing.T, raw any) []SubtreeLeafInput {
t.Helper()
encoded, err := json.Marshal(raw)
if err != nil {
t.Fatalf("marshal leaves: %v", err)
}
var leaves []SubtreeLeafInput
if err := json.Unmarshal(encoded, &leaves); err != nil {
t.Fatalf("unmarshal leaves: %v", err)
}
return leaves
}
func buildSubtree(t *testing.T, subtree string, raw any) (string, []string) {
t.Helper()
ordered, err := OrderSubtreeLeaves(leafInputs(t, raw))
if err != nil {
t.Fatalf("order %s: %v", subtree, err)
}
merkle, err := SubtreeMerkleRoot(subtree, ordered)
if err != nil {
t.Fatalf("merkle %s: %v", subtree, err)
}
root, err := SubtreeRoot(subtree, merkle, len(ordered))
if err != nil {
t.Fatalf("root %s: %v", subtree, err)
}
return root, ordered
}
func TestProvenanceCapsuleRootReproducesRustForest(t *testing.T) {
vector := loadProvenanceVector(t, "provenance-capsule-root-valid")
expected := vector["expected"].(map[string]any)
claimsRoot, claimsOrdered := buildSubtree(t, "claims", vector["claims"])
for index, want := range expected["claims_ordered_leaves"].([]any) {
if claimsOrdered[index] != want.(string) {
t.Fatalf("claims leaf %d mismatch", index)
}
}
if claimsRoot != expected["claims_root"].(string) {
t.Fatalf("claims_root mismatch:\n got %s\nwant %s", claimsRoot, expected["claims_root"])
}
evidenceRoot, _ := buildSubtree(t, "evidence", vector["evidence"])
if evidenceRoot != expected["evidence_root"].(string) {
t.Fatalf("evidence_root mismatch")
}
policyRoot, _ := buildSubtree(t, "policy_results", vector["policy_results"])
if policyRoot != expected["policy_results_root"].(string) {
t.Fatalf("policy_results_root mismatch")
}
randomizers := vector["top_randomizers"].(map[string]any)
commitments := map[string]string{
"subject_commitment": vector["subject_commitment"].(string),
"claims_root": claimsRoot,
"evidence_root": evidenceRoot,
"policy_results_root": policyRoot,
"attestation_commitment": vector["attestation_commitment"].(string),
"vault_identity_commitment": vector["vault_identity_commitment"].(string),
}
digests := make([]string, 0, len(TopLeafRoles))
for _, role := range TopLeafRoles {
digest, err := TopLeafDigest(role, commitments[role], randomizers[role].(string))
if err != nil {
t.Fatalf("top leaf %s: %v", role, err)
}
digests = append(digests, digest)
}
for index, want := range expected["top_leaf_digests"].([]any) {
if digests[index] != want.(string) {
t.Fatalf("top leaf digest %d mismatch", index)
}
}
root, err := CapsuleRoot(digests)
if err != nil {
t.Fatalf("capsule root: %v", err)
}
if root != expected["capsule_root"].(string) {
t.Fatalf("capsule_root mismatch:\n got %s\nwant %s", root, expected["capsule_root"])
}
}
func TestProvenanceOddNodesArePromotedNotDuplicated(t *testing.T) {
vector := loadProvenanceVector(t, "provenance-capsule-root-promoted-odd-node")
if vector["rule"] != "promote-odd-node" {
t.Fatalf("unexpected rule %v", vector["rule"])
}
seen := map[string]bool{}
for _, entry := range vector["cases"].([]any) {
testCase := entry.(map[string]any)
ordered, err := OrderSubtreeLeaves(leafInputs(t, testCase["leaves"]))
if err != nil {
t.Fatalf("order: %v", err)
}
merkle, err := SubtreeMerkleRoot("claims", ordered)
if err != nil {
t.Fatalf("merkle: %v", err)
}
if merkle != testCase["expected_merkle_root"].(string) {
t.Fatalf("merkle mismatch for %v leaves", testCase["leaf_count"])
}
if seen[merkle] {
t.Fatal("two tree sizes shared a root — odd node was duplicated")
}
seen[merkle] = true
}
}
func TestProvenanceLeafOrderIsIndependentOfCallerOrder(t *testing.T) {
vector := loadProvenanceVector(t, "provenance-capsule-root-valid")
leaves := leafInputs(t, vector["claims"])
reversed := make([]SubtreeLeafInput, len(leaves))
for index, leaf := range leaves {
reversed[len(leaves)-1-index] = leaf
}
forward, err := OrderSubtreeLeaves(leaves)
if err != nil {
t.Fatalf("order forward: %v", err)
}
backward, err := OrderSubtreeLeaves(reversed)
if err != nil {
t.Fatalf("order reversed: %v", err)
}
for index := range forward {
if forward[index] != backward[index] {
t.Fatal("leaf ordering depended on caller order")
}
}
}
func loadProof(t *testing.T, name string) (TwoHopProof, bool) {
t.Helper()
vector := loadProvenanceVector(t, name)
encoded, err := json.Marshal(vector["proof"])
if err != nil {
t.Fatalf("marshal proof: %v", err)
}
var proof TwoHopProof
if err := json.Unmarshal(encoded, &proof); err != nil {
t.Fatalf("unmarshal proof: %v", err)
}
return proof, vector["expected_verified"].(bool)
}
func TestProvenanceValidDisclosureVerifies(t *testing.T) {
proof, expected := loadProof(t, "provenance-disclosure-valid")
ok, err := VerifyTwoHop(proof)
if err != nil {
t.Fatalf("verify: %v", err)
}
if ok != expected {
t.Fatalf("verification mismatch: got %v want %v", ok, expected)
}
}
func TestProvenanceInvalidDisclosuresAreRefused(t *testing.T) {
for _, name := range []string{
"provenance-disclosure-cross-tree-leaf",
"provenance-disclosure-invalid-inclusion",
"provenance-disclosure-foreign-leaf",
} {
proof, expected := loadProof(t, name)
if expected {
t.Fatalf("%s should be a negative vector", name)
}
ok, err := VerifyTwoHop(proof)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if ok {
t.Fatalf("%s verified but must not", name)
}
}
}
func TestProvenanceClaimLeafCannotBeReaimedAtEvidenceRoot(t *testing.T) {
valid, _ := loadProof(t, "provenance-disclosure-valid")
cross, _ := loadProof(t, "provenance-disclosure-cross-tree-leaf")
if cross.Leaf != valid.Leaf {
t.Fatal("the vector must reuse the same claim leaf")
}
if cross.Subtree != "evidence" {
t.Fatalf("expected the evidence side, got %s", cross.Subtree)
}
ok, err := VerifyTwoHop(cross)
if err != nil {
t.Fatalf("verify: %v", err)
}
if ok {
t.Fatal("a claim leaf verified against the evidence root")
}
}
func TestProvenanceCanonicalJSONMatchesRust(t *testing.T) {
vector := loadProvenanceVector(t, "provenance-canonical-json")
for _, entry := range vector["accepted"].([]any) {
testCase := entry.(map[string]any)
rendered, err := CanonicalJSON(testCase["value"])
if err != nil {
t.Fatalf("canonical: %v", err)
}
if string(rendered) != testCase["canonical_json"].(string) {
t.Fatalf("canonical mismatch:\n got %s\nwant %s", rendered, testCase["canonical_json"])
}
}
}