agentprovider

package
v0.2.0 Latest Latest
Warning

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

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

Documentation

Overview

Package agentprovider implements a hosted agent provider for AAuth (draft-hardt-oauth-aauth-protocol-11 §5.1; draft-hardt-aauth-bootstrap-02).

A Server holds a provider signing key separate from every agent's key, publishes /.well-known/aauth-agent.json and its JWKS, and issues aa-agent+jwt tokens:

  • for any public key the hosting application authorizes, directly with Server.IssueAgentToken — so a token can be tied to the application's own records, such as a work session — or over HTTP for a key that proves possession under the hwk scheme (enrollment, single-key refresh; bootstrap §8.2);
  • by the two-key refresh ceremony (bootstrap §8.1): a jkt-jwt naming JWT signed by the durable key delegates to a fresh ephemeral key, which signs the request; naming JWTs are single-use;
  • for sub-agents under a hosted provider (bootstrap §9.2): the parent signs with its own agent token, and the sub-agent token has the parent's iss and ps, parent_agent naming the parent, its own key, and an exp no later than the parent's; a sub-agent cannot have sub-agents.

Every HTTP issuance goes through the caller's Registrar, and optional platform attestation through an AttestationVerifier (bootstrap §5). Server.RevokeAgentToken revokes an agent token at the agent's person server (§11.12). Client is the agent side of the same endpoints.

Index

Constants

View Source
const (
	SchemeHWK    = "hwk"
	SchemeJKTJWT = "jkt-jwt"
	// TypJKTS256 is the typ of a jkt-jwt naming JWT whose thumbprint is
	// SHA-256; its iss is urn:jkt:sha-256:<thumbprint>.
	TypJKTS256 = "jkt-s256+jwt"
)

Signature-Key schemes an agent provider accepts besides jwt (signature-key §3.4, §3.5; bootstrap §8): hwk carries a public key inline, for enrollment and single-key refresh; jkt-jwt carries a JWT signed by a durable (enclave) key that delegates to an ephemeral key, for two-key refresh.

View Source
const DefaultMaxNamingJWTLifetime = 10 * time.Minute

DefaultMaxNamingJWTLifetime is the longest naming-JWT lifetime a provider accepts unless Config.MaxNamingJWTLifetime says otherwise.

View Source
const DefaultReplayCacheEntries = 100_000

DefaultReplayCacheEntries is the capacity of a MemoryReplayCache whose MaxEntries is zero.

Variables

View Source
var ErrReplayCacheFull = errors.New("agentprovider: replay cache is full")

ErrReplayCacheFull means a ReplayCache cannot admit another entry. The provider fails closed: it refuses the request rather than forget an identifier a captured request could replay.

Functions

func AttachHWK

func AttachHWK(req *http.Request, pub crypto.PublicKey) error

AttachHWK sets Signature-Key to the hwk scheme carrying pub inline (signature-key §3.4): kty, crv, x (and y), and the fully-specified alg; no kid.

func JKTURN

func JKTURN(j aauth.JWK) string

JKTURN is the JWK Thumbprint URI of a key (signature-key §3.5): urn:jkt:sha-256:<RFC 7638 thumbprint>. It is the stable, pseudonymous identity of a durable key.

func NewNamingJWT

func NewNamingJWT(durable crypto.Signer, ephemeral crypto.PublicKey, ttl time.Duration) (string, error)

NewNamingJWT creates the jkt-jwt naming JWT of a two-key refresh (bootstrap §8.1; signature-key §3.5): signed by the durable key, whose public JWK is in the header and whose thumbprint URI is iss, delegating to the ephemeral key in cnf.jwk, with iat, exp (now + ttl), and a jti for replay protection. durable may be any crypto.Signer — typically a hardware-backed key.

func SignHWK

func SignHWK(req *http.Request, key crypto.Signer) error

SignHWK signs req with key under the hwk scheme: the request proves possession of the key it presents.

func SignJKTJWT

func SignJKTJWT(req *http.Request, namingJWT string, ephemeral crypto.Signer) error

SignJKTJWT signs req with the ephemeral key under the jkt-jwt scheme, presenting namingJWT (bootstrap §8.1 step 3).

Types

type AgentTokenParams

type AgentTokenParams struct {
	// Name is the agent identifier's local part; the domain is the
	// provider's. A sub-agent's name carries "+discriminator".
	Name string
	// Key is the agent's public key (cnf.jwk); never the provider's.
	Key aauth.JWK
	// PS is the ps claim; ParentAgent marks a sub-agent token.
	PS          string
	ParentAgent string
	// TTL is the lifetime (default Config.TokenTTL, at most 24 hours);
	// NotAfter, when set, caps exp (a sub-agent's parent token).
	TTL      time.Duration
	NotAfter time.Time
}

AgentTokenParams describes an agent token to issue.

type Attestation

type Attestation struct {
	Format string         // e.g. "webauthn", "app-attest", "play-integrity"
	Claims map[string]any // what the verifier established
}

Attestation is a verified platform attestation result (bootstrap §5).

type AttestationVerifier

type AttestationVerifier interface {
	VerifyAttestation(ctx context.Context, evidence json.RawMessage, key aauth.JWK) (*Attestation, error)
}

AttestationVerifier verifies the attestation evidence a request body carries in its "attestation" member against the key being enrolled.

type Client

type Client struct {
	// BaseURL is the provider's issuer.
	BaseURL string
	// Paths are the provider's endpoint paths (default DefaultPaths).
	Paths Paths
	// HTTPClient makes requests; nil uses http.DefaultClient.
	HTTPClient *http.Client
}

Client is the agent side of a hosted agent provider: it obtains agent tokens from a Server's endpoints. Pair it with aauth.WithTokenSource so an aauth.Agent presents provider-issued tokens:

ap := &agentprovider.Client{BaseURL: "https://ap.example"}
src := ap.TokenSource(func(ctx context.Context) (*agentprovider.TokenResponse, error) {
	return ap.Issue(ctx, key, nil)
})
agent, _ := aauth.NewAgent(id, aauth.WithKey(key), aauth.WithTokenSource(src))

func (*Client) Issue

func (c *Client) Issue(ctx context.Context, key crypto.Signer, body any) (*TokenResponse, error)

Issue requests an agent token for key, proving possession by signing the request with it under the hwk scheme (enrollment, or single-key refresh; bootstrap §8.2). body carries the provider's own enrollment fields, and attestation evidence in "attestation"; nil sends {}.

func (*Client) Refresh

func (c *Client) Refresh(ctx context.Context, durable, ephemeral crypto.Signer, body any) (*TokenResponse, error)

Refresh performs the two-key refresh (bootstrap §8.1): durable — the enrolled, typically hardware-bound key — names ephemeral in a jkt-jwt, and ephemeral signs the request. The new token binds ephemeral.

func (*Client) Subagent

func (c *Client) Subagent(ctx context.Context, parent *aauth.Agent, subPub crypto.PublicKey, discriminator string) (*TokenResponse, error)

Subagent requests a sub-agent token for the key subPub from the provider, signed by parent with its own agent token (bootstrap §9.2). discriminator may be empty to let the provider choose.

func (*Client) TokenSource

func (c *Client) TokenSource(fetch func(ctx context.Context) (*TokenResponse, error)) func() (string, error)

TokenSource caches the token fetch returns and fetches a new one when fewer than the refresh margin (aauth.DefaultRefreshMargin) remain, for use with aauth.WithTokenSource.

type Config

type Config struct {
	// Issuer is the provider's server identifier: the iss of the agent
	// tokens it issues.
	Issuer string
	// Domain is the domain of the agent identifiers it issues; default the
	// issuer's host.
	Domain string
	// Key is the provider's signing key — separate from every agent's key
	// — and KeyID its kid (default: the RFC 7638 thumbprint).
	// PublishedKeys are extra keys to publish, for rotation.
	Key           crypto.Signer
	KeyID         string
	PublishedKeys []aauth.JWK
	// Registrar enables the HTTP endpoints; without it the provider only
	// issues through [Server.IssueAgentToken].
	Registrar Registrar
	// Attestation verifies attestation evidence; without it a request
	// carrying evidence is refused.
	Attestation AttestationVerifier
	// Replay remembers naming-JWT identifiers; nil uses an in-process
	// cache.
	Replay ReplayCache
	// Limiter, when set, limits issuance per presented key ("issue:"),
	// refreshes per durable key ("refresh:"), and sub-agent requests per
	// parent ("subagent:"), answered 429 rate_limited with Retry-After.
	// It is checked after the request authenticates and before the
	// Registrar is asked.
	Limiter aauth.Limiter
	// OnIssue is called for every token issued.
	OnIssue func(ctx context.Context, t IssuedToken)
	// TokenTTL is the default token lifetime (default one hour).
	TokenTTL time.Duration
	// Paths overrides endpoint paths; Metadata supplies display members
	// and callback_endpoint, event_endpoint, and localhost_callback_allowed.
	Paths    Paths
	Metadata aauth.AgentProviderMetadata
	// SignatureWindow is the HTTP signature validity window.
	SignatureWindow time.Duration
	// MaxNamingJWTLifetime is the furthest ahead a refresh's naming JWT may
	// expire (default [DefaultMaxNamingJWTLifetime]). A longer lifetime is
	// refused, not shortened: replay state is kept until the JWT expires.
	MaxNamingJWTLifetime time.Duration
	// HTTPClient makes outbound requests (revocation); nil uses
	// aauth.DiscoveryClient, which reaches only public https destinations.
	HTTPClient *http.Client
	// Logger receives operational errors; nil discards them.
	Logger *slog.Logger
	// Now returns the current time; nil means time.Now.
	Now func() time.Time
}

Config configures a Server. Issuer and Key are required.

type IssueRequest

type IssueRequest struct {
	// Key is the presented public key; the token's cnf.jwk.
	Key aauth.JWK
	// Body is the request body (the AP's own enrollment fields).
	Body json.RawMessage
	// Attestation is the verified platform attestation, when the body
	// carried one (bootstrap §5).
	Attestation *Attestation
}

IssueRequest is an issuance request over HTTP for the key the request presented under the hwk scheme, which also signed it.

type IssuedToken

type IssuedToken struct {
	Token  string
	Claims *aauth.AgentClaims
}

IssuedToken is reported to Config.OnIssue for every agent token the provider issues: record it to revoke it later (the jti and exp) or to count a parent's live sub-agents.

type MemoryReplayCache

type MemoryReplayCache struct {
	// MaxEntries bounds the cache; zero means [DefaultReplayCacheEntries].
	MaxEntries int
	// contains filtered or unexported fields
}

MemoryReplayCache is an in-process ReplayCache. It holds at most MaxEntries unexpired identifiers; beyond that Remember fails with ErrReplayCacheFull.

func NewMemoryReplayCache

func NewMemoryReplayCache() *MemoryReplayCache

NewMemoryReplayCache returns an empty MemoryReplayCache.

func (*MemoryReplayCache) Remember

func (c *MemoryReplayCache) Remember(_ context.Context, key string, exp time.Time) (bool, error)

Remember implements ReplayCache. Expired entries are pruned at most once a minute, or when the cache is full, rather than on every call.

type Paths

type Paths struct {
	JWKS     string
	Token    string // hwk: enrollment and single-key refresh (bootstrap §8.2)
	Refresh  string // jkt-jwt: two-key refresh (bootstrap §8.1)
	Subagent string // jwt: sub-agent issuance under a hosted AP (bootstrap §9.2)
}

Paths are the URL paths the agent provider serves, relative to its issuer. The metadata document is always /.well-known/aauth-agent.json. Bootstrap defines no endpoint paths ("each AP defines its own"); these defaults live under /ap/ so an AP can share an origin with a PS and an AS.

func DefaultPaths

func DefaultPaths() Paths

DefaultPaths returns the default endpoint paths.

type RefreshRequest

type RefreshRequest struct {
	Durable     aauth.JWK
	DurableID   string // urn:jkt:sha-256:...
	Ephemeral   aauth.JWK
	Body        json.RawMessage
	Attestation *Attestation
}

RefreshRequest is a two-key refresh (bootstrap §8.1): the durable key identified by its thumbprint URI delegated to a fresh ephemeral key.

type Registrar

type Registrar interface {
	// AuthorizeIssue authorizes an hwk-signed issuance request; r is the
	// HTTP request, so the application can read its own authentication
	// (a session, a bearer credential) from it.
	AuthorizeIssue(ctx context.Context, r *http.Request, req *IssueRequest) (*Registration, error)
	// AuthorizeRefresh authorizes a two-key refresh: look up the
	// enrollment by DurableID and re-check posture.
	//
	// The provider remembers the naming JWT's identifier only after this
	// returns successfully, so that a key that is not enrolled cannot fill
	// the replay cache. A captured request that is replayed therefore
	// reaches AuthorizeRefresh again before the replay is rejected: the
	// method must be a side-effect-free lookup, or idempotent, and must not
	// consume one-time state.
	AuthorizeRefresh(ctx context.Context, req *RefreshRequest) (*Registration, error)
	// AuthorizeSubagent applies sub-agent policy: whether this parent may
	// spawn, how many may be live, for how long.
	AuthorizeSubagent(ctx context.Context, req *SubagentRequest) (*SubagentGrant, error)
}

Registrar is the hosting application's issuance policy. It decides who gets an agent token for which key, so tokens can be tied to the application's own records (an enrollment, a work session). Returning an error refuses the request (403 denied).

type Registration

type Registration struct {
	// Name is the agent identifier's local part (the domain is the
	// provider's); it must be stable for the agent install (bootstrap §6).
	Name string
	// PS is the person server, carried as the ps claim.
	PS string
	// TTL is the token lifetime (default Config.TokenTTL, at most 24h).
	TTL time.Duration
}

Registration is what the provider issues an agent token with.

type ReplayCache

type ReplayCache interface {
	// Remember records key until exp and reports whether it was new.
	Remember(ctx context.Context, key string, exp time.Time) (bool, error)
}

ReplayCache remembers naming-JWT identifiers until they expire, so a captured refresh request cannot be replayed (bootstrap §8.1).

type Server

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

Server is a hosted agent provider (draft-hardt-aauth-bootstrap-02): it publishes aauth-agent.json and its JWKS, issues agent tokens for keys it has authorized, refreshes them by the two-key jkt-jwt ceremony, issues sub-agent tokens to parents, and revokes agent tokens at person servers.

func New

func New(cfg Config) (*Server, error)

New validates cfg and returns a Server.

func (*Server) IssueAgentToken

func (s *Server) IssueAgentToken(ctx context.Context, p AgentTokenParams) (string, *aauth.AgentClaims, error)

IssueAgentToken issues an agent token for an arbitrary public key the hosting application has authorized — for example a key bound to its own session record — signed by the provider's key (draft -11 §5.3). It reports the token to Config.OnIssue.

func (*Server) Issuer

func (s *Server) Issuer() string

Issuer returns the provider's issuer.

func (*Server) JWKS

func (s *Server) JWKS() aauth.JWKS

JWKS returns the provider's published key set.

func (*Server) Metadata

func (s *Server) Metadata() aauth.AgentProviderMetadata

Metadata returns the provider's metadata document (draft -11 §11.2.1).

func (*Server) Register

func (s *Server) Register(mux *http.ServeMux)

Register adds the provider routes to mux. The issuance endpoints are registered only with a Registrar.

func (*Server) Resolver

func (s *Server) Resolver() aauth.KeyResolver

Resolver is a KeyResolver for this provider's own tokens, for verifiers collocated with it.

func (*Server) RevokeAgentToken

func (s *Server) RevokeAgentToken(ctx context.Context, ps, jti string, exp time.Time) error

RevokeAgentToken revokes an agent token the provider issued at the agent's person server (draft -11 §11.12.2: agent tokens are revoked at the PS only), discovering its revocation endpoint and signing as the provider. The PS cascades to the person and auth tokens it issued to the agent.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP serves the provider routes.

func (*Server) Signer

func (s *Server) Signer() aauth.ServerSigner

Signer returns the provider's server signer, for revocations (§11.3.2).

type SubagentGrant

type SubagentGrant struct {
	// Discriminator overrides the requested one; empty keeps it, or
	// generates one when none was requested.
	Discriminator string
	// TTL is the lifetime; never later than the parent's token.
	TTL time.Duration
}

SubagentGrant shapes an authorized sub-agent token.

type SubagentRequest

type SubagentRequest struct {
	Parent        *aauth.AgentClaims
	Key           aauth.JWK
	Discriminator string
}

SubagentRequest is a parent's request for a sub-agent token (bootstrap §9.2): the parent's verified agent token, the sub-agent's public key, and the discriminator it asked for, if any.

type TokenResponse

type TokenResponse struct {
	AgentToken string `json:"agent_token"`
	ExpiresIn  int64  `json:"expires_in"`
}

TokenResponse is the body of a successful issuance, refresh, or sub-agent response.

Jump to

Keyboard shortcuts

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