Documentation
¶
Overview ¶
Package provenance signs and verifies append-only memory entries: per-publisher Ed25519 signatures over a canonical entry digest, hash-chained per (scope, publisher) for gap/truncation detection.
Index ¶
- Variables
- func ComputeChainHeads(ctx context.Context, mem memory.Memory, scope memory.Scope) (map[string]ChainHead, error)
- func DecodePubKey(b64 string) (ed25519.PublicKey, error)
- func EntryDigest(e memory.Entry) string
- func KeyID(pub ed25519.PublicKey) string
- func SessionPublisher(namespace, name string) string
- func VerifyEntrySignature(keys PublisherKeyLookup, e memory.Entry) error
- type ChainHead
- type Finding
- type MapKeyLookup
- type PubKeyRef
- type PublisherKeyLookup
- type Report
- type Signer
- func (s *Signer) EnsureSeeded(ctx context.Context, mem memory.Memory, scope memory.Scope) error
- func (s *Signer) InvalidateSeed(scope memory.Scope)
- func (s *Signer) KeyID() string
- func (s *Signer) Publisher() string
- func (s *Signer) SeedFromMemory(ctx context.Context, mem memory.Memory, scope memory.Scope) error
- func (s *Signer) Sign(e *memory.Entry) error
- type SigningMemory
- type Verdict
- type Verifier
- type WriteVerifier
Constants ¶
This section is empty.
Variables ¶
var ErrEntryAppendedDispatchPut = errors.New("signingmem: refusing an append-only Put from inside an entry-appended dispatch")
ErrEntryAppendedDispatchPut is returned when an append-only Put reaches a SigningMemory from inside its own SignalEntryAppended dispatch — a ScopeHooks reaction Put()ing back through the same wrapper that raised the signal. See the comment inside Put for why this cannot be performed safely on the calling goroutine.
Functions ¶
func ComputeChainHeads ¶
func ComputeChainHeads(ctx context.Context, mem memory.Memory, scope memory.Scope) (map[string]ChainHead, error)
ComputeChainHeads scans every append-only kind in scope and returns, per publisher, the highest-seq entry's chain head {Seq, EntryDigest(entry)}. It anchors tail-truncation detection for ended sessions.
Entries with nil Provenance (unsigned writers) attribute to no publisher and are skipped — they anchor nothing. The per-publisher max is taken ACROSS all append-only kinds, matching the Signer's per-scope (not per-kind) chain.
func DecodePubKey ¶
DecodePubKey decodes a standard-base64 Ed25519 public key, validating it is exactly ed25519.PublicKeySize bytes. Delegates to pkg/x/keyid; see KeyID.
func EntryDigest ¶
EntryDigest returns the hex SHA-256 of the canonical serialization of e: scope, kind, id, createdAt (UTC RFC3339Nano), sorted tags, links, content, publisher, keyID, seq, prevHash — everything except the signature. It is BOTH the chain link and the signed message.
func KeyID ¶
KeyID returns the key identifier for a public key: hex of the first 16 bytes of its SHA-256.
The derivation lives in pkg/x/keyid so a package can name a key without importing this one — importing this one confers the ability to SIGN, which pkg/web/webui/chat is guarded against acquiring at any depth.
func SessionPublisher ¶
SessionPublisher returns the provenance publisher string for a session's signed audit entries: "session:<ns>/<name>". Controller (key registration) and runner (signing) MUST derive it identically.
func VerifyEntrySignature ¶
func VerifyEntrySignature(keys PublisherKeyLookup, e memory.Entry) error
VerifyEntrySignature checks one entry's provenance envelope on its own: the envelope is present, a key is registered for the (publisher, keyID) it claims, and the signature verifies over the entry digest. It says NOTHING about chain position — no seq, no prevHash — so it answers "did this publisher sign this payload?" and not "does this entry belong where it sits". Use VerifyChain for the latter.
It is the check a reader owes any entry whose CONTENT it is about to act on (a witnessed key binding, for one) and the same check WriteVerifier applies at the door, so the two cannot drift apart.
Types ¶
type ChainHead ¶
ChainHead is a trusted tail anchor for a publisher's chain: the sequence and digest of the last entry the publisher is known to have signed. Recorded externally (e.g. AgentSession.status) so truncation of the stored tail is detectable.
type Finding ¶
Finding is one verifier verdict against a specific entry (or, for tail truncation, against the chain head). Seq is the offending entry's sequence (or the expected anchor seq for truncation).
type MapKeyLookup ¶
MapKeyLookup is a static (publisher,keyID)→key PublisherKeyLookup built from a trusted snapshot (used by oap audit verify and tests). Keying by BOTH publisher and keyID — not keyID alone — is what stops a key registered for one publisher from verifying another publisher's chain, matching WriteVerifier's write-time semantics.
func (MapKeyLookup) PublisherKey ¶
func (m MapKeyLookup) PublisherKey(publisher, keyID string) (ed25519.PublicKey, bool)
PublisherKey resolves a (publisher, keyID) pair. Satisfies PublisherKeyLookup.
type PubKeyRef ¶
PubKeyRef identifies a trusted key by the (publisher, keyID) pair — the same composite the write-time PublisherKeyLookup keys on.
type PublisherKeyLookup ¶
type PublisherKeyLookup interface {
PublisherKey(publisher, keyID string) (ed25519.PublicKey, bool)
}
PublisherKeyLookup resolves a publisher's registered public keys for verify-on-write. The operator composes the per-session key registry (tokens) and the component key registry behind this.
type Report ¶
Report is the outcome of verifying one publisher's chain. OK counts the entries that passed every check; Findings lists every problem.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer attests append-only entries for one publisher with one Ed25519 key. It maintains per-scope hash-chain state under a mutex so concurrent Puts across distinct scopes don't interleave, and a single scope's chain advances atomically. A Signer is safe for concurrent use.
func NewSigner ¶
func NewSigner(priv ed25519.PrivateKey, publisher string) *Signer
NewSigner builds a Signer for publisher over priv. The key ID is derived from the public half via KeyID.
func (*Signer) EnsureSeeded ¶
EnsureSeeded runs SeedFromMemory at most once per scope. Subsequent calls for the same scope are no-ops, so the hot Put path pays the query cost only on the first append-only write into a scope.
func (*Signer) InvalidateSeed ¶
InvalidateSeed forces the next EnsureSeeded(scope) to re-derive the chain head from durable storage. Call it whenever a signed entry did NOT land at the position Sign minted for it — the store rejected the write, or accepted it without persisting that position (an append-only re-put whose payload matches a stored entry is idempotent: the facade returns the stored entry, discarding the freshly-minted seq). Sign advances the in-memory head before the Put, so either case otherwise strands the head ahead of the durable tail, and the next accepted append would skip a seq (gap) and link its prevHash to a digest that was never stored (fork). Re-seeding rebuilds the head to the highest seq that actually landed — the right answer whether the lost entry was the chain tail or an older position being re-put.
func (*Signer) SeedFromMemory ¶
SeedFromMemory rebuilds this Signer's chain head for scope from persisted entries, so a freshly constructed Signer (after a process restart or session resume) continues the chain rather than restarting it at Seq 1. It scans every registered append-only Kind for entries this publisher signed and adopts the one with the highest Seq as the new chain head.
func (*Signer) Sign ¶
Sign fills e.Provenance with this Signer's publisher/keyID, the next sequence number for e.Scope, and the prior entry's digest as PrevHash, then signs the canonical digest and advances the chain. The signature covers the digest, which already includes the provenance header (publisher/keyID/seq/prevHash) — so it commits to chain position as well as payload.
type SigningMemory ¶
SigningMemory decorates a memory.Memory so that Put on an append-only Kind seeds the publisher's chain (once per scope) and signs the entry before it reaches the inner store. Query, Search, and SendSignal pass through unchanged via the embedded Memory.
Every append-only writer in the process (authz-decision recording, approval requests, audit-log appends, lifecycle events, transcript writes — on a detached goroutine or the live turn) goes through one SigningMemory per session, so serializing Sign+Put here covers all of them from a single choke point; see Put.
func NewSigningMemory ¶
func NewSigningMemory(inner memory.Memory, signer *Signer) *SigningMemory
NewSigningMemory wraps inner so append-only Puts are attested by signer.
func (*SigningMemory) Put ¶
Put signs append-only entries before forwarding them; mutable-Kind entries pass through untouched (Provenance stays nil).
func (*SigningMemory) PutToPool ¶
func (m *SigningMemory) PutToPool(ctx context.Context, sessionScope, poolScope memory.Scope, e memory.Entry) (memory.Entry, error)
PutToPool signs an append-only entry before forwarding it into a resource pool, the same way Put signs one before forwarding it to the caller's own scope — see Put's own comments for why Sign and the durable write must be atomic per scope, why a rejected write must not strand the in-memory head, and why a nil error is not proof the signed position landed. This method mirrors that discipline against poolScope rather than e.Scope, because the entry lands at the pool, not at sessionScope.
FAILS LOUDLY — a distinct, named error identifying the concrete inner type — when m.Memory does not implement memory.PoolWriter, rather than silently declining the write. SigningMemory embeds Memory as an INTERFACE field, so Go's method promotion never forwards a concrete inner's extra methods on its own; an operator reading this error learns exactly which concrete type behind the signing wrapper needs to grow the capability, instead of chasing a permission-shaped refusal that was never about permissions.
e.Scope is stamped to poolScope before signing (overwriting whatever the caller set, or left zero) because EntryDigest signs e.Scope, and the server forces the STORED entry's scope to be exactly the pool the request line named — signing anything else would produce a signature that could never verify against what actually lands.
type Verdict ¶
type Verdict string
Verdict labels one thing the verifier found wrong (or, by absence, right) about a chain entry. A defined type (not a string alias) so the compiler type-checks verdict values at use sites.
const ( // VerdictUnsigned: an append-only entry carries no Provenance. VerdictUnsigned Verdict = "unsigned" // VerdictUnknownKey: the entry's KeyID is not in the trusted set. VerdictUnknownKey Verdict = "unknown-key" // VerdictBadSignature: the Ed25519 signature does not verify over // the recomputed digest. VerdictBadSignature Verdict = "bad-signature" // VerdictGap: a sequence number is missing from the chain. VerdictGap Verdict = "gap" // VerdictFork: two entries share a Seq, or a PrevHash does not link // to the prior entry's digest. VerdictFork Verdict = "fork" // VerdictTailTruncated: a trusted anchor expects a higher max Seq // (or a different head hash) than the stored chain provides. VerdictTailTruncated Verdict = "tail-truncated" )
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier checks chains against a trusted key set. It resolves keys by (publisher, keyID) — the same composite the write-time WriteVerifier uses — so a key registered for one publisher cannot verify a chain attributed to another.
func NewVerifier ¶
func NewVerifier(keys PublisherKeyLookup) *Verifier
NewVerifier builds a Verifier over the given (publisher,keyID) lookup. Pass a MapKeyLookup, a *publisherkeys.Registry, or any PublisherKeyLookup.
func (*Verifier) VerifyChain ¶
VerifyChain walks publisher's chain within entries and reports every problem found. Entries signed by other publishers are ignored. Append-only entries carrying no Provenance are reported unsigned. The remaining entries are sorted by Seq and checked for forks (duplicate Seq, broken prevHash linkage), gaps (missing Seq from 1), unknown keys, and bad signatures. When anchor is non-nil, a stored max Seq below the anchor — or an equal Seq whose head digest differs — is reported as tail truncation.
type WriteVerifier ¶
type WriteVerifier struct {
// contains filtered or unexported fields
}
WriteVerifier implements memory.ProvenanceVerifier: it requires a provenance envelope on append-only writes and verifies the signature under a registered key for the entry's publisher.
func NewWriteVerifier ¶
func NewWriteVerifier(keys PublisherKeyLookup) *WriteVerifier
NewWriteVerifier builds a verify-on-write enforcer over keys.
func (*WriteVerifier) VerifyEntry ¶
VerifyEntry rejects an append-only write that lacks provenance, whose publisher is not one the authenticated writer may author as, whose key is not registered, or whose signature does not verify over the entry digest.
The publisher check (checkAuthor) and the key/signature check are two different guarantees and BOTH are load-bearing. The signature proves the entry was produced by whoever holds the publisher's key; the publisher check proves the writer on this connection IS that publisher. Drop the second and forged attribution is refused only for as long as no key ever leaks or is mis-registered — precisely the containment an audit log exists to provide.