Article 6 explanation envelope
Independently verify a saved Article 6 explanation JSON — offline against the JWKS, or via the public verify endpoint.
Every response from GET /api/v1/actions/{id}/explanation carries an
_envelope block. It's an Ed25519 signature over the canonical JSON
of everything else in the response — a cryptographic receipt for the
structured form of the explanation, not just the rendered PDF.
Why this exists
Compliance officers save JSON explanations and share them between teams, between orgs, and eventually with auditors or regulators. Without a signature on the JSON form there's no way to tell whether the exported file was modified after Aira issued it. The PDF carries its own signature (the compliance report), but the JSON travels further — through email, shared drives, audit software — and needs its own proof.
The envelope is the structured-data sibling of receipt verification: receipts prove an action happened the way it was reported; explanation envelopes prove the regulator-facing JSON describing how the policy chain ran hasn't been tampered with after the fact.
What the envelope looks like
{
"_envelope": {
"alg": "Ed25519",
"signing_key_id": "aira-signing-key-v1",
"content_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb924...",
"signature": "ed25519:MEUCIQCmNhU...",
"generated_at": "2026-04-12T14:22:01Z"
}
}content_hash— SHA-256 of the canonical JSON of the explanation, with the_envelopeblock and therequest_idkey excluded. Those two fields are rebuilt per response (the envelope is the output;request_idis per-call trace metadata) so they're always excluded from what we sign.signature— Ed25519 signature over the same canonical JSON, base64url-encoded, prefixed with the algorithm name.signing_key_id— Key identifier published via the JWKS endpoint at/.well-known/jwks.json. Keys are versioned and never deleted, so an envelope minted years ago still verifies after the active key has been rotated.
Verifying via the public endpoint
Easiest option — POST the saved JSON back to Aira:
curl -X POST https://api.airaproof.com/api/v1/verify/explanation \
-H "Content-Type: application/json" \
-d @saved-explanation.jsonThe endpoint is public — no API key required — so a regulator or auditor who just received the file can verify it without an Aira account. Response:
{
"valid": true,
"checks": {
"key_known": true,
"content_hash_matches": true,
"signature_valid": true
},
"signing_key_id": "aira-signing-key-v1",
"request_id": "req_..."
}valid is true iff every entry in checks is the literal boolean
true. On failure the matching check is a string describing the
specific problem — missing envelope, unknown key, content hash
mismatch (payload was modified), or bad signature.
Verifying from the SDK
from aira import Aira
client = Aira(api_key="aira_live_...")
# Fetch + verify
exp = client.get_action_explanation("action-uuid")
result = client.verify_action_explanation(exp)
assert result.valid, result.checksverify_action_explanation accepts either the ActionExplanation
dataclass returned by get_action_explanation or a raw dict loaded
from disk — so a saved JSON file works without re-fetching.
import { Aira } from "aira-sdk";
const aira = new Aira({ apiKey: "aira_live_..." });
const exp = await aira.getActionExplanation("action-uuid");
const result = await aira.verifyActionExplanation(exp);
if (!result.valid) console.error(result.checks);Verifying offline with OpenSSL
Full transparency mode — verify the signature without calling Aira
at all. You'll need jq, openssl, and the public key from the
JWKS endpoint (or the published signing_key_id).
# 1. Pull the public key for the signing_key_id in the envelope.
KEY_ID=$(jq -r '._envelope.signing_key_id' saved-explanation.json)
PUB_KEY_B64=$(curl -s https://api.airaproof.com/.well-known/jwks.json \
| jq -r --arg k "$KEY_ID" '.keys[] | select(.kid==$k) | .x')
# 2. Recompute the canonical payload (strip _envelope and request_id,
# sort keys, no whitespace).
jq 'del(._envelope, .request_id)' saved-explanation.json \
| jq -cS . > canonical.json
# 3. Compare the SHA-256 of the canonical payload to content_hash.
echo "$(shasum -a 256 canonical.json | cut -d' ' -f1)"
jq -r '._envelope.content_hash' saved-explanation.json
# 4. Decode the public key + signature, then verify with openssl.
echo "$PUB_KEY_B64" | base64 -d > pubkey.raw
# Build a PEM from the raw Ed25519 public key:
printf '\x30\x2a\x30\x05\x06\x03\x2b\x65\x70\x03\x21\x00' > pubkey.der
cat pubkey.raw >> pubkey.der
openssl pkey -pubin -inform DER -in pubkey.der -out pubkey.pem
jq -r '._envelope.signature' saved-explanation.json \
| cut -d':' -f2 \
| base64 -d > sig.bin
openssl pkeyutl -verify -pubin -inkey pubkey.pem \
-rawin -in canonical.json -sigfile sig.bin
# → "Signature Verified Successfully"The canonical JSON encoder uses RFC 8785 conventions: sorted object
keys, no insignificant whitespace, UTF-8. jq -cS produces the same
output.
What each failure means
checks.* | Likely cause |
|---|---|
envelope_present is a string | The JSON has no _envelope block — it was never signed (pre-envelope data, or a stripped export). |
key_known is a string | The signing_key_id isn't in Aira's published keys. Either the envelope was forged or the key has been revoked. |
content_hash_matches is a string | Someone edited the explanation after signing. The payload hash has changed. |
signature_valid is a string | The signature doesn't verify against the public key. Either the signature or the public key was tampered with. |
If content_hash_matches fails, signature_valid will almost always
fail too (they're testing overlapping properties), so the practical
read is "someone modified this JSON after it was issued."
Annex IV technical documentation
Generate the full Annex IV technical file — nine sections, mapped 1:1 to the EU AI Act requirements, derived from the cryptographic evidence Aira already holds.
DORA compliance
Overview of Aira's support for the EU Digital Operational Resilience Act (Regulation 2022/2554) — incident management, ICT third-party risk, resilience testing.