capauth

package
v1.28.21 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package capauth wires ZAP object-capability authentication into the IAM HTTP surface. First production use of the cap model — replaces bearer-JWT auth on /v1/iam/whoami (and follow-on endpoints) with typed, attenuable, revocable caps.

It lives outside the routers and controllers packages so both can depend on it without forming a cycle (routers already imports controllers).

Wire shape:

Authorization: ZAP <base64-of-Capability-bytes>

The bytes inside are the canonical Capability wire format from github.com/zap-proto/zap-spec. They are validated by cap.Wrap + (cap.Verifier).Verify — this package never reimplements verification.

Package capauth provides Hanzo IAM's ZAP capability authentication.

Layered design

This package has two coherent layers stacked on top of github.com/zap-proto/go/cap:

  • The library layer (this file, issue.go, attenuate.go, verify.go, revoke.go, keys.go, store.go) exposes friendly verbs IAM and resource-server callers use directly: Issue a root cap, Attenuate it, Verify a presented cap, Revoke it, swap the Store / Clock / Signer for tests or alternate backends.

  • The HTTP middleware layer (cap_auth.go) wraps the library layer for the Beego controllers that today live in github.com/hanzoai/iam. It parses Authorization: ZAP <b64>, stashes the verified cap on the request context, and surfaces RFC 6750-style failure modes.

The library layer is what every resource server (ATS, BD, TA, KMS, MPC) will embed. The middleware layer is IAM-flavoured (it pulls in Beego) and is kept out of the import surface of the library so resource servers can depend on capauth without dragging Beego.

Wire shape

The on-the-wire token IS a ZAP-framed cap.Capability. There is no JSON envelope, no protobuf wrapper, no "ZCAP" magic of our own. The bytes are the bytes the zap-spec defines (capabilities.zap v1.0, schema FROZEN) and the bytes the zap-proto/go/cap runtime produces. We use base64-std for HTTP transport only — the bytes inside are the truth.

Schemes

The cap runtime's Signer interface is fixed-size: every signature occupies the cap.SigSize (3408-byte) Sig footer at v1.1, with the algorithm tag at sig[cap.AlgTagOffset]. This package recognises two schemes today:

  • Scheme 1 (Ed25519): 64-byte signature in the leading bytes of the footer, scheme tag SchemeEd25519 (0x02 on the wire to align with capabilities_kinds.md "Wire schemes" Ed25519 row). The bootstrap scheme. Identity.sol's claim.scheme=1 maps here.

  • Scheme 2 (ML-DSA-65): full FIPS 204 §5.2 Level-3 signature (3309 bytes) in the leading bytes of the 3408-byte footer, scheme tag SchemeMLDSA65 (0x03). The production PQ scheme. Hanzo IAM signs caps with this once a Hanzo KMS key reference is wired in via LoadMLDSA65FromKMS. Identity.sol's claim.scheme=2 maps here.

ErrSchemeWireIncompat is retained as the sentinel error a future scheme (e.g. SLH-DSA-SHA2-256s at ~49 KB) would surface if its signature does not fit cap.SigSize. It is no longer raised by the in-tree ML-DSA-65 path.

Threat model boundary

This package handles cap minting, attenuation, presentation, and verification. It does NOT handle:

  • Holder proof-of-possession (DPoP-style binding). v1 ships without the out-of-band holderSig over a session nonce that the zap-spec describes in §2.3 step 1. Resource servers that want PoP MUST layer it on top of this package (e.g. via a CaveatBearerKey allocation in zap-spec v1.1). Until then, possession of the cap bytes is the access proof — this is a known regression versus the spec's full §2.3, called out so it can be closed in a follow-up.

  • Third-party caveats (zcap-ld §3 discharge tokens). Only first-party caveats in capabilities_kinds.md are honoured.

  • Cap rotation policy. The package issues caps; IAM decides when to rotate them.

Concurrency

Issuer, Verifier, and Store implementations are safe for concurrent use. The Clock interface is read-only and trivially safe.

Index

Constants

View Source
const (
	// CtxKeyCap stashes the verified cap.Cap so the controller can surface
	// caveats, expiry, permissions without re-parsing.
	CtxKeyCap = "zapCap"

	// CtxKeyHolderHex stashes hex-encoded Cap.Holder() — the principal the
	// whoami endpoint returns.
	CtxKeyHolderHex = "zapHolderHex"

	// CtxKeyKind stashes the cap kind so the controller can branch on
	// IAMSession vs ServiceToken etc. without poking the cap directly.
	CtxKeyKind = "zapKind"
)

Context keys used to hand off a verified cap to controllers. Reads come back through ctx.Input.GetData(key) on the controller side.

View Source
const (
	// SchemeEd25519 is the bootstrap scheme: 64-byte signature in the
	// leading bytes of the cap.SigSize (3408-byte) footer, scheme tag
	// 0x02 at cap.AlgTagOffset.
	SchemeEd25519 = cap.SchemeEd25519

	// SchemeMLDSA65 is the FIPS 204 Level-3 PQ scheme: 3309-byte
	// signature in the leading bytes of the cap.SigSize footer, scheme
	// tag 0x03 at cap.AlgTagOffset. Live at v1.1 of the ZAP wire spec
	// (zap-proto/go v1.1.0+).
	SchemeMLDSA65 = cap.SchemeMLDSA65
)

Scheme constants are re-exported from the cap runtime so consumers of this package see capauth.SchemeEd25519 without a separate import.

View Source
const ChainDepthMax = 8

ChainDepthMax bounds the cap chain a Verifier will walk. The cap runtime itself does not impose a depth limit; we layer one here because every added link costs O(verify) crypto and unbounded chains are a DoS vector. 8 is comfortably more than any legitimate IAM → org-admin → service → instance → operation chain we have today.

Variables

View Source
var (
	ErrNoZAPHeader = errors.New("zap: no Authorization: ZAP header")
	ErrBadBase64   = errors.New("zap: header value is not valid base64")
	ErrWrongKind   = errors.New("zap: cap kind does not match endpoint")
)

Errors specific to the ZAP path. Distinct from the cap package errors so HTTP responses can map them to RFC 6750–style WWW-Authenticate values.

View Source
var (
	// ErrSchemeWireIncompat indicates the requested signing scheme does
	// not fit the cap.SigSize wire footer. With sig96 → sig3408 (v1.1),
	// Ed25519 and ML-DSA-65 both fit; this sentinel remains the named
	// failure mode any future scheme (e.g. SLH-DSA, ~49 KB) MUST raise
	// rather than truncating-and-corrupting a signature.
	ErrSchemeWireIncompat = errors.New("capauth: scheme is wire-incompatible with cap.SigSize")

	// ErrChainTooDeep indicates the presented cap chain exceeds
	// ChainDepthMax. Returned by Verify on a too-long chain.
	ErrChainTooDeep = errors.New("capauth: cap chain exceeds depth limit")

	// ErrAudienceMismatch indicates the cap carries a CaveatAudience that
	// does not name the resource server doing the verification.
	ErrAudienceMismatch = errors.New("capauth: audience caveat does not match verifier identity")

	// ErrCaveatWidened indicates an attempted attenuation tried to add a
	// caveat that would relax (rather than tighten) a parent constraint.
	// This is impossible to express in the wire format — a child can only
	// add caveats, never remove parent ones — but the helper-side
	// Attenuate refuses the call early to give callers a friendly error
	// instead of "verification fails later for opaque reasons".
	ErrCaveatWidened = errors.New("capauth: attenuation would widen a parent caveat")

	// ErrCaveatUnknown indicates a caveat kind not in capabilities_kinds.md.
	// Verifiers fail-closed on unknown caveats per SPEC.md §2.3 step 4.
	ErrCaveatUnknown = errors.New("capauth: unknown caveat kind")

	// ErrPermsWidened is the helper-side mirror of cap.ErrPermsExceedPar:
	// surfaced at Attenuate time so callers learn at mint-time, not later
	// at verify-time, that they tried to grant a child more than they had.
	ErrPermsWidened = errors.New("capauth: attenuation would widen parent permissions")
)

Errors specific to the library layer. The runtime-level errors (cap.ErrExpired, cap.ErrRevoked, cap.ErrSigMismatch, …) flow through unchanged; the errors here name failure modes the runtime doesn't already have a sentinel for.

View Source
var ErrCaveatDuplicate = errors.New("capauth: extra caveat duplicates helper-managed kind")

ErrCaveatDuplicate is returned by Issue when the caller supplies an ExtraCaveat whose kind is also being emitted by a helper-managed field. Avoids the silent ambiguity of two caveats of the same kind in one cap (which the wire format allows but the verifier evaluates as AND, which is rarely what the issuer meant).

View Source
var ErrProcessNotInitialised = errors.New("capauth: process issuer not initialised")

ErrProcessNotInitialised is returned by accessors before InitProcessIssuer completes. Controllers map this to a 503 — caps cannot be minted or verified until the boot sequence has loaded the signing key from KMS.

Functions

func ApplyRevocation

func ApplyRevocation(encoded []byte, issuerPub []byte, store RevocationStore) error

ApplyRevocation parses a wire-encoded Revocation from a peer, verifies the RevokerSig against the cap's original Issuer pubkey, and applies the revocation to the local store.

This is the receiving side of the gossip protocol: when an IAM replica publishes a Revocation, every other replica (and every resource server listening to the topic) calls ApplyRevocation to bring its local store up to date.

The caller MUST supply the original issuer's public-key bytes; this package does not hold the inverse mapping (capID → issuer pub) since that's the IAM cap-store table's job. In practice the gossip message carries both the encoded Revocation AND the original cap's issuer hash, and the receiver looks up the pubkey via its IssuerRegistry.

func ClearIssuers

func ClearIssuers()

ClearIssuers wipes the in-memory issuer registry. Test-only.

func ClearRevocations

func ClearRevocations()

ClearRevocations resets the in-memory list. Test-only.

func HasHeader

func HasHeader(ctx *context.Context) bool

HasHeader reports whether the request carries an Authorization: ZAP header. Used by /v1/iam/whoami to decide whether to dispatch on the cap path or fall through to the legacy JWT path.

func Hex32

func Hex32(h [32]byte) string

Hex32 is the exported form of hex32 — callers (controllers) need it to render Issuer hashes onto the wire alongside the holder hex.

func InitProcessIssuer

func InitProcessIssuer(cfg ProcessConfig) error

InitProcessIssuer constructs the singleton Issuer + Store + Registry. Idempotent — repeated calls with the same seed return nil; repeated calls with a different seed are an error (would invalidate previously-minted caps in a deployment that didn't intend a key rotation).

The seed bytes are consumed via NewEd25519SignerFromSeed; the caller is expected to zero its own buffer once this function returns.

func ParseHeader

func ParseHeader(ctx *context.Context) ([]byte, error)

ParseHeader pulls an Authorization: ZAP <b64> header off the request and returns the decoded bytes. Returns ErrNoZAPHeader if the header is absent or uses a different scheme — the caller can then fall back to Bearer.

func ProcessContextID

func ProcessContextID() string

ProcessContextID returns the CtxID supplied to InitProcessIssuer. Empty string before init.

func ProcessIssuerPublicKey

func ProcessIssuerPublicKey() (ed25519.PublicKey, [32]byte, error)

ProcessIssuerPublicKey returns a fresh copy of the singleton's raw ed25519 public key. Suitable for serving on /v1/iam/cap/issuer-keys.

func RegisterIssuer

func RegisterIssuer(hash [32]byte, pub ed25519.PublicKey)

RegisterIssuer adds an issuer pubkey to the in-memory registry. hash MUST be cap.Hash32(pub) — i.e., SHA-256 of the raw 32-byte ed25519 pubkey. The cap package's Hash32 is the canonical hash function.

func ResetProcessForTest

func ResetProcessForTest()

ResetProcessForTest wipes the singleton. Test-only — callers MUST be in the same package, so the symbol is not exported in a way that helps foreign packages. We export it because the controller-package tests live outside capauth and need a way to roll boot state between cases.

func Revoke

func Revoke(capID [32]byte)

Revoke marks a cap as revoked. Idempotent.

func Verify

func Verify(ctx *context.Context, expectKind cap.CapKind) error

Verify is the canonical entry point: parse + wrap + verify + kind check, then stash holder/kind on the Beego context for the controller. Returns nil on success; on any failure the request is unauthenticated and the caller writes a 401 with WWW-Authenticate: ZAP error="<code>".

func VerifyMLDSA65

func VerifyMLDSA65(pubBytes, payload, ctx []byte, sig [cap.SigSize]byte) error

VerifyMLDSA65 is the verifier counterpart to MLDSA65Signer.Sign. It unpacks the leading mldsa65SignatureSize bytes of sig and calls into FIPS 204 verification with the supplied public-key encoding (pubBytes) and domain-separation context (ctx — must match what Sign used). The pad bytes between the signature and the algorithm tag are ignored.

Returns nil on success; a wrapped error on framing failure; or cap.ErrSigMismatch on a cryptographically valid-shape signature that fails the FIPS 204 §5.3 check.

Types

type AttenuateParams

type AttenuateParams struct {
	// NewHolder is the 32-byte hash of the wielder's public key the
	// child cap is for.
	NewHolder [32]byte

	// Permissions is the bitmask of operations to grant the child.
	// MUST be a subset of the parent's; cap.Attenuate intersects, so
	// passing 0xFFFF... is equivalent to "inherit all".
	Permissions uint64

	// Audience, if set, narrows the audience to a more specific
	// resource server. MUST equal the parent's audience or be the
	// child's own narrowing under the parent's. A child cap MAY add an
	// audience that the parent didn't carry (that's narrowing the
	// implicit "anyone" to "this one"), but a child MAY NOT change a
	// parent's existing audience to a different one. v1 enforces the
	// strict rule "audience may be added but never changed" — that's
	// the only safe option without a richer audience-hierarchy schema.
	Audience [32]byte

	// ExpiresAt, if non-zero, is the child's expiry. cap.Attenuate
	// floors this to the parent's; we additionally refuse to *raise*
	// it via the helper boundary so the caller's error is friendly.
	ExpiresAt int64

	// MaxDepth is the child's remaining-hops budget. 0 means "inherit
	// (parent's - 1)" if the parent carries a MaxDepth caveat, or
	// "unbounded by caveat" if the parent does not. A positive
	// MaxDepth that exceeds (parent's - 1) is ErrCaveatWidened.
	MaxDepth uint8

	// ExtraCaveats are caveats the caller wants to add beyond the
	// helper-managed kinds. Same duplicate-vs-helper rule as
	// IssueParams.
	ExtraCaveats []cap.Caveat
}

AttenuateParams describes a child cap to be derived from a parent.

The cap runtime's cap.Attenuate enforces the wire-level invariants: child's Issuer = parent's Holder, child's Permissions ⊆ parent's, expiry can only tighten. This struct + Attenuate add the helper-level invariants:

  • Audience (CaveatAudience) may only narrow from the parent. A child with the same audience or no audience is fine; a child trying to set an audience different from the parent's is rejected with ErrCaveatWidened.

  • MaxDepth (CaveatMaxDepth) is decremented automatically and refuses to go below 1 (which would forbid the child from attenuating further, a legitimate ask). A request to widen MaxDepth above the parent's remaining budget is ErrCaveatWidened.

  • ExpiresAt may only shrink (mirrors cap.Attenuate's own behaviour but enforced here too so the helper-level error is friendly).

type Clock

type Clock interface {
	Now() time.Time
}

Clock abstracts the time source for verification. Production passes SystemClock; tests pass a fixed clock so expiry edges are deterministic.

type Ed25519Signer

type Ed25519Signer = ed25519Signer

Ed25519Signer is the exported alias for the in-package signer type, kept so consumers can name the type without reaching into unexported names.

func LoadEd25519FromKMS

func LoadEd25519FromKMS(client KMSClient, keyRef string) (*Ed25519Signer, ed25519.PublicKey, error)

LoadEd25519FromKMS materialises an Ed25519Signer from a KMS-supplied 32-byte seed. The seed is expanded into an ed25519 keypair, the signer constructed, and the seed bytes wiped.

keyRef shape is the KMSClient's concern; for Hanzo KMS the canonical shape is "providers/hanzo/iam/{env}/cap-signing-{keyid}" but this package does not enforce it.

func NewEd25519Signer

func NewEd25519Signer() (*Ed25519Signer, ed25519.PublicKey, error)

NewEd25519Signer mints a fresh ed25519 keypair via crypto/rand.Reader. Returns the signer (suitable for Issuer{Signer:…, Scheme:SchemeEd25519}) and the raw 32-byte ed25519 public key bytes (suitable for registering with cap.Verifier.IssuerKey).

func NewEd25519SignerFromSeed

func NewEd25519SignerFromSeed(seed []byte) (*Ed25519Signer, ed25519.PublicKey, error)

NewEd25519SignerFromSeed materialises a signer from a 32-byte ed25519 seed (NOT the full 64-byte expanded private key). This is the canonical "load from KMS" path: the seed is what KMS stores, and ed25519.NewKeyFromSeed is the standard library's deterministic expansion to a full keypair.

The seed bytes the caller passes are NOT zeroed by this function; the caller is expected to wipe its own buffer once the signer is constructed (see LoadEd25519FromKMS for the canonical pattern).

type FixedClock

type FixedClock struct{ T time.Time }

FixedClock is a Clock that returns the same instant every call. Test-only.

func (FixedClock) Now

func (f FixedClock) Now() time.Time

Now returns the fixed instant.

type IdentityCtx

type IdentityCtx struct {
	// PrincipalHex is the lowercase-hex Hash32 of the holder pubkey
	// (or, in v1, the userID hash when the controller bound to the
	// signed-in principal instead of a device pubkey). Suitable for
	// logging, audit trails, and "who am I" responses.
	PrincipalHex string

	// ScopesBits is the cap's Permissions field. Handlers that need to
	// enforce per-route gating beyond what the middleware checked
	// inspect this bitmask.
	ScopesBits uint64

	// CapKind is the cap.CapKind the holder presented. Useful for
	// branching: a KindIAMSession cap shouldn't be doing KindKMSSign work.
	CapKind uint32

	// ChainDepth is len(chain)+1. v1 only accepts root caps (chain
	// length 0) so this is always 1 today; the field is present for
	// the day we allow attenuated caps at the edge.
	ChainDepth int

	// CapID is the cap's ID() — the SHA-256 of the wire bytes. Audit
	// trails MUST log this; on revocation it's the index key.
	CapID [32]byte
}

IdentityCtx is the verified identity attached to a request after the middleware accepts a cap.

The struct is intentionally small and concrete. We do NOT pass the raw cap.Cap here — that's available via a separate context key in the middleware package for handlers that need attenuation. Most handlers only need to know "who is this" and "what may they do", and that's PrincipalHex + ScopesBits.

type IssueParams

type IssueParams struct {
	// Kind is the CapKind from capabilities_kinds.md. Issuers MUST pass a
	// known kind; the cap runtime does not enforce a closed set, but
	// resource servers fail-closed on unknown kinds and so will Verify.
	Kind cap.CapKind

	// Target is the 32-byte BLAKE3 hash of the object this cap grants
	// over. The verifier compares this against the resource server's
	// notion of the target it serves; mismatch is ErrTargetMismatch.
	Target [32]byte

	// Holder is the 32-byte hash of the wielder's public key. The wire
	// format does not bind to the keypair unless the caller layers
	// proof-of-possession on top of this package (see CaveatBearerKey
	// note in capauth.go); v1 trusts possession of the cap bytes.
	Holder [32]byte

	// Permissions is the bitmask of operations this cap allows on
	// Target. See capabilities_kinds.md for per-Kind bit assignments.
	Permissions uint64

	// Audience, if non-zero, is the 32-byte hash of the resource server
	// this cap is intended for. Encoded as a CaveatAudience entry in the
	// minted cap. Verifiers reject when the cap's audience does not name
	// them.
	Audience [32]byte

	// NotBefore, if non-zero, is the Unix-seconds floor before which the
	// cap is not yet valid. Encoded as a CaveatNotBefore-style entry —
	// see the caveat-kind allocation note below.
	NotBefore int64

	// ExpiresAt, if non-zero, is the Unix-seconds expiry. Encoded both
	// in the cap's ExpiresAt field (for the runtime's fast-path check)
	// and as a CaveatExpiresAt entry (which the verifier ANDs over the
	// whole chain).
	ExpiresAt int64

	// MaxDepth, if non-zero, caps how many further attenuations this
	// cap may produce. Encoded as a CaveatMaxDepth entry.
	MaxDepth uint8

	// ExtraCaveats appends caveats the caller already knows about. The
	// helper-side rule is: helper-managed caveats (Audience, NotBefore,
	// ExpiresAt, MaxDepth) are emitted automatically and MUST NOT appear
	// in ExtraCaveats, or Issue returns ErrCaveatDuplicate.
	ExtraCaveats []cap.Caveat
}

IssueParams describes a single capability the issuer should mint.

The shape is deliberately distinct from cap.Issuance so the library layer can introduce policy that the wire layer doesn't carry: e.g. the Audience field below becomes a CaveatAudience entry in the underlying Issuance, but at this layer it's named for what it is.

type Issuer

type Issuer struct {
	// Signer is the cap.Signer that signs minted caps. Required.
	Signer cap.Signer
	// Scheme is the algorithm tag advertised in the cap.SigSize footer
	// (at byte cap.AlgTagOffset). The cap runtime writes the tag inside
	// the Signer.Sign call so the wire layer and the Issuer's typed
	// notion stay aligned: cap-aware callers can read back "what scheme
	// did this cap use" without re-parsing the signed bytes.
	Scheme Scheme
	// Clock supplies IssuedAt timestamps; defaults to SystemClock if nil.
	Clock Clock
}

Issuer assembles caps. The fields here are the static configuration of a minting identity (signer + scheme + clock); per-cap parameters are passed to Issue/Attenuate. Multiple Issuers may coexist (e.g. per-org IAM identity).

func ProcessIssuerHandle

func ProcessIssuerHandle() (*Issuer, error)

ProcessIssuerHandle returns the singleton Issuer or ErrProcessNotInitialised if InitProcessIssuer hasn't run.

func (*Issuer) Attenuate

func (iss *Issuer) Attenuate(parent cap.Cap, p AttenuateParams) (cap.Cap, error)

Attenuate derives a child cap from parent. The Issuer's Signer MUST be the parent's Holder's signing key — cap.Attenuate enforces this and returns ErrChainBroken if violated, but the more common failure mode at this layer is the caller forgetting to swap Signers between the "I'm minting" and "I'm delegating" call sites; the helper-level error surfaces in cap.Attenuate's return as ErrChainBroken regardless.

func (*Issuer) Issue

func (iss *Issuer) Issue(p IssueParams) (cap.Cap, error)

Issue mints a new root cap. The caller MUST hold the signing key; the Issuer's Signer is what signs the wire bytes.

Root caps have Parent = zero. For derived caps, use Attenuate.

Scheme dispatch is the Signer's responsibility: the Signer writes the algorithm tag at cap.AlgTagOffset of the SigSize footer so verifiers can pick the matching primitive. Issue itself is scheme-agnostic.

func (*Issuer) Revoke

func (iss *Issuer) Revoke(c cap.Cap, store RevocationStore) (cap.Revocation, error)

Revoke produces a signed cap.Revocation record for the supplied cap. The Issuer's Signer MUST be the cap's original issuer (cap.Revoke enforces this and returns ErrChainBroken otherwise).

The returned Revocation is the on-the-wire record. Callers SHOULD also call store.Revoke(rev.CapID) to make the revocation effective at THIS verifier, then publish the encoded bytes (via cap.EncodeRevocation) to the IAM revocation log so peers learn about it.

Two-phase contract:

  1. Local: store.Revoke(capID) — the local verifier immediately rejects further presentations of that cap.
  2. Global: encoded revocation gossiped to peers via the IAM PubSub topic, which then call store.Revoke on their side.

Step 1 is synchronous; step 2 is eventually-consistent. The CAP_MIGRATION non-negotiable "revocation must propagate globally within 5 seconds" is the SLO on step 2; this package's contribution is making step 1 atomic at each verifier.

type IssuerKeyDescriptor

type IssuerKeyDescriptor struct {
	// Scheme is the SchemeXxx value (1 = ed25519 today).
	Scheme uint8 `json:"scheme"`

	// FingerprintHex is the lowercase-hex Hash32 of the public-key bytes,
	// which is also what cap.Cap.Issuer() returns. Resource servers index
	// their registry by this hash.
	FingerprintHex string `json:"fingerprintHex"`

	// PublicKeyBase64 is the raw public-key bytes (32 for ed25519,
	// FIPS-204-canonical for ML-DSA-65) standard-base64 encoded.
	PublicKeyBase64 string `json:"publicKeyBase64"`

	// NotAfter, if non-zero, is the Unix-seconds at which this issuer key
	// is scheduled to retire — resource servers may then drop it from
	// their registry on next refresh. Zero means "no scheduled retirement".
	NotAfter int64 `json:"notAfter,omitempty"`
}

IssuerKeyDescriptor is the on-the-wire shape for /v1/iam/cap/issuer-keys. One entry per active signing key the resource servers may encounter on minted caps.

func ListIssuerKeys

func ListIssuerKeys() ([]IssuerKeyDescriptor, error)

ListIssuerKeys returns the current set of active issuer keys. v1 is the trivial one-entry list: the process singleton. When key rotation lands this returns the union of {current, prev-still-acceptable}.

Exported so the controller layer can serve it on /v1/iam/cap/issuer-keys without reaching into private state.

type IssuerRegistry

type IssuerRegistry interface {
	// Lookup returns the raw public-key bytes for the issuer whose
	// 32-byte hash is hashedPub. For ed25519, the returned bytes are
	// the 32-byte raw public key. For ML-DSA-65, the returned bytes
	// are the FIPS 204 canonical public-key encoding.
	//
	// Implementations must return cap.ErrIssuerUnknown for unknown
	// hashes — the cap runtime's Verifier.Verify is sensitive to this
	// specific sentinel.
	Lookup(hashedPub [32]byte) ([]byte, error)

	// Register associates a raw pubkey with its hash. Idempotent — a
	// repeated registration of the same (hash, pub) pair is a no-op;
	// a repeated registration with a different pub bytes panics (this
	// would indicate a hash collision, which is cryptographic-grade
	// unlikely and a sign of a broken hash function).
	Register(hashedPub [32]byte, pub []byte)
}

IssuerRegistry resolves issuer-pubkey-hash to the raw public key bytes every verifier needs to validate a cap's signature.

As with RevocationStore, the interface separates the resolution semantics from any one backing store. Lab and tests use MemoryRegistry; production wires this into the IAM key table.

func ProcessRegistryHandle

func ProcessRegistryHandle() (IssuerRegistry, error)

ProcessRegistryHandle returns the singleton IssuerRegistry.

type KMSClient

type KMSClient interface {
	// FetchEd25519Seed returns the 32-byte ed25519 seed at keyRef. NOT
	// the 64-byte expanded private key — KMS stores the seed; expansion
	// happens here, in-process, and the seed bytes are zeroed
	// immediately afterwards.
	FetchEd25519Seed(keyRef string) ([]byte, error)

	// FetchMLDSA65Private returns the canonical FIPS 204 private-key
	// encoding at keyRef. The bytes are unmarshalled in-process and the
	// raw buffer zeroed.
	FetchMLDSA65Private(keyRef string) ([]byte, error)
}

KMSClient is the narrow interface this package consumes from a KMS. We avoid pulling github.com/hanzoai/kms/sdk/go transitively into every resource server that wants to verify caps; signing is an IAM-side concern, so the interface lives here and the concrete wiring stays in IAM's controllers package.

type LibVerifier

type LibVerifier struct {
	// Store backs the IsRevoked check. Nil means "treat everything as
	// non-revoked" (only sane in tests).
	Store RevocationStore

	// Registry resolves issuer hashes to raw public-key bytes. Nil is a
	// configuration error and Verify returns ErrIssuerUnknown.
	Registry IssuerRegistry

	// Clock supplies the current time for expiry checks. Defaults to
	// SystemClock if nil.
	Clock Clock

	// Identity is the 32-byte hash of THIS resource server's public
	// identity. Compared in constant time against CaveatAudience entries
	// in the cap. Setting Identity to the zero hash disables the
	// audience check — useful for IAM itself (which mints caps for
	// anyone) and for resource servers that haven't onboarded an
	// identity yet, but production resource servers MUST set it.
	Identity [32]byte

	// MLDSA65Ctx is the FIPS 204 §5.2 domain-separation context string
	// that ML-DSA-65 signers used at issue time. The verifier passes it
	// into mldsa65.Verify so a cap minted with ctx="hanzo-iam/cap/v1"
	// only verifies under a verifier configured with the same ctx —
	// preventing cross-protocol signature replay.
	//
	// Leaving this empty is legal (matches an empty signer ctx) but
	// cryptographically discouraged for production deployments.
	MLDSA65Ctx []byte
}

LibVerifier is the library-layer verifier. It composes cap.Verifier (signature/expiry/revocation/chain-walk) with the helper-level invariants the cap runtime does not enforce on its own:

  • Audience caveats must name THIS resource server.
  • Chain depth must be ≤ ChainDepthMax (and ≤ any CaveatMaxDepth on the chain).
  • Unknown CaveatKind values fail-closed per SPEC.md §2.3 step 4.
  • Time source is pluggable via Clock for deterministic tests.

Name disambiguation: the cap runtime exports `cap.Verifier`; this package exports `capauth.LibVerifier` to avoid shadowing the upstream type. Most callers see only `Verifier` because LibVerifier is the exported alias below.

func (*LibVerifier) Verify

func (v *LibVerifier) Verify(p VerifyParams) error

Verify validates the cap chain against the verifier's static configuration and the per-call params. Returns nil on success; on any failure, the request must be treated as unauthenticated. The returned error is the most specific sentinel describing the failure mode.

type MLDSA65Signer

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

MLDSA65Signer is the FIPS 204 Level-3 PQ signer used by Hanzo IAM (and every cap-aware Hanzo service) at v1.1 of the ZAP capability wire format. A real ML-DSA-65 signature is 3309 bytes; the v1.1 sig footer is 3408 bytes (cap.SigSize), large enough to hold the full signature plus the scheme tag byte at cap.AlgTagOffset.

Wire layout produced by Sign:

out[0:3309]                = ML-DSA-65 signature bytes
out[3309:cap.AlgTagOffset] = zero pad
out[cap.AlgTagOffset]      = byte(SchemeMLDSA65) = 0x03

The tag byte sits at the end of the footer and is part of the signed payload (the signer reads it back as part of the SignedBytes scope on verify), so a tag flip changes the verification result.

func LoadMLDSA65FromKMS

func LoadMLDSA65FromKMS(client KMSClient, keyRef string, ctx []byte) (*MLDSA65Signer, []byte, error)

LoadMLDSA65FromKMS materialises an MLDSA65Signer from a KMS-supplied FIPS 204 private-key encoding. ctx is the per-deployment domain separator.

Note: the signer Sign-refuses until zap-spec v1.1; LoadMLDSA65FromKMS still succeeds so production can stage keys and exercise the boundary.

func MLDSA65SignerFromKey

func MLDSA65SignerFromKey(sk *mldsa65.PrivateKey, pubBytes []byte, ctx []byte) (*MLDSA65Signer, error)

MLDSA65SignerFromKey constructs a signer from an existing ML-DSA-65 private key (e.g. one loaded from Hanzo KMS). pubBytes is the matching FIPS 204 canonical public-key encoding.

func NewMLDSA65Signer

func NewMLDSA65Signer(ctx []byte) (*MLDSA65Signer, []byte, error)

NewMLDSA65Signer generates a fresh ML-DSA-65 keypair.

ctx is the FIPS 204 §5.2 context string baked into every signature this signer produces. Pass a per-deployment label like "hanzo-iam/cap/v1"; empty is legal but cryptographically discouraged. ctx MUST be at most 255 bytes (FIPS 204 §5.2 limit); longer values are rejected here.

func (*MLDSA65Signer) Public

func (s *MLDSA65Signer) Public() [32]byte

Public returns the 32-byte hash of the ML-DSA-65 public key.

func (*MLDSA65Signer) PublicBytes

func (s *MLDSA65Signer) PublicBytes() []byte

PublicBytes returns a fresh copy of the canonical FIPS 204 public key encoding. Safe to register directly with cap.Verifier.IssuerKey once the wire bump lands; today the signer is unusable because Sign refuses.

func (*MLDSA65Signer) Sign

func (s *MLDSA65Signer) Sign(payload []byte) ([cap.SigSize]byte, error)

Sign produces a FIPS 204 ML-DSA-65 signature over payload and packs it into the cap.SigSize (3408-byte) footer used by zap-spec v1.1:

out[0:3309]                   = mldsa65.Sign(sk, payload, ctx)
out[3309:cap.AlgTagOffset]    = zero pad
out[cap.AlgTagOffset]         = byte(SchemeMLDSA65)

The signature is randomized (randomized=true in FIPS 204 §5.2) so the caller does not need to thread its own entropy; the ML-DSA-65 design is hedged against bad randomness, but fresh entropy hardens against fault-injection. ctx (set at signer construction) is the FIPS 204 domain-separation string that prevents cross-protocol replay.

type MemoryRegistry

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

MemoryRegistry is an in-memory IssuerRegistry. Safe for concurrent use.

func NewMemoryRegistry

func NewMemoryRegistry() *MemoryRegistry

NewMemoryRegistry constructs an empty MemoryRegistry.

func (*MemoryRegistry) Clear

func (r *MemoryRegistry) Clear()

Clear wipes the registry. Test-only.

func (*MemoryRegistry) Lookup

func (r *MemoryRegistry) Lookup(hashedPub [32]byte) ([]byte, error)

Lookup returns the raw public-key bytes for hashedPub or cap.ErrIssuerUnknown.

func (*MemoryRegistry) Register

func (r *MemoryRegistry) Register(hashedPub [32]byte, pub []byte)

Register associates hashedPub with pub. Idempotent for identical (hash, pub); panics on conflicting bytes (a real hash collision would be a cryptographic emergency).

type MemoryStore

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

MemoryStore is an in-memory RevocationStore. Safe for concurrent use. Test code and lab IAM deployments use this directly; production IAM wraps a DB-backed store in the same interface.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore constructs an empty MemoryStore.

func (*MemoryStore) Clear

func (s *MemoryStore) Clear()

Clear wipes the store. Test-only.

func (*MemoryStore) IsRevoked

func (s *MemoryStore) IsRevoked(capID [32]byte) bool

IsRevoked reports whether capID has been revoked.

func (*MemoryStore) Revoke

func (s *MemoryStore) Revoke(capID [32]byte)

Revoke marks capID as revoked. Idempotent.

type ProcessConfig

type ProcessConfig struct {
	// Seed is the 32-byte ed25519 seed loaded from KMS. The caller is
	// responsible for wiping this slice after InitProcessIssuer returns.
	Seed []byte

	// CtxID is the per-deployment label baked into log lines + future
	// PQ signer context. Today only carried for traceability.
	CtxID string
}

ProcessConfig drives InitProcessIssuer.

type RevocationStore

type RevocationStore interface {
	// IsRevoked reports whether capID has been revoked. Must be safe
	// for concurrent use. Implementations should return false on lookup
	// errors; a transient storage failure must not cause caps to be
	// silently accepted as valid, so callers wrap this with a tighter
	// freshness budget at a higher layer.
	IsRevoked(capID [32]byte) bool

	// Revoke marks capID as revoked. Idempotent. Must be safe for
	// concurrent use.
	Revoke(capID [32]byte)
}

RevocationStore tracks which caps have been revoked.

The interface separates IAM's responsibility (publish a revocation, expose it for resource servers to consult) from any one storage engine. The default implementation, MemoryStore, holds an in-memory set and is the right answer for tests and single-process IAM deployments. Production IAM swaps in a store backed by the IAM cap-store table; resource servers swap in a store backed by the PubSub gossip topic and a local LRU cache.

The interface is intentionally smaller than a full CRL surface: a revocation is irreversible (un-revoke does not exist; mint a fresh cap if you change your mind), so the only operations are "mark revoked" and "is it revoked".

func ProcessStoreHandle

func ProcessStoreHandle() (RevocationStore, error)

ProcessStoreHandle returns the singleton RevocationStore.

type Scheme

type Scheme = cap.Scheme

Scheme identifies the signature algorithm a cap uses. Aliased to the runtime's cap.Scheme so there is exactly one source of truth: the numeric values here are the bytes written at sig[cap.AlgTagOffset] and the bytes the verifier dispatches on. Identity.sol's claim.scheme integers are deliberately NOT aligned with these values (claim.scheme=1 → Ed25519 / claim.scheme=2 → ML-DSA-65 in the smart contract namespace); callers that need to map between the two surfaces do so explicitly at the boundary.

type SystemClock

type SystemClock struct{}

SystemClock is the production Clock backed by time.Now().

func (SystemClock) Now

func (SystemClock) Now() time.Time

Now returns time.Now().

type Verifier

type Verifier = LibVerifier

Verifier is the canonical library-layer verifier name; consumers see `capauth.Verifier` rather than `LibVerifier` in their code, with the long name kept as a type alias for documentation symmetry.

type VerifyParams

type VerifyParams struct {
	// Leaf is the cap the wielder is presenting.
	Leaf cap.Cap

	// Chain is the chain of parent caps from Leaf.Parent up to root.
	// chain[0] is Leaf's parent; chain[len-1] is the root (Parent ==
	// zero). An empty Chain means Leaf is itself a root.
	Chain []cap.Cap

	// RequiredOp is the bitmask of operations the caller is about to
	// perform. Verify rejects if Leaf.Permissions does not cover every
	// bit set here.
	RequiredOp uint64

	// Target is the 32-byte hash of the object the caller is acting on.
	// Verify rejects if Leaf.Target does not match.
	Target [32]byte

	// Holder is the 32-byte hash of the holder presenting the cap.
	// Verify rejects if Leaf.Holder does not match. v1 has no
	// proof-of-possession; this is a sanity check that the holder field
	// names the principal the caller's higher-level auth layer believes
	// is acting.
	Holder [32]byte
}

VerifyParams carries the per-call parameters Verify needs alongside the static configuration on the Verifier struct.

Directories

Path Synopsis
middleware
http
Package http provides the resource-server middleware.
Package http provides the resource-server middleware.
Package sdk is the convenience layer SDK consumers use to attenuate a cap they hold, without touching cap.Attenuate directly or threading an Issuer through their call sites.
Package sdk is the convenience layer SDK consumers use to attenuate a cap they hold, without touching cap.Attenuate directly or threading an Issuer through their call sites.

Jump to

Keyboard shortcuts

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