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>
396 lines
12 KiB
Go
396 lines
12 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"
|
|
"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)
|
|
// 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"])
|
|
}
|
|
}
|
|
}
|