Verify a proof

Every verdict carries an append-only, Ed25519-signed, hash-chained proof. This page is the complete recipe to verify one yourself, with no trust in us and no AI Verify software — just a published key and a few lines of standard crypto.

A passing check means the bytes below were signed by the named key and have not changed since. It says nothing about whether the verdict is right — that is the verdict’s own determination, not what the signature attests.

What you need (two endpoints)

1. The signed envelope — the exact bytes that were hashed and signed, plus the signature. The proof owner downloads it with their API key (raw storage stays private; this is the public door to it) and can hand the result to anyone.

GET /v1/verifications/{id}/proof/envelope     (Authorization: Bearer <your API key>)

{
  "verification_id": "…",
  "format": "acretix-verify/proof/2",
  "signing_domain": "acretix-verify/proof-sig/2:",
  "jwks_uri": "/.well-known/acretix-verify/keys.json",
  "entries": [
    {
      "seq": 0,
      "key_id": "ed25519-…",
      "algo": "ed25519",
      "content_hash": "<sha256 hex>",
      "prev_hash": null,
      "signature": "<base64 Ed25519>",
      "envelope": "<the exact canonical JSON bytes that were hashed>"
    }
  ]
}

2. The public key — published as a JWKS. Pick the key whose kid equals the entry’s key_id. No private key is ever served.

GET /.well-known/acretix-verify/keys.json     (public, no auth)

{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "<base64url public key>", "kid": "ed25519-…", "use": "sig", "alg": "EdDSA" } ] }

The signed bytes (the exact recipe)

The whole verification, in a few lines

Save the envelope JSON to a file; pass it and the JWKS URL. Runs on stock Node (≥ 18), no dependencies. Exits non-zero if any entry fails.

import { readFileSync } from 'node:fs';
import { createHash, createPublicKey, verify } from 'node:crypto';

const envelope = JSON.parse(readFileSync(process.argv[2], 'utf8')); // .../proof/envelope output
const jwks = await fetch(process.argv[3]).then((r) => r.json());     // the public JWKS URL

for (const e of envelope.entries) {
  const hash = createHash('sha256').update(e.envelope).digest('hex');            // 1. hash the bytes
  const key = createPublicKey({ key: jwks.keys.find((k) => k.kid === e.key_id), format: 'jwk' }); // 2. the key
  const ok = hash === e.content_hash &&                                          // 3. integrity
    verify(null, Buffer.from('acretix-verify/proof-sig/2:' + hash), key, Buffer.from(e.signature, 'base64')); // 4. signature
  console.log('seq ' + e.seq + ': ' + (ok ? 'VERIFIED' : 'FAILED'));
}

The signature is checked over the hash you compute from the envelope, so editing any byte of the verdict breaks the check. This is the same logic the standalone verifier (tools/verify-proof/from-endpoints.ts) runs in our own test suite.