attested-proxy
Verification of World App attested-key canonical requests, as Rust crates and as a sidecar container that sits in front of a service.
A World App request is signed by a device key that the Attestation Gateway attested. The
gateway's JWT (Integrity-Token) carries that key as cnf.jwk. The app signs an RFC 9421
signature base covering the method, scheme, authority, path, query, body digest and the token
itself; a verifier rebuilds the same base from the request it received and checks the signature
with the attested key. The wire format is the TFH RFC 9421 Integrity Request Signing Profile.
Verification proves that an attested app on an attested device signed this exact request. It does not identify a user, a PCP owner or a relying party, and it authorizes nothing. See Authorized Principal for that layer.
Crates
| Crate | Use it to |
|---|---|
attested-request |
Sign and verify canonical requests without any transport. Types, Signature-Input parsing and canonical serialization, the signature base, Attestation Gateway token verification (ES256 only), iOS App Attest and Android signature checks, a Signer trait for clients, an optional replay guard. The remote-jwks feature adds a cached HTTPS JWKS; test-util adds software signers and a fake gateway. |
attested-request-tower |
Verify in-process, in any tower or axum service. AttestedRequestLayer puts a VerifiedAttestedKeyContext in the request extensions; the axum feature adds the AttestedKey extractor. |
attested-proxy |
Verify in a sidecar, so the service itself needs no attestation code. |
A Rust service can embed the tower layer instead of running the sidecar; the sidecar is built on that same layer.
let app = new
.route
.route_layer;
async
The sidecar
The sidecar is the service's only ingress. For each request it:
- forwards it unverified if its path is listed in
ATTESTED_PROXY_UNPROTECTED_PATHS(exact match, meant for the load balancer's health check); - otherwise reads the body under a size limit and deadline, and verifies the request, answering
{"error": "<reason>"}with the reason's status when verification fails; - forwards the verified request to the one configured upstream. Clients never choose the destination. WebSocket upgrades are verified on the handshake and then tunnelled byte for byte.
Everything that can fail does so closed: an unreachable JWKS, an unreachable replay store and an unreadable body all refuse the request.
What the upstream receives
| Header | Value |
|---|---|
x-attested-platform |
ios or android |
x-attested-key-thumbprint |
RFC 7638 thumbprint of the device key: a stable per-device identifier, for example to rate limit on |
x-attested-request-binding |
base64(SHA-256(signature base)) of this exact request |
Client-sent copies of these headers are removed on every path, and the Integrity-Token,
Signature-Input and Signature headers are not forwarded. The upstream can trust these headers
only because it is reachable solely through the sidecar, so keep its port off the Service and the
load balancer.
Configuration
Every flag has an ATTESTED_PROXY_* environment variable; attested-proxy --help lists them.
| Variable | Default | |
|---|---|---|
ATTESTED_PROXY_UPSTREAM |
required | The sibling container, http://127.0.0.1:8000. |
ATTESTED_PROXY_AUTHORITY |
required | The host clients dial, as signed in @authority. |
ATTESTED_PROXY_SCHEME |
https |
As signed in @scheme. wss:// clients sign https. |
ATTESTED_PROXY_AUDIENCES |
required | Accepted token audiences, comma-separated. |
ATTESTED_PROXY_ISSUER |
required | The Attestation Gateway iss. |
ATTESTED_PROXY_JWKS_URL |
required | The Attestation Gateway HTTPS JWKS URL. |
ATTESTED_PROXY_UNPROTECTED_PATHS |
none | Exact paths forwarded without verification. |
ATTESTED_PROXY_LISTEN |
0.0.0.0:8080 |
Proxy listener. |
ATTESTED_PROXY_ADMIN_LISTEN |
0.0.0.0:8081 |
/health (liveness) and /ready (readiness). |
ATTESTED_PROXY_MAX_AGE_SECS |
300 |
Oldest accepted created. |
ATTESTED_PROXY_MAX_FUTURE_SKEW_SECS |
60 |
Furthest-ahead accepted created. |
ATTESTED_PROXY_MAX_BODY_BYTES |
1048576 |
Largest body verified. |
ATTESTED_PROXY_BODY_READ_TIMEOUT_SECS |
10 |
Deadline for the request body. |
ATTESTED_PROXY_HEADER_READ_TIMEOUT_SECS |
10 |
Deadline for the request headers. |
ATTESTED_PROXY_UPSTREAM_CONNECT_TIMEOUT_MS |
2000 |
Deadline for connecting upstream. |
ATTESTED_PROXY_UPSTREAM_RESPONSE_TIMEOUT_SECS |
60 |
Deadline for upstream response headers. Open tunnels have none; the upstream owns their idle timeout. |
ATTESTED_PROXY_MAX_CONNECTIONS |
1024 |
Connections and tunnels together. Beyond it, connections wait in the listen backlog. |
ATTESTED_PROXY_SHUTDOWN_GRACE_SECS |
45 |
How long in-flight work may finish after SIGTERM. |
ATTESTED_PROXY_REPLAY_REDIS_URL |
none | Enables replay tracking (redis:// or rediss://). |
ATTESTED_PROXY_REPLAY_TIMEOUT_MS |
250 |
Deadline for one replay-store command. |
Telemetry is configured by telemetry-batteries (TELEMETRY_PRESET=datadog,
TELEMETRY_SERVICE_NAME, TELEMETRY_METRICS_BACKEND=statsd, …), as in Flamingo.
Deploying next to a service
In a common-app chart, add the sidecar and move traffic to its port. For Flamingo:
service:
port: 8080
httpRoute:
port: 8080
targetGroupConfigurations:
- defaultConfiguration:
healthCheckConfig:
healthCheckPath: /ready # unprotected, forwarded to Flamingo's enclave-aware /ready
healthCheckPort: "8080"
sidecars:
- name: attested-proxy
image: <attested-proxy image>
env:
-
-
-
-
-
-
ports:
-
livenessProbe:
readinessProbe:
The pod is ready only when both containers are: the service's own probe as before, and the sidecar's once it holds a usable JWKS. The sidecar stops reporting ready as soon as it receives SIGTERM, then drains.
Health, metrics and logs
| Metric | |
|---|---|
attested_request.verified |
Verified requests, by platform. |
attested_request.rejected |
Refused requests, by reason, platform and status. |
attested_request.verify.duration |
Verification latency, by outcome. |
attested_proxy.upstream.errors |
Upstream failures, by kind (unavailable, timeout). |
attested_proxy.tunnels.opened, .failed, .active |
WebSocket tunnels. |
attested_proxy.connections.active |
Open client connections. |
attested_proxy.jwks.age_seconds |
Age of the cached JWKS; alert well before it reaches six hours, when verification starts failing. |
attested_proxy.jwks.usable |
1 while the cached JWKS can verify tokens, 0 before the first fetch and once it is too stale. Alert on 0: age_seconds reports nothing until a fetch succeeds. |
attested_proxy.jwks.fetch_failures |
Failed startup JWKS fetches, by class (the HTTP status code, timeout, request, invalid, ...). |
Client rejections are counted, not logged. Failures of our own dependencies (JWKS, replay store,
upstream) are logged at warn. Tokens, signatures and bodies are never logged.
Replay tracking
created bounds how long a captured request can be replayed (five minutes by default). Replay
tracking additionally refuses a second use inside that window, at the cost of a store on the
request path that must be up for any request to succeed. It is off unless
ATTESTED_PROXY_REPLAY_REDIS_URL is set. A route whose effect can be repeated needs it; a route
that only opens a session, such as Flamingo's WebSocket handshake, usually does not.
The replay store must use maxmemory-policy noeviction: evicting an unexpired claim allows that
request to be replayed. Provision enough memory for the acceptance window; when full, writes must
fail so verification fails closed. Losing claims during a restart or failover also permits replay
until those requests expire; configure persistence and recovery accordingly.
Profile notes
@signature-paramsis serialized, not copied. RFC 9421 §3.2 step 7 builds it by serializing the parsedSignature-Inputmember. Some existing verifiers copy the raw substring afterintegrity=instead. Current signers emit the canonical form, so both approaches accept their signatures today; only this one also accepts equivalent spellings, such as extra whitespace, that Structured Fields allows.@schemeand@authoritycome from configuration, never from theHostheader.- Reject reasons and statuses match go-sonic's canonical request middleware, so clients see one taxonomy across services.
- iOS assertions are checked against
cnf.jwkonly. Their rpId and counter are not checked: App Attest keys are app-scoped, the gateway verified the app when it attested the key, and freshness comes fromcreated.
Test vectors
test-vectors/ holds vectors for any implementation of the profile:
signature-base.json: signature bases and their hashes, including equivalent whitespace, parameter order and derived components. Generated bygenerate_signature_base.py, independently of the Rust code.verification.json: signed requests and the verdict a verifier must reach, from a fixed JWKS and clock. Generated bycargo run -p attested-request --example generate-vectors --features test-util.
CI fails if either file differs from what its generator produces.
Development
Container image
The sidecar image is published to GHCR as ghcr.io/worldcoin/attested-proxy:
- every push to
mainpublishes:latestand:sha-<sha>; - a pushed
attested-proxy/vX.Y.Ztag (or a manual run of the release image workflow) publishes:vX.Y.Zand opens a draft GitHub release recording the image digest; - pull requests that touch the build inputs build without pushing.
Deploy by digest (ghcr.io/worldcoin/attested-proxy@sha256:…), not by tag.