Help us shape the future of Go! Take the 2026 Go Developer Survey

audit

package
v0.23.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 24, 2026 License: Apache-2.0 Imports: 42 Imported by: 0

Documentation

Overview

Package audit turns an agent's durable journal into a verifiable, tamper-evident record. The journal already captures every step of a run (model turns, tool calls and results, approvals, saga outcomes); this package commits to that history with a hash chain and Merkle trees and lets you sign/verify the commitment under ed25519, ML-DSA-65, or a hybrid of both. Stdlib-only.

Security model — read this before relying on it for compliance:

  • Head computes a SHA-256 hash chain over the run's journal in its persisted order. Any modification, insertion, deletion, or reordering of a record changes the head.
  • That gives INTEGRITY (detect corruption / partial writes) unconditionally, and TAMPER-EVIDENCE only if you ANCHOR the head out-of-band — sign it with a key the app tier doesn't fully control, and/or publish it to a separate trust domain. A hash chain stored in the same database an attacker controls can be rewritten and rehashed; the guarantee is "you committed the head elsewhere, so divergence is provable." This package returns the head and provides signing; anchoring policy is yours.

Where to start: audit.go holds Head, Sign and VerifySignature; signing.go the signature schemes; merkle.go, consistency.go and sth.go the Merkle trees, consistency proofs and signed tree heads; proof.go and step.go ProofBundle and ProveStep; anchor.go and audited_store.go the anchoring of heads; evidence.go and runcert.go the EvidencePackage and RunCertificate. The reading-order map of every package is docs/ARCHITECTURE.md in the repository.

Index

Examples

Constants

View Source
const (
	// FlagDecidedAfterRecorded: the signed decision time is more than the tolerance after bide
	// recorded the decision, so the approver's clock was running ahead (a decision cannot be
	// recorded before it is made).
	FlagDecidedAfterRecorded = "decided_at is later than recorded_at by more than the tolerance: the approver's clock appears to run ahead"
	// FlagDecidedBeforeAsked: the signed decision time is before the run recorded that it paused
	// for approval. The gate orders decisions by their place in the journal, never by this time:
	// it reads only decisions on the exact call, and for a plan flow node only those recorded after
	// the gate's request (SubmitDecision refuses one recorded before it). So a decision with this
	// flag was recorded after the request; its signed time says it was made before the approver
	// was asked, which points at a clock that runs behind, or a decision signed in advance.
	FlagDecidedBeforeAsked = "" /* 211-byte string literal not displayed */
	// FlagAskedUnknown: the evidence does not say when the run asked for approval (no request or
	// pause record with a time, as from an earlier version, or a pause record lost to a crash), so
	// FlagDecidedBeforeAsked could not be checked. It is a note, not a problem.
	FlagAskedUnknown = "when the run asked for approval is not available, so decided-before-asked was not checked"
	// FlagRecordedAtInvalid: the decision's recorded_at is missing or not a canonical time. bide
	// writes one with every decision, and it is not signed, so whoever wrote the journal changed
	// or dropped it; readers show it quoted, as text, and FlagDecidedAfterRecorded is not checked.
	FlagRecordedAtInvalid = "" /* 138-byte string literal not displayed */
	// FlagRecordedOutOfOrder: the decision's recorded_at is more than the tolerance before the
	// recorded_at of a decision journaled earlier, or before the run asked: the journal's order
	// and its times disagree, so a submitter's clock is wrong or the time was not the recording's.
	FlagRecordedOutOfOrder = "" /* 186-byte string literal not displayed */
	// FlagRecordedAfterHead: the decision's recorded_at is more than the tolerance after the signed
	// tree head that covers its record was signed: the record existed by then, so the time is not
	// when it was recorded.
	FlagRecordedAfterHead = "" /* 141-byte string literal not displayed */
)

Sanity flags on a decision's times (DecisionTime.Flags). Each is a note for a person, not a finding against the gate.

View Source
const (
	// DefaultAnchorTimeout is how long AuditedEventStore waits for one head to be built, signed and
	// published before it gives up on it (see AuditedEventStore.WithTimeout).
	DefaultAnchorTimeout = 3 * time.Second
	// DefaultAnchorCooldown is how long, after a failed publish, AuditedEventStore skips publishing
	// before it probes the anchor again (see AuditedEventStore.WithCooldown).
	DefaultAnchorCooldown = 30 * time.Second
)
View Source
const (
	// CoverageCovered: a verified signed head over this journal includes the decision's record.
	CoverageCovered = "covered"
	// CoveragePending: no such head includes it yet, and neither bound has passed.
	CoveragePending = "pending"
	// CoverageOverdue: no such head includes it and a bound has passed (see Basis).
	CoverageOverdue = "overdue"
)

Coverage statuses of a recorded decision (DecisionCoverage.Status).

View Source
const (
	// BasisRecords: the journal holds at least CoverageBound.MaxRecords records after the decision
	// and none of the heads covers it. It compares journal positions with head sizes only, so no
	// clock is in it.
	BasisRecords = "records"
	// BasisAge: the decision was recorded more than CoverageBound.MaxAge (plus Tolerance) before
	// CoverageBound.Now. It compares the recording process's clock (the decision's unsigned
	// recorded_at) with the checker's, so it is the weaker basis: clocks across processes skew.
	BasisAge = "age"
)

The bases of an overdue decision (DecisionCoverage.Basis).

View Source
const (
	// ProofFormat is the format of a ProofBundle. v3 carries the record as the bytes the journal
	// stores (record_bytes) rather than a decoded record.
	ProofFormat = "bide.audit.proof.v3"
	// AbsenceFormat is the format of an AbsenceBundle. v3 carries a bide.audit.sth.v5 head.
	AbsenceFormat = "bide.audit.absence.v3"
	// RunCertificateFormat is the format of a RunCertificate. v3 carries bide.audit.sth.v5 heads
	// and bide.audit.proof.v3 bundles.
	RunCertificateFormat = "bide.audit.runcert.v3"
	// CurrentGrantFormat is the format of a CurrentGrantProof. v3 carries a bide.audit.proof.v3
	// leaf and bide.audit.grant.v2 grants.
	CurrentGrantFormat = "bide.audit.current-grant.v3"
	// EventInclusionFormat is the format of an EventInclusion. v3 proves a
	// bide.audit.event-leaf.v3 leaf, whose event JSON is snake_case.
	EventInclusionFormat = "bide.audit.event-inclusion.v3"
	// STHFormat is the format of a SignedTreeHead, and the tag of the bytes it signs. v5 signs
	// the scheme (Alg) with the head.
	STHFormat = "bide.audit.sth.v5"
	// AnchorEntryFormat is the format of an AnchorEntry.
	AnchorEntryFormat = "bide.audit.anchor-entry.v1"
)

The format tags an artifact carries in its "format" field. Each names the artifact's JSON layout and what its proofs commit to, so a verifier refuses a layout it does not read with ErrFormat instead of misreading it or failing on a field it does not know.

Before 1.0 a verifier reads only the current format of each artifact; an artifact an older release made is refused with ErrFormat, and is re-created from the journal with this release's producer. From 1.0 on, a verifier reads the current format and the one before it.

View Source
const (
	// RedactionTrailName is the name of the deployment's redaction trail in an EventStore.
	RedactionTrailName = "redact:@log"
	// RedactionRecordFormat is the format of a RedactionRecord.
	RedactionRecordFormat = "bide.audit.redaction-record.v1"
)
View Source
const (
	RedactionBegin = "begin"
	RedactionEnd   = "end"
	// RedactionAcknowledge is the phase of a record in which an operator acknowledges a planted
	// leaf (see AcknowledgeRecord).
	RedactionAcknowledge = "acknowledge"
	// RedactionHoldPlaced and RedactionHoldLifted are the phases of the records of a legal hold
	// being placed and lifted (see PlaceLegalHold and LiftLegalHold).
	RedactionHoldPlaced = "hold_placed"
	RedactionHoldLifted = "hold_lifted"
)

Phases of a RedactionRecord.

View Source
const (
	// TreeJournal is a run's journal: one leaf per record, in persisted order (NewTreeHead).
	TreeJournal = "journal"
	// TreeEvents is a run's semantic event stream (EventLog.TreeHead).
	TreeEvents = "events"
	// TreeToolUse is the sorted, distinct tool-use keys of a run's completed tool calls
	// (ToolUseKeys), the tree tool-call absence proofs verify against.
	TreeToolUse = absenceKindPrefix + "tool-use"
	// TreePolicyUsed is the sorted, distinct policy digests a run's governed actions used
	// (PolicyUsedKeys), the tree used-policy absence proofs and run certificates verify against.
	TreePolicyUsed = absenceKindPrefix + "policy-used"
)

Tree kinds, committed in every signed tree head.

View Source
const DefaultClockSkew = 5 * time.Minute

DefaultClockSkew is how far past the verifier's clock a signed head's TimestampNanos may be before it is refused: five minutes, for clocks that are not exactly in step.

View Source
const DefaultDecisionTimeTolerance = 5 * time.Minute

DefaultDecisionTimeTolerance is how far two clocks may disagree before CheckDecisionTimes flags it: a decision's signed time running ahead of its recorded time, or a recorded time out of journal order or after the signed tree head that covers it.

View Source
const EarnedScopeLimit = "limit"

EarnedScopeLimit is the scope key an earned grant sets to the earned limit (at most the root's).

View Source
const EvidenceFormat = "bide.audit.evidence.v6"

EvidenceFormat is the format/version tag written into every EvidencePackage. It is domain-separated and versioned so a verifier rejects a package produced by an incompatible layout rather than misreading it. v2 binds tree heads to their run, seals the package, and carries the earlier signed head a consistency proof starts from. v3 is v2 over journals that key a tool call's records by its encoded ID (agent.ToolResultStep, agent.ApprovalTallyStep) and a sub-agent's run by agent.SubRunID, so the record names a v3 package proves differ from a v2 package's for the same run. v4 is v3 with snake_case names in every proof it carries (the inclusion and consistency proofs spelled "Index", "Size", "Path" and "First" before) and a "format" on each ProofBundle and on the RunCertificate. v5 names the signing key by scheme and key ({alg, public_key}) rather than by ed25519 hex, carries bide.audit.proof.v3 bundles (each record as its stored bytes) and bide.audit.sth.v5 heads, and labels its actions snake_case kinds. v6 is v5 with a different seal: the log key signs a fixed 60-byte message (a 28-byte tag, a 32-byte digest), the seal tag followed by the SHA-256 of the package's canonical JSON, not the JSON itself, so a signer with a message size limit (AWS KMS signs at most 4096 bytes) can seal a package of any size. Nothing else changed, so a v5 package still verifies (under its original whole-package seal) but is never produced.

View Source
const EvidenceFormatV5 = "bide.audit.evidence.v5"

EvidenceFormatV5 is the previous evidence format, whose seal covers the whole package JSON rather than its digest. Verify reads it, so evidence sealed before v6 stays checkable; nothing produces it, and Seal refuses a package of this format (re-create the evidence to seal it as v6).

View Source
const GrantFormat = "bide.audit.grant.v2"

GrantFormat tags the canonical bytes of a grant (Grant.Bytes), which are both signed and digested. v2 names the expiry not_after_unix and tags the signed bytes as well as the digest.

View Source
const JournalExportFormat = "bide.audit.journal-export.v1"

JournalExportFormat is the format of a JournalExport.

View Source
const LiftAuthorizationFormat = "bide.audit.redaction-authorization.v1"

LiftAuthorizationFormat is the format of a LiftAuthorization.

View Source
const MaxStopRecords = 20000

MaxStopRecords bounds the records VerifyStops reads from the control run for its comparison with the trail: past it the comparison is not made and the count is a problem, so a flood of records written into the control run is a finding, not a hang. A deployment's stops number in the tens.

View Source
const NoteNoRecordedTime = "no recorded time"

NoteNoRecordedTime is DecisionCoverage.Note for a decision whose record has no usable recorded_at.

View Source
const NoteRedacted = "redacted, name not attested"

NoteRedacted is DecisionCoverage.Note for a decision whose record a redaction replaced with a tombstone: its approver, verdict and recorded_at are gone, and it is bounded like any other. The decision is listed by the name the tombstone carries, which is outside the leaf hash and so not attested by any head or signature (anyone holding an export can rename a tombstone); a signed redaction trail attests it.

View Source
const NotifyTrailExportFormat = "bide.audit.notify-trail-export.v1"

NotifyTrailExportFormat is the format of a NotifyTrailExport.

View Source
const ReceiptFormat = "bide.audit.decision-receipt.v1"

ReceiptFormat identifies a decision receipt.

View Source
const StopTrailExportFormat = "bide.audit.stop-trail-export.v1"

StopTrailExportFormat is the format of a StopTrailExport.

Variables

View Source
var (
	// ErrMalformed reports an artifact that cannot be read as what it claims to be.
	ErrMalformed = fmt.Errorf("audit: malformed artifact: %w", agent.ErrProtocol)
	// ErrFormat reports an artifact whose "format" is missing or is not the one this version of
	// the package reads: an artifact produced by an older (or newer) release. Re-create the
	// artifact with this release's producer (ProveRecord, Evidence, CertifyRun, ...) from the
	// journal.
	ErrFormat = fmt.Errorf("audit: unsupported artifact format: %w", ErrMalformed)
	// ErrNotVerified reports an artifact that was read and understood and does not hold.
	ErrNotVerified = errors.New("audit: not verified")
	// ErrCertificateBound reports a certificate-bound decision receipt given to DecisionReceipt.Verify,
	// which knows only a key: use DecisionReceipt.VerifyCertificate. It wraps ErrNotVerified.
	ErrCertificateBound = fmt.Errorf("audit: certificate-bound receipt: %w", ErrNotVerified)
	// ErrRedacted reports a journal holding a redacted record (agent.Record.Redacted) given to a
	// producer that projects what the records say (an absence key set, a used-policy set, the
	// event stream): the redacted record's content is gone, so the projection would silently omit
	// it, and a key set without it would prove absent a call the journal tree commits. Proofs of the
	// other records, and the journal root, are unaffected. It wraps agent.ErrConfig: the caller
	// asked for a claim the journal can no longer support.
	ErrRedacted = fmt.Errorf("audit: the journal holds a redacted record: %w", agent.ErrConfig)
)

The errors a verifier returns. A verifier returns nil when the artifact verifies. Otherwise its error wraps exactly one of:

  • ErrNotVerified: the artifact was read and understood, and it does not hold (a signature that does not verify, a proof against another root, a record of the wrong kind for its role);
  • ErrFormat: the artifact, or one it carries, is not of the format this version reads;
  • ErrMalformed: the artifact cannot be read as what it claims to be (JSON that does not read strictly, record bytes that do not decode, a key that is not of its scheme's form).

ErrFormat wraps ErrMalformed, and ErrMalformed wraps agent.ErrProtocol. A caller's mistake (no verifier, an invalid expected policy) wraps agent.ErrConfig instead. Only nil means verified.

View Source
var (
	// ErrRedactNoHead is a redaction of a run no verified signed head covers.
	ErrRedactNoHead = fmt.Errorf("redaction refused: no signed journal head that verifies and matches the journal covers the whole run, so its evidence is not anchored (make it first, or allow an uncovered redaction, which is recorded): %w", agent.ErrConfig)
	// ErrRedactTrailUntrusted is a redaction whose trail does not match the signed heads given: it is
	// not the trail they attest (records deleted or changed, or another trail altogether). A record
	// planted in the trail does not stop an erasure: it is reported and accounts for nothing.
	ErrRedactTrailUntrusted = fmt.Errorf("redaction refused: the redaction trail does not match its signed heads, so it is not the trail they attest (records were deleted or changed, or this is another trail): %w", agent.ErrConfig)
	// ErrRedactNoTrail is a redaction whose begin record could not be written.
	ErrRedactNoTrail = fmt.Errorf("redaction refused: the redaction trail cannot be written, so nothing was erased: %w", agent.ErrConfig)
)

Refusals of RedactJournal, each wrapping agent.ErrConfig.

EarnedRules is the ScopeRules an earned grant attenuates its root under.

View Source
var ErrCRLSignature = errors.New("audit: the CRL does not verify under the issuing CA")

ErrCRLSignature is wrapped by VerifyCRL when the CRL's algorithm is outside the profile, its signature does not verify under the issuer's certificate, or it names another issuer.

View Source
var ErrLiftUnauthorized = fmt.Errorf("legal hold lift refused: it needs the signed authorization of a second person: %w", agent.ErrConfig)

ErrLiftUnauthorized is a lift of a legal hold that needs a second person's signed authorization and has none that checks out: it is refused, and the hold stays in force.

View Source
var ErrRootMismatch = errors.New("audit: tree head root does not match")

ErrRootMismatch is wrapped by the error of a proof or check that finds a signed tree head's root is not the root of the run's journal at the head's size: the journal was changed after the head was signed, or the head is for another history.

View Source
var ErrSeqTaken = errors.New("audit: event position already holds a different leaf")

ErrSeqTaken is what EventStore.Append returns, wrapped, when a different leaf is already stored at the requested seq: the position was taken. A caller that inserts if absent uses it to tell a lost race (another writer stored its leaf there first) or a fork from a real failure, which carries some other error. Test for it with errors.Is. A retry of the same leaf is not an error.

View Source
var ErrSnapshotFull = errors.New("audit: the status snapshot is full")

ErrSnapshotFull is returned by AddCRLNoEvict when adding the CRL would evict another.

View Source
var ErrWeakKey = errors.New("audit: weak ed25519 public key")

ErrWeakKey reports an Ed25519 public key that is not a usable verification key (see CheckEd25519PublicKey).

View Source
var PolicyUsedKeys = KeySet{Kind: TreePolicyUsed, Prefix: policyUsedKeyPrefix, Key: PolicyUsedKey}

PolicyUsedKeys is the key set of policy digests exercised by governed actions, keyed "policy_used:<digest>" (see PolicyUsedKey).

View Source
var ToolUseKeys = KeySet{Kind: TreeToolUse, Prefix: toolUseKeyPrefix, Key: ToolUseKey}

ToolUseKeys is the key set of the tool calls a run's model requested, keyed "tooluse:<ToolUseID>" (see ToolUseKey). It holds every call the run started, since the run starts only a call its model requested, so it proves "no tool call with this ID was requested, and so none happened, in the run": neither completed, nor started and left unresolved, nor failed in a saga.

Functions

func AbsenceRoot

func AbsenceRoot(records []agent.Record, set KeySet) (root []byte, size int, err error)

AbsenceRoot is the RFC 6962 Merkle root over the run's sorted, distinct keys in set, and the number of keys. Anyone holding the journal recomputes it to confirm a signed key-set head reflects the run: the head's Root and Size must equal it. It refuses records it cannot project as NewAbsenceTreeHead does, so a recomputation never confirms a head that omits a record: a journal holding a redacted record is ErrRedacted (the record's keys cannot be read), and a record whose stored bytes read two ways is ErrMalformed.

func AcknowledgeRecord added in v0.21.0

func AcknowledgeRecord(ctx context.Context, store EventStore, signer Signer, deployment string, position int, reason, ticket, executor string, now time.Time) error

AcknowledgeRecord appends to the redaction trail a record, signed with signer (the redaction key), in which the operator acknowledges the leaf at position: a planted, replayed or unreadable one that VerifyRedactions reports and that the operator has investigated. The record names the leaf's position and the hash of its bytes, with the reason, the ticket and who, so VerifyRedactions then reports that leaf as acknowledged and no longer fails on it. It acknowledges nothing else: not a leaf the tool wrote, not a tombstone no pass accounts for, and not a different leaf at that position later. It refuses a position that holds no finding.

func AppendRedactionRecord added in v0.21.0

func AppendRedactionRecord(ctx context.Context, store EventStore, r RedactionRecord, signer Signer) error

AppendRedactionRecord signs r with signer (the redaction key) and appends it to the redaction trail in store, at the next position. A lost race for the position is retried; any other failure is returned.

func ApprovalArgsDigest added in v0.17.0

func ApprovalArgsDigest(s agent.ApprovalSubject) string

ApprovalArgsDigest returns the SHA-256 of the canonical arguments of s, in lowercase hex: the digest a signature over agent.ApprovalDecisionBytes(s, ...) binds. It is read out of the decision bytes themselves, at the offset their layout fixes (after the domain tag and the three length-prefixed ids), so it is the digest signed, not a second computation of it.

func AttenuatingSubAgent

func AttenuatingSubAgent(name, description string, sub *agent.Agent, cfg AttenuationConfig, opts ...agent.ToolOption) agent.Tool

AttenuatingSubAgent wraps a sub-agent as a delegation Tool that attenuates authority by default. On each call, if a signed grant is bound to ctx (WithGrant), it mints a narrower child grant (ParentRef hash-linked to the parent, checked with CheckAttenuation under rules, signed by the same signer), records it as a durable leaf in the sub-run so the chain is anchored, rebinds the sub-run's identity to the child (Actor = this sub-agent, OnBehalfOf = the parent's Subject, AuthorityRef = the child's digest), and propagates the child grant so a deeper delegation narrows again. With no grant on ctx it is a plain sub-agent (it inherits the identity), so it is safe to use either way. With a grant, it must be called from an agent run (Agent.Run), whose run scope gives each call its own sub-run (agent.SubRunID of the parent run and the tool-use id); called outside one it refuses rather than fall back to a sub-run ID that every parent run would share.

The effect is that capabilities only ever shrink down a delegation tree, by default rather than by remembering to wire it, and the whole chain is provable end to end via VerifyDelegationChain. It is a composition of the grant machinery into the sub-agent seam, not a new agent type: the wrapped `sub` still runs its own full agent loop and reasons autonomously within the narrower authority.

cfg says where the child grants are recorded and how authority narrows; opts are passed to agent.SubAgent, so agent.WithApproval makes the parent wait for a human before it delegates. AttenuatingSubAgent panics, with an error wrapping agent.ErrConfig, on a nil cfg.Store or cfg.Narrow, and on an option agent.SubAgent refuses.

func CRLTBSHash added in v0.20.0

func CRLTBSHash(der []byte) ([32]byte, bool)

CRLTBSHash is the SHA-256 of the TBSCertList of a CRL's DER, for comparing two CRLs without their (possibly randomised) signatures. It is false when der does not parse as a CertificateList.

func CheckAttenuation added in v0.7.0

func CheckAttenuation(parent, child Grant, rules ScopeRules) error

CheckAttenuation returns nil if child is a valid delegation of parent, and otherwise an error naming the first rule it breaks:

  1. child.ParentRef is parent's Digest (the hash link);
  2. child.Issuer is parent.Subject (only the holder of a grant can delegate it);
  3. child expires no later than parent: if parent.NotAfterUnix is set, child.NotAfterUnix is set and at most parent.NotAfterUnix (a child of an expiring grant can never be non-expiring);
  4. every parent scope key is in child, with the same value or, where rules has a ScopeRule for that key, a value the rule accepts as narrower.

Scope entries are constraints: each one restricts what the grant allows, so a child may add keys its parent lacks (adding a constraint narrows) but never drop or loosen one. A domain whose scope key grants rather than restricts must express it so that its ScopeRule decides narrowing (for example a comma-separated allowlist whose rule requires a subset).

func CheckCRLURL added in v0.20.0

func CheckCRLURL(raw string) (*url.URL, error)

CheckCRLURL applies the URL rules of a CRL source: http or https, a host, no userinfo, no fragment. LDAP is named because it is the one banks ask about.

func CheckDeclaredGoDebug added in v0.19.0

func CheckDeclaredGoDebug(declared, environment string) error

CheckDeclaredGoDebug compares the x509 GODEBUG settings an operator declares (CertApprover.GoDebug) with the process's real environment value, which the caller reads (os.Getenv("GODEBUG"): the engine itself reads no environment), and returns an error wrapping agent.ErrConfig when they differ. An application with a certificate-bound approver calls it at startup, and generates the declaration from the real settings, so the identity record's declared settings are the ones in force. The check is of the value at startup: a later os.Setenv("GODEBUG") is not caught, so do not change it after startup.

func CheckEd25519PublicKey added in v0.9.0

func CheckEd25519PublicKey(pub []byte) error

CheckEd25519PublicKey reports whether pub is a usable Ed25519 verification key: 32 bytes, the canonical encoding (y < p, and no sign bit on x = 0) of a point on the curve, in the prime-order subgroup ([L]A is the identity), and not the identity itself, which leaves no small-order point. It returns nil for every key crypto/ed25519.GenerateKey produces, and an error wrapping ErrWeakKey otherwise. The check does arithmetic on the public key only, so it need not run in constant time.

Cost: a key that decodes to a curve point needs a subgroup check, a scalar multiplication in math/big that takes milliseconds of CPU: 1.1 to 3.6 ms per key measured on an Apple M1 Pro, depending on load, against about 45 microseconds for a signature check. Results for such keys are kept in a small LRU cache (keyCheckCacheMax keys), and concurrent first checks of one key run it once, so a verifier that checks the same few keys pays it once per key. An application that resolves keys from untrusted input pays it for every new key an attacker sends that decodes to a point: bound or rate-limit such lookups. Keys that do not decode are refused cheaply and not cached, so they cannot fill the cache.

func CheckRedactionTrail added in v0.21.0

func CheckRedactionTrail(ctx context.Context, store EventStore, signer Signer, deployment string, prior []SignedTreeHead) error

CheckRedactionTrail checks, as RedactJournal does before it appends, that the redaction trail in store is the one the earlier heads attest, and returns ErrRedactTrailUntrusted if it is not. A planted record does not fail it.

func CheckTimestamp added in v0.8.0

func CheckTimestamp(th TreeHead, now time.Time, skew time.Duration) error

CheckTimestamp applies the timestamp rule to one signed head: its TimestampNanos must be positive and not later than now plus skew. A head that breaks the rule is an error wrapping ErrNotVerified; a negative skew is an error wrapping agent.ErrConfig.

func CheckTimestampOrder added in v0.8.0

func CheckTimestampOrder(earlier, later TreeHead) error

CheckTimestampOrder checks that later, a head that extends earlier, was not signed before it. A pair out of order is an error wrapping ErrNotVerified.

func ConvergenceLeafName added in v0.8.0

func ConvergenceLeafName(digest string) string

ConvergenceLeafName is the journal name of the convergence leaf RecordConvergence writes for digest. A verifier handed a bundle as a convergence leaf checks that its record is a StepValue of this name.

func ECDSAP256PublicKeyFromSPKI added in v0.17.0

func ECDSAP256PublicKeyFromSPKI(spki []byte) ([]byte, error)

ECDSAP256PublicKeyFromSPKI returns the 65-byte uncompressed SEC1 point (the encoding NewVerifier reads) from a DER SubjectPublicKeyInfo, which is what KMS GetPublicKey and Key Vault return. It refuses a key that is not ECDSA on P-256.

func ECDSAP256SignatureFromDER added in v0.17.0

func ECDSAP256SignatureFromDER(der []byte) ([]byte, error)

ECDSAP256SignatureFromDER turns an ASN.1 DER ECDSA-Sig-Value (SEQUENCE { r INTEGER, s INTEGER }, what AWS KMS returns) into the canonical 64-byte low-S r||s. The DER must be strictly well-formed (minimal encoding, no trailing bytes, no negative or out-of-range integer).

func ECDSAP256SignatureFromRaw added in v0.17.0

func ECDSAP256SignatureFromRaw(rs []byte) ([]byte, error)

ECDSAP256SignatureFromRaw turns a 64-byte r||s signature (what Azure Key Vault and PKCS#11 CKM_ECDSA return) into the canonical form this package verifies: the same r and the low s. It refuses a signature of the wrong length or with r or s outside [1, n-1].

func ExecLatencyBound added in v0.19.0

func ExecLatencyBound(publishDelay, period, overlap, execAge time.Duration) time.Duration

ExecLatencyBound is the published revocation latency of the execution-time re-check for one CA tier: the longest time from the CA recording a revocation to a gate refusing to execute on it, publishDelay (the time the CA takes to publish a CRL after the revocation; AD CS stamps a revocation at publication, so publish right after revoking) plus period (the CRL or delta period) plus overlap plus execAge. It is a bound on when the gate can know, not on the instant between the check and the call. With a daily CRL the number is a day; for the offline root's CRL it is the ceremony delay and the root's period, days to a week: it is not hidden, and never to be read as minutes.

func FormatPublicKey added in v0.9.0

func FormatPublicKey(alg Alg, pub []byte) string

FormatPublicKey returns the text form of a public key that ParsePublicKey reads: the scheme, a colon, and the encoded key in lowercase hex ("ed25519:1a2b...").

func GovernedPolicyDigest added in v0.8.0

func GovernedPolicyDigest(result json.RawMessage) (string, error)

GovernedPolicyDigest returns the policy digest a governed-action payload (a tool result, as an attested govern.EventTool journals it) carries. It is the one reading PolicyUsedKey and bide audit share, so an action cannot be said to run under one policy by the used-policy set and under another by the CLI. The payload is open (it may hold other fields, such as the acting identity), so its names are not checked against a type, but it must decode strictly as an object (see UnmarshalStrict: no duplicate names, valid UTF-8, no lone surrogate escapes), and the digest is read from the exact name "policy_digest" only, never from a case variant, as a reader of the file would read it. A payload without that name, or whose value is not a string, is an error.

func GrantFrom

func GrantFrom(ctx context.Context) (SignedGrant, Signer, bool)

GrantFrom returns the acting signed grant and signer bound to ctx, and whether one was set.

func Head(ctx context.Context, store *agent.Journal, runID string) ([]byte, error)

Head returns the hash-chain commitment to runID's journal: head_0 = H(domain), and head_i = H(head_{i-1} || stored(record_i)) over the records in persisted order, where stored is the bytes the journal stores for the record (agent.Record.Raw), verbatim. Two runs produce the same head iff their journals are byte-identical in the same order, so the head is a deterministic fingerprint of the entire execution history.

func JournalLeafHash added in v0.9.0

func JournalLeafHash(recordBytes []byte) []byte

JournalLeafHash returns the RFC 6962 leaf hash of a journal record from the bytes the journal stores for it: SHA-256(0x00 || "bide.audit.journal-leaf.v1\x00" || recordBytes). A redaction records it, hex-encoded, in the tombstone that replaces the bytes (see agent.Record.Redacted), so the record keeps its place in every tree over the journal.

func KeyID added in v0.9.0

func KeyID(alg string, pub []byte) string

KeyID is the key identity the audit verifiers report from KeyIDs: alg, a colon, and the lowercase hex SHA-256 of the public key's encoding (for Ed25519 the 32-byte key, for ML-DSA its FIPS 204 encoding). It depends only on the key, so every verifier over one key reports it.

func LiftLegalHold added in v0.21.0

func LiftLegalHold(ctx context.Context, trail EventStore, holder agent.Holder, signer Signer, deployment string, prior []SignedTreeHead, id, reason, ticket, executor string, now time.Time, opts ...LiftOption) error

LiftLegalHold lifts the hold id in holder (the store) after recording the lift in the redaction trail: the record comes first, so a hold is never lifted without evidence, and a failure to lift after the record leaves a hold still in force that the audit reports (HOLD_NOT_IN_STORE reads the other way, a hold the trail shows active and the store lacks; this case shows as the store holding a hold the trail shows lifted, HOLD_NOT_IN_TRAIL). It refuses a hold the store does not hold.

func NumericAtMost added in v0.7.0

func NumericAtMost(parent, child string) bool

NumericAtMost is the ScopeRule for an integer upper bound (a limit): the child's value, parsed as a base-10 integer, must be at most the parent's. Unparseable values fail closed.

func PersistEvent

func PersistEvent(ctx context.Context, evStore EventStore, runID string, seq int, e agent.RunEvent) error

PersistEvent appends one event's canonical leaf to evStore at position seq, for callers streaming events into a store directly; prefer PersistJournal for the crash-safe path. The leaf commits to a fresh random salt (agent.SaltSize bytes from crypto/rand), which is stored in the leaf. If seq is already stored, the event is salted with the stored leaf's salt, so a retry of the same event is a no-op and a different event is still refused as a fork. Do not mix it with PersistJournal on one run: the two salt the same event differently, so the second reports a fork.

func PersistJournal

func PersistJournal(ctx context.Context, evStore EventStore, jStore *agent.Journal, runID string) error

PersistJournal mirrors runID's durable event trail into evStore by projecting the journal (agent.ReplayEvents) and appending each canonical event leaf at its position. It is IDEMPOTENT: call it after each turn, once at run end, or on a schedule — re-runs append only what is new and never fork the trail, because the projection is deterministic and resume-stable. After it returns, LoadEventLog(evStore, runID) reconstructs the committed trail for anchoring/proofs even if the journal is later deleted.

Each leaf commits to its event's salt, derived from the salt of the journal record the event projects (see EventLogFromJournal), so a re-run computes the same leaf bytes and a stored leaf carries the salt its proof discloses. It errors if a source record has no agent.SaltSize salt, and refuses a journal it cannot project (ErrRedacted, ErrMalformed; see EventLogFromJournal).

The event store is a copy: redacting a journal record later does not reach it. Leaves persisted before the redaction keep the redacted record's content (a tool's arguments or result, a model turn's text), so a deployment that redacts must delete or redact the run's event trail in evStore as well.

func PlaceLegalHold added in v0.21.0

func PlaceLegalHold(ctx context.Context, trail EventStore, holder agent.Holder, signer Signer, deployment string, prior []SignedTreeHead, h agent.Hold, reason, ticket, executor string, now time.Time) error

PlaceLegalHold places h in holder (the store) and records it in the redaction trail: a record signed with signer (the redaction key) naming the hold, its matter, the runs it covers, the reason, the ticket and who. The store comes first: the protection is in force before the record exists, and a failure to record leaves a hold that the audit reports (HOLD_NOT_IN_TRAIL), never an unprotected run. The trail is checked against prior (the heads so far) first, as RedactJournal does.

func PlainDistributionPoints added in v0.21.0

func PlainDistributionPoints(c *x509.Certificate) ([][]string, bool)

PlainDistributionPoints returns the URIs of each of c's CRL distribution points that is a plain point: one that holds only a fullName (no reasons, no cRLIssuer, no relative name), in the order the certificate has them, each point's URIs in order. A point of any other shape, and a name that is not a URI, is left out. It reports false when the extension does not parse. These are the points bide matches a CRL's issuing distribution point against.

func PoliciesUsed

func PoliciesUsed(records []agent.Record) ([]string, error)

PoliciesUsed returns the sorted, distinct policy digests exercised by governed actions in the run. An auditor compares this against the approved set; for any disallowed digest it then obtains an absence proof (ProveAbsentBundle with PolicyUsedKeys) showing no action ran under it. It refuses records it cannot project, as CertifyRun does, so a recomputation never omits a policy the journal commits: a journal holding a redacted record is ErrRedacted (the redacted action's policy cannot be read), and a record whose stored bytes read two ways is ErrMalformed.

func PolicyLeafName added in v0.8.0

func PolicyLeafName(digest string) string

PolicyLeafName is the journal name of the policy leaf RecordPolicy writes for digest. A verifier handed a bundle as a policy leaf checks that its record is a StepValue of this name: any other record, such as a tool result whose output has a policy leaf's shape, anchors no policy.

func PolicyUsedKey

func PolicyUsedKey(r agent.Record) []string

PolicyUsedKey is the KeyFunc of PolicyUsedKeys (see ProveAbsent / ProveAbsentBundle) over governed-action leaves: a completed tool call whose result carries a policy_digest, as an attested govern.EventTool records. It keys by that digest, so the absence machinery commits the set of policies actually exercised in a run. Proving a digest ABSENT under this KeyFunc shows that no governed action ran under that policy; recomputing AbsenceRoot lists exactly which policies were used, so an auditor can confirm every one is in the approved set.

Scope: this is a policy-level negative ("no action ran under a disallowed policy"), which, combined with the approved policies being oracle-certified convergent and invariant-preserving, supports "no violation was admitted". It is not a per-action state-validity proof (a per-transition state digest is roadmap), and it does not close the runtime refinement gap: the runtime is differentially tested against the verified reference, not proven equal to it.

func PolicyUsedKeyFor

func PolicyUsedKeyFor(digest string) string

PolicyUsedKeyFor is the absence key for a specific policy digest: pass it to ProveAbsent / ProveAbsentBundle with PolicyUsedKeys to prove no governed action ran under that policy.

func RecordConvergence

func RecordConvergence(ctx context.Context, store *agent.Journal, runID string, cert []byte, digest string) (agent.Record, error)

RecordConvergence commits the serialized convergence certificate as a dedicated journal leaf (idempotent per (runID, digest)), so it is covered by the same STH and inclusion proofs as the policy leaf with the same digest and the actions taken under it. Pass the certificate bytes from govern.ConfluenceCertificate.Marshal and the same digest used for the policy leaf.

func RecordGrant

func RecordGrant(ctx context.Context, store *agent.Journal, runID string, sg SignedGrant) (agent.Record, error)

RecordGrant commits a signed grant as a dedicated journal leaf keyed by its digest (idempotent per (runID, digest)), so it is covered by the same signed tree head and inclusion proofs as the actions taken under it. A governed action whose Identity.AuthorityRef equals the grant's digest then links to an anchored, issuer-signed authority.

func RecordPolicy

func RecordPolicy(ctx context.Context, store *agent.Journal, runID string, policy []byte, digest string) (agent.Record, error)

RecordPolicy commits the serialized policy as a dedicated journal leaf (idempotent per (runID, digest)), so the policy is covered by the same STH and inclusion proofs as the actions taken under it. The digest is the policy owner's stable identifier (e.g. gsm.Registry.PolicyDigest); audit treats it as opaque and does not recompute it from the bytes. A verifier recomputes the digest from the disclosed bytes and runs the external oracle (bide audit verify-governance), so a leaf that lies about its digest is caught there.

func RecordRunCertificate

func RecordRunCertificate(ctx context.Context, store *agent.Journal, runID string, cert RunCertificate) (agent.Record, error)

RecordRunCertificate anchors a RunCertificate as a dedicated journal leaf (idempotent per runID), so the certificate is itself covered by a signed tree head and inclusion proofs, mirroring RecordPolicy / RecordConvergence. Anchoring the certificate lets a later verifier prove the certificate was committed in the run, not produced after the fact out of band.

Note the ordering: the certificate commits to a run STH taken at some size; anchoring it grows the journal past that size. That is intended: the run STH inside the certificate is the tree the policy/convergence bundles are proven against, and a fresh STH taken after RecordRunCertificate commits the certificate leaf itself (ProveRunCertificate proves it against that later STH).

func RecordStream added in v0.11.0

func RecordStream(log *EventLog, stream *agent.RunStream, onEvent func(agent.RunEvent)) (*agent.Result, error)

RecordStream drains stream through log, committing every event, while forwarding each event to onEvent (nil to skip), then returns the run's Result and error, as RunStream.Result does (the Result is non-nil whenever the run ID is valid, whatever the error, including *ApprovalPending and *OutcomeUnknown). One pass gives you both the live UI feed and a committed, provable audit trail. A failure to commit an event is returned only if the run itself did not fail.

func RedactJournal added in v0.21.0

RedactJournal erases the records plan names, accountably: it checks the plan can run and that a signed head covers the run, appends a begin record to the redaction trail in trail, runs agent.Redact, and appends an end record, even when the redaction failed partway (its error is in the record). If the begin record cannot be written, nothing is erased (ErrRedactNoTrail). If the end record cannot be written, the erasure stands, the begin has no end (VerifyRedactions reports it as unfinished) and the error says so; run the same plan again to finish and close it.

func RedactURL added in v0.20.0

func RedactURL(raw string) string

RedactURL removes the userinfo, query and fragment from a URL, so a token in one is never printed.

func Root

func Root(ctx context.Context, store *agent.Journal, runID string) ([]byte, error)

Root returns the RFC 6962 Merkle root committing to runID's journal (records in persisted order, each leaf = the canonical record bytes). Anchor it like Head; unlike Head it supports per-record inclusion proofs (Prove / VerifyInclusion).

func Sign

func Sign(head []byte, s Signer) ([]byte, error)

Sign returns s's signature over a head commitment ("bide.audit.head.v1\x00" || head). Anchor it (store it in a separate trust domain) to make the journal tamper-evident against later rewrites.

func StatusFresh added in v0.20.0

func StatusFresh(now, this, next time.Time, maxAge, skew, tolerance time.Duration) bool

StatusFresh is the live freshness test the status rule applies to every CRL, so a refresher's alarm cannot disagree with a gate: a CRL is fresh when now is before nextUpdate, its age is at most maxAge plus the declared thisUpdate skew, and thisUpdate is not beyond the coverage tolerance in the future. A non-positive maxAge accepts nothing.

func StopPolicyFromDoc added in v0.21.0

func StopPolicyFromDoc(d agent.StopPolicyDoc) (agent.StopPolicy, error)

StopPolicyFromDoc builds the stop policy a document names, with this package's verifiers, and validates it.

func StopVerifierFromAudit added in v0.21.0

func StopVerifierFromAudit(alg agent.Alg, pub []byte) (agent.ApproverVerifier, error)

StopVerifierFromAudit builds the verifier of a policy document's approver with this package's verifiers (agent.StopVerifierFunc): pass it to agent.WithStopVerifier, or to agent.PolicyFromDoc.

func ToolUseKey

func ToolUseKey(r agent.Record) []string

ToolUseKey is the KeyFunc of ToolUseKeys: every record that shows a call ran or may have run, keyed by its ToolUseID. The model turn that requested calls (StepModel) adds each call it requested, whether or not the call ran: a call can run and leave no other record (a retry-safe call journals no attempt marker, and a crash or a lost write can drop its result), and the request is journaled before the call starts. The completed call's result (StepToolResult), the attempt marker journaled before a side effect fired (StepAttempt), and a saga step's failure (StepSagaFail) add their ToolUseID too. An attempt marker of a durable Step carries the step's name as its ToolUseID, so it adds that name as a key: the set may hold a key no tool call has, or a call the model requested that never ran, which only makes an absence proof for that key impossible, never a wrong one. Journal records a redaction replaced cannot be projected; producers refuse a journal holding one (ErrRedacted).

func ToolUseKeyFor

func ToolUseKeyFor(toolUseID string) string

ToolUseKeyFor is the absence key for a tool-use id: pass it to ProveAbsent / ProveAbsentBundle with ToolUseKeys to prove no tool call with that id happened in the run. It mirrors PolicyUsedKeyFor for the PolicyUsedKeys key set.

func UnmarshalStrict added in v0.7.0

func UnmarshalStrict(data []byte, v any) error

UnmarshalStrict decodes a JSON proof artifact (a bundle, certificate, evidence package, or tree head) so that what a person reads in the file is exactly what is verified. encoding/json accepts a duplicate key and keeps the last value, matches keys case-insensitively (so "Size" and "SIZE" are the same field), ignores unknown fields, and rewrites invalid UTF-8; each lets a file show a reader one value while the verifier checks another. UnmarshalStrict rejects all four: duplicate names (at any depth, including inside embedded raw JSON), names that do not match a field exactly, unknown fields, and invalid UTF-8.

It also rejects the spellings encoding/json decodes lossily, so each decoded value has one spelling: an escaped lone surrogate ("\ud800"), which becomes U+FFFD, in any string decoded into Go, and, in a []byte field, base64 that is not the standard encoding of the bytes it decodes to (line breaks, which encoding/json skips, or unused bits set in the last character). JSON kept verbatim (json.RawMessage) is checked for duplicate names only: it is committed as written.

A message (agent.Message) decodes with its own UnmarshalJSON, which matches names loosely, so UnmarshalStrict checks it against its wire shape: exactly "role" and "parts", and each part exactly the fields of the part its "type" names.

An artifact with a format (ProofBundle, AbsenceBundle, RunCertificate, CurrentGrantProof, EventInclusion, EvidencePackage, SignedTreeHead, AnchorEntry, JournalExport) is checked for its "format" first, at the top level and wherever one artifact carries another: one that has none, or not the one this version reads, is refused with an error wrapping ErrFormat, before any rule above can report a field its own format names differently. Every other error wraps ErrMalformed.

It uses only the standard encoding/json: the input is checked token by token against the target type before it is decoded, so the rules above hold without depending on encoding/json/v2.

func VerifyAbsence

func VerifyAbsence(root []byte, proof Absence) error

VerifyAbsence returns nil if proof shows Key is absent from the key set committed by root: each named neighbor is included at its index, they bracket Key in sort order, and they are ADJACENT (or Key sits before the first / after the last / the set is empty). Adjacency is the load-bearing check: without it, bracketing alone does not preclude Key being present between two non-consecutive neighbors. It checks the proof against a bare root; AbsenceBundle.Verify is the form that also authenticates the root and its key set. A proof that does not hold is an error wrapping ErrNotVerified.

func VerifyAnchorInclusion

func VerifyAnchorInclusion(root []byte, entry AnchorEntry, proof Inclusion) error

VerifyAnchorInclusion returns nil if entry is the leaf at proof.Index in an anchor log of proof.Size entries committed by root, from the entry and proof alone. The entry must be of AnchorEntryFormat, carrying a head of STHFormat (ErrFormat), and state what the proof proves: its Seq is proof.Index, and its RunID is the run its signed head names (the only entries MemAnchorLog.Publish writes). An entry that misstates either, or is not the leaf, is an error wrapping ErrNotVerified, since the anchor log is another party's and a monitor reads both fields. It does not verify the entry's signed head; do that with entry.STH.Verify.

func VerifyConsistency

func VerifyConsistency(firstRoot, laterRoot []byte, proof Consistency) error

VerifyConsistency returns nil if firstRoot (an earlier Root over First records) is an append-only prefix of laterRoot (a Root over Size records), given the proof, checked from the two roots and the proof alone. A rewrite or reorder of any early record fails with an error wrapping ErrNotVerified.

func VerifyCurrentGrant added in v0.7.0

func VerifyCurrentGrant(sg SignedGrant, ledgerRunID string, p CurrentGrantProof, lastSeen *SignedTreeHead, logV Verifier) error

VerifyCurrentGrant returns nil if sg is the current earned grant of the ledger run ledgerRunID, under logV (the log key, out of band). It requires that:

  • the proof is about ledgerRunID. A grant recorded in any other run signed by the same log key (an agent run that records the grant it acts under, say) proves nothing about the ledger;
  • the proof verifies, its record is the anchored leaf of exactly sg, and that leaf is the LAST leaf of the signed ledger head (index Size-1). A grant superseded by a later promotion or demotion fails, because a later ledger head's last leaf is the newer grant;
  • when lastSeen is not nil, lastSeen is an authentic head of ledgerRunID under logV and the presented head is an append-only extension of it (checked with the proof's consistency proof), so a head older than one the verifier already saw cannot be presented as current.

How an offline verifier obtains the latest head: it takes the newest head of ledgerRunID from the anchor log (anchor the ledger through an AuditedStore, or sign and publish its heads to an Anchor), confirms that head is in the anchor log (MemAnchorLog.Prove, VerifyAnchorInclusion), and keeps the newest head it has verified as lastSeen for its next check, asking the prover for a proof from lastSeen.Size. Pass nil only on first contact with a ledger: the check then shows that sg was current at the presented head, which may be older than the latest one. Freshness beyond what the verifier has seen comes from the anchor log, not from the proof.

It does not check sg's chain to the root or its expiry; do that with VerifyDelegationChain(..., EarnedRules) and Grant.Expired. A proof that does not hold is an error wrapping ErrNotVerified; one of another format, ErrFormat.

func VerifyDelegationChain

func VerifyDelegationChain(chain []SignedGrant, issuerVerifier func(issuer string) (Verifier, bool), rules ScopeRules) error

VerifyDelegationChain returns nil if chain, ordered root-first (index 0) to leaf-last, is valid:

  1. each grant's signature verifies under its issuer's verifier (resolved via issuerVerifier),
  2. the root has no ParentRef,
  3. every other grant is a valid delegation of the previous one (CheckAttenuation with rules): hash-linked, issued by the parent's subject, expiring no later, and keeping every scope constraint equal or narrower.

It establishes that the leaf's authority descends, unbroken and never widened, from the root grant, with each hop signed by the principal it names. The caller still checks that the root's Issuer is a principal it trusts to grant that authority (issuerVerifier resolves any issuer it knows), and that the leaf is unexpired at the time of use (Grant.Expired, which needs a clock). A chain that does not hold, including one with an issuer issuerVerifier does not know, is an error wrapping ErrNotVerified.

func VerifyEventInclusion

func VerifyEventInclusion(root []byte, event agent.RunEvent, proof EventInclusion) error

VerifyEventInclusion returns nil if event, under the salt proof discloses, is the leaf at proof.Index in a log of proof.Size events committed by root: from the event and proof alone, no other events needed. The event must canonicalize identically to when it was added. A proof that does not hold is an error wrapping ErrNotVerified; one whose format is not EventInclusionFormat, ErrFormat; an event that cannot be canonicalized or a salt that is not agent.SaltSize bytes, ErrMalformed.

func VerifyInclusion

func VerifyInclusion(root, recordBytes []byte, proof Inclusion) error

VerifyInclusion returns nil if recordBytes, the bytes a journal stores for a record, are the leaf at proof.Index in a run of proof.Size records committed by root: checked from the bytes and the proof alone, no other records needed, and without decoding the bytes. Otherwise the error wraps ErrNotVerified.

func VerifySignature

func VerifySignature(head, sig []byte, v Verifier) error

VerifySignature returns nil if sig is a valid signature of head (see Sign) under v, and an error wrapping ErrNotVerified otherwise.

func WithGrant

func WithGrant(ctx context.Context, sg SignedGrant, signer Signer) context.Context

WithGrant binds the acting principal's signed grant and its signer to ctx. AttenuatingSubAgent reads this to mint a narrower, signed child grant on each delegation and rebind the sub-run's identity to it. Set it once at the root (the top principal's grant and key); each delegation then propagates the narrowed child grant automatically, so authority only ever shrinks down the tree.

A nil signer, or a typed nil (a nil pointer in the Signer interface), binds the grant with no signer: a delegation under it is refused with ErrConfig (recording nothing, so binding a signer and driving again continues it), and a rollback cannot verify a delegation against it.

func WithRollbackGrants added in v0.10.0

func WithRollbackGrants(ctx context.Context, signer Signer, grants ...SignedGrant) context.Context

WithRollbackGrants binds grants, with the signer the delegations minted under them signed their child grants with, as further parents a saga rollback accepts for the delegations it walks. A delegation's journaled grant is verified against the bound grant its ParentRef links to, so a saga whose delegations were minted under several grants (the root grant was rotated, or expired and was replaced, between drives) rolls back with every one of them bound: the acting grant (WithGrant) and the earlier ones here. A grant bound here is never minted from and is not the acting grant (GrantFrom does not return it): it only lets the rollback verify the delegations minted under it. It may have expired, as the rollback does not check expiry.

Each call adds to the grants an outer context bound, so grants signed with different signers (a rotated key) are bound by calling it once per signer. They reach the delegations the acting grant reaches (those of the run ctx is passed to, and of its plain sub-agents), never inside a delegation's sub-run: an AttenuatingSubAgent's sub-run, live or rolled back, is bound to its own journaled grant alone, so a grandchild still verifies against its parent. With a nil or typed-nil signer it binds nothing.

The journal is trusted, as everywhere in bide: a rollback verifies the journaled child grant's signature and narrowing, not that this delegation is the one that minted it. Whoever can write the journal could therefore plant a validly signed child grant of a rollback-only grant (one minted by another run, say) and have a compensation run under it; binding a grant here adds its children to what such a writer could use. Writing the journal is already trusted, so this is outside the threat model (see the delegation guide).

Types

type Absence

type Absence struct {
	Key   string    `json:"key"`             // the key claimed absent from the committed set
	Size  int       `json:"size"`            // number of committed keys the proof is against
	Left  *Neighbor `json:"left,omitempty"`  // committed key just below Key in sort order; nil if Key sorts before all
	Right *Neighbor `json:"right,omitempty"` // committed key just above Key in sort order; nil if Key sorts after all
}

Absence is a non-membership proof: Key is not among the Size committed keys, shown by the adjacent committed keys that bracket it. Left is nil when Key sorts before all keys; Right is nil when it sorts after all; both nil only for the empty key set.

What it discloses: the absent key, the number of distinct keys in the set (Size), and, in plain text, the one or two committed keys adjacent to it in sort order with their indices, which place them in the sorted set. For ToolUseKeys that is the ID of up to two tool calls the run did make; for PolicyUsedKeys, up to two policy digests the run did use. This is inherent to a sorted-set absence proof: the verifier must see the neighbours to check they bracket the key. It does not disclose the neighbours' journal records (their tool names, arguments, or results), and each neighbour's audit path carries hashes of further keys, which are not salted: a holder of the proof who can guess a key of the set (a policy digest is public; a tool-use ID usually is not) can confirm it against those hashes.

func ProveAbsent

func ProveAbsent(records []agent.Record, set KeySet, key string) (Absence, error)

ProveAbsent builds an absence proof for key over records in set, or errors if the key is actually present (you cannot prove absence of something that happened) or does not carry the set's prefix (it could never be present, so its absence would say nothing).

type AbsenceBundle

type AbsenceBundle struct {
	Format  string         `json:"format"`  // AbsenceFormat
	RunID   string         `json:"run_id"`  // the run whose key set the proof is against; must equal STH.RunID
	Absence Absence        `json:"absence"` // the non-membership proof
	STH     SignedTreeHead `json:"sth"`     // the signed commitment to the key set the proof is proven against
}

AbsenceBundle is the portable, anchorable form of an absence proof, mirroring ProofBundle: the proof plus the signed tree head committing the key set it is proven against. The STH commits to the key set's kind, the run, and the journal tree the set was projected from.

func ProveAbsentBundle

func ProveAbsentBundle(records []agent.Record, set KeySet, key string, sth SignedTreeHead) (AbsenceBundle, error)

ProveAbsentBundle builds an anchorable absence proof bound to sth, a signed key-set head of set (see SignAbsenceRoot). records is the run's journal: its prefix of sth.Journal.Size records must hash to sth.Journal.Root, and the key set projected from that prefix must be the one sth signs (the fidelity guard: a stale, foreign, or wrong-kind STH is rejected).

func (AbsenceBundle) Verify

func (b AbsenceBundle) Verify(v Verifier, set KeySet) error

Verify returns nil if the bundle authentically proves Absence.Key absent from the key set set of run RunID, under v (obtained out of band). It checks the STH signature, that the STH is a head of set's kind for RunID (with its source journal named), that the key carries the set's prefix, that the proof is bound to the signed size, and non-membership under the signed root. The absence holds for the journal prefix STH.Journal names; confirm from the anchor log that it is the run's latest head before reading it as "never happened in the run." A bundle that does not hold is an error wrapping ErrNotVerified; one of another format, ErrFormat.

type Alg added in v0.9.0

type Alg = agent.Alg

Alg names a signature scheme. It is agent.Alg, so the scheme an approver decision is journaled under (agent.Record.ApproverAlg) and the scheme of an audit Verifier are one type.

const (
	AlgEd25519 Alg = "ed25519"           // classical Ed25519 (small, fast, FIPS-approved)
	AlgMLDSA65 Alg = "ml-dsa-65"         // post-quantum ML-DSA-65 (FIPS 204)
	AlgHybrid  Alg = "ed25519+ml-dsa-65" // both schemes; valid only if both verify

)

The signature schemes.

const AlgECDSAP256 Alg = "ecdsa-p256"

AlgECDSAP256 is the ECDSA P-256 scheme.

func CertDecisionKey added in v0.19.0

func CertDecisionKey(leaf *x509.Certificate) (Alg, []byte, error)

CertDecisionKey returns the decision scheme of a leaf certificate's public key and the key in the encoding NewVerifier and a signer's PublicKey use, so a command that is about to sign with a key can check, before it signs, that the key is the certificate's. Only ed25519, ecdsa-p256 and ml-dsa-65 keys have a scheme; the error is a *CertError otherwise. It checks nothing else about the certificate.

type Anchor

type Anchor interface {
	Publish(ctx context.Context, runID string, sth SignedTreeHead) error
}

Anchor is the bring-your-own port for out-of-band anchoring: publish a SignedTreeHead to a trust domain separate from the app (a Certificate-Transparency-style log, a notary / timestamping service, another account's WORM store, a public ledger). Bide ships MemAnchorLog as a reference; you implement Publish against the external log you trust.

Order: one AuditedStore publishes a run's heads in increasing size. When two processes anchor the same run (around a lease handoff), a smaller head can arrive after a larger one. Both are valid commitments to prefixes of one journal, so a monitor should check consistency between a run's heads ordered by size, not by arrival.

type AnchorEntry

type AnchorEntry struct {
	Format string         `json:"format"` // AnchorEntryFormat
	Seq    int            `json:"seq"`    // the entry's position in the anchor log
	RunID  string         `json:"run_id"` // the run whose commitment was published
	STH    SignedTreeHead `json:"sth"`    // the published signed tree head
}

AnchorEntry is one published commitment recorded by a transparency log: the run it commits to, its signed tree head, and the entry's position in the anchor log.

Anchor leaves are not salted. An anchor-log proof's path hashes cover neighbouring entries, and a neighbour's sequence number and run ID may be guessable, but its signed head is not: the head's root commits to salted leaves and its Ed25519 signature needs the signing key, so a proof holder can confirm a neighbouring entry only by already holding that exact signed head, which tells them only that the head they hold was anchored there. A head published without a signature has no such entropy; do not anchor one.

type ApprovalVerdict added in v0.4.0

type ApprovalVerdict struct {
	// Need is the expected policy's k.
	Need int `json:"need"`
	// ToolName and Args are the call the approvers decided on, from the proven request.
	ToolName string          `json:"tool_name"`
	Args     json.RawMessage `json:"args,omitempty"`
	// Counted lists the approvers whose approval counted in the recount, in journal order.
	Counted []string `json:"counted"`
	// DeniedBy lists the approvers whose counted decision was a denial.
	DeniedBy []string `json:"denied_by,omitempty"`
	// Ignored lists every other disclosed decision, with the reason it did not count.
	Ignored []IgnoredDecision `json:"ignored,omitempty"`
	// Times lists the two times of every counted decision (approvals and denials), in journal
	// order, with sanity flags under DefaultDecisionTimeTolerance (see FlagDecisionTimes). They are
	// for a person reading the verdict: no time, and no flag, affects the count or OK.
	Times []DecisionTime `json:"times,omitempty"`
	// AskedAt is when the run asked for approval, by the run's clock: for a gated plan flow node the
	// recorded_at of its proven request (agent.ApprovalRequest), else that of the proven
	// approval-pending record (agent.ApprovalWait); "" when the evidence holds neither with a time
	// (each decision is then flagged FlagAskedUnknown).
	AskedAt string `json:"asked_at,omitempty"`
	// Requesters are the requesters the recount excluded, from the proven run:principal record
	// (agent.RunPrincipal.Requesters: the principals of the runs above the run, root first, then the
	// run's own), when the expected policy excludes the requester
	// (agent.ApprovalPolicy.ExcludePrincipal); empty otherwise, or when the evidence does not prove
	// one.
	Requesters []string `json:"requesters,omitempty"`
	// Reask and ReaskedAt: when the gate asked again over a recorded tally it did not act on, why
	// (agent.PendingReapproval, agent.PendingSoDReapproval) and when (its recorded_at, by the run's
	// clock), from the newest proven re-ask note (agent.ApprovalReaskStep); empty when the evidence
	// holds none. Display only, like AskedAt.
	Reask     string `json:"reask,omitempty"`
	ReaskedAt string `json:"reasked_at,omitempty"`
	// HeadAt is when the signed tree head the evidence is proven under was signed (its
	// TimestampNanos, in agent.FormatTimestamp's form): every record it covers, the decisions
	// included, was journaled by then.
	HeadAt string `json:"head_at,omitempty"`
	// Problems lists every way the evidence is inconsistent with itself or with the expected
	// policy: an omitted decision the gate read, a recount that disagrees with the gate's
	// journaled tally, a different enforced policy, or records out of order. Any problem fails
	// the verdict.
	Problems []string `json:"problems,omitempty"`
	// OK reports no Problems and at least Need approvals counted.
	OK bool `json:"ok"`
}

ApprovalVerdict is the offline result of checking an m-of-n approval gate from evidence.

func VerifyApprovals added in v0.4.0

func VerifyApprovals(actions []EvidenceAction, toolUseID string, policy agent.ApprovalPolicy, verifierFor agent.ApproverVerifierFor, logV Verifier) (ApprovalVerdict, error)

VerifyApprovals checks offline that the m-of-n gate on toolUseID held, trusting only its inputs: actions (ApprovalEvidence's output, alone or inside an EvidencePackage), the policy the auditor expects, verifierFor (each approver's key, the same resolver shape the gate uses at run time), and logV (the log operator's key, obtained out of band).

It verifies the proofs of the request, the gate's journaled tally, and the call's result, all under one signed tree head and in that journal order (for a gated plan flow node, the request is its agent.ApprovalRequest and the result its node record, as ApprovalEvidence emits them), then recounts the disclosed decisions with agent.TallyApprovals, the exact rule the gate ran, against the proven call's name and arguments. It reports a Problem if the evidence omits a decision the gate read, if the recount disagrees with the journaled tally, or if the gate enforced a different policy than expected. A decision signed for other arguments, another run, or another call, or under another scheme than its approver's key, does not verify and is listed in Ignored. So does a decision whose signed decision time was changed, is missing (one signed under an earlier decision format), or is not in canonical form.

Separation of duties: when the expected policy excludes the requester, or the journaled tally says the gate did, the evidence must prove the run's run:principal record (ApprovalEvidence emits it; it holds the principals and nothing of the run's input), under the same signed tree head and run, before the request. The recount reads the requesters from it, as the gate did, and lists their decisions in Ignored (agent.ReasonRequester). It reports a Problem if the gate's exclusion differs from the expected policy's, if the tally names other requesters than the run:principal, or if the run:principal is missing, not proven, or not before the request; without it the recount counts no one.

It reports each counted decision's signed decision time and recorded time in Times, flagged under DefaultDecisionTimeTolerance, and when the run was asked in AskedAt. These are display only: they never change Counted, Problems or OK.

It returns the verdict with a nil error when the gate held (OK), and with an error wrapping ErrNotVerified when it did not: Problems, too few approvals, or evidence that lacks the request, the tally, or the result, or whose proof of one of them does not verify. An error wrapping agent.ErrConfig is an invalid expected policy, no resolver, or a policy two of whose approvers resolve to one signing key under verifierFor or one of whose approvers' verifiers reports no key identity (see agent.ApprovalPolicy.ValidateKeys); ErrFormat or ErrMalformed, a proof or a record that cannot be read. It certifies that the gate held as recorded, not that the approvers' judgment was right.

type ApproverCert added in v0.18.0

type ApproverCert struct {
	Leaf  *x509.Certificate
	Chain []*x509.Certificate // the accepted path, leaf first, ending at the anchor
	// Anchor is the key of the trust anchor the chain ended at.
	Anchor SPKIHash
	// Scheme is the decision scheme derived from the leaf's key. A record whose approver_alg differs
	// from it does not count.
	Scheme Alg
	// PublicKey is the leaf key in the encoding NewVerifier reads for Scheme.
	PublicKey []byte
	// Identity is the verified canonical identifier of the leaf.
	Identity string
	// LeafHash is the SHA-256 of the leaf's DER, which the v4 decision bytes commit to.
	LeafHash [32]byte
	// KeyHash is the SHA-256 of the leaf's SubjectPublicKeyInfo: two counted decisions by different
	// approver ids with equal KeyHash are one key and do not both count.
	KeyHash SPKIHash
}

ApproverCert is the result of a successful VerifyChain: what the later steps (the signature, the status, the seat) work from.

func (*ApproverCert) SeatKeyID added in v0.18.0

func (a *ApproverCert) SeatKeyID() string

SeatKeyID is the agent.ApproverVerifier.KeyIDs entry of a certificate-bound approver: the seat is the canonical identity, so a person with two approver ids, or two certificates, fills one seat.

type AttenuateFunc

type AttenuateFunc func(parent Grant, subAgent string) Grant

AttenuateFunc derives a child grant from the parent grant and the delegating sub-agent's name. It sets the narrower Scope (and may set an earlier NotAfterUnix; a Subject it sets must be the sub-agent's name, or the delegation is refused); AttenuatingSubAgent fills in ParentRef and, if empty, Issuer (the parent's Subject), Subject (the sub-agent name), and NotAfterUnix (the parent's), then checks the result with CheckAttenuation before signing it, so a widening func is refused at delegation time rather than caught later by VerifyDelegationChain.

type AttenuationConfig added in v0.10.0

type AttenuationConfig struct {
	// Store is the journal the sub-runs journal to, where each child grant is recorded as
	// a durable leaf of its sub-run. Give it the store the parent and the sub-agent use, for a
	// unified, provable journal.
	Store *agent.Journal
	// Narrow derives each child grant from the parent's (see AttenuateFunc).
	Narrow AttenuateFunc
	// Rules says how each scope key may narrow; CheckAttenuation enforces it before a child
	// grant is signed.
	Rules ScopeRules
}

AttenuationConfig configures AttenuatingSubAgent.

type AuditedEventStore added in v0.17.0

type AuditedEventStore struct {
	// contains filtered or unexported fields
}

AuditedEventStore wraps an EventStore so every append is continuously anchored, as AuditedStore does for a journal: whenever a trail grows it commits an RFC 6962 Merkle root over the trail's leaves, signs a Signed Tree Head, and publishes it to an Anchor. The head is the one SignNotifyTrail signs (kind events, RunID the trail's name), with the same Signer and the same Anchor interface as the journal's, so an HSM or KMS signer and an external transparency log work unchanged. Wrap the store a notifier's trail lives in (NewNotifyTrail(NewAuditedEventStore(...))) and the notification trail is signed and anchored as it is written; bide audit notifications then verifies it against the heads. Any other event trail can be wrapped the same way.

Anchoring is a SIDE CHANNEL, as for AuditedStore: a publish failure NEVER fails the append. The leaf is stored before anything is signed, and failing the append because a head could not be published would have a caller retry a write that already succeeded. Failures go to the OnError hook; the trail remains the source of truth, and Reanchor, ReanchorPending and AnchorTrails publish a head that a failure left out.

Publishing is off the write's path: Append returns when the inner store has stored the leaf, and a background publisher builds, signs and publishes the head a moment later (Flush waits for it). So a slow or hung anchor, signer or clock cannot lengthen a write, and a notifier's delivery rounds keep the timing the claim arithmetic of the design note assumes. The publisher is bounded in turn: one attempt (reading the trail, signing, publishing) is abandoned after the timeout (default DefaultAnchorTimeout), even if the Anchor or the Signer ignores its context, and after a failed or timed-out publish the store pauses publishing for the cooldown (default DefaultAnchorCooldown) and then lets a single attempt probe the anchor, so an outage costs one timeout per cooldown. Trails not published are remembered (Unanchored) and published by the next append to any trail after the cooldown, or by Reanchor, ReanchorPending or AnchorTrails. The window a head leaves open, from an append to its head, is therefore the publisher's delay when the anchor is healthy, and the outage plus the time to the next sweep when it is not. A process that dies in that window leaves its last records unattested until another process's sweep or recovery pass signs the trail again, and bide audit notifications reports them.

Anchoring is serialized per trail: a trail's head is built, signed and published while the trail's slot is held, from the trail as it is then stored, and only if it is longer than the last head this store published. So the heads one AuditedEventStore publishes for a trail only grow, which a monitor reads as the trail never being rolled back. Two processes anchoring one trail (the notifier runs in several) can still deliver a smaller head after a larger one; both commit to prefixes of one trail, so a monitor should compare heads by size, not arrival. A store that restarts publishes the current head of a trail again the first time it anchors it.

It keeps a few bytes per trail it has anchored, for the life of the store. It is safe for concurrent use; configure it (WithClock, OnError, WithTimeout, WithCooldown) before the first write.

func NewAuditedEventStore added in v0.17.0

func NewAuditedEventStore(inner EventStore, signer Signer, anchor Anchor) (*AuditedEventStore, error)

NewAuditedEventStore wraps inner so each growth of a trail is signed by signer (ed25519, ECDSA, ML-DSA-65, hybrid or an HSM or KMS signer) and published to anchor.

It refuses a nil store or anchor, and a signer without a usable key, with an error wrapping agent.ErrConfig: the append may not fail once the inner store has stored the leaf, so a signer that could sign nothing is refused here, before any write.

func (*AuditedEventStore) AnchorTrails added in v0.17.0

func (a *AuditedEventStore) AnchorTrails(ctx context.Context, trails []string) error

AnchorTrails is the best-effort catch-up the notifier calls on its sweep and recovery pass (agent.NotifyTrailAnchorer): it anchors the named trails, and up to a few more that earlier attempts left unanchored, unless the anchor is in its cooldown, when it only remembers them. It stops at the first error, which it returns, and spends at most ten timeouts in all, remembering the trails it did not reach. Unlike Reanchor it respects the cooldown, so a sweep over many trails does not wait out a timeout for each while the anchor is down.

func (*AuditedEventStore) Append added in v0.17.0

func (a *AuditedEventStore) Append(ctx context.Context, trail string, seq int, leaf []byte) error

Append stores leaf in the inner store and returns its result, then has a background publisher publish a head over the trail if it is longer than the last head published. The head is built, signed and published off the append's path, so no anchor, signer or clock problem can slow or fail a write, or lengthen a notifier's delivery round: the claim arithmetic of the notifier (docs/design/notifier.md) counts the store's round trips as short, and this keeps it true. A burst of appends to a trail is covered by one head. Flush waits for the publisher.

A position taken by a different leaf (ErrSeqTaken) stored nothing of this caller's and publishes nothing. An append that failed otherwise may have committed all the same, so the trail is remembered as possibly unanchored (Unanchored) and tried again with the next write, Reanchor, ReanchorPending or AnchorTrails.

func (*AuditedEventStore) Flush added in v0.17.0

func (a *AuditedEventStore) Flush(ctx context.Context) error

Flush waits until the background publisher has tried every trail appended to before the call (or ctx is done, whose error it returns). It does not retry what failed: use ReanchorPending. Call it before a process exits, so the last records are covered; whatever a process leaves behind is covered by the next sweep or recovery pass. Agent.FlushNotifications does not wait for this publisher: it signs the notify trails itself (AnchorTrails) and reports a failure to its own error handler, so a caller that needs the OnError reports of the appends it made calls Flush.

With WithMinInterval it also publishes, now, the growth that was held back, and returns the first error of those (it still tries the others).

func (*AuditedEventStore) Load added in v0.17.0

func (a *AuditedEventStore) Load(ctx context.Context, trail string) ([][]byte, error)

Load reads through to the inner store.

func (*AuditedEventStore) OnError added in v0.17.0

func (a *AuditedEventStore) OnError(fn func(trail string, err error)) *AuditedEventStore

OnError sets a hook called when anchoring a write fails: a head could not be built, signed or published, an attempt timed out, or the write's context was done. Without it the errors are dropped; the write has succeeded all the same. The hook may be called from several goroutines. It returns the store.

func (*AuditedEventStore) OnRecovered added in v0.21.0

func (a *AuditedEventStore) OnRecovered(fn func(trail string)) *AuditedEventStore

OnRecovered sets a hook called when a trail's head is published after a streak of failed publishes by the WithMinInterval timer: OnError reports the first failure of a streak only, and this ends it. It returns the store.

func (*AuditedEventStore) Reanchor added in v0.17.0

func (a *AuditedEventStore) Reanchor(ctx context.Context, trail string) error

Reanchor publishes a head over trail now if it holds records no head this store published covers. It tries the anchor even during the cooldown, once, within the timeout, and returns the error that stopped it; it publishes nothing when the last head is current as far as this store knows (see the type's note on restarts).

func (*AuditedEventStore) ReanchorPending added in v0.17.0

func (a *AuditedEventStore) ReanchorPending(ctx context.Context) error

ReanchorPending reanchors every trail that a failure, a skipped attempt or a done context left possibly unanchored, one after the other, and stops at the first error. Each attempt is bounded by the timeout; the whole call is not, so give ctx a deadline.

func (*AuditedEventStore) Unanchored added in v0.17.0

func (a *AuditedEventStore) Unanchored() []string

Unanchored returns, sorted, the trails that may hold records no published head covers: those whose last attempt failed, timed out or was skipped. A trail whose attempt was never made (a process that died) is not here; the notifier's sweep and recovery pass anchor those (AnchorTrails).

func (*AuditedEventStore) Unwrap added in v0.17.0

func (a *AuditedEventStore) Unwrap() EventStore

Unwrap returns the wrapped store, so a decorator above this one, and NotifyTrail, find it and the stores beneath it. Reads and writes pass run IDs and positions through unchanged.

func (*AuditedEventStore) WithClock added in v0.17.0

func (a *AuditedEventStore) WithClock(now func() int64) *AuditedEventStore

WithClock sets a deterministic timestamp source for the heads (for tests). It returns the store.

func (*AuditedEventStore) WithCooldown added in v0.17.0

func (a *AuditedEventStore) WithCooldown(d time.Duration) *AuditedEventStore

WithCooldown sets how long, after a failed publish, writes skip publishing before one probes the anchor again (default DefaultAnchorCooldown). Zero turns the pause off: every write tries. A negative d leaves the default. It returns the store.

func (*AuditedEventStore) WithMinInterval added in v0.21.0

func (a *AuditedEventStore) WithMinInterval(d time.Duration) *AuditedEventStore

WithMinInterval limits how often a trail's head is published: after one head, growth of the same trail is held back until d has passed, and then published as one head that covers all of it. Without it (the default, or d not positive) every append that the background publisher sees publishes a head, which for an anchor that keeps each head for years is one retained object per append at worst.

What stays immediate: a trail's first head, Reanchor, ReanchorPending, AnchorTrails (the notifier's sweep and recovery pass) and Flush. What is bounded: while this process lives, a record is covered by a published head at most d (plus one publish) after it was appended, because held-back growth is published by a timer, not only by a later append. A process that dies loses the timer: the records it held back stay unanchored until the next start or recovery pass anchors the trail (AnchorTrails, the notifier's sweep and recovery pass, or Reanchor), not for d. The cooldown after a failed publish still applies. It returns the store; configure it before the first write.

func (*AuditedEventStore) WithTimeout added in v0.17.0

func (a *AuditedEventStore) WithTimeout(d time.Duration) *AuditedEventStore

WithTimeout sets how long one anchoring attempt may take before it is abandoned (default DefaultAnchorTimeout). The attempt includes reading the trail, signing, and publishing, and a write waits for it at most this long, whatever the Anchor does with its context. A d that is not positive leaves the default. It returns the store.

type AuditedStore

type AuditedStore struct {
	// contains filtered or unexported fields
}

AuditedStore wraps a store so every journal write is continuously anchored: whenever a run's journal grows, it commits an RFC 6962 Merkle root over the run, signs a Signed Tree Head, and publishes it to an Anchor (an external transparency log). The caller drives the agent over a Journal on it exactly as on the store it wraps (agent.NewJournal(audit.NewAuditedStore(store, signer, anchor))) and gets a stream of signed, out-of-band commitments for free.

Anchoring is a SIDE CHANNEL: a publish failure NEVER fails the write. Failing an Insert because an STH could not be published would be dangerous: the write already succeeded, and the caller could wrongly retry a non-idempotent step. Publish errors go to the optional OnError hook instead; the journal remains the source of truth.

func NewAuditedStore

func NewAuditedStore(inner agent.Store, signer Signer, anchor Anchor) (*AuditedStore, error)

NewAuditedStore wraps inner so each journal growth is signed by signer (ed25519, ML-DSA-65, or hybrid) and published to anchor. Timestamps default to time.Now().UnixNano(); override with WithClock for tests.

It refuses a nil store or anchor, and a signer without a usable key, with an error wrapping agent.ErrConfig. Such a signer could sign nothing, and the store may not fail a write once the inner store has stored it, so the signer is refused here, before any write, rather than when the first write is anchored.

Example

Wrap a store so every journal write is signed and anchored, run an agent over it, and verify the latest anchored tree head: its signature, and that its root is the run's journal root.

package main

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"fmt"

	"github.com/bide-ai/bide/agent"
	"github.com/bide-ai/bide/agent/agenttest"
	"github.com/bide-ai/bide/audit"
)

func main() {
	ctx := context.Background()
	signer := audit.Ed25519Signer{Priv: ed25519.NewKeyFromSeed(bytes.Repeat([]byte{7}, ed25519.SeedSize))}
	anchor := audit.NewMemAnchorLog() // in production, a log in another trust domain
	store, err := audit.NewAuditedStore(agent.NewMemStore(), signer, anchor)
	if err != nil {
		fmt.Println(err)
		return
	}
	j := agenttest.MustJournal(store)
	a := agenttest.MustNew(agenttest.NewScriptedModel(agenttest.TextTurn("hello")), j)
	if _, err := a.Run(ctx, "run-1", agent.UserText("hi")); err != nil {
		fmt.Println(err)
		return
	}

	// A verifier holds only the public key.
	v, err := audit.VerifierOf(signer)
	if err != nil {
		fmt.Println(err)
		return
	}
	entries := anchor.Entries()
	sth := entries[len(entries)-1].STH
	root, err := audit.Root(ctx, j, "run-1")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("signature valid:", sth.Verify(v) == nil)
	fmt.Println("root matches the journal:", bytes.Equal(sth.Root, root))
}
Output:
signature valid: true
root matches the journal: true

func (*AuditedStore) Flush added in v0.21.0

func (a *AuditedStore) Flush(ctx context.Context) error

Flush publishes now every run whose growth WithMinInterval held back, stopping their timers, and returns the first error (it still tries the others). Call it before the process exits, so the last records are covered. It publishes nothing for a run whose head is current.

func (*AuditedStore) Format added in v0.7.0

func (a *AuditedStore) Format(f fmt.State, _ rune)

Format writes String for every verb, so no fmt verb prints the signing key.

func (*AuditedStore) Get added in v0.11.0

func (a *AuditedStore) Get(ctx context.Context, runID, name string) (agent.Entry, bool, error)

Get reads through to the inner store.

func (*AuditedStore) Insert added in v0.11.0

func (a *AuditedStore) Insert(ctx context.Context, runID, name string, data []byte) (agent.Entry, bool, error)

Insert stores the entry in the inner store, then anchors the run's tree head if the journal grew past the last anchored head. It anchors after every insert but the journal header's (a journal writes the header just before the run's first record, whose insert anchors both, so a run's anchored heads never stop at its header): also after an insert that found the entry stored (another writer's, or this writer's retry of a write whose first attempt committed but reported an error, A3), and after one that failed, which may have committed all the same. The anchoring never changes what Insert returns.

Under a context that is done it cannot anchor (the read and the publish would fail with the context), and anchoring without the caller's context could block with no deadline. If the call stored the entry, or failed (it may have committed all the same), it reports an error wrapping the context's to OnError, the signal to Reanchor the run: a run's last write, run:complete included, may be written by a drive whose context is cancelled meanwhile. An insert that stored nothing (the entry was already there) has nothing to anchor and reports nothing.

func (*AuditedStore) Load added in v0.11.0

func (a *AuditedStore) Load(ctx context.Context, runID string, after int64) iter.Seq2[agent.Entry, error]

Load reads through to the inner store.

func (*AuditedStore) OnError

func (a *AuditedStore) OnError(fn func(runID string, err error)) *AuditedStore

OnError sets a hook called when anchoring a write fails (Publish, or committing the root). Without it, anchor errors are swallowed: the write still succeeds. Returns the store.

func (*AuditedStore) OnRecovered added in v0.21.0

func (a *AuditedStore) OnRecovered(fn func(runID string)) *AuditedStore

OnRecovered sets a hook called when a run's head is published after a streak of failed publishes by the WithMinInterval timer: OnError reports the first failure of a streak only, and this ends it. Returns the store.

func (*AuditedStore) Pending added in v0.21.0

func (a *AuditedStore) Pending() []string

Pending returns, sorted, the runs whose growth WithMinInterval is holding back.

func (*AuditedStore) Reanchor added in v0.11.0

func (a *AuditedStore) Reanchor(ctx context.Context, runID string) error

Reanchor anchors runID's journal now, if it holds records no published head covers. A failed publish is covered by the run's next write, but a run's last write has none, and a write under a context that is done is not anchored at all (see Insert): OnError is the signal to call Reanchor for that run (once the anchor is reachable again), out of band. It returns the anchoring error, and publishes nothing when the anchored head is current as far as this AuditedStore knows: it remembers the heads it published, so another AuditedStore over the same store (another process, or after a restart) publishes the current head again, which a monitor reads as the same size, not a change.

func (*AuditedStore) ReanchorRuns added in v0.21.0

func (a *AuditedStore) ReanchorRuns(ctx context.Context, f agent.RunFilter) error

ReanchorRuns is the restart path for WithMinInterval: it anchors now every run f admits whose journal this store has not anchored, which after a start is every run, since the store remembers nothing of an earlier process. Growth held back by a process that died (its timer died with it) stays unanchored until this runs, so call it at start, or in the recovery pass, with a filter that skips the runs that ended (agent.RunFilter{ExcludeHolding: ...} with the end markers: the write of a run's end anchors at once). It publishes one head per run it lists, the current one, which a monitor reads as the same size as an earlier head, not a change. It needs a store that lists runs (agent.Lister, found through Unwrap) and returns an error wrapping agent.ErrConfig without one. It tries every run, and returns the first error.

func (*AuditedStore) String added in v0.7.0

func (*AuditedStore) String() string

String returns a description of the store that does not include its signing key.

func (*AuditedStore) Unwrap added in v0.8.0

func (a *AuditedStore) Unwrap() agent.Store

Unwrap returns the wrapped store, so agent.Capability finds its optional capabilities (agent.Leaser, agent.Lister) through the wrapper: an AuditedStore has exactly the capabilities of the store it wraps. Leasing and listing touch no journal, so they bypass anchoring. It passes run IDs and names through unchanged, as Unwrap requires.

func (*AuditedStore) WithClock

func (a *AuditedStore) WithClock(now func() int64) *AuditedStore

WithClock sets a deterministic timestamp source (for tests). Returns the store for chaining.

func (*AuditedStore) WithMinInterval added in v0.21.0

func (a *AuditedStore) WithMinInterval(d time.Duration) *AuditedStore

WithMinInterval limits how often a run's head is published: after one head, growth of the same run is held back until d has passed, and then published as one head that covers all of it. Without it (the default, or d not positive) every journal write publishes a head, which for an anchor that keeps each head for years (an S3 Object Lock bucket, say) is one retained object per write.

What stays immediate: a run's first head, the write of its end marker (run:complete, run:aborted or run:cancelled), Reanchor, and Flush. What is bounded: while this process lives, a record is covered by a published head at most d (plus one publish) after it was written, because held-back growth is published by a timer, not only by a later write. A process that dies loses the timer: the records it held back stay unanchored until the next start or recovery pass anchors them (ReanchorRuns, which a deployment calls at start), not for d. Heads of one run still only grow. Call Flush before the process exits. It returns the store; configure it before the first write.

type CRL added in v0.20.0

type CRL struct {
	Hash       [32]byte // SHA-256 of the DER
	TBSHash    [32]byte // SHA-256 of the TBSCertList: a re-signed copy of one CRL has the same value
	Number     *big.Int
	ThisUpdate time.Time
	NextUpdate time.Time
	Delta      bool
	BaseNumber *big.Int // for a delta, the base number it is cumulative since
	// DistributionPoints are the URIs of the CRL's issuing distribution point, none when it has no IDP.
	DistributionPoints []string
	// contains filtered or unexported fields
}

CRL is one CRL as a refresher sees it: what the snapshot holds, or what VerifyCRL returned.

func VerifyCRL added in v0.20.0

func VerifyCRL(der []byte, issuer *x509.Certificate, maxEntries int) (*CRL, error)

VerifyCRL checks der as a CRL of the given issuing CA: the signature algorithm profile (never SHA-1), the signature against that one certificate, the issuer name, and the structure rules of AddCRL. It adds nothing to any snapshot. A failure of the first three wraps ErrCRLSignature; a structure the status rule will not read without guessing is a *StatusError.

func (*CRL) Serials added in v0.20.0

func (c *CRL) Serials() []string

Serials returns the decimal serial numbers the CRL lists, sorted.

type CRLInfo added in v0.18.0

type CRLInfo struct {
	Hash       [32]byte // SHA-256 of the DER
	Number     *big.Int
	ThisUpdate time.Time
	NextUpdate time.Time
	Entries    int
}

CRLInfo describes a CRL accepted into a StatusSnapshot.

type CRLSource added in v0.20.0

type CRLSource struct {
	// URLs are the CRL (base) locations, http or https. Every URL is asked on each attempt and the
	// highest-numbered valid CRL wins, so list every mirror of the same CRL, never different CRLs.
	URLs []string `json:"urls"`
	// DeltaURLs are the delta CRL locations, if the CA publishes deltas.
	DeltaURLs []string `json:"delta_urls"`
	// FromPinnedCDP adds the CRL distribution point (and freshest CRL) URLs of the supplied
	// certificate whose issuer is this CA. It helps only for a CA above the leaf's issuing CA: a leaf
	// certificate names its issuer's CRL, and leaves arrive from clients and are never a source.
	FromPinnedCDP bool `json:"from_pinned_cdp"`
}

CRLSource is where the refresher fetches the CRLs of one issuing CA from. Only URLs the operator configured here (or, with FromPinnedCDP, those in the pinned certificate that names this CA's CRL) are ever fetched: never a URL from a client-supplied chain.

type CRLSources added in v0.20.0

type CRLSources map[SPKIHash]CRLSource

CRLSources maps an issuing CA, by the SHA-256 of its public key, to where its CRLs are fetched. The URLs are not part of the policy digest: a verdict depends on the CRL (verified against the pinned key), never on the URL it came from.

func ParseCRLSources added in v0.20.0

func ParseCRLSources(name string, b []byte, readFile func(rel string) ([]byte, error)) (CRLSources, []*x509.Certificate, error)

ParseCRLSources reads the refresher members of a certificate policy file: "crl" (the sources, keyed by the lowercase hex SPKI hash of the issuing CA) and "intermediate_certs" (PEM files of the pinned intermediates, relative to the policy file's directory like the anchors). The same bytes are what ParseIdentityPolicy reads for the policy; this reads the members it ignores. readFile returns a file's bytes by its path relative to the policy file's directory (this package does no file I/O).

func (CRLSources) Validate added in v0.20.0

func (s CRLSources) Validate() error

Validate checks every URL. It does not need the policy; NewCRLRefresher checks the keys.

type CanonicalConfig added in v0.18.0

type CanonicalConfig struct {
	// Scheme is the one URI scheme accepted. Empty means "https".
	Scheme string
	// Host is the one URI host accepted, in lowercase. Empty means no URI identifier is accepted.
	Host string
	// Path matches the URI path (the part after the host, starting with "/"). Empty means the
	// default: "/person/" followed by 32 lowercase hex digits. It is matched against the whole
	// path, whatever anchors the expression has. Configure exactly one spelling of the id.
	Path *regexp.Regexp
}

CanonicalConfig configures the default canonicalizer (NewCanonicalizer).

type Canonicalizer added in v0.18.0

type Canonicalizer interface {
	// Name identifies the function (for the identity record).
	Name() string
	// Version changes when the function's results do.
	Version() int
	// Canonical returns the canonical form of s as an identifier of the given kind, or an error when
	// s is not in the accepted profile.
	Canonical(kind IDKind, s string) (string, error)
}

Canonicalizer maps an identifier to its one canonical form. It is used in every place an identity is compared: the identity map, the certificate's subject alternative name, the requester principal of a run and the identity of a bare-key approver, so two spellings of one identifier cannot be two seats. Canonical is a pure function: it returns the same result for the same input, and an error for anything outside the bank's profile. The verdict records Name and Version, so an auditor knows which function produced the form.

func NewCanonicalizer added in v0.18.0

func NewCanonicalizer(cfg CanonicalConfig) Canonicalizer

NewCanonicalizer returns the default canonicalizer, "bide.default" version 1.

Email: ASCII only (an internationalized address is refused until a bank needs one), exactly one "@", a local part of RFC 5322 atext characters and dots (no leading, trailing or doubled dot), a domain of letter-digit-hyphen labels, and the whole address lowercased (the bank states that its directory is case-insensitive). Nothing is trimmed and no alias is folded: a.smith and asmith are two identifiers.

URI (a strict profile, because a CA that could issue one person's identifier in two spellings would give one person two seats): the configured scheme, no userinfo, no port, no query, no fragment, no percent-encoding, no trailing slash, no dot segments, a host equal to the configured lowercase host, and a path that matches the configured pattern. A URI is returned unchanged: there is one accepted spelling, not a normalisation.

type CertApprover added in v0.19.0

type CertApprover struct {
	// ApproverID is the approver id this verifier serves: the key of the policy's identity map.
	ApproverID string
	// Policy is the compiled certificate policy of the gate.
	Policy *CertIdentity
	// Heads returns the signed tree heads of the run known to the caller (the anchor log's list, not
	// the producer's). A(d) is the time of the smallest head that covers the decision. Nil means no
	// head is known and no decision is provable.
	Heads func(runID string) ([]SignedTreeHead, error)
	// LogKey verifies the heads. Nil verifies none.
	LogKey Verifier
	// ClockTolerance is how far a larger head may be stamped before a smaller one without the heads
	// contradicting each other (default one minute).
	ClockTolerance time.Duration
	// Status holds the CRLs behind the revocation check. Nil means no status: no decision counts.
	Status *StatusSnapshot
	// StatusParams are the status rule's parameters (the window, the declared CA period, holds...). For a
	// live gate set CountLive and MaxStatusAge, and Now supplies the clock for each check; an offline
	// audit leaves Now nil and the rule judges the series against the decision.
	StatusParams StatusParams
	// Now, when set, is read for each decision to set StatusParams.Now.
	Now func() time.Time
	// ExecNow, when set, is the clock of the execution-time check (VerifyCertExec); otherwise Now, and
	// when that is nil too, the wall clock. It exists so a test or a deterministic simulation can move the
	// execution check in time apart from the count.
	ExecNow func() time.Time
	// RequesterIdentity, when set, maps a requester principal (as the run records it) to the canonical
	// identifier of the person, so separation of duties can exclude the requester by identifier. Without
	// it the roster is used: a principal equal (ASCII case folded) to an approver id of the identity map
	// maps to that approver's identifier, which only repeats the exclusion by approver id: a bank that
	// wants a requester who holds no approver id (a login that maps to a person) excluded by identity
	// must supply this function. A requester it cannot map is then excluded by approver id only, and
	// the finding carries a warning; without the function no warning is made.
	RequesterIdentity func(principal string) (identifier string, ok bool)
	// ExecStatusAge is how old the newest CRL of the leaf's issuing CA may be when the call is about to
	// run (the status check before execution). ValidateConfig requires that CA's period to be at most
	// half of it. It is set from the CA's actual period; there is no default.
	ExecStatusAge time.Duration
	// ExecStatusAgeFor is the same age for each CA tier above the leaf's issuing CA, by issuer key: the
	// status of an intermediate rests on its issuer's CRL, which an offline root publishes rarely. Every
	// CA in an accepted chain above the leaf's issuer must be named here (the execution check holds the
	// call otherwise), and ValidateConfig requires, for each: CAPeriodFor + CAOverlapFor <=
	// ExecStatusAgeFor <= StatusParams.MaxStatusAgeFor. A CA's tier is its position in the accepted
	// chain, never its key: the leaf's issuing CA always uses ExecStatusAge.
	ExecStatusAgeFor map[SPKIHash]time.Duration
	// CAOverlapFor is, for a CA tier, how long after its period ends the CA may still be publishing the
	// next CRL (an offline root's ceremony slack, AD CS's CRL overlap); the oldest status a gate can hold
	// for that CA is CAPeriodFor plus this. Zero is allowed.
	CAOverlapFor map[SPKIHash]time.Duration
	// PolicyEffectiveFrom is when the certificate policy took effect (the bank's change record), kept in
	// the identity record so the mapping at A(d) is knowable years later. ValidateConfig requires it.
	PolicyEffectiveFrom time.Time
	// StatusArchive receives the bytes of a CRL above agent.MaxStatusInline, which the journal records
	// by hash only. Without one the gate refuses to count on such a CRL.
	StatusArchive StatusArchive
	// GoDebug is the x509 settings of GODEBUG the process runs under (they change how certificates
	// verify), as the operator DECLARES them: the engine does not read the environment, so this is a
	// declaration, recorded as such in the identity record. Generate it from the real process settings and
	// check it at startup with CheckDeclaredGoDebug(declared, os.Getenv("GODEBUG")).
	GoDebug string
}

CertApprover is the verifier of a certificate-bound approver (docs/design/approver-identity.md): it implements agent.CertDecisionVerifier, so the counting rule (agent.TallyApprovals) checks the approver's decisions as certificate-bound ones.

A decision counts when all of these hold: it carries a chain and is signed under bide.approval.v4 (never v3: a bare signature is not a certificate-bound decision); a signed tree head of the run covers it, which fixes A(d), the time it became provable; the chain is accepted for this approver at A(d) (CertIdentity.VerifyChain); the signature verifies under the leaf's key over the v4 bytes of exactly this call, which bind the leaf's hash; and no certificate of the chain was revoked with an effective time at or before A(d) (CertIdentity.EvaluateStatus), with status that exists and is fresh enough for the counting mode. The finding carries the verified identifier, the tier and basis of the status, and what A(d) rests on, for the verdict and the identity record.

func (*CertApprover) Alg added in v0.19.0

func (*CertApprover) Alg() agent.Alg

Alg reports agent.AlgCertificate: the scheme is the leaf key's, known only from the certificate.

func (*CertApprover) ArchiveStatus added in v0.19.0

func (c *CertApprover) ArchiveStatus(ctx context.Context, hash string, der []byte) error

ArchiveStatus implements agent.StatusArchiver with the verifier's StatusArchive, if any.

func (*CertApprover) CheckCertificate added in v0.21.0

func (c *CertApprover) CheckCertificate(chain [][]byte, decidedAt, anchoredAt time.Time, mode CheckMode) agent.CertCheck

CheckCertificate is the part of the verifier that needs no journal, signature or signed head: whether the chain is accepted for this approver (CertIdentity.VerifyChain) and what the status rule makes of it, for a decision signed at decidedAt that became provable at anchoredAt, judged by the count rule or the pre-execution rule. The verdict (Reason empty when it would count) is what VerifyCertDecision or VerifyCertExec reports for a decision with that chain, signed correctly, and that time: both call the same function. The clock for the status rule is Now (CheckCount) or ExecNow, then Now (CheckExec). The signature, the key's scheme and the covering head are not checked; a caller says so.

func (*CertApprover) DescribeIdentity added in v0.19.0

func (c *CertApprover) DescribeIdentity() agent.IdentityPolicyRecord

DescribeIdentity implements agent.IdentityDescriber: the certificate policy and status values in force, every one that reaches a verdict, as the identity record keeps them (defaults filled in). Anchors and intermediates are named by the SHA-256 of their SubjectPublicKeyInfo; durations are time.Duration.String and times RFC 3339 with nanoseconds. TestIdentityPolicyRecordCoversEveryField fails when a field is added to IdentityPolicy, StatusParams or CertApprover and not to the record.

func (*CertApprover) HasIdentity added in v0.19.0

func (c *CertApprover) HasIdentity(approverID string) bool

HasIdentity implements agent.IdentityRoster: whether the policy's identity map names the approver id.

func (*CertApprover) Identifier added in v0.19.0

func (c *CertApprover) Identifier(approverID string) (string, bool)

Identifier implements agent.IdentityRoster: the canonical identifier the policy gives the approver id.

func (*CertApprover) IdentityMappingConfigured added in v0.19.0

func (c *CertApprover) IdentityMappingConfigured() bool

IdentityMappingConfigured implements agent.IdentityRoster: whether RequesterIdentity is set.

func (*CertApprover) KeyIDs added in v0.19.0

func (c *CertApprover) KeyIDs() []string

KeyIDs is the approver's seat, "ident:" and the canonical identifier the policy's identity map gives the approver id, so two approver ids of one person fill one seat. An id the policy does not name reports none, and the gate refuses it.

func (*CertApprover) PolicyDigest added in v0.19.0

func (c *CertApprover) PolicyDigest() string

PolicyDigest implements agent.IdentityDescriber: the SHA-256 (lowercase hex) of the JSON of DescribeIdentity, written with encoding/json without HTML escaping, fields in declaration order and map keys sorted, which the pause note and the identity record both carry.

func (*CertApprover) RequesterIdentifier added in v0.19.0

func (c *CertApprover) RequesterIdentifier(principal string) (string, bool)

RequesterIdentifier implements agent.IdentityRoster: RequesterIdentity if set; else the principal canonicalized as an identifier of the policy's kind, if it is one; else the roster's identifier of the approver id the principal equals.

func (*CertApprover) RosterDigest added in v0.19.0

func (c *CertApprover) RosterDigest() string

RosterDigest implements agent.IdentityRoster: the SHA-256 over the whole identity map (bare-key approvers included) in sorted approver order, each entry len32(approver) approver len32(identifier) identifier, so an id or identifier containing '=' or a newline cannot collide with another entry.

func (*CertApprover) ValidateConfig added in v0.19.0

func (c *CertApprover) ValidateConfig() error

ValidateConfig checks the verifier's configuration when the gate is configured (agent calls it from the approver seat check, at build and on every evaluation): the policy and approver id, the status parameters (StatusParams.Validate), and the rule that the pre-execution status check cannot be fresher than the CA publishes: every CA period, the default and each per-issuer one, is at most half of ExecStatusAge (for the leaf's issuing CA; the per-CA tiers follow with the execution-time check), so the newest CRL available at execution is always fresh enough. A bad value fails here, wrapping agent.ErrConfig, not at the first decision.

func (*CertApprover) VerifierInfo added in v0.19.0

func (c *CertApprover) VerifierInfo() (revision, goDebug, buildGoDebug string)

VerifierInfo implements agent.IdentityDescriber: the build's vcs.revision, if the binary has one, the x509 GODEBUG settings the operator declares (CertApprover.GoDebug), and the build's baked-in DefaultGODEBUG (the toolchain's go line fixes many x509 behaviours), read from the build info and not from the environment.

func (*CertApprover) Verify added in v0.19.0

func (*CertApprover) Verify(message, sig []byte) bool

Verify is always false: a bare signature over bytes, with no certificate, is never a certificate-bound approver's decision.

func (*CertApprover) VerifyCertDecision added in v0.19.0

func (c *CertApprover) VerifyCertDecision(cd agent.CertDecision) agent.CertCheck

VerifyCertDecision implements agent.CertDecisionVerifier.

func (*CertApprover) VerifyCertExec added in v0.19.0

func (c *CertApprover) VerifyCertExec(cd agent.CertDecision) agent.CertCheck

VerifyCertExec implements agent.CertExecVerifier: the check a gate runs on a counted decision immediately before the call it authorizes executes. It is the counting check again at the current time with the execution ages instead of MaxStatusAge: the newest status of the leaf's issuing CA must be no older than ExecStatusAge, and that of each CA tier above it no older than its ExecStatusAgeFor (an undeclared tier holds the call). The clock is Now or, when Now is nil, the wall clock: an execution check never runs without one (a check that judged the series against the decision would accept any old CRL), and status issued before the decision counts, as in a live count. A decision whose certificate now shows a revocation with E <= A(d), or whose status is stale or unknown, fails with the same Reason constants as at count time. ValidateConfig checks the ages at configuration time.

func (*CertApprover) VerifyCertIntake added in v0.19.0

func (c *CertApprover) VerifyCertIntake(cd agent.CertDecision) agent.CertCheck

VerifyCertIntake implements agent.CertIntakeVerifier: the check a decision passes before it is written (agent.WithDecisionCheck, so the approvals intake and `bide approve`). It is the counting check at the time of the write, with no covering head to find (there is none yet; the write is what a head will cover): the chain is accepted for the approver at the current time, the signature verifies over the bide.approval.v4 bytes of this call under the leaf's key, and the status of every certificate of the chain is usable now by the execution ages and shows no revocation with E <= now. A decision that fails for its chain, identity, scheme or signature, or that is revoked, is invalid (the intake answers 422); one that fails only for status that is missing or stale returns agent.ReasonStatusUnknown or ReasonStatusStale (the intake answers 503 and writes nothing: the decision is not refused, the gate cannot judge it yet). The clock is ExecNow, else Now, else the wall clock.

type CertError added in v0.18.0

type CertError struct {
	Reason string
	Detail string
}

CertError is the failure of VerifyChain. Reason is one of the agent.ReasonNoCert, agent.ReasonCertChain, agent.ReasonCertIdentity, agent.ReasonCertUsage or agent.ReasonCertWindow categories; Detail says precisely what failed. Neither holds key material, and any text taken from the certificate is quoted and truncated.

func (*CertError) Error added in v0.18.0

func (e *CertError) Error() string

Error reports the category and the precise cause.

type CertIdentity added in v0.18.0

type CertIdentity struct {
	// contains filtered or unexported fields
}

CertIdentity is a compiled IdentityPolicy: immutable, safe for concurrent use.

func ParseIdentityPolicy added in v0.19.0

func ParseIdentityPolicy(name string, b []byte, readAnchor func(rel string) ([]byte, error)) (*CertIdentity, error)

ParseIdentityPolicy compiles a certificate policy file's bytes (see identityPolicyFile for its members). name labels errors; readAnchor returns the bytes of a PEM anchor file the policy names, by its path relative to the policy file's directory (the caller reads files: this package does none). Every failure wraps agent.ErrConfig: a check against a policy that did not load must not fall back to a weaker one.

func (*CertIdentity) AllowsEmailProtectionEKU added in v0.21.0

func (c *CertIdentity) AllowsEmailProtectionEKU() bool

AllowsEmailProtectionEKU reports whether the policy accepts id-kp-emailProtection in place of EKU.

func (*CertIdentity) ApproverIDs added in v0.20.0

func (c *CertIdentity) ApproverIDs() []string

ApproverIDs returns the approver ids of the policy's identity map, sorted.

func (*CertIdentity) Canonicalizer added in v0.18.0

func (c *CertIdentity) Canonicalizer() Canonicalizer

Canonicalizer returns the function this policy compares identifiers with, for the identity record and for the other places an identity is compared (the requester principal).

func (*CertIdentity) DistrustFrom added in v0.20.0

func (c *CertIdentity) DistrustFrom(k SPKIHash) time.Time

DistrustFrom returns when the policy ends trust in the key (zero when it does not).

func (*CertIdentity) EKU added in v0.21.0

EKU returns the extended key usage a leaf must carry, as a copy. A policy that also accepts id-kp-emailProtection (AllowsEmailProtectionEKU) accepts a leaf with that usage instead.

func (*CertIdentity) EvaluateStatus added in v0.18.0

func (c *CertIdentity) EvaluateStatus(ac *ApproverCert, a time.Time, s *StatusSnapshot, p StatusParams) ChainStatus

EvaluateStatus applies the status rule to every non-anchor certificate of ac's chain at A(d) = a, over the CRLs in s. A CRL counts for a certificate only if it verifies under that certificate's issuing CA in the chain; one signed by any other key is ignored, and so are entries for the same serial number from another issuer.

func (*CertIdentity) IDKind added in v0.21.0

func (c *CertIdentity) IDKind() IDKind

IDKind returns the kind of the one subject alternative name a leaf must carry.

func (*CertIdentity) Identity added in v0.18.0

func (c *CertIdentity) Identity(approverID string) (string, bool)

Identity returns the canonical identifier the policy expects for an approver id.

func (*CertIdentity) IsPinnedIntermediate added in v0.20.0

func (c *CertIdentity) IsPinnedIntermediate(k SPKIHash) bool

IsPinnedIntermediate reports whether the key is a pinned intermediate.

func (*CertIdentity) IsTrustAnchor added in v0.20.0

func (c *CertIdentity) IsTrustAnchor(k SPKIHash) bool

IsTrustAnchor reports whether the key is one of the policy's trust anchors.

func (*CertIdentity) MaxLeafLifetime added in v0.21.0

func (c *CertIdentity) MaxLeafLifetime() time.Duration

MaxLeafLifetime returns the bound on a leaf's notAfter minus notBefore: the policy's value, or the 24 hour default when it set none. The policy's NotBeforeBackdate widens the check beyond it.

func (*CertIdentity) PinnedIntermediates added in v0.20.0

func (c *CertIdentity) PinnedIntermediates() []SPKIHash

PinnedIntermediates returns the pinned intermediate keys, sorted.

func (*CertIdentity) PolicyOID added in v0.21.0

func (c *CertIdentity) PolicyOID() x509.OID

PolicyOID returns the certificate policy OID a leaf must assert.

func (*CertIdentity) TrustAnchors added in v0.20.0

func (c *CertIdentity) TrustAnchors() []*x509.Certificate

TrustAnchors returns the policy's trust anchor certificates, ordered by key.

func (*CertIdentity) TrustRoots added in v0.20.0

func (c *CertIdentity) TrustRoots() *x509.CertPool

TrustRoots returns the policy's trust anchors as a certificate pool.

func (*CertIdentity) VerifyChain added in v0.18.0

func (c *CertIdentity) VerifyChain(approverID string, chain [][]byte, decidedAt, anchoredAt time.Time) (*ApproverCert, error)

VerifyChain checks a decision's certificate chain for approver approverID.

decidedAt is the time the approver signed (their claim); anchoredAt is A(d), the time of the smallest signed head that covers the decision record. chain is the leaf-first DER chain the decision carries. The check is rules 1 to 3 of the counting rule in the design note:

  • exactly one chain builds from the pinned intermediates to a trust anchor of this gate, at min(A, leaf notAfter) for the leaf and A for every CA certificate;
  • the extended key usage nests along the chain, the algorithms are the profile's, no anchor or intermediate is past its DistrustAfter;
  • the leaf is not a CA, may sign, carries the required extended key usage and policy, has a key a decision scheme can verify, and has exactly one subject alternative name of the configured kind whose canonical value is the approver's identity;
  • notBefore <= decidedAt <= notAfter, decidedAt <= A + skew, and A <= notAfter + anchorGrace.

It does not check revocation status or the signature (the decision verifier built from ApproverCert.PublicKey refuses weak Ed25519 keys when the signature is checked, so KeyHash is over bytes that step must still vet). Every carried certificate must parse, pinned or not: one unparseable certificate refuses the chain. The signature algorithm profile is SHA-256 or better, with ECDSA limited to SHA-256 and SHA-384 as the design note says (the RFC 3161 verifier also accepts SHA-512 on ECDSA). An error is always a *CertError.

type CertStatus added in v0.18.0

type CertStatus struct {
	Serial *big.Int
	// Revoked: a CRL lists the certificate with an effective time E at or before A(d) (outside any
	// routine grace). E, ReasonCode, Compromise and RevokedBy describe the listing with the earliest E.
	Revoked    bool
	E          time.Time
	ReasonCode int
	Compromise bool     // keyCompromise, cACompromise or aACompromise: the bank's process treats a counted decision as disputed
	RevokedBy  [32]byte // hash of the CRL that lists it
	// Tier is the evidence of absence when not Revoked.
	Tier StatusTier
	// S is the thisUpdate of the latest usable CRL, LC that of the first CRL beyond notAfter when
	// one is established (LCKnown), Unbounded when expired entries are retained.
	S, LC     time.Time
	LCKnown   bool
	Unbounded bool
	// Stale: usable-in-time CRLs exist but fail the live freshness rules.
	Stale bool
	// Window is W' for this certificate.
	Window time.Duration
	// Used are the hashes of the CRLs that count as evidence of absence, in thisUpdate order.
	Used [][32]byte
	// Note says why the tier is not final.
	Note string
	// Caveat is stated with the verdict: what the status does not cover. Empty only when there is no
	// status.
	Caveat string
	// PolicyRefusal: the finding is the status policy refusing a certificate that was ever on hold
	// (StatusParams.AllowHolds off), not a revocation the CA recorded for the decision's time. It
	// refuses even a decision made before a later hold, so it must not be read as "revoked at E".
	PolicyRefusal bool
	// Basis says what the status rests on: "status issued at or after the decision", or "live count:
	// status issued before the decision (provisional only)". A tier is not a verification: only Final
	// is, and a live count never is. Empty when there is no status.
	Basis string
	// Warning is set when the series shows something the declared CAPeriod does not allow (a gap
	// longer than the CA said it would leave), or the snapshot evicted CRLs the verdict needed
	// (EvictionGap). It does not change the tier by itself; the gap already limits it.
	Warning string
	// EvictionGap: the snapshot evicted CRLs from the range the verdict needs, so it is at most
	// provisional.
	EvictionGap bool
}

CertStatus is the status finding for one certificate of a chain.

func (CertStatus) IsFinal added in v0.18.0

func (c CertStatus) IsFinal() bool

IsFinal reports whether the finding is a verified one: not revoked, and final under the status rule. Provisional and no-information findings are not, and neither is a live count.

type ChainStatus added in v0.18.0

type ChainStatus struct {
	Certs   []CertStatus
	Revoked bool       // any certificate revoked with E <= A(d)
	Tier    StatusTier // the weakest certificate's tier (meaningful when not Revoked)
}

ChainStatus is the finding for every non-anchor certificate of an accepted chain, leaf first.

func (ChainStatus) Err added in v0.18.0

func (cs ChainStatus) Err() error

Err is nil when the chain has usable status and no revocation (a provisional or live-count finding included: check IsFinal before calling a decision verified). Otherwise it is a *StatusError: ReasonRevoked, ReasonStatusStale (usable-in-time status exists but is too old) or ReasonStatusUnknown.

func (ChainStatus) IsFinal added in v0.18.0

func (cs ChainStatus) IsFinal() bool

IsFinal reports whether every certificate of the chain is final and none is revoked. Err() == nil does not mean that: it means the status is usable (a live count may proceed on it) and nothing is revoked. Only IsFinal is verification.

type CheckMode added in v0.21.0

type CheckMode int

CheckMode is the counting rule a CheckCertificate verdict is judged by.

const (
	// CheckCount judges as the gate does when it counts a decision: status no older than MaxStatusAge
	// (and MaxStatusAgeFor).
	CheckCount CheckMode = iota
	// CheckExec judges as the gate does immediately before the call runs: ExecStatusAge for the leaf's
	// issuing CA and ExecStatusAgeFor for each CA tier above it.
	CheckExec
)

type Consistency

type Consistency struct {
	First int      `json:"first"` // m: the earlier tree size
	Size  int      `json:"size"`  // n: the current tree size
	Path  [][]byte `json:"path"`  // proof hashes
}

Consistency is an RFC 6962 consistency proof: the first First records are an append-only prefix of the Size-record tree.

func ProveConsistency

func ProveConsistency(ctx context.Context, store *agent.Journal, runID string, first int) (Consistency, error)

ProveConsistency proves that runID's first `first` records are an append-only prefix of its current journal — i.e. nothing before `first` was changed or reordered, only appended.

type ConvergenceContent

type ConvergenceContent struct {
	Digest      string          `json:"digest"`      // the policy digest this certificate certifies (treated as opaque here)
	Certificate json.RawMessage `json:"certificate"` // the opaque, serialized convergence certificate bytes
}

ConvergenceContent is the payload of a convergence leaf: an opaque, serialized convergence certificate and the policy digest it certifies. Committing it as a journal leaf puts the convergence evidence under the same signed tree head and inclusion proofs as the policy and the actions taken under it, so an auditor can prove not merely that a policy was anchored but that the anchored policy was certified convergent in the same committed tree.

audit treats the certificate as opaque bytes and the digest as an opaque identifier, exactly as it does for a policy leaf: it does not import gsm and does not recompute either. The producer of the certificate is govern.CertifyConvergence; a rigorous verifier re-derives the certificate from the disclosed policy bytes (by rebuilding the gsm machine) and compares, so a leaf that overstates convergence is caught outside audit, not trusted here.

type CoverageBound added in v0.17.0

type CoverageBound struct {
	MaxRecords int
	// MaxAge, with Now, flags a decision still uncovered MaxAge+Tolerance after its recorded_at.
	// Without a Now it is not applied.
	MaxAge    time.Duration
	Now       time.Time
	Tolerance time.Duration // clock skew allowed between the recorder and the checker
}

CoverageBound is how long a recorded decision may stay outside every signed tree head before CheckDecisionCoverage reports it overdue. A zero field is a bound not set; with none set a decision is pending or covered, never overdue.

MaxRecords is the journal's growth past the decision, in records, that its signer is allowed before a head must cover the decision. It is the bound that needs no clock, and it is only reached when the journal grows. A decision on a run that nobody writes to again, a rejection, say, never grows the journal, so MaxAge is what finds it.

type CoverageReport added in v0.17.0

type CoverageReport struct {
	RunID          string             `json:"run_id"`
	Records        int                `json:"records"`
	Heads          int                `json:"heads"`        // heads that verified and belong to this journal
	LargestHead    int                `json:"largest_head"` // the largest such head's size, 0 if none
	Decisions      []DecisionCoverage `json:"decisions"`
	RejectedHeads  []RejectedHead     `json:"rejected_heads,omitempty"`  // ignored: invalid, or not this run's
	DivergentHeads []DivergentHead    `json:"divergent_heads,omitempty"` // signed for this run, and not this journal's
	// NoCountedHeads is set when the journal holds decisions and no head counted: every head given
	// was ignored or divergent (a wrong -pubkey makes every head ignored), or none was given. With a
	// bound set, OK is false then: nothing shows a decision is inside it.
	NoCountedHeads bool `json:"no_counted_heads,omitempty"`
	// contains filtered or unexported fields
}

CoverageReport is CheckDecisionCoverage's result.

func CheckDecisionCoverage added in v0.17.0

func CheckDecisionCoverage(runID string, recs []agent.Record, heads []SignedTreeHead, v Verifier, bound CoverageBound) (CoverageReport, error)

CheckDecisionCoverage reports, for each approval decision in recs (runID's journal, in order), whether a signed tree head covers it, and flags the ones no head has covered for longer than bound. It finds the gap that opens when a decision is written by a process other than the one that signs the journal (a CLI or an approval service writing the database while the application holds the log key): the decision is in the journal, and under no signed head until the signer next writes to the run, which for a rejection may never happen.

A head counts only if it verifies under v, is a journal head of runID, and its root is the root of recs' own prefix of its size. A head that is not a journal head of runID, or does not verify, is ignored (RejectedHeads). A head the log key signed for runID that does not match recs, because recs is shorter than the head or differs from what it signed, is a DivergentHead and fails the check: the export is stale, truncated, or has had a record removed (re-export the journal; if it still diverges, the journal is not what was signed). A head of size n covers the records at positions 0 to n-1.

"Covered" means a signed head includes the record, not that the record is authentic: the check ratifies what the journal holds, so verify-approvals is the check that a decision is genuine. The bounds are only as strong as the export's completeness, which divergent heads test: an export cut short shrinks every decision's Behind, and recorded_at is not signed. Heads are used by size, never by the order they are given in or by their timestamps: two processes anchoring one run can deliver a smaller head after a larger one, and both commit to prefixes of one journal.

Overdue is decided by positions first (BasisRecords) and, when bound.MaxAge and bound.Now are set, by age (BasisAge), which compares two machines' clocks and is labeled as the weaker. A decision a head covers is covered however old it is. Halt resolutions are not decisions and are not checked.

It returns an error wrapping agent.ErrConfig for a nil verifier, and no error for a journal with no decisions.

func (CoverageReport) OK added in v0.17.0

func (r CoverageReport) OK() bool

OK reports whether no decision is overdue and no signed head diverges from the journal given.

type CurrentGrantProof added in v0.7.0

type CurrentGrantProof struct {
	Format      string      `json:"format"`      // CurrentGrantFormat
	Leaf        ProofBundle `json:"leaf"`        // the ledger's last leaf under the presented head
	Consistency Consistency `json:"consistency"` // from the verifier's last-seen size to Leaf.STH.Size
}

CurrentGrantProof proves which earned grant is current in a ledger: Leaf proves the grant is the last leaf of a signed ledger head, and Consistency proves that head is an append-only extension of the head the verifier saw last, so a verifier that remembers its last-seen head cannot be shown an older head as current.

func ProveCurrentGrant added in v0.7.0

func ProveCurrentGrant(ctx context.Context, ledger *agent.Journal, ledgerRunID string, sth SignedTreeHead, lastSeenSize int) (CurrentGrantProof, error)

ProveCurrentGrant proves which earned grant is current in the ledger at the signed ledger head sth: an inclusion proof of the ledger's last leaf (index sth.Size-1), plus a consistency proof from lastSeenSize (the size of the ledger head the verifier saw last, 0 if none) to sth.Size. Pass the latest anchored head of the ledger run. It fails if sth is not a head of ledgerRunID's journal, or if lastSeenSize is outside [0, sth.Size].

type DecisionCoverage added in v0.17.0

type DecisionCoverage struct {
	Step       string `json:"step"`
	Approver   string `json:"approver,omitempty"` // empty for a decision recorded without a signature (Approve)
	Index      int    `json:"index"`              // the record's 0-based position in the journal
	RecordedAt string `json:"recorded_at,omitempty"`
	Status     string `json:"status"`
	// Basis says what made an overdue decision overdue: BasisRecords, or BasisAge. Empty otherwise.
	Basis string `json:"basis,omitempty"`
	// CoveredBySize is the size of the smallest verified head that includes the record, 0 if none.
	CoveredBySize int `json:"covered_by_size,omitempty"`
	// Behind is how many records the journal holds after this one.
	Behind int `json:"behind"`
	// Note says what limits the finding: "no recorded time" for a record with no usable
	// recorded_at (an unsigned agent.Approve decision records none), which an age bound treats as
	// over the bound.
	Note string `json:"note,omitempty"`
}

DecisionCoverage is one recorded decision and the signed head, if any, that covers it.

type DecisionReceipt added in v0.17.0

type DecisionReceipt struct {
	Format    string          `json:"format"` // ReceiptFormat
	RunID     string          `json:"run_id"`
	ToolUseID string          `json:"tool_use_id"`
	ToolName  string          `json:"tool_name"`
	Args      json.RawMessage `json:"args"`        // the call's arguments, as the approver signed them
	ArgsSHA   string          `json:"args_sha256"` // SHA-256 of the canonical arguments, lowercase hex: what the signature binds
	Approver  string          `json:"approver"`
	Approved  bool            `json:"approved"`
	DecidedAt string          `json:"decided_at"` // the approver's clock, signed (agent.FormatTimestamp form)
	Alg       Alg             `json:"alg"`
	Signature []byte          `json:"signature"`
	// Cert is the leaf-first DER certificate chain of a certificate-bound decision (base64 in JSON),
	// signed under agent.DecisionFormatCert: the signature binds the leaf's hash, so a receipt whose
	// chain is stripped or swapped does not verify. Empty for a bare-key decision. Certificates are
	// public values; a receipt still holds no key material.
	Cert [][]byte `json:"cert,omitempty"`
	// Record is the journal record's name, Index its position in the run's journal (0 is the
	// journal header), and RecordedAt when it was journaled, by the clock of the process that wrote
	// it (not signed by the approver).
	Record     string `json:"record"`
	Index      int    `json:"index"`
	RecordedAt string `json:"recorded_at"`
	// LeafHash is the journal record's leaf hash (JournalLeafHash), lowercase hex: what a head's
	// tree holds at Index, and what a redaction tombstone keeps of the record.
	LeafHash string `json:"leaf_hash"`
	// Anchor is the proof that a signed head of the journal commits to the decision at Index: the
	// record's inclusion under that head. It is empty on a receipt just written and added by
	// AnchorReceipt (bide audit verify-receipts -update) once a signed head covers the decision.
	// Everything above it is the approver's claim: Record, Index, RecordedAt and LeafHash are not
	// signed, and nothing in a receipt without an Anchor shows the application ever journaled the
	// decision. With an Anchor it does, and only then can CheckReceipt say the decision was deleted.
	Anchor *ProofBundle `json:"anchor,omitempty"`
}

DecisionReceipt is one signed approver decision, with the journal record that holds it.

func AnchorReceipt added in v0.17.0

func AnchorReceipt(r DecisionReceipt, journal []agent.Record, heads []SignedTreeHead, logKey Verifier) (DecisionReceipt, error)

AnchorReceipt returns r with its anchoring proof: the inclusion of the decision's record under the smallest signed head of heads that reaches its position and matches journal. It fails, with ErrNotVerified, if the journal does not hold the decision at the position under such a head (a proof is made only of what holds), and if the record was redacted (its bytes are gone).

func NewDecisionReceipt added in v0.17.0

func NewDecisionReceipt(s agent.ApprovalSubject, d agent.Decision, rec agent.Record, index int) (DecisionReceipt, error)

NewDecisionReceipt returns the receipt of decision d on call s, held by the journal record rec at position index of the run's journal. It refuses a record that is not that decision.

func (DecisionReceipt) AnchorTime added in v0.19.0

func (r DecisionReceipt) AnchorTime(logKey Verifier) (time.Time, error)

AnchorTime is the timestamp of the signed head the receipt's proof carries, as logKey vouches for it. It is an upper bound on when the decision became provable (A in the design note): the proof is not shown to carry the first head that covers the decision, and an earlier one may exist. It fails if the receipt has no proof or the proof does not hold. The time is the log signer's clock; an independent time source is a separate attestation.

func (DecisionReceipt) Verify added in v0.17.0

func (r DecisionReceipt) Verify(v Verifier) error

Verify checks the receipt on its own: it is of ReceiptFormat, its arguments hash to ArgsSHA, and Signature is a valid signature, by the approver's key v, over the decision bytes (agent.ApprovalDecisionBytes) of exactly this run, call, tool, arguments, approver, decision and time. It does not look at the journal. An error wraps ErrNotVerified, or ErrFormat.

A receipt that carries a chain (Cert) is a certificate-bound decision, and Verify refuses it with an error wrapping ErrCertificateBound and ErrNotVerified: a caller holding only a key cannot learn whose decision it is. VerifyCertificate checks such a receipt.

func (DecisionReceipt) VerifyCertificate added in v0.19.0

func (r DecisionReceipt) VerifyCertificate(ci *CertIdentity, logKey Verifier) error

VerifyCertificate checks a certificate-bound receipt against the gate's certificate policy: the chain it carries is accepted for r.Approver at a, the time of the head the receipt's anchoring proof carries as logKey vouches for it (see AnchorTime; a receipt with no proof fails, and the caller cannot choose the time), by exactly the rules CertIdentity.VerifyChain applies to a decision the gate counts; the receipt's scheme is the one the leaf's key has; and the signature verifies under that key over the v4 bytes. A receipt with no chain, a chain to another anchor, the wrong identifier, or a time outside the certificate's validity fails with an error wrapping both ErrNotVerified and the *CertError that says why. It does not check revocation status (see CertIdentity.EvaluateStatus), and "verified" here is the identity check at a, not the verdict.

func (DecisionReceipt) VerifyIdentity added in v0.19.0

func (r DecisionReceipt) VerifyIdentity(ci *CertIdentity, logKey Verifier) (ReceiptIdentity, error)

VerifyIdentity checks a certificate-bound receipt on its own: the anchoring proof holds under logKey and fixes A (AnchorTime), and the chain, the identity and the signature are accepted by ci at A (VerifyCertificate). It does not check revocation status and it reads no journal, so a nil error is the identity check at A, not the gate's verdict. The time is the log signer's clock.

type DecisionTime added in v0.16.0

type DecisionTime struct {
	Approver   string   `json:"approver"`
	Step       string   `json:"step"`
	DecidedAt  string   `json:"decided_at"`
	RecordedAt string   `json:"recorded_at,omitempty"`
	Flags      []string `json:"flags,omitempty"`
}

DecisionTime is the two times of one counted approver decision, kept apart on purpose:

  • DecidedAt is when the approver says they decided, by their own clock. It is inside the signed decision (agent.ApprovalDecisionBytes), so it cannot be changed after signing, but it is the approver's claim: their clock may be wrong.
  • RecordedAt is when the decision was journaled, by the clock of the process that recorded it (SubmitDecision's caller: its WithClock, else its context's clock, else its wall clock). It is that submitter's claim, not a time bide vouches for: it is not signed, and an identical resubmission keeps the first submitter's time. The journal's Merkle trail anchors it, with the record, under the signed tree head, so it cannot change after the head is signed, and the head's own time bounds it (FlagRecordedAfterHead).

Neither time affects the count (see agent.TallyApprovals). Flags are sanity checks for a person reading the evidence (FlagDecisionTimes); they are never a verdict and never fail verification.

func CheckDecisionTimes added in v0.16.0

func CheckDecisionTimes(times []DecisionTime, c TimeCheck) []DecisionTime

CheckDecisionTimes returns times, in journal order, with Flags recomputed: FlagDecisionTimes' flags under c.AskedAt and c.Tolerance, and, for a canonical RecordedAt, FlagRecordedOutOfOrder when it is more than the tolerance before an earlier decision's RecordedAt or before c.AskedAt, and FlagRecordedAfterHead when it is more than the tolerance after c.HeadAt. The input is not modified. No flag affects the count or the verdict.

func FlagDecisionTimes added in v0.16.0

func FlagDecisionTimes(times []DecisionTime, askedAt string, tolerance time.Duration) []DecisionTime

FlagDecisionTimes returns times with Flags recomputed under tolerance: FlagDecidedAfterRecorded when a decision's DecidedAt is more than tolerance after its RecordedAt (FlagRecordedAtInvalid in its place when RecordedAt is missing or not a canonical time), and FlagDecidedBeforeAsked when its DecidedAt is before askedAt, the time the run asked for approval (ApprovalVerdict.AskedAt), and FlagAskedUnknown in its place when askedAt is "" or not a canonical time. A negative tolerance is treated as zero. The input is not modified. No flag affects the count or the verdict.

func (DecisionTime) Gap added in v0.16.0

func (d DecisionTime) Gap() (gap time.Duration, ok bool)

Gap returns RecordedAt minus DecidedAt: how long after the approver's signed time bide recorded the decision (negative when the signed time is later). ok is false when either time is missing or not in canonical form, or when the two are too far apart for a time.Duration (about 292 years; Sub would saturate). It is shown, not judged; the flags compare the times themselves.

type DivergentHead added in v0.17.0

type DivergentHead struct {
	Size   int    `json:"size"`
	Reason string `json:"reason"`
}

DivergentHead is a head the log key signed for this run that does not match the journal given: the journal is shorter than the head (a stale or truncated export), or its first Size records are not what the head signed (an export, or a journal, that differs from what was signed). It is evidence against the export, not noise: a decision deleted from the export looks exactly like this, so any such head fails the check.

type ECDSAP256Signer added in v0.17.0

type ECDSAP256Signer struct {
	Priv *ecdsa.PrivateKey // the P-256 private key that signs
}

ECDSAP256Signer signs with an ECDSA P-256 private key.

func (ECDSAP256Signer) Alg added in v0.17.0

func (ECDSAP256Signer) Alg() Alg

Alg reports the signature scheme (AlgECDSAP256).

func (ECDSAP256Signer) PublicKey added in v0.17.0

func (s ECDSAP256Signer) PublicKey() []byte

PublicKey returns the 65-byte uncompressed SEC1 public key, or nil if there is no key or it is not on P-256.

func (ECDSAP256Signer) Sign added in v0.17.0

func (s ECDSAP256Signer) Sign(m []byte) ([]byte, error)

Sign returns the 64-byte low-S r||s signature over SHA-256(m), or an error if the key is unusable.

type ECDSAP256Verifier added in v0.17.0

type ECDSAP256Verifier struct {
	Pub *ecdsa.PublicKey // the P-256 public key that verifies
}

ECDSAP256Verifier verifies with an ECDSA P-256 public key.

func (ECDSAP256Verifier) Alg added in v0.17.0

func (ECDSAP256Verifier) Alg() Alg

Alg reports the signature scheme (AlgECDSAP256).

func (ECDSAP256Verifier) KeyIDs added in v0.17.0

func (v ECDSAP256Verifier) KeyIDs() []string

KeyIDs identifies the public key, for agent.ApproverVerifier: one entry, KeyID(AlgECDSAP256, the 65-byte point). A verifier without a usable key reports none.

func (ECDSAP256Verifier) PublicKey added in v0.17.0

func (v ECDSAP256Verifier) PublicKey() []byte

PublicKey returns the 65-byte uncompressed SEC1 public key, or nil if there is none or it is not a P-256 point.

func (ECDSAP256Verifier) Verify added in v0.17.0

func (v ECDSAP256Verifier) Verify(m, sig []byte) bool

Verify reports whether sig is a canonical (64-byte, 1 <= r < n, 1 <= s <= (n-1)/2) ECDSA P-256 signature over SHA-256(m). Any other encoding, including a valid signature with a high S, is refused. A verifier without a usable key verifies nothing.

type EarnedAuthority

type EarnedAuthority struct {
	// contains filtered or unexported fields
}

EarnedAuthority is a control loop that adjusts an agent's delegated limit from its provable track record. Authority starts at the ladder's baseline rung and is promoted one rung after promoteEvery compliant actions, capped at the top rung; an anomaly resets it to baseline at once. Each change re-issues a signed grant that is a CHILD of the root grant (ParentRef to root, every root scope constraint kept, "limit" = the current rung), so the earned limit provably never exceeds the root ceiling: every earned grant passes VerifyDelegationChain against the root with ScopeRules{"limit": NumericAtMost}, and even a buggy or compromised controller cannot widen past what the root principal authorized. Every earned grant carries the root's NotAfterUnix, so none outlives the authority it was carved from.

Revocation. A demotion must take the old, higher grant out of use at once, not when it expires. Each re-issued grant is appended to a ledger: a journal run (ledgerRunID in ledger) that holds nothing but this controller's grants, one leaf per issue (the grant's ID names its ledger index, so no two issues share a digest, even at the same rung). The CURRENT grant is the last leaf of the ledger; every earlier grant is superseded, whether the change was a promotion or a demotion. An offline verifier learns the current grant the way it learns any run's state: from the latest signed head of the ledger run in the anchor log (anchor the ledger with an AuditedStore, or sign and publish its heads), checked for consistency with the last head it saw, so the ledger cannot be rolled back to a superseded grant. ProveCurrentGrant produces the proof and VerifyCurrentGrant checks it: the grant is the leaf at index Size-1 of an authentic head of the ledger run, and that head extends the verifier's last-seen head.

It is deliberately a sequential controller, not a convergent gsm machine. Earning is temporal: how many compliant actions preceded a promotion, and whether an anomaly interleaved, is order-dependent, so it belongs to the durable/saga tier, not the order-independent governance tier (a promote event and a record event do not commute, so no convergent machine expresses it). The ENFORCEMENT of the current limit stays a convergent gsm invariant on the work machine (see examples/govern/earned-authority); the controller only decides the limit and re-issues the grant.

The asymmetry is the safety property: promotion is slow, capped, and evidence-gated; attenuation is immediate and needs no gate, because shrinking authority is always safe.

func NewEarnedAuthority

func NewEarnedAuthority(ctx context.Context, ladder []int, promoteEvery int, root SignedGrant, signer Signer, subject string, ledger *agent.Journal, ledgerRunID string) (*EarnedAuthority, error)

NewEarnedAuthority builds a controller for subject, granting the limits in ladder (rung 0 is the baseline) and promoting one rung every promoteEvery compliant actions. root is the ceiling: the top rung must not exceed root's numeric "limit" scope. signer signs as root's Subject. Each issued grant is recorded in ledgerRunID of ledger, which must hold only this controller's grants; a restarted controller continues the ledger at baseline. It issues the baseline grant immediately.

func (*EarnedAuthority) FlagAnomaly

func (e *EarnedAuthority) FlagAnomaly(ctx context.Context) (changed bool, err error)

FlagAnomaly resets authority to the baseline rung immediately, returning whether it changed. The re-issued baseline grant supersedes every earlier grant in the ledger.

func (*EarnedAuthority) Grant

func (e *EarnedAuthority) Grant() SignedGrant

Grant returns the current signed earned grant (a child of root, scope limit = the current rung).

func (*EarnedAuthority) Limit

func (e *EarnedAuthority) Limit() int

Limit returns the currently earned limit.

func (*EarnedAuthority) RecordCompliant

func (e *EarnedAuthority) RecordCompliant(ctx context.Context) (promoted bool, err error)

RecordCompliant notes one clean governed action; it promotes one rung when the streak reaches the threshold (unless already at the top), returning whether a promotion happened.

func (*EarnedAuthority) Tier

func (e *EarnedAuthority) Tier() int

Tier returns the current rung (0 is baseline).

type Ed25519Signer

type Ed25519Signer struct {
	Priv ed25519.PrivateKey // the Ed25519 private key that signs
}

Ed25519Signer signs with an Ed25519 private key.

func (Ed25519Signer) Alg

func (Ed25519Signer) Alg() Alg

Alg reports the signature scheme (AlgEd25519).

func (Ed25519Signer) Format added in v0.7.0

func (s Ed25519Signer) Format(f fmt.State, _ rune)

Format writes String for every verb, so no fmt verb prints the key.

func (Ed25519Signer) GobEncode added in v0.7.0

func (Ed25519Signer) GobEncode() ([]byte, error)

GobEncode refuses: the signer holds a private key.

func (Ed25519Signer) MarshalJSON added in v0.7.0

func (Ed25519Signer) MarshalJSON() ([]byte, error)

MarshalJSON refuses: the signer holds a private key.

func (Ed25519Signer) PublicKey added in v0.9.0

func (s Ed25519Signer) PublicKey() []byte

PublicKey returns the 32-byte Ed25519 public key, or nil if the private key has the wrong length.

func (Ed25519Signer) Sign

func (s Ed25519Signer) Sign(m []byte) ([]byte, error)

Sign returns the Ed25519 signature over m, or an error if the private key has the wrong length.

func (Ed25519Signer) String added in v0.7.0

func (Ed25519Signer) String() string

String returns a description of the signer that does not include the key.

type Ed25519Verifier

type Ed25519Verifier struct {
	Pub ed25519.PublicKey // the Ed25519 public key that verifies
}

Ed25519Verifier verifies with an Ed25519 public key.

func (Ed25519Verifier) Alg

func (Ed25519Verifier) Alg() Alg

Alg reports the signature scheme (AlgEd25519).

func (Ed25519Verifier) KeyIDs added in v0.9.0

func (v Ed25519Verifier) KeyIDs() []string

KeyIDs identifies the public key, for agent.ApproverVerifier: one entry, KeyID(AlgEd25519, Pub). A weak public key (see CheckEd25519PublicKey) verifies nothing and reports no entry, so an approval gate refuses it as a configuration error rather than seating it. This also keeps one secret to one identity: a mixed-order key A + T, which A's secret can sign for, and a non-canonical spelling of A are both refused.

func (Ed25519Verifier) PublicKey added in v0.9.0

func (v Ed25519Verifier) PublicKey() []byte

PublicKey returns the public key, or nil if it has the wrong length.

func (Ed25519Verifier) Verify

func (v Ed25519Verifier) Verify(m, sig []byte) bool

Verify reports whether sig is a valid Ed25519 signature over m. A weak public key (see CheckEd25519PublicKey: the wrong length, not canonically encoded, or not in the prime-order subgroup) verifies nothing, since a small-order key accepts forged signatures; it never panics.

type ErasedEnd added in v0.21.0

type ErasedEnd struct {
	RunID string `json:"run_id"`
	// Marker is the end marker's name the trail recorded: run:complete, run:aborted or run:cancelled.
	Marker string `json:"marker"`
	// Kind is how the run ended: completed, aborted or cancelled.
	Kind string `json:"kind"`
}

ErasedEnd is a run whose end marker was erased, with how it ended per the signed redaction trail.

type EventInclusion added in v0.7.0

type EventInclusion struct {
	Format string `json:"format"` // EventInclusionFormat
	Salt   []byte `json:"salt"`   // the proven event's salt (agent.SaltSize bytes)
	Inclusion
}

EventInclusion is an inclusion proof for one event: the event's own salt, which its leaf commits to, and the RFC 6962 audit path (the same Inclusion journal proofs use) from that leaf to the log's Root. It discloses the proven event's salt, its index, the log's size, and the sibling hashes on its path. Each sibling hash covers other events' leaves, and every leaf commits to its own random salt, so a holder of the proof cannot confirm a guess about any other event by hashing it: the salt a guess would need is disclosed only by that event's own proof.

type EventLog

type EventLog struct {
	// contains filtered or unexported fields
}

EventLog is an append-only, tamper-evident log of one run's RunEvents. Build it by Add-ing events in emission order (Agent.Stream emits them ordered from a single goroutine); then Root/Head to commit, Prove for selective disclosure, and audit.Sign to anchor. Not safe for concurrent Add — feed it from the one goroutine ranging Events.

func EventLogFromJournal

func EventLogFromJournal(ctx context.Context, store *agent.Journal, runID string) (*EventLog, error)

EventLogFromJournal builds an EventLog from runID's DURABLE journal by projecting it to the semantic events Agent.Stream re-emits on resume (agent.ReplayEvents). This is the crash-durable, resume-stable counterpart to filling an EventLog from the live stream: it is a deterministic function of the persisted journal, so its Root / STH are byte-identical before and after a crash. THIS is the artifact to anchor for a durable audit trail; the live-stream EventLog is a real-time view (and its Root shifts between a fresh run and its replay because live-only events differ). Root / Head / TreeHead / Prove / ProveConsistency then work exactly as they do on any EventLog.

Each projected event comes from one journal record, and its salt is derived from that record's random salt (journalEventSalt), so the projection needs no state beyond the journal. It errors if a source record has no agent.SaltSize salt, and refuses a journal it cannot project: one holding a redacted record (ErrRedacted) or a record whose stored bytes read two ways to JSON readers (ErrMalformed).

func LoadEventLog

func LoadEventLog(ctx context.Context, evStore EventStore, runID string) (*EventLog, error)

LoadEventLog rebuilds an EventLog from evStore's persisted leaves for runID, ready for Root / Head / TreeHead / Prove / ProveConsistency — from the store alone, no journal needed. The leaves were canonicalized, salts included, when stored, so inclusion proofs verify against the original events exactly as if the log had been built live. It refuses a leaf that is not a salted bide.audit.event-leaf.v2 leaf: its proof would have no salt to disclose.

func NewEventLog

func NewEventLog() *EventLog

NewEventLog returns an empty log.

func (*EventLog) Add

func (l *EventLog) Add(e agent.RunEvent) error

Add appends one event as a canonical, kind-tagged leaf that commits to a fresh random salt (agent.SaltSize bytes from crypto/rand; see EventInclusion). It errors if the event cannot be canonicalized (it holds a string that is not valid UTF-8, for one) or the system's random source fails.

func (*EventLog) Head

func (l *EventLog) Head() []byte

Head is the linear SHA-256 hash-chain commitment over the events (domain-separated), the event-stream analogue of audit.Head: head_0 = H(eventDomain), head_i = H(head_{i-1} || canonical(event_i)). Cheaper than Root, no selective disclosure.

func (*EventLog) Len

func (l *EventLog) Len() int

Len is the number of events committed so far.

func (*EventLog) Prove

func (l *EventLog) Prove(index int) (EventInclusion, error)

Prove returns an inclusion proof for the event at index against Root: enough to verify that one event WITHOUT revealing any other (selective disclosure over the event stream). The proof carries the event's salt and nothing about any other event beyond the hashes on its path (see EventInclusion). audit.Sign over Root and this proof compose exactly as they do for the journal.

func (*EventLog) ProveConsistency

func (l *EventLog) ProveConsistency(first int) (Consistency, error)

ProveConsistency proves the first `first` events are an append-only PREFIX of the current log — nothing observed earlier was rewritten or reordered, only appended. Verify with the shared VerifyConsistency against two published event-log roots (e.g. two STH Roots taken at different points in the run). This is the transparency-log guarantee over the event stream, the same one ProveConsistency gives over the journal.

func (*EventLog) Root

func (l *EventLog) Root() []byte

Root is the RFC 6962 Merkle root over every event added so far (empty-log root = SHA-256(); identical shape to audit.Root over the journal). Supports Prove.

func (*EventLog) TreeHead

func (l *EventLog) TreeHead(runID string, timestamp int64) TreeHead

TreeHead returns a commitment to runID's events added so far at the given timestamp, the EventLog analogue of NewTreeHead over the journal. Its Kind is TreeEvents, so a signed event head never verifies as a journal head. timestamp is Unix nanoseconds. Sign it with SignTreeHead and verify with SignedTreeHead.Verify; the signature binds kind, run, root, size, and timestamp, so an event trail gets the same anchored guarantee a journal STH gives. Inclusion proofs (Prove / VerifyEventInclusion) check against the resulting Root; Size counts events.

type EventStore

type EventStore interface {
	// Append durably records leaf at position seq (0-based, contiguous) for runID. It MUST be
	// append-only and idempotent on (runID, seq): re-appending an already-stored seq with the
	// SAME bytes is a no-op (so a retried mirror is safe), while a DIFFERENT leaf at an
	// existing seq must fail with an error that wraps ErrSeqTaken (a fork / tamper attempt, or a
	// lost race with another writer of that position, of which exactly one wins). A seq beyond the
	// next position, or a negative one, is a gap and must error without wrapping ErrSeqTaken. The
	// store may be shared by several handles or processes; every one sees the same positions.
	// Positions are contiguous per run, so a writer that wants seq n must have seen n-1 stored.
	Append(ctx context.Context, runID string, seq int, leaf []byte) error
	// Load returns every leaf for runID in seq order (0..n-1), or nil if the run is unknown.
	Load(ctx context.Context, runID string) ([][]byte, error)
}

EventStore is the bring-your-own port: append canonical event leaves to your append-only backend (a Postgres table with UNIQUE(run_id, seq) and insert-only grants, object storage with object-lock/WORM, or a log) and read them back. Bide ships MemEventStore as the in-memory default; you implement Append/Load against infrastructure you already run. A leaf holds its event's salt (see EventInclusion), so a store must keep the bytes verbatim: a proof built from the reloaded trail discloses the salt from there.

type EvidenceAction

type EvidenceAction struct {
	// Label is a short human description of the action: the tool-use id for "tool", the step name
	// for "step", the called tool's name for "call", the approver id for "approval", "approval
	// tally" for "approval-tally", and "grant <issuer> -> <subject>" for "grant".
	Label string `json:"label"`
	// Kind is the action category, one of "tool", "step", "grant", "call", "approval", or
	// "approval-tally"; the proven record must be of that kind.
	Kind EvidenceKind `json:"kind"`
	// Ref is the identifier the action was selected by: the tool-use id ("tool", "call"), the record
	// name ("step", "approval", "approval-tally"), or the grant digest ("grant").
	Ref string `json:"ref,omitempty"`
	// Bundle is the inclusion proof that this action is a committed leaf under the package's STH.
	Bundle ProofBundle `json:"bundle"`
}

EvidenceAction is one proven action inside an EvidencePackage: a ProofBundle plus the label, kind, and reference the report names it by. Verify derives all three from the proven record and fails the item if they differ, so they cannot misdescribe what was proven.

func ApprovalEvidence added in v0.4.0

func ApprovalEvidence(ctx context.Context, store *agent.Journal, runID, toolUseID string, sth SignedTreeHead) ([]EvidenceAction, error)

ApprovalEvidence builds the complete evidence for an m-of-n gated call under one STH, in this order: for a gate whose tally excluded the requester (agent.ApprovalTally.ExcludePrincipal), the run's run:principal record (Kind "step", Ref "run:principal"; agent.RunPrincipal), which names the requesters and nothing of the run's input; the model turn that requested the call (Kind "call"), the record that the run paused at the gate (agent.ApprovalWait, Kind "step", when the journal holds one: it carries when approvers were asked), every decision record the gate read (Kind "approval", from the journaled tally's Records, in journal order), the gate's terminal tally (Kind "approval-tally"), and the call's result (Kind "tool", labelled by its tool-use id as every tool action is). For a gated plan flow node, toolUseID is the node's key ("node:<name>", "node:iter:<n>:<name>"): the request is the gate's recorded agent.ApprovalRequest and the result is the node's own record (Kind "step"). The result appends to EvidencePackage.Actions, where each entry verifies as a package action; drop the trailing "tool" entry if the package already carries the call.

It discloses everything the gate read, valid or not, so VerifyApprovals can recount from the same inputs and detect an omission. It errors if the call has no journaled tally (it was not m-of-n gated, or its gate has not reached an outcome), no recorded request, or no result, or if a proof cannot be built against sth. The tally is read as VerifyApprovals reads it (UnmarshalStrict), so a tally that reads two ways is ErrMalformed here too.

type EvidenceConsistency added in v0.7.0

type EvidenceConsistency struct {
	// From is an earlier signed journal head of the same run. Proof.First must equal From.Size.
	From SignedTreeHead `json:"from"`
	// Proof is the RFC 6962 consistency proof from From's tree to the package STH's tree.
	Proof Consistency `json:"proof"`
}

EvidenceConsistency proves the journal only grew between two signed heads of one run: From, an earlier head (typically taken from the anchor log), and the package STH.

type EvidenceGrants

type EvidenceGrants struct {
	// Chain is the delegation chain, root-first to leaf-last, as signed grants (may be a single grant).
	Chain []SignedGrant `json:"chain"`
	// Anchored is one ProofBundle per grant in Chain, in the same order, proving that exact signed
	// grant is committed in the signed log.
	Anchored []EvidenceAction `json:"anchored"`
}

EvidenceGrants carries the run's authority: the delegation chain as signed grants (root-first) and, per grant, a ProofBundle proving the grant leaf is anchored in the same signed log as the actions.

Note the trust boundary: the anchoring bundles verify under the package's log key, and Verify checks that Chain is exactly the grants they prove. A grant's own issuer signature is made with the ISSUER's key, which is distinct from the log key and is not carried here; verifying the issuer signatures and the attenuation chain (VerifyDelegationChain) requires the issuers' public keys, which a caller supplies from their own PKI.

type EvidenceItem

type EvidenceItem struct {
	Kind     EvidenceKind `json:"kind"`  // the item's kind: an action kind, KindRunCertificate, or KindConsistency
	Label    string       `json:"label"` // the human label for the item
	Ref      string       `json:"ref,omitempty"`
	Verified bool         `json:"verified"` // whether this item verified under the key
	Note     string       `json:"note"`     // plain-English detail, on success or failure
}

EvidenceItem is one line of an EvidenceReport: what was checked, whether it verified, and a short human note (the reason on failure, or a one-line description on success).

type EvidenceKind added in v0.9.0

type EvidenceKind string

EvidenceKind names what one item of an EvidencePackage proves.

const (
	KindCall          EvidenceKind = "call"           // the model turn that requested the gated call (its name and arguments)
	KindApproval      EvidenceKind = "approval"       // one approver decision record
	KindApprovalTally EvidenceKind = "approval-tally" // the gate's journaled terminal tally
	KindTool          EvidenceKind = "tool"           // the call's result: the action, or the denial the model received
)

Evidence kinds for an m-of-n approval, in the order ApprovalEvidence emits them.

const (
	KindStep           EvidenceKind = "step"            // a durable step's recorded value
	KindGrant          EvidenceKind = "grant"           // an anchored signed grant leaf
	KindRunCertificate EvidenceKind = "run-certificate" // the package's run certificate (a report item only)
	KindConsistency    EvidenceKind = "consistency"     // the package's consistency proof (a report item only)
)

Other evidence kinds.

type EvidenceOption

type EvidenceOption func(*evidenceOptions)

An EvidenceOption selects what an EvidencePackage includes. With no action option (WithToolCall, WithStep, or WithAllToolCalls), Evidence defaults to proving every completed tool call, so the zero-configuration package is "prove every action this run took".

func WithAllToolCalls

func WithAllToolCalls() EvidenceOption

WithAllToolCalls proves every completed tool call in the run, enumerated from its history. It is the default when no other action option is given.

func WithConsistencyFrom added in v0.7.0

func WithConsistencyFrom(earlier SignedTreeHead) EvidenceOption

WithConsistencyFrom includes an append-only consistency proof from earlier, a signed journal head of the same run taken before this package (for instance the last head an auditor saw in the anchor log), to the package's STH: nothing earlier than earlier.Size was rewritten or reordered.

func WithGrants

func WithGrants() EvidenceOption

WithGrants includes the run's authority: every anchored grant leaf, proven included in the signed log, plus the signed grants themselves as the delegation chain (in journal order, root-first for a well-formed attenuation chain).

func WithLabel

func WithLabel(label string) EvidenceOption

WithLabel sets a human-readable description of what the evidence establishes.

func WithRunCertificate

func WithRunCertificate(spec RunCertSpec) EvidenceOption

WithRunCertificate includes the proof-carrying run certificate for the whole run, certified against spec.ApprovedPolicies. It fails assembly if the run used a policy outside the allowlist (CertifyRun refuses). Evidence signs the certificate with its own signer at its own timestamp; spec.Signer and spec.TimestampNanos are ignored. The verifier checks the certificate against its own allowlist (WithApprovedPolicies), not this one.

func WithStep

func WithStep(name string) EvidenceOption

WithStep adds a proof that the durable step with this name (a Step / Parallel task) happened.

func WithToolCall

func WithToolCall(toolUseID string) EvidenceOption

WithToolCall adds a proof that the completed tool call with this tool-use id happened in the run.

type EvidencePackage

type EvidencePackage struct {
	// Format is the domain-separated format/version tag (EvidenceFormat).
	Format string `json:"format"`
	// RunID is the run this evidence is about; it must equal STH.RunID.
	RunID string `json:"run_id"`
	// Label is an optional producer-supplied description of what this evidence establishes (e.g.
	// "Q3 refund approvals"). No proof covers it; the seal attributes it to the log key holder.
	Label string `json:"label,omitempty"`
	// Alg and PublicKey name the key that signed the STH and the seal: its scheme and its encoded
	// public key (see NewVerifier), carried so a reader knows which key to obtain out of band.
	// Verify requires them to equal the verifier's; it never trusts them as the key.
	Alg       Alg    `json:"alg"`
	PublicKey []byte `json:"public_key"`

	// STH is the run's signed journal head: the commitment every action proof below is checked
	// against. One STH is built for the whole package so all proofs share one root. Its Timestamp is
	// when the package's commitment was made.
	STH SignedTreeHead `json:"sth"`

	// Actions are the material tool calls / steps proven included in the signed log. Each carries a
	// human label and the ProofBundle proving that one action is a leaf under the STH's root.
	Actions []EvidenceAction `json:"actions"`

	// Grants optionally carries the authority behind the run: the delegation chain (root-first) as
	// signed grants, plus a ProofBundle per grant proving the grant leaf is anchored in the signed
	// log. It is nil when the caller did not request grants (WithGrants).
	Grants *EvidenceGrants `json:"grants,omitempty"`

	// RunCertificate optionally carries the proof-carrying run certificate (only-approved-policies +
	// policies-convergence-certified) for the whole run. It is nil unless WithRunCertificate was set.
	RunCertificate *RunCertificate `json:"run_certificate,omitempty"`

	// Consistency optionally carries an append-only consistency proof from an earlier signed head of
	// this run's journal to STH. It is nil unless WithConsistencyFrom was set.
	Consistency *EvidenceConsistency `json:"consistency,omitempty"`

	// Signature is the seal: the log key's signature, under Alg, over the seal tag and the SHA-256
	// of the package's JSON with Signature empty (see Seal). A package changed after sealing, including one with actions appended, fails to
	// verify until resealed.
	Signature []byte `json:"signature"`
}

An EvidencePackage is the portable, offline-verifiable evidence artifact for one run: a single self-contained bundle that PACKAGES a run's already-produced cryptographic proofs (a signed tree head, per-action inclusion proofs, and optionally the run certificate, the authority grant chain, and an append-only consistency proof) into one JSON document you can store, email, or hand to an auditor. Verify re-checks each one against the out-of-band public key.

Every field is verified or derived from verified data. Format must be EvidenceFormat. RunID must be the run the signed STH commits to. Each action's Kind, Label, and Ref must be what its proven record says (a tool result cannot be presented as an approval). The grant chain must be exactly the grants the anchored leaves prove. The run certificate must be for this run and tree. The consistency proof is checked from an earlier signed head of the same run to the STH. Alg and PublicKey must be the verifying key's. Label, the one field no proof covers, is covered by the seal (Signature): the log key's signature over the whole package, so nothing (Label included) can be edited, and no item added or dropped, after the producer sealed it.

It is deliberately PURE DATA: no funcs, no keys, no unexported state, so it round-trips losslessly through JSON. The verifying key must be obtained out of band (the anchor / transparency log); Verify takes it as an argument rather than trusting Alg and PublicKey.

func Evidence

func Evidence(ctx context.Context, store *agent.Journal, runID string, s Signer, timestamp int64, opts ...EvidenceOption) (EvidencePackage, error)

Evidence assembles and seals the portable, offline-verifiable EvidencePackage for runID. It reads the journal once, builds ONE signed tree head over it (signed with s at timestamp, Unix nanoseconds), then, per the options, gathers inclusion proofs for the material tool calls / steps and, optionally, the run certificate, the anchored authority grants, and an append-only consistency proof, all bound to that one STH and built for exactly the records it commits to. With no action option it proves every completed tool call in the run. It finishes with Seal(s).

It mints no new proof types: every proof is an existing audit primitive, packaged into one document a verifier re-checks offline with EvidencePackage.Verify against the out-of-band key. The signer's scheme and public key are recorded as metadata (Verify requires them to match the verifier it is given; it does not trust the embedded ones).

func (*EvidencePackage) Seal added in v0.7.0

func (e *EvidencePackage) Seal(s Signer) error

Seal signs the package with s, the log key that signed its STH. The signed message is fixed-size: the domain tag "bide.audit.evidence.v6.seal\x00" followed by the SHA-256 of the package's JSON encoding with Signature empty, so the size of the package never reaches the signer. Evidence seals what it builds; a caller that adds actions afterwards (for instance ApprovalEvidence's output) seals again. It refuses a signer whose scheme and key are not the ones the package names (Alg, PublicKey), a package with invalid UTF-8 in any string, which JSON would silently rewrite, and a package of the older EvidenceFormatV5, which is never re-sealed.

func (EvidencePackage) Verify

Verify re-checks every proof in the package against v, a verifier for the log key obtained OUT OF BAND (the anchor / transparency log), entirely offline. It does not trust the package's embedded key: it requires it to be v's. It verifies: (0) the format tag, that Alg and PublicKey are v's scheme and key, the seal, and that the STH is an authentic journal head of RunID; (1) each action ProofBundle, confirming the action is a leaf under that signed root and that its Kind, Label, and Ref are what the proven record says; (2) each anchored grant bundle the same way, and that Chain is exactly the grants they prove; (3) the run certificate, if present, is for this run and tree and passes VerifyRun against the allowlist given with WithApprovedPolicies; and (4) the consistency proof, if present, from its authentic earlier head of this run to the STH. A package with none of (1) to (4) proves nothing and does not verify. Every signed head the package carries is held to the timestamp rule (see CheckTimestamp): against time.Now and DefaultClockSkew unless WithVerifyTime and WithClockSkew say otherwise.

It returns a structured EvidenceReport with an overall verdict and a per-item verdict. The error is nil when the package verifies (OK), and wraps ErrNotVerified when it does not (the report says why). Any other error means the package could not be checked: it is not of the format this version reads (ErrFormat), or a proof it carries cannot be read (ErrFormat, ErrMalformed). Grant ISSUER signatures and the attenuation chain are NOT checked here (they need the issuers' keys, which are not in the package); a caller verifies those with VerifyDelegationChain and its own PKI.

type EvidenceReport

type EvidenceReport struct {
	// OK is the overall verdict: true only when the package-level checks pass and every item verified.
	OK bool `json:"ok"`
	// RunID echoes the package's run id; when OK it is the run the signed STH commits to.
	RunID string `json:"run_id"`
	// Label echoes the package's optional label; when OK it is the label the log key sealed.
	Label string `json:"label,omitempty"`
	// STHVerified is true when the signed tree head is authentic under the verifying key and is a
	// journal head of RunID. Every action proof is checked against this same root.
	STHVerified bool `json:"sth_verified"`
	// Problems lists every package-level failure (format, seal, key, run binding), empty when none.
	Problems []string `json:"problems,omitempty"`
	// Items is one verdict per proven item (each action, each anchored grant, the run certificate, and
	// the consistency proof), in report order.
	Items []EvidenceItem `json:"items"`
}

EvidenceReport is the structured outcome of EvidencePackage.Verify: an overall verdict plus a per-item verdict list, so a caller (or the CLI) can print exactly which proven item held and which did not. It is the machine-readable form behind the plain-English CLI report.

type EvidenceVerifyOption added in v0.7.0

type EvidenceVerifyOption func(*evidenceVerifyOptions)

EvidenceVerifyOption supplies what the auditor, not the package, decides.

func WithApprovedPolicies added in v0.7.0

func WithApprovedPolicies(digests ...string) EvidenceVerifyOption

WithApprovedPolicies is the auditor's approved-policy allowlist, against which a package's run certificate is checked. A package that carries a run certificate fails verification without it.

func WithClockSkew added in v0.8.0

func WithClockSkew(d time.Duration) EvidenceVerifyOption

WithClockSkew sets how far past the verifier's clock a signed head's timestamp may be (DefaultClockSkew by default). A negative skew makes every timestamp check fail.

func WithVerifyTime added in v0.8.0

func WithVerifyTime(now time.Time) EvidenceVerifyOption

WithVerifyTime sets the clock EvidencePackage.Verify checks timestamps against (time.Now by default), so a package can be checked as of a given moment.

type Grant

type Grant struct {
	ID      string            `json:"id"`              // unique identifier for this grant
	Issuer  string            `json:"issuer"`          // principal that authorized the grant (resolves to the signing key)
	Subject string            `json:"subject"`         // actor the grant authorizes to act
	Scope   map[string]string `json:"scope,omitempty"` // domain-defined constraints (e.g. limits); a child keeps every one, equal or narrower
	// NotAfterUnix is the expiry in Unix seconds; zero never expires, and only a grant whose parent
	// never expires may be zero.
	NotAfterUnix int64  `json:"not_after_unix,omitempty"`
	ParentRef    string `json:"parent_ref,omitempty"` // Digest of the grant this was attenuated from; empty for a root grant
}

Grant is a signed statement by a principal (Issuer) authorizing an actor (Subject) to act within Scope until NotAfterUnix. It is the non-repudiable authority behind a governed action: the action's agent.Identity.AuthorityRef is a grant's Digest, and the grant is signed by the issuer's own key (distinct from the log's tree-head key), so an auditor attributes the authorization to the principal, not merely to the operator that runs the log.

ParentRef links a grant to the one it was attenuated from (empty for a root grant), forming a hash-linked delegation chain: a holder of a grant may mint a strictly narrower sub-grant for a sub-agent without returning to the root issuer (capability attenuation), and the chain back to the root is provable. Scope entries are constraints whose meaning is domain-defined; the chain verifier requires a child to keep every one of its parent's, unchanged or narrowed by a ScopeRule the verifier supplies (NumericAtMost is provided for limits). See CheckAttenuation.

func (Grant) Bytes

func (g Grant) Bytes() []byte

Bytes is the canonical serialization that is signed and digested: "bide.audit.grant.v2\n" followed by the grant's JSON. json.Marshal sorts map keys and emits struct fields in declaration order, so it is deterministic in-ecosystem. It is injective only over valid UTF-8 (JSON rewrites invalid bytes to U+FFFD), so SignGrant refuses, and SignedGrant.Verify rejects, a grant with invalid UTF-8 in any string.

func (Grant) Digest

func (g Grant) Digest() string

Digest is a stable SHA-256 over the grant's canonical bytes (Bytes), used as agent.Identity.AuthorityRef and as the anchoring key. It is recomputable by any verifier from the disclosed grant.

func (Grant) Expired

func (g Grant) Expired(nowUnix int64) bool

Expired reports whether the grant is past NotAfterUnix at nowUnix (Unix seconds). A zero NotAfterUnix never expires. The caller supplies the clock, so verification stays deterministic.

type HybridSigner

type HybridSigner struct {
	Ed Ed25519Signer // the Ed25519 component signer
	ML MLDSASigner   // the ML-DSA-65 component signer
}

HybridSigner signs with both schemes; the signature is only accepted if BOTH verify, so the anchor is safe as long as either scheme remains unbroken. Each component signs its own label followed by the message ("bide.hybrid.ed25519.v1\x00" || m and "bide.hybrid.mldsa65.v1\x00" || m), so a component stripped out of a hybrid signature is not a signature over m under its scheme.

func (HybridSigner) Alg

func (HybridSigner) Alg() Alg

Alg reports the signature scheme (AlgHybrid).

func (HybridSigner) Format added in v0.7.0

func (s HybridSigner) Format(f fmt.State, _ rune)

Format writes String for every verb, so no fmt verb prints either key.

func (HybridSigner) PublicKey added in v0.9.0

func (s HybridSigner) PublicKey() []byte

PublicKey returns the hybrid public key: a 4-byte big-endian length of the Ed25519 key, the Ed25519 key, then the ML-DSA-65 key. It is nil if either component has no usable key.

func (HybridSigner) Sign

func (s HybridSigner) Sign(m []byte) ([]byte, error)

Sign returns the packed pair of domain-separated Ed25519 and ML-DSA-65 signatures over m.

func (HybridSigner) String added in v0.7.0

func (HybridSigner) String() string

String returns a description of the signer that does not include either key.

type HybridVerifier

type HybridVerifier struct {
	Ed Ed25519Verifier // the Ed25519 component verifier
	ML MLDSAVerifier   // the ML-DSA-65 component verifier
}

HybridVerifier requires both component signatures to verify, each over its own label and the message.

func (HybridVerifier) Alg

func (HybridVerifier) Alg() Alg

Alg reports the signature scheme (AlgHybrid).

func (HybridVerifier) KeyIDs added in v0.9.0

func (v HybridVerifier) KeyIDs() []string

KeyIDs identifies both component keys, for agent.ApproverVerifier: the Ed25519 entry, then the ML-DSA-65 entry. They are listed separately, not as one identity of the pair, because a hybrid signature is meant to hold while either scheme holds: if one scheme breaks, the other component's key alone signs, so two approvers sharing either component are one seat. If either component reports no entry the hybrid reports none, since it can verify nothing.

func (HybridVerifier) PublicKey added in v0.9.0

func (v HybridVerifier) PublicKey() []byte

PublicKey returns the hybrid public key (see HybridSigner.PublicKey), or nil if either component has no usable key.

func (HybridVerifier) Verify

func (v HybridVerifier) Verify(m, sig []byte) bool

Verify reports whether both component signatures over m are valid.

type IDKind added in v0.18.0

type IDKind string

IDKind names the kind of identifier that is an approver's seat. The seat is the identifier, so what it is matters more than its syntax: it must be immutable, never reassigned, one per natural person (docs/design/approver-identity.md, "How an approver id binds to a certificate").

const (
	// IDPersonURI is an opaque person id in a URI subject alternative name that has a host
	// ("https://idp.bank.example/person/8f3a..."), a stable directory key and not a name. It is the
	// recommended kind.
	IDPersonURI IDKind = "person-uri"
	// IDEmail is an rfc822Name. Use it only when the bank states and evidences that its mailboxes are
	// one per person, never aliased and never reassigned after a leaver; otherwise the address is
	// informational and not the seat.
	IDEmail IDKind = "email"
)

The identifier kinds.

type IdentityPolicy added in v0.18.0

type IdentityPolicy struct {
	// Anchors are the CA certificates this gate trusts: not the system roots. Each must be a CA with
	// an allowed key (see the algorithm profile in the design note).
	Anchors []*x509.Certificate
	// Inter pins the intermediates a chain may pass through, by key. A certificate carried in a
	// decision that is not pinned is ignored. A policy with no pins accepts leaves issued directly by
	// an anchor.
	Inter []SPKIHash
	// Distrust ends trust in an anchor or pinned intermediate (by key) for decisions provable at or
	// after the given time, without voiding earlier history. It is how a compromised CA is retired.
	Distrust map[SPKIHash]time.Time
	// Identity maps EVERY approver id of the gate, bare-key approvers included, to the identifier of
	// that approver's person. Two ids that canonicalize to one identifier are refused: one person is
	// one seat.
	Identity map[string]string
	// IDKind is the kind of the one subject alternative name a leaf must carry.
	IDKind IDKind
	// EKU is the extended key usage a leaf must carry. It is checked by hand and also along the
	// chain: every CA certificate that carries extended key usages must permit it.
	EKU asn1.ObjectIdentifier
	// AllowEmailProtectionEKU accepts id-kp-emailProtection in place of EKU. Set it only when the
	// anchors are a dedicated issuing CA for approvers: every S/MIME certificate carries that usage,
	// often with different key handling.
	AllowEmailProtectionEKU bool
	// Policy is the certificate policy OID the leaf must assert. The bank's CA asserts it only after
	// single sign-on with multi-factor step-up and a key generated or held in hardware; bide cannot
	// see either, it can require that the CA says so.
	Policy x509.OID
	// Canonical maps identifiers to their one canonical form. Nil selects NewCanonicalizer with no
	// configuration, which suits IDEmail only: an IDPersonURI policy must supply one (it needs the
	// host).
	Canonical Canonicalizer
	// MaxChain bounds the path from the leaf to the anchor, and the number of certificates a decision
	// may carry. Zero means 4; at most 8.
	MaxChain int
	// Skew is how far a decision's signed time may lie after the time it became provable
	// (decided_at <= A + Skew). Zero means 2 minutes.
	Skew time.Duration
	// AnchorGrace is how long after the leaf's notAfter a decision may first become provable
	// (A <= notAfter + AnchorGrace). Zero means 60 seconds.
	AnchorGrace time.Duration
	// RSA2048Until permits RSA-2048 CA keys while the attested time (here A(d), the time the decision
	// became provable) is strictly before it; many bank roots are still RSA-2048. Zero refuses them,
	// and anything under 2048 bits is always refused: the default is RSA-3072 or better. The field has
	// the same meaning and name in the RFC 3161 verifier's policy.
	RSA2048Until time.Time
	// MaxLeafLifetime bounds notAfter minus notBefore. Zero means 24 hours; at most 31 days.
	MaxLeafLifetime time.Duration
	// NotBeforeBackdate is how much earlier than the real issue a CA stamps notBefore (Microsoft AD CS
	// and EJBCA both back-date it by about ten minutes to absorb clock skew), added to MaxLeafLifetime
	// so that a certificate the CA issued for exactly that lifetime is not refused. It widens only the
	// lifetime check: it moves no validity bound, and the certificate is valid from its notBefore as
	// stamped. Zero declares no back-dating and is never assumed; at most one hour.
	NotBeforeBackdate      time.Duration
	RequireNameConstraints bool
	// RequirePolicyOnCAs refuses a path in which an issuing CA carries no certificatePolicies
	// extension (an anchor that does not issue the leaf directly is not required to). Without it a
	// CA with no extension is unconstrained, so the bank's policy OID is only the leaf's assertion.
	// Recommended: put certificatePolicies on the issuing CA, naming the approver policy, and set
	// this. It is off by default only so that a policy can be adopted before the CA is reissued.
	RequirePolicyOnCAs bool
}

IdentityPolicy is the certificate rules of one gate. It is configuration, change-controlled by the bank: whoever can edit it can enroll. Compile checks it and fixes it into a CertIdentity.

It is the certificate policy of certificate-bound approvers (docs/design/approver-identity.md): an approver is wired in by an audit.CertApprover built over the compiled policy and returned by the gate's resolver (docs/guides/certificate-approvers.md).

The zero value of Skew, AnchorGrace and MaxChain selects the default.

func (IdentityPolicy) Compile added in v0.18.0

func (p IdentityPolicy) Compile() (*CertIdentity, error)

Compile validates the policy and fixes a copy of it. Every failure is an error wrapping agent.ErrConfig: a gate with a bad certificate policy must not start, not fall back to bare keys.

type IgnoredDecision added in v0.4.0

type IgnoredDecision struct {
	Approver string `json:"approver"`
	Step     string `json:"step"`
	Reason   string `json:"reason"`
}

IgnoredDecision is one disclosed approver decision that did not count toward the gate.

type Inclusion

type Inclusion struct {
	Index int      `json:"index"` // the record's position in the journal
	Size  int      `json:"size"`  // the journal length the root committed to
	Path  [][]byte `json:"path"`  // sibling hashes, leaf-to-root
}

Inclusion is an RFC 6962 audit path proving one record's membership in a committed run.

func Prove

func Prove(ctx context.Context, store *agent.Journal, runID string, index int) (Inclusion, error)

Prove returns an inclusion proof for the record at index in runID's journal: enough to verify that record's stored bytes (agent.Record.Raw) against Root WITHOUT revealing any other record (selective disclosure). A redacted record cannot be proven.

type JournalExport added in v0.9.0

type JournalExport struct {
	Format  string   `json:"format"`  // JournalExportFormat
	RunID   string   `json:"run_id"`  // the run the journal is of
	Records [][]byte `json:"records"` // each record's stored bytes, in persisted order
}

A JournalExport is a run's journal as the bytes the journal stores for each record, in persisted order: the input a prover that holds no store (bide audit prove and prove-absent) builds proofs from. Each record is carried verbatim (base64 in JSON), never re-encoded, so a proof built from an export commits to exactly the leaves a proof built from the store does, including the fields of a record this version does not know.

func ExportJournal added in v0.9.0

func ExportJournal(ctx context.Context, store *agent.Journal, runID string) (JournalExport, error)

ExportJournal returns runID's journal as a JournalExport.

func (JournalExport) Journal added in v0.9.0

func (x JournalExport) Journal() ([]agent.Record, error)

Journal decodes the exported records, each keeping its stored bytes as its Raw, so the proof builders commit to the bytes exported. A redaction tombstone decodes as a redacted record. It errors (wrapping ErrFormat or ErrMalformed) if the export is not of JournalExportFormat or a record does not decode.

type KeyFunc

type KeyFunc func(agent.Record) []string

KeyFunc extracts the absence keys a record adds to a key set: none (nil) to exclude the record, or one or more (a model turn that requests several tool calls adds one key per call). Absence is proven over the sorted set of distinct keys the KeyFunc yields over the journal.

type KeySet added in v0.7.0

type KeySet struct {
	Kind   string  // tree kind committed in the signed head, e.g. TreeToolUse
	Prefix string  // every key the set yields (and every key proven absent from it) starts with Prefix
	Key    KeyFunc // projects one journal record onto the set
}

KeySet names one absence key set: the tree Kind its signed heads commit to, the Prefix every key it yields starts with, and the KeyFunc that projects a journal onto it. Kind must start with "absence/" and be distinct per key set; Prefix must be distinct per key set.

func KeySetForKey added in v0.7.0

func KeySetForKey(key string) (KeySet, bool)

KeySetForKey returns the built-in key set a key belongs to, by its prefix.

type LiftAuthorization added in v0.21.0

type LiftAuthorization struct {
	Format string `json:"format"`
	Kind   string `json:"kind"`
	// Deployment is the deployment whose trail the hold is in.
	Deployment string `json:"deployment"`
	// HoldID, HoldMatter, HoldRuns and HoldTree describe the hold as the authorizer saw it.
	HoldID     string   `json:"hold_id"`
	HoldMatter string   `json:"hold_matter"`
	HoldRuns   []string `json:"hold_runs"`
	HoldTree   bool     `json:"hold_tree,omitempty"`
	// PlacedPosition and PlacedLeaf are the trail position and the hex leaf hash of the hold_placed
	// record the authorizer saw in force; TrailSize is how many records the trail held then.
	PlacedPosition int    `json:"placed_position"`
	PlacedLeaf     string `json:"placed_leaf"`
	TrailSize      int    `json:"trail_size"`
	Reason         string `json:"reason"`
	Ticket         string `json:"ticket"`
	// LifterKeyID, when set, is the KeyID of the trail key that must sign the lift.
	LifterKeyID    string `json:"lifter_key_id,omitempty"`
	AuthorizedAtMs int64  `json:"authorized_at_ms"`
	NotAfterMs     int64  `json:"not_after_ms"`
	// Alg, PublicKey and KeyID identify the authorizer's key; Signature signs every other member.
	Alg       string `json:"alg"`
	PublicKey []byte `json:"public_key"`
	KeyID     string `json:"key_id"`
	Signature []byte `json:"signature"`
}

A LiftAuthorization is a second person's signed consent to lift one placement of one legal hold. The authorizer signs it with a key of their own, which is not the key that signs the redaction trail (the lifter's), and the lift's trail record embeds it, so a verifier checks it offline without the file. It is bound to the hold as it stood: the placement record's position and leaf hash, so a file cannot be reused against a re-placed hold with the same ID, against another deployment, or after NotAfterMs.

func NewLiftAuthorization added in v0.21.0

func NewLiftAuthorization(ctx context.Context, trail EventStore, trailKey Verifier, authorizer Signer, deployment, holdID, reason, ticket, lifterKeyID string, now time.Time, validFor time.Duration) (LiftAuthorization, error)

NewLiftAuthorization reads the redaction trail (its records verified under trailKey, the key that signs the trail) and returns authorizer's signed consent to lift the hold holdID as the trail shows it now. The authorization is valid until now + validFor. lifterKeyID, when not empty, names the trail key that must sign the lift. It is refused for a hold the trail does not show in force, for the trail's own key (the authorizer must be a second key), and for a validity that is not positive.

type LiftOption added in v0.21.0

type LiftOption func(*liftConfig)

LiftOption is an option of LiftLegalHold.

func RequireLiftAuthorization added in v0.21.0

func RequireLiftAuthorization() LiftOption

RequireLiftAuthorization makes LiftLegalHold refuse a lift that has no valid authorization (ErrLiftUnauthorized).

func WithLiftAuthorization added in v0.21.0

func WithLiftAuthorization(a LiftAuthorization) LiftOption

WithLiftAuthorization gives LiftLegalHold the second person's authorization to embed in the lift's record. It is checked first, against the placement in force, and a lift it does not cover is refused.

func WithLiftAuthorizers added in v0.21.0

func WithLiftAuthorizers(v ...Verifier) LiftOption

WithLiftAuthorizers pins the keys an authorization must be signed by. With none, any key but the lifter's is accepted, and the verifier (RedactionVerifyInput.LiftAuthorizers) is where the keys are pinned.

type MLDSASigner

type MLDSASigner struct {
	Priv *mldsa.PrivateKey // the ML-DSA-65 private key that signs
}

MLDSASigner signs with an ML-DSA-65 private key. Signing is deterministic, so replaying the same tree head reproduces the same signature.

func (MLDSASigner) Alg

func (MLDSASigner) Alg() Alg

Alg reports the signature scheme (AlgMLDSA65).

func (MLDSASigner) Format added in v0.7.0

func (s MLDSASigner) Format(f fmt.State, _ rune)

Format writes String for every verb, so no fmt verb prints the key.

func (MLDSASigner) MarshalJSON added in v0.7.0

func (MLDSASigner) MarshalJSON() ([]byte, error)

MarshalJSON refuses: the signer holds a private key.

func (MLDSASigner) PublicKey added in v0.9.0

func (s MLDSASigner) PublicKey() []byte

PublicKey returns the encoded ML-DSA-65 public key, or nil if there is no key or it is of another ML-DSA parameter set.

func (MLDSASigner) Sign

func (s MLDSASigner) Sign(m []byte) ([]byte, error)

Sign returns the deterministic ML-DSA-65 signature over m, or an error if there is no key or it is of another parameter set.

func (MLDSASigner) String added in v0.7.0

func (MLDSASigner) String() string

String returns a description of the signer that does not include the key.

type MLDSAVerifier

type MLDSAVerifier struct {
	Pub *mldsa.PublicKey // the ML-DSA-65 public key that verifies
}

MLDSAVerifier verifies with an ML-DSA-65 public key.

func (MLDSAVerifier) Alg

func (MLDSAVerifier) Alg() Alg

Alg reports the signature scheme (AlgMLDSA65).

func (MLDSAVerifier) KeyIDs added in v0.9.0

func (v MLDSAVerifier) KeyIDs() []string

KeyIDs identifies the public key, for agent.ApproverVerifier: one entry, KeyID(AlgMLDSA65, Pub.Bytes()). A verifier with no key, or a key of another ML-DSA parameter set, verifies nothing (see Verify) and reports no entry, so an approval gate refuses it rather than seating it.

func (MLDSAVerifier) PublicKey added in v0.9.0

func (v MLDSAVerifier) PublicKey() []byte

PublicKey returns the encoded public key, or nil if there is none or it is not ML-DSA-65.

func (MLDSAVerifier) Verify

func (v MLDSAVerifier) Verify(m, sig []byte) bool

Verify reports whether sig is a valid ML-DSA-65 signature over m. A verifier with no key, or a key of another parameter set, verifies nothing.

type MemAnchorLog

type MemAnchorLog struct {
	// contains filtered or unexported fields
}

MemAnchorLog is a reference external transparency log: an append-only, independently Merkle-committed record of published STHs. In a real deployment the anchor lives in a DIFFERENT trust domain than the journal (a separate service / account / public log); this in-memory version is for tests and local dev. Because it keeps its OWN RFC 6962 tree over the entries, a third party can verify that the anchor log itself only grew (ProveConsistency) and that a specific STH was anchored (Prove) — the properties that make anchoring meaningful.

func NewMemAnchorLog

func NewMemAnchorLog() *MemAnchorLog

NewMemAnchorLog returns an empty transparency log.

func (*MemAnchorLog) Entries

func (l *MemAnchorLog) Entries() []AnchorEntry

Entries returns a copy of the anchored entries in order.

func (*MemAnchorLog) Len

func (l *MemAnchorLog) Len() int

Len is the number of anchored entries.

func (*MemAnchorLog) Prove

func (l *MemAnchorLog) Prove(seq int) (Inclusion, error)

Prove returns an inclusion proof that the entry at seq is in the anchor log committed by Root — i.e. that a given STH was anchored, disclosing no other entry.

func (*MemAnchorLog) ProveConsistency

func (l *MemAnchorLog) ProveConsistency(first int) (Consistency, error)

ProveConsistency proves the first `first` anchored entries are an append-only prefix of the current log — the transparency-log guarantee that no earlier anchor was rewritten.

func (*MemAnchorLog) Publish

func (l *MemAnchorLog) Publish(_ context.Context, runID string, sth SignedTreeHead) error

Publish appends an STH for runID at the next sequence. Append-only: entries are never modified or removed. It refuses a head that commits to a different run than runID, so an entry's RunID is always the run its signed head names.

func (*MemAnchorLog) Root

func (l *MemAnchorLog) Root() ([]byte, error)

Root is the RFC 6962 Merkle root over all anchored entries — the anchor log's own commitment. Publish/sign IT in turn (or expose it) so a monitor can check the log's growth.

type MemEventStore

type MemEventStore struct {
	// contains filtered or unexported fields
}

MemEventStore is the in-memory reference EventStore for tests and local dev. A real backend (Postgres, object storage) enforces the same append-only/idempotent contract with a unique constraint and insert-only permissions.

func NewMemEventStore

func NewMemEventStore() *MemEventStore

NewMemEventStore returns an empty in-memory EventStore.

func (*MemEventStore) Append

func (s *MemEventStore) Append(_ context.Context, runID string, seq int, leaf []byte) error

Append stores leaf at position seq of runID's trail. Re-appending identical bytes at a stored position is a no-op; different bytes there fail with ErrSeqTaken, and a seq past the end of the trail (or a negative one) is another error.

func (*MemEventStore) Load

func (s *MemEventStore) Load(_ context.Context, runID string) ([][]byte, error)

Load returns copies of runID's stored leaves in order, or nil for a run with none.

type Neighbor

type Neighbor struct {
	Key   string    `json:"key"`   // the committed key adjacent to the absent key
	Proof Inclusion `json:"proof"` // inclusion proof placing Key at its index in the committed key set
}

Neighbor is one committed key adjacent to an absent key, with its inclusion proof.

type NotifyEventVerdict added in v0.17.0

type NotifyEventVerdict struct {
	Trail  string       `json:"trail"`
	Key    string       `json:"key"`
	Kind   string       `json:"kind"`
	RunID  string       `json:"run_id,omitempty"`
	Status NotifyStatus `json:"status"`
	// Why says, in a sentence, what makes the status a problem or what the status rests on.
	Why string `json:"why,omitempty"`
	// InJournal is true when a journal supplied shows the event.
	InJournal bool `json:"in_journal,omitempty"`

	EventAt     time.Time `json:"event_at,omitzero"`
	ObservedAt  time.Time `json:"observed_at,omitzero"`
	DeadlineAt  time.Time `json:"deadline_at,omitzero"`
	FirstSentAt time.Time `json:"first_sent_at,omitzero"`
	AckedAt     time.Time `json:"acked_at,omitzero"`
	ClearedAt   time.Time `json:"cleared_at,omitzero"`
	Attempts    int       `json:"attempts,omitempty"`
	Rounds      int       `json:"rounds,omitempty"`
	// Detection is ObservedAt - EventAt: how long the event was unseen. Delivery is AckedAt -
	// ObservedAt. Total is AckedAt - EventAt.
	Detection time.Duration `json:"detection_ns,omitempty"`
	Delivery  time.Duration `json:"delivery_ns,omitempty"`
	Total     time.Duration `json:"total_ns,omitempty"`
	// Auth is how the acknowledged delivery was authenticated.
	Auth *agent.NotifyAuth `json:"auth,omitempty"`
	// Receiver is the receiver the event was to ("default" when the trail names none): the verdicts
	// of one receiver are the answer to whether every event reached it. Level is the step of an
	// escalation chain for a NotificationEscalated event, 0 otherwise.
	Receiver string `json:"receiver,omitempty"`
	Level    int    `json:"level,omitempty"`
}

NotifyEventVerdict is the verdict on one event.

type NotifyReport added in v0.17.0

type NotifyReport struct {
	Trails   []NotifyTrailReport  `json:"trails"`
	Events   []NotifyEventVerdict `json:"events"`
	Problems []string             `json:"problems,omitempty"`
}

NotifyReport is the result of VerifyNotifications.

func VerifyNotifications added in v0.17.0

func VerifyNotifications(in NotifyVerifyInput) (NotifyReport, error)

VerifyNotifications reads notification trails strictly and says, for every event that needed a person, whether it was notified and acknowledged, and when. It checks:

  • every leaf is a notification record of a kind and shape this version reads (else the error wraps ErrMalformed and no verdict is given);
  • each trail is a valid history: an event is observed once and before anything else about it, nothing follows its acknowledgement or clearing, an acknowledgement follows a claim and carries a time, an attempt count and a 2xx status, an overdue record follows the deadline and states the latency the times imply, an overdue notice follows the overdue record it reports, an all-clear follows the clearing or acknowledgement it reports, records are dated in order within MaxClockSkew, and the recovery failure episodes open and close in turn;
  • with a signed head, the head verifies, is a head of this trail, and commits to its first HeadSize leaves, so a record deleted, reordered or changed before the head is found;
  • against the journals, every approval gate, unknown-outcome halt and aborted run they show is observed in the trail of its root run (or is within Grace of beginning); a recovery failure episode open longer than RecoveryFailingAfter, an overdue record, and a clearing after an attempted delivery each owe a notification that the trail must hold.

The error is nil only if the trails are well formed and anchored (or Unanchored), and no event has a status that is a Problem. The report is returned with every error but a malformed leaf, so a failing audit can say what failed. An error wraps ErrNotVerified, ErrMalformed or agent.ErrConfig (for input that cannot be used together: two exports of one trail, heads and no Verifier).

What it does not establish: that anyone read a notification (an acknowledgement is the sink's 2xx); the times are those of the notifying processes' clocks, bounded above only by the head's timestamp; an event that left no trace in the supplied journals (a halt a later result resolved) and no record in the trail cannot be found; and records after the newest head are read but not attested.

func (NotifyReport) Counts added in v0.17.0

func (r NotifyReport) Counts() map[NotifyStatus]int

Counts returns how many events have each status.

func (NotifyReport) CountsByReceiver added in v0.17.0

func (r NotifyReport) CountsByReceiver() map[string]map[NotifyStatus]int

CountsByReceiver returns, for each receiver, how many of its events have each status.

type NotifyStatus added in v0.17.0

type NotifyStatus string

NotifyStatus is what a trail and the run journals together say about one event that needed a person. NotifyStatus.Problem says which statuses fail an audit.

const (
	// NotifyStatusAcked: the sink acknowledged the event within its deadline (and within the bound).
	NotifyStatusAcked NotifyStatus = "acked"
	// NotifyStatusAckedLate: acknowledged, but after the event's deadline or the bound. A problem.
	NotifyStatusAckedLate NotifyStatus = "acked_late"
	// NotifyStatusPending: observed, not yet acknowledged, and not yet past its deadline at Now.
	NotifyStatusPending NotifyStatus = "pending"
	// NotifyStatusFailing: like pending, and delivery has failed repeatedly (a failing record).
	NotifyStatusFailing NotifyStatus = "failing"
	// NotifyStatusOverdue: observed, not acknowledged, and past its deadline or the bound at Now.
	// A problem.
	NotifyStatusOverdue NotifyStatus = "overdue"
	// NotifyStatusClearedBeforeNotification: the event stopped being current before any delivery
	// was attempted, so no one was owed a notification.
	NotifyStatusClearedBeforeNotification NotifyStatus = "cleared_before_notification"
	// NotifyStatusClearedUnacknowledged: delivery was attempted, never acknowledged, and the event
	// then stopped being current. Whether the receiver was told is unknown, which is why the
	// all-clear is owed (see NotifyStatusNeverNotified for a missing one).
	NotifyStatusClearedUnacknowledged NotifyStatus = "cleared_unacknowledged"
	// NotifyStatusNeverNotified: the journal (or the trail itself) shows an event that needed a
	// person and the trail holds no observation of it. A problem.
	NotifyStatusNeverNotified NotifyStatus = "never_notified"
	// NotifyStatusNotYetDue: like never_notified, but the event is younger than the grace period at
	// Now, so a notifier has not yet been late with it.
	NotifyStatusNotYetDue NotifyStatus = "not_yet_due"
	// NotifyStatusUnobservedResolved: the journal shows a gate that was decided, and the trail
	// holds no observation of it. The person who decided it knew of it, so this is not a failure,
	// and it is reported so a gate that was never notified is not invisible.
	NotifyStatusUnobservedResolved NotifyStatus = "unobserved_resolved"
	// NotifyStatusAllClearUntimed: an acknowledged event was resolved (the journal shows its gate
	// decided, or its halt resolved) and the trail holds no all-clear, but the resolving record
	// holds no recorded_at (one an earlier version wrote), so the all-clear cannot be judged late.
	// A warning, like unobserved_resolved.
	NotifyStatusAllClearUntimed NotifyStatus = "all_clear_untimed"
	// NotifyStatusUnattested: the event's observation, acknowledgement or clearing is in the records
	// after the newest signed head, which nothing attests: whoever can append to the stored trail
	// could have written it. A problem unless every tail record it rests on is within NotifyVerifyInput.MaxTailAge.
	NotifyStatusUnattested NotifyStatus = "unattested"
	// NotifyStatusNoDelivery: the event was delivered to some receiver and not to an expected one
	// (NotifyVerifyInput.ExpectedReceivers). A problem.
	NotifyStatusNoDelivery NotifyStatus = "no_delivery"
)

func (NotifyStatus) Problem added in v0.17.0

func (s NotifyStatus) Problem() bool

Problem reports whether a status fails an audit.

func (NotifyStatus) Warning added in v0.17.0

func (s NotifyStatus) Warning() bool

Warning reports whether a status is shown as a warning: it does not fail an audit unless NotifyVerifyInput.Strict is set.

type NotifyTrail added in v0.16.0

type NotifyTrail struct {
	// contains filtered or unexported fields
}

NotifyTrail adapts an EventStore to agent.NotifyTrail: the notification trail of a run is an event trail of its own (named agent.NotifyTrailName), each record a salted, canonical event leaf committed like any other, so EventLog, TreeHead, Prove and ProveConsistency work on it and bide audit can verify it. It is deliberately a different trail from the one PersistJournal mirrors for the run, whose positions that projection owns.

Append is insert-if-absent at a position, on the store's contract: a different leaf at a stored position is an error. The adapter turns that error into agent.ErrTrailConflict, and recognises the caller's own earlier write (an Append whose reply was lost) so a retry is a no-op. It needs a durable EventStore with UNIQUE(run_id, seq) semantics to give two processes mutual exclusion.

func NewNotifyTrail added in v0.16.0

func NewNotifyTrail(store EventStore) *NotifyTrail

NewNotifyTrail returns a NotifyTrail over store.

func (*NotifyTrail) AnchorTrails added in v0.17.0

func (t *NotifyTrail) AnchorTrails(ctx context.Context, trails []string) error

AnchorTrails implements agent.NotifyTrailAnchorer: it asks the AuditedEventStore beneath the trail's store (found through Unwrap, under any decorator) to publish a head for each named trail that has grown past its last one, and for some that an earlier failure left unanchored. A trail over a store that anchors nothing has nothing to publish, and returns nil.

func (*NotifyTrail) Append added in v0.16.0

func (t *NotifyTrail) Append(ctx context.Context, trail string, seq int, rec agent.NotifyRecord) error

Append implements agent.NotifyTrail.

func (*NotifyTrail) Load added in v0.16.0

func (t *NotifyTrail) Load(ctx context.Context, trail string) ([]agent.NotifyRecord, error)

Load implements agent.NotifyTrail. It refuses a leaf that is not a notify record (a trail that is not a notification trail, or a record of a kind this version does not know is not skipped).

type NotifyTrailExport added in v0.17.0

type NotifyTrailExport struct {
	Format string   `json:"format"` // NotifyTrailExportFormat
	Trail  string   `json:"trail"`  // the trail's name (agent.NotifyTrailName)
	Leaves [][]byte `json:"leaves"` // each leaf, in position order
}

A NotifyTrailExport is a notification trail as the leaves its EventStore holds, in position order: the input VerifyNotifications and bide audit notifications read when they hold no store. Each leaf is carried verbatim (base64 in JSON), never re-encoded, so a signed head over the trail (SignNotifyTrail) commits to exactly the bytes exported.

func ExportNotifyTrail added in v0.17.0

func ExportNotifyTrail(ctx context.Context, store EventStore, trail string) (NotifyTrailExport, error)

ExportNotifyTrail returns the trail named trail in store as a NotifyTrailExport. A trail the store does not hold exports with no leaves.

type NotifyTrailReport added in v0.17.0

type NotifyTrailReport struct {
	Trail   string `json:"trail"`
	Records int    `json:"records"`
	// Anchored is true when a signed head over the trail verified, and the trail's first HeadSize
	// records are exactly the leaves it commits to.
	Anchored bool `json:"anchored"`
	HeadSize int  `json:"head_size,omitempty"`
	// Heads counts the signed heads supplied for the trail, each of which verified and commits to a
	// prefix of it; HeadSize and HeadTime are the newest's.
	Heads int `json:"heads,omitempty"`
	// Tail counts the records after the newest head: read, but not attested by it. TailSince is
	// the time of the first of them and TailAge is how long before Now that was: how long the oldest
	// unattested record has gone without a signed head over it.
	Tail      int           `json:"tail,omitempty"`
	TailSince time.Time     `json:"tail_since,omitzero"`
	TailAge   time.Duration `json:"tail_age_ns,omitempty"`
	HeadTime  time.Time     `json:"head_time,omitzero"`
	// Violations are the ways the trail is malformed, out of order, or does not match its head.
	Violations []string `json:"violations,omitempty"`
}

NotifyTrailReport is what was established about one trail.

type NotifyVerifyInput added in v0.17.0

type NotifyVerifyInput struct {
	// Trails are the notification trails to verify. A trail the journals say is owed events and
	// that is not here is read as a trail with no records.
	Trails []NotifyTrailExport
	// Heads are signed tree heads over the trails (SignNotifyTrail, or the ones an
	// AuditedEventStore publishes as the trail is written), matched to a trail by RunID, which is the
	// trail's name. A trail may have several, as an anchor log holds one for every size: each is
	// verified under Verifier and must commit to a prefix of the trail, and the largest is the
	// newest. They are verified under Verifier.
	Heads []SignedTreeHead
	// Verifier verifies Heads. It is required if there are any, or unless Unanchored.
	Verifier Verifier
	// Unanchored allows a trail with no signed head. The trail is then read but nothing attests that
	// it is complete or unaltered, and the report says so (NotifyTrailReport.Anchored is false).
	Unanchored bool
	// Journals are run journals the events are cross-checked against: every gate, halt and abort
	// they show is expected in the trail of its root run.
	Journals []JournalExport
	// Stops is the state of the kill switch's control run, as the deployment's own mode judges it: the events
	// it owes a notifier (agent.StopNotifyEvents, at Now, with the window StopsWithin, 0 the notifier's default)
	// are expected in the trail of the control run, and a stop that is lifted owes its all-clear and its lift.
	// Which lifts are owed depends on which stops the trail shows were told of, so it is judged here, not by
	// the caller. StopAnomalies are findings about the control run itself (agent.StopClockAnomalies): each is
	// a problem.
	Stops         *agent.StopState
	StopsWithin   time.Duration
	StopAnomalies []string

	// Routing, if set, is the notifier's routing (agent.RoutingOf, the same options the notifier was
	// built with): each event that needs a person must have been delivered to the receivers it
	// routes to (NotifyRouting.Receivers, the method the notifier routes by), and each escalation
	// step that was due to its chain's receiver. A missing delivery is NotifyStatusNoDelivery. An
	// approval routed by its approvers needs its journal to be supplied, to know them; without it
	// the approval is not checked. Without Routing, per-receiver completeness is not checked: the
	// trail alone cannot know the routing.
	Routing *agent.NotifyRouting

	// Strict makes a warning status (NotifyStatus.Warning) a problem, so an audit fails on it.
	Strict bool

	// Now is the time pending and overdue events are judged at; time.Now if zero.
	Now time.Time
	// MaxAck, if positive, is the longest an event may wait, from the time the run entered the state
	// (the observation time if the record has none) to its acknowledgement; an event past it is late
	// or overdue even within its recorded deadline. The recorded deadline always applies.
	MaxAck time.Duration
	// Grace is how long after an event began a missing observation is not yet a failure. It is the
	// notifier's detection window: the default, 5 minutes, matches agent.WithNotifyHaltAge and the
	// default deadline.
	Grace time.Duration
	// RecoveryFailingAfter is how long a recovery failure episode may be open before a
	// recovery_failing event is owed. The default, 15 minutes, is the notifier's.
	RecoveryFailingAfter time.Duration
	// MaxTailAge is the longest a record may sit after the newest signed head of its trail,
	// unattested. A trail whose oldest unattested record is older than this at Now is a violation
	// (the check that a notifier's automatic anchoring is keeping up; zero does not apply it). An
	// event whose observation, acknowledgement or clearing is only in that tail is
	// NotifyStatusUnattested, a problem, unless MaxTailAge is positive and every tail record of the
	// event is no older than it. Zero, the default, is strict. A record accepted by the bound is
	// accepted without a signature: its content and its time (written by whoever appended it) are
	// trusted.
	MaxTailAge time.Duration
	// MaxClockSkew is how far a record may be dated before the one that precedes it (the writers'
	// clocks differ) before the trail is out of order. The default is DefaultClockSkew.
	MaxClockSkew time.Duration
}

NotifyVerifyInput is what VerifyNotifications reads.

type PolicyContent

type PolicyContent struct {
	Digest string `json:"digest"` // the policy owner's stable identifier (treated as opaque here)
	Policy string `json:"policy"` // the serialized governed policy bytes
}

PolicyContent is the payload of a policy leaf: the serialized governed policy and its digest. Committing it as a journal leaf puts the policy itself under the same signed tree head and inclusion proofs as the actions taken under it, so an auditor can prove not merely that an action happened but that the exact policy admitting it was anchored in the same committed tree.

type PolicyConvergence

type PolicyConvergence struct {
	Digest      string      `json:"digest"`      // the used policy digest this evidence is for
	PolicyLeaf  ProofBundle `json:"policy_leaf"` // proof the policy leaf is anchored in the run STH
	Certificate ProofBundle `json:"certificate"` // proof the convergence-certificate leaf is anchored in the same tree
}

PolicyConvergence bundles the anchored evidence for one used policy: its anchored policy leaf and its anchored convergence-certificate leaf, both proven against the run STH. The verifier confirms both bundles are authentic and in the same tree, the convergence certificate certifies the policy leaf's digest, and the policy leaf's bytes hash to that digest (done in the CLI, independent of gsm), then optionally runs the oracle on the disclosed policy bytes.

type ProofBundle

type ProofBundle struct {
	Format      string         `json:"format"`       // ProofFormat
	RunID       string         `json:"run_id"`       // the run whose journal the record and STH belong to; must equal STH.RunID
	RecordBytes []byte         `json:"record_bytes"` // the single disclosed action: the bytes the journal stores for it
	Inclusion   Inclusion      `json:"inclusion"`    // its RFC 6962 audit path
	STH         SignedTreeHead `json:"sth"`          // the signed commitment it is proven against
}

A ProofBundle is a single, portable, self-describing artifact that proves ONE action happened inside a signed, committed run. It discloses the record (with its salt) as the bytes the journal stores for it, its index, the run's size and signed head, and the sibling hashes on its path, and no other record: each record's leaf commits to its own random salt, so the sibling hashes cannot be tested against a guessed neighbour (see merkle.go). It is what "produce a proof" returns: hand it (plus the signer's public key, obtained out-of-band) to an auditor and they verify it offline.

The record is carried as RecordBytes, the stored bytes verbatim, and the verifier hashes exactly those bytes: it never re-encodes a decoded record. So a record a later release wrote, with fields this release does not know, verifies here, and what a proof commits to cannot depend on how this release encodes a record. Record decodes the bytes, leniently (unknown fields are ignored), for display and for the checks that read what the record is (its kind, its name).

The trust model, stated precisely: the public key must come from OUT OF BAND (the anchor / transparency log), NOT from the bundle. Given a trusted key, Verify checks (1) the STH's signature, so the {Kind, RunID, Size, Root, TimestampNanos} commitment is authentic; (2) that the STH commits to a journal and to the bundle's RunID, so the run a verifier reports is the run the signer committed to; (3) that the inclusion proof is against that same signed size; and (4) that the record bytes are the leaf at their index under the signed root. "Proofs you verify, not logs you trust": nothing here asks the verifier to trust the producer, its database, or its logs. For full tamper-evidence the auditor also confirms the STH itself appears in the anchor log (MemAnchorLog.Prove).

func ProveApproval added in v0.4.0

func ProveApproval(ctx context.Context, store *agent.Journal, runID, step string, sth SignedTreeHead) (ProofBundle, error)

ProveApproval builds a ProofBundle proving one approver-decision record, named step, is committed in the tree sth signs. Decision records are named per decision (see agent.ApprovalTally.Records and agent.DecisionCheck.Step), so the step name identifies one exactly. The disclosed record carries the approver id and signature, so a verifier holding the approver's key can check it over agent.ApprovalDecisionBytes independently. ProveStep matches only StepValue records, so it cannot reach a decision.

func ProveConvergence

func ProveConvergence(ctx context.Context, store *agent.Journal, runID, digest string, sth SignedTreeHead) (ProofBundle, error)

ProveConvergence builds a ProofBundle proving the convergence certificate for this digest was committed in the tree sth signs. Pair it with the policy leaf's ProofBundle for the same digest to show, in one committed tree, that the anchored policy was certified convergent.

func ProveGrant

func ProveGrant(ctx context.Context, store *agent.Journal, runID, digest string, sth SignedTreeHead) (ProofBundle, error)

ProveGrant builds a ProofBundle proving the grant with this digest was committed in the tree sth signs. Pair it with an action's bundle whose Identity.AuthorityRef matches the digest to show the action ran under an in-log, issuer-signed authority.

func ProvePolicy

func ProvePolicy(ctx context.Context, store *agent.Journal, runID, digest string, sth SignedTreeHead) (ProofBundle, error)

ProvePolicy builds a ProofBundle proving the policy with this digest was committed in the tree sth signs. Pair it with an action's ProofBundle whose result embeds the same digest to show the action ran under an in-log, anchored policy; the verifier then recomputes the digest from the disclosed bytes and runs the external oracle to certify the policy converges.

func ProveRecord

func ProveRecord(ctx context.Context, store *agent.Journal, runID string, index int, sth SignedTreeHead) (ProofBundle, error)

ProveRecord builds a bundle proving the record at index is in the tree that sth commits to. The inclusion proof is built against the first sth.Size records of runID's journal (the tree the STH signed), so the bundle binds to an anchored STH rather than a freshly minted one. It fails if index is outside that tree, if the record is redacted, or if sth.Root does not match runID's journal at that size (wrong STH, or the history diverged from what was signed).

func ProveRunCertificate

func ProveRunCertificate(ctx context.Context, store *agent.Journal, runID string, sth SignedTreeHead) (ProofBundle, error)

ProveRunCertificate builds a ProofBundle proving the run certificate leaf for runID was committed in the tree sth signs. Because RecordRunCertificate grows the journal, sth here is a tree head taken AFTER the certificate was anchored (later than the run STH embedded in the certificate).

func ProveStep

func ProveStep(ctx context.Context, store *agent.Journal, runID, name string, sth SignedTreeHead) (ProofBundle, error)

ProveStep builds a ProofBundle proving that the durable step with this name (an agent.Journal.Step / Parallel task, recorded as a StepValue) is committed in the tree sth signs. It is the fan-in counterpart to ProveToolCall: after a Parallel run, each task is an independently provable record, so an auditor can prove "this specific compliance check ran and produced this result" without disclosing the other stages.

func ProveToolCall

func ProveToolCall(ctx context.Context, store *agent.Journal, runID, toolUseID string, sth SignedTreeHead) (ProofBundle, error)

ProveToolCall is the semantic form of ProveRecord: prove that a specific tool call happened, by its ToolUseID, without the caller needing to know its journal index. It resolves the completed tool-result record for toolUseID and proves it against sth. This is the ergonomic entry point ("prove the charge on run X"), not "prove record index 7".

func (ProofBundle) Record

func (b ProofBundle) Record() (agent.Record, error)

Record decodes the proven record from RecordBytes, for display and role checks. The decoding is lenient, as the journal's own: a field this version does not know is ignored. It errors (wrapping ErrMalformed) only if the bytes do not decode as a journal record.

func (ProofBundle) Verify

func (b ProofBundle) Verify(v Verifier) error

Verify returns nil if the bundle is internally consistent and authentic under v (a key obtained out-of-band, e.g. from the anchor log operator): the STH verifies under v, it is a journal head of RunID, the inclusion proof is for the signed size, and RecordBytes are the leaf at their index under the signed root. It then requires RecordBytes to decode as a record (ErrMalformed if not). A bundle that does not hold is an error wrapping ErrNotVerified; one of another format, ErrFormat. It does not check the head's timestamp; apply CheckTimestamp to b.STH for that.

type ReceiptFinding added in v0.17.0

type ReceiptFinding struct {
	Verdict ReceiptVerdict
	Detail  string
}

ReceiptFinding is the verdict on one receipt, with the reason.

func CheckReceipt added in v0.17.0

func CheckReceipt(r DecisionReceipt, journal []agent.Record, heads []SignedTreeHead, logKey Verifier) ReceiptFinding

CheckReceipt decides whether the signed tree heads of the run commit to the receipt's decision. journal is the run's journal (JournalExport.Journal) and heads are heads of it signed by the log key logKey (a head that does not verify, or is of another run or tree, is ignored).

"Should contain" is positional, and never by time: the clocks of the approver's machine and the application differ, and heads reach an anchor out of order. A journal only grows, and the decision is at the receipt's Index. So a head whose size is above Index commits to a journal prefix that includes position Index, and that prefix must hold the decision there. CheckReceipt rebuilds the prefix's leaves and root from journal and compares the root with the head's, so what it says about a head it says about a head that matches this journal; the decision is held when the leaf at Index is the decision's leaf hash under a valid Anchor, or the record there is the decision (isRecord) without one; a redaction tombstone holds when it keeps the receipt's claimed leaf hash. Only a receipt with a valid Anchor is evidence against a forged tombstone; without one, a tombstone that keeps the claimed hash is the approver's claim read back. The check is as fresh as the journal and heads supplied: a later write or head is not seen. A head at or below Index says nothing of the decision. recorded_at is display only.

What a mismatch means depends on the receipt. Its Record, Index and LeafHash are the approver's claim, and without an Anchor nothing shows the application ever journaled the decision, so a mismatch is ReceiptUnmatched. With a valid Anchor (the decision under a signed head A) a head B that matches the journal and commits to something else at Index contradicts A: the log's signer signed both, and that is ReceiptDeleted. B need not be later than A in time or arrival: B is checked against the journal, not against A.

type ReceiptIdentity added in v0.19.0

type ReceiptIdentity struct {
	Approver string
	// Identifier is the verified canonical identifier of the leaf (the identity the policy maps the
	// approver id to).
	Identifier string
	// DisplayName is the leaf's subject common name, if it has one. It is the CA's label for a
	// person, not part of the identity: it is never compared or counted.
	DisplayName string
	// LeafHash and AnchorKey are the SHA-256 of the leaf's DER and of the trust anchor's
	// SubjectPublicKeyInfo, in hex; Chain is the hash of each certificate of the accepted path, leaf first.
	LeafHash, AnchorKey string
	Chain               []string
	NotBefore, NotAfter time.Time
	// DecidedAt is the approver's signed time; AnchoredAt is A, the time the decision became provable,
	// from TimeSource.
	DecidedAt, AnchoredAt time.Time
	TimeSource            string
	// Revocation is "not checked": VerifyIdentity does not look at revocation status, so the result is
	// never a verdict that the decision counts.
	Revocation string
}

ReceiptIdentity is what VerifyIdentity establishes about a certificate-bound receipt.

type ReceiptVerdict added in v0.17.0

type ReceiptVerdict string

ReceiptVerdict is what CheckReceipt finds about one receipt.

const (
	// ReceiptAnchored: a signed tree head of the journal commits to the decision at its position.
	ReceiptAnchored ReceiptVerdict = "anchored"
	// ReceiptRedacted: as anchored, and a redaction has since replaced the record's bytes with a
	// tombstone that keeps its leaf hash. A legitimate redaction, not a deletion.
	ReceiptRedacted ReceiptVerdict = "redacted (anchored)"
	// ReceiptPending: the journal holds the decision and no signed head covers its position yet. The
	// application signs it at its next write to the run, or when agent.Recover or RecoverLoop next
	// finds the run waiting; until then this is expected (bide audit verify-decision-coverage flags a
	// decision that stays unsigned past a bound).
	ReceiptPending ReceiptVerdict = "pending"
	// ReceiptDeleted: the receipt carries an anchoring proof (the decision's record under a signed
	// head A), and a signed head B that matches the journal given reaches the position and commits
	// to something else there. The log's signer signed A, with the decision, and B, without it. Only
	// a receipt with a valid Anchor gets this verdict.
	ReceiptDeleted ReceiptVerdict = "decision deleted after signing"
	// ReceiptUnmatched: the journal does not hold the decision at the receipt's position (or a signed
	// head that matches the journal commits to something else there), and the receipt carries no
	// anchoring proof. That is the approver's claim: it proves deletion only if the receipt is
	// trusted, and a receipt for a decision never submitted looks the same.
	ReceiptUnmatched ReceiptVerdict = "receipt not matched by the journal (the approver's claim)"
	// ReceiptUnchecked: a head reaches the position but does not match the journal given, or the
	// receipt's anchoring proof does not verify, so nothing can be said of the decision from it. A
	// journal that does not match a signed head is itself a failure, which the journal's own
	// verification reports.
	ReceiptUnchecked ReceiptVerdict = "unchecked"
)

The verdicts. Each says only what the inputs prove. Without an Anchor in the receipt, nothing but the approver's word says the application ever journaled the decision, so a receipt the journal does not match is ReceiptUnmatched, an accusation of no one. ReceiptDeleted needs the proof.

func (ReceiptVerdict) Failed added in v0.17.0

func (v ReceiptVerdict) Failed() bool

Failed reports whether the verdict is a failure: the journal does not show a decision that the receipt says is there (ReceiptDeleted, proven; ReceiptUnmatched, claimed), or nothing could be checked (ReceiptUnchecked).

type RedactedRef added in v0.21.0

type RedactedRef struct {
	Position int    `json:"position"`
	Name     string `json:"name"`
	Kind     string `json:"kind,omitempty"`
	Length   int    `json:"length,omitempty"`
	LeafHash string `json:"leaf_hash"`
	// Already is set when the record held a tombstone when the redaction was planned.
	Already bool `json:"already,omitempty"`
}

A RedactedRef names one record a redaction replaces: its position in the run's journal, its name, kind and stored length, and the hex leaf hash its tombstone keeps. It holds no content.

type RedactionFinding added in v0.21.0

type RedactionFinding struct {
	Kind   RedactionFindingKind `json:"kind"`
	RunID  string               `json:"run_id,omitempty"`
	Detail string               `json:"detail"`
}

A RedactionFinding is one problem VerifyRedactions found.

type RedactionFindingKind added in v0.21.0

type RedactionFindingKind string

RedactionFindingKind names what VerifyRedactions found wrong.

const (
	// FindingUnanchored: the trail has no signed head, so nothing shows it is complete.
	FindingUnanchored RedactionFindingKind = "UNANCHORED"
	// FindingTrailAltered: a head contradicts the trail (records deleted or changed).
	FindingTrailAltered RedactionFindingKind = "TRAIL_ALTERED"
	// FindingUnattestedTail: records after the newest signed head.
	FindingUnattestedTail RedactionFindingKind = "UNATTESTED_TAIL"
	// FindingIncomplete: a begin with no end: an unfinished pass.
	FindingIncomplete RedactionFindingKind = "INCOMPLETE"
	// FindingUnaccounted: a tombstone no trail record names (a redaction made by hand).
	FindingUnaccounted RedactionFindingKind = "UNACCOUNTED"
	// FindingNotErased: a record the trail lists as erased still holds content.
	FindingNotErased RedactionFindingKind = "NOT_ERASED"
	// FindingNameMismatch: a tombstone's name is not the name the trail recorded at that position.
	FindingNameMismatch RedactionFindingKind = "NAME_MISMATCH"
	// FindingHashMismatch: a tombstone's leaf hash is not the one the trail recorded.
	FindingHashMismatch RedactionFindingKind = "HASH_MISMATCH"
	// FindingUncovered: a redaction that ran with no covering signed head.
	FindingUncovered RedactionFindingKind = "UNCOVERED"
	// FindingUnderHold: a pass erased records of a run a legal hold in force covered.
	FindingUnderHold RedactionFindingKind = "REDACTED_UNDER_HOLD"
	// FindingHoldUnmatched: a hold_lifted record for a hold the trail shows no active placement of, or a placement of an ID already active.
	FindingHoldUnmatched RedactionFindingKind = "HOLD_UNMATCHED"
	// FindingLiftUnauthorized: a hold lift that has no second-person authorization where one is required, or
	// whose embedded authorization does not check out (a bad signature, another hold or placement, expired
	// at the lift, the lifter's own key, or a key that is not a pinned authorizer).
	FindingLiftUnauthorized RedactionFindingKind = "LIFT_UNAUTHORIZED"
	// FindingHoldNotInStore: the trail says a hold is active and the store holds none: lifted by hand.
	FindingHoldNotInStore RedactionFindingKind = "HOLD_NOT_IN_STORE"
	// FindingHoldNotInTrail: the store holds a hold the trail never recorded: placed by hand.
	FindingHoldNotInTrail RedactionFindingKind = "HOLD_NOT_IN_TRAIL"
	// FindingHoldDiffers: the store's hold and the trail's have the same ID and differ in runs, tree setting or matter.
	FindingHoldDiffers RedactionFindingKind = "HOLD_DIFFERS"
	// FindingReordered: a trail record that is not at the position, or after the record, it was signed for: a replayed, copied or reordered record.
	FindingReordered RedactionFindingKind = "REORDERED"
	// FindingBadSignature: a trail record with no signature, or one that does not verify under the key: not the tool's, whatever head covers it.
	FindingBadSignature RedactionFindingKind = "BAD_SIGNATURE"
	// FindingEventCopies: an erasure that left the run's events in the event store.
	FindingEventCopies RedactionFindingKind = "EVENT_COPIES_KEPT"
	// FindingTimeMismatch: a tombstone stamped at a time no pass of the trail that listed it carries.
	FindingTimeMismatch RedactionFindingKind = "TIME_MISMATCH"
	// FindingUnchecked: the trail names a run no journal was given for.
	FindingUnchecked RedactionFindingKind = "UNCHECKED"
	// FindingMalformed: a trail record that does not read.
	FindingMalformed RedactionFindingKind = "MALFORMED"
)

The findings of VerifyRedactions.

type RedactionOptions added in v0.21.0

type RedactionOptions struct {
	// Reason and Ticket say why, and under what authority: both are required and are recorded.
	Reason, Ticket string
	// Signer signs every record the redaction writes (required): the redaction key, which
	// VerifyRedactions verifies under the same log key that signs the trail's heads.
	Signer Signer
	// EventCopiesKept is the number of events of the run the event store holds that the erasure
	// leaves; it is recorded and VerifyRedactions reports it.
	EventCopiesKept int
	// Deployment names the deployment the trail belongs to (required); it is signed into every record,
	// and VerifyRedactions checks it, so two deployments that share a key cannot copy records between
	// their trails.
	Deployment string
	// Executor says who is erasing (a key ID, a user name); required.
	Executor string
	// Now is the time stamped into the tombstones and the trail; the zero value is an error.
	Now time.Time
	// Version is the tool's version, recorded.
	Version string
	// Heads are signed journal heads of the plan's run, and Verifier the log key that verifies them:
	// the redaction needs one that covers the whole journal as it stands (its size is at least the
	// journal's), so the evidence of the run is anchored before its content is erased. Without
	// such a head the redaction is refused, unless AllowUncovered is set, which is recorded.
	Heads    []SignedTreeHead
	Verifier Verifier
	// Redactor, when set, replaces the records instead of the journal's own store (see agent.RedactUsing).
	Redactor agent.Redactor
	// TrailHeads are the signed heads of the redaction trail so far (the operator's anchor, or the
	// -head-out file): RedactJournal checks the trail against them before it appends, so a trail
	// that is not the one they attest (a typo in the trail's database starts a new one) is refused.
	TrailHeads []SignedTreeHead
	// AllowUncovered lets a redaction go ahead with no covering head. It is recorded in the trail.
	AllowUncovered bool
}

RedactionOptions are what an accountable redaction needs besides the plan.

type RedactionRecord added in v0.21.0

type RedactionRecord struct {
	Format      string        `json:"format"`
	Phase       string        `json:"phase"`
	PlanDigest  string        `json:"plan_digest"`
	RunID       string        `json:"run_id"`
	Records     []RedactedRef `json:"records,omitempty"` // begin
	JournalSize int           `json:"journal_size,omitempty"`
	Reason      string        `json:"reason"`
	Ticket      string        `json:"ticket"`
	// Executor is who ran it, as the tool reports: the key ID of the trail's signer and the
	// operating-system user, informational.
	Executor string `json:"executor"`
	AtMs     int64  `json:"at_ms"`
	Version  string `json:"version,omitempty"`
	// HeadSize is the size of the signed journal head that covered the run when it was erased, 0
	// when the redaction ran without one (Uncovered).
	HeadSize  int  `json:"head_size,omitempty"`
	Uncovered bool `json:"uncovered,omitempty"`
	// End only: how many records this pass replaced, and how many already held a tombstone, and the
	// error that stopped it, if any.
	Redacted int    `json:"redacted,omitempty"`
	Already  int    `json:"already,omitempty"`
	Error    string `json:"error,omitempty"`
	// EventCopiesKept is the number of events of the run the event store holds, which the erasure
	// left (the operator chose to erase the journal anyway); 0 when it held none or was not asked.
	EventCopiesKept int `json:"event_copies_kept,omitempty"`
	// Position and Prev bind the record to its place in the trail: its index, and the leaf hash of
	// the record before it (empty for the first). They are signed, so a signed record copied to
	// another position, or after other records, or into another trail, no longer verifies.
	Position int    `json:"position"`
	Prev     []byte `json:"prev,omitempty"`
	// Deployment names the deployment the trail belongs to (RedactionOptions.Deployment), signed, so a
	// record cannot be moved into another deployment's trail that holds the same key.
	Deployment string `json:"deployment,omitempty"`
	// AckPosition and AckLeafHash, on an acknowledge record only, name the planted leaf the operator
	// acknowledges: its position in the trail and the hex leaf hash of its bytes.
	AckPosition int    `json:"ack_position,omitempty"`
	AckLeafHash string `json:"ack_leaf_hash,omitempty"`
	// HoldID, HoldMatter, HoldRuns and HoldTree describe the legal hold of a hold_placed or hold_lifted
	// record: the hold's ID, its matter, the runs it covers and whether it covers their sub-agent runs.
	HoldID     string   `json:"hold_id,omitempty"`
	HoldMatter string   `json:"hold_matter,omitempty"`
	HoldRuns   []string `json:"hold_runs,omitempty"`
	HoldTree   bool     `json:"hold_tree,omitempty"`
	// Authorization, on a hold_lifted record only, is the second person's signed consent to the lift
	// (LiftAuthorization), embedded so a verifier checks it without the original file.
	Authorization *LiftAuthorization `json:"authorization,omitempty"`
	// Alg and Signature sign the record's other members under the redaction key: a record without a
	// signature that verifies is not the tool's, whatever signed head covers it.
	Alg       string `json:"alg"`
	Signature []byte `json:"signature"`
}

A RedactionRecord is one leaf of the redaction trail. A redaction writes a begin record, then redacts, then an end record; a begin with no end is an unfinished pass, finished by running the same plan again.

func DecodeRedactionLeaf added in v0.21.0

func DecodeRedactionLeaf(leaf []byte) (RedactionRecord, error)

DecodeRedactionLeaf reads a redaction record from a stored event leaf. The leaf's kind must be the record's phase, the format must be this one, and an unknown member is an error: a record of another version is not skipped.

func LoadRedactionTrail added in v0.21.0

func LoadRedactionTrail(ctx context.Context, store EventStore) ([]RedactionRecord, error)

LoadRedactionTrail returns the records of the redaction trail in store, in order.

type RedactionReport added in v0.21.0

type RedactionReport struct {
	Records    int                `json:"records"`
	Redactions int                `json:"redactions"` // begin records
	Anchored   bool               `json:"anchored"`
	HeadSize   int                `json:"head_size,omitempty"`
	Findings   []RedactionFinding `json:"findings,omitempty"`
	// Holds are the legal holds the trail shows active at its end, by ID.
	Holds []agent.Hold `json:"holds,omitempty"`
	// Notes are things that are not findings: an earlier failed pass that a later pass finished.
	Notes []string `json:"notes,omitempty"`
	// Ended are the runs whose end marker was erased and whose tombstone the signed trail accounts
	// for (name, leaf hash and time all match): how each ended, as the trail recorded it. The journal
	// itself says only that the run is over (agent.RunEnded), because a tombstone's name is outside
	// the leaf hash; the signed trail is the one source of the kind.
	Ended []ErasedEnd `json:"ended,omitempty"`
}

A RedactionReport is the result of VerifyRedactions.

func VerifyRedactions added in v0.21.0

func VerifyRedactions(in RedactionVerifyInput) (RedactionReport, error)

VerifyRedactions checks the redaction trail against its signed heads and the journals. A tombstone's name is outside the journal's leaf hashes, so it is attested only here: it is checked against the name the signed trail recorded at that position, so names are attested exactly when the trail verifies. It returns ErrNotVerified wrapping the first finding when any is found.

func (RedactionReport) OK added in v0.21.0

func (r RedactionReport) OK() bool

OK reports whether nothing was found.

type RedactionVerifyInput added in v0.21.0

type RedactionVerifyInput struct {
	// Trail is the redaction trail's leaves, in order (an ExportNotifyTrail export of RedactionTrailName).
	Trail [][]byte
	// Heads are signed heads over the trail (TreeEvents, RunID RedactionTrailName) and Verifier their key.
	Heads    []SignedTreeHead
	Verifier Verifier
	// Unanchored accepts a trail with no head, reported but not failed.
	Unanchored bool
	// Deployment is the deployment every record must be signed for (RedactionOptions.Deployment); required.
	Deployment string
	// StoreHolds, when not nil, are the legal holds the store holds (agent.Holder.Holds): they are
	// compared with the holds the trail shows active, and a hold in one and not the other is a finding.
	StoreHolds *[]agent.Hold
	// LiftAuthorizers pins the keys a hold lift's embedded authorization must be signed by; with none,
	// an authorization is checked for everything but who the authorizer is.
	LiftAuthorizers []Verifier
	// RequireLiftAuthorization says a hold lift needs a valid second-person authorization: one with
	// none is LIFT_UNAUTHORIZED. It needs LiftAuthorizers (without keys pinned, any key would do).
	RequireLiftAuthorization bool
	// Journals are the journals to check against, by run ID: the records as read (a store's History,
	// or JournalExport.Journal). A run the trail names that has no journal here is not checked.
	Journals map[string][]agent.Record
}

RedactionVerifyInput is what VerifyRedactions checks.

type RejectedHead added in v0.17.0

type RejectedHead struct {
	Size   int    `json:"size"`
	Reason string `json:"reason"`
}

RejectedHead is a head CheckDecisionCoverage ignored, and why: it does not verify under the log key, or it is not a journal head of the run. It says nothing about the journal.

type RunCertSpec

type RunCertSpec struct {
	// ApprovedPolicies is the allowlist of policy digests the run was permitted to exercise. Every
	// policy the run actually used must be a member, or CertifyRun fails: a certificate cannot be
	// issued for a run that exercised a disallowed policy. It is not written into the certificate:
	// the verifier supplies its own allowlist.
	ApprovedPolicies []string
	// Signer signs the used-policy head. It must hold the key that signed the run's journal head
	// (the log's tree-head key), so a verifier checks both heads under one out-of-band key;
	// CertifyRun refuses another. Evidence sets it to its own signer.
	Signer Signer
	// TimestampNanos stamps the used-policy head, in Unix nanoseconds. It must not be earlier than
	// the journal head's. Evidence sets it to its own timestamp.
	TimestampNanos int64
}

RunCertSpec carries the caller's inputs to CertifyRun: what the run was allowed, and how to sign the certificate's used-policy head. It is a struct so future properties (authority roots, quorum thresholds) can be added without changing the signature.

type RunCertificate

type RunCertificate struct {
	// Format is RunCertificateFormat.
	Format string `json:"format"`

	// RunID is the run this certificate is about. It must equal STH.RunID and
	// UsedPolicyAbsence.RunID, both signed.
	RunID string `json:"run_id"`

	// Properties names the asserted properties, so a reader sees the scope without decoding the
	// bindings. v1: exactly "only-approved-policies", "policies-convergence-certified".
	Properties []string `json:"properties"`

	// UsedPolicies is the sorted, distinct set of policy digests exercised by governed actions in the
	// run, recomputed from the committed leaves (PoliciesUsed). It is disclosed in full because the
	// only-approved-policies property is completeness-bearing: the verifier recomputes the key-set
	// root over this set and confirms UsedPolicyAbsence commits to it, so the set cannot omit a
	// policy that was in fact used.
	UsedPolicies []string `json:"used_policies"`

	// UsedPolicyAbsence is the signed commitment to the run's used-policy key set: a PolicyUsedKeys
	// tree head (NewAbsenceTreeHead) whose Journal is STH's tree. Its Size is the number of distinct
	// used policies and its Root is the RFC 6962 root over their sorted keys.
	UsedPolicyAbsence SignedTreeHead `json:"used_policy_absence"`

	// Convergence carries, per used policy digest and in UsedPolicies order, the anchored evidence
	// that the policy is certified convergent: the policy-leaf ProofBundle and the convergence-leaf
	// ProofBundle, both bound to the run STH below. The oracle cross-check (does the policy actually
	// converge) is performed by the verifier with a checker, as verify-convergence does; these
	// bundles establish the cryptographic root (anchored, digest-linked, same tree) that the oracle
	// check hangs off.
	Convergence []PolicyConvergence `json:"convergence"`

	// STH is the run's signed journal head: the commitment the policy and convergence bundles are
	// proven against and the journal tree the used-policy set was projected from.
	STH SignedTreeHead `json:"sth"`
}

A RunCertificate is a proof-carrying run: one portable, offline-checkable artifact asserting behavioral-property compliance over a WHOLE run, discharged from the audit package's existing primitives rather than from new cryptography. It composes what already exists (RFC 6962 inclusion proofs, signed tree heads, the policy-used absence commitment, anchored policy and convergence leaves) into one per-run statement plus a verifier, so a compliance reviewer checks a single bundle against one out-of-band public key instead of assembling the proofs by hand.

v1 asserts these properties, each dischargeable from a committed leaf:

  • only-approved-policies: every policy digest exercised by a governed action in the run is a member of the AUDITOR's approved allowlist (passed to VerifyRun; the certificate carries no allowlist of its own). This is the COMPLETENESS-bearing property: it commits WHICH policies were used, via the policy-used absence commitment (a PolicyUsedKeys tree head). The used set is exactly the sorted distinct keys that tree commits to, so a verifier recomputes its root from the disclosed set and confirms the signed head commits to it; that closes the "some other policy was quietly used" gap that disclosing only positive leaves would leave open.
  • policies-convergence-certified: for each used policy digest, an anchored convergence certificate leaf exists in the same signed tree as the policy leaf and links to its digest. The certificate is a producer claim; a rigorous verifier cross-checks it against the external oracle (bide audit verify-run -checker), exactly as verify-convergence does, so a certificate that overstates convergence is caught outside this package.

Binding. The used-policy head is bound to this run and to this journal, not merely signed by the right key: its signed encoding commits to Kind TreePolicyUsed, to RunID, and to the journal tree (Size and Root) whose records the used set was projected from. VerifyRun requires that journal tree to be exactly STH (the certificate's signed journal head, itself a journal head of RunID). So a used-policy head from another run, from a different point in this run, or from a tool-use key set cannot stand in, and the used set is the projection of the same records the convergence bundles are proven against.

What the certificate proves, stated precisely: it proves properties of the GOVERNED, COMMITTED boundary of the run up to STH.Size records: which policies ran, that each is on the approved allowlist, and that each has an anchored convergence certificate that (with the oracle) is confirmed convergent. Composing the two properties yields "every governed state in the run was produced by an approved, oracle-certified-convergent policy," so the enforced invariant held throughout the governed boundary. It does NOT prove the model's judgment was correct, that ungoverned side effects were appropriate, or the runtime-refinement claim (the replay differential check covers the events this run took, not all inputs). It covers the journal up to STH.Size; that STH is the run's final head is a fact the anchor log supplies, not the certificate.

func CertifyRun

func CertifyRun(ctx context.Context, store *agent.Journal, runID string, sth SignedTreeHead, spec RunCertSpec) (RunCertificate, error)

CertifyRun assembles a RunCertificate for runID from its committed leaves, against the run's signed journal head sth. It confirms sth is a journal head of runID that matches the journal, recomputes the used-policy set from exactly the sth.Size records sth commits to (PoliciesUsed), confirms every used policy is in spec.ApprovedPolicies (refusing to certify a run that used a disallowed policy), signs the used-policy key-set head bound to sth's tree with spec.Signer at spec.TimestampNanos, and for each used policy assembles the anchored policy-leaf and convergence-leaf proof bundles against sth. It fails if a used policy has no anchored policy leaf or no anchored convergence leaf, so the certificate can only be issued for a run whose governed policies are fully anchored and certified.

spec.Signer must hold the key that signed sth (the log's tree-head key): the used-policy head is a second commitment over the SAME run, signed the same way, so a verifier checks both under one out-of-band key. A signer of another key or scheme is refused, and so is a used-policy timestamp earlier than sth's.

type RunVerification

type RunVerification struct {
	// OK is the overall verdict: true only when every asserted property held.
	OK bool `json:"ok"`
	// OnlyApprovedPolicies is true when every used policy is in the allowlist AND the disclosed
	// used-policy set is bound to this run's journal by the signed used-policy head (completeness).
	OnlyApprovedPolicies bool `json:"only_approved_policies"`
	// ConvergenceCertified is true when every used policy has an anchored policy leaf and convergence
	// leaf, authentic and in the same signed tree, with linking digests. It does NOT include the
	// oracle cross-check, which the caller performs separately (the CLI does, with -checker).
	ConvergenceCertified bool `json:"convergence_certified"`
	// Reasons carries human-readable detail for any property that failed (empty on full success).
	Reasons []string `json:"reasons,omitempty"`
	// Policies carries, when ConvergenceCertified holds, what the verification read from each used
	// policy's leaves, in UsedPolicies order, so a caller that cross-checks a policy (the CLI's
	// -checker runs the external oracle on it) checks exactly the bytes that were verified.
	Policies []VerifiedPolicy `json:"policies,omitempty"`
}

RunVerification is the structured outcome of VerifyRun: the overall verdict plus per-property results, so a caller (or the CLI) can report exactly which property held and which failed.

func VerifyRun

func VerifyRun(cert RunCertificate, approved []string, v Verifier) (RunVerification, error)

VerifyRun checks a RunCertificate's claims against the disclosed proofs, the auditor's own approved allowlist, and the out-of-band verifier v, entirely offline. It does NOT trust the certificate; it re-derives each property from the bundles it carries:

  1. the certificate itself: Properties is exactly the v1 list, and STH is an authentic journal head of RunID.
  2. only-approved-policies (completeness-bearing): the used-policy head is authentic under v, is a TreePolicyUsed head of RunID whose source journal is exactly STH's tree, and its Root equals the RFC 6962 root recomputed from the disclosed UsedPolicies (in the sorted, distinct order the tree commits; any other order, a duplicate, or a missing or extra digest changes the root), so the disclosed set IS what this run's journal committed; and every used policy is a member of approved. A policy that was actually used cannot be dropped from UsedPolicies without changing the root, and a head for another run or journal cannot be swapped in.
  3. policies-convergence-certified: for every used policy there is exactly one Convergence entry, in UsedPolicies order and none for an unused digest, whose policy-leaf and convergence-leaf bundles both verify under v, are in the same tree as the run STH, and whose leaves link by digest. The leaves are read with UnmarshalStrict, so a leaf that reads differently to a person than to encoding/json does not verify. The oracle cross-check of the certificate's convergence claim is left to the caller (see the CLI's -checker), on the contents returned in Policies.

It returns the per-property result with a nil error when OK, and with an error wrapping ErrNotVerified when a property failed (Reasons says which). Any other error means the certificate could not be checked: it, or a bundle it carries, is not of the format this version reads (ErrFormat), or a bundle's record or a leaf could not be read (ErrMalformed). It does not check the heads' timestamps; EvidencePackage.Verify and bide audit verify-run do (see CheckTimestamp).

type SPKIHash added in v0.18.0

type SPKIHash [32]byte

SPKIHash is the SHA-256 of a certificate's SubjectPublicKeyInfo. Intermediates are pinned and distrust is dated by it: a key, not a certificate, so a reissued CA certificate for the same key is the same CA.

func SPKIHashOf added in v0.18.0

func SPKIHashOf(c *x509.Certificate) SPKIHash

SPKIHashOf returns the pin of c's public key.

type ScopeRule added in v0.7.0

type ScopeRule func(parent, child string) bool

ScopeRule reports whether child, a changed value for one scope key, is within parent, the parent grant's value for that key.

type ScopeRules added in v0.7.0

type ScopeRules map[string]ScopeRule

ScopeRules maps a scope key to the rule that decides whether a changed value narrows it. A key with no rule can only be carried to a child unchanged.

type SignedGrant

type SignedGrant struct {
	Grant Grant  `json:"grant"` // the grant whose canonical bytes are signed
	Alg   Alg    `json:"alg"`   // signature scheme the issuer used (see signing.go)
	Sig   []byte `json:"sig"`   // issuer's signature over Grant.Bytes()
}

SignedGrant is a Grant plus the issuer's signature over its canonical bytes.

func SignGrant

func SignGrant(g Grant, s Signer) (SignedGrant, error)

SignGrant signs a grant with the issuer's key (Ed25519, ML-DSA, or hybrid, per the Signer).

func (SignedGrant) Verify

func (sg SignedGrant) Verify(v Verifier) error

Verify returns nil if the issuer's signature over the grant verifies under v. The verifier is the issuer's public key, resolved by the caller's PKI; this is non-repudiation of the authorization, separate from the log's tree-head signature. A grant signed under another scheme than v's, or whose signature does not verify, is an error wrapping ErrNotVerified; a grant with invalid UTF-8 in a string, ErrMalformed.

type SignedTreeHead

type SignedTreeHead struct {
	Format   string `json:"format"` // STHFormat
	TreeHead        // the commitment being signed
	// Alg names the signature scheme (see signing.go). It is signed with the head.
	Alg       Alg    `json:"alg"`
	Signature []byte `json:"signature"` // signature over TreeHead.canonical(Alg) under Alg
}

SignedTreeHead is a TreeHead with a signature over its canonical encoding under the scheme Alg names. The scheme is part of the signed bytes.

func SignAbsenceRoot

func SignAbsenceRoot(records []agent.Record, set KeySet, journal TreeHead, s Signer, timestamp int64) (SignedTreeHead, error)

SignAbsenceRoot commits and signs a run's key set in one call: NewAbsenceTreeHead, then SignTreeHead with s. Absence proofs verify against this separate commitment (never the journal STH). Anchor the result like any STH. timestamp is Unix nanoseconds.

func SignNotifyTrail added in v0.17.0

func SignNotifyTrail(ctx context.Context, store EventStore, trail string, s Signer, now time.Time) (SignedTreeHead, error)

SignNotifyTrail signs a tree head over the leaves store holds for trail, at now. The head's Kind is TreeEvents and its RunID is the trail's name, so it verifies only as a commitment to that trail. Publish it to an Anchor (or any log in another trust domain) as often as the audit needs to bound what could be rewritten: VerifyNotifications reads records after the newest head it is given as unattested. The notifier does not call this; a deployment signs and anchors on its own schedule, as it does for a journal (AuditedStore).

func SignRedactionTrail added in v0.21.0

func SignRedactionTrail(ctx context.Context, store EventStore, signer Signer, v Verifier, deployment string, prior []SignedTreeHead, now time.Time) (SignedTreeHead, error)

SignRedactionTrail signs a tree head over the redaction trail in store, after checking the trail is the one the earlier heads attest: every head in prior must verify under v, be a head of the trail, and commit to a prefix of it, and every record in the trail must carry a signature that verifies under v. A record that is not the tool's (unsigned, signed for another deployment or position, or not a record) does not stop the signing: the head attests that the leaves exist, VerifyRedactions reports them, and they account for nothing. The head is over exactly the leaves it checked, never a fresh load, so a record appended meanwhile is not attested. It returns an error wrapping ErrNotVerified when a prior head does not match the trail.

func SignStopTrail added in v0.21.0

func SignStopTrail(ctx context.Context, store EventStore, trail string, s Signer, now time.Time) (SignedTreeHead, error)

SignStopTrail signs a tree head over the leaves store holds for trail, at now: the head AuditedEventStore publishes as records arrive, for a deployment that signs on its own schedule.

func SignStopTrailExport added in v0.21.0

func SignStopTrailExport(exp StopTrailExport, s Signer, now time.Time) (SignedTreeHead, error)

SignStopTrailExport signs a tree head over exactly the leaves exp carries, at now: the head `bide audit stops -sign-head` appends to a head file after it has verified those leaves. Unlike SignStopTrail it never reads a store, so what is signed is what was verified, even if the trail grew in between. An empty trail is refused: a head over nothing attests nothing.

func SignTreeHead

func SignTreeHead(th TreeHead, s Signer) (SignedTreeHead, error)

SignTreeHead signs th under s's scheme. Anchor the result out-of-band (this is what makes the journal tamper-evident against later rewrites). Build th with NewTreeHead, NewAbsenceTreeHead, or EventLog.TreeHead. It refuses a head of an unknown kind or shape, which no verifier accepts, and a signer without a usable key.

func (SignedTreeHead) Hash added in v0.19.0

func (h SignedTreeHead) Hash() [32]byte

Hash is the SHA-256 of the signed head's canonical encoding followed by its signature, length prefixed: what identifies this head, so two heads with the same size and root but different timestamps or signatures differ.

func (SignedTreeHead) Verify

func (sth SignedTreeHead) Verify(v Verifier) error

Verify returns nil if the STH's signature is valid under v: the head is of format STHFormat, of a known kind and shape, signed under v's scheme, and its signature over its canonical encoding (scheme included) verifies. Any change to any field invalidates it. It checks authenticity only: a caller that needs a particular tree also checks Kind and RunID (the bundle verifiers in this package do), and a caller that relies on when the head was signed applies CheckTimestamp (EvidencePackage.Verify and bide audit do). The error wraps ErrFormat, ErrMalformed, or ErrNotVerified.

type Signer

type Signer interface {
	// Alg names the scheme.
	Alg() Alg
	// PublicKey returns the encoded public key of the signing key (see NewVerifier for the
	// encoding of each scheme), or nil if the signer holds no usable key.
	PublicKey() []byte
	// Sign returns a signature over message, or an error if the key is unusable.
	Sign(message []byte) ([]byte, error)
}

Signer produces signatures under one scheme with one key.

type SnapshotOption added in v0.18.0

type SnapshotOption func(*StatusSnapshot)

SnapshotOption configures NewStatusSnapshot.

func WithIssuers added in v0.18.0

func WithIssuers(certs ...*x509.Certificate) SnapshotOption

WithIssuers names the CA certificates whose CRLs the snapshot is for. A CRL is then authenticated before its entries are built: its signature must verify under one of them or AddCRL refuses it. Without issuers the snapshot accepts any well-formed CRL and authenticates only when a chain is evaluated, which costs memory for CRLs that are later discarded; a refresher that knows its issuer should always set it.

func WithLimits added in v0.18.0

func WithLimits(maxBytes, maxEntries int) SnapshotOption

WithLimits sets the largest CRL accepted in bytes and in entries (defaults 16 MiB and 1,000,000).

func WithMaxCRLs added in v0.18.0

func WithMaxCRLs(n int) SnapshotOption

WithMaxCRLs bounds how many CRLs the snapshot keeps (default 1024; a value below 1 keeps the default). Beyond it the CRL with the earliest thisUpdate is evicted, so a long-running refresher does not grow without limit. A policy that needs a longer series for a decision (the journal's evidence) keeps it in the archive, not here.

func WithUnboundedCRLs added in v0.18.0

func WithUnboundedCRLs() SnapshotOption

WithUnboundedCRLs removes the bound on how many CRLs the snapshot keeps, for an audit tool that loads an archived series and must not lose any of it. A long-running refresher should not use it.

type StatusArchive added in v0.19.0

type StatusArchive interface {
	PutStatus(ctx context.Context, hash string, der []byte) error
}

StatusArchive stores the bytes of a CRL too large for the journal (agent.MaxStatusInline) under its SHA-256 (lowercase hex), durably, before the gate records its hash. Put must be idempotent.

type StatusDecl added in v0.20.0

type StatusDecl struct {
	Params           StatusParams
	ExecStatusAge    time.Duration
	ExecStatusAgeFor map[SPKIHash]time.Duration
	CAOverlapFor     map[SPKIHash]time.Duration
	DeltaPeriodFor   map[SPKIHash]time.Duration
	// ClockTolerance and EffectiveFrom are the gate's CertApprover.ClockTolerance and PolicyEffectiveFrom, when
	// the file states them.
	ClockTolerance time.Duration
	EffectiveFrom  time.Time
	// contains filtered or unexported fields
}

StatusDecl is the declared CA timings read from a file.

func ParseStatusDecl added in v0.20.0

func ParseStatusDecl(name string, b []byte) (StatusDecl, error)

ParseStatusDecl reads the declaration file. A per-issuer exec_status_age makes that CA a declared tier and then needs ca_period, ca_overlap and max_status_age beside it (zero overlap is written "0s", never left out), as the gate's own validation requires.

func (StatusDecl) Approver added in v0.20.0

func (d StatusDecl) Approver(policy *CertIdentity, snap *StatusSnapshot, now func() time.Time) (*CertApprover, error)

Approver builds the CertApprover the refresher reads its declared values from, for a refresher run outside a gate. It names the policy's first approver id and takes the file's effective_from as the date the policy took effect, or now when the file has none (a refresher never reads it, but the policy digest does).

func (StatusDecl) MissingForDigest added in v0.21.0

func (d StatusDecl) MissingForDigest() []string

MissingForDigest returns, sorted, the members of the declaration the policy digest covers and the file does not state. With none missing, Approver builds an approver whose PolicyDigest equals the gate's (when the values are the gate's); with some, the digest still reflects the file but cannot be compared with the gate's, because the missing members take defaults or a stand-in start time.

type StatusError added in v0.18.0

type StatusError struct {
	Reason string
	Detail string
}

StatusError is a status failure. Reason is agent.ReasonStatusUnknown, agent.ReasonStatusStale or agent.ReasonRevoked; Detail says what failed.

func (*StatusError) Error added in v0.18.0

func (e *StatusError) Error() string

Error reports the reason and the detail.

type StatusParams added in v0.18.0

type StatusParams struct {
	// W is the compromise-reporting window the bank commits to (default 24 hours). W' = min(W, L(C)
	// - A(d)) is what a certificate's own life leaves.
	W time.Duration
	// MinW is the smallest W' for a final verdict (default 30 minutes).
	MinW time.Duration
	// RoutineGrace: for routine reasons (affiliationChanged, superseded, cessationOfOperation,
	// privilegeWithdrawn) a decision with A < E + RoutineGrace still counts. Compromise reasons,
	// unspecified and certificateHold never get grace. Default 0.
	RoutineGrace time.Duration
	// Now, when non-zero, applies freshness: a CRL is evidence of absence only while Now < nextUpdate
	// and Now - thisUpdate <= MaxStatusAge, and not when dated in the future. It does not relax the
	// rule that evidence is issued at or after the decision (see CountLive). A freshness check without
	// a positive MaxStatusAge accepts no evidence (a configuration mistake fails closed). Leave Now
	// zero for an offline audit, which judges the series against the decision, not the clock.
	Now          time.Time
	MaxStatusAge time.Duration
	// MaxStatusAgeFor overrides MaxStatusAge for the issuing CA with the given key. An offline root
	// publishes the CRL that covers its issuing CA rarely (Microsoft AD CS: weekly), so the status of
	// that CA certificate rests on a CRL days old; the policy says so for that issuer only, and the leaf's
	// CRLs keep the global limit. Zero means the global limit, never unlimited; a negative value or one over 400 days makes the status unknown.
	MaxStatusAgeFor map[SPKIHash]time.Duration
	// CountLive is the live gate's counting mode and needs Now: a fresh CRL issued before the decision
	// is accepted, so a decision is not held for a CRL period. A verdict resting on status issued
	// before the decision is provisional, never final (its Basis says so): a revocation effective
	// before the decision may be reported after that CRL. A live count that finds a CRL at or after
	// the decision and its window can be final like any other. An audit never sets it.
	CountLive bool
	// Distrust adds distrust dates to the policy's own, by issuer key: status signed by that key with
	// thisUpdate at or after the date is no information. It is the auditor's overlay.
	Distrust map[SPKIHash]time.Time
	// Retained says the bank's CA keeps the entries of expired certificates for as long as audits
	// need them (recorded retention in the policy), as expiredCertsOnCRL does per CRL. It is the
	// bank's recorded statement, not something the CRLs show.
	Retained bool
	// CAPeriod is the CA's declared issuance period (the design note's CAPeriod, the same value that
	// bounds ExecStatusAge): consecutive CRLs of the series are issued no further apart than this.
	// Time coverage, and so the first CRL beyond notAfter (L(C)), is shown by it, not by nextUpdate: a
	// CA may issue earlier than nextUpdate (hourly issue, 24 hour validity), so a producer who omits a
	// CRL cannot be caught by nextUpdate alone. Zero declares nothing: with no CAPeriod and no
	// ConsecutiveNumbers no verdict is final. A series whose observed gap exceeds it carries a Warning.
	CAPeriod time.Duration
	// CAPeriodFor overrides CAPeriod for the issuing CA with the given key.
	CAPeriodFor map[SPKIHash]time.Duration
	// ConsecutiveNumbers is for a CA whose CRL numbers increase by exactly one with every CRL it
	// issues (bases and deltas alike): consecutive numbers then show that no CRL lies between two.
	// Off by default; a CA that skips numbers, shares a counter oddly or resets after a restore would
	// otherwise never reach final.
	ConsecutiveNumbers bool
	// CoverageTolerance absorbs publication jitter in time coverage (default one minute).
	CoverageTolerance time.Duration
	// ThisUpdateSkew is how much earlier than its real issue a CA stamps thisUpdate (Microsoft AD CS back-dates
	// it by about ten minutes). A CRL is then really issued at thisUpdate plus the skew: a hold released
	// before the next CRL is released no later than that, and a CRL's age is measured from it.
	// ThisUpdateSkewFor overrides it per issuing CA. Zero takes thisUpdate at face value; it is never
	// assumed. It is the CA's own configuration (AD CS: ClockSkewMinutes), declared from a capture of
	// that CA, with room for the variation of a few seconds (the release bound adds CoverageTolerance).
	// A negative value or one over 30 minutes is refused: the certificate has no information.
	ThisUpdateSkew    time.Duration
	ThisUpdateSkewFor map[SPKIHash]time.Duration
	// AllowHolds accepts certificates that have been on hold (certificateHold, removeFromCRL). Off by
	// default: a removeFromCRL entry's date is the CA's own (RFC 5280 does not define it as the
	// release time), so a CA that stamps the original hold date makes a release look old and could hide
	// a decision made during the hold when the CRLs that list the hold are missing. Without it any
	// hold or release entry for the certificate refuses it wholesale. Banks should not use holds on
	// approver certificates (revoke and re-issue); opt in only for a CA whose release dates have been
	// checked (the conformance captures record what EJBCA and AD CS write).
	AllowHolds bool
}

StatusParams are the knobs of the status rule. Zero values select the defaults.

func (StatusParams) Validate added in v0.19.0

func (p StatusParams) Validate() error

Validate checks the status parameters when a gate is configured, so a bad value fails at startup and not at the first decision. Zero durations select the documented defaults, so the zero value is valid. Every failure wraps agent.ErrConfig.

type StatusSnapshot added in v0.18.0

type StatusSnapshot struct {
	// contains filtered or unexported fields
}

StatusSnapshot is a set of CRLs: what a gate fetched, or an auditor's archived series. It is safe for concurrent use.

func NewStatusSnapshot added in v0.18.0

func NewStatusSnapshot(opts ...SnapshotOption) *StatusSnapshot

NewStatusSnapshot returns an empty snapshot.

func (*StatusSnapshot) AddCRL added in v0.18.0

func (s *StatusSnapshot) AddCRL(der []byte) (CRLInfo, error)

AddCRL parses a CRL and keeps it, or refuses it with a *StatusError (ReasonStatusUnknown) naming why. Adding the same CRL twice keeps one.

A CRL is refused when bide cannot read it without guessing, because a wrong reading is a wrong "not revoked": a signature algorithm outside the profile (never SHA-1), no CRL number, no nextUpdate (RFC 5280 requires it; absent means unknown), an issuing distribution point that is not exactly a critical full name of URIs (see identity_idp.go; one that is is read, and is evidence of absence only for a certificate that names it), an indirect CRL (a certificateIssuer entry extension), any critical extension bide does not process, removeFromCRL in a full CRL, a malformed invalidity date, and a malformed delta indicator (a delta CRL must carry a critical delta CRL indicator, a DER INTEGER: the number of the base it is cumulative since; it is merged with a base when a chain is evaluated and never gives evidence of absence alone). The signature is not checked here: that needs the issuing certificate, which belongs to a chain (see CertIdentity.EvaluateStatus).

func (*StatusSnapshot) AddCRLNoEvict added in v0.20.0

func (s *StatusSnapshot) AddCRLNoEvict(der []byte) (CRLInfo, error)

AddCRLNoEvict is AddCRL that never drops a CRL to make room: with the snapshot full it returns ErrSnapshotFull. A refresher seeds from its archive with it so that loading old CRLs cannot raise the eviction count and make live status non-final.

func (*StatusSnapshot) Evicted added in v0.18.0

func (s *StatusSnapshot) Evicted() int

Evicted is the number of CRLs the snapshot has dropped to stay within WithMaxCRLs. An evaluation that needs a dropped CRL says so (CertStatus.EvictionGap) instead of reading the absence as silence.

func (*StatusSnapshot) HeldCRLs added in v0.20.0

func (s *StatusSnapshot) HeldCRLs(issuer *x509.Certificate) []*CRL

HeldCRLs returns the CRLs the snapshot holds that verify under issuer, ordered by thisUpdate and then number.

func (*StatusSnapshot) Len added in v0.18.0

func (s *StatusSnapshot) Len() int

Len is the number of distinct CRLs held.

func (*StatusSnapshot) Limits added in v0.20.0

func (s *StatusSnapshot) Limits() (maxBytes, maxEntries, maxCRLs int)

Limits returns the largest CRL the snapshot accepts in bytes and in entries, and how many CRLs it keeps (0: unbounded).

func (*StatusSnapshot) RemoveCRL added in v0.20.0

func (s *StatusSnapshot) RemoveCRL(hash [32]byte) bool

RemoveCRL drops the CRL with the given hash from the snapshot without touching the eviction count (Evicted), and reports whether it was held. A refresher uses it only for an operator's reset of a poisoned CRL number.

type StatusTier added in v0.18.0

type StatusTier int

StatusTier is how strong the evidence of absence from the CRLs is.

const (
	// TierNoInformation: no usable status. A silent CRL that cannot be shown to cover the time
	// after the decision is no information, never "not revoked".
	TierNoInformation StatusTier = iota
	// TierProvisional: status exists but a later one could change the answer (it ends before
	// A+W', the window is short, or the series is not shown complete to L(C)).
	TierProvisional
	// TierFinal: status reaches A+W' over a window of at least MinW, and the series is complete.
	TierFinal
)

The tiers, weakest first.

func (StatusTier) String added in v0.18.0

func (t StatusTier) String() string

String names the tier.

type StopReport added in v0.21.0

type StopReport struct {
	Records       int               `json:"records"`
	Anchored      bool              `json:"anchored"`
	Heads         int               `json:"heads"`
	HeadSize      int               `json:"head_size"`
	HeadTime      time.Time         `json:"head_time"`
	Tail          int               `json:"tail"`
	TailAge       time.Duration     `json:"tail_age,omitempty"`
	Mode          string            `json:"mode"`
	PolicyDigest  string            `json:"policy_digest,omitempty"`
	PolicySeq     int               `json:"policy_seq"`
	Stops         []StopReportEntry `json:"stops"`
	RejectedTotal int               `json:"rejected_total"`
	Rejected      []agent.Rejection `json:"rejected,omitempty"`
	ChainErr      string            `json:"chain_error,omitempty"`
	// StoreOnly are records the control run holds that the trail lacks; TrailOnly records the trail holds
	// that the control run lacks. Both are only filled when the control run was supplied.
	StoreOnly []StopReportRecord `json:"store_only,omitempty"`
	TrailOnly []StopReportRecord `json:"trail_only,omitempty"`
	// Problems are the reasons the trail is not what a deployment's stop trail should be; any makes
	// VerifyStops return ErrNotVerified.
	Problems []string `json:"problems,omitempty"`
	// Unapplied are records of the trail, signed and accepted by the fold, that are not stops and that the
	// control run lacks: what a crash between the trail write and the control-run write leaves. They are not
	// in force and not a forgery, so they are not a problem; agent.StopControl.ApplyPending applies them.
	Unapplied []StopReportRecord `json:"unapplied,omitempty"`
	// Superseded are such records that a record the control run holds follows on the trail: the next write
	// went on without them, so they cannot be applied without reordering. They are not in force either.
	Superseded []StopReportRecord `json:"superseded,omitempty"`
}

StopReport is what VerifyStops found.

func VerifyStops added in v0.21.0

func VerifyStops(in StopVerifyInput) (StopReport, error)

VerifyStops checks a stop trail: that its signed heads verify and commit to prefixes of it (so no record was deleted, changed or reordered since a head covered it), that every record decodes and verifies under the policy the deployment pinned and the chain of policy transitions from its root (a record that does not is a problem: a forgery written into the trail, or a trail from another deployment), that the chain can be trusted, and, with the control run, that the two hold the same records. It returns the stops in force as the trail shows them. A trail that does not hold together returns the report with an error wrapping ErrNotVerified.

type StopReportEntry added in v0.21.0

type StopReportEntry struct {
	ID           string   `json:"id"`
	Scope        string   `json:"scope"`
	Reason       string   `json:"reason,omitempty"`
	By           string   `json:"by"`
	At           string   `json:"at"`
	Signed       bool     `json:"signed"`
	Lifted       bool     `json:"lifted"`
	LiftedBy     []string `json:"lifted_by,omitempty"`
	LiftsHeld    int      `json:"lifts_held,omitempty"`
	LiftsNeeded  int      `json:"lifts_needed,omitempty"`
	Compensation string   `json:"compensation,omitempty"`
}

StopReportEntry is one stop as VerifyStops reports it.

type StopReportRecord added in v0.21.0

type StopReportRecord struct {
	Kind string `json:"kind"`
	By   string `json:"by"`
	At   string `json:"at"`
	ID   string `json:"id,omitempty"`
}

StopReportRecord names a record in a comparison finding.

type StopTrail added in v0.21.0

type StopTrail struct {
	// contains filtered or unexported fields
}

StopTrail adapts an EventStore to agent.StopTrail: the stop trail of a deployment is an event trail of its own (named agent.StopTrailName), each signed stop record a salted, canonical event leaf committed like any other, so EventLog, TreeHead, Prove and ProveConsistency work on it, an AuditedEventStore signs and anchors its heads, and bide audit stops can verify it. Append is insert-if-absent at a position, as NotifyTrail's is: a different leaf at a stored position is agent.ErrTrailConflict, and a caller's own earlier write (a reply that was lost) is recognised, so a retry is a no-op.

func NewStopTrail added in v0.21.0

func NewStopTrail(store EventStore) *StopTrail

NewStopTrail returns a StopTrail over store.

func (*StopTrail) AnchorTrails added in v0.21.0

func (t *StopTrail) AnchorTrails(ctx context.Context, trails []string) error

AnchorTrails asks the AuditedEventStore beneath the trail's store (found through Unwrap) to publish a head for each named trail that has grown past its last one. A trail over a store that anchors nothing has nothing to publish, and returns nil.

func (*StopTrail) Append added in v0.21.0

func (t *StopTrail) Append(ctx context.Context, trail string, seq int, rec agent.StopRecord) error

Append implements agent.StopTrail.

func (*StopTrail) Load added in v0.21.0

func (t *StopTrail) Load(ctx context.Context, trail string) ([]agent.StopRecord, error)

Load implements agent.StopTrail. It refuses a leaf that is not a stop record.

type StopTrailExport added in v0.21.0

type StopTrailExport struct {
	Format string   `json:"format"` // StopTrailExportFormat
	Trail  string   `json:"trail"`  // agent.StopTrailName
	Leaves [][]byte `json:"leaves"`
}

A StopTrailExport is the stop trail as the leaves its EventStore holds, in position order: what VerifyStops and bide audit stops read when they hold no store. Each leaf is carried verbatim, so a signed head over the trail (SignStopTrail) commits to exactly the bytes exported.

func ExportStopTrail added in v0.21.0

func ExportStopTrail(ctx context.Context, store EventStore, trail string) (StopTrailExport, error)

ExportStopTrail returns the trail named trail in store as a StopTrailExport. A trail the store does not hold exports with no leaves.

type StopVerifyInput added in v0.21.0

type StopVerifyInput struct {
	Trail StopTrailExport
	// Heads are signed tree heads over the trail (SignStopTrail, or the heads an AuditedEventStore
	// anchored); Verifier verifies them. Each must verify and commit to a prefix of the trail.
	Heads    []SignedTreeHead
	Verifier Verifier
	// Unanchored accepts a trail with no signed head: it is read, and nothing attests that it is complete
	// or unaltered.
	Unanchored bool
	// Now dates the check; MaxClockSkew bounds how far a head's time may be ahead of it; MaxTailAge, when
	// positive, fails a trail whose newest records have gone longer than that without a signed head.
	Now          time.Time
	MaxClockSkew time.Duration
	MaxTailAge   time.Duration
	// Options configure the fold of the trail's records (agent.WithStopMode, WithStopPolicy,
	// WithStopRoot): the policy the deployment pinned. Without a policy the records are read as a basic
	// control reads them, and nothing is verified.
	Options []agent.StopOption
	// Control are the records of the control run, in store order, to compare with the trail; nil when the
	// store is not at hand (the comparison is then not made).
	Control     []agent.StopRecord
	HaveControl bool
}

StopVerifyInput is what VerifyStops checks: a stop trail, the signed heads over it, the policy the deployment pinned, and, when the control run is at hand, its records to compare with.

type TimeCheck added in v0.16.0

type TimeCheck struct {
	AskedAt   string
	HeadAt    string
	Tolerance time.Duration
}

TimeCheck is what CheckDecisionTimes compares decisions' times with: when the run asked (ApprovalVerdict.AskedAt), when the signed tree head covering the decisions was signed (ApprovalVerdict.HeadAt; "" skips that check), and how far clocks may disagree (Tolerance; negative is zero).

type TreeHead

type TreeHead struct {
	Kind  string `json:"kind"`   // which tree: TreeJournal, TreeEvents, or an absence kind
	RunID string `json:"run_id"` // the run the tree is about
	Size  int    `json:"size"`   // number of leaves committed
	Root  []byte `json:"root"`   // RFC 6962 Merkle root over those leaves
	// TimestampNanos is when the head was signed, in Unix nanoseconds (time.Now().UnixNano()); see
	// CheckTimestamp.
	TimestampNanos int64 `json:"timestamp_nanos"`
	// Journal is set on an absence key-set tree: the journal tree (of the same run) whose
	// records the key set was projected from. It is nil on a journal or event tree.
	Journal *TreeRef `json:"journal,omitempty"`
}

TreeHead is a commitment to one tree of a run at a point in time.

func NewAbsenceTreeHead added in v0.7.0

func NewAbsenceTreeHead(records []agent.Record, set KeySet, journal TreeHead, timestamp int64) (TreeHead, error)

NewAbsenceTreeHead builds the key-set tree head for set over a run's journal. journal is the run's journal tree head (NewTreeHead) and records the run's journal: the first journal.Size records must hash to journal.Root. The head commits to set.Kind, the run, the key set projected from exactly those records, and the journal tree itself, so it is bound to one run's history.

func NewTreeHead

func NewTreeHead(ctx context.Context, store *agent.Journal, runID string, timestamp int64) (TreeHead, error)

NewTreeHead builds a TreeHead committing to runID's journal at the given timestamp.

func (TreeHead) SameTree added in v0.7.0

func (th TreeHead) SameTree(o TreeHead) bool

SameTree reports whether th and o commit to the same tree: the same kind, run, size, root, and (for a key-set tree) source journal. Timestamps may differ.

type TreeRef added in v0.7.0

type TreeRef struct {
	Size int    `json:"size"` // number of leaves
	Root []byte `json:"root"` // RFC 6962 Merkle root over them
}

TreeRef names one tree of a run by its size and root.

type VerifiedPolicy added in v0.7.0

type VerifiedPolicy struct {
	Digest      string          `json:"digest"`
	Policy      string          `json:"policy"`
	Certificate json.RawMessage `json:"certificate"`
}

VerifiedPolicy is one used policy as VerifyRun read it from its anchored leaves: the policy bytes from the policy leaf and the serialized convergence certificate from the convergence leaf.

type Verifier

type Verifier interface {
	// Alg names the scheme.
	Alg() Alg
	// PublicKey returns the encoded public key (see NewVerifier), or nil if the verifier holds none.
	PublicKey() []byte
	// Verify reports whether signature is valid over message. A verifier without a usable key
	// verifies nothing; it never panics.
	Verify(message, signature []byte) bool
	// KeyIDs identifies the signing keys behind Verify (see agent.ApproverVerifier.KeyIDs and
	// KeyID), so every Verifier is an agent.ApproverVerifier. A verifier without a usable key
	// reports none. It MUST list one ID per independent key component (a hybrid key lists its
	// Ed25519 and its ML-DSA component separately), because seat, quorum and second-person checks
	// (approvals, the stop quorum, a hold lift's authorizer) treat two keys whose sets intersect as one
	// person; a verifier that reports one combined ID for a hybrid key defeats them.
	KeyIDs() []string
}

Verifier checks signatures under one scheme with one public key.

func NewVerifier added in v0.9.0

func NewVerifier(alg Alg, pub []byte) (Verifier, error)

NewVerifier returns the verifier of scheme alg for the encoded public key pub:

ed25519:           the 32-byte Ed25519 public key
ecdsa-p256:        the 65-byte SEC1 uncompressed point 0x04 || X || Y (see signing_p256.go)
ml-dsa-65:         the 1952-byte ML-DSA-65 public key encoding (mldsa.PublicKey.Bytes)
ed25519+ml-dsa-65: a 4-byte big-endian length of the Ed25519 key, the Ed25519 key, then the
                   ML-DSA-65 key (HybridSigner.PublicKey)

A key that is not of its scheme's form is an error wrapping ErrMalformed; a weak Ed25519 key (see CheckEd25519PublicKey) wraps ErrWeakKey as well.

func ParsePublicKey added in v0.9.0

func ParsePublicKey(s string) (Verifier, error)

ParsePublicKey reads the text form of a public key (FormatPublicKey) and returns its verifier. A bare hex string of 32 bytes is read as an Ed25519 key. Surrounding white space is ignored. Any other text is an error wrapping ErrMalformed.

func VerifierOf added in v0.9.0

func VerifierOf(s Signer) (Verifier, error)

VerifierOf returns the verifier for s's public key: NewVerifier(s.Alg(), s.PublicKey()).

Directories

Path Synopsis
Package anchorobj is the part of an object-store audit anchor that does not depend on any storage SDK: the layout of the objects that hold signed tree heads, their canonical body, the checks a Reader makes on an object it reads back, the equivocation check, and the tripwire for run IDs that look like personal data.
Package anchorobj is the part of an object-store audit anchor that does not depend on any storage SDK: the layout of the objects that hold signed tree heads, their canonical body, the checks a Reader makes on an object it reads back, the equivocation check, and the tripwire for run IDs that look like personal data.
Package crlrefresh keeps the status snapshot of a certificate-bound approver (audit.CertApprover) supplied with the newest verified CRLs of the issuing CAs its policy pins, and archives every CRL it accepts (docs/design/crl-refresher.md, docs/guides/certificate-approvers.md).
Package crlrefresh keeps the status snapshot of a certificate-bound approver (audit.CertApprover) supplied with the newest verified CRLs of the issuing CAs its policy pins, and archives every CRL it accepts (docs/design/crl-refresher.md, docs/guides/certificate-approvers.md).
Package eventstoretest checks that an audit.EventStore keeps the contract the event trail builds on: contiguous, append-only positions per run, an idempotent re-append of the same leaf, a refusal (audit.ErrSeqTaken) of a different leaf at a stored position, and exactly one winner among writers racing for one position.
Package eventstoretest checks that an audit.EventStore keeps the contract the event trail builds on: contiguous, append-only positions per run, an idempotent re-append of the same leaf, a refusal (audit.ErrSeqTaken) of a different leaf at a stored position, and exactly one winner among writers racing for one position.
Package rfc3161 verifies RFC 3161 time-stamp tokens with the standard library alone.
Package rfc3161 verifies RFC 3161 time-stamp tokens with the standard library alone.
Package verify is a dependency-light, standalone verifier for Bide audit proofs.
Package verify is a dependency-light, standalone verifier for Bide audit proofs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL