feat(s15): ADR-0014 accepted — bundle provenance root, key lifecycle, offline revocation
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>
This commit is contained in:
+169
-4
@@ -167,10 +167,10 @@ func TestProvenanceMalformedRandomizersAreRefused(t *testing.T) {
|
||||
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
|
||||
// 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",
|
||||
@@ -560,3 +560,168 @@ func TestProvenanceSafeAssemblyAcceptsAWellFormedTree(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user