Option C. A verifier bundle over a provenance stream carries provenance_root (Merkle over one leaf per event: seq_no, capsule_root, installation, key, assurance, occurred_at, under attesto.provenance.v1.bundle_tree), the event count and vault_key_lifecycle, all conditional so legacy bundle hashes are unchanged. Rust is normative (edge/src/bundle_tree.rs, nine golden vectors); Python, Go and TypeScript verify an inclusion and apply the frozen revocation rule against the receipt time offline. The inclusion endpoint in router.py lands with the next commit, which carries the shared router edits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
728 lines
24 KiB
Go
728 lines
24 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)
|
|
// Nineteen provenance domains (ADR-0014 added the bundle tree) plus the
|
|
// three REVIEW-02 protocols that own their own preimage spaces: disclosure
|
|
// v2 and the ZK range statement and transcript.
|
|
const expected = 22
|
|
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"])
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------ bundle provenance tree
|
|
|
|
func bundleLeaves(t *testing.T, raw any) []BundleLeaf {
|
|
t.Helper()
|
|
encoded, err := json.Marshal(raw)
|
|
if err != nil {
|
|
t.Fatalf("marshal leaves: %v", err)
|
|
}
|
|
var leaves []BundleLeaf
|
|
if err := json.Unmarshal(encoded, &leaves); err != nil {
|
|
t.Fatalf("unmarshal leaves: %v", err)
|
|
}
|
|
return leaves
|
|
}
|
|
|
|
func bundleInclusion(t *testing.T, raw any) BundleInclusionProof {
|
|
t.Helper()
|
|
encoded, err := json.Marshal(raw)
|
|
if err != nil {
|
|
t.Fatalf("marshal inclusion: %v", err)
|
|
}
|
|
var proof BundleInclusionProof
|
|
if err := json.Unmarshal(encoded, &proof); err != nil {
|
|
t.Fatalf("unmarshal inclusion: %v", err)
|
|
}
|
|
return proof
|
|
}
|
|
|
|
func verifyBundleInclusion(t *testing.T, proof BundleInclusionProof) bool {
|
|
t.Helper()
|
|
ok, err := VerifyBundleProvenanceInclusion(proof.ProvenanceRoot, proof.Leaf, proof.Steps, proof.LeafCount)
|
|
if err != nil {
|
|
t.Fatalf("verify inclusion: %v", err)
|
|
}
|
|
return ok
|
|
}
|
|
|
|
func TestBundleTreeReproducesRustRoot(t *testing.T) {
|
|
vector := loadProvenanceVector(t, "bundle-tree-valid")
|
|
expected := vector["expected"].(map[string]any)
|
|
leaves := bundleLeaves(t, vector["leaves"])
|
|
for index, leaf := range leaves {
|
|
digest, err := BundleProvenanceLeaf(leaf)
|
|
if err != nil {
|
|
t.Fatalf("leaf %d: %v", index, err)
|
|
}
|
|
if digest != expected["leaf_digests"].([]any)[index] {
|
|
t.Fatalf("leaf %d digest mismatch", index)
|
|
}
|
|
}
|
|
tree, err := BundleProvenanceRoot(leaves)
|
|
if err != nil {
|
|
t.Fatalf("root: %v", err)
|
|
}
|
|
if tree.LeafCount != 5 || tree.MerkleRoot != expected["merkle_root"] || tree.ProvenanceRoot != expected["provenance_root"] {
|
|
t.Fatalf("tree mismatch: %+v", tree)
|
|
}
|
|
reversed := make([]BundleLeaf, 0, len(leaves))
|
|
for index := len(leaves) - 1; index >= 0; index-- {
|
|
reversed = append(reversed, leaves[index])
|
|
}
|
|
shuffled, err := BundleProvenanceRoot(reversed)
|
|
if err != nil || shuffled.ProvenanceRoot != tree.ProvenanceRoot {
|
|
t.Fatalf("caller order must not change the root: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBundleTreeSingleLeafIsItsOwnMerkleRoot(t *testing.T) {
|
|
vector := loadProvenanceVector(t, "bundle-tree-single-leaf")
|
|
expected := vector["expected"].(map[string]any)
|
|
tree, err := BundleProvenanceRoot(bundleLeaves(t, vector["leaves"]))
|
|
if err != nil {
|
|
t.Fatalf("root: %v", err)
|
|
}
|
|
if tree.MerkleRoot != tree.OrderedLeafDigests[0] || tree.MerkleRoot != expected["merkle_root"] {
|
|
t.Fatalf("single leaf must be its own merkle root")
|
|
}
|
|
if tree.ProvenanceRoot != expected["provenance_root"] {
|
|
t.Fatalf("provenance root mismatch")
|
|
}
|
|
proof := bundleInclusion(t, vector["inclusion"])
|
|
if len(proof.Steps) != 0 || verifyBundleInclusion(t, proof) != vector["expected_verified"].(bool) {
|
|
t.Fatalf("single-leaf inclusion must verify with no steps")
|
|
}
|
|
}
|
|
|
|
func TestBundleTreeEveryLeafProvesToTheRoot(t *testing.T) {
|
|
vector := loadProvenanceVector(t, "bundle-tree-inclusion-valid")
|
|
leaves := bundleLeaves(t, loadProvenanceVector(t, "bundle-tree-valid")["leaves"])
|
|
for _, raw := range vector["cases"].([]any) {
|
|
c := raw.(map[string]any)
|
|
proof := bundleInclusion(t, c["inclusion"])
|
|
if !c["expected_verified"].(bool) || !verifyBundleInclusion(t, proof) {
|
|
t.Fatalf("seq_no %d must verify", proof.Leaf.SeqNo)
|
|
}
|
|
// The Go prover reproduces the Rust proof exactly, including the
|
|
// promoted leaf that emits no step for its odd level.
|
|
produced, err := BundleProvenanceProof(leaves, proof.Leaf.SeqNo)
|
|
if err != nil {
|
|
t.Fatalf("prove %d: %v", proof.Leaf.SeqNo, err)
|
|
}
|
|
want, _ := json.Marshal(proof)
|
|
got, _ := json.Marshal(produced)
|
|
if string(want) != string(got) {
|
|
t.Fatalf("proof for seq_no %d differs from Rust:\n%s\n%s", proof.Leaf.SeqNo, want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBundleTreeRefusesTheFrozenNegatives(t *testing.T) {
|
|
for _, name := range []string{
|
|
"bundle-tree-inclusion-wrong-root",
|
|
"bundle-tree-inclusion-wrong-seq-no",
|
|
"bundle-tree-inclusion-wrong-installation",
|
|
"bundle-tree-inclusion-wrong-capsule-root",
|
|
"bundle-tree-wrong-domain",
|
|
} {
|
|
vector := loadProvenanceVector(t, name)
|
|
if vector["expected_verified"].(bool) {
|
|
t.Fatalf("%s should be a negative vector", name)
|
|
}
|
|
if verifyBundleInclusion(t, bundleInclusion(t, vector["inclusion"])) {
|
|
t.Fatalf("%s verified but must not", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBundleTreeLeafDomainIsLoadBearing(t *testing.T) {
|
|
vector := loadProvenanceVector(t, "bundle-tree-wrong-domain")
|
|
leaf := bundleLeaves(t, []any{vector["leaf"]})[0]
|
|
digest, err := BundleProvenanceLeaf(leaf)
|
|
if err != nil {
|
|
t.Fatalf("leaf: %v", err)
|
|
}
|
|
if digest != vector["bundle_tree_leaf_digest"] || digest == vector["wrong_domain_leaf_digest"] {
|
|
t.Fatalf("leaf digest must be domain-separated")
|
|
}
|
|
}
|
|
|
|
func TestBundleTreePromotesAndRefusesADuplicateSeqNo(t *testing.T) {
|
|
vector := loadProvenanceVector(t, "bundle-tree-leaf-duplicated-not-promoted")
|
|
leaves := bundleLeaves(t, vector["leaves"])
|
|
if _, err := BundleProvenanceRoot(leaves); err == nil || !strings.Contains(err.Error(), "duplicate leaf id") {
|
|
t.Fatalf("a repeated seq_no must be refused, got %v", err)
|
|
}
|
|
five, err := BundleProvenanceRoot(leaves[:5])
|
|
if err != nil {
|
|
t.Fatalf("root: %v", err)
|
|
}
|
|
if five.MerkleRoot != vector["promoted_merkle_root"] || five.MerkleRoot == vector["duplicated_fold_merkle_root"] {
|
|
t.Fatalf("odd leaf must be promoted, never duplicated")
|
|
}
|
|
}
|
|
|
|
func TestBundleTreeRefusesASignedL3AndAnEmptyBundle(t *testing.T) {
|
|
leaf := bundleLeaves(t, loadProvenanceVector(t, "bundle-tree-valid")["leaves"])[0]
|
|
leaf.VaultAssurance = "L3"
|
|
if _, err := BundleProvenanceLeaf(leaf); err == nil {
|
|
t.Fatalf("a leaf claiming L3 must be malformed")
|
|
}
|
|
if _, err := BundleProvenanceRoot(nil); err == nil {
|
|
t.Fatalf("an empty bundle has no tree")
|
|
}
|
|
}
|