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 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.
  2. 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).
  2. Two integers in [0,231), namely, Account and Index, and an integer Role which is either 0, 1 or 2. They contribute to the BIP32-Ed25519 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, kL, from the master key, under the derivation path m/1852'/1815'/account'/role/index.
  2. Compute the credential 𝒞=Blake2b-224(kL·G). Here G is the Ed25519 base point.
  3. Compute the digest Blake2b-256(“ROOT-OWNERSHIP-v1”  𝒞), 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 Blake2b-256(“ROOT-OWNERSHIP-DESTINATION-v1”  𝒞  destination), here 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 𝒞1,,𝒞N are derived based on the same master key with the N derivation paths. The digest is defined as Blake2b-256(“ROOT-OWNERSHIP-MULTI-v1”  N  𝒞1    𝒞N  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.