What are attestations?
An attestation is a token the QWED server signs to show it reached a verdict. It:- Uses ES256 (ECDSA P-256) signatures
- Is formatted as a JWT, with header
typqwed-attestation+jwt - Can be verified by anyone who holds the server’s public keys, published at
GET /.well-known/jwks.json
proof_ref, the statement’s hash and, on a route that decides one, the admission. It doesn’t cover other fields of the response, such as agent_message.
Getting an attestation
EveryVERIFIED answer carries attestation. You don’t need to ask for it:
qwed.admission. Routes that decide none (logic, fact, stats, image, RAG, process, consensus) sign none.
A Verification Context document carries its own attestation, of type qwed-context+jwt, which signs every other field of the document. Verify it with verify_document_attestation(), described on the Python SDK page.
Verifying attestations
With the client
The client fetches the server’s JWKS, caches it for five minutes, and checks the token against the answer:verify_attestation() returns the verified claims, or raises AttestationError. It checks:
- The signature, with an ES256 key in the JWKS, and that the verified header’s
kidnames that key. - The issuer, and that the token has not expired (nor is used before any
nbf). - That it attests a
VERIFIEDresult. - That its
qwed.proof_hashis the answer’sproof_ref, so it’s this answer’s token. - When you pass
query, that itsqwed.query_hashis the SHA-256 of that statement. For a natural-language answer, pass the translated expression the answer reports (translated_expressionin its developer fields), not your question: the verdict covers that expression. - On a route that decides an admission, that the token signs the answer’s
admission.
KeysUnavailableError, a subclass of AttestationError. KeyMismatchError means no key in the JWKS verifies the token.
The async client has the same method: await client.verify_attestation(result, query=...).
Offline, or against keys you pinned
Other SDKs
The TypeScript, Go, and Rust SDKs make the same checks, with the same key caching:
Each SDK’s README has the details: TypeScript, Go, Rust.
A token says the server reached the verdict and, where it signs one, the admission. Act on the admission: a verified
BLOCKED is still BLOCKED.Signing keys
The server signs with one ES256 key.- Configured key. Set
QWED_SIGNING_KEY(PEM text) orQWED_SIGNING_KEY_FILE(a path to a PEM file). The key survives restarts and every replica shares it. Itskidis<issuer>#<RFC 7638 thumbprint>, the same on every restart and replica. A key that can’t be loaded stops the server at startup. - Generated key. Without either variable, the server generates a key per process and logs
attestation.ephemeral_keyat startup. Its tokens stop verifying after a restart, and on another replica.
QWED_ATTESTATION_TTL_SECONDS (24 hours by default, from 60 seconds to 30 days). Rotating the key stops old tokens from verifying against the server’s new key. Callers that still hold the old public key, cached for up to five minutes or pinned, can verify those tokens until they expire, so rotation is not an immediate revocation. Revocation is held per process and is not shared, so a token’s lifetime is what bounds its use.
AttestationService.get_issuer_info() reports the key’s lifecycle: key_continuity_policy ("ephemeral" or "configured"), key_generated_at (null for a configured key), and key_loaded_at. QWED logs attestation.key_generated on every new key generation.
Attestation structure
Header
Payload
qwed.admission is present only on routes that decide an admission. It is ADMIT or BLOCKED, and the server signs ADMIT only for a VERIFIED result that is verified and bound to its proof.
Issuing attestations
This section is for code that runs QWED’s engines in-process and signs its own results.Fail-closed contract
AttestationResult
AttestationStatus
required
Lifecycle state of the attestation. One of
ISSUED, BLOCKED, or UNVERIFIABLE.string | None
Signed JWT string. Present only when
status == ISSUED; None otherwise.string | None
Machine-readable failure code, or
None on success. See the table below.string | None
Human-readable failure detail.
None on success.bool
Property.
True only when status is AttestationStatus.ISSUED. You must check this flag before you use token.AttestationStatus values
Caller pattern
Issuance-side proof enforcement
create_verification_attestation() won’t sign a VERIFIED verdict that has no proof. If you pass verified=True with neither proof_data nor proof_hash, it returns:
UNVERIFIABLE and BLOCKED verdicts are unaffected. Pass the diagnostic’s proof_ref as proof_hash, or the JSON string your engine hashed into it as proof_data, so the token’s qwed.proof_hash claim matches the result on the consuming side.
Server-side verification
Inside the server,AttestationService.verify_attestation(token) returns (is_valid, claims, error) and checks the token against the server’s own key.
Uniform rejection error
Every rejection returns the same"Invalid token" message. The result never says why a token was rejected: expired, revoked, malformed, oversized, untrusted-issuer, and unsupported-external-issuer tokens all look the same from the outside.
"Attestation has expired" apart from "Untrusted issuer: did:...:X" or "Attestation has been revoked" would let an attacker enumerate the trusted-issuer registry, probe token expiry, and confirm revocation state from the response text. The reason goes only to the server’s debug log.
Enforce the trust boundary
Release gates and any code path that admits aVERIFIED result MUST route through enforce_trust_decision() before acting on it. It is the single consumption-side entry point that binds a verification result to its attestation token and fails closed on any mismatch. Don’t consume DiagnosticResult.status == VERIFIED directly.
Parameters
DiagnosticResult
required
The verification result returned by the engine.
enforce_trust_decision passes through any already-fail-closed status (BLOCKED, UNVERIFIABLE) unchanged, and only applies attestation checks to VERIFIED.string | None
The JWT emitted by
create_verification_attestation(). May be None, but if require_attestation=True and the result is VERIFIED, a missing token blocks the decision.bool
default:"True"
When
True (default, mandatory policy), a VERIFIED result without a valid token is downgraded to BLOCKED. When False (advisory policy), attestation is best-effort: a missing token still passes, but a present-and-invalid token still blocks.list[str] | None
Optional list of trusted issuer DIDs. Defaults to the service’s own issuer. The server verifies only tokens signed by its own key: external issuers are not supported.
string | None
Optional statement the verdict covers. For a natural-language verdict, that’s the translated expression, not the user’s question. When provided, the token’s
qwed.query_hash claim must equal sha256(query), otherwise the decision is blocked. Without it, query binding is not checked.string | None
Optional admission,
ADMIT or BLOCKED. When provided, the token must sign this admission as qwed.admission, otherwise the decision is blocked.Detached result snapshot
enforce_trust_decision() always returns a new DiagnosticResult, never the reference you passed in. Before running any validation, it takes an isolated snapshot of developer_fields (recursive rebuild admitting only immutable scalars, AdvisoryCheck, and JSON-safe containers), so:
- The value you validate is the value you return. Concurrent code holding the original
resultreference can’t mutatedeveloper_fieldsbetween the enforcement check and your admission decision, closing a TOCTOU window thatfrozen=Truealone did not cover. - The returned result does not alias caller data. Downstream code can safely add breadcrumbs to
decision.developer_fieldswithout leaking into the original object.
developer_fields contains an unsupported value type or an object whose __deepcopy__ refuses to copy, enforce_trust_decision() fails closed with a BLOCKED result carrying constraint_id="trust_gate.diagnostic_snapshot_failed". Only the exception type is recorded in the audit log; caller-supplied error text is never surfaced.
Claims binding
When a token is present,enforce_trust_decision checks these bindings against the result before admitting it:
Every block decision is logged at
WARNING with a structured trust_gate.blocked event (constraint_id, reason, policy) so auditors can replay the enforcement trail.
Fail-closed matrix
Use cases
- Audit trails: keep the token with the answer to show the server verified it.
- Compliance: a signed, time-bounded record of each verdict and admission.
- Downstream checks: services that receive an answer verify it against the server’s JWKS before acting on it.