AgentPKI

AgentPKI Protocol v0.2

Status: Draft (active) Date: 2026-05-27 Editors: AgentPKI Project License: Apache License 2.0 Supersedes: v0.1 (additive — see §10 Backward Compatibility) Repository: github.com/agentpki/spec


Abstract

This document specifies AgentPKI Protocol v0.2, an additive extension to v0.1 covering the operational layer required to run the protocol at production scale. The protocol core (PASETO v4 passport format, Ed25519 signing, issuer directory at /.well-known/agentpki-issuer.json, the Mode A bearer header, and the Mode B HTTP Message Signatures binding) is unchanged and remains binary-compatible.

v0.2 specifies five additions:

  1. Issuer directory caching with KV-backed cross-isolate persistence and explicit TTL hints
  2. Certificate Revocation List (CRL) format, distribution, and verifier behavior
  3. Replay detection for Mode B via Durable Object-backed first-seen cache
  4. Abuse aggregation reporting so verifiers can feed back observed misuse to issuers
  5. Extended verifier response schema exposing freshness, replay status, and cache hints

It also documents one operational concern: the same-zone Worker-to-Worker fetch trap (§8) that production deployments must work around using service bindings.

v0.1 implementations remain conformant. v0.2-aware verifiers MUST accept v0.1 passports without modification.


Status of this Document

This is a v0.2 working draft. The features specified here are already implemented and running in production at verify.agentpki.dev (the reference verifier), agentpki.dev/.well-known/agentpki-crl.json (the production issuer’s CRL), and the @agentpki/sdk TypeScript and agentpki Python SDKs from version 0.2.0-alpha.1 forward. This document publishes the wire-format and protocol-level rules of those features as a draft for review.

The protocol follows the v0.1 versioning rule (§18 of v0.1): pre-1.0 may carry breaking changes between minor versions; v0.2 specifically is additive and therefore non-breaking. All v0.1 implementations continue to interoperate with v0.2 verifiers, and v0.2 SDKs can talk to v0.1-only verifiers (with the new response fields absent or null).

The latest version, errata, and conformance test vectors are maintained at:

https://agentpki.dev/spec/v0.2

1. Conventions and Terminology

This document inherits terminology and conformance keywords (MUST, SHOULD, MAY per RFC 2119) from v0.1 §1. New terms introduced in v0.2:


2. Summary of Changes from v0.1

Areav0.1 behaviorv0.2 addition
Verifier responseverdict, passport, abuse_score, failure_reason, failure_detail, verifier_idAdds crl_fresh, replay_checked, cached_until (all optional)
Issuer directory cachingVerifier-internal, unspecified TTLKV-tier specified, default 300 s TTL, Cache-Control hint honored
RevocationOCSP-style on-demand check (v0.1 §10.4)Full CRL document format + caching + verifier procedure
Replay protectionSignature window only (Mode B, v0.1 §12)Replay cache (Durable Object) bound to jti + signature
Abuse reportingOut-of-scope in v0.1Verifier POST /v1/abuse/report endpoint + storage shape
Same-zone deploymentNot addressedService binding pattern required for production

No breaking changes. The wire format of the passport token itself is byte-identical to v0.1.


3. Verifier Response Schema (Extended)

3.1 New Optional Response Fields

The verifier response defined in v0.1 §8.1.2 gains three optional fields. All are OPTIONAL; v0.1 verifiers MAY omit them.

{
  // ── all v0.1 fields unchanged ──
  "verified": true,
  "verdict": "allow",
  "passport": { ... },
  "abuse_score": 0.02,
  "verifier_id": "agentpki-verifier-edge",

  // ── v0.2 additions (all optional) ──
  "crl_fresh": true,           // §3.1.1
  "replay_checked": true,      // §3.1.2
  "cached_until": 1779540437   // §3.1.3
}

3.1.1 crl_fresh (boolean)

true if the verifier successfully fetched a current Certificate Revocation List from the issuer’s crl_url (or had one in cache that has not yet exceeded next_update) at the time of this verification.

false if the CRL could not be loaded (network failure, malformed document, parse error). In this case, the verifier MUST treat the token as not revoked by default (defense-in-depth: transient CRL unavailability MUST NOT block legitimate traffic), but the crl_fresh: false flag SHOULD be surfaced to the consuming application so it can adjust its trust assessment.

Verifiers that did not attempt a CRL check (e.g., minimal v0.1 verifiers) SHOULD omit this field rather than emit false.

3.1.2 replay_checked (boolean)

true if the verifier consulted a replay cache during Mode B verification AND the signature was not previously observed.

false if the verifier consulted a replay cache AND the signature WAS previously observed (in which case verdict MUST be deny and failure_reason MUST be replay_detected).

OMITTED if the verifier did not consult a replay cache. Mode A verifications MUST omit this field (replay detection is meaningless without request binding).

3.1.3 cached_until (unix timestamp, seconds)

A hint to the consuming application indicating the latest second at which the verifier’s result can be considered fresh enough to short-circuit a re-verification. Typically min(token.exp, now + 60).

This is an OPTIONAL optimization hint; consumers SHOULD NOT take this as a permission to skip token validation across a session boundary, only as a permission to cache the verdict for the indicated window.

3.2 Backward Compatibility


4. Issuer Directory Caching

4.1 Tiered Cache Model

Verifiers SHOULD implement a tiered cache for the issuer directory document fetched per v0.1 §6:

  1. Tier 1 — memory cache (per-isolate) — In-process map keyed by issuer DNS name. Sub-millisecond lookup. Cleared on isolate restart.
  2. Tier 2 — KV cache (cross-isolate) — A Cloudflare Workers KV namespace (or equivalent shared store) keyed by issuer DNS name. Persists across isolate restarts and replicas. Typical lookup latency 10–30 ms.
  3. Tier 3 — origin fetch — HTTPS GET to https://<issuer>/.well-known/agentpki-issuer.json. Typical latency 50–200 ms.

Verifier lookup order MUST be Tier 1 → Tier 2 → Tier 3.

4.2 Cache Keys

Implementations using Cloudflare Workers KV SHOULD use the key format:

dir:<issuer-dns-name>

For example: dir:anthropic.com, dir:agentpki.dev.

CRL documents (§5) SHOULD use the parallel key format:

crl:<issuer-dns-name>

4.3 TTL Hints

The default TTL for an issuer directory cache entry is 300 seconds (5 minutes).

Issuers MAY influence cache TTL by returning a Cache-Control: public, max-age=<n> header on the well-known directory response. Verifiers SHOULD honor max-age values between 60 seconds (minimum) and 3600 seconds (maximum), clamping outside that range.

When stale-while-revalidate=<n> is also present, verifiers SHOULD serve from cache while asynchronously refreshing for up to n additional seconds beyond the primary max-age.

4.4 Cache Invalidation

A verifier MAY proactively evict an issuer directory cache entry when:

Verifiers MUST NOT cache a directory document that fails the validation criteria of v0.1 §6.2 (valid JSON, v === 1, issuer matches request, current_keys non-empty).

4.5 Cache Persistence Across Deploys

KV-backed cache entries SHOULD persist across verifier deploys. Verifiers that ship a breaking change to the response schema MUST bump their verifier_id and SHOULD migrate or invalidate the KV namespace correspondingly.


5. Certificate Revocation List (CRL)

5.1 CRL Endpoint

Each issuer’s well-known directory document (v0.1 §6.2) includes a crl_url field pointing to the issuer’s revocation list. The canonical location is:

https://<issuer>/.well-known/agentpki-crl.json

Issuers MAY host the CRL at any URL; verifiers MUST follow the crl_url value from the directory.

5.2 CRL Document Format

{
  "v": 1,
  "issuer": "agentpki.dev",
  "generated_at": 1779478468,
  "next_update": 1779478768,
  "revoked": [
    {
      "jti": "8dcaabba98c024e60324d739e94c5990",
      "revoked_at": 1779478000,
      "reason": "key_compromise"
    }
  ],
  "signature": null
}

Field semantics:

5.3 Verifier Behavior

For each verification request, after the signature-verification step (v0.1 §8.2 Step 7), the verifier MUST:

  1. Lookup the CRL for the passport’s issuer, using the tiered cache model of §5.4.
  2. Scan the revoked array for an entry matching the passport’s jti.
  3. If a match is found, set:
    • verdict: "deny"
    • failure_reason: "revoked"
    • failure_detail: "jti revoked at <revoked_at> (<reason>)"
    • crl_fresh: true
  4. If no match is found and the CRL was retrieved successfully, continue with normal verification and set crl_fresh: true in the response.
  5. If the CRL could not be retrieved (network failure, malformed, expired beyond a tolerance), continue verification but set crl_fresh: false. The token MUST NOT be denied solely on transient CRL unavailability.

5.4 CRL Caching

CRLs follow the same tiered cache model as issuer directories (§4.1):

  1. Memory cache (per-isolate, sub-millisecond)
  2. KV cache (cross-isolate, key crl:<issuer>, default TTL = next_update - now, clamped to [60, 3600] seconds)
  3. Origin fetch (typical latency 50–200 ms)

A verifier MAY refresh the CRL eagerly when the cached next_update is within 60 seconds of now, to avoid latency spikes at boundary times.

5.5 Revocation Propagation Window

The maximum window between an issuer revoking a jti and a verifier honoring that revocation is bounded by:

worst_case = (CRL_publish_latency) + (verifier_CRL_TTL) + (replica_propagation)

For the reference verifier with default TTLs, this is typically under 6 minutes. Sites requiring stricter revocation guarantees MAY:


6. Replay Detection for Mode B

6.1 Threat Model

Mode B passports carry a signed request envelope (method, URL, body hash, signature window) per v0.1 §7.2. The signature window prevents replays across sites and outside the time window. However, within the signature window, at the same destination, an intercepted request can be replayed by an attacker that can re-emit the byte-identical TLS request.

v0.2 closes this gap with a verifier-side replay cache that records the first observation of a (jti, signature) pair and rejects subsequent observations within the signature window.

6.2 Replay Cache Storage

The reference verifier implements the replay cache as a Cloudflare Durable Object (class ReplayCacheDO). The replay cache MUST provide:

Alternative storage backends (Redis, in-memory map per region, etc.) are conformant provided they satisfy atomicity and TTL semantics.

6.3 Verification Procedure

For Mode B requests, after signature verification (v0.1 §8.2 Step 9) and before returning a verdict, the verifier:

  1. Constructs a replay key as <issuer>:<jti>:<signature_b64>.
  2. Calls the replay cache with that key, requesting first-observation registration.
  3. If first observation, sets replay_checked: true in the response and proceeds normally.
  4. If signature was previously observed, sets:
    • verdict: "deny"
    • failure_reason: "replay_detected"
    • failure_detail: "signature for jti=<jti> first seen at <timestamp>"
    • replay_checked: true

The replay check applies ONLY to Mode B verifications. Mode A bearer-token verifications MUST omit the replay_checked field (and MAY return verdict: "allow" for the same token used multiple times within its exp window, which is the intended Mode A semantics).

6.4 Signature Window Constraint

Independently of replay detection, Mode B verifiers MUST enforce:

These bounds prevent abuse of arbitrarily-long-lived signatures and bound the size of the replay cache.


7. Abuse Aggregation Reporting

7.1 Submission Endpoint

The reference verifier exposes an abuse-report submission endpoint:

POST <verifier_base>/v1/abuse/report
Content-Type: application/json

Sites observing misuse of a passport (e.g., scraping behavior beyond declared scope, abnormal traffic patterns, fraud signals) MAY submit reports to feed back into the issuer’s abuse score for future passports from the same issuer.

7.2 Report Format

{
  "jti": "8dcaabba98c024e60324d739e94c5990",
  "issuer": "agentpki.dev",
  "reporter_url": "https://reuters.com/api",
  "reason": "scope_violation",
  "evidence": {
    "observed_scope": ["read:articles"],
    "requested_path": "/admin/users",
    "request_count": 47,
    "observed_at": 1779540500
  },
  "severity": "medium",
  "submitter_id": "datadome-edge-us-east"
}

Field semantics:

7.3 Aggregation Semantics

The reference verifier stores submitted reports in a KV namespace keyed by <issuer>:<jti>. Multiple reports for the same jti accumulate.

The aggregated abuse score for future passports from an issuer (the abuse_score field in verifier responses) is computed by the verifier from these aggregated reports plus issuer-specific reputation history. The specific scoring function is implementation-defined; the reference verifier uses a sliding-window decay weighted by severity.

7.4 Issuer Feedback Loop

Issuers MAY periodically poll <verifier_base>/v1/abuse/by-issuer/<issuer> (planned for v0.3, NOT yet specified) to retrieve aggregated reports against their issued passports for proactive revocation.

v0.2 verifiers MUST NOT directly notify issuers of new reports; the feedback model is poll-based, not push-based, to keep verifier responsibilities scoped to verification.

7.5 Privacy

Submitted abuse reports MAY contain identifying information about the agent (jti) and the reporter (URL). They MUST NOT contain end-user PII. Submitters MUST sanitize report payloads before submission.


8. Internal Routing (Operational)

This section is operational guidance specific to deploying on Cloudflare Workers. It is informative, not normative, but is included because production deployments without this guidance will exhibit a subtle and confusing failure mode.

8.1 The Same-Zone Fetch Trap

A Cloudflare Worker on zone Z (e.g., verify.agentpki.dev) calling globalThis.fetch("https://Z/.well-known/agentpki-issuer.json") will, depending on Cloudflare’s edge routing, bypass any Workers routes on the destination URL and hit the underlying Pages origin (or Custom Hostname target) directly.

When that destination is itself a Worker (e.g., an issuer Worker serving Z/.well-known/agentpki-issuer.json), the result is:

Symptom in the verifier logs: failure_reason: unknown_issuer with failure_detail: invalid JSON at https://<issuer>/.well-known/agentpki-issuer.json: Unexpected token '<'.

8.2 Required Fix — Service Bindings

Production verifiers deployed alongside in-account issuer Workers on the same Cloudflare zone MUST route same-zone issuer-directory and CRL fetches through a service binding:

# In the verifier's wrangler.toml
[[services]]
binding = "SELF_ISSUER"
service = "agentpki-self-issuer"  # the issuer Worker we operate

The verifier code MUST detect when a fetch target hostname is “internal” (an issuer we operate on the same zone) and route through env.SELF_ISSUER.fetch(url, init) instead of globalThis.fetch(url, init). The reference verifier implements this in src/internal-fetch.ts.

External issuers (different Cloudflare account, different zone, non-Cloudflare hosting) continue to use globalThis.fetch and are unaffected.

8.3 Non-Cloudflare Deployments

Verifiers deployed on other edge runtimes (Fastly Compute@Edge, Vercel Edge, AWS Lambda@Edge, self-hosted NGINX) are NOT affected by this trap and MAY use globalThis.fetch uniformly.


9. Migration Notes for Implementers

9.1 For verifier operators upgrading from v0.1 to v0.2

  1. Provision a KV namespace for issuer directory caching. Bind as ISSUER_CACHE.
  2. Provision a KV namespace for CRL caching. Bind as CRL_CACHE.
  3. Provision a KV namespace for abuse report storage. Bind as ABUSE_REPORTS.
  4. Provision a Durable Object for replay detection. Bind as REPLAY_CACHE.
  5. Update verify response handler to include crl_fresh, replay_checked, cached_until when applicable.
  6. Add CRL fetch + scan to the verification procedure after signature validation.
  7. Add replay check to Mode B verifications.
  8. Add abuse-report submission endpoint at POST /v1/abuse/report.
  9. If deploying on Cloudflare alongside an in-account issuer, add service binding per §8.2.

Verifiers that complete (1)–(7) are v0.2 conformant. (8) is OPTIONAL but recommended. (9) is operational and not protocol-level.

9.2 For SDK consumers upgrading from v0.1 to v0.2

The verifier-response decoder in v0.2 SDKs adds optional fields. Consumers that already gracefully ignore unknown fields require no code change. Consumers that wish to surface new fields:

// TypeScript v0.2 SDK example
const result = await verify(token);
if (result.crl_fresh === false) {
  console.warn("CRL stale; trust assessment may be lagging");
}
if (result.replay_checked === false && mode === "B") {
  // Verifier saw this signature before — already denied
}

9.3 For issuers upgrading from v0.1 to v0.2

Issuers MUST publish a CRL at the URL declared in their directory’s crl_url field (if they were previously declaring one with no document, v0.1 verifiers accepted that; v0.2 verifiers will accept that with crl_fresh: false).

Recommended CRL publication cadence: regenerate every 5 minutes, set next_update = now + 300.

For issuers using the reference real-issuer Worker template, CRL endpoint generation is automatic from v0.2 of the template forward.


10. Backward Compatibility with v0.1

DirectionBehavior
v0.2 verifier ← v0.1 passportFully compatible. Passport is verified per v0.1 procedure. CRL/replay checks proceed if the issuer publishes them; absent if not.
v0.1 verifier ← v0.2 passportFully compatible. Passport token format is byte-identical. v0.2-only response fields are simply absent in v0.1 verifier responses.
v0.2 SDK ← v0.1 verifier responseCompatible. SDKs treat absent v0.2 fields as “not checked.”
v0.1 SDK ← v0.2 verifier responseCompatible. v0.1 SDKs ignore unknown fields.
v0.2 issuer directory documentSame schema as v0.1. CRL inclusion was already specified as OPTIONAL in v0.1.

There are no breaking changes between v0.1 and v0.2.


11. Security Considerations (New in v0.2)

11.1 CRL Spoofing

If signature is OMITTED from the CRL document, a network-positioned attacker who can serve the issuer’s crl_url could omit known revocations. Mitigations:

11.2 Replay Cache Exhaustion

A malicious party could submit large numbers of distinct Mode B signatures to a verifier’s replay cache to consume Durable Object storage. Mitigations:

11.3 Cache Poisoning

A compromised KV namespace could serve attacker-controlled issuer directory documents. Mitigations:

11.4 Abuse Report Forgery

Anyone can submit an abuse report; without authentication, this enables a denial-of-reputation attack against arbitrary issuers/jti combinations. Mitigations:

11.5 Same-Zone Routing Information Leak

The service-binding requirement of §8 means an attacker who can compromise the verifier Worker has direct (unauthenticated, sub-network) access to the bound issuer Worker. Mitigations:


12. Conformance

A v0.2-conformant verifier MUST:

A v0.2-conformant verifier SHOULD:

A v0.2-conformant issuer MUST:


13. References

13.1 Normative

13.2 Informative


Appendix A: Open Questions for v0.3


Acknowledgments

The v0.2 features described here were implemented in production at verify.agentpki.dev during alpha launch. The same-zone fetch trap (§8) was discovered and worked around during initial end-to-end verification of agentpki.dev against itself.

This document is published under the Apache License 2.0.