Production deployment guide for PEAC Protocol TAP verification on Cloudflare Workers.
This guide implements:
- RFC 9421 - HTTP Message Signatures (authentication headers)
- RFC 9457 - Problem Details for HTTP APIs (error responses)
- RFC 8615 - Well-Known URIs (JWKS discovery)
- Visa TAP - Built on RFC 9421 for cryptographic agent authentication
- Cloudflare account with Workers enabled
- Node.js 18+ and pnpm 8+
- Wrangler CLI installed:
npm install -g wrangler - PEAC issuer JWKS configured
cd surfaces/workers/cloudflare
pnpm installCreate wrangler.toml:
name = "peac-tap-verifier"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
ISSUER_ALLOWLIST = "https://issuer1.example.com,https://issuer2.example.com"
MODE = "tap_only" # or "receipt_or_tap", "unsafe_no_tap"
JWKS_CACHE_TTL_SECONDS = "3600"
# Optional: Replay protection (requires D1 or KV)
# REPLAY_STORE_TYPE = "d1" # or "kv"
[[d1_databases]]
binding = "DB"
database_name = "peac_replay"
database_id = "your-database-id"
# Alternative: KV for best-effort replay (not atomic)
# [[kv_namespaces]]
# binding = "REPLAY_KV"
# id = "your-kv-id"# Create database
wrangler d1 create peac_replay
# Apply schema
wrangler d1 execute peac_replay --file=./schema.sqlschema.sql:
CREATE TABLE IF NOT EXISTS replays (
nonce TEXT PRIMARY KEY,
expires_at INTEGER NOT NULL
);
CREATE INDEX idx_expires ON replays(expires_at);# Deploy to production
wrangler deploy
# Or deploy to staging
wrangler deploy --env staging┌─────────────────────────────────────┐
│ Cloudflare Workers Runtime │
│ │
│ ┌───────────────────────────────┐ │
│ │ @peac/worker-core │ │
│ │ (Runtime-neutral handler) │ │
│ └───────────────┬───────────────┘ │
│ │ │
│ ┌───────────────▼───────────────┐ │
│ │ Cloudflare Adapter │ │
│ │ - D1 replay store │ │
│ │ - KV JWKS cache │ │
│ │ - Request/Response bridge │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
| Mode | Behavior | Use Case |
|---|---|---|
tap_only |
TAP required (default) | Production APIs |
receipt_or_tap |
TAP or receipt accepted | Migration period |
unsafe_no_tap |
No TAP required (DEV ONLY) | Local development |
Comma-separated list of trusted issuer URLs:
ISSUER_ALLOWLIST = "https://issuer1.example.com,https://issuer2.example.com"CRITICAL: Empty allowlist returns HTTP 500 (fail-closed security).
Recommended: Use D1 for strong replay protection (atomic operations).
REPLAY_STORE_TYPE = "d1"
[[d1_databases]]
binding = "DB"
database_name = "peac_replay"
database_id = "your-database-id"Alternative: Use KV for best-effort replay (NOT atomic, race conditions possible).
REPLAY_STORE_TYPE = "kv"
[[kv_namespaces]]
binding = "REPLAY_KV"
id = "your-kv-id"JWKS_CACHE_TTL_SECONDS = "3600" # 1 hour (default)Implementation-Specific: JWKS caching behavior depends on your worker implementation. Cloudflare Workers do not automatically cache fetch() responses. For persistent caching across requests, implement explicit caching using the Cache API or KV storage. The TTL value controls cache duration when caching is implemented.
All error responses follow RFC 9457 (Problem Details for HTTP APIs):
HTTP/1.1 401 Unauthorized
WWW-Authenticate: PEAC realm="peac", error="E_TAP_SIGNATURE_INVALID"
Content-Type: application/problem+json
PEAC-Error: E_TAP_SIGNATURE_INVALID
{
"type": "https://www.peacprotocol.org/problems/E_TAP_SIGNATURE_INVALID",
"title": "TAP Signature Invalid",
"status": 401,
"detail": "Signature verification failed",
"instance": "/api/resource"
}| HTTP | Code | Description | Retryable |
|---|---|---|---|
| 400 | E_TAP_MALFORMED | Invalid TAP format | No |
| 400 | E_TAP_TAG_UNKNOWN | Unknown TAP tag | No |
| 400 | E_TAP_WINDOW_TOO_LARGE | Time window > 8 min | No |
| 401 | E_TAP_SIGNATURE_INVALID | Signature verification failed | No |
| 401 | E_TAP_TIME_INVALID | Outside time window | Yes |
| 401 | E_TAP_KEY_NOT_FOUND | Key ID not in JWKS | Maybe |
| 401 | E_TAP_REPLAY_PROTECTION_REQUIRED | Nonce present but no replay store | No |
| 402 | E_TAP_MISSING | TAP required but not provided | Yes |
| 403 | E_TAP_ISSUER_NOT_ALLOWED | Issuer not in allowlist | No |
| 409 | E_TAP_REPLAY_DETECTED | Nonce already used | No |
| 503 | E_TAP_JWKS_UNAVAILABLE | Cannot fetch JWKS | Yes |
- Empty
ISSUER_ALLOWLIST→ HTTP 500 - Unknown TAP tags → HTTP 400 (reject)
- Missing replay protection when nonce present → HTTP 401
- TAP time window: max 8 minutes (480 seconds, hard limit)
- Clock skew tolerance: 30 seconds
- Validation:
created <= now <= expires
- Fixed: 480 seconds (8 minutes, not configurable)
- Matches TAP max time window
- Auto-expires old nonces
JWKS fetching includes SSRF guards:
- Block private IP ranges (10.0.0.0/8, 192.168.0.0/16, 127.0.0.1)
- Block metadata URLs (169.254.169.254)
- Enforce HTTPS (except localhost)
- 5-second timeout
For development only. NEVER use in production.
[vars.dev]
MODE = "unsafe_no_tap"
UNSAFE_DEV_MODE = "true"Enables:
- Bypass TAP validation
- Skip issuer allowlist
- Detailed error messages (includes sensitive data)
What This Worker Protects Against:
- Signature forgery: Ed25519 cryptographic verification prevents unauthorized request signing
- Replay attacks: With D1 replay store, prevents nonce reuse (atomic check-and-set)
- Issuer impersonation: ISSUER_ALLOWLIST enforces trusted issuer policy
- Time-based attacks: Enforces 8-minute max window with clock skew tolerance
What This Worker Does NOT Protect Against:
- Eventual consistency replay: KV-based replay protection is eventually consistent (race conditions possible)
- DNS hijacking: JWKS fetching trusts DNS resolution (no DNSSEC validation)
- Compromised JWKS origin: If issuer's JWKS endpoint is compromised, attacker can sign valid TAPs
- DDoS attacks: Standard Cloudflare DDoS protection applies, but no PEAC-specific rate limiting
Trusted:
- JWKS origin (issuer's
.well-known/jwks.jsonendpoint) - must be HTTPS - Cloudflare Workers runtime (code execution, crypto primitives)
- D1/KV storage integrity (for replay protection)
Not Trusted:
- Request headers (validated and sanitized)
- Request body (not used in TAP verification)
- Client-provided timestamps (validated against server clock)
Fail-Closed (Default):
- Empty
ISSUER_ALLOWLIST→ 500 (server error) - Unknown TAP tags → 400 (reject unless
UNSAFE_ALLOW_UNKNOWN_TAGS=true) - Nonce present but no replay store → 401 (reject unless
UNSAFE_ALLOW_NO_REPLAY=true) - JWKS fetch failure → 503 (reject)
Fail-Open (Explicit Opt-In):
UNSAFE_DEV_MODE=true→ bypass all validation (development only)UNSAFE_ALLOW_UNKNOWN_TAGS=true→ accept unknown TAP tagsUNSAFE_ALLOW_NO_REPLAY=true→ accept nonces without replay protection
With D1 (Recommended for High-Value APIs):
- Atomic check-and-set prevents race conditions
- Suitable for financial transactions, sensitive operations
- Latency: ~5-10ms per request
With KV (Best-Effort):
- Eventually consistent (global replication ~10 seconds)
- Race condition window exists (same nonce could be accepted on multiple edge nodes)
- Suitable for non-critical operations, acceptable risk tolerance
- Latency: ~1-2ms per request
Without Replay Protection:
- Nonces are ignored (replay attacks possible)
- Only suitable for public APIs with no sensitive operations
- NOT recommended for production
JWKS Rotation:
- Workers fetch JWKS on-demand per request (unless Cache API is implemented)
- Issuer can rotate keys by updating JWKS endpoint
- No worker restart required
- Consider implementing Cache API with short TTL (5-10 minutes) for production
Key Pinning:
- Not currently supported
- All keys in issuer's JWKS are trusted if issuer is in allowlist
- For enhanced security, implement key ID allowlist in worker code
Secrets:
- Never log JWKS responses (contains public keys but reveals key IDs)
- Never log signature values (reveals cryptographic material)
- Log only: issuer origin, key ID (hashed), error codes
NOTE: These are design targets, not verified benchmarks.
- P95 verification latency: < 5ms (target)
- Cold start: < 50ms (target)
- WASM bundle size: < 100KB (target)
- Use D1 for replay - Faster than KV for small datasets
- Enable JWKS caching - Reduces JWKS fetches by 95%+
- Use
tap_onlymode - Skips receipt verification overhead
Cloudflare Workers automatically collect:
- Request count
- Error rate (by status code)
- P50/P95/P99 latency
- Worker CPU time
import { handleTAP } from '@peac/worker-core';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const start = Date.now();
const response = await handleTAP(request, {
mode: env.MODE,
issuerAllowlist: env.ISSUER_ALLOWLIST.split(','),
replayStore: env.DB ? createD1ReplayStore(env.DB) : undefined,
});
// Log metrics
const duration = Date.now() - start;
env.ANALYTICS?.writeDataPoint({
blobs: [env.MODE, response.status.toString()],
doubles: [duration],
indexes: [request.url],
});
return response;
},
};Configure alerts in Cloudflare dashboard:
- Error rate > 5%
- P95 latency > 10ms
- JWKS fetch failures
Cause: ISSUER_ALLOWLIST not configured or empty.
Fix: Set ISSUER_ALLOWLIST in wrangler.toml:
[vars]
ISSUER_ALLOWLIST = "https://issuer.example.com"Cause: TAP contains nonce but no replay store configured.
Fix: Configure D1 or KV replay store in wrangler.toml.
Cause: Nonce already used (possible replay attack).
Action: This is expected behavior. Client should NOT retry with same nonce.
Cause: Cannot fetch issuer JWKS (network error, timeout, SSRF block).
Fix:
- Verify issuer URL is reachable
- Check SSRF guards (not private IP, not metadata URL)
- Verify HTTPS (required except localhost)
-
MODEset totap_only(notunsafe_no_tap) -
ISSUER_ALLOWLISTcontains trusted issuers only - D1 replay database created and schema applied
-
UNSAFE_DEV_MODENOT set (or set to"false") - Alerts configured (error rate, latency, JWKS failures)
- Staging deployment tested with production-like traffic
- Rollback plan documented
If migrating from receipt-only verification:
- Phase 1: Deploy with
MODE = "receipt_or_tap"(accepts both) - Phase 2: Update clients to send TAP headers
- Phase 3: Monitor TAP adoption (log TAP vs receipt usage)
- Phase 4: Switch to
MODE = "tap_only"when adoption > 95%
# Clone repo
git clone https://github.com/peacprotocol/peac.git
cd peac/surfaces/workers/cloudflare
# Install dependencies
pnpm install
# Configure wrangler.toml (see above)
vi wrangler.toml
# Create D1 database
wrangler d1 create peac_replay
wrangler d1 execute peac_replay --file=schema.sql
# Deploy
wrangler deploy# Test with curl (RFC 9421 HTTP Message Signatures)
curl -i https://your-worker.workers.dev/api/resource \
-H 'Signature-Input: sig1=("@method" "@path" "@authority" "content-type");created=1704067200;keyid="https://issuer.example/.well-known/jwks.json#key-1";alg="ed25519";tag="peac/v0.9"' \
-H 'Signature: sig1=:BASE64_SIGNATURE_HERE:' \
-H "Content-Type: application/json"Header Notes:
Signature-Input: RFC 9421 signature metadata (covered components, created timestamp, key ID, algorithm, TAP tag)Signature: RFC 9421 signature value (base64-encoded)- Headers are case-insensitive per HTTP spec
- Documentation: https://www.peacprotocol.org/docs
- Issues: https://github.com/peacprotocol/peac/issues
- Discussions: https://github.com/peacprotocol/peac/discussions