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
A2ACryptoService signs and verifies the interceptor’s own verdict attestations. It does not verify signatures from sending agents, and no sender-signature check exists. Over HTTP, sender identity comes from the per-agent API key. Direct library callers own identity binding.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
verified replaced forwarded as the name of the admissible status. VerdictStatus.FORWARDED is a deprecated alias of VerdictStatus.VERIFIED, and a legacy "forwarded" value is still accepted on input (so tokens minted by 0.3.x issuers keep verifying), but it is never emitted. Both will be removed in a future minor release.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
Check admissibility
A valid attestation is not an admission decision. It only proves which verdict an issuer reached for this context. A genuineblocked, heuristic_pass, or error attestation verifies too.
After verify_attestation() succeeds, call A2ACryptoService.is_admissible(claims) before acting on the message. It returns True only when the qwed_a2a.verdict claim is in ADMISSIBLE_VERDICTS, which currently contains only verified. It fails closed and returns False for None, malformed claims, and unknown verdict values.
The legacy claim value
"forwarded" from 0.3.x issuers still parses as verified, so is_admissible returns True for those tokens. In 0.3.x, "forwarded" covered logic-guard passes as well as finance passes, so a legacy token admitted this way may represent what is now a non-admissible heuristic_pass. Upgrading only the consumer does not enforce the new finance-only policy — a legacy logic pass is still accepted as verified. To enforce it, also upgrade the issuers (so logic passes emit heuristic_pass, not forwarded), or check which engine issued a legacy token before acting on it. A 0.3.x consumer that compares the claim to "forwarded" denies tokens carrying "verified", so upgrade consumers before, or together with, issuers.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.