Every completed real-engine verdict has an append-only, Ed25519-signed proof chain. This page describes the strict relying-party check: signature, hash chain, terminal head, and binding to the request you expected.
A passing check means the proof bytes were signed by a trusted proof key and have not changed. It does not mean the verdict is correct.
You need the envelope entries, trusted proof keys, the trusted terminal head, and your expected verification_id / submission_hash. If a holder hands you an envelope, treat every authority field inside it as untrusted until independently checked.
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",
"proof_head_hash": "<server-served terminal content_hash>",
"entries": [
{
"seq": 0,
"key_id": "ed25519-...",
"algo": "ed25519",
"format": "acretix-verify/proof/2",
"prev_hash": null,
"content_hash": "<sha256 hex>",
"signature": "<base64 Ed25519>",
"envelope": "<the exact canonical JSON bytes that were hashed>"
}
]
}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" } ] }envelope.jwks_uri.proof_head_hash or your own pinned receipt. Never use a proof_head_hash copied from a holder-supplied envelope as the anchor.verification_id and submission_hash to your request. A valid but unrelated proof must fail.seq and require contiguous order from 0.entry.envelope bytes.entry.content_hash to equal the recomputed hash.format acretix-verify/proof/2 and algo ed25519.key_id, algo, format, seq, and prev_hash to the signed body.acretix-verify/proof-sig/2: plus the recomputed content hash.prev_hash equals the previous recomputed content hash.proof_head_hash.verification_id and submission_hash to match the request you expected.This sketch uses only Node built-ins. The JWKS argument must be fetched from your configured Acretix Verify origin or another pinned key source, and the expected head must come from a trusted side channel.
import { readFileSync } from 'node:fs';
import { createHash, createPublicKey, verify } from 'node:crypto';
const FORMAT = 'acretix-verify/proof/2';
const DOMAIN = 'acretix-verify/proof-sig/2:';
const [proofPath, jwksPath, expectedHead, expectedVerificationId, expectedSubmissionHash] = process.argv.slice(2);
if (!proofPath || !jwksPath || !expectedHead || !expectedVerificationId || !expectedSubmissionHash) {
throw new Error('usage: node verify-proof.mjs envelope.json trusted-jwks.json expected-head verification-id submission-hash');
}
// The JWKS file must come from your pinned/configured trust root, not envelope.jwks_uri.
// expectedHead must come from the server-served trusted proof_head_hash or your own pinned receipt,
// not from a holder-supplied envelope.
const proof = JSON.parse(readFileSync(proofPath, 'utf8'));
const jwks = JSON.parse(readFileSync(jwksPath, 'utf8'));
const entries = [...proof.entries].sort((a, b) => a.seq - b.seq);
let expectedPrev = null;
let terminalHash = null;
for (let index = 0; index < entries.length; index += 1) {
const entry = entries[index];
const hash = createHash('sha256').update(entry.envelope, 'utf8').digest('hex');
if (hash !== entry.content_hash) throw new Error('content_hash_mismatch');
const signed = JSON.parse(entry.envelope);
if (signed.format !== FORMAT || signed.algo !== 'ed25519') throw new Error('unsupported_format');
if (
entry.key_id !== signed.key_id ||
entry.algo !== signed.algo ||
entry.format !== signed.format ||
entry.seq !== signed.seq ||
entry.prev_hash !== signed.prev_hash
) {
throw new Error('envelope_mismatch');
}
if (entry.seq !== index || signed.seq !== index || entry.prev_hash !== expectedPrev) {
throw new Error('chain_invalid');
}
if (signed.verification_id !== expectedVerificationId || signed.submission_hash !== expectedSubmissionHash) {
throw new Error('subject_mismatch');
}
const jwk = jwks.keys.find((k) =>
k.kid === signed.key_id && k.kty === 'OKP' && k.crv === 'Ed25519'
);
if (!jwk) throw new Error('unknown_key');
const key = createPublicKey({ key: jwk, format: 'jwk' });
const ok = verify(null, Buffer.from(DOMAIN + hash, 'utf8'), key, Buffer.from(entry.signature, 'base64'));
if (!ok) throw new Error('signature_invalid');
expectedPrev = hash;
terminalHash = hash;
}
if (entries.length === 0) throw new Error('empty_chain');
if (terminalHash !== expectedHead) throw new Error('terminal_head_mismatch');
console.log('VERIFIED');| Code | Check |
|---|---|
content_hash_mismatch | The SHA-256 of entry.envelope does not equal entry.content_hash. |
unsupported_format | The signed envelope format is not acretix-verify/proof/2, or its algo is not ed25519. |
envelope_mismatch | The entry key_id, algo, format, seq or prev_hash differs from the signed envelope. |
chain_invalid | Entries are not numbered from 0 without gaps, or a prev_hash does not equal the previous content hash. |
subject_mismatch | The signed verification_id or submission_hash is not the one you expected. |
unknown_key | No Ed25519 key in your trusted JWKS has the entry key_id. |
signature_invalid | The Ed25519 signature over acretix-verify/proof-sig/2: plus the content hash does not verify. |
empty_chain | The proof has no entries. |
terminal_head_mismatch | The last content hash is not the trusted proof_head_hash. |
Fail closed on unknown keys, unsupported format, content-hash mismatch, envelope mismatch, broken chain, signature failure, subject mismatch, empty chain, and terminal-head mismatch. A missing argument, including the trusted terminal head, stops the script. For the reusable library version, see @trevor-ux/proof-verifier.