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
+522
View File
@@ -0,0 +1,522 @@
package attesto
// The bundle provenance tree (ADR-0014, Option C) and the frozen revocation
// rule an offline verifier applies through it.
//
// A verifier bundle over a provenance stream commits to the capsule roots it
// spans through one Merkle root. Everything here is a client of
// edge/src/bundle_tree.rs; the bundle-tree-* vectors pin the agreement. The
// revocation rule mirrors the platform's key_revocation.evaluate exactly, so
// the ingest path and an offline verifier reach the same verdict from the same
// facts.
import (
"crypto/subtle"
"encoding/json"
"fmt"
"sort"
"time"
)
const (
BundleTreeDomain = "attesto.provenance.v1.bundle_tree"
bundleTreeName = "bundle_provenance"
BundleInclusionKind = "bundle_provenance_inclusion"
bundleProvenanceRootKey = "provenance_root"
bundleProvenanceCountKey = "provenance_event_count"
bundleKeyLifecycleKey = "vault_key_lifecycle"
KeyStatusValid = "valid"
KeyStatusRevokedAtReceipt = "revoked_at_receipt"
KeyStatusUnknownInstallation = "unknown_installation"
KeyStatusNotEvaluated = "not_evaluated"
FlagSuspectBackdated = "suspect_backdated"
// RevocationReasonUnrecorded marks an instant migration reconstructed rather
// than measured. The verdict stands; the caller is told not to read the
// instant as measured.
RevocationReasonUnrecorded = "unrecorded"
InclusionValid = "VALID"
InclusionInvalid = "INVALID"
)
// BundleLeaf is one provenance event as the bundle tree commits to it. The
// installation, key, assurance and vault-claimed occurred_at are bound with the
// capsule root on purpose: revocation is applied to the installation that
// produced the capsule, and a leaf carrying only the root would let that
// installation be swapped under it.
type BundleLeaf struct {
SeqNo int64 `json:"seq_no"`
CapsuleRoot string `json:"capsule_root"`
InstallationID string `json:"installation_id"`
KeyID string `json:"key_id"`
VaultAssurance string `json:"vault_assurance"`
OccurredAt string `json:"occurred_at"`
}
// BundleProvenanceTree is the committed tree plus what a prover needs.
type BundleProvenanceTree struct {
LeafCount int
MerkleRoot string
ProvenanceRoot string
OrderedLeaves []BundleLeaf
OrderedLeafDigests []string
}
// BundleInclusionProof is one leaf, proven to the typed provenance root.
type BundleInclusionProof struct {
Leaf BundleLeaf `json:"leaf"`
LeafCount int `json:"leaf_count"`
Steps []ProvenanceProofStep `json:"steps"`
ProvenanceRoot string `json:"provenance_root"`
}
func bundleLeafValue(leaf BundleLeaf) (map[string]any, error) {
if leaf.SeqNo < 0 {
return nil, fmt.Errorf("bundle leaf seq_no must be a non-negative integer")
}
if leaf.InstallationID == "" || leaf.KeyID == "" || leaf.OccurredAt == "" {
return nil, fmt.Errorf("bundle leaf installation_id, key_id and occurred_at must be non-empty")
}
known := false
for _, level := range VaultAssuranceLevels {
if level == leaf.VaultAssurance {
known = true
break
}
}
if !known {
// L3 included: it is verifier-derived and has no on-wire form, so a
// leaf claiming it is malformed rather than merely invalid.
return nil, fmt.Errorf("bundle leaf vault_assurance must be one of %v", VaultAssuranceLevels)
}
if err := assertProvenanceDigest("leaf.capsule_root", leaf.CapsuleRoot); err != nil {
return nil, err
}
return map[string]any{
"kind": "leaf",
"seq_no": leaf.SeqNo,
"capsule_root": leaf.CapsuleRoot,
"installation_id": leaf.InstallationID,
"key_id": leaf.KeyID,
"vault_assurance": leaf.VaultAssurance,
"occurred_at": leaf.OccurredAt,
}, nil
}
// BundleProvenanceLeaf hashes one provenance event as the bundle tree commits
// to it.
func BundleProvenanceLeaf(leaf BundleLeaf) (string, error) {
value, err := bundleLeafValue(leaf)
if err != nil {
return "", err
}
if err := AssertCommitmentSafeNumbers(value, "$"); err != nil {
return "", err
}
return DomainHashHex(BundleTreeDomain, value)
}
func bundleTypedRoot(leafCount int, merkleRoot string) (string, error) {
return DomainHashHex(BundleTreeDomain, map[string]any{
"kind": "root",
"tree": bundleTreeName,
"leaf_count": leafCount,
"merkle_root": merkleRoot,
})
}
// BundleProvenanceRoot folds the events a bundle spans, in seq_no order, into
// its typed root. A repeated seq_no is refused: one event cannot be two leaves.
func BundleProvenanceRoot(leaves []BundleLeaf) (*BundleProvenanceTree, error) {
if len(leaves) == 0 {
return nil, fmt.Errorf("cannot build an empty %s tree", bundleTreeName)
}
ordered := append([]BundleLeaf(nil), leaves...)
sort.SliceStable(ordered, func(left, right int) bool {
return ordered[left].SeqNo < ordered[right].SeqNo
})
digests := make([]string, 0, len(ordered))
for index, leaf := range ordered {
if index > 0 && ordered[index-1].SeqNo == leaf.SeqNo {
return nil, fmt.Errorf("duplicate leaf id %d", leaf.SeqNo)
}
digest, err := BundleProvenanceLeaf(leaf)
if err != nil {
return nil, err
}
digests = append(digests, digest)
}
merkleRoot, err := provenanceFold(BundleTreeDomain, digests)
if err != nil {
return nil, err
}
root, err := bundleTypedRoot(len(digests), merkleRoot)
if err != nil {
return nil, err
}
return &BundleProvenanceTree{
LeafCount: len(digests),
MerkleRoot: merkleRoot,
ProvenanceRoot: root,
OrderedLeaves: ordered,
OrderedLeafDigests: digests,
}, nil
}
func collectProvenanceProof(domain string, level []string, index int) ([]ProvenanceProofStep, error) {
steps := []ProvenanceProofStep{}
current := append([]string(nil), level...)
for len(current) > 1 {
next := make([]string, 0, (len(current)+1)/2)
nextIndex := index
for cursor := 0; cursor < len(current); cursor += 2 {
if cursor+1 >= len(current) {
// Promoted node: it rises with no sibling, so no proof step.
if cursor == index {
nextIndex = len(next)
}
next = append(next, current[cursor])
continue
}
if cursor == index {
steps = append(steps, ProvenanceProofStep{Side: "right", Sibling: current[cursor+1]})
nextIndex = len(next)
} else if cursor+1 == index {
steps = append(steps, ProvenanceProofStep{Side: "left", Sibling: current[cursor]})
nextIndex = len(next)
}
node, err := provenanceNode(domain, current[cursor], current[cursor+1])
if err != nil {
return nil, err
}
next = append(next, node)
}
current = next
index = nextIndex
}
return steps, nil
}
// BundleProvenanceProof proves one event under the bundle's provenance root.
func BundleProvenanceProof(leaves []BundleLeaf, seqNo int64) (*BundleInclusionProof, error) {
tree, err := BundleProvenanceRoot(leaves)
if err != nil {
return nil, err
}
for index, leaf := range tree.OrderedLeaves {
if leaf.SeqNo != seqNo {
continue
}
steps, err := collectProvenanceProof(BundleTreeDomain, tree.OrderedLeafDigests, index)
if err != nil {
return nil, err
}
return &BundleInclusionProof{
Leaf: leaf,
LeafCount: tree.LeafCount,
Steps: steps,
ProvenanceRoot: tree.ProvenanceRoot,
}, nil
}
return nil, fmt.Errorf("unknown seq_no: %d", seqNo)
}
// VerifyBundleProvenanceInclusion checks that leaf sits under provenanceRoot.
//
// The leaf is re-hashed from its fields, never taken as a digest, so a proof
// cannot substitute one between leaf and root. It returns (false, nil) for a
// cryptographic failure and an error for a malformed object.
func VerifyBundleProvenanceInclusion(provenanceRoot string, leaf BundleLeaf, steps []ProvenanceProofStep, leafCount int) (bool, error) {
if err := assertProvenanceDigest("provenance_root", provenanceRoot); err != nil {
return false, err
}
if leafCount < 1 {
return false, fmt.Errorf("leaf_count must be a positive integer")
}
digest, err := BundleProvenanceLeaf(leaf)
if err != nil {
return false, err
}
merkleRoot, err := replayProvenanceProof(BundleTreeDomain, digest, steps)
if err != nil {
return false, err
}
derived, err := bundleTypedRoot(leafCount, merkleRoot)
if err != nil {
return false, err
}
return subtle.ConstantTimeCompare([]byte(derived), []byte(provenanceRoot)) == 1, nil
}
// KeyRevocationVerdict is what the key lifecycle says about one event, and why.
type KeyRevocationVerdict struct {
Status string `json:"status"`
Flags []string `json:"flags"`
RevokedAt *time.Time `json:"revoked_at"`
Reason string `json:"reason"`
// True when the effective instant was reconstructed by migration. The
// verdict still stands; the caller is told not to read it as measured.
InstantReconstructed bool `json:"instant_reconstructed"`
}
// Accepted reports whether the key was live at receipt. It says nothing about
// the signature, which is checked separately.
func (v KeyRevocationVerdict) Accepted() bool { return v.Status == KeyStatusValid }
// EvaluateKeyRevocation decides whether a key was live when the platform
// received the event.
//
// Revocation is evaluated against the platform receipt time, never the
// vault-claimed occurred_at: a holder controls what it claims, not when the
// platform received it. The boundary is inclusive — an event receipted exactly
// at the revocation instant is revoked, because the alternative gives a
// compromised key one more accepted event. A claim that predates revocation
// while its receipt does not is flagged suspect_backdated in addition to being
// revoked, not instead of.
//
// A nil revokedAt means the key was never revoked, which is a different thing
// from a key revoked in the future and must not be conflated: the second is a
// scheduled retirement and is still evidence.
func EvaluateKeyRevocation(revokedAt *time.Time, receiptTime time.Time, claimedOccurredAt *time.Time, reason string) KeyRevocationVerdict {
if revokedAt == nil {
return KeyRevocationVerdict{Status: KeyStatusValid, Flags: []string{}}
}
effective := revokedAt.UTC()
received := receiptTime.UTC()
reconstructed := reason == RevocationReasonUnrecorded
if received.Before(effective) {
return KeyRevocationVerdict{
Status: KeyStatusValid,
Flags: []string{},
RevokedAt: &effective,
Reason: reason,
InstantReconstructed: reconstructed,
}
}
flags := []string{KeyStatusRevokedAtReceipt}
if claimedOccurredAt != nil && claimedOccurredAt.UTC().Before(effective) {
flags = append(flags, FlagSuspectBackdated)
}
return KeyRevocationVerdict{
Status: KeyStatusRevokedAtReceipt,
Flags: flags,
RevokedAt: &effective,
Reason: reason,
InstantReconstructed: reconstructed,
}
}
// BundleProvenanceNotClaimed states what a verified inclusion does not prove.
var BundleProvenanceNotClaimed = []map[string]string{
{
"id": "bundle_asserts_capsule_existence_not_contents",
"statement": "The provenance_root proves this capsule root was among the events " +
"the bundle spans. It says nothing about what the capsule contains; the " +
"platform never opens one.",
},
{
"id": "revocation_evaluated_against_platform_receipt_time_not_vault_occurred_at",
"statement": "Key revocation is evaluated against the platform receipt time of " +
"this seq_no. The vault-claimed occurred_at is reported, never trusted to " +
"escape revocation.",
},
{
"id": "key_lifecycle_as_of_bundle_build",
"statement": "vault_key_lifecycle is the installation's lifecycle when the bundle " +
"was built. A revocation recorded later is not in this bundle.",
},
}
// BundleProvenanceReport is what one inclusion established against its bundle,
// fact by fact. Inclusion and KeyStatus are separate on purpose: a capsule root
// can be provably under the bundle while its key was revoked before receipt,
// and collapsing the two would lose exactly the distinction an investigator
// needs. Ok is true only when the bundle hash holds, the inclusion verifies and
// the key was live at receipt.
type BundleProvenanceReport struct {
Ok bool `json:"ok"`
Inclusion string `json:"inclusion"`
KeyStatus string `json:"key_status"`
Flags []string `json:"flags"`
SeqNo *int64 `json:"seq_no"`
InstallationID string `json:"installation_id"`
CapsuleRoot string `json:"capsule_root"`
ReceiptTime string `json:"receipt_time"`
RevokedAt string `json:"revoked_at"`
Problems []string `json:"problems"`
NotClaimed []map[string]string `json:"not_claimed"`
}
func parseRFC3339(raw string) (*time.Time, bool) {
parsed, err := time.Parse(time.RFC3339Nano, raw)
if err != nil {
return nil, false
}
return &parsed, true
}
func receiptIssuedAt(bundle map[string]any, seqNo int64) string {
for _, raw := range asSlice(bundle["receipts"]) {
item, ok := raw.(map[string]any)
if !ok {
continue
}
itemSeq, ok := item["seq_no"].(float64)
if !ok || int64(itemSeq) != seqNo {
continue
}
receipt, _ := item["receipt"].(map[string]any)
payload, _ := receipt["payload"].(map[string]any)
return toString(payload["issued_at"])
}
return ""
}
// VerifyBundleProvenance verifies one capsule's inclusion in a verifier bundle,
// offline. It needs nothing but the bundle and the inclusion object: it
// recomputes the bundle hash so the provenance root and key lifecycle are
// authenticated, checks the leaf under the root, takes the receipt time for the
// leaf's seq_no from the bundle's own receipts, and applies the frozen
// revocation rule to the installation the leaf names. Every problem is
// collected rather than returned on the first one.
func VerifyBundleProvenance(bundle map[string]any, inclusion map[string]any) BundleProvenanceReport {
problems := []string{}
payload, ok := bundle["payload"].(map[string]any)
if !ok {
payload = map[string]any{}
problems = append(problems, "invalid bundle object")
}
bundleHash := toString(bundle["bundle_hash"])
if len(payload) > 0 {
derived, err := DomainHashHex(ProofstreamDomains["bundle"], payload)
if err != nil || derived != bundleHash {
problems = append(problems, "bundle_hash mismatch")
}
}
var leaf BundleLeaf
var seqNo *int64
if rawLeaf, ok := inclusion["leaf"].(map[string]any); ok {
if encoded, err := json.Marshal(rawLeaf); err == nil {
_ = json.Unmarshal(encoded, &leaf)
}
if value, ok := rawLeaf["seq_no"].(float64); ok {
n := int64(value)
seqNo = &n
}
}
if toString(inclusion["kind"]) != BundleInclusionKind {
problems = append(problems, "inclusion is not a bundle_provenance_inclusion object")
}
if toString(inclusion["bundle_hash"]) != bundleHash {
problems = append(problems, "inclusion is for a different bundle")
}
included := InclusionInvalid
declaredRoot, hasRoot := payload[bundleProvenanceRootKey].(string)
if !hasRoot {
problems = append(problems, "bundle carries no provenance_root")
} else {
if toString(inclusion["provenance_root"]) != declaredRoot {
problems = append(problems, "inclusion names a different provenance_root")
}
leafCount, _ := inclusion["leaf_count"].(float64)
declaredCount, _ := payload[bundleProvenanceCountKey].(float64)
if leafCount != declaredCount {
problems = append(problems, "inclusion leaf_count does not match provenance_event_count")
}
var steps []ProvenanceProofStep
if encoded, err := json.Marshal(inclusion["steps"]); err == nil {
_ = json.Unmarshal(encoded, &steps)
}
verified, err := VerifyBundleProvenanceInclusion(declaredRoot, leaf, steps, int(leafCount))
switch {
case err != nil:
problems = append(problems, "inclusion is malformed: "+err.Error())
case verified:
included = InclusionValid
default:
problems = append(problems, "leaf is not included under the bundle's provenance_root")
}
}
receiptTime := ""
if seqNo != nil {
receiptTime = receiptIssuedAt(bundle, *seqNo)
}
if receiptTime == "" {
problems = append(problems, fmt.Sprintf("bundle carries no receipt for seq_no %v", formatSeqNo(seqNo)))
}
var entry map[string]any
for _, raw := range asSlice(payload[bundleKeyLifecycleKey]) {
candidate, ok := raw.(map[string]any)
if ok && toString(candidate["installation_id"]) == leaf.InstallationID && leaf.InstallationID != "" {
entry = candidate
break
}
}
keyStatus := KeyStatusUnknownInstallation
flags := []string{}
revokedAt := ""
switch {
case entry == nil:
problems = append(problems, fmt.Sprintf("installation %s is not in the bundle's key lifecycle", leaf.InstallationID))
case receiptTime == "":
keyStatus = KeyStatusNotEvaluated
default:
if toString(entry["key_id"]) != leaf.KeyID {
problems = append(problems, "key lifecycle key_id does not match the leaf")
}
received, ok := parseRFC3339(receiptTime)
if !ok {
keyStatus = KeyStatusNotEvaluated
problems = append(problems, "receipt issued_at is not an RFC 3339 instant")
break
}
var effective *time.Time
if raw := toString(entry["revoked_at"]); raw != "" {
parsed, ok := parseRFC3339(raw)
if !ok {
keyStatus = KeyStatusNotEvaluated
problems = append(problems, "key lifecycle is malformed: revoked_at is not an RFC 3339 instant")
break
}
effective = parsed
revokedAt = raw
}
claimed, _ := parseRFC3339(leaf.OccurredAt)
verdict := EvaluateKeyRevocation(effective, *received, claimed, toString(entry["revocation_reason"]))
keyStatus = verdict.Status
flags = verdict.Flags
if !verdict.Accepted() {
problems = append(problems, fmt.Sprintf(
"installation %s was revoked before seq_no %s was received", leaf.InstallationID, formatSeqNo(seqNo),
))
}
}
return BundleProvenanceReport{
Ok: len(problems) == 0 && included == InclusionValid && keyStatus == KeyStatusValid,
Inclusion: included,
KeyStatus: keyStatus,
Flags: flags,
SeqNo: seqNo,
InstallationID: leaf.InstallationID,
CapsuleRoot: leaf.CapsuleRoot,
ReceiptTime: receiptTime,
RevokedAt: revokedAt,
Problems: problems,
NotClaimed: BundleProvenanceNotClaimed,
}
}
func formatSeqNo(seqNo *int64) string {
if seqNo == nil {
return "<none>"
}
return fmt.Sprintf("%d", *seqNo)
}