Introduction

On August 24th, 2026, zkSecurity started a security review of the zero-knowledge proof stack developed by EPFL and SICPA as part of the Innosuisse project Secure and Privacy-Preserving Credentials for E-ID. The project is a collaboration between EPFL’s Center for Digital Trust (C4DT) and SICPA. The review lasted two weeks with one senior consultant and focused on the soundness, completeness, and privacy of the circuits for credential presentation and their custom proving backend.

The reviewed stack uses Noir circuits to prove properties of Swiss E-ID SD-JWT credentials, including issuer authentication, holder binding, age verification, and non-revocation, without disclosing the complete credential. The circuits are compiled with EPFL’s T-256 fork of Noir and translated from ACIR into R1CS by a custom Spartan backend.

Scope

The review covered the following repositories at the commit references listed below:

The eid-privacy/noir T-256 fork at tag t256-v0.26-1 was originally outside the audit scope but was used to compile the circuits and generate witnesses. Selected parts were consulted to establish the intended ACIR and T-256 semantics. A preliminary review of the delta of this fork with respect to the original repository was performed with the help of our AI tool zkao.

Overview

The reviewed system is designed to let a user prove facts about a digital credential without directly revealing personal information. A trusted issuer signs the credential to authenticate its contents, such as date of birth and the public key associated with their device. The holder can then generate a cryptographic proof that they meet an age threshold (for instance, “I am 25 years old or older”) and possess the corresponding device private key, without disclosing their date of birth or the complete credential. The implementation uses credentials from Switzerland’s swiyu infrastructure, described below, together with Noir circuits, a modified Noir compiler, and a custom ACIR to R1CS backend using Vega, a proving system built on Spartan.

In the following we briefly explain the main architectural modules under review. The stack separates the definition of the proof from its generation for a particular credential. Developers write the required checks as Noir circuits. EPFL’s modified Noir compiler translates those circuits into ACIR, an intermediate representation of their constraints. The Spartan backend then translates ACIR into the constraint system used by the underlying Vega proving library.

For each presentation, the holder’s preprocessing tools read the credential and prepare the private inputs and signature intermediates needed by the circuit. Noir executes the circuit to compute a witness, and the Spartan backend uses that witness to generate a proof. The verifier checks the proof against the intended circuit and independently selected public inputs.

Architecture of the reviewed proving system
Figure 1: Intended production architecture for the C0200/C0202 T-256, Spartan backend, and Vega proving flow.

The scoped circuits use two distinct proving flows:

Circuits Credential format Compiler and proving backend
c0200 and c0202 swiyu SD-JWT T-256 Noir, Spartan backend, and Vega
c06 and c09 Custom fixed size credential string Standard Noir and Barretenberg

Figure 1 and the sections describing the T-256 toolchain and Spartan backend refer to the first flow. The non-revocation section later in this overview describes the separate C06/C09 experiments.

Swiyu credentials and selective disclosure

Swiyu is Switzerland’s infrastructure for issuing, holding, and verifying digital credentials, including the Swiss E-ID. Its credential profile uses SD-JWT, or Selective Disclosure JSON Web Token. A JWT carries a set of claims encoded as JSON and authenticated by an issuer’s digital signature. SD-JWT allows some claims to be represented by cryptographic hashes in the signed payload. The holder receives the corresponding disclosures, which contain the claim values and random salts, and can reveal selected disclosures for the verifier to check against those hashes.

The current swiyu Beta-ID includes selectively disclosable age threshold claims such as age_over_18, and the official verifier example requests only that claim. A simplified payload signed by the issuer can be represented as follows:

{
  "iss": "did:tdw:...",
  "_sd_alg": "sha-256",
  "_sd": ["<digest of the age disclosure>"],
  "cnf": {
    "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
  }
}

The holder may then select the corresponding disclosure, shown here after decoding:

["random salt", "age_over_18", true]

The verifier hashes the encoded disclosure and checks that the result occurs in the signed _sd array. It learns only the Boolean value asserted by the issuer, rather than the holder’s birth date. A zero-knowledge proof is therefore unnecessary for this predefined majority test. However, this selective disclosure mechanism works only because the issuer calculated age_over_18 in advance and included it in the documented Beta-ID credential. It cannot calculate a new predicate from a hidden birth date when the credential is presented and is limited to the thresholds and relationships chosen by the issuer.

The Innosuisse milestone report motivates the need for adding zero-knowledge proofs. Such proofs can evaluate predicates over private attributes without requiring the issuer to precompute every useful relationship, including richer statements such as an age interval. The project also aims to hide values unique to the credential, including the issuer signature and holder device public key, which could otherwise link separate presentations. The reviewed circuits follow this approach by privately checking the signed credential and birth date disclosure and proving an age predicate such as being at least 25. In this stack, the issuer uses a P-256 ECDSA signature, and the credential identifies a device public key used for holder binding.

Credential preprocessing

Before proof generation, a preprocessing step reads the credential and its signatures, performs part of the P-256 arithmetic outside the circuit, and prepares the private and public values used by the prover and verifier.

On the issuer side it recovers the ECDSA point R from the JWT signature and inverts s. The circuit later checks R=ts−1G+rs−1Qiss, where t is the SHA-256 digest of the JWT signing input converted into a circuit field element and r=R.x.

On the holder side, the device has signed a verifier challenge with a P-256 ECDSA pair (r,s). Performing the standard verification equation inside the circuit would require several scalar multiplications. The construction instead follows the modified ECDSA verification equation used by Microsoft’s Crescent. Starting from R=s−1(eG+rQdev), multiplying by sr−1 and rearranging gives Qdev=s(r−1R)−er−1G. Crescent therefore defines the following two derived points, as can also be seen in its reference implementation:

T=Rr−1,U=−er−1G,

where e is the challenge digest. This reduces the in circuit P-256 workload to one scalar multiplication and one point addition, moving the inversions and other point calculations into preprocessing and substantially reducing the constraint count. The circuit then checks x(Ts+U)=x(Qdev). T and U are public, so they can be checked against the challenge outside the circuit (s stays private). The recovered Rdev is used only to form T on the host.

Age verification circuits

The c0200_swiyu_jwt and c0202_sicpa_backend_constant circuits are intended to establish three facts in one proof: the issuer authenticated the credential payload, the hidden birth date satisfies the age requirement, and the device associated with the credential approved a verifier challenge. Both circuits keep the payload and birth date private while exposing the issuer key, current date, and values needed for holder binding as public inputs.

First, the circuits authenticate the credential. They reconstruct the JWT signing input from its header and payload, hash it with SHA-256, and check the resulting digest with the issuer’s P-256 public key. The preprocessing values R and s−1 are used to express this check as the issuer equation described above.

Second, the circuits check the age claim. They rebuild and encode the SD-JWT birth date disclosure ["salt","birth_date","value"], hash the encoded disclosure, and require the resulting digest to occur in the authenticated payload. They then interpret the disclosed value as a date in YYYY-MM-DD form and compare it with the verifier supplied current date to apply the 25 year threshold.

Third, the circuits perform the holder binding check. They extract the x-coordinate of the device public key from the credential and enforce the Crescent equation x(Ts+U)=x(Qdev). Here, s remains private, while T and U are public values derived during preprocessing. Complete holder authentication also requires the verifier to check that T and U correspond to its fresh challenge, as discussed later in this report.

The circuits do not parse the complete JSON structure. Instead, the payload is stored in a Noir BoundedVec, which has a fixed capacity and a separate logical length, and the prover supplies byte offsets identifying the disclosure digest and device key. This design choice is relevant to several findings below.

The two circuits otherwise enforce the same intended statement but construct the JWT signing input differently. c0200 fixes the encoded JWT header to {"typ":"JWT","alg":"ES256"}. c0202 instead accepts a public encoded header that also carries the issuer public key.

T-256 Noir toolchain

Noir circuits perform their arithmetic in a finite field. Noir’s usual configuration uses the scalar field of BN254, which is approximately 254 bits, and its embedded curve operations target Grumpkin. P-256 coordinates instead belong to a field defined by a different 256-bit prime. They therefore cannot in general be represented canonically as ordinary values in the BN254 field. This makes the usual Noir configuration unsuitable for directly expressing the P-256 operations used by the issuer and device signatures. Solving this usually involves chunking coordinates into smaller limbs, which requires more complex and less efficient arithmetic in-circuit.

EPFL’s eid-privacy/noir fork adds a t256 configuration for this purpose. In this configuration, Noir’s native Field and the ACIR FieldElement use the scalar field of Tom-256. Its modulus equals the P-256 coordinate field modulus, so P-256 x and y coordinates can be carried directly by circuit wires without first splitting them into smaller limbs.

P-256 quantity or operation Mathematical domain Effect of the T-256 circuit field
Coordinates x,y P-256 base field 𝔽p Native: the circuit field is exactly 𝔽p
Point addition and doubling Formulas over 𝔽p Efficient: coordinate arithmetic uses ordinary circuit field operations
Scalar k P-256 scalar field 𝔽n, where n≠p Not native: scalars require an explicit canonical range and arithmetic modulo n
Scalar multiplication kP P-256 group operation controlled by k∈𝔽n Still requires a custom MSM gadget, scalar bit decomposition, and repeated point operations

T-256 therefore makes the coordinate arithmetic inside P-256 point operations efficient, and indirectly reduces the cost of scalar multiplication, but it does not turn P-256 scalar multiplication into native field multiplication. P-256 scalars remain defined modulo n, not modulo the T-256 scalar modulus p, and must be range constrained and interpreted separately.

The nargo-t256 compiler is nargo built with this configuration. It runs the usual Noir compilation passes and emits ACIR over the new field. Calls to t256_ec_add and t256_msm become the standard ACIR EmbeddedCurveAdd and MultiScalarMul opcodes, while the fork’s T-256 solver interprets those opcodes as P-256 operations when generating the witness.

The Spartan backend then reads the ACIR and witness, translates the P-256 opcodes into R1CS constraints, and asks Vega to produce the proof. Vega’s Hyrax commitment operations use Tom-256 because the scalar field of the commitment curve must match the field of the R1CS. The two curves consequently have different roles. On the one hand, P-256 expresses the credential signature and holder binding equations. On the other hand, Tom-256 supports the proof system. Correctness depends on the compiler, witness solver, and Spartan lowering all assigning exactly the same meaning to the shared ACIR opcodes.

Spartan backend and Vega proving system

The repository and Rust crate retain the name spartan-backend, but proof generation and verification use Vega’s VegaZkSNARK implementation from the vega-prover library. Vega is built on the Spartan proof protocol and incorporates folding and reusable preparation. The custom backend supplies the ACIR lowering and gadgets, while Vega supplies the underlying proof system.

Together, the backend and Vega replace Noir’s usual Barretenberg proving step. The backend receives the compiled ACIR program and the witness generated by nargo-t256, then translates each supported ACIR operation into an equivalent Bellpepper R1CS constraint over the Tom-256 scalar field. The same synthesis process identifies the public inputs and is run during proof generation and verification before the resulting constraint system is passed to Vega.

The backend implements only the subset of ACIR needed by the reviewed circuits. The supported opcodes are AssertZero, MemoryInit, and MemoryOp, together with BlackBoxFuncCall for RANGE, Sha256Compression, EmbeddedCurveAdd, and MultiScalarMul. Multi scalar multiplication is limited to one point. Calls to separate constrained ACIR functions and unsupported black boxes, including ACIR’s native P-256 ECDSA operation, are rejected. Brillig computations are not translated into R1CS, and their outputs must already be present in the witness and must be constrained by later ACIR operations.

Embedded curve addition and scalar multiplication are handled by custom P-256 gadgets in the black box router. These gadgets implement the curve formulas inside R1CS and bind their calculated coordinates to the ACIR output witnesses. They are therefore responsible for enforcing all required point membership, scalar, predicate, and point at infinity rules.

The backend integrates Vega’s optional prepared proving mode for credentials that are presented repeatedly. The application identifies the inputs that may change for each presentation, such as the device signature scalar and the Crescent points in c0200. The backend’s dependency analysis divides the circuit into a reusable invariant part and an online part. Vega commits to the invariant witness during preparation and reuses that state with fresh randomness for subsequent proofs, while the backend recomputes the online portion. This improves presentation time, but it also makes correct dependency tracking and protection of the prepared state security sensitive.

Non-revocation proof

The zkp-pocs repository explores non revocation using a compact custom credential format rather than the swiyu SD-JWT format described above. Each credential is a fixed length string containing a device key, date of birth, and unique credential identifier at predetermined positions.

The issuer periodically signs a revocation list covering a consecutive range of credential identifiers. The list contains the first identifier in that range, a timestamp, and a bitmap with one bit for each credential. A set bit means that the corresponding credential has been revoked. The list and selected credential remain private, while the verifier supplies the issuer public key and current time.

The standalone c06_non_revocation circuit is intended to prove that the credential identifier falls within the selected list, its bit is clear, the public current time is before the list’s seven day expiration boundary, and the issuer signed the list. It isolates the non revocation calculation as a proof of concept. It does not authenticate the credential string from which the identifier is read, so by itself it does not establish that the checked identifier belongs to an issuer issued credential held by the prover.

The c09_full_proof circuit combines the same list check with three additional checks over the same credential: an issuer signature on the credential, a device signature on the verifier challenge, and an age threshold of 18. This binds the revocation identifier to the authenticated credential and is closer to a complete presentation than c06.

Both circuits verify P-256 signatures through Noir’s native std::ecdsa_secp256r1::verify_signature operation. The reviewed Spartan backend does not implement that ACIR black box, and the custom revocation list is not the status list format used by swiyu.

Summary

The reviewed stack is primarily a research and performance prototype. Its overall approach is technically coherent: the T-256 Noir fork provides a field suitable for representing P-256 coordinates, the Spartan backend translates the resulting ACIR into the Vega proof system, and the prepared proving flow demonstrates a promising way to reduce presentation time. We did not identify a fundamental flaw in this high level architecture. The findings instead concern concrete implementation details and missing validation in the compiler fork, backend, circuits, and verifier integration. Several of these omissions currently affect soundness, and the demonstrated components do not yet form a complete production presentation protocol.

Although known before this review, three limitations are central to its security: the demonstration verifier does not enforce trusted policy or presentation freshness, the T-256 compiler can accept invalid P-256 points during constant folding, and the Spartan backend omits P-256 point membership checks.

The additional implementation findings show further differences between the intended Noir program and the statement ultimately proved. The T-256 compiler can calculate an MSM with several terms incorrectly and embed that result in the compiled circuit. In the Spartan backend, missing constraints make range checks and scalar decomposition unsound. Its treatment of the point at infinity and other exceptional curve cases can both reject valid operations and produce false MSM results.

The application circuits introduce further weaknesses. In c0200 and c0202, prover supplied offsets can make the age and holder checks read data outside the issuer signed payload. Their issuer signature equation also accepts a zero inverse scalar and the point at infinity once the backend handles infinity correctly. Separately, C06 intentionally isolates the non revocation check for benchmarking; its documentation should clarify that it does not authenticate the credential or holder.

We also found narrower compatibility, completeness, and operational issues. These include a difference between the scalar domains accepted by ACIR and Spartan, incomplete handling of public return values, rejection of some valid issuer signatures, incomplete birth date validation, and sensitive witness data in prepared proving artifacts and debug logs. The detailed findings and discussion below describe the conditions under which each issue matters and distinguish defects in the proved relation from requirements that must be enforced by a production verifier.

Discussion and Recommendations

The findings above concern the relations enforced by the circuits and backend. A complete credential presentation also depends on protocol decisions outside those relations, including who selects each public input, which credential formats are accepted, and how current revocation state is obtained. This section makes those assumptions explicit and describes ways to reduce them as the prototype moves toward production.

Prover and verifier roles

A zero-knowledge proof binds its public inputs to the proved computation, but it does not establish that those inputs came from a trusted source. The relying party must independently select or validate every value that expresses policy, including the circuit verification key, issuer key, credential type, current time, and presentation challenge.

Although known before this review, the demonstration verifier does not enforce this separation. The same host flow that prepares the private witness also writes the public verifier inputs, including the Crescent points T and U derived from challenge_nonce. A production integration should instead have the verifier issue a fresh, expiring challenge, derive or validate the corresponding public values, pin the intended circuit and issuer policy, and reject reuse of the same presentation context.

Offset based JWT lookups

How far the JWT circuits go toward a presentation also depends on the assumptions we make about the credentials. In c0200/c0202, the date of birth and the holder key are not found by parsing the SD-JWT JSON. The prover supplies byte offsets (dob_sd_offset, x_offset), and the circuit matches a disclosure digest and a "x":" coordinate at those positions in the payload. That is a simple lookup using byte offsets rather than a JSON decoder. The present implementation of those lookups is also unsound; see Offsets supplied by the prover allow reads beyond the signed payload. Independently of that bug, the offset design is only as strong as those assumptions:

Level 1: rely on a controlled credential format. The circuit can keep matching bytes at offsets supplied by the prover, without proving their place in the JSON. This is sound only if:

  • Credential structure. Each accepted payload is one valid JSON object, with no trailing data other than whitespace, and with no repeated property that affects security. Every accepted credential follows a format and version covered by an explicit compatibility policy (the issuer signature covers vct, which these circuits do not check).
  • Date of birth. Claims supplied by the holder cannot contain arbitrary text or JSON fragments. A matching disclosure digest for the date of birth occurs only as a complete entry of the intended root _sd array, not in another claim, inside a longer value, or as a disclosure for a different birth_date claim.
  • Selective disclosure. The issuer only signs credentials that use SHA-256 for selective disclosure. The root _sd property is unique, and _sd_alg is not set to another algorithm.
  • Holder key. Every "x":" followed by 43 valid base64url characters is the intended holder key coordinate, or the same coordinate repeated. The payload contains no other device, recovery, or extension key with a different x. The intended key is a JWK with kty, crv, x, and y, 43-character unpadded base64url coordinates of a valid P-256 point, and every copy of the key uses the same coordinates.

We compared these assumptions with Swiyu Generic Issuer at commit a6d8ec46. That implementation emits a single JSON object, keeps cnf, vct, _sd, and _sd_alg out of subject data, uses SHA-256 disclosures, and stores the verified holder key under cnf.jwk. Whether other claims can contain data controlled by the holder still depends on the issuer integration. The same signing key can also cover several credential types. The c0200 sample stores the holder key under both cnf and cnf.jwk, while this issuer version uses only cnf.jwk. The layout has already changed, so a circuit that uses byte offsets needs a pinned type and version.

Level 2: validate the relevant JSON structure. If those restrictions on the issuer are not something the verifier wants to take as given, the circuit has to prove more about the payload: that dob_sd_offset is a complete 43-character element of the unique root _sd array, that _sd_alg is absent or uniquely sha-256, and that the holder key is the unique P-256 JWK at root.cnf.jwk, with complete x and y of a valid point. The scan should reject malformed structure, trailing data, repeated relevant properties, matches at the wrong nesting level, and incorrectly escaped strings. Supporting the c0200 layout also means checking that root.cnf.x matches root.cnf.jwk.x. Colliding bytes in unrelated fields then no longer matter. The circuit still assumes each accepted credential follows one of the supported layouts.

Level 3: parse the complete JSON document and validate its schema. If the same issuer key signs several credential types, or if the JSON layout is not frozen, the circuit parses the logical payload, rejects duplicate property names after decoding JSON escapes, and validates the schema selected by the authenticated vct. Claims are identified without fixed byte positions and without restricting unrelated fields. The verifier still pins which issuers and credential types it trusts.

Non-revocation

The C06 and C09 circuits appear to be preliminary experiments rather than components of the T-256 and Spartan flow. The repository describes them as proofs of concept, pins standard Noir v1.0.0-beta.21 and Barretenberg, and uses nargo compile followed by the Barretenberg bb commands for proof generation and verification. They do not compile with the T-256 Noir fork or produce proofs through the reviewed Spartan backend.

Integrating non-revocation with the rest of the stack is therefore still an open design problem. At a basic level, c06 and c09 are written for a packed credential string and verify signatures with std::ecdsa_secp256r1::verify_signature. The Spartan backend has no handler for that black box. The list they consume is also not the statuslist+jwt Token Status List that swiyu issues, and c0200/c0202 do not read status.status_list from the SD-JWT, so these circuits cannot be checked against the status registry swiyu already runs. C06 intentionally omits credential authentication to measure the non revocation component independently. This benchmark boundary should be documented explicitly; see Standalone non revocation circuit limitations should be documented.

For a presentation that includes both age and non-revocation to be sound, those checks also have to be about the same credential. One option is a single circuit, as c09 already does for the packed format. Another is to keep separate proofs and have the SD-JWT circuit extract the identifier from the signed payload, under the same kind of layout assumptions as the date of birth and the holder key, and share it with the revocation circuit, for example as a public output or as a commitment both proofs open.

Finally, the signed list should identify the context in which it is valid. Its current signed contents do not include a format version, registry identifier, credential type, status purpose, deployment identifier, or epoch. Without those fields, safe use assumes that the same signing key and credential identifier namespace are never reused across incompatible contexts. The mechanism used to distribute the latest state should provide this context and freshness without requiring a presentation time query that reveals the holder’s credential range or identifier.

Noir compiler and fork assurance

Noir is a sophisticated compiler with a substantial testing strategy, including unit and integration tests, differential fuzzing, and tests for individual optimization passes. Parts of the compiler have also received an internal security review, and the public issue tracker records at least one compiler finding from an external Veridise review. Formal verification tools are also emerging for selected components, including Noir’s SSA verification artifacts, NAVe’s analysis of compiled ACIR, and Lampe’s Lean model of Noir programs. Noir also has meaningful public adoption: at the time of this review its GitHub repository had approximately 1,400 stars and 400 forks, and its official ecosystem page listed projects using Noir across payments, infrastructure, and other applications.

These indicators support an assessment of moderate engineering maturity, but popularity, testing, and partial reviews do not prove that every compilation path preserves the relation expressed by the source program. Noir’s own security policy states that it is not fully audited or recommended for production. Its security assurance should therefore be considered below that of a production hardened or formally verified compiler, particularly for identity and authorization. The exact compiler version and generated ACIR remain part of the trusted computing base.

This assurance does not transfer automatically to the T-256 fork, which changes the circuit field and adds fork specific point operations, solver behavior, and constant folding paths. In our preliminary review of the fork’s delta from upstream Noir, we identified one High severity compiler issue: constant folding can calculate an MSM with several terms incorrectly, which can cause soundness failures in circuits that use several points with different scalars. The reviewed C0200 and C0202 circuits use only single point MSMs and are not affected by this particular issue. We recommend a deeper audit of the complete fork delta, including the interaction of its changes with compile time evaluation, optimization, witness generation, validation, and Spartan lowering.

Before moving toward production, the project should resolve the reported findings and Known Issues across the complete stack. We also recommend another independent audit once the circuits and surrounding protocol are close to their production design. In particular, the prover and verifier integration, non-revocation design, Spartan backend, and Noir fork will be important areas to audit given the project’s current prototype status.