aic-verifier — API (EN)EN

API reference

Everything lives in package aicverifier (github.com/varwof/aic-verifier); decisions and records come from github.com/varwof/register/semantics. This page covers the surface you are meant to use; go doc github.com/varwof/aic-verifier has the exhaustive list, and the same content is on pkg.go.dev.

Two integration styles

Style Entry point What it gives you
Middleware (*Config).Handler(next) (http.Handler, error) wraps your own handler; the verified identity is on the request context
Middleware (inline) (*Config).AuthMiddleware(next) http.Handler same, panics on a bad config so it can be used inside http.Server{...}
Reverse proxy NewServer(c *Config, routes []Route) (*Server, error) listens on one address, forwards admitted requests, injects X-AIC-*

Inside a handler, read the verified identity with aicverifier.FromContext(ctx) *AuthContext; it is nil when the pipeline did not run (which also means the request never reached the handler).

Configuration

Config carries everything; all fields are optional except the ones the mode you pick requires. Grouped by what you reach for:

Group Fields
TLS / transports TLSCertFile, TLSKeyFile, CACertFile (mTLS trust anchor), JWTCAFile, BackendRootCA, AuthMode (MTLSOnly / BearerOnly / MTLSOrBearer), IdentityMode
Bearer tokens JWTIssuer, JWTAudience, ReplayProtection
What is required RequireAIC, RequiredCapabilities, RequiredOperations, EnforceConstraints, DisallowRepresentative, RequireUserAuth
Principal authorization UserCert, UserCertResolver, DischargeObligations, ObligationsUnderstood, UnresolvedEvaluator
Decision context (RATS §10) RequireFreshDecisionContext, DecisionContext
Evidence Evidence *EvidenceConfig, EvidenceProfile, EvidenceRequirement, EvidenceFacts, EvidenceExporter
Refusal shape Challenges *ChallengeConfig, ChallengeCarrier
Operational CRLCache, OCSPCache, AuditLogger, AuditLogFile, AuditTSAURL, NonceCache, Logger, LogFile, StreamBody, Hooks, ServerOptions
Plugins / approvals PluginRegistry, CapabilityRegistry, ApprovalRequester, OverrideRecorder, SupervisionPolicy, SupervisionStore, RequireApproval

Routes (reverse proxy)

type Route struct {
    Path                 string   // URL path prefix, e.g. "/api"
    Target               *url.URL // backend base URL
    AllowMethods         []string // optional method allowlist
    RequiredCapabilities []string // capability ids the caller must hold
}

Decisions (the CLC surface)

func AuthorizeOperation(aic *AIC, pa *PrincipalAuthorization, opID string, params map[string]any) (semantics.Decision, error)
func AuthorizeCapabilities(caps []Capability, opID string, params map[string]any) (semantics.Decision, error)
func AuthorizeCapabilitiesWithConstraints(caps, constraints []Capability, opID string, params map[string]any) (semantics.Decision, error)
func AuthorizeGrants(grants []semantics.Grant, opID string, params map[string]any) (semantics.Decision, error)

The result is a CLC semantics.Decision:

Field Meaning
Verdict allow, allow_unresolved, or deny
Reason stable reason code (capability_not_authorized, max_rows:violated, …)
Unresolved recognized-but-unevaluated constraints carried by allow_unresolved

allow_unresolved is not allow: the operation carries obligations nobody has discharged. A deployment either evaluates them under a pinned rule — ConnectionConstraintEvaluator(clientIP) is the built-in one for network:cidr — or refuses. Nothing in the SDK silently turns an obligation into a release.

Verdicts and reason codes are the language's, not the SDK's; the specification and its corpus live in varwof/capability.

Identity and refusals

type AuthContext struct {
    ClientCert   *x509.Certificate
    Principal    string
    AgentID      string
    SPIFFEID     string
    Roles        []string
    Capabilities []string
    AIC          *AIC
    Bearer       bool
    Serial       string
    Verdict      string
    Reason       string
    Unresolved   []string
    OperationDecisions []OperationDecision
    Evidence     []RecordRef
    Satisfaction *semantics.RequirementResult
}
type AuthError struct {
    Code         ErrorCode
    Status       int
    Message      string
    Stage        string
    Evidence     []RecordRef
    Problem      *ProblemDetails
    Satisfaction *semantics.RequirementResult
}

AuthError is what a middleware refusal returns (as JSON, or as application/problem+json when Problem is set). It carries the records the refusal itself produced, so a caller can log or forward them next to the answer.

Evidence

type EvidenceConfig struct {
    Sink       EvidenceSink            // nil → SlogSink with the SDK logger
    Strict     bool                    // emission failure denies the request
    TTL        time.Duration           // RATS §10 clock on the record; the per-admission nonce (RATS §10.2) is bound regardless
    Audience   string                  // relying party the evidence is addressed to
    RecorderID string                  // which admission point emitted it
    Now        func() time.Time
    OnError    func(ctx EvidenceContext, err error)
    Gaps       *GapCounter             // counts emissions that left no record
    Sign       func(pae []byte) ([]byte, error)
    KeyID      string
    EmitOutcome bool                   // middleware + reverse proxy report what the effect boundary saw
    // Profile, Requirement, ... see go doc
}
type EvidenceSink interface {
    Emit(ctx EvidenceContext, rec semantics.DecisionRecord, env semantics.Envelope) (RecordRef, error)
    EmitAdmission(ctx EvidenceContext, rec AdmissionRecord, env semantics.Envelope) (RecordRef, error)
}

FileSink{Dir: ...} writes one DSSE envelope per record; SlogSink logs them; your own sink can do whatever a deployment needs. RecordRef reports the digest and, for file-like sinks, where the record went.

Reading records back:

Call Use
LoadEvidenceRecord(path) one file → re-computed *semantics.DecisionRecord
CheckEvidenceEnvelope(env) validate either payload type through one entry point
VerifyEvidenceDir(dir, verifyFn) whole directory; per-file failures are collected, not fatal; outcomes whose decisionDigest does not resolve to a decision in the same directory are reported as orphans (OrphanOutcome), never counted as consent
VerifyFnFromPublicKey(pub) the verifyFn above, from a pinned emission key
(*EvidenceBundle).CheckDecisions() a report bundle built by an exporter: its decision section must reference a record that still recomputes

Payload kinds and their predicate types: AdmissionRecordPredicateType (aic/v1/admission-record), OutcomeRecordPredicateType (aic/v1/outcome-record), and CLC decision records from register/semantics. See evidence.md for the shapes and for the evidence profiles that pin them.

Refusals that carry a challenge

type ChallengeConfig struct {
    TTL         time.Duration          // how long a corrected presentation is welcome
    Audience    string
    RetryAfter  time.Duration          // becomes Retry-After
    ObtainHints []semantics.ObtainHint // where the missing item can be obtained
    Now func() time.Time; NewID, NewNonce func() string
}

Set Config.Challenges and a refusable denial answers 403 with application/problem+json (ProblemDetails) carrying CLC-CHALLENGE-v1: what is required, a nonce, the action digest, and when the retry stops being welcome. A denial with nothing to obtain stays a plain refusal — the SDK will not dress up a hard no as "try later".

Helpers worth knowing

Call Use
BuildSourceChain(clientCert, aic, userCert) the material this admission relied on, as a CLC source chain
ConnectionConstraintEvaluator(clientIP) discharge network:cidr obligations at the connection
HasAIC(cert), AICFingerprint(cert), ExtractRoles(cert), ExtractSPIFFEIDFromCert(cert) inspect a peer certificate
MTLSServerConfig, ClientTLSConfig, LoadCA, LoadCert TLS plumbing with the SDK's defaults
CanonicalJSON(v) the JCS bytes the digests are taken over

Versioning

Version is the SDK version; CLCRevision is the language revision it evaluates (currently CLC-1.8, from register/semantics). A record carries the revision it was decided under, and an implementation refuses a revision it cannot read rather than downgrading silently.