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+requestAttestationnow returns 422circuit_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_idandvk_hash, and come back verbatim assigned_payload.
Merkle fields are outside the signature (top-level merkle_*, and an unsigned section).
- Under
vk_only, verify withoutrequestAttestationstill returns 200, echoes the requestedcircuit_idand spends the proof, but the result is not signed. Never treat thatcircuit_idas proven;bound_circuit_idisnull. POST /v1/attest/proofnow spends the proof, the same as verify.
Action
- Upgrade:
npm i affixio@2.0.3.kind: "verify"(used bysdk.verify()) rejectsvk_onlyattestations and attestations withoutcircuit_binding(circuit_not_bound).sdk.verify()reports the server's bound circuit. - Until then, on 2.0.2: accept a
/verifyattestation only if its signed payload hascircuit_bindingequal to"server_prove_record"or"unique_vk"andcircuit_idis the one you expect. Treat attestations signed before 2026-10-11 (nocircuit_binding) as unbound. - If you need a third-party-checkable "AffixIO verified this proof for circuit X", use server mode:
/prove, then/v1/attest/proof, thenverifyAffixioAttestation(signed_payload, attestation, { expectedCircuitId }). - Check signatures over
response.signed_payload. Hand-rolled "response minus attestation and merkle_*" now fails (digest_mismatch), because responses also carrysigned_payloadandunsigned.
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/