Files
attesto-go/provenance_parity_test.go
T
CodexandClaude Opus 5 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>
2026-08-21 16:36:13 +02:00

563 lines
18 KiB
Go

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"
"strings"
"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"])
}
// Four outcomes, deliberately distinct. "invalid" means the check ran
// and answered no; "rejected" means the input was refused before any
// check could run; "differs" is not a validity claim but a requirement
// that two values not be equal.
switch vector["expectation"] {
case "valid", "differs":
case "invalid", "rejected":
invalid++
default:
t.Fatalf("%s: bad expectation %v", entry, vector["expectation"])
}
if vector["requires"] == nil {
t.Fatalf("%s: does not declare what it requires", entry)
}
}
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)
// Eighteen provenance domains plus the three REVIEW-02 protocols that own
// their own preimage spaces: disclosure v2 and the ZK range statement and
// transcript.
const expected = 21
if len(domains) != len(ProvenanceDomains) || len(domains) != expected {
t.Fatalf(
"registry size mismatch: vector=%d go=%d expected=%d",
len(domains), len(ProvenanceDomains), expected,
)
}
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"])
}
}
}
// --------------------------------------------------- canonical claim descriptor
func TestProvenanceCanonicalClaimDescriptorReproducesRustLeaf(t *testing.T) {
// A claim leaf commits subject, authority and evidence refs, not just a
// value. REVIEW-02 §7.2 widened the descriptor precisely so a disclosure
// cannot present semantics the leaf never committed; building the older
// shape here would break every cross-language disclosure.
vector := loadProvenanceVector(t, "provenance-canonical-claim-descriptor-valid")
digest, err := ProvenanceCommitmentDigest(
vector["domain"].(string),
vector["descriptor"],
vector["randomizer"].(string),
)
if err != nil {
t.Fatalf("commit: %v", err)
}
if digest != vector["expected_digest"].(string) {
t.Fatalf("claim leaf mismatch:\n got %s\nwant %s", digest, vector["expected_digest"])
}
}
func TestProvenanceClaimBackingChangesTheLeaf(t *testing.T) {
// Two claims that read the same but rest on different backing must not share
// a leaf: sharing one would let a claim attested by one provider be presented
// as attested by another, which no later check catches.
for _, name := range []string{
"provenance-canonical-claim-descriptor-authority-mutation",
"provenance-canonical-claim-descriptor-evidence-ref-mutation",
} {
vector := loadProvenanceVector(t, name)
digest, err := ProvenanceCommitmentDigest(
vector["domain"].(string),
vector["descriptor"],
vector["randomizer"].(string),
)
if err != nil {
t.Fatalf("%s: commit: %v", name, err)
}
if digest != vector["expected_digest"].(string) {
t.Fatalf("%s: digest mismatch:\n got %s\nwant %s", name, digest, vector["expected_digest"])
}
if digest == vector["must_differ_from"].(string) {
t.Fatalf("%s: leaf collided with the unmutated claim", name)
}
}
}
// ------------------------------------------------------ malformed capsule trees
func TestProvenanceMalformedCapsuleTreeIsRefused(t *testing.T) {
// The duplicate-role case is the one that needs OrderedTopLeafDigests:
// CapsuleRoot receives digests, and by then the role is gone, so a tree
// carrying one role twice and another not at all folds to a root it accepts.
vector := loadProvenanceVector(t, "provenance-capsule-root-invalid-leaf")
valid := loadProvenanceVector(t, "provenance-capsule-root-valid")
digest := strings.Repeat("aa", 32)
randomizer := strings.Repeat("11", 32)
commitments := map[string]string{}
randomizers := map[string]string{}
for _, role := range TopLeafRoles {
commitments[role] = digest
randomizers[role] = randomizer
}
reasons := map[string]struct{}{}
cases := vector["cases"].([]any)
for _, raw := range cases {
c := raw.(map[string]any)
switch {
case c["omit_role"] != nil:
broken := map[string]string{}
for role, value := range commitments {
if role != c["omit_role"].(string) {
broken[role] = value
}
}
if _, err := OrderedTopLeafDigests(broken, randomizers); err == nil {
t.Fatalf("a tree missing %v was accepted", c["omit_role"])
}
case c["duplicate_role"] != nil:
role := c["duplicate_role"].(string)
dropped := ""
for _, candidate := range TopLeafRoles {
if candidate != role {
dropped = candidate
break
}
}
broken := map[string]string{}
for r, value := range commitments {
if r != dropped {
broken[r] = value
}
}
broken[role+"_again"] = digest
if _, err := OrderedTopLeafDigests(broken, randomizers); err == nil {
t.Fatalf("a tree carrying %s twice was accepted", role)
}
case c["malformed_commitment"] != nil:
if _, err := TopLeafDigest("claims_root", c["malformed_commitment"].(string), randomizer); err == nil {
t.Fatal("a malformed commitment was accepted")
}
case c["empty_subtree"] != nil:
if _, err := SubtreeMerkleRoot(c["empty_subtree"].(string), nil); err == nil {
t.Fatalf("an empty %v tree was accepted", c["empty_subtree"])
}
case c["duplicate_leaf_id"] != nil:
leaves := leafInputs(t, valid["claims"])
repeated := leaves[0]
repeated.LeafID = c["duplicate_leaf_id"].(string)
if _, err := OrderSubtreeLeaves([]SubtreeLeafInput{repeated, repeated}); err == nil {
t.Fatal("a duplicate leaf id was accepted")
}
default:
t.Fatalf("unhandled refusal case: %v", c)
}
reasons[c["expected_error"].(string)] = struct{}{}
}
if len(reasons) != len(cases) {
t.Fatalf("each case must fail for its own reason: %d reasons for %d cases", len(reasons), len(cases))
}
}
func TestProvenanceSafeAssemblyAcceptsAWellFormedTree(t *testing.T) {
// The refusals above must not be a function that refuses everything.
vector := loadProvenanceVector(t, "provenance-capsule-root-valid")
claimsRoot, _ := buildSubtree(t, "claims", vector["claims"])
evidenceRoot, _ := buildSubtree(t, "evidence", vector["evidence"])
policyRoot, _ := buildSubtree(t, "policy_results", vector["policy_results"])
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),
}
randomizers := map[string]string{}
for role, value := range vector["top_randomizers"].(map[string]any) {
randomizers[role] = value.(string)
}
digests, err := OrderedTopLeafDigests(commitments, randomizers)
if err != nil {
t.Fatalf("assemble: %v", err)
}
root, err := CapsuleRoot(digests)
if err != nil {
t.Fatalf("capsule root: %v", err)
}
expected := vector["expected"].(map[string]any)
if root != expected["capsule_root"].(string) {
t.Fatalf("capsule root mismatch:\n got %s\nwant %s", root, expected["capsule_root"])
}
}