Security advisories

AFFIXIO-2026-001: circuit binding of /verify attestations (affixio 2.0.2 and earlier)

Severity: High · Affected: affixio npm 2.0.0 – 2.0.2 (and 1.x, which is already deprecated) · Fixed: API (live 2026-10-11) + affixio 2.0.3 (npm, 2026-10-11)

Summary

Many AffixIO circuits compile to identical bytecode and so share one UltraHonk verification key. A proof made for one of them verifies under all of them, and the proof doesn't say which circuit it was made for. Until 2026-10-11, POST /v1/circuits/:id/verify (and /v1/gate/verify) with requestAttestation: true signed an ML-DSA-65 attestation naming the circuit id the caller asked for, even when the proof could have been made for a different circuit sharing the key (vk_only). For example, a simple_yesno proof anyone can make locally could get a signed attestation for attested_boolean. affixio 2.0.2 verifyAffixioAttestation only checks the signature, so it accepts these attestations. If your relying party decides from the attested circuit_id ("AffixIO signed that this is an attested_boolean / security-clearance / … proof"), it can be fooled.

Who is affected

You're affected if you accept AffixIO /verify attestations made by another party, and the circuit they name matters to the decision. You're not affected if you only verify proofs you generated yourself for a fixed circuit, or you use POST /v1/attest/proof with SDK 2.0.3.

What changed on the API (2026-10-11, no action needed)

  • vk_only + requestAttestation now returns 422 circuit_not_bound. Nothing is signed or spent. Prove on the server first

(POST /v1/circuits/:id/prove), then verify or attest that proof.

  • Signed verify cores now include circuit_binding, bound_circuit_id, requested_circuit_id and vk_hash, and come back verbatim as signed_payload.

Merkle fields are outside the signature (top-level merkle_*, and an unsigned section).

  • Under vk_only, verify without requestAttestation still returns 200, echoes the requested circuit_id and spends the proof, but the result is not signed. Never treat that circuit_id as proven; bound_circuit_id is null.
  • POST /v1/attest/proof now spends the proof, the same as verify.

Action

  1. Upgrade: npm i affixio@2.0.3. kind: "verify" (used by sdk.verify()) rejects vk_only attestations and attestations without circuit_binding (circuit_not_bound). sdk.verify() reports the server's bound circuit.
  2. Until then, on 2.0.2: accept a /verify attestation only if its signed payload has circuit_binding equal to "server_prove_record" or "unique_vk" and circuit_id is the one you expect. Treat attestations signed before 2026-10-11 (no circuit_binding) as unbound.
  3. If you need a third-party-checkable "AffixIO verified this proof for circuit X", use server mode: /prove, then /v1/attest/proof, then verifyAffixioAttestation(signed_payload, attestation, { expectedCircuitId }).
  4. Check signatures over response.signed_payload. Hand-rolled "response minus attestation and merkle_*" now fails (digest_mismatch), because responses also carry signed_payload and unsigned.

Credit

Found by Articus (AffixIO internal testing), 2026-10-11. Published 2026-10-11. URL: https://www.affix-io.com/security/advisories/affixio-2026-001/

Free proof allocations

Agents get 150 free proofs. Humans get 100. No card required.

AI agents receive 150 free AffixIO SDK proofs on BoundProof Agent provision. Eligible new Hub account holders (humans) can claim one allocation of 100 free SDK proofs to test AffixIO verification, agentic payment checks, transaction intent proof and signed yes, no or review outcomes.

Terms: one allocation per account holder, per person or business owner. No card is required. Proofs expire after 30 days. Duplicate, shared, automated or abusive signups may be refused or removed. Agents: /boundproof/agent/. Humans: Hub onboarding with offer params.

See free proofs split