Skip to content

About

Open specification and reference verifiers for Seal records: ATTESTATION-v1 for AI model calls, ACTION-v1 for agent actions, and GRANT-v1 approvals (draft). Ed25519, canonical JSON, checked offline against a published key.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

9 stars

Watchers

0 watching

Forks

Seal

ATTESTATION-v1 · ACTION-v1

Open formats and reference verifiers for Seal records: what an AI decided, and what an agent was allowed to do. Ed25519. Offline-verifiable. No call to the issuer required.

CI PyPI npm Spec CC-BY-4.0 Code Apache-2.0

Check a receipt in the browser · ATTESTATION-v1 · ACTION-v1 · 5-minute walkthrough

The formats are written up for the IETF as an individual Internet-Draft, draft-chueayen-attestation-receipts (revision -05, October 2026). It is a draft, not a standard.

What this is

Two record formats in one family, signed by the same key and checked by the same verifiers:

Format Answers Fields
ATTESTATION-v1 What did the gateway decide about this model call? 12
ACTION-v1 What was this agent allowed to do? 13

ACTION-v1 covers agent tool actions: a declared tool call authorised or refused under policy before it runs, bound to the session's registered intent, with the arguments hashed canonically. A refusal is a record in its own right. It is a sibling format, not a new version: the v1 receipt envelope is untouched and every existing receipt verifies exactly as before. Profile selection is always explicit, never guessed from field shape:

npx aqta-verify-receipt record.json --profile action-1 --key <key>

Draft: GRANT-v1, USE-v1 and the chain profile (1.3.0). A GRANT-v1 record is a designated approver's signed approval for a bounded number of uses of an action; a USE-v1 record is the boundary's signed statement that one action consumed use i of N. The chain profile checks a bundle of grants, uses and the ACTION-v1 records they authorised together, and returns PASS, FAIL or INCOMPLETE. Missing evidence is INCOMPLETE, never PASS. Both specifications are drafts and not frozen: their wire formats may change. Opt in explicitly:

npx aqta-verify-receipt bundle.json --profile chain --key <issuer key> --approver-keys <k1,k2>
# exit 0 PASS, 1 FAIL, 3 INCOMPLETE, 2 usage or IO

Specifications: spec/GRANT-v1.md, spec/USE-v1.md. Vectors: test-vectors/chain/.

Anyone with the issuer's published public key can verify either offline.

This repo is the formats, two reference verifiers, stand-alone reference issuers, and 67 test vectors. It is not the managed Seal gateway (app.aqta.ai).

A valid signature proves what the gateway said, not what the provider's compute did. Details: WHAT-RECEIPTS-PROVE.md, THREAT-MODEL.md.

Verify (pin the key)

pip install aqta-verify-receipt
# or: npm install aqta-verify-receipt
from aqta_verify_receipt import verify_receipt, fetch_published_public_key

trusted = fetch_published_public_key()  # once, then pin
result = verify_receipt(receipt, trusted_public_key=trusted)
print(result.valid)

From v1.0.4, a pinned trusted_public_key is required by default. Verifying against the key embedded in the receipt alone only proves integrity, not issuer identity. CLI: --key <pinned> or --integrity-only.

Current production key (also at /v1/attestation/public-key):

9Y3Eiq6V8QjRDUM5nPqSwKIOPQaoEU4SbagfYFdvWa4

Keys rotate; receipts do not. A receipt verifies against the key that was current when it was signed. The permanent key record, including retired keys and their validity windows, is at app.aqta.ai/security/issuer-keys.txt.

What you'll see

Default output is one compact line. Words carry the verdict; colour is optional.

$ aqta-verify-receipt test-vectors/valid/001-allowed.json --integrity-only
✓ valid  ALLOWED  0000…0001  untrusted embedded key (integrity only)

Tampered signature:

$ aqta-verify-receipt test-vectors/invalid/001-tampered-signature.json --integrity-only
✕ invalid  signature mismatch  0000…ffff

Exit 0 valid, 1 invalid, 2 usage/IO. --json for automation. --pretty adds a short flourish (seal intact · verified offline); it is never the proof.

--integrity-only checks the receipt against the key embedded in it, which proves internal consistency but not who issued it. Pass --key with the published key above to bind it to the issuer.

Contents

Path What
spec/ATTESTATION-v1.md Wire format (CC-BY-4.0)
spec/ACTION-v1.md Agent action records (frozen)
spec/GRANT-v1.md, spec/USE-v1.md Approvals, uses and the chain profile (DRAFT, not frozen)
packages/verify-receipt-py Python verifier (PyPI)
packages/verify-receipt TypeScript verifier (npm)
examples/ Reference issuer and sample receipt
test-vectors/ Known-good and known-bad receipts
CONFORMANCE.md Issuer and verifier expectations
RELATIONSHIP-TO-SCITT.md Where this sits against RFC 9943, and where it is weaker

Run the whole suite from a clean checkout. Each block is independent, so you can paste them one at a time or all together.

# Python verifier
pip install -e packages/verify-receipt-py
pip install pytest cryptography
pytest packages/verify-receipt-py/tests/ -q

# TypeScript verifier. The build step is required, dist/ is not committed.
(cd packages/verify-receipt && npm ci && npm run build && npm test)

# Cross-implementation check: every test vector, both verifiers, same verdict.
node scripts/make-interop-fixture.mjs
node scripts/transparency-interop-sweep.mjs
node scripts/chain-interop-sweep.mjs

Cross-language pitfalls

The two reference verifiers, TypeScript and Python, must return the same verdict on the same bytes. These defaults differ between the languages and each one caused a divergence that is now pinned by a vector:

Pitfall What differs Fixed in
String ordering JavaScript sorts by UTF-16 code unit; Python's sorted() sorts by code point. They split once a character outside the BMP appears. Sort by s.encode("utf-16-be", "surrogatepass") in Python. 1.2.5
Number formatting Canonical numbers follow ECMA-262 Number::toString (RFC 8785), not Python's repr. They disagree for 0 < abs(x) < 1e-4. CHANGELOG
Invalid UTF-8 Node's readFileSync(file, "utf8") replaces bad bytes with U+FFFD. Decode with a fatal decoder in both. 1.2.6
End anchors Python's $ also matches before a final line feed. Use fullmatch(). 1.2.7
Digits Python's \d matches any Unicode digit unless re.ASCII is set. 1.2.7
Lone surrogates They have no UTF-8 form: TextEncoder writes U+FFFD and Python raises. Refuse them explicitly. 1.2.7

Every class has vectors under test-vectors/, and the sweeps in scripts/ (action-interop-sweep.mjs, bytes-interop-sweep.mjs, transparency-interop-sweep.mjs, chain-interop-sweep.mjs, differential-fuzz.mjs) run both implementations over them in CI and fail on any disagreement. If you write a third verifier, run it against the same vectors.

Attribution

Please credit the authors when you implement, fork, cite, or redistribute.

Specification (spec/): CC-BY-4.0. You must give appropriate credit to Aqta Technologies Ltd, link the licence, and note if you changed the text. Suggested credit line:

ATTESTATION-v1 by Aqta Technologies Ltd, https://github.com/Aqta-ai/attestation-spec (CC-BY-4.0)

Code (packages/, examples/, scripts/, test-vectors/): Apache-2.0. Keep copyright and licence notices when you redistribute.

Machine-readable citation: CITATION.cff (GitHub → Cite this repository).

ATTESTATION-v1 is adjacent to SCITT/COSE, W3C Verifiable Credentials, and in-toto/SLSA. It is not a conforming profile of those standards.

Links

About

Open specification and reference verifiers for Seal records: ATTESTATION-v1 for AI model calls, ACTION-v1 for agent actions, and GRANT-v1 approvals (draft). Ed25519, canonical JSON, checked offline against a published key.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages