approval

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package approval is the cryptographic authorization layer for privileged actions. It sits above the capability grant at the dispatch waist: the grant decides what a run is allowed to do at all, while approval requires a detached, verifiable signature from an authorized approver before the waist admits a specific privileged action (release a credential, run a destructive command, spend over a threshold, deploy, escalate autonomy). No valid signature, no action: the gate is fail-closed.

The unit of authorization is a canonical, replay-proof Envelope: it names the action, the scope and principal it is bound to, a single-use nonce, a validity window, and the host it is valid on. An approver signs the envelope out of band (from a phone, a laptop, a hardware token) and the run carries the resulting Approval to the waist, where a Verifier checks the signature against a keyring of authorized approvers and enforces the binding: an approval is good for exactly one action, on one run, on one host, once, within its window. A captured approval cannot be replayed, widened to another action, or reused.

The signing method is a port, not a fixed dependency: the default is Ed25519 from the standard library (no external dependency), and any other method (hardware token, KMS, a wallet-based signer) plugs in behind the same Signer and keyring without changing the gate. High-risk actions can require a quorum of independent signatures (M-of-N), expressed as policy data. Every decision, grant or denial, is offered to a Sink so it lands on the event spine as an immutable, after-the-fact-verifiable record of who authorized what.

Index

Constants

This section is empty.

Variables

View Source
var ErrNonceUsed = fault.New(fault.Forbidden, "approval_nonce_used", "approval: nonce already used (replay)")

ErrNonceUsed means an approval's nonce has already been spent: the approval is a replay and is refused.

Functions

func Into

func Into(ctx context.Context, approvals ...Approval) context.Context

Into returns a context carrying approvals presented for the run's privileged actions, accumulating with any already bound, so a quorum can be assembled from approvals that arrive separately. The gate reads them from the context, so a caller binds them once rather than threading them through every call.

func WithDetail

func WithDetail(ctx context.Context, detail string) context.Context

WithDetail binds a target descriptor for the next privileged action, so an approval can be narrowed to a specific target (a resource id, an environment) rather than the action name alone. The gate folds it into the envelope it requires, and an approver must have signed for the same detail. Empty (the default) binds by action and scope only.

Types

type Approval

type Approval struct {
	// Envelope is the authorization that was signed.
	Envelope Envelope `json:"envelope"`
	// KeyID identifies the approver's key in the verifier's keyring, so the verifier
	// knows which public key to check the signature against and the audit record
	// shows who signed.
	KeyID string `json:"keyId"`
	// Signature is the detached signature over Envelope.signingBytes().
	Signature []byte `json:"signature"`
}

Approval is a signed Envelope: the authorization plus the identity of the key that signed it and the detached signature over the envelope's signing bytes. A run presents one (or several, for a quorum) to the waist.

func FromContext

func FromContext(ctx context.Context) []Approval

FromContext returns the approvals bound to ctx, or nil when none are.

type Decision

type Decision struct {
	Envelope Envelope
	// KeyIDs are the distinct authorized approvers whose valid signatures counted
	// toward the requirement (empty on a denial with no valid signatures).
	KeyIDs []string
	// Granted reports whether the action was authorized.
	Granted bool
	// Reason explains a denial (empty when granted).
	Reason string
	// At is the unix-nano time of the decision, from the gate's clock.
	At int64
}

Decision is the record of one authorization check at the waist, offered to a Sink so it lands on the event spine. It captures the envelope that was required, the approvers whose signatures counted, whether the action was granted, and why when it was not.

type DiscardSink

type DiscardSink struct{}

DiscardSink is the default Sink that records nothing.

func (DiscardSink) Record

Record implements Sink.

type Ed25519Signer

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

Ed25519Signer signs envelopes with an Ed25519 private key. It is the default, standard-library signer: no external dependency, small keys, fast verification.

func GenerateEd25519Signer

func GenerateEd25519Signer(keyID string, rand io.Reader) (*Ed25519Signer, ed25519.PublicKey, error)

GenerateEd25519Signer mints a fresh Ed25519 keypair from rand and returns a signer over it together with its public key, so a caller can register the public key in a verifier's keyring. In production rand is crypto/rand.Reader; a test can inject a deterministic reader.

func NewEd25519Signer

func NewEd25519Signer(keyID string, priv ed25519.PrivateKey) (*Ed25519Signer, error)

NewEd25519Signer builds a signer over an existing private key, identified by keyID. It refuses a malformed key so a signer is never silently unable to sign.

func (*Ed25519Signer) KeyID

func (s *Ed25519Signer) KeyID() string

KeyID identifies the signing key.

func (*Ed25519Signer) Sign

func (s *Ed25519Signer) Sign(e Envelope) (Approval, error)

Sign signs e and returns the Approval.

type Envelope

type Envelope struct {
	// Action is the dispatch action name being authorized (e.g. "secret.release",
	// "deploy"). An approval for one action never authorizes another.
	Action string `json:"action"`
	// Scope is the instance/project/workspace the action runs in. An approval is
	// valid only for its own scope.
	Scope state.Scope `json:"scope"`
	// Principal is the run (or actor) id the approval is bound to, so an approval
	// granted to one run cannot be used by another.
	Principal string `json:"principal"`
	// Detail is an optional target descriptor that further narrows the authority
	// (e.g. a resource id or "prod"). It is signed, so an approval for one target
	// does not authorize another. Empty means the action name and scope alone bound it.
	Detail string `json:"detail,omitempty"`
	// Nonce makes the approval single-use: the verifier records it on first use and
	// refuses it thereafter, so a captured approval cannot be replayed.
	Nonce string `json:"nonce"`
	// NotBefore is the unix-nano time before which the approval is not yet valid.
	// Zero means no lower bound.
	NotBefore int64 `json:"notBefore,omitempty"`
	// Expiry is the unix-nano time at or after which the approval is no longer
	// valid. Zero means it never expires, which a policy should avoid for anything
	// high-risk.
	Expiry int64 `json:"expiry,omitempty"`
	// Host is the host the approval is valid on, so an approval minted for one agent
	// host cannot authorize an action on another. Empty means any host.
	Host string `json:"host,omitempty"`
}

Envelope is the canonical, replay-proof description of one authorization. It is what an approver signs, so every field that scopes the authority is inside the signature: changing any of them invalidates the signature. It deliberately binds to the action name and scope, not the action's arguments, because the dispatch waist authorizes by action identity, not payload; a caller that needs to bind to a specific target sets it in Detail, which is also signed.

func Binding

func Binding(ctx context.Context, a dispatch.Action, host string) Envelope

Binding builds the envelope a privileged action requires, from the action, the run's principal and target on the context, and the gate's host. An approver signs this envelope (after setting a nonce and validity window) so the signature is over exactly what the gate checks; the gate builds the same binding to verify.

type Gate

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

Gate is the approval enforcement at the dispatch waist: a dispatch.Hook whose Before refuses a privileged action unless a sufficient quorum of valid approvals is presented for it. Because the waist governs every action, one Gate enforces approval across the whole run with no per-call wiring. It composes with, and sits above, the capability admitter: capability decides the action is allowed in principle, approval requires a fresh human (or policy, or peer) authorization for this specific privileged instance.

func NewGate

func NewGate(policy Policy, verifier *Verifier, opts ...GateOption) *Gate

NewGate builds an approval gate over a policy and verifier. Add it to a dispatcher with dispatch.WithHook so every privileged action it governs requires approval.

func (*Gate) After

After is a no-op: approval is decided before the action runs.

func (*Gate) Before

func (g *Gate) Before(ctx context.Context, a dispatch.Action) error

Before refuses a privileged action that lacks a sufficient quorum of valid approvals. A non-privileged action (policy requirement zero) is admitted untouched. The decision, grant or denial, is recorded to the sink before the result is returned, so the audit trail captures refused attempts too. A denial with no approvals presented is NeedsApproval (the run should pause and request one); a denial with approvals that did not verify is Forbidden (it must not be retried as-is).

type GateOption

type GateOption func(*Gate)

GateOption configures a Gate.

func WithGateHost

func WithGateHost(host string) GateOption

WithGateHost sets the host id the gate stamps on the envelope it requires, so an approval must be valid for this host. It should match the verifier's host.

func WithSink

func WithSink(s Sink) GateOption

WithSink wires the audit sink decisions are recorded to (default: DiscardSink).

type Keyring

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

Keyring is the set of authorized approvers: a map from key id to the public key that authorizes signatures from it. Only a signature from a key in the ring can count toward an authorization, so revoking an approver is removing their key. It is read-only after construction in normal use, so it is safe for concurrent reads.

func NewKeyring

func NewKeyring() *Keyring

NewKeyring builds an empty keyring.

func (*Keyring) Add

func (k *Keyring) Add(keyID string, pub ed25519.PublicKey) error

Add registers an authorized approver's public key under keyID. A later Add for the same id replaces the key (a key rotation). It refuses a malformed key so the ring never holds one that can never verify.

type MemStore

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

MemStore is the default in-memory NonceStore, safe for concurrent use. Spent nonces last for the life of the process; they do not survive a restart.

func NewMemStore

func NewMemStore() *MemStore

NewMemStore builds an empty in-memory nonce store.

func (*MemStore) Seen

func (s *MemStore) Seen(_ context.Context, nonce string) (bool, error)

Seen reports whether nonce has been spent.

func (*MemStore) Use

func (s *MemStore) Use(_ context.Context, nonce string) error

Use marks nonce spent, refusing a replay.

type NonceStore

type NonceStore interface {
	// Seen reports whether nonce has already been spent.
	Seen(ctx context.Context, nonce string) (bool, error)
	// Use marks nonce spent, returning ErrNonceUsed if it already was. It is the
	// atomic test-and-set that makes single-use hold even under concurrency.
	Use(ctx context.Context, nonce string) error
}

NonceStore enforces the single-use property of an approval: once a nonce has been spent, it can never be spent again, so a captured approval cannot be replayed. Seen is the non-committing check the verifier uses while deciding whether a quorum is met; Use is the commit it makes only once the action is authorized, so a partial or failed attempt does not burn a valid approval's nonce. An implementation must be safe for concurrent use. The default MemStore keeps the spent set in memory; a fleet that must survive a restart supplies a durable store behind the same port.

type Policy

type Policy interface {
	Required(a dispatch.Action) int
}

Policy decides how many independent approver signatures an action requires before the waist admits it. Zero means the action needs no approval and runs under the capability grant alone; a positive number is the quorum (1 for a single approver, more for a high-risk M-of-N action). Keeping the requirement as data lets a host raise the bar on a destructive action without touching the gate.

type Requirements

type Requirements map[string]int

Requirements is a map-based Policy from action name to required signature count. An action not in the map requires no approval, so the default is permissive and a host opts specific privileged actions into the gate.

func (Requirements) Required

func (r Requirements) Required(a dispatch.Action) int

Required returns the signatures the action needs, or zero when it is not listed.

type Signer

type Signer interface {
	// Sign returns an Approval carrying e, the signer's KeyID, and the signature
	// over e's signing bytes.
	Sign(e Envelope) (Approval, error)
	// KeyID identifies the signing key in a verifier's keyring.
	KeyID() string
}

Signer produces a detached signature over an Envelope. It is the approver's side of the boundary: the method (Ed25519, a hardware token, a KMS, a wallet) is an implementation detail behind this port, so the gate verifies a signature without depending on how it was produced. KeyID identifies the signing key so a verifier knows which public key to check against and the audit trail shows who signed.

type Sink

type Sink interface {
	Record(ctx context.Context, d Decision) error
}

Sink records an authorization Decision, so a grant or a denial lands on the event spine as an immutable, after-the-fact-verifiable record of who authorized what. The default DiscardSink drops it, keeping standalone use zero-config; a host wires this to the spine.

type Verifier

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

Verifier checks presented approvals against a required authorization. It holds the keyring of authorized approvers, the nonce store that enforces single use, the host the agent runs on, and a clock for the validity window. It is the trusted core of the layer: the gate asks it whether an action is authorized, and it answers only yes when enough independent, in-window, correctly-bound, not-yet-replayed signatures from authorized keys are present.

func NewVerifier

func NewVerifier(keyring *Keyring, nonces NonceStore, opts ...VerifierOption) *Verifier

NewVerifier builds a verifier over a keyring and nonce store.

func (*Verifier) Check

func (v *Verifier) Check(ctx context.Context, want Envelope, presented []Approval, required int) (Decision, error)

Check decides whether the presented approvals authorize want, requiring at least `required` valid signatures from distinct authorized approvers. It returns a Decision describing the outcome (for the audit record) and, when authorized, commits the contributing nonces so they cannot be replayed. It commits nothing unless the quorum is met, so a partial attempt does not burn a valid approval.

An approval counts only when every binding holds: its envelope matches want on action, scope, principal, and detail; its host is this host or unset; it is inside its validity window; its signature verifies against an authorized key; and its nonce has not been spent. Signatures from the same key id count once, so one approver cannot satisfy a multi-signature requirement alone.

type VerifierOption

type VerifierOption func(*Verifier)

VerifierOption configures a Verifier.

func WithClock

func WithClock(c clock.Clock) VerifierOption

WithClock sets the time source the validity window is checked against (default: clock.System).

func WithHost

func WithHost(host string) VerifierOption

WithHost sets the host id an approval must be valid on. An approval whose envelope names a different host is refused; an approval with an empty host is valid on any host. Default is the empty host (any).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL