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.
Index ¶
- Constants
- Variables
- func AbsenceRoot(records []agent.Record, set KeySet) (root []byte, size int, err error)
- func ApprovalArgsDigest(s agent.ApprovalSubject) string
- func AttenuatingSubAgent(name, description string, sub *agent.Agent, cfg AttenuationConfig, ...) agent.Tool
- func CheckAttenuation(parent, child Grant, rules ScopeRules) error
- func CheckDeclaredGoDebug(declared, environment string) error
- func CheckEd25519PublicKey(pub []byte) error
- func CheckTimestamp(th TreeHead, now time.Time, skew time.Duration) error
- func CheckTimestampOrder(earlier, later TreeHead) error
- func ConvergenceLeafName(digest string) string
- func ECDSAP256PublicKeyFromSPKI(spki []byte) ([]byte, error)
- func ECDSAP256SignatureFromDER(der []byte) ([]byte, error)
- func ECDSAP256SignatureFromRaw(rs []byte) ([]byte, error)
- func ExecLatencyBound(publishDelay, period, overlap, execAge time.Duration) time.Duration
- func FormatPublicKey(alg Alg, pub []byte) string
- func GovernedPolicyDigest(result json.RawMessage) (string, error)
- func GrantFrom(ctx context.Context) (SignedGrant, Signer, bool)
- func Head(ctx context.Context, store *agent.Journal, runID string) ([]byte, error)
- func JournalLeafHash(recordBytes []byte) []byte
- func KeyID(alg string, pub []byte) string
- func NumericAtMost(parent, child string) bool
- func PersistEvent(ctx context.Context, evStore EventStore, runID string, seq int, ...) error
- func PersistJournal(ctx context.Context, evStore EventStore, jStore *agent.Journal, runID string) error
- func PoliciesUsed(records []agent.Record) ([]string, error)
- func PolicyLeafName(digest string) string
- func PolicyUsedKey(r agent.Record) []string
- func PolicyUsedKeyFor(digest string) string
- func RecordConvergence(ctx context.Context, store *agent.Journal, runID string, cert []byte, ...) (agent.Record, error)
- func RecordGrant(ctx context.Context, store *agent.Journal, runID string, sg SignedGrant) (agent.Record, error)
- func RecordPolicy(ctx context.Context, store *agent.Journal, runID string, policy []byte, ...) (agent.Record, error)
- func RecordRunCertificate(ctx context.Context, store *agent.Journal, runID string, cert RunCertificate) (agent.Record, error)
- func RecordStream(log *EventLog, stream *agent.RunStream, onEvent func(agent.RunEvent)) (*agent.Result, error)
- func Root(ctx context.Context, store *agent.Journal, runID string) ([]byte, error)
- func Sign(head []byte, s Signer) ([]byte, error)
- func ToolUseKey(r agent.Record) []string
- func ToolUseKeyFor(toolUseID string) string
- func UnmarshalStrict(data []byte, v any) error
- func VerifyAbsence(root []byte, proof Absence) error
- func VerifyAnchorInclusion(root []byte, entry AnchorEntry, proof Inclusion) error
- func VerifyConsistency(firstRoot, laterRoot []byte, proof Consistency) error
- func VerifyCurrentGrant(sg SignedGrant, ledgerRunID string, p CurrentGrantProof, ...) error
- func VerifyDelegationChain(chain []SignedGrant, issuerVerifier func(issuer string) (Verifier, bool), ...) error
- func VerifyEventInclusion(root []byte, event agent.RunEvent, proof EventInclusion) error
- func VerifyInclusion(root, recordBytes []byte, proof Inclusion) error
- func VerifySignature(head, sig []byte, v Verifier) error
- func WithGrant(ctx context.Context, sg SignedGrant, signer Signer) context.Context
- func WithRollbackGrants(ctx context.Context, signer Signer, grants ...SignedGrant) context.Context
- type Absence
- type AbsenceBundle
- type Alg
- type Anchor
- type AnchorEntry
- type ApprovalVerdict
- type ApproverCert
- type AttenuateFunc
- type AttenuationConfig
- type AuditedEventStore
- func (a *AuditedEventStore) AnchorTrails(ctx context.Context, trails []string) error
- func (a *AuditedEventStore) Append(ctx context.Context, trail string, seq int, leaf []byte) error
- func (a *AuditedEventStore) Flush(ctx context.Context) error
- func (a *AuditedEventStore) Load(ctx context.Context, trail string) ([][]byte, error)
- func (a *AuditedEventStore) OnError(fn func(trail string, err error)) *AuditedEventStore
- func (a *AuditedEventStore) Reanchor(ctx context.Context, trail string) error
- func (a *AuditedEventStore) ReanchorPending(ctx context.Context) error
- func (a *AuditedEventStore) Unanchored() []string
- func (a *AuditedEventStore) Unwrap() EventStore
- func (a *AuditedEventStore) WithClock(now func() int64) *AuditedEventStore
- func (a *AuditedEventStore) WithCooldown(d time.Duration) *AuditedEventStore
- func (a *AuditedEventStore) WithTimeout(d time.Duration) *AuditedEventStore
- type AuditedStore
- func (a *AuditedStore) Format(f fmt.State, _ rune)
- func (a *AuditedStore) Get(ctx context.Context, runID, name string) (agent.Entry, bool, error)
- func (a *AuditedStore) Insert(ctx context.Context, runID, name string, data []byte) (agent.Entry, bool, error)
- func (a *AuditedStore) Load(ctx context.Context, runID string, after int64) iter.Seq2[agent.Entry, error]
- func (a *AuditedStore) OnError(fn func(runID string, err error)) *AuditedStore
- func (a *AuditedStore) Reanchor(ctx context.Context, runID string) error
- func (*AuditedStore) String() string
- func (a *AuditedStore) Unwrap() agent.Store
- func (a *AuditedStore) WithClock(now func() int64) *AuditedStore
- type CRLInfo
- type CanonicalConfig
- type Canonicalizer
- type CertApprover
- func (*CertApprover) Alg() agent.Alg
- func (c *CertApprover) ArchiveStatus(ctx context.Context, hash string, der []byte) error
- func (c *CertApprover) DescribeIdentity() agent.IdentityPolicyRecord
- func (c *CertApprover) HasIdentity(approverID string) bool
- func (c *CertApprover) Identifier(approverID string) (string, bool)
- func (c *CertApprover) IdentityMappingConfigured() bool
- func (c *CertApprover) KeyIDs() []string
- func (c *CertApprover) PolicyDigest() string
- func (c *CertApprover) RequesterIdentifier(principal string) (string, bool)
- func (c *CertApprover) RosterDigest() string
- func (c *CertApprover) ValidateConfig() error
- func (c *CertApprover) VerifierInfo() (revision, goDebug, buildGoDebug string)
- func (*CertApprover) Verify(message, sig []byte) bool
- func (c *CertApprover) VerifyCertDecision(cd agent.CertDecision) agent.CertCheck
- func (c *CertApprover) VerifyCertExec(cd agent.CertDecision) agent.CertCheck
- func (c *CertApprover) VerifyCertIntake(cd agent.CertDecision) agent.CertCheck
- type CertError
- type CertIdentity
- func (c *CertIdentity) Canonicalizer() Canonicalizer
- func (c *CertIdentity) EvaluateStatus(ac *ApproverCert, a time.Time, s *StatusSnapshot, p StatusParams) ChainStatus
- func (c *CertIdentity) Identity(approverID string) (string, bool)
- func (c *CertIdentity) VerifyChain(approverID string, chain [][]byte, decidedAt, anchoredAt time.Time) (*ApproverCert, error)
- type CertStatus
- type ChainStatus
- type Consistency
- type ConvergenceContent
- type CoverageBound
- type CoverageReport
- type CurrentGrantProof
- type DecisionCoverage
- type DecisionReceipt
- func (r DecisionReceipt) AnchorTime(logKey Verifier) (time.Time, error)
- func (r DecisionReceipt) Verify(v Verifier) error
- func (r DecisionReceipt) VerifyCertificate(ci *CertIdentity, logKey Verifier) error
- func (r DecisionReceipt) VerifyIdentity(ci *CertIdentity, logKey Verifier) (ReceiptIdentity, error)
- type DecisionTime
- type DivergentHead
- type ECDSAP256Signer
- type ECDSAP256Verifier
- type EarnedAuthority
- type Ed25519Signer
- func (Ed25519Signer) Alg() Alg
- func (s Ed25519Signer) Format(f fmt.State, _ rune)
- func (Ed25519Signer) GobEncode() ([]byte, error)
- func (Ed25519Signer) MarshalJSON() ([]byte, error)
- func (s Ed25519Signer) PublicKey() []byte
- func (s Ed25519Signer) Sign(m []byte) ([]byte, error)
- func (Ed25519Signer) String() string
- type Ed25519Verifier
- type EventInclusion
- type EventLog
- func (l *EventLog) Add(e agent.RunEvent) error
- func (l *EventLog) Head() []byte
- func (l *EventLog) Len() int
- func (l *EventLog) Prove(index int) (EventInclusion, error)
- func (l *EventLog) ProveConsistency(first int) (Consistency, error)
- func (l *EventLog) Root() []byte
- func (l *EventLog) TreeHead(runID string, timestamp int64) TreeHead
- type EventStore
- type EvidenceAction
- type EvidenceConsistency
- type EvidenceGrants
- type EvidenceItem
- type EvidenceKind
- type EvidenceOption
- func WithAllToolCalls() EvidenceOption
- func WithConsistencyFrom(earlier SignedTreeHead) EvidenceOption
- func WithGrants() EvidenceOption
- func WithLabel(label string) EvidenceOption
- func WithRunCertificate(spec RunCertSpec) EvidenceOption
- func WithStep(name string) EvidenceOption
- func WithToolCall(toolUseID string) EvidenceOption
- type EvidencePackage
- type EvidenceReport
- type EvidenceVerifyOption
- type Grant
- type HybridSigner
- type HybridVerifier
- type IDKind
- type IdentityPolicy
- type IgnoredDecision
- type Inclusion
- type JournalExport
- type KeyFunc
- type KeySet
- type MLDSASigner
- type MLDSAVerifier
- type MemAnchorLog
- func (l *MemAnchorLog) Entries() []AnchorEntry
- func (l *MemAnchorLog) Len() int
- func (l *MemAnchorLog) Prove(seq int) (Inclusion, error)
- func (l *MemAnchorLog) ProveConsistency(first int) (Consistency, error)
- func (l *MemAnchorLog) Publish(_ context.Context, runID string, sth SignedTreeHead) error
- func (l *MemAnchorLog) Root() ([]byte, error)
- type MemEventStore
- type Neighbor
- type NotifyEventVerdict
- type NotifyReport
- type NotifyStatus
- type NotifyTrail
- type NotifyTrailExport
- type NotifyTrailReport
- type NotifyVerifyInput
- type PolicyContent
- type PolicyConvergence
- type ProofBundle
- func ProveApproval(ctx context.Context, store *agent.Journal, runID, step string, ...) (ProofBundle, error)
- func ProveConvergence(ctx context.Context, store *agent.Journal, runID, digest string, ...) (ProofBundle, error)
- func ProveGrant(ctx context.Context, store *agent.Journal, runID, digest string, ...) (ProofBundle, error)
- func ProvePolicy(ctx context.Context, store *agent.Journal, runID, digest string, ...) (ProofBundle, error)
- func ProveRecord(ctx context.Context, store *agent.Journal, runID string, index int, ...) (ProofBundle, error)
- func ProveRunCertificate(ctx context.Context, store *agent.Journal, runID string, sth SignedTreeHead) (ProofBundle, error)
- func ProveStep(ctx context.Context, store *agent.Journal, runID, name string, ...) (ProofBundle, error)
- func ProveToolCall(ctx context.Context, store *agent.Journal, runID, toolUseID string, ...) (ProofBundle, error)
- type ReceiptFinding
- type ReceiptIdentity
- type ReceiptVerdict
- type RejectedHead
- type RunCertSpec
- type RunCertificate
- type RunVerification
- type SPKIHash
- type ScopeRule
- type ScopeRules
- type SignedGrant
- type SignedTreeHead
- func SignAbsenceRoot(records []agent.Record, set KeySet, journal TreeHead, s Signer, ...) (SignedTreeHead, error)
- func SignNotifyTrail(ctx context.Context, store EventStore, trail string, s Signer, now time.Time) (SignedTreeHead, error)
- func SignTreeHead(th TreeHead, s Signer) (SignedTreeHead, error)
- type Signer
- type SnapshotOption
- type StatusArchive
- type StatusError
- type StatusParams
- type StatusSnapshot
- type StatusTier
- type TimeCheck
- type TreeHead
- type TreeRef
- type VerifiedPolicy
- type Verifier
Examples ¶
Constants ¶
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.
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 )
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).
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).
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.
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.
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.
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.
const EarnedScopeLimit = "limit"
EarnedScopeLimit is the scope key an earned grant sets to the earned limit (at most the root's).
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.
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).
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.
const JournalExportFormat = "bide.audit.journal-export.v1"
JournalExportFormat is the format of a JournalExport.
const NoteNoRecordedTime = "no recorded time"
NoteNoRecordedTime is DecisionCoverage.Note for a decision whose record has no usable recorded_at.
const NoteRedacted = "redacted"
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.
const NotifyTrailExportFormat = "bide.audit.notify-trail-export.v1"
NotifyTrailExportFormat is the format of a NotifyTrailExport.
const ReceiptFormat = "bide.audit.decision-receipt.v1"
ReceiptFormat identifies a decision receipt.
Variables ¶
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.
var EarnedRules = ScopeRules{EarnedScopeLimit: NumericAtMost}
EarnedRules is the ScopeRules an earned grant attenuates its root under.
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.
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.
var ErrWeakKey = errors.New("audit: weak ed25519 public key")
ErrWeakKey reports an Ed25519 public key that is not a usable verification key (see CheckEd25519PublicKey).
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).
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 ¶
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 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 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:
- child.ParentRef is parent's Digest (the hash link);
- child.Issuer is parent.Subject (only the holder of a grant can delegate it);
- 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);
- 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 CheckDeclaredGoDebug ¶ added in v0.19.0
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
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 CheckTimestamp ¶ added in v0.8.0
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
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
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
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
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
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
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
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 ¶
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
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
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 NumericAtMost ¶ added in v0.7.0
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 PoliciesUsed ¶
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
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 ¶
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 ¶
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 Root ¶
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 ¶
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 ToolUseKey ¶
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 ¶
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
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 ¶
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:
- each grant's signature verifies under its issuer's verifier (resolved via issuerVerifier),
- the root has no ParentRef,
- 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 ¶
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 ¶
VerifySignature returns nil if sig is a valid signature of head (see Sign) under v, and an error wrapping ErrNotVerified otherwise.
func WithGrant ¶
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
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 ¶
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
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 ¶
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
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, as a notifier does when it is flushed, so the last records are covered; whatever a process leaves behind is covered by the next sweep or recovery pass.
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) 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) 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 ¶
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) 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) 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) 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) 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.
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 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
ArchiveStatus implements agent.StatusArchiver with the verifier's StatusArchive, if any.
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
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.
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) 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) 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) 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) 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 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
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.
type ECDSAP256Verifier ¶ added in v0.17.0
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 ¶
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 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 ¶
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 (*EventLog) Add ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
func (e EvidencePackage) Verify(v Verifier, opts ...EvidenceVerifyOption) (EvidenceReport, error)
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 ¶
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.
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.
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
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 ¶
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
KeySetForKey returns the built-in key set a key belongs to, by its prefix.
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 ¶
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) 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.
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.
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
// 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 RejectedHead ¶ added in v0.17.0
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:
- the certificate itself: Properties is exactly the v1 list, and STH is an authentic journal head of RunID.
- 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.
- 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
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
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 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
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 StatusError ¶ added in v0.18.0
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 (partitioned CRLs), 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) 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) Len ¶ added in v0.18.0
func (s *StatusSnapshot) Len() int
Len is the number of distinct CRLs held.
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 TimeCheck ¶ added in v0.16.0
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.
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.
KeyIDs() []string
}
Verifier checks signatures under one scheme with one public key.
func NewVerifier ¶ added in v0.9.0
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
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
VerifierOf returns the verifier for s's public key: NewVerifier(s.Alg(), s.PublicKey()).
Source Files
¶
- absence.go
- absence_sign.go
- anchor.go
- approval.go
- approval_times.go
- audit.go
- audited_event_store.go
- audited_store.go
- canonical.go
- cert_approver.go
- cert_approver_config.go
- cert_exec.go
- confluence.go
- consistency.go
- decision_coverage.go
- delegate.go
- earned.go
- ed25519key.go
- eventsink.go
- eventstore.go
- evidence.go
- format.go
- grant.go
- identity_canonical.go
- identity_cert.go
- identity_policy_file.go
- identity_status.go
- journal_export.go
- keycache.go
- merkle.go
- notify_export.go
- notify_open.go
- notify_trail.go
- notify_verify.go
- policy.go
- proof.go
- receipt.go
- receipt_identity.go
- redact_keys.go
- runcert.go
- signing.go
- signing_p256.go
- step.go
- sth.go
- timestamp.go
Directories
¶
| Path | Synopsis |
|---|---|
|
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. |