aic-verifier — Architecture (EN)EN
aic-verifier architecture
aic-verifier is the service side of AIC (Agent Identity Certificate)
authorization over HTTP, packaged as a standalone library. Its admission
engine is extracted from the varwof gateway-core so a plain API service can
enforce AIC without running a gateway.
Where this fits
protocol carriers admission core effects
──────────────── ────────────── ───────
HttpClient / compiler produced aicverifier.RunAccessPipeline HTTP middleware
mTLS client cert ──▶ chain→CRL/OCSP→roles→AIC→CLC ──▶ handler / backend
Authorization: Bearer AIC-JWT capability ∩ PA constraints │
(RATS §10 decision context) │ verdict evidence:
decision/admission/
peer (any carrier) │ outcome records + DSSE
gRPC carrier (NewDecisionServer) CLC decision (semantics) │
queue / in-process Decide() allow/allow_unresolved/deny audit (merkle) + supervision
aic-verifier is a library: it does not own the network listener, the
certificate minting, or the policy authoring tool. It owns the decision and the
records. The repo layout mirrors that:
| Area | Files |
|---|---|
| AuthN entry (HTTP) | aicverifier.go (Authenticate, Handler) |
| Admission pipeline | config.go, clc.go, decision.go, delegation_chain.go, constraints.go |
| Revocation & freshness | crl.go, ocsp.go, nonce_cache.go, jwt.go |
| Identity propagation | identity.go |
| Evidence | evidence*.go, admission_record.go, evidence_sink.go, evidence_verify.go, evidence_profile.go, evidence_signing.go |
| Challenge / refusal shape | challenge.go (CLC-CHALLENGE-v1) |
| DecisionServer (transport-independent) | decide.go, admin.go |
| Audit & supervision | audit.go, merkle.go, supervision.go |
| Reverse proxy | server.go (routes, X-AIC-*, outcome reporting) |
| gRPC binding | grpc/ (subpackage, imports google.golang.org/grpc) |
| MCP admin panel | mcp/ (subpackage, imports mark3labs/mcp-go) |
Trust model
A service operator configures the CAs it trusts:
JWTCAFile(for Bearer AIC-JWT): the CA that issues the agent credential key. Tokenkidis the CA SPKI hash;cnf.jktbinds the presenter key.CACertFile(for mTLS): the CA that issues client certificates. Client certificates must chain to this CA and carry the AIC X.509 extension.
Three modes (AuthMode):
BearerOnly: anAuthorization: Bearer <AIC-JWT>header is required and verified againstJWTCAFile.MTLSOnly: a validated mTLS client certificate is required.MTLSOrBearer(default): either credential admits; the pipeline evaluates whichever is presented (mTLS chain wins when both are present).
Admission pipeline
For every request the SDK runs RunAccessPipeline (same engine core as
gateway-core pipeline.go):
- Chain validity (time window, CA scope:
CheckFullChainorCheckLeafOnly). - Optional CRL / OCSP revocation checks (offline-tolerant; fail-closed per configuration).
- Optional RBAC role allow-list from the certificate.
- Optional SPIFFE trust-domain / allow-list enforcement.
- Optional offline maximum certificate lifetime.
- AIC decision (
CheckAdmission): AIC present, capability requirements, representative delegation constraints, user-auth requirements, nonce/replay checks, parameter size limits. - P∩C capability intersection and parameter boundary validation.
- Scheme plugins for declared capabilities (phase-one connection decisions).
Differences from the gateway:
- No network listener / connection throttle / full PKI management.
- The SDK does not mint certificates; it consumes them.
- Reverse-proxy serving is optional (
Server) but middleware is the primary integration path.
Identity propagation
Admitted requests get X-AIC-* headers injected before forwarding to the
backend (identity.go):
X-AIC-Agent-IdX-AIC-Principal-UidX-AIC-Capabilities
Client-supplied identity headers are stripped first so a caller cannot spoof
them. Embedded users read the same values from AuthContext via
aicverifier.FromContext.
Security hardening (post-v0.1)
| Ref | Fix | File |
|---|---|---|
| S1 | IdentityMode moved to Config; proxy reads from config, never from the attacker-controlled X-AICN-Identity-Mode header. |
identity.go, server.go |
| S2 | buildChain rejects a nil CA pool (fail-closed) instead of silently passing; Authenticate denies any client certificate presented without a configured mTLS CA. |
aicverifier.go |
| S3 | Proxy strips Authorization and Proxy-Authorization headers before forwarding; the backend receives only server-asserted X-AIC-* headers. |
server.go |
Config.IdentityMode
Defaults to IdentityForwardClientCert. The reverse proxy uses this value
directly; in middleware style, callers pass the mode via
aicverifier.SetIdentityHeaderMode and must ensure it originates from server
configuration, never from client request headers.
Callbacks and configuration surface
Hooks (Config.Hooks)
Hooks is the reserved extension point for lifecycle callbacks:
Authenticated (ctx, r) error— runs on every admitted request before the upstream handler; returning an error denies the request with 403.Denied (r, *AuthError)— fires on every pipeline or route-level rejection.Forwarded (r, resp)— fires after a reverse-proxy round trip (proxy style).
Plugins (PluginRegistry, capability schemes), AuditLogger,
CRLCache/OCSPCache and UserCertResolver remain the deeper extension
points already provided.
Configurable server
Config.ServerOptions tunes the embedded http.Server in proxy style:
ReadTimeout, ReadHeaderTimeout, WriteTimeout, IdleTimeout,
MaxHeaderBytes. Zero values fall back to safe defaults
(ReadHeaderTimeout=30s, IdleTimeout=120s).
Log file
Config.LogFile, when non-empty, appends SDK logs to that file (0644) instead
of stdout. Callers that need programmatic control pass Config.Logger instead;
Config.CloseLogger() releases the opened file.
Verification files
Authorization verification is configured from files: CACertFile (mTLS CA
bundle), JWTCAFile (AIC-JWT trust root), plus optional UserCert /
UserCertResolver and CRLCache/OCSPCache for revocation.
BackendRootCA (one or more PEM files) is appended to the system roots for
the reverse proxy's outbound TLS to HTTPS backends, so internal PKIs can be
trusted for backend calls.
JSON configuration (LoadConfigFile / ParseConfig)
All of the above can be set from a single JSON file (config.example.json):
listener timeouts, log file, TLS/mTLS files, JWT policy, auth/identity modes,
and admission policy. Unknown fields are rejected so typos surface as errors.
Request flow (middleware style)
client request
│
▼
middleware handler ── tls.ValidClientCertPair (earliest TLS validation)
│
├─ bearer? Authenticate reads Authorization: Bearer <aic+jwt>
│ kid→SPKI→JWTCAFile; jti single-use (ReplayProtection); cnf.jkt
├─ mtls? peer cert chain → buildChain (CACertFile) → CRL/OCSP → roles/SPIFFE
│
▼
RunAccessPipeline: chain → CRL/OCSP → RBAC → AIC decision → CLC capability ∩ PA
│ → parameter bounds → verdict
│
├─ deny ──▶ AuthError (JSON) or application/problem+json (RFC 9457, challenge)
│ admission record / CLC decision record already emitted
│
├─ allow_unresolved ──▶ DischargeObligations ? evaluate : refuse (never silent allow)
│
▼
allow: inject X-AIC-* (proxy) / AuthContext on context (middleware)
│
├─ handler runs ──▶ (EmitOutcome) middleware probes status → outcome record
│
▼
response → auth audit log (optional, TSA-signed)
Reverse-proxy style
NewServer(cfg, routes) builds one http.Server; every request goes through
the same middleware (so outcomes are emitted once), then a route match
(Route.Path prefix) schedules forwarding. The proxy strips client-supplied
X-AIC-* / Authorization / Proxy-Authorization headers before injecting
its own and contacting the backend; BackendRootCA pins HTTPS backends.
Transport failures are reported as OutcomeIndeterminate (never a forged 502
outcome).
Transport-independent decisions
NewDecisionServer(cfg) exposes the same admission on Decide(ctx, view),
Health, gRPC (Verify/Decide), and AdminHandler surfaces, so HTTP, gRPC,
queue and in-process carriers agree on the identical decision for one Config.
ReloadPolicy swaps the authorization policy at runtime.
Read the SDK in this order
config.go (surface) → aicverifier.go (entry) → decision.go / clc.go
(core semantics) → evidence*.go (records) → challenge.go (refusal shape) →
server.go (proxy) → decide.go/admin.go (transport-independent core) →
grpc/ & mcp/ (wire bindings).