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).
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
OpAuthHellokeep working inauth=offandauth=advisorymodes. - v1.14.x clients that send
OpAuthHelloagainst a v1.13.x server receive an "unknown opcode" error and may proceed plaintext. - The flip to
ZAP_AUTH_REQUIRED=truehappens after every KMS deployment ships with a build that mints the bearer (luxfi/kmsv1.9.0 +pkg/iamclient).