# Audit of SecondFi's proof-tool circuits and prover patches to gnark

- **Client**: SecondFi
- **Date**: September 9, 2026
- **Tags**: gnark, R1CS, SHA-512, blake2b

## Introduction

On August 3rd, 2026, zkSecurity was commissioned to perform a security audit of the `proof-tool` repository for use by SecondFi, which enables users to prove ownership of a Cardano wallet credential with zero-knowledge, more importantly, without revealing their private keys or the derivation paths. The project also lets users create proofs in browsers by optimizing both the computational power and memory needed.

As such, this project includes the circuits and the prover patches to the `consensys/gnark` and the `consensys/gnark-crypto` repositories. The audit lasted two weeks with two consultants.

During the audit, the audit team identified two high-severity issues in the upstream code and two low-severity issues in the target repository. The client addressed the two high-severity issues, and the audit team reviewed the changes with tests and confirmed that both issues were properly fixed. While the client acknowledges the two low-severity issues and they remain open, the audit team believes the bugs are not practically exploitable.

### Scope

The scope of the audit included the circuits and the prover patches in the [`Emurgo/proof-tool`](https://github.com/Emurgo/proof-tool/blob/ba065e6) repository at commit `ba065e6`:

1. **The ownership circuits.** The `internal/circuit` folder contains the three ownership circuits under three subfolders: `ownership`, `ownershipdest` and `ownershipmulti`, implemented using the _gnark_ circuit library. Their primitives, `ckd` (child-key derivation), `ed25519`, `hash`, `sha256` and `u64util` are also in scope.
1. **Custom `consensys/gnark` and `consensys/gnark-crypto` patches.** The seven `.patch` files under `experiments/wasm-prover/patches`, which patches mostly to the backend in the `consensys/gnark` repository and the cryptographic primitives in the `consensys/gnark-crypto` repository.

## Summary

### The ownership circuits

The `ownership` circuit is proving the knowledge of a master extended private key that derives a given Cardano key credential without revealing neither the master key or the derivation path.

Here the master key and the derivation path are the private witnesses of the circuit:

1. The 96-byte master key is decomposed into three 32-byte `MasterKL`, `MasterKR` and `MasterCC` (following Cardano's [master key definition](https://cardano-foundation.github.io/cardano-wallet/concepts/master-key-generation)).
2. Two integers in $[0, 2^{31})$, namely, `Account` and `Index`, and an integer `Role` which is either 0, 1 or 2. They contribute to the [BIP32-Ed25519](https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf) derivation path.

With the private witnesses above, it could generate the credential and compare its hash against the only public input `Pub`. It follows the below steps:

1. Derive the Ed25519 private scalar, $\textsf{kL}$, from the master key, under the derivation path `m/1852'/1815'/account'/role/index`.
2. Compute the credential $\mathcal{C} = \texttt{Blake2b-224}(\textsf{kL} \cdot G)$. Here $G$ is the Ed25519 base point.
3. Compute the digest $\texttt{Blake2b-256}(\texttt{"ROOT-OWNERSHIP-v1"}\ \| \ \mathcal{C})$, taken modulo $r$ (BLS12-381's curve order), and compare it with `Pub`.

A ZKP can be constructed if `Pub` checks out with the digest.

The `ownershipdest` and `ownershipmulti` circuits are similar to the `ownership` circuit, except that

- The `ownershipdest` circuit computes the digest with $$\texttt{Blake2b-256}(\texttt{"ROOT-OWNERSHIP-DESTINATION-v1"}\ \| \ \mathcal{C} \ \| \ \textsf{destination}),$$ here $\textsf{destination}$ is a bytearray of size 58.
- The `ownershipmulti` circuit accepts $N$ copies of `(Account, Role, Index)`, where the exact $N$ is defined while the circuit is compiled. Then $\mathcal{C}_1, \dots, \mathcal{C}_N$ are derived based on the same master key with the $N$ derivation paths. The digest is defined as $$\texttt{Blake2b-256}(\texttt{"ROOT-OWNERSHIP-MULTI-v1"}\ \| \ N \ \| \ \mathcal{C}_1 \ \| \ \dots \ \| \ \mathcal{C}_N \ \| \ \textsf{destination}).$$

### The custom gnark patches

It is difficult to perform Groth16 on browsers because of intensive memory usage and large proving key (the current `.pk` is about 1.2 GiB). The seven patches towards `consensys/gnark` and `consensys/gnark-crypto` under the `experiments/wasm-prover/patches` folder made numerous improvements which attempt to 

1. reduce RAM usage so that proving on browsers are possible,
2. speed up the proving time, and
3. reduce the number of rows in R1CS, which lead to a smaller proving key and faster proving time.

Below is a brief summary of the seven patches:

- `prove-stream.patch` introduces a `ProveStream` and a `VectorSource` to enable the entries in the proving key to be loaded upon use (and discard afterwards).
- `domain-read-no-precompute.patch` adds an opt-in `ReadFromWithoutPrecompute` that skips the four FFT tables (`twiddles`, `twiddlesInv`, `cosetTable` and `cosetTableInv`), which the streaming prover never uses.
- `release-ccs-after-solve.patch` releases the constraint system after a successful `Solve`, as it is no longer needed.
- `dispatch-before-fft.patch` dispatches tasks before running FFT on the main goroutine, while the MSM workers are idle. This improves efficiency.
- `computeh-scoped-coset-tables.patch` reuses the same coset tables, which are otherwise built twice within `computeH`.
- `uints-constant-fold.patch` patches the `twoArgFn` method to not create constraints for operations with only compile-time constants. This removes the lookup constraints for the constant byte operations in the BLAKE2b and SHA-512 gadgets (IV and round constants, `Not`, padding).
- `computeh-parallel-transforms.patch` offloads the transforms in `computeH` to dedicated FFT workers when an `hEngine` is available: the inverse FFT of the three input vectors, then their forward FFT on the coset, then the final `ifft_coset` of `a` alone. Each phase falls back to the serial path on error.

## Findings

### Missing range checks of the carries of emulated field multiplication

- **Severity**: High
- **Location**: internal/circuit/ed25519/ed/gadget.go

**Description**. gnark v0.15.0 does _not_ range-check carries of emulated field multiplication. 

Public keys in Cardano are elements of Ed25519, which has `p=2^255 - 19`, different from the modulus of the native field, denoted $R$. In order to perform multiplication in the non-native field, to get $r = a \cdot b \mod p$, gnark first decomposes the inputs into a limb representation $x = \sum_{i=0}^{N-1} 2^{t\cdot i} x_i$. 

In the honest situation, a prover would compute the correct $r$ and $k$ such that $a \cdot b = r + k \cdot p$. They would then consider the decomposed field elements as polynomials: $a(X) = \sum_{i=0}^{N-1} a_i X^i$, and similarly for $b$. For $X = 2^t$, the relation is correct over the integers, and so $c(X) = (a(X)b(X) - r(X) - k(X)p(X))/(2^t - X)$ exists. The coefficients of $c(X)$ are small, roughly $< d \cdot N \cdot 2^t$ for a small constant $d$. Importantly, they must not be so large such that wrap $\mod R$. Specifically, since we want the equality of these polynomials to be over the integers, the coefficients of the two sides of the equations must be $< R$, imposing a requirement on the size of the coefficients of $c$. A sufficient bound would be $c_i < \frac{R}{2^t+1}$.

A malicious prover can choose $r$ as they wish, choose $k = (a \cdot b - r) \cdot p^{-1} \mod R$ and compute $c$ $\mod R$. Since $k$ is chosen such that the equation is satisfied only $\mod R$, the expression for $c$ is not polynomial division over the integers, and instead it is just polynomial division $\mod R$. As a result, $c$ exists $\mod R$, its limbs have size $\approx R$ and differ from the honest bounded values. $c$ being unbounded is what allows it.

**Impact**. Field multiplication is used in many places in the codebase, including elliptic curve addition. This allows a malicious prover to claim they own a victim's public key while starting from their own chosen seed.

**Recommendation**. Update gnark to v0.16.3 version.

**Client Response**. The client has fixed the issue at [PR#7](https://github.com/Emurgo/proof-tool/pull/7) by updating gnark to v0.16.3 ([`go.mod`](https://github.com/Emurgo/proof-tool/pull/7/changes#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6R7)). The audit team has reviewed the diff, retested and confirmed the fix is valid.

### Missing range checks to the output of `uapi.Xor` and `uapi.And`

- **Severity**: High
- **Location**: internal/circuit/hash/blake2b.go, internal/circuit/sha512/sha/gadget.go

**Description**. gnark v0.15.0 does _not_ range-check the output and the intermediate values for the operations in their `uints` package, including the `Xor` and `And` methods that is used to compute Blake2b and SHA-512 digests here.

Here gnark implements the aforementioned operations using BSB22, where there is a lookup table $\mathbb{T}$ with entries being

$$\mathbb{T} = \{ a + 256 \cdot b + 256^2 \cdot c \ | \ a, b \in \mathbb{Z}_{256} \wedge c = a \ \text{OP}\ b \}.$$

For instance, $\mathbb{T}_\textsf{xor}$ would contain entries like `0x000101` and `0x243713` because $\texttt{01}_{16} \oplus \texttt{01}_{16} = \texttt{00}_{16}$ and $\texttt{13}_{16} \oplus \texttt{37}_{16} = \texttt{24}_{16}$.

Methods like `uapi.Xor(a, b)` and `uapi.And(a, b)` compute $c = a \ \text{OP}\ b$ from a hint and check that $(a, b, c)$ satisfy the below constraints:

1. **Correctness.** $a + 256 \cdot b + 256^2 \cdot c \in \mathbb{T}$ and
2. **Input in range.** $a, b \in \mathbb{Z}_{256}$.

However, the output (and the intermediate values if there are more than two operands) are not range-checked. This would lead to an aliasing issue: One could alias $\texttt{000101}_{16}$ in $\mathbb{T}_\textsf{xor}$ with

$$(a, b, c) = (1, 0, 1/256),$$

because $a + 256 \cdot b + 256^2 \cdot c = 1 + 256 \cdot 0 + 256^2 \cdot 1/256 = 257 = \texttt{000101}_{16}$. This, when chained, could allow an malicious prover to return an arbitrary value from any fixed input. The `choose` method below is one of the examples:

```go
// snipped from proof-tool/internal/circuit/sha512/sha/gadget.go
func choose(uapi *uints.BinaryField[uints.U64], e, f, g uints.U64) uints.U64 {
	return uapi.Xor(g, uapi.And(e, uapi.Xor(f, g)))
}
```

**Impact**. This primarily allows a malicious prover to compute impossible digests like 

$$\texttt{SHA512}(\texttt{foo}) = \texttt{0000...00}_{16} \quad \text{and} \quad \texttt{Blake2b}(\texttt{bar}) = \texttt{0000...00}_{16}.$$

This would allow them to bind an arbitrary credential.

**Recommendation**. Update gnark to v0.16.3 version.

**Client Response**. The client has fixed the issue at [PR#7](https://github.com/Emurgo/proof-tool/pull/7) by updating gnark to v0.16.3 ([`go.mod`](https://github.com/Emurgo/proof-tool/pull/7/changes#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6R7) and corresponding [custom patch](https://github.com/Emurgo/proof-tool/pull/7/changes#diff-b4b2d06d15f83150ef54d73822d4a83863f7ee31bab7eff4e93a71baaa6ba099)). The audit team has reviewed the diff, retested and confirmed the fix is valid.

### Missing point validity checks allow multiple attempts at the Fiat-Shamir challenge

- **Severity**: Low
- **Location**: contracts/ownership-verifier/src/Ownership/Verify.hs

**Description**. [BSB22](https://eprint.iacr.org/2022/1072) allows for committing to data inside a circuit, such that the commitment can be used to derive a random challenge that suits further protocols. gnark uses the challenges for range-checks, batching computations, and more. The commitment is a point on the curve of the proof system.

An honest implementation uses the uncompressed form of the curve point and hashes it to derive a random challenge. The point is then compressed and used in the BSB22 verifier equation. A correct verifier has a bijective correspondence between compressed and uncompressed points. The SecondFi verifier implementation has some relaxation in its compression implementation. 

First, it does not check point validity, accepting to process any $(x, y)$ and checks the size of $y$ to decide whether to activate the corresponding bit in the compressed point. Second, it allows point coordinates that have active bits already in $x$ and encodes new ones by addition, such that overflow can cause unintended bits to activate.

These two facts allow a malicious prover to obtain the same compressed point for many different $y$s and use those attempts to lower the security of the system instead of being bound to one.

**Impact**. The current usage of the challenges seems to have a high margin of security, close to the curve scalar field size, and so trying out a reasonable number of points still leaves a good security margin. As a result, the implementation doesn't seem immediately vulnerable. The same issue would become critical if the security margins of any of the underlying checks are not sufficiently high.

**Recommendation**. Match the serialization specification to gnark, such that point validity is checked and active bits are cleared before compressing.

**Client Response**. The client has acknowledged the issue.

### Missing range checks of `MasterKR`, `MasterCC` and `Destination` fields in circuits

- **Severity**: Low
- **Location**: internal/circuit/ownership/circuit.go, internal/circuit/ownershipdest/circuit.go, internal/circuit/ownershipmulti/circuit.go

**Description**. In _gnark_, defining a wire to be of type `uints.U8` does not automatically generates rows to R1CS to constrain the wires.

**Impact**. Fields like `Destination` allows aliasing. However, there are no practical impacts in the current usage.

**Recommendation**. Apply range check constraints to the `MasterKR`, `MasterCC` and `Destination` and make sure that each byte is in $[0, 256)$.

**Client Response**. The client has acknowledged the issue.

---

This report was published on the [zkSecurity Audit Reports](https://reports.zksecurity.xyz) site by [ZK Security](https://www.zksecurity.xyz), a leading security firm specialized in zero-knowledge proofs, MPC, FHE, and advanced cryptography. For the full list of audit reports, see [llms.txt](https://reports.zksecurity.xyz/llms.txt).
