Verification
PEAC verification is offline and deterministic. Given a receipt, the issuer's public key, and optionally a policy digest, verification produces the same result every time with no external dependencies.
Verification runs entirely locally. The verifier needs only the receipt (a JWS string) and the issuer's public key (Ed25519). No callback to the issuer, no token introspection endpoint, no online dependency.
Verification checks
The verifier runs a sequence of checks against each receipt. verifyLocal() auto-detects wire version and applies the appropriate checks.
Common checks (both wire formats)
| # | Check | Description |
|---|---|---|
| 1 | Signature valid | Ed25519 signature verification against the issuer's public key |
| 2 | Algorithm allowed | Algorithm must be EdDSA (no other algorithms accepted) |
| 3 | Type header present | typ must be peac-receipt/0.1 or interaction-record+jwt |
| 4 | Key ID present | kid must exist in the JWS header |
| 5 | Claims well-formed | All required claims present and correctly typed |
| 6 | Issued-at valid | iat is a valid Unix timestamp |
Wire 0.2 additional checks
| # | Check | Description |
|---|---|---|
| 7 | JOSE hardening | Rejects embedded keys (jwk, x5c, x5u, jku), crit, b64: false, zip |
| 8 | Issuer canonical | iss must be canonical (https:// or did: only) |
| 9 | Kind valid | kind must be evidence or challenge |
| 10 | Type grammar | type must be valid reverse-DNS or absolute URI |
| 11 | Pillars valid | All pillars must be from the 10-pillar taxonomy, sorted and unique |
| 12 | Extension schemas | Known extension groups pass their registered schema validation |
| 13 | Occurred-at coherence | occurred_at only allowed on evidence kind |
| 14 | Policy binding | Policy digest comparison (3-state: verified, failed, unavailable) |
Basic verification
Pass a receipt JWS string and the issuer's public key. verifyLocal() auto-detects wire version.
import { verifyLocal } from '@peac/protocol';
const result = verifyLocal(receiptJWS, publicKey);
if (result.valid) {
console.log('Wire version:', result.wireVersion); // '0.1' or '0.2'
console.log('Claims:', result.claims);
console.log('Policy binding:', result.policy_binding);
if (result.wireVersion === '0.2') {
console.log('Kind:', result.claims.kind);
console.log('Type:', result.claims.type);
console.log('Warnings:', result.warnings);
}
} else {
console.log('Verification failed:', result.reason);
console.log('Error code:', result.code);
}
With policy binding (Wire 0.2)
For Wire 0.2 receipts, provide a policy digest to verify policy binding.
import { verifyLocal, computePolicyDigestJcs } from '@peac/protocol';
// Compute digest from your policy document
const policyDigest = await computePolicyDigestJcs(policyDocument);
const result = verifyLocal(receiptJWS, publicKey, {
policyDigest,
});
if (result.valid) {
console.log('Policy binding:', result.policy_binding);
// 'verified' -- receipt policy matches local policy
// 'unavailable' -- either receipt or local policy digest absent
}
Policy binding returns one of three states:
| State | Meaning |
|---|---|
verified | Receipt policy digest matches local policy digest |
failed | Digests present but do not match (verification fails) |
unavailable | Either receipt or local policy digest is absent |
Result types
Success result
interface VerifySuccess {
valid: true;
wireVersion: '0.1' | '0.2';
claims: Wire01Claims | Wire02Claims;
policy_binding: 'verified' | 'failed' | 'unavailable';
warnings: VerificationWarning[]; // Wire 0.2 only
}
Failure result
interface VerifyFailure {
valid: false;
reason: string;
code: string; // E_INVALID_SIGNATURE, E_ISS_NOT_CANONICAL, etc.
}
VerificationWarning (Wire 0.2)
Wire 0.2 introduces advisory warnings that do not fail verification:
interface VerificationWarning {
code: string; // Warning code (e.g., 'type_unregistered')
message: string; // Human-readable description
pointer?: string; // RFC 6901 JSON pointer (e.g., '/type')
}
| Warning code | Meaning |
|---|---|
type_unregistered | Receipt type is not in the recommended type registry |
unknown_extension_preserved | Unknown extension key preserved without schema validation |
typ_missing | typ header absent (interop mode only) |
occurred_at_skew | occurred_at differs significantly from iat |
CLI verification
Verify a receipt directly from the command line.
npx peac verify <receipt-jws> --jwks keys.json
npx peac verify <receipt-jws> --issuer https://api.example.com
The CLI outputs a structured result with pass/fail status for each check.
MCP server verification
AI agents using the PEAC MCP server can verify receipts via the peac_verify tool.
{
"tool": "peac_verify",
"arguments": {
"receipt": "<jws-string>",
"issuer_url": "https://api.example.com"
}
}
The response includes the full verification result, decoded claims, wire version, and server metadata in the _meta field.