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>
269 lines
11 KiB
Go
269 lines
11 KiB
Go
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")
|
|
}
|
|
}
|