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 ¶
- Variables
- func Into(ctx context.Context, approvals ...Approval) context.Context
- func WithDetail(ctx context.Context, detail string) context.Context
- type Approval
- type Decision
- type DiscardSink
- type Ed25519Signer
- type Envelope
- type Gate
- type GateOption
- type Keyring
- type MemStore
- type NonceStore
- type Policy
- type Requirements
- type Signer
- type Sink
- type Verifier
- type VerifierOption
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
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.
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 ¶
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) Before ¶
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.
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.
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 ¶
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 ¶
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.
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 ¶
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).