LLM Gateway Verification Specification
Status: Active implementation contract
Last updated: 2026-07-10
Applies to: llm-gateway/service (github.com/eqtylab/gateway/llm-gateway/service)
1. Purpose and Scope
Section titled “1. Purpose and Scope”This spec defines Guardian LLM Gateway behavior for:
- RFC 9421 request signature validation
- sidecar-first agent auto-registration integration point
- provider-routed forwarding for verified Anthropic/OpenAI/ChatGPT backend traffic
- signed runtime project-membership presentation and project-context resolution
- append-only request/response audit persistence
Out of scope:
- control plane service implementation
- sidecar implementation
- OPA authorization policy runtime
Sidecar registration integration details:
llm-gateway/docs/sidecar-agent-registration.md
2. Request Lifecycle (/ proxy path)
Section titled “2. Request Lifecycle (/ proxy path)”For normal proxied requests, gateway executes:
- Assign/read correlation ID, classify the route, and run environment
request.starthooks. - Verify the RFC 9421 signature,
viper.v1profile, content digest, freshness/replay, attestation, and registereddid:keyidentity. - Bind any signed project presentation to a non-authoritative project candidate.
- Resolve and pin exactly that candidate project’s active snapshot, with environment fallback disabled.
- Run only its typed
project.bootstrapmembership entry. - Establish authoritative project context only from a validated typed success; projectless requests instead pin the environment-default snapshot and skip bootstrap.
- Run pinned
proxy.pre, forward to the provider or relay destination, then run pinnedproxy.post. - Persist request/response audit attribution and enqueue pinned
request.endwork.
2.1 Provider Route Prefix Contract
Section titled “2.1 Provider Route Prefix Contract”Gateway provider routing is prefix-based:
/v1/anthropic_api/{*path}forwards toanthropic-upstream-url/{*path}/v1/openai_api/{*path}forwards toopenai-upstream-url/{*path}/v1/chatgpt_backend/{*path}forwards tochatgpt-backend-upstream-url/{*path}
Rules:
- provider prefix alone (no trailing path) is unsupported
- query string is preserved
- request method is preserved
3. RFC 9421 Requirements
Section titled “3. RFC 9421 Requirements”Required request headers (proxy path):
Signature-InputSignatureX-Viper-State-CID— CID of the state attestation blob (sidecar uploads it toPUT /v1/blobs/{cid}ahead of the signed request)X-Viper-Nonce— per-request UUIDv4X-Viper-TimestampContent-Digestwhen request has a bodyX-Viper-Identity-CID— only when identity attestation is producedX-Viper-Credential-CID— one signed canonical list containing the registered agent VC CID and any additional presented credential CIDs
The credential renewal path (POST /v1/agents/credentials/renew) requires only X-Viper-Nonce alongside the signature headers; the blob upload path (PUT /v1/blobs/{cid}) requires no X-Viper-* attestation headers.
Content-Digest requirements:
- must include a
blake3digest entry - gateway recomputes BLAKE3 over the request body and rejects mismatches
Required signature profile (viper.v1) covered components (proxy path):
@method@target-urix-viper-state-cidx-viper-noncex-viper-timestampcontent-digest— required whenContent-Digestheader is present (and for requests with a body)x-viper-identity-cid— required when the header is presentx-viper-credential-cid— required when the header is present
For the credential renewal path the covered components reduce to @method, @target-uri, x-viper-nonce. For blob upload, they reduce to @method, @target-uri, content-digest.
Additional header components are allowed (except authorization).
3.1 Runtime Project-Membership Presentation
Section titled “3.1 Runtime Project-Membership Presentation”The project-presentation.v1 headers are:
X-Viper-Credential-CID: one canonical, lexicographically sorted and deduplicated comma-separated list of 1–8 JCS credential CIDs. Despite the singular header name, the value is a bounded list. Viper automatically includes its registered agent VC CID; configured project-membership credentials are merged into the same list.X-Viper-Project-URN: an optional canonicalurn:uuid:<project_uuid>selector used to disambiguate multi-project membership.
When either header is present, it must be covered by the verified RFC 9421 signature. The signing profile rejects a present-but-uncovered credential list because that list also carries the agent identity credential; the project binder rejects an uncovered project selector as a non-configurable core failure in either enabled rollout mode.
X-Viper-Verifiable-Credential-CID is deprecated. The Gateway accepts a signed legacy header when the unified header is absent, but rejects requests that send both. Viper emits only the unified header.
Forms A–D remain unchanged. Gateway core verifies canonical header grammar/coverage, CID/body binding, signer/credential-subject binding, project-reference existence, and deterministic non-ambiguous selection. The result is a non-authoritative ProjectCandidate: it may select policy but grants no project access.
For a candidate, Gateway resolves only the active (deployment environment, project URN) snapshot and pins it. It does not fall back to the environment snapshot. The project snapshot must contain one enabled premade builtin.project_membership entry at project.bootstrap. That plugin uses snapshot configuration for trusted issuers, required membership, accepted statuses, expiration/revocation, roles, and observe/enforce behavior. Gateway accepts only its typed result—never generic plugin context_writes—before constructing authoritative projectctx.Context. Ordinary project hooks run only after typed success, against the same pin through proxy.pre, proxy.post, audit, and request.end.
When no candidate exists, the request is explicitly projectless: it pins the environment-default snapshot and skips project.bootstrap. No external membership lookup occurs on the request path; stored evidence/status is refreshed out of band.
Gateway rollout configuration:
project_context.mode/LLM_GATEWAY_PROJECT_CONTEXT_MODE:disabled(default),observe, orenforce.observeandenforceboth enable the core binder and both fail closed on core protocol/binding errors; project membership observe/enforce behavior is configured onbuiltin.project_membership.project_context.require_membershipandproject_context.trusted_issuer_didsremain accepted only as deprecated rollout configuration. They no longer decide project-specific policy; usemembership_requiredandtrusted_issuerson the bootstrap plugin.- Enabling project context requires the plugin runtime.
Required signature params:
createdkeyidalg=ed25519oralg=ecdsa-p256-sha256
keyid format:
did:key:z... (Ed25519 or P-256 multikey).
4. Attestation Claims
Section titled “4. Attestation Claims”The state attestation blob referenced by X-Viper-State-CID is canonical JSON containing the agent/host snapshot (host info, agent app fields, captured env vars, derived request metadata) and is content-addressed with a BLAKE3 multihash CID. After signature validation the gateway loads the blob from its store, parses version from the blob JSON, and validates nonce/timestamp freshness from the signed X-Viper-Nonce and X-Viper-Timestamp headers against the configured clock-skew window.
5. Trusted Agent Registry Backend
Section titled “5. Trusted Agent Registry Backend”Gateway uses postgres as the trusted registry source of truth for sidecar registration.
Request verification depends on trusted registry lookups after signature validation:
- key lookup by DID (
did:key) - key status check (
active) - agent status check (
activevsrevoked)
Revoked agents are denied locally with:
- status
403 - code
agent_revoked - message
Agent access has been revoked
6. Sidecar Auto-Registration Integration
Section titled “6. Sidecar Auto-Registration Integration”Gateway exposes:
POST /v1/agents/registrations
Behavior:
- available when
-agent-registration-enabled=true - validates
provisioning_tokenusing configured registration token secret - upserts agent/users bindings in the trusted registry
- key lifecycle is idempotent by
did:key - same
(agent_id, did)->noop - new
didfor existingagent_id-> new active key record inserted (updated) - activation policy is auto-active for newly registered agents/keys
- revoked agents cannot be reactivated by registration; they must be restored via control-plane kill-switch endpoint
- emits append-only
registry_registration_events
Request fields:
agent_iddid(did:key:z..., Ed25519 or P-256 multikey)did_source(required —"Software (ed25519)","Software (p256)", or"YubiKey")display_name(optional)provisioning_token
The initial user-to-agent binding is derived gateway-side from the provisioning token subject; the request body carries no users field.
Success response fields:
agent_idagent_statuskey_statusregistration_result(created|updated|noop)registered_atvc— W3C Verifiable Credential issued for the registered DID (Ed25519Signature2020); always presentcorrelation_id
7. Provisioning Token Format
Section titled “7. Provisioning Token Format”Current registration verifier uses HMAC-signed token format:
ggr1.<payload_b64url>.<sig_b64url>
Payload claims:
jtiagent_idsubiatexp
Signature: HMAC-SHA256(secret, "ggr1.<payload_b64url>")
Secret comes from standardized runtime config (registry-registration-token-secret, LLM_GATEWAY_REGISTRATION_TOKEN_SECRET, or TOML registry.registration_token_secret).
8. Replay Protection
Section titled “8. Replay Protection”Replay key:
(keyid, nonce, created)
Behavior:
- atomic reserve on first-seen nonce tuple
- duplicate tuple within TTL is denied (
sig_replay) - TTL extends to
created + default_sig_max_age + clock_skew
Current backend: in-memory replay store.
9. Success Forwarding Contract
Section titled “9. Success Forwarding Contract”On successful verification and authorization, gateway forwards the request upstream and injects:
X-Guardian-Verify-Result: verifiedX-Guardian-Agent-IdX-Guardian-Key-IdX-Guardian-ProfileX-Guardian-Correlation-IdX-Forwarded-HostX-Forwarded-ProtoX-Forwarded-For(append behavior)
Before forwarding, Gateway strips RFC 9421 signature headers and all internal X-Viper-* headers, including project credential CIDs and project selectors. Audit header snapshots retain header names for diagnostics but redact X-Viper-* values; resolved project identifiers and statuses are stored only in structured audit fields.
Host behavior in phase-1:
- preserve original inbound
Hostheader when forwarding upstream
10. Deny/Error Contract
Section titled “10. Deny/Error Contract”On verification or authorization failure, gateway returns local deny response and does not forward upstream.
Response headers:
content-type: application/jsonx-guardian-correlation-id: <correlation_id>
Response body:
{ "code": "sig_invalid", "message": "signature verification failed", "correlation_id": "...."}10.1 Reason codes
Section titled “10.1 Reason codes”Verifier reason codes (implemented):
sig_missing_headerssig_invalidsig_expiredsig_replaykey_not_registeredagent_revokedattestation_missingattestation_invalidnonce_missingprofile_invalidverify_internal_errorrequest_read_error
Gateway-specific policy/upstream codes:
policy_deniedpolicy_errorupstream_unavailable
Registration error codes:
registration_invalid_requestregistration_invalid_tokenregistration_conflictregistration_internal_error
Runtime project reason codes are defined normatively in rfc-project-membership-presentation-contract.md. Core codes (unsigned/malformed presentation, signer/subject or project mismatch, ambiguity, unknown project, CID mismatch, evidence conflict, resolver/snapshot/bootstrap infrastructure errors) fail closed whenever the feature is enabled. Configurable membership-policy codes (missing/unresolvable/invalid evidence, issuer, validity, revocation, and roles) follow the selected snapshot’s builtin.project_membership.mode; observe records deny_ignored without establishing context or running ordinary project plugins, while enforce returns a local 403.
Status mapping:
401: malformed/invalid signature, freshness, replay, missing required inputs403:agent_revokedor policy deny500: policy evaluation internal error or request/audit internal processing errors502: upstream forwarding failure
11. Runtime Configuration
Section titled “11. Runtime Configuration”Gateway binary flags:
-listen(default:10000)-registry-backend(defaultpostgres)-registry-db-dsn(required whenregistry-backend=postgres; defaults toaudit-db-dsnwhen set)-agent-registration-enabled(defaulttrue)-registry-registration-token-secret(required whenagent-registration-enabled=true)-registry-registration-token-issuer(defaultcontrol-plane)-signature-label(defaultsig)-default-sig-max-age(default60s)-default-clock-skew(default120s)-anthropic-upstream-url(defaulthttps://api.anthropic.com)-openai-upstream-url(defaulthttps://api.openai.com)-chatgpt-backend-upstream-url(defaulthttps://chatgpt.com/backend-api)-audit-enabled(defaulttrue)-audit-db-dsn(required when audit enabled)-audit-queue-dir(defaultdata/audit-queue)-audit-queue-max-bytes(default10GiB)-audit-batch-size(default100)-audit-flush-interval(default1s)-audit-retention-days(default90)
Registry trust settings are currently global at verification time (configured via gateway flags).
12. Testing and Tooling
Section titled “12. Testing and Tooling”Unit tests:
- verifier logic:
internal/verify/verifier_test.go - gateway HTTP behavior:
internal/gatewayhttp/server_test.go - audit queue and encoding:
internal/audit/queue_test.go,internal/audit/encoding_test.go - registration and trusted registry behavior:
internal/registry/*_test.go
Developer signer CLI:
cmd/guardian-sigtool
13. Audit Persistence Contract
Section titled “13. Audit Persistence Contract”Phase-1 audit writes are asynchronous and fail-open for gateway traffic:
- request path always attempts to enqueue audit exchange record
- queue write failures do not block traffic and are logged as critical drops
- sink write failures retry on subsequent flush cycles
- audit persistence is append-only (no upsert/overwrite by
correlation_id)
Each exchange row captures:
- request metadata + headers + full body bytes
- response metadata + headers + full body bytes
- route outcome (
proxied,local_deny,upstream_error) - decision code and correlation ID
- verified identity fields when available (
agent_id,key_id,profile) - safely attributable project candidate URN separately from authoritative established
project_urn - membership establishment/status/credential/issuer/reason and bootstrap outcome
- pinned snapshot ID/version/scope/fallback metadata (candidate project traffic always records
scope=project,fallback=false)
Body storage:
- full request/response bytes are stored
- request/response bodies are stored as captured; gateway-internal
X-Viper-*header values are redacted before persistence, and raw membership credential JSON is never copied into structured project audit fields
Retention:
- gateway periodically deletes rows older than
-audit-retention-daysusingreceived_at(exchange time), not DB insertion time
Migration standard:
- SQL files live in
llm-gateway/service/migrations - file naming:
NNNN_description.up.sql - applied versions tracked in
schema_migrations - first migration for this feature:
0001_audit_exchanges.up.sql
14. Registry Schema and Control-Plane Read Model
Section titled “14. Registry Schema and Control-Plane Read Model”Migration 0002_trusted_agent_registry.up.sql creates the initial registry schema, and 0004_single_tenant_schema.up.sql migrates it to the tenantless model used by current runtime:
registry_agentsregistry_agent_keysregistry_usersregistry_agent_user_bindingsregistry_registration_events
Control-plane read views:
v_registry_agentsv_registry_agent_keysv_registry_agent_usersv_registry_agent_audit_link
Control plane can query PostgreSQL directly for discovery and audit linkage.
15. Planned Backend Evolution
Section titled “15. Planned Backend Evolution”Verifier and authorization flow remain interface-based so backends can be swapped without changing the request contract.