Files
zeekay d40098c959 feat(zapauth): JWKS-bearer gate on KMS-facing ZAP server (LP-103)
New pkg/zapauth verifies a JWT presented in OpAuthHello (0x00EF)
BEFORE the X25519+ML-KEM-768 hybrid handshake. Per-peer claims are
attached by ZAP peer ID and consulted on every opcode dispatch.

Backwards compatible: defaults to auth=off so v1.13.x clients keep
working. ZAP_AUTH_REQUIRED=true rejects unauthenticated peers; the
flip happens after every KMS deploys with bearer minting (luxfi/kms
v1.9.0 pkg/iamclient).

- pkg/zapauth/{verifier,verifier_test}.go — Verifier, JWKS cache,
  AuthHello frame, stdlib-only signature verification (RS/ES/EdDSA).
- pkg/api/zap_kms_server.go — KMSZapConfig, StartKMSZAPWith, per-op
  checkAuth gate, OpAuthHello handler.
- pkg/api/zap_kms_server_auth_test.go — required+advisory+missing+bad
  token + nil-verifier integration tests against a self-contained
  ES256 JWKS endpoint.
- cmd/mpcd/main.go — wires ZAP_JWKS_URL / ZAP_EXPECTED_ISS /
  ZAP_EXPECTED_AUDIENCES / ZAP_AUTH_REQUIRED env vars.
- docs/zap-auth.md, LLM.md — protocol reference + LLM map entry.

Tests: pkg/zapauth (13/13), pkg/api KMSZAP (10/10 — 5 original +
5 new auth integration), pkg/zap (existing untouched).
2026-05-02 09:30:48 -07:00

2.9 KiB

ZAP bearer-token gate (LP-103, v1.14.0)

pkg/zapauth validates a JWKS-signed JWT presented BEFORE the existing X25519 + ML-KEM-768 hybrid handshake (pkg/zap). Per-peer verified claims are stored on the connection so downstream handlers can authorize per-opcode.

Wire sequence

client → server   OpAuthHello   (0x00EF)  zapauth.AuthHelloFrame{ Version=1, Token }
server → client   OpAuthHello              {"ok":true}  | {"error":"..."}
client → server   OpClientHello (0x00F0)  X25519 pub + ML-KEM-768 pub
server → client   OpServerHello (0x00F1)  X25519 pub + ML-KEM-768 ct  (rejected with "auth required" when ZAP_AUTH_REQUIRED=true and AuthHello did not validate)
... AEAD-sealed KMS ops ...

OpAuthHello body layout (little-endian):

+0   uint8  version           // CurrentFrameVersion = 1
+1   uint16 token_len
+3   [token_len]byte token

Tokens above 64 KiB are rejected at marshal time.

Server configuration (mpcd)

Env var Required when Default
ZAP_JWKS_URL enabling auth unset (auth disabled)
ZAP_EXPECTED_ISS ZAP_JWKS_URL set unset (init fails)
ZAP_EXPECTED_AUDIENCES ZAP_JWKS_URL set unset (init fails) — comma-separated allow-list
ZAP_AUTH_REQUIRED optional false in v1.14.0; flips to true in v1.15.0 by default

When ZAP_JWKS_URL is unset the server starts with auth=off and behaves exactly like v1.13.x. When set without ZAP_AUTH_REQUIRED=true the server starts with auth=advisory: it verifies submitted tokens, attaches claims, logs a WARN for unauthenticated peers, and still serves them. With ZAP_AUTH_REQUIRED=true (auth=required) every opcode dispatch checks for attached claims and rejects unauthenticated peers with {"error":"auth required"}.

Tokens

JWT requirements:

  • alg ∈ { RS256, RS384, RS512, ES256, ES384, ES512, EdDSA }
  • kid resolves against the JWKS document (cached 5 minutes)
  • iss exactly matches ZAP_EXPECTED_ISS
  • aud contains at least one entry in ZAP_EXPECTED_AUDIENCES
  • exp is in the future

ECDSA signatures may be encoded as ASN.1 (RFC 3279) or fixed-width R||S (RFC 7518 §3.4 — what real JWTs emit).

Backwards compatibility

  • v1.13.x clients that never send OpAuthHello keep working in auth=off and auth=advisory modes.
  • v1.14.x clients that send OpAuthHello against a v1.13.x server receive an "unknown opcode" error and may proceed plaintext.
  • The flip to ZAP_AUTH_REQUIRED=true happens after every KMS deployment ships with a build that mints the bearer (luxfi/kms v1.9.0 + pkg/iamclient).