ZKSAR (Zero-Knowledge Sanctions/AML Reporting) attestation verification.
Verifies Xochi oracle-signed attestation results. The actual ZK proof
verification (Noir UltraHonk) happens on-chain or in the Xochi oracle.
This module verifies the oracle's signed result: type, expiry, issuer,
structural integrity, and (the security-relevant part) that the
attestation's ECDSA signature actually recovers to the claimed issuer
and that the issuer is on the caller's trusted allowlist.
Without the signature check a caller could forge an attestation by simply
naming a trusted issuer, inflating its Raxol.Payments.Zksar.TrustScore
to claim a higher Raxol.Payments.PrivacyTier and a lower settlement fee
tier than earned (and defeat the local sanctions/AML gate this module is
named for). verify/2 therefore recovers the signer and requires
signer == issuer in allowed_issuers by default.
Provisional digest scheme
attestation_digest/1 defines the bytes the oracle is assumed to sign
(an EIP-191 personal_sign over the canonical attestation fields). This
encoding has NOT yet been confirmed byte-for-byte against the live Xochi
oracle signer. Verification fails closed on any mismatch, so a wrong
scheme rejects real attestations (safe) rather than accepting forged ones.
The scheme must still be reconciled with Xochi, and a real on-chain
vector added, before enabling signature checks against production data.
Proof Types
Six ZK proof types from Noir circuits:
| Code | Type | Purpose |
|---|---|---|
| 0x01 | Compliance | Score below jurisdiction threshold |
| 0x02 | Risk Score | Score comparison without revealing score |
| 0x03 | Pattern | No structuring/velocity anomalies |
| 0x04 | Attestation | Valid credential exists |
| 0x05 | Membership | Address in whitelist |
| 0x06 | Non-Membership | NOT on sanctions list |
Summary
Functions
The 32-byte digest the oracle is assumed to sign for an attestation.
Parse a proof from Xochi API JSON (camelCase).
Look up numeric code from proof type name.
Look up proof type name from numeric code.
All known proof type names.
Verify a single attestation proof.
Verify a batch of proofs. Does not fail-fast.
Types
@type proof_type() ::
:compliance
| :risk_score
| :pattern
| :attestation
| :membership
| :non_membership
@type verification_error() ::
:expired
| :unknown_type
| :invalid_issuer
| :issuer_required
| :invalid_signature
| :malformed
@type verified_proof() :: %{ type: proof_type(), subject: String.t(), issuer: String.t(), issued_at: integer(), expires_at: integer(), valid: true }
Functions
@spec attestation_digest(proof()) :: <<_::256>>
The 32-byte digest the oracle is assumed to sign for an attestation.
An EIP-191 personal_sign over the canonical, newline-joined attestation
fields (a domain tag, then type code, issuer, subject, issued-at,
expires-at, and the lowercase-hex payload). Exposed so the Xochi oracle and
any local signer can reproduce the exact bytes, and so the scheme can be
reconciled across repos (see the module note on the provisional scheme).
Parse a proof from Xochi API JSON (camelCase).
Expected keys: typeCode, issuer, subject, issuedAt, expiresAt,
signature, payload (hex-encoded).
@spec proof_type_code(proof_type()) :: {:ok, pos_integer()} | :error
Look up numeric code from proof type name.
@spec proof_type_name(pos_integer()) :: {:ok, proof_type()} | :error
Look up proof type name from numeric code.
@spec proof_types() :: [proof_type()]
All known proof type names.
@spec verify( proof(), keyword() ) :: {:ok, verified_proof()} | {:error, verification_error()}
Verify a single attestation proof.
Checks type code, expiry, structural integrity, the issuer allowlist, and
(by default) that the ECDSA signature recovers to the claimed issuer.
Options
:now-- override current time (unix seconds) for testing.:allowed_issuers-- list of trusted oracle addresses (case-insensitive). Required when:verify_signatureis on (the default): a missing allowlist fails closed with:issuer_required, since trust cannot be established without knowing which signer is trusted.:verify_signature-- recover the signer fromsignatureand require it to equalissuer. Defaults totrue. Passfalseonly for structural checks where no real signature is available (tests, the pre-reconciliation transition window); this restores the old lenient allowlist behavior and is NOT safe against forgery.
Errors
:malformed | :expired | :unknown_type | :issuer_required | :invalid_issuer | :invalid_signature
@spec verify_batch( [proof()], keyword() ) :: {[verified_proof()], [{proof(), verification_error()}]}
Verify a batch of proofs. Does not fail-fast.
Returns {verified, errors} where errors are {proof, reason} tuples.