Introduction

On September 1st 2025, zkSecurity started a security audit of Self’s Aadhaar-related Circom circuits. The audit lasted one week with two consultants. We reviewed the Self repository at commit 3905a30a.

This is zkSecurity’s third audit collaboration with Self. The previous audits are available here: first audit and second audit.

Scope

We reviewed the following Circom circuits and all their new or updated dependencies:

  • REGISTER_AADHAAR: verifies the authenticity of an Aadhaar QR code by checking its RSA signature; extracts structured user data; outputs a nullifier (deterministic identifier for uniqueness/Sybil‑resistance) and a commitment (for Merkle registration without exposing personal data).

  • VC_AND_DISCLOSE_Aadhaar: proves inclusion of the user’s commitment in a known Merkle tree; optionally discloses selected Aadhaar fields (e.g., DOB, address) via a bitmap selector; checks whether name and DOB/YOB appear on an OFAC‑like watchlist using Sparse Merkle Tree proofs.

As part of the dependency set, we also reviewed pack.circom, adapted from the Anon Aadhaar repository by PSE.

Finally, and independent of the Aadhaar focus of the audit, we reviewed a new generic, non-native field exponentiation circuit used for RSA verification:

Overview

Aadhaar QR code format

At its core, the Aadhaar QR code contains a serialized data structure containing various identification fields, which is digitally signed by UIDAI (Unique Identification Authority of India) using RSA with SHA-256. The Aadhaar circuits target the “V2” version of this structure. Unfortunately, official documentation for the Aadhaar QR code format is limited, and a specification of the structure of the serialized data is not publicly available. The parsing and extraction of fields is therefore based on reverse-engineering and empirical testing.

The fields are separated by the byte 0xff (255 in decimal). At the beginning, there are two constant bytes "V2" and then a separator byte 0xff. After that, there are the following fields, each separated by a 0xff byte:

  1. Email and phone presence flags, can be 0, 1, 2 or 3
  2. Reference ID, which is the last 4 digits of Aadhaar number and timestamp of the certificate generation
  3. Name
  4. Date of Birth
  5. Gender
  6. Address - Care of
  7. Address - District
  8. Address - Landmark
  9. Address - House
  10. Address - Location
  11. Address - Pin code
  12. Address - Post office
  13. Address - State
  14. Address - Street
  15. Address - Sub district
  16. Address - VTC
  17. Last 4 digits of the mobile number

After the 18th 0xff delimiter, it is stored the photo, up until the end of the data.

Nullifier design

The nullifier is a crucial component of the Self system, as it is used to prevent double-registration of the same document. Due to the fact that the full Aadhaar number itself is not included in the signed QR code, the nullifier must be derived from other fields. In the current design, the nullifier is computed as the Poseidon hash of the following fields:

  • Name
  • Date of Birth
  • Last 4 digits of Aadhar number

All other fields are not used in the nullifier computation, as the user can somewhat freely change them (e.g., address, phone number) and generate a new signed QR code.

We make two general remarks about this design:

  • As discussed in details in one finding below, it is not entirely true that the name field is immutable, as users can request changes to their name in the Aadhaar system with relatively low effort.
  • The nullifier is not hiding, since it is derived from low-entropy fields. This means that, for example, an attacker who knows the full name of a target registered user can recover their date of birth using very little effort (e.g., by brute-forcing all possible dates of birth and all possible last 4 digits of Aadhaar numbers).

We note that this is a design decision that Self team is aware of, and is inherently a trade-off between usability and security. We are not aware of any way to significantly improve the design without having access to more stable and high-entropy fields, which are not available in the signed data.