Aira

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 _envelope block and the request_id key excluded. Those two fields are rebuilt per response (the envelope is the output; request_id is 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.json

The 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.checks

verify_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 stringThe JSON has no _envelope block — it was never signed (pre-envelope data, or a stripped export).
key_known is a stringThe 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 stringSomeone edited the explanation after signing. The payload hash has changed.
signature_valid is a stringThe 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."

On this page