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
Verifying an attestation
verify_attestation() requires an AttestationContext describing the request the token is being presented against. A cryptographically valid signature is not enough — the token must also be bound to the current sender, receiver, payload, and (optionally) session. Detached attestations replayed against a different message are rejected.
The AttestationContext dataclass
The verifier hashes
payload internally with the same deterministic method as sign_verdict() (json.dumps(..., sort_keys=True, default=str)), so callers never handle raw hashes.
Verification steps
verify_attestation() runs these checks in order — the first failure short-circuits and returns a structured error:
- Cryptographic signature (ES256 / ECDSA P-256).
- Expiry (
expclaim). - Required claims present (
iss,sub,iat,exp,jti). - Structural validation of the
qwed_a2aclaim block. - Deployment context — the
deployment_idclaim matches this instance. - Context binding — sender, receiver, payload hash, and (if provided)
session_idall match theAttestationContext. jtireplay check — rejects previously seen trace IDs.
The replay check deliberately runs last, after context binding. This prevents an out-of-context token — for example one replayed against the wrong receiver — from polluting the
jti registry and locking out a legitimate future presentation of the same token in its correct context.Call signature
is_valid=True guarantees the token is cryptographically sound and bound to the supplied context — not just that its signature is valid.
Verification outcomes
Migrating to context-bound verification
If you’re upgrading from the pre-context release, update everyverify_attestation() call site to construct an AttestationContext from the same fields you signed with:
sign_verdict()) is unchanged.
Tamper detection
Thesub claim contains a SHA-256 hash of the original payload:
verify_attestation() performs this hash comparison internally as part of context binding — as long as you pass the current payload in the AttestationContext, a modified payload will be rejected with "Attestation payload hash mismatch — detached attestation rejected". You only need to hash content directly when you want the digest for logging or out-of-band comparison.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:
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.