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:
Codex
2026-08-23 14:08:14 +02:00
co-authored by Claude Fable 5
parent 974095c5f9
commit 2894298317
4 changed files with 960 additions and 4 deletions
+268
View File
@@ -0,0 +1,268 @@
package attesto
// ADR-0014 — a verifier bundle's provenance root, checked offline.
//
// The Merkle rules are pinned by bundle-tree-* in the golden corpus; these
// tests cover what sits on top of them: the bundle hash authenticating the root
// and the key lifecycle, the receipt time coming from the bundle's own receipts,
// and the frozen revocation rule applied to the installation the leaf names.
import (
"encoding/json"
"fmt"
"strings"
"testing"
"time"
)
const testRevokedAt = "2026-08-18T12:40:00.000Z"
func testBundleLeaf(seqNo int64, installation, occurredAt string) BundleLeaf {
if occurredAt == "" {
occurredAt = fmt.Sprintf("2026-08-18T12:3%d:00.000Z", seqNo)
}
return BundleLeaf{
SeqNo: seqNo,
CapsuleRoot: strings.Repeat(fmt.Sprintf("%02x", seqNo), 32),
InstallationID: installation,
KeyID: "key-" + installation,
VaultAssurance: "L1",
OccurredAt: occurredAt,
}
}
func testLifecycle(installation, revokedAt, reason string) map[string]any {
status := "active"
var revoked any
if revokedAt != "" {
status = "revoked"
revoked = revokedAt
}
var reasonValue any
if reason != "" {
reasonValue = reason
}
return map[string]any{
"installation_id": installation,
"key_id": "key-" + installation,
"public_key_hex": strings.Repeat("ab", 32),
"status": status,
"revoked_at": revoked,
"revocation_reason": reasonValue,
"replaced_by_installation_id": nil,
"revocation_instant_reconstructed": reason == "unrecorded",
}
}
// roundTrip turns typed values into the map[string]any shape a JSON bundle has.
func roundTrip(t *testing.T, value any) map[string]any {
t.Helper()
encoded, err := json.Marshal(value)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var out map[string]any
if err := json.Unmarshal(encoded, &out); err != nil {
t.Fatalf("unmarshal: %v", err)
}
return out
}
func testBundle(t *testing.T, leaves []BundleLeaf, lifecycle []map[string]any, issued map[int64]string) map[string]any {
t.Helper()
tree, err := BundleProvenanceRoot(leaves)
if err != nil {
t.Fatalf("root: %v", err)
}
payload := map[string]any{
"kind": "verifier-bundle",
"event_count": len(leaves),
"provenance_root": tree.ProvenanceRoot,
"provenance_event_count": tree.LeafCount,
"vault_key_lifecycle": lifecycle,
}
hash, err := DomainHashHex(ProofstreamDomains["bundle"], payload)
if err != nil {
t.Fatalf("hash: %v", err)
}
receipts := []map[string]any{}
for seqNo, moment := range issued {
receipts = append(receipts, map[string]any{
"seq_no": seqNo,
"receipt": map[string]any{"payload": map[string]any{"seq_no": seqNo, "issued_at": moment}},
})
}
return roundTrip(t, map[string]any{"payload": payload, "bundle_hash": hash, "receipts": receipts})
}
func testInclusion(t *testing.T, bundle map[string]any, leaves []BundleLeaf, seqNo int64) map[string]any {
t.Helper()
proof, err := BundleProvenanceProof(leaves, seqNo)
if err != nil {
t.Fatalf("prove: %v", err)
}
inclusion := roundTrip(t, proof)
inclusion["kind"] = BundleInclusionKind
inclusion["protocol"] = "ATTESTO-PROOFSTREAM-001"
inclusion["protocol_version"] = "0.1-alpha"
inclusion["bundle_hash"] = bundle["bundle_hash"]
return inclusion
}
var (
testLeaves = []BundleLeaf{testBundleLeaf(1, "lvi_alpha", ""), testBundleLeaf(2, "lvi_beta", ""), testBundleLeaf(3, "lvi_alpha", "")}
testIssued = map[int64]string{1: "2026-08-18T12:31:05.000Z", 2: "2026-08-18T12:32:05.000Z", 3: "2026-08-18T12:33:05.000Z"}
)
func liveLifecycle() []map[string]any {
return []map[string]any{testLifecycle("lvi_alpha", "", ""), testLifecycle("lvi_beta", "", "")}
}
func TestBundleProvenanceValidInclusionUnderLiveKeyIsAccepted(t *testing.T) {
bundle := testBundle(t, testLeaves, liveLifecycle(), testIssued)
report := VerifyBundleProvenance(bundle, testInclusion(t, bundle, testLeaves, 2))
if !report.Ok {
t.Fatalf("expected ok: %v", report.Problems)
}
if report.Inclusion != InclusionValid || report.KeyStatus != KeyStatusValid || len(report.Flags) != 0 {
t.Fatalf("unexpected report: %+v", report)
}
if *report.SeqNo != 2 || report.InstallationID != "lvi_beta" || report.ReceiptTime != testIssued[2] {
t.Fatalf("unexpected facts: %+v", report)
}
ids := map[string]bool{}
for _, claim := range report.NotClaimed {
ids[claim["id"]] = true
}
if !ids["bundle_asserts_capsule_existence_not_contents"] ||
!ids["revocation_evaluated_against_platform_receipt_time_not_vault_occurred_at"] {
t.Fatalf("non-claims missing: %v", report.NotClaimed)
}
}
func TestBundleProvenanceForeignLeafIsNotIncluded(t *testing.T) {
bundle := testBundle(t, testLeaves, liveLifecycle(), testIssued)
inclusion := testInclusion(t, bundle, testLeaves, 1)
inclusion["leaf"].(map[string]any)["capsule_root"] = strings.Repeat("ee", 32)
report := VerifyBundleProvenance(bundle, inclusion)
if report.Ok || report.Inclusion != InclusionInvalid {
t.Fatalf("foreign leaf must not be included: %+v", report)
}
// The key verdict is still reported: the two facts are independent.
if report.KeyStatus != KeyStatusValid {
t.Fatalf("key status must still be evaluated: %+v", report)
}
}
func TestBundleProvenanceWrongRootIsRefused(t *testing.T) {
bundle := testBundle(t, testLeaves, liveLifecycle(), testIssued)
inclusion := testInclusion(t, bundle, testLeaves, 1)
inclusion["provenance_root"] = strings.Repeat("ab", 32)
report := VerifyBundleProvenance(bundle, inclusion)
if report.Ok || !strings.Contains(strings.Join(report.Problems, ";"), "different provenance_root") {
t.Fatalf("wrong root must be refused: %+v", report)
}
}
func TestBundleProvenanceKeyRevokedBeforeReceipt(t *testing.T) {
leaves := []BundleLeaf{testBundleLeaf(1, "lvi_alpha", ""), testBundleLeaf(2, "lvi_beta", ""), testBundleLeaf(3, "lvi_alpha", "2026-08-18T12:45:00.000Z")}
issued := map[int64]string{1: testIssued[1], 2: testIssued[2], 3: "2026-08-18T12:45:05.000Z"}
lifecycle := []map[string]any{testLifecycle("lvi_alpha", testRevokedAt, "key_compromise"), testLifecycle("lvi_beta", "", "")}
bundle := testBundle(t, leaves, lifecycle, issued)
report := VerifyBundleProvenance(bundle, testInclusion(t, bundle, leaves, 3))
if report.Ok || report.Inclusion != InclusionValid || report.KeyStatus != KeyStatusRevokedAtReceipt {
t.Fatalf("expected revoked_at_receipt: %+v", report)
}
if strings.Join(report.Flags, ",") != "revoked_at_receipt" || report.RevokedAt != testRevokedAt {
t.Fatalf("unexpected flags: %+v", report)
}
// The same installation's earlier event, received before revocation, stands.
earlier := VerifyBundleProvenance(bundle, testInclusion(t, bundle, leaves, 1))
if !earlier.Ok || earlier.KeyStatus != KeyStatusValid {
t.Fatalf("earlier event must stand: %+v", earlier)
}
}
func TestBundleProvenanceBackdatedClaimIsFlagged(t *testing.T) {
issued := map[int64]string{1: testIssued[1], 2: testIssued[2], 3: "2026-08-18T12:45:05.000Z"}
lifecycle := []map[string]any{testLifecycle("lvi_alpha", testRevokedAt, ""), testLifecycle("lvi_beta", "", "")}
bundle := testBundle(t, testLeaves, lifecycle, issued)
report := VerifyBundleProvenance(bundle, testInclusion(t, bundle, testLeaves, 3))
if report.KeyStatus != KeyStatusRevokedAtReceipt || strings.Join(report.Flags, ",") != "revoked_at_receipt,suspect_backdated" {
t.Fatalf("expected suspect_backdated: %+v", report)
}
}
func TestBundleProvenanceUnknownInstallation(t *testing.T) {
bundle := testBundle(t, testLeaves, []map[string]any{testLifecycle("lvi_alpha", "", "")}, testIssued)
report := VerifyBundleProvenance(bundle, testInclusion(t, bundle, testLeaves, 2))
if report.Ok || report.Inclusion != InclusionValid || report.KeyStatus != KeyStatusUnknownInstallation {
t.Fatalf("expected unknown_installation: %+v", report)
}
}
func TestBundleProvenanceTamperedLifecycleBreaksTheBundleHash(t *testing.T) {
lifecycle := []map[string]any{testLifecycle("lvi_alpha", testRevokedAt, ""), testLifecycle("lvi_beta", "", "")}
bundle := testBundle(t, testLeaves, lifecycle, testIssued)
inclusion := testInclusion(t, bundle, testLeaves, 1)
entries := bundle["payload"].(map[string]any)["vault_key_lifecycle"].([]any)
entries[0].(map[string]any)["revoked_at"] = nil
report := VerifyBundleProvenance(bundle, inclusion)
if report.Ok || !strings.Contains(strings.Join(report.Problems, ";"), "bundle_hash mismatch") {
t.Fatalf("a tampered lifecycle must break the bundle hash: %+v", report)
}
}
func TestBundleProvenanceMissingReceiptLeavesTheKeyUnevaluated(t *testing.T) {
bundle := testBundle(t, testLeaves, liveLifecycle(), map[int64]string{1: testIssued[1]})
report := VerifyBundleProvenance(bundle, testInclusion(t, bundle, testLeaves, 2))
if report.Ok || report.KeyStatus != KeyStatusNotEvaluated {
t.Fatalf("expected not_evaluated: %+v", report)
}
}
func TestBundleProvenanceInclusionForAnotherBundleIsRefused(t *testing.T) {
bundle := testBundle(t, testLeaves, liveLifecycle(), testIssued)
inclusion := testInclusion(t, bundle, testLeaves, 1)
inclusion["bundle_hash"] = strings.Repeat("00", 32)
report := VerifyBundleProvenance(bundle, inclusion)
if !strings.Contains(strings.Join(report.Problems, ";"), "inclusion is for a different bundle") {
t.Fatalf("expected refusal: %+v", report)
}
}
func TestKeyRevocationMirrorsThePlatformRule(t *testing.T) {
revoked := time.Date(2026, 8, 18, 12, 40, 0, 0, time.UTC)
never := EvaluateKeyRevocation(nil, revoked, nil, "")
if never.Status != KeyStatusValid || len(never.Flags) != 0 || never.RevokedAt != nil {
t.Fatalf("never revoked must be valid: %+v", never)
}
before := EvaluateKeyRevocation(&revoked, revoked.Add(-time.Millisecond), nil, "")
if !before.Accepted() || !before.RevokedAt.Equal(revoked) {
t.Fatalf("received before revocation must be valid: %+v", before)
}
// Inclusive boundary: exactly at the instant is revoked.
at := EvaluateKeyRevocation(&revoked, revoked, nil, "")
if at.Status != KeyStatusRevokedAtReceipt || strings.Join(at.Flags, ",") != "revoked_at_receipt" {
t.Fatalf("at the instant must be revoked: %+v", at)
}
claimedBefore := revoked.Add(-time.Minute)
backdated := EvaluateKeyRevocation(&revoked, revoked.Add(time.Minute), &claimedBefore, "")
if strings.Join(backdated.Flags, ",") != "revoked_at_receipt,suspect_backdated" {
t.Fatalf("backdated claim must be flagged: %+v", backdated)
}
claimedAfter := revoked.Add(time.Minute)
honest := EvaluateKeyRevocation(&revoked, revoked.Add(time.Minute), &claimedAfter, "")
if strings.Join(honest.Flags, ",") != "revoked_at_receipt" {
t.Fatalf("honest claim must not be flagged: %+v", honest)
}
reconstructed := EvaluateKeyRevocation(&revoked, revoked.Add(-time.Hour), nil, "unrecorded")
if !reconstructed.Accepted() || !reconstructed.InstantReconstructed {
t.Fatalf("unrecorded reason must be reported: %+v", reconstructed)
}
if EvaluateKeyRevocation(&revoked, revoked.Add(-time.Hour), nil, "rotation").InstantReconstructed {
t.Fatalf("a measured instant must not be reported as reconstructed")
}
}