Overview
Every verification verdict is signed with an ES256 JWT attestation — a cryptographic proof that the verification took place. This enables:- Tamper detection — any modification invalidates the signature
- Non-repudiation — the signing service is identified by DID
- Audit compliance — attestations are stored and queryable
- Cross-service verification — any QWED node can verify the token
How it works
JWT structure
Header
kid is derived from a SHA-256 fingerprint of the public key, so it stays stable as long as QWED_A2A_SIGNING_KEY_PEM is unchanged — even across process restarts.
Payload
Configure the signing key
QWED A2A requires a persistent ECDSA P-256 signing key so that JWT attestations signed before a restart can still be verified afterwards. Keys are never generated inside the process — the service loads them from theQWED_A2A_SIGNING_KEY_PEM environment variable (or the pem_key constructor argument).
Generate an unencrypted PKCS#8 P-256 key with openssl:
key_id and can verify each other’s tokens.
Signing a verdict
trace_id becomes the token’s jti and must be unique per instance: signing the same trace_id again within its validity window raises ValueError. See Replay protection.
Verifying an attestation
verify_attestation checks the token against an AttestationContext — the sender, receiver, and payload you expect the attestation to cover. A valid signature alone is not enough: the token must also be bound to the provided context.
iss and kid are bound to the key that verified them — routing never trusts unverified claims.
Verification outcomes
Tamper detection
Thesub claim contains a SHA-256 hash of the original payload:
Publishing the public key (JWKS)
External consumers verify attestations by fetching the public key.A2ACryptoService.get_public_key_jwk() returns the current key in JWK format:
GET /.well-known/jwks.json so downstream services and auditors can fetch keys over HTTP.
Cross-service verification
Because every replica loads the same PEM fromQWED_A2A_SIGNING_KEY_PEM, they all derive the same key_id and can verify each other’s tokens:
Cross-deployment verification (trusted issuers)
By default an attestation verifies only against the issuing deployment’s own key. A token from another deployment fails closed as an unknown issuer, so sharing one private key across deployments — which would let any agent mint attestations as any other — is never required and never works. To verify attestations from a peer deployment, register the peer as a trusted issuer with its deployment ID and public JWKS. Copy the JWKS entry verbatim from the peer’s/.well-known/jwks.json endpoint:
kid/x/y placeholders with the peer’s real values. The environment variable is re-read on every verification, so key rotation takes effect without a restart.
You can also pass the mapping directly to verify_attestation — the explicit argument takes precedence over the environment variable:
Verifier-only nodes
A node that only verifies peer attestations doesn’t need a signing key of its own. WithQWED_A2A_SIGNING_KEY_PEM unset and trusted issuers configured, verify_attestation verifies peer tokens normally — only sign_verdict, get_public_key_jwk, and the interceptor still require the local key. With neither a local key nor trusted issuers, verification returns No verification keys available (no local key, no trusted issuers).
Replay protection (jti lifecycle)
Each attestation’sjti (the trace_id) can be consumed once per verifying instance. A2ACryptoService keeps two separate records:
- Issuance record —
trace_idvalues this instance has signed. Signing a duplicatetrace_idwithin its validity window raisesValueErroratsign_verdict: each attestation needs a uniquejti, and minting two tokens under onejtiwould poison every consumer’s replay registry. - Consumption registry —
jtivalues this instance has verified. The first verification of a token succeeds; presenting the same token again returnsReplay detected: jti already seen.
Replay scope and multi-worker deployments
For cross-worker replay protection, inject a shared registry via thejti_registry constructor argument:
ReplayRegistry contract:
check_and_register(jti, now=..., *, valid_until=...)with atomic insert-if-absent semantics — two workers racing on the samejtimust not both be accepted.- Retention covering each token’s full lifetime: honor
valid_untiland never evict a live token’s slot. - Thread safety for in-process concurrent use.
- A
ttl_secondsproperty reporting the retention window.
ttl_seconds is missing, non-numeric, non-finite, or shorter than validity_seconds raises ValueError at construction.
Fail-closed behavior
QWED A2A treats missing or invalid signing keys as an unrecoverable configuration error, not a soft warning. There is no ephemeral-key fallback.
The FastAPI gateway surfaces these as
HTTP 503 Signing key unavailable on both /a2a/intercept and /.well-known/jwks.json so orchestrators can detect a misconfigured deployment before it accepts traffic.