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

capability

package module
v2.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 21 Imported by: 0

README

capability

CI CodeQL Documentation Go Reference Release Go License

capability issues and verifies narrowly scoped, tamper-evident, expiring Go capabilities and signed URLs. It is designed for downloads, uploads, invitations, callbacks, one-time actions, delegated access, and service-to-service handoffs.

The module is not an authentication system, a policy engine, a session format, a JWT or PASETO replacement, payload encryption, DRM, or legal non-repudiation. Applications remain responsible for authenticating callers and authorizing each attempted use of a verified grant.

For identity workflows, opaque proof custody adds explicit one-transfer ownership, purpose/reference metadata and original command binding without changing the cap1 protocol or automatically consuming on verification. ServiceConfig.Issuer, verifier key owners and attempted uses must independently select the same trusted issuer. Token claims never choose application trust. The optional postgres/identity module supplies coordinator- owned durable issuance, one-use reservation and original-outcome recovery.

Version 2 uses github.com/faustbrian/go-capability/v2. The examples below use its explicit issuer configuration, issuer-scoped replay and finite memory admission. Published v1 remains available at the unversioned module path; its issuer option fields differ from these examples. See the major adoption guidance.

Install

Install v2.0.0 once it appears in Releases:

go get github.com/faustbrian/go-capability/v2@v2.0.0

Existing v1 consumers can retain their published API until deliberate adoption:

go get github.com/faustbrian/go-capability@v1

The core module has no non-standard-library runtime dependencies.

In v2, use github.com/faustbrian/go-capability/v2/adapters/http for net/http integration and github.com/faustbrian/go-capability/v2/adapters/memory for process-local replay and revocation state. The v2/caphttp and v2/memory facades remain alongside the canonical adapters; existing published v1 paths remain subject to their documented compatibility interval.

Run make clean-consumer for an isolated external-module rehearsal of owned v2 source. Its temporary local replacement is not public release resolution or published-consumer proof; publication needs a separate no-replacement check. make interoperability reproduces the HMAC golden token with Python's independent standard-library implementation.

Issue and verify

key := []byte("a 32-byte-or-longer secret belongs outside source")
signer, _ := capability.NewHMACSHA256Signer("2026-08", key)
verifier, _ := capability.NewHMACSHA256Verifier(key)

payload := capability.Payload{
    Version: 1,
    Issuer: "https://issuer.example",
    Audiences: []string{"download-service"},
    Bearer: true,
    Resource: "documents/report-42",
    Operation: "download",
    IssuedAt: now,
    NotBefore: now,
    ExpiresAt: now.Add(5 * time.Minute),
    ID: "cap-42",
    MaxUses: 1,
}
token, _ := capability.Issue(ctx, payload, signer, capability.DefaultLimits())

keys, _ := capability.NewKeySet([]capability.Key{{Issuer: "https://issuer.example", ID: "2026-08", Verifier: verifier}})
grant, _ := capability.Verify(ctx, token, keys, capability.VerifyOptions{
    Issuer: "https://issuer.example", Now: now, Skew: time.Minute, Limits: capability.DefaultLimits(),
})

err := grant.Authorize(capability.Use{
    Issuer: "https://issuer.example",
    Audience: "download-service",
    Resource: "documents/report-42",
    Operation: "download",
})

Parse only establishes canonical structure. Verify authenticates the token, checks the explicitly selected issuer and key owner, time, key lifecycle, and optional revocation policy. Authorize checks the explicit attempted issuer and the concrete resource operation. A bounded capability must additionally call Grant.Consume against an atomic store before performing the protected side effect.

Signed URLs

Signed URLs use the same capability payload and key policy. The URL profile fixes schemes, authorities, allowed query names, the signature parameter, the method, and whether a SHA-256 body digest is required. Duplicate parameters, fragments, traversal segments, encoded slashes, userinfo, authority changes, and insecure scheme changes are rejected.

profile := capability.URLProfile{
    Name: "download-v1",
    SignatureParameter: "cap",
    AllowedSchemes: []string{"https"},
    AllowedAuthorities: []string{"files.example"},
    QueryParameters: []string{"download"},
}

payload.Resource = ""
payload.Operation = ""
signed, _ := capability.SignURL(ctx, payload, capability.URLRequest{
    Method: "GET",
    RawURL: "https://files.example/report/42?download=1",
}, profile, signer, capability.DefaultLimits())

Absolute URL schemes and authorities are allowlisted. Relative URLs are accepted only when AllowRelative is explicit. Query names and values are encoded with url.Values.Encode; exactly one value per query name is allowed.

Replay and revocation

MaxUses == 0 means reusable. Positive limits require a ConsumptionStore whose Consume operation atomically commits only while the count remains below the signed maximum for the authenticated issuer/capability-ID tuple. Missing issuer is rejected. Replay exhaustion and identity conflict are terminal; other storage errors have an unknown outcome and are returned as ErrConsumptionUnknown. Do not retry the business side effect blindly.

Durable next-major adoption requires explicit legacy ownership and fencing all old writers. PostgreSQL's schema-v2 migration is caller-transaction-owned; Valkey's required LegacyIssuer preserves its owner's exact old keys and quotas. See replay migration guidance.

adapters/memory.ConsumptionStore and adapters/memory.Revocations are process-local adapters. They are suitable only when one process owns all decisions. They do not provide cluster coordination or instant global revocation.

Both memory paths now default to 10,000 records and 4 MiB of retained key strings per store, with explicit positive StoreLimits constructors for other finite budgets. Revocations share one allowance across all five maps and never evict. Handle administrative insertion errors; ErrCapacity from Grant.Consume means no use committed. See finite admission and cleanup ownership.

Revocation checks can match capability ID, signing key ID, subject, exact issuer/tenant/resource, or an issuer-wide issued-before cutoff. Remote stores must document their consistency and maximum stale-acceptance window.

Key rotation and remote resolution

KeySet binds every key ID to exactly one algorithm and preserves explicit disabled, revoked, not-before, and not-after state. Include old and new keys during a planned overlap, issue only with the new signer, then disable or remove the old verifier after all old capabilities expire.

BoundedResolver constrains a caller-provided remote source by algorithm, key-ID length, and deadline. The source must honor context cancellation. Key material must never be placed in tokens, URLs, errors, logs, traces, fixtures, or metrics.

Canonical v1 contract

Tokens use canonical JSON, unpadded base64url, explicit algorithms and key IDs, bounded parsing, and byte-preserving UTF-8 semantics. See the protocol and threat model for the complete wire contract.

Documentation

Use the documentation index for the protocol, API, deployment, conformance, replay, revocation, security, and adoption guidance. Material standards interpretations and local protocol policy are recorded in the specification decision register.

For ecosystem-wide package selection, construction, ownership, and lifecycle guidance, see the versioned Golib ecosystem index and its Service edge family.

Development

make check and make ci use the published tool pinned in .golib.yaml for bounded verification, retaining PostgreSQL/Valkey fixtures, the integration tag, the one-use atomic-consumption race and the owned protocol/Python HMAC guard. CI requires both its shared workflow and protocol job. Base-free checks run fuzz seeds; changed-source CI selects active smoke. make check-full retains full campaigns. See contributor verification for isolated tests, installation and explicit release rehearsal.

License

MIT. See LICENSE.

Documentation

Overview

Package capability issues and verifies local cap1/v1 grants. Start with DefaultLimits, an explicit signer and issuer-owned NewKeySet. Parsing, Verify, Grant.Authorize and Grant.Consume are separate trust boundaries: verification alone does not authorize business effects or consume a use.

Core code owns no network, database, worker or clock lifecycle and adds no cgo. Optional HTTP/memory/PostgreSQL/Valkey packages compose caller-owned resources. Tokens encode visible bearer authority, not encryption, JWT/JWS or RFC 9421. Local policies and pinned standards are in docs/specification-decisions.md.

Constructors copy secrets without secure erasure. Maps/slices and callbacks stay borrowed during calls: use safe immutable collaborators and bounded context-aware I/O. Cancellation cannot forcibly preempt synchronous callbacks.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrProofTransferFailed redacts a failed or panicking custody callback.
	ErrProofTransferFailed = errors.New("capability: proof transfer failed")
	// ErrCapacity reports refusal before state insertion or consumption. Trusted
	// adapters must only return this classification when no use was committed.
	ErrCapacity = errors.New("capability: state capacity reached")
	// ErrInvalidConfiguration reports an unsafe or incomplete caller policy.
	ErrInvalidConfiguration = errors.New("capability: invalid configuration")
	// ErrInvalidPayload reports a malformed or semantically invalid payload.
	ErrInvalidPayload = errors.New("capability: invalid payload")
	// ErrNonCanonical reports a valid representation that is not the unique v1 encoding.
	ErrNonCanonical = errors.New("capability: non-canonical encoding")
	// ErrInvalidToken reports malformed token framing or protected metadata.
	ErrInvalidToken = errors.New("capability: invalid token")
	// ErrInvalidSignature reports that the protected token did not authenticate.
	ErrInvalidSignature = errors.New("capability: invalid signature")
	// ErrSigningFailed reports a signer failure without exposing its diagnostic.
	ErrSigningFailed = errors.New("capability: signing failed")
	// ErrKeyResolution reports a resolver failure without exposing its diagnostic.
	ErrKeyResolution = errors.New("capability: key resolution failed")
	// ErrAlgorithmMismatch reports an algorithm and key-type mismatch.
	ErrAlgorithmMismatch = errors.New("capability: algorithm mismatch")
	// ErrUnknownKey reports an unresolved key identifier.
	ErrUnknownKey = errors.New("capability: unknown key")
	// ErrKeyDisabled reports a key that policy has disabled without revoking it.
	ErrKeyDisabled = errors.New("capability: key disabled")
	// ErrKeyRevoked reports a key that policy has revoked.
	ErrKeyRevoked = errors.New("capability: key revoked")
	// ErrKeyNotActive reports a key outside its configured activation interval.
	ErrKeyNotActive = errors.New("capability: key not active")
	// ErrNotYetValid reports a capability used before its validity interval.
	ErrNotYetValid = errors.New("capability: not yet valid")
	// ErrExpired reports a capability used after its validity interval.
	ErrExpired = errors.New("capability: expired")
	// ErrUnauthorized reports that a verified grant does not cover a requested use.
	ErrUnauthorized = errors.New("capability: unauthorized use")
	// ErrInvalidURL reports a URL or profile with ambiguous or unsafe semantics.
	ErrInvalidURL = errors.New("capability: invalid URL")
	// ErrURLBinding reports a valid capability presented for a different URL use.
	ErrURLBinding = errors.New("capability: URL binding mismatch")
	// ErrReplayExhausted reports a capability whose bounded uses were consumed.
	ErrReplayExhausted = errors.New("capability: use limit exhausted")
	// ErrReplayConflict reports inconsistent state for one capability identity.
	ErrReplayConflict = errors.New("capability: replay state conflict")
	// ErrConsumptionUnknown reports a store failure whose commit outcome is unknown.
	ErrConsumptionUnknown = errors.New("capability: consumption outcome unknown")
	// ErrRevoked reports authority matched by a configured revocation boundary.
	ErrRevoked = errors.New("capability: revoked")
	// ErrRevocationUnknown reports a revocation check that could not complete.
	ErrRevocationUnknown = errors.New("capability: revocation status unknown")
	// ErrAdapterProtocol reports a malformed durable-adapter response.
	ErrAdapterProtocol = errors.New("capability: adapter protocol violation")
)

Functions

func CanonicalPayload

func CanonicalPayload(payload Payload, limits Limits) ([]byte, error)

CanonicalPayload returns owned local fixed-order JSON under DEC-003. It copies/sorts audiences and maps before admission, rejecting duplicate audiences and invalid text/time/bounds. Unicode bytes are not normalized; equivalent Unicode spellings remain different authority. This is not RFC 8785 canonicalization or authentication.

func Issue

func Issue(ctx context.Context, payload Payload, signer Signer, limits Limits) (string, error)

Issue validates/signs local cap1/v1 under DEC-001–003 with non-nil ctx/signer. Payload collections are copied, but signer methods remain trusted synchronous code with stable key IDs no longer than 256 bytes. It generates no ID or randomness. Signatures must be 1–512 bytes; output length is checked after encoding. Signer errors retain only ErrSigningFailed and safe context categories; panics/typed nil are not contained.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/faustbrian/go-capability/v2"
)

func main() {
	now := time.Date(2026, 8, 9, 12, 0, 0, 0, time.UTC)
	key := []byte("0123456789abcdef0123456789abcdef")
	signer, _ := capability.NewHMACSHA256Signer("example-key", key)
	verifier, _ := capability.NewHMACSHA256Verifier(key)
	payload := capability.Payload{
		Version: 1, Issuer: "https://issuer.example",
		Audiences: []string{"download"}, Bearer: true,
		Resource: "reports/42", Operation: "download",
		IssuedAt: now, NotBefore: now, ExpiresAt: now.Add(time.Minute),
		ID: "example-capability",
	}
	token, _ := capability.Issue(context.Background(), payload, signer, capability.DefaultLimits())
	keys, _ := capability.NewKeySet([]capability.Key{{Issuer: "https://issuer.example", ID: "example-key", Verifier: verifier}})
	grant, _ := capability.Verify(context.Background(), token, keys, capability.VerifyOptions{Issuer: "https://issuer.example",
		Now: now, Limits: capability.DefaultLimits(),
	})
	err := grant.Authorize(capability.Use{Issuer: "https://issuer.example",
		Audience: "download", Resource: "reports/42", Operation: "download",
	})
	fmt.Println(err == nil)
}
Output:
true

func SignURL

func SignURL(
	ctx context.Context,
	payload Payload,
	request URLRequest,
	profile URLProfile,
	signer Signer,
	limits Limits,
) (string, error)

SignURL owns empty Payload.Resource/Operation, canonicalizes the request and adds copied reserved profile/digest caveats, which must be absent. No caller collection is mutated. The returned URL contains visible bearer credentials; callers prevent disclosure. It generates no ID, reads no body and performs no egress (DEC-006/007).

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/faustbrian/go-capability/v2"
)

func main() {
	now := time.Date(2026, 8, 9, 12, 0, 0, 0, time.UTC)
	signer, _ := capability.NewHMACSHA256Signer(
		"example-key", []byte("0123456789abcdef0123456789abcdef"),
	)
	payload := capability.Payload{
		Version: 1, Issuer: "https://issuer.example",
		Audiences: []string{"download"}, Bearer: true,
		IssuedAt: now, NotBefore: now, ExpiresAt: now.Add(time.Minute),
		ID: "example-url",
	}
	profile := capability.URLProfile{
		Name: "download-v1", SignatureParameter: "cap",
		AllowedSchemes:     []string{"https"},
		AllowedAuthorities: []string{"files.example"},
		QueryParameters:    []string{"download"},
	}
	signed, _ := capability.SignURL(context.Background(), payload, capability.URLRequest{
		Method: "GET", RawURL: "https://files.example/report/42?download=1",
	}, profile, signer, capability.DefaultLimits())
	fmt.Println(len(signed) > len("https://files.example/report/42?download=1"))
}
Output:
true

Types

type Algorithm

type Algorithm string

Algorithm identifies a reviewed signature construction and its required key type.

const (
	// HMACSHA256 is HMAC with SHA-256 and keys of at least 256 bits.
	HMACSHA256 Algorithm = "hmac-sha256"
	// Ed25519 is the standard-library Ed25519 signature scheme.
	Ed25519 Algorithm = "ed25519"
)

type BoundedResolver

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

BoundedResolver constrains remote key lookups by algorithm, identifier size, and deadline. Construct with NewBoundedResolver; zero is unusable. It owns no cache or goroutine and cannot forcibly stop a source ignoring context. Shared use requires safe borrowed source/verifier methods.

func NewBoundedResolver

func NewBoundedResolver(options BoundedResolverOptions) (*BoundedResolver, error)

NewBoundedResolver copies a nonempty unique algorithm allowlist while borrowing Source. Invalid bounds return nil and ErrInvalidConfiguration. It does not call the source, start workers or create a timer until Resolve.

func (*BoundedResolver) Resolve

func (resolver *BoundedResolver) Resolve(ctx context.Context, keyID string, algorithm Algorithm) (ResolvedKey, error)

Resolve admits key/algorithm before one timed source call and rechecks issuer text plus verifier binding. Verify still owns lifecycle and selected-issuer equality. Source errors retain safe key/algorithm/context categories, not diagnostics. It does not forcibly interrupt or independently reject a source returning successfully after its deadline.

type BoundedResolverOptions

type BoundedResolverOptions struct {
	// Source is mandatory borrowed policy; it must honor context and return promptly.
	Source Resolver
	// Timeout must be positive and bounds a cooperative source, not arbitrary callback
	// execution.
	Timeout time.Duration
	// AllowedAlgorithms must be nonempty/unique built-in constructions and is copied into
	// an allowlist.
	AllowedAlgorithms []Algorithm
	// MaxKeyIDBytes must be positive and at most 256, checked before a source call.
	MaxKeyIDBytes int
}

BoundedResolverOptions limits a caller-provided remote key source. Source must honor context cancellation; this adapter creates no hidden goroutines.

type Consumption

type Consumption struct {
	// Issuer is required authenticated namespace; never merge equal IDs across issuers.
	Issuer string
	// CapabilityID is required exact signed identity, not an application transaction ID.
	CapabilityID string
	// ExpiresAt is signed expiry bound; each store applies its own clock and supported
	// precision.
	ExpiresAt time.Time
	// MaxUses must be positive for direct store calls and remains immutable for live
	// identity.
	MaxUses uint32
}

Consumption binds an exact authenticated issuer and capability ID to one immutable expiry and maximum-use bound. Issuer is required; stores must not merge different issuer namespaces even when their capability IDs match.

type ConsumptionResult

type ConsumptionResult struct {
	// Use is the one-based committed ordinal for bounded uses; zero for reusable grants.
	Use uint32
	// Remaining is the allowance after this committed use, not a reservation for future
	// operations.
	Remaining uint32
	// Reusable is true only for the core unbounded-use shortcut; Use/Remaining are then
	// zero.
	Reusable bool
}

ConsumptionResult is the committed use ordinal and remaining allowance.

type ConsumptionStore

type ConsumptionStore interface {
	// Consume atomically compares exact issuer/ID, live expiry/max and count before
	// increment. Honor ctx; document server clock/precision, retention and unknown
	// commit windows.
	Consume(context.Context, Consumption) (ConsumptionResult, error)
}

ConsumptionStore atomically increments a capability only when its committed use count remains below MaxUses. ErrCapacity, like ErrReplayExhausted and ErrReplayConflict, asserts that no use was committed. Custom adapters are trusted to return those classifications only for known no-consume outcomes. Any non-policy error may represent an unknown commit outcome and callers must fail closed rather than retry blindly.

type ConsumptionStoreFunc

type ConsumptionStoreFunc func(context.Context, Consumption) (ConsumptionResult, error)

ConsumptionStoreFunc adapts a function to ConsumptionStore.

func (ConsumptionStoreFunc) Consume

func (function ConsumptionStoreFunc) Consume(ctx context.Context, consumption Consumption) (ConsumptionResult, error)

Consume implements ConsumptionStore.

type Grant

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

Grant is authenticated capability authority. Its contents are copied so caller mutation cannot widen a previously verified grant. Zero Grant cannot authorize or consume. Value copies support concurrent reads with immutable Use inputs; they do not recheck time, revocation or key policy. Reverify before later effects as application policy requires.

func Verify

func Verify(ctx context.Context, token string, resolver Resolver, options VerifyOptions) (Grant, error)

Verify reparses/authenticates the token, returning zero Grant on failure. Trusted selected issuer must match claim and key owner before cryptography. Key activation is inclusive/exclusive at unskewed Now; payload expiry is exclusive with Skew. Revocation runs after crypto/time success. Errors strip collaborator diagnostics; callbacks remain borrowed/cooperative. Authorize and Consume remain explicit; no cache/worker is added (DEC-004/005/009/010).

func VerifyURL

func VerifyURL(
	ctx context.Context,
	request URLRequest,
	profile URLProfile,
	resolver Resolver,
	options VerifyOptions,
) (Grant, error)

VerifyURL authenticates the embedded capability and independently binds canonical method/URL/profile/digest. Success removes reserved URL caveats from its copied Grant; application dimensions still require Authorize and explicit Consume. Refusal returns zero Grant. It neither reads bodies nor derives trusted issuer/origin from forwarding headers.

func (Grant) Authorize

func (grant Grant) Authorize(use Use) error

Authorize compares exact required authority under DEC-005. Any mismatch returns ErrUnauthorized; extra use caveats are ignored and bearer grants do not check subject. No clock, consumption, revocation or protected business operation runs here.

func (Grant) Consume

func (grant Grant) Consume(ctx context.Context, store ConsumptionStore) (ConsumptionResult, error)

Consume forwards authenticated issuer/ID/expiry/max for one atomic bounded use; reusable grants need no store. Non-nil ctx and verified grant are required. It performs no Authorize, clock or revocation recheck, and trusts successful custom results. Store errors retain only no-consume policy/capacity/context categories or ErrConsumptionUnknown, not arbitrary graphs/text. Callbacks/typed nil remain trusted; business effects are not coupled (DEC-008).

func (Grant) EnvelopeDigest added in v2.1.0

func (grant Grant) EnvelopeDigest() [32]byte

EnvelopeDigest returns SHA-256 of the complete canonical token authenticated by Verify, including its signature. The value is immutable, retains no bearer bytes, and is zero for a zero Grant. It differs from Reference.Digest and is not a keyed replay identifier, consumption receipt, or fresh validity evidence. Matching this metadata never replaces signature, authority, or lifecycle checks.

func (Grant) Header

func (grant Grant) Header() Header

Header returns the authenticated key and algorithm metadata.

func (Grant) Payload

func (grant Grant) Payload() Payload

Payload returns a defensive copy of the authenticated payload.

type Header struct {
	// Version must be 1 for local cap1 framing.
	Version int `json:"v"`
	// Type must be capability, not a JWT/JWS media type.
	Type string `json:"typ"`
	// Algorithm is protected but cannot select a different trusted verifier construction.
	Algorithm Algorithm `json:"alg"`
	// KeyID is protected public key identity with a fixed 256-byte canonical-header
	// ceiling.
	KeyID string `json:"kid"`
}

Header is protected token metadata. Algorithm is authenticated but is also checked against the trusted verifier returned by Resolver.

type Key

type Key struct {
	// Issuer is required independently established key ownership, never inferred from a
	// token.
	Issuer string
	// ID is required globally unique exact key identity with a 256-byte ceiling.
	ID string
	// Verifier is mandatory borrowed trusted code; concurrent resolution/use requires it to
	// be safe.
	Verifier Verifier
	// Disabled refuses verified use without marking compromise.
	Disabled bool
	// Revoked refuses verified use regardless of Disabled.
	Revoked bool
	// NotBefore is inclusive unskewed activation; zero is open.
	NotBefore time.Time
	// NotAfter is exclusive unskewed expiry; zero is open, otherwise must follow nonzero
	// NotBefore.
	NotAfter time.Time
}

Key is immutable verifier policy for one globally unique key ID owned by exactly one explicit issuer namespace.

type KeySet

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

KeySet is an immutable in-process resolver suitable for explicit rotation overlap. Construct with NewKeySet; zero Resolve returns ErrUnknownKey. Policy values are copied, Verifiers remain borrowed and must support concurrent use. Publish a new set for rotation; no refresh, worker or close lifecycle is owned.

func NewKeySet

func NewKeySet(keys []Key) (*KeySet, error)

NewKeySet copies nonempty key policy into a snapshot but borrows Verifier objects. Duplicate globally unique IDs are rejected even across algorithms. No key-count cap or typed-nil detection is added; invalid identity/lifecycle returns nil and ErrInvalidConfiguration.

func (*KeySet) Resolve

func (set *KeySet) Resolve(ctx context.Context, keyID string, algorithm Algorithm) (ResolvedKey, error)

Resolve returns copied local policy plus its borrowed Verifier. Non-nil ctx is required; missing keys return ErrUnknownKey and algorithm mismatch returns ErrAlgorithmMismatch. It does not enforce key activation/revocation or selected issuer equality; Verify owns those checks.

type Limits

type Limits struct {
	// MaxTokenBytes caps encoded payload input/output and complete token bytes; positive,
	// not an exact heap quota.
	MaxTokenBytes int
	// MaxFieldBytes caps bounded text fields; positive. Protected key IDs retain a separate
	// 256-byte ceiling.
	MaxFieldBytes int
	// MaxAudiences is the positive maximum audience count.
	MaxAudiences int
	// MaxCaveats is the maximum caveat count; zero permits none, negative is invalid.
	MaxCaveats int
	// MaxCaveatBytes is the positive byte limit for each caveat value.
	MaxCaveatBytes int
	// MaxLifetime is positive; expiry minus the earlier issued/not-before second must fit
	// its whole seconds.
	MaxLifetime time.Duration
	// MaxUses is the positive ceiling on signed bounded use counts; payload zero still
	// means reusable.
	MaxUses uint32
}

Limits defines explicit payload/token admission. Zero is invalid; start with DefaultLimits. CanonicalPayload copies collections before validation, and URL profile counts remain caller-owned. Finite defaults are not an exact heap quota; very large custom limits require allocation/arithmetic review.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits returns the reviewed v1 resource limits.

type Parsed

type Parsed struct {
	// Header is decoded untrusted metadata, not trusted resolver policy.
	Header Header
	// Payload is decoded untrusted authority with caller-owned slices/maps; changing it
	// does not verify anything.
	Payload Payload
	// contains filtered or unexported fields
}

Parsed is a structurally valid canonical token. It is not authenticated and grants no authority until passed to Verify. Exported decoded fields remain untrusted and mutable; returned collections belong to the caller. Verify reparses token text rather than trusting this value.

func Parse

func Parse(token string, limits Limits) (Parsed, error)

Parse bounds complete input before splitting/decoding and requires canonical decoded header/payload JSON, without authenticating authority. Refusal returns zero Parsed. Go Strict RawURLEncoding rejects padding/nonzero trailing bits but permits CR/LF during decoding; this is not a separate unique-token-text re-encoding check.

type Payload

type Payload struct {
	// Version must be local wire version 1, independent of the Go module major.
	Version int
	// Issuer is required exact issuer text; verification separately binds trusted key
	// ownership.
	Issuer string
	// Audiences is required borrowed text; canonicalization copies/sorts it and rejects
	// duplicate values.
	Audiences []string
	// Subject is the exact independently authenticated subject; it is mutually exclusive
	// with Bearer.
	Subject string
	// Bearer permits use without subject matching and requires an empty Subject; tokens
	// remain transferable.
	Bearer bool
	// Resource is required exact application authority; SignURL owns it instead.
	Resource string
	// Operation is required exact application operation; SignURL substitutes its HTTP
	// method.
	Operation string
	// IssuedAt encodes nonnegative Unix seconds; subsecond precision is discarded.
	IssuedAt time.Time
	// NotBefore encodes nonnegative Unix seconds; it may precede IssuedAt, but expiry must
	// follow both.
	NotBefore time.Time
	// ExpiresAt is exclusive signed Unix-second expiry; Verify applies caller skew
	// separately.
	ExpiresAt time.Time
	// ID is required public capability identity; callers generate collision-resistant
	// values per issuer.
	ID string
	// Tenant is exact optional authority; empty is an explicit empty boundary.
	Tenant string
	// CorrelationID is optional diagnostic attribution, never authorization.
	CorrelationID string
	// MaxUses zero is reusable; positive requires explicit atomic consumption, not just
	// verification.
	MaxUses uint32
	// Caveats is an optional borrowed map of exact required pairs; Use may supply extras.
	Caveats map[string]string
}

Payload is the authority encoded by a v1 capability. Subject and Bearer are mutually exclusive. MaxUses zero means reusable; a positive value is an upper bound enforced by a replay store, not by signature verification alone. Zero is invalid. Collections are borrowed during calls and must not be concurrently mutated; times encode UTC Unix seconds, discarding fractions.

func ParsePayload

func ParsePayload(encoded []byte, limits Limits) (Payload, error)

ParsePayload bounds input before decoding and accepts only exact local canonical JSON. Re-encoding rejects alternate order, duplicates, whitespace and trailing data. Returned slices/maps are owned; refusal returns zero Payload. It authenticates nothing (DEC-003).

type Proof added in v2.1.0

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

Proof owns canonical token bytes through shared private lifetime state. Copies alias that state; zero and transferred values are invalid. Transfer clears owned/borrowed allocation capacity, not malicious caller copies or arbitrary runtime memory. Concurrent Transfer/Verify is safe.

func ParseProof added in v2.1.0

func ParseProof(token string, limits Limits) (Proof, error)

ParseProof bounds ingress through Parse before copying canonical token bytes. Parsing establishes structure only, not signature, current validity or use authority. Failure returns a zero proof. Limits must be explicitly valid.

func (Proof) CommandDigest added in v2.1.0

func (proof Proof) CommandDigest(key [32]byte, fieldPath string, maximumBytes int) ([32]byte, error)

CommandDigest binds the exact original canonical token, including signature, to a trusted coordinator's versioned secret key and field path without exposing or transferring custody. Supply a nonzero 32-byte key, an exact 1..4096-byte valid UTF-8 path and a positive maximumBytes chosen by the consumer's trusted transport profile. Parser limits remain unchanged; use Limits.MaxTokenBytes to retain a configured ingress profile. No normalization, key selection, signature/time/revocation check or consumption occurs.

Framing matches identity-command-v1: ASCII "identity-command-secret-v1" without NUL, big-endian uint32 path byte length, path bytes, big-endian uint64 token byte length and exact token bytes, authenticated with HMAC-SHA-256. The caller owns the outer command fingerprint, stored key version and retained-key recovery. This digest is sensitive correlation data, not a capability, reference digest, current authority or consumption receipt. Never expose a digest oracle or log the key, token or returned binding.

Invalid key/path/maximum returns zero and ErrInvalidConfiguration. Zero, transferred or oversized proofs return zero and ErrInvalidToken, retaining original custody on failure. Copies share the existing lifetime: a snapshot acquired before Transfer may finish, but later observations fail. No callback or hashing runs under the custody lock. Work/allocation is bounded by the admitted token size; the private snapshot is cleared on every exit. Complete erasure of cryptographic state/runtime memory cannot be guaranteed. Concurrent calls and Transfer are safe; no IO, clocks, caches or goroutines are owned.

Example
package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/faustbrian/go-capability/v2"
)

func main() {
	// The published synthetic fixture and demonstration key are not production
	// credentials. Trusted composition supplies its private versioned key.
	bytes, err := os.ReadFile("testdata/v1-hmac.token")
	if err != nil {
		return
	}
	proof, err := capability.ParseProof(strings.TrimSpace(string(bytes)), capability.DefaultLimits())
	if err != nil {
		return
	}
	digest, err := proof.CommandDigest([32]byte{1}, "password-reset-proof", capability.DefaultLimits().MaxTokenBytes)
	fmt.Println(err == nil && digest != ([32]byte{}))
	fmt.Println(proof.ValidateBounds(32, 4096) == nil)
}
Output:
true
true

func (Proof) Format added in v2.1.0

func (Proof) Format(state fmt.State, _ rune)

Format redacts every fmt verb, including pointer and container formatting.

func (Proof) LogValue added in v2.1.0

func (Proof) LogValue() slog.Value

LogValue prevents structured log reflection from exposing private storage.

func (Proof) Transfer added in v2.1.0

func (proof Proof) Transfer(ctx context.Context, callback func(context.Context, []byte) error) (err error)

Transfer invalidates every alias before invoking one synchronous callback outside locks. The callback borrows bytes only until it returns; all exposed capacity is cleared on success, error or panic. Failures never retry, consume authority or imply transport success. Pre-cancellation preserves custody. Explicit transport copies remain the caller's secret-custody responsibility.

func (Proof) ValidateBounds added in v2.1.0

func (proof Proof) ValidateBounds(minimum, maximum int) error

ValidateBounds checks inclusive canonical token byte bounds without copying, exposing, transferring or consuming the proof. Both bounds must be positive and minimum must not exceed maximum; invalid bounds return ErrInvalidConfiguration. Zero, transferred or out-of-range proofs return ErrInvalidToken. Consumer bounds do not change ParseProof's configured limits. This is a synchronized custody observation, not authentication or authority: concurrent Transfer may invalidate the proof immediately after success.

type Purpose added in v2.1.0

type Purpose string

Purpose names one literal operation. Dots are punctuation, not hierarchy; no wildcard, prefix, case folding or cross-purpose matching is supported. Its zero value is invalid.

func NewPurpose added in v2.1.0

func NewPurpose(value string) (Purpose, error)

NewPurpose accepts 1..128 ASCII bytes matching [A-Za-z0-9][A-Za-z0-9._-]*. It never normalizes caller input.

type Reference added in v2.1.0

type Reference struct {
	// CapabilityID is the original bounded grant ID, not proof of existence.
	CapabilityID string
	// Purpose is the exact literal operation bound by the producer.
	Purpose Purpose
	// Digest identifies original canonical token framing, not use authority.
	Digest [32]byte
	// ExpiresAt is the exclusive original expiry at UTC whole-second precision.
	ExpiresAt time.Time
}

Reference is non-secret lookup metadata, never authority. It is a mutable value: applications must validate reconstructed values before storage/use.

func NewReference added in v2.1.0

func NewReference(id string, purpose Purpose, digest [32]byte, expiry time.Time) (Reference, error)

NewReference validates a bounded ID, literal purpose, nonzero caller-supplied digest and positive UTC-second expiry. It cannot authenticate the digest's preimage or prove that a capability exists. Service.Issue derives its digest from the documented domain-separated canonical token framing.

type ResolvedKey

type ResolvedKey struct {
	// Issuer is the trusted namespace owning this key, not a token-derived claim.
	Issuer string
	// Verifier is mandatory borrowed trusted cryptographic code; typed nil is not detected.
	Verifier Verifier
	// Disabled refuses use without declaring the key revoked.
	Disabled bool
	// Revoked refuses all use and takes precedence over Disabled.
	Revoked bool
	// NotBefore is inclusive key activation at unskewed verification time; zero is open.
	NotBefore time.Time
	// NotAfter is exclusive key expiry at unskewed verification time; zero is open.
	NotAfter time.Time
}

ResolvedKey is trusted key-policy state returned by a Resolver.

type Resolver

type Resolver interface {
	// Resolve supplies independently trusted issuer/key policy for exact global
	// ID/algorithm; honor ctx and return safe ErrUnknownKey/ErrAlgorithmMismatch
	// categories.
	Resolve(context.Context, string, Algorithm) (ResolvedKey, error)
}

Resolver finds a trusted verifier and its current lifecycle policy. Shared implementations must be concurrency-safe, bounded and context-aware. No core worker, timeout, cache or panic boundary owns collaborator execution.

type ResolverFunc

type ResolverFunc func(context.Context, string, Algorithm) (ResolvedKey, error)

ResolverFunc adapts a function to Resolver.

func (ResolverFunc) Resolve

func (function ResolverFunc) Resolve(ctx context.Context, keyID string, algorithm Algorithm) (ResolvedKey, error)

Resolve implements Resolver.

type RevocationChecker

type RevocationChecker interface {
	// Check returns true for revoked authority, false only after successful policy
	// read. Errors fail Verify closed as ErrRevocationUnknown; document
	// propagation/cache staleness.
	Check(context.Context, RevocationQuery) (bool, error)
}

RevocationChecker checks capability, key, subject, resource, and issued-before policy. Shared implementations must be concurrency-safe, bounded and context-aware. No core worker, timeout, cache or panic boundary owns collaborator execution.

type RevocationCheckerFunc

type RevocationCheckerFunc func(context.Context, RevocationQuery) (bool, error)

RevocationCheckerFunc adapts a function to RevocationChecker.

func (RevocationCheckerFunc) Check

func (function RevocationCheckerFunc) Check(ctx context.Context, query RevocationQuery) (bool, error)

Check implements RevocationChecker.

type RevocationQuery

type RevocationQuery struct {
	// Issuer is authenticated exact namespace for every matching boundary.
	Issuer string
	// Tenant scopes resource revocation; empty remains exact.
	Tenant string
	// CapabilityID is authenticated signed ID for individual revocation.
	CapabilityID string
	// KeyID is authenticated protected key identity for issuer/key revocation.
	KeyID string
	// Subject is authenticated subject; memory skips this boundary when empty.
	Subject string
	// Resource is authenticated exact application authority, not a filesystem or egress
	// permission.
	Resource string
	// IssuedAt is signed issue time; memory issued-before cutoffs match strictly earlier
	// times.
	IssuedAt time.Time
}

RevocationQuery is the complete authenticated identity checked after signature and time validation.

type Service added in v2.1.0

type Service interface {
	// Issue signs one payload under the selected issuer and returns owned proof
	// custody plus reference metadata. Expiry must be explicit; no use is consumed.
	Issue(context.Context, Payload) (Proof, Reference, error)
	// Verify authenticates a private proof snapshot and exact purpose/audience.
	// It returns zero on failure and never consumes or transfers the proof.
	Verify(context.Context, Proof, Purpose, string) (Grant, error)
}

Service is the canonical issuing and purpose/audience verification boundary. Verification never consumes a grant or commits a protected operation.

func NewService added in v2.1.0

func NewService(config ServiceConfig) (Service, error)

NewService constructs an immutable policy holder. A nil signer, resolver or clock, invalid limits or negative skew fails with ErrInvalidConfiguration. Collaborator panics are not recovered; only Transfer callback panics are.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/faustbrian/go-capability/v2"
)

func main() {
	now := time.Unix(1786276800, 0).UTC()
	signer, err := capability.NewHMACSHA256Signer("example", make([]byte, 32))
	if err != nil {
		panic(err)
	}
	verifier, err := capability.NewHMACSHA256Verifier(make([]byte, 32))
	if err != nil {
		panic(err)
	}
	keys, err := capability.NewKeySet([]capability.Key{{Issuer: "example", ID: "example", Verifier: verifier}})
	if err != nil {
		panic(err)
	}
	service, err := capability.NewService(capability.ServiceConfig{Issuer: "example", Signer: signer, Resolver: keys, Clock: func() time.Time { return now }, Limits: capability.DefaultLimits()})
	if err != nil {
		panic(err)
	}
	proof, reference, err := service.Issue(context.Background(), capability.Payload{Version: 1, Issuer: "example", Audiences: []string{"service"}, Bearer: true, Resource: "document", Operation: "download", ExpiresAt: now.Add(time.Minute), ID: "example-capability", MaxUses: 1})
	if err != nil {
		panic(err)
	}
	grant, err := service.Verify(context.Background(), proof, reference.Purpose, "service")
	if err != nil {
		panic(err)
	}
	// This proves verification only. The protected transaction owner must
	// separately authorize and atomically consume the one-time effect.
	fmt.Println(grant.Payload().ID)
	err = proof.Transfer(context.Background(), func(_ context.Context, borrowed []byte) error {
		// Send synchronously through a confidential, no-store transport here.
		// Do not retain borrowed; explicit copies have caller-owned custody.
		fmt.Println(len(borrowed) > 0)
		return nil
	})
	fmt.Println(err == nil)
}
Output:
example-capability
true
true

type ServiceConfig added in v2.1.0

type ServiceConfig struct {
	// Issuer is independently selected trusted deployment policy, not a token
	// claim. It fixes both issuance and verification to this exact key owner.
	Issuer string
	// Signer is the borrowed mandatory issuer-owned signing collaborator.
	Signer Signer
	// Resolver supplies independently trusted key ownership and lifecycle policy.
	Resolver Resolver
	// Clock is a borrowed fast local trusted clock, invoked outside custody locks.
	Clock func() time.Time
	// Limits bounds parsing and issuance; zero selects no implicit defaults.
	Limits Limits
	// Skew is nonnegative payload time tolerance, never key-lifecycle tolerance.
	Skew time.Duration
	// Revocations optionally checks current policy after signature/time validation.
	// Nil is permitted only when deployment policy allows no revocation source.
	Revocations RevocationChecker
}

ServiceConfig pins explicit policy. Clock must be a fast local trusted clock; all collaborators must support concurrent calls if the service is shared. Limits has no implicit defaults: use DefaultLimits explicitly. Revocations may be nil only when the application's policy permits no revocation source.

type Signer

type Signer interface {
	// Algorithm returns the stable trusted construction, never untrusted caller
	// selection.
	Algorithm() Algorithm
	// KeyID returns stable public identity as valid nonempty text <=256 bytes.
	KeyID() string
	// Sign authenticates exact borrowed bytes, honoring context and returning owned
	// signature storage.
	Sign(context.Context, []byte) ([]byte, error)
}

Signer signs canonical protected bytes with an explicitly bound algorithm and key ID. Shared implementations must be concurrency-safe, bounded and context-aware. No core worker, timeout, cache or panic boundary owns collaborator execution.

func NewEd25519Signer

func NewEd25519Signer(keyID string, key ed25519.PrivateKey) (Signer, error)

NewEd25519Signer copies a standard-library sized private key and valid nonempty <=256-byte ID, returning nil/ErrInvalidConfiguration on refusal. It validates size, not key consistency/entropy. Concurrent Sign checks context before synchronous RFC 8032 work; copies are not erased.

func NewHMACSHA256Signer

func NewHMACSHA256Signer(keyID string, key []byte) (Signer, error)

NewHMACSHA256Signer copies >=32 secret bytes and valid nonempty <=256-byte key ID into a concurrent signer. Entropy is caller-owned; copies are not erased. Failure returns nil/ErrInvalidConfiguration. Sign checks context before synchronous work (RFC 2104/4231, local DEC-002).

type URLProfile

type URLProfile struct {
	// Name is required versioned local policy identity stored in a reserved caveat.
	Name string
	// SignatureParameter is required token query name and must not also be allowlisted.
	SignatureParameter string
	// AllowRelative permits path/query-only authority; the application must supply trusted
	// origin context.
	AllowRelative bool
	// AllowedSchemes is borrowed sorted/unique http/https names; empty is allowed only for
	// relative-only policy.
	AllowedSchemes []string
	// AllowedAuthorities is borrowed sorted/unique canonical lowercase ASCII authorities
	// paired with schemes.
	AllowedAuthorities []string
	// QueryParameters is borrowed sorted/unique allowed query names, excluding
	// SignatureParameter.
	QueryParameters []string
	// RequireBodyDigest requires exactly 32 caller-supplied SHA-256 bytes covered through a
	// reserved caveat.
	RequireBodyDigest bool
}

URLProfile fixes every URL component whose interpretation affects authority. QueryParameters is an allowlist; duplicate parameters are always rejected. Zero profile is invalid. Slices remain borrowed and immutable during calls/verifier lifetime. Names version local DEC-006/007, not DNS/SSRF admission, downstream authorization or provider interoperability.

func (URLProfile) Validate

func (profile URLProfile) Validate(limits Limits) error

Validate checks local names, sorted unique allowlists and canonical authorities under valid Limits. Refusal returns ErrInvalidConfiguration. It adds no defensive copies, profile slice-count cap or I/O.

type URLRequest

type URLRequest struct {
	// Method is an exact nonempty uppercase HTTP token, not normalized.
	Method string
	// RawURL is borrowed URL text; verification requires canonical presentation, signing
	// normalizes it.
	RawURL string
	// BodyDigest is borrowed precomputed SHA-256 data; omit it when the profile does not
	// require a digest.
	BodyDigest []byte
}

URLRequest contains the already selected external request representation. BodyDigest, when required, is exactly a SHA-256 digest rather than body bytes.

type Use

type Use struct {
	// Issuer must match the application-selected nonempty issuer exactly.
	Issuer string
	// Audience must equal one authenticated audience.
	Audience string
	// Subject must match subject-bound grants; bearer grants do not check it.
	Subject string
	// Resource must equal authenticated resource bytes exactly.
	Resource string
	// Operation must equal authenticated operation bytes exactly.
	Operation string
	// Tenant must equal authenticated tenant, including empty.
	Tenant string
	// Caveats is borrowed attempted-use data; every encoded caveat must match, extras are
	// ignored.
	Caveats map[string]string
}

Use describes one concrete application operation to authorize. Use fields must come from trusted application context rather than echoed token claims. Subject must be independently authenticated; Caveats stays borrowed during Authorize.

type Verifier

type Verifier interface {
	// Algorithm returns the stable trusted construction, never untrusted caller
	// selection.
	Algorithm() Algorithm
	// Verify returns nil only for authentic exact borrowed bytes; honor ctx, retain
	// no inputs and return ErrInvalidSignature on refusal.
	Verify(context.Context, []byte, []byte) error
}

Verifier authenticates canonical protected bytes with one bound algorithm.

func NewEd25519Verifier

func NewEd25519Verifier(key ed25519.PublicKey) (Verifier, error)

NewEd25519Verifier copies a standard-library sized public key, returning nil/ErrInvalidConfiguration on wrong length. Concurrent Verify checks context before synchronous work; issuer/key lifecycle remains resolver-owned.

func NewHMACSHA256Verifier

func NewHMACSHA256Verifier(key []byte) (Verifier, error)

NewHMACSHA256Verifier copies >=32 secret bytes, returning nil/ErrInvalidConfiguration for a short key. It is concurrently usable, checks ctx before work and compares MACs with hmac.Equal, not constant total latency. Keys are not erased.

type VerifyOptions

type VerifyOptions struct {
	// Issuer is selected by trusted application configuration. It is required
	// and must match both the signed claim and the resolved key's owner.
	Issuer string
	// Now is required trusted processing time; no clock is implicitly sampled.
	Now time.Time
	// Skew is nonnegative tolerance on payload validity, not key activation or store
	// expiry.
	Skew time.Duration
	// Limits is explicit finite parser policy; zero is invalid.
	Limits Limits
	// Revocations is optional borrowed policy invoked after signature/time validation; nil
	// disables that check.
	Revocations RevocationChecker
}

VerifyOptions defines trusted issuer selection, time, skew, and parser limits.

Directories

Path Synopsis
adapters
http
Package capabilityhttp integrates verified signed-URL grants with net/http without hiding application authorization or bounded-use consumption.
Package capabilityhttp integrates verified signed-URL grants with net/http without hiding application authorization or bounded-use consumption.
memory
Package capabilitymemory provides process-local replay and revocation adapters.
Package capabilitymemory provides process-local replay and revocation adapters.
Package caphttp is the compatibility facade for the target-oriented HTTP adapter.
Package caphttp is the compatibility facade for the target-oriented HTTP adapter.
Package memory is the compatibility facade for the target-oriented process-local replay and revocation adapter.
Package memory is the compatibility facade for the target-oriented process-local replay and revocation adapter.
Package postgres provides durable atomic capability consumption through database/sql and PostgreSQL row locks.
Package postgres provides durable atomic capability consumption through database/sql and PostgreSQL row locks.
Package valkey provides atomic bounded-use consumption through one Valkey EVAL operation.
Package valkey provides atomic bounded-use consumption through one Valkey EVAL operation.

Jump to

Keyboard shortcuts

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