Raxol.Payments.Zksar (Raxol Payments v0.2.0)

Copy Markdown View Source

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:

CodeTypePurpose
0x01ComplianceScore below jurisdiction threshold
0x02Risk ScoreScore comparison without revealing score
0x03PatternNo structuring/velocity anomalies
0x04AttestationValid credential exists
0x05MembershipAddress in whitelist
0x06Non-MembershipNOT 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

proof()

@type proof() :: %{
  type_code: pos_integer(),
  issuer: String.t(),
  subject: String.t(),
  issued_at: integer(),
  expires_at: integer(),
  signature: String.t(),
  payload: binary()
}

proof_type()

@type proof_type() ::
  :compliance
  | :risk_score
  | :pattern
  | :attestation
  | :membership
  | :non_membership

verification_error()

@type verification_error() ::
  :expired
  | :unknown_type
  | :invalid_issuer
  | :issuer_required
  | :invalid_signature
  | :malformed

verified_proof()

@type verified_proof() :: %{
  type: proof_type(),
  subject: String.t(),
  issuer: String.t(),
  issued_at: integer(),
  expires_at: integer(),
  valid: true
}

Functions

attestation_digest(map)

@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).

from_json(arg1)

@spec from_json(map()) :: {:ok, proof()} | {:error, :malformed}

Parse a proof from Xochi API JSON (camelCase).

Expected keys: typeCode, issuer, subject, issuedAt, expiresAt, signature, payload (hex-encoded).

proof_type_code(name)

@spec proof_type_code(proof_type()) :: {:ok, pos_integer()} | :error

Look up numeric code from proof type name.

proof_type_name(code)

@spec proof_type_name(pos_integer()) :: {:ok, proof_type()} | :error

Look up proof type name from numeric code.

proof_types()

@spec proof_types() :: [proof_type()]

All known proof type names.

verify(proof, opts \\ [])

@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_signature is 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 from signature and require it to equal issuer. Defaults to true. Pass false only 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

verify_batch(proofs, opts \\ [])

@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.