├── specs ├── 1 │ ├── lifecycle.png │ └── README.md ├── 2 │ ├── anon-aadhaar-circuit.png │ └── README.md ├── 3 │ ├── images │ │ ├── leanimt-structure.png │ │ └── semaphore-v4-identity.svg │ └── README.md └── 4 │ └── README.md ├── README.md └── LICENSE /specs/1/lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-ethereum/zkspecs/HEAD/specs/1/lifecycle.png -------------------------------------------------------------------------------- /specs/2/anon-aadhaar-circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-ethereum/zkspecs/HEAD/specs/2/anon-aadhaar-circuit.png -------------------------------------------------------------------------------- /specs/3/images/leanimt-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-ethereum/zkspecs/HEAD/specs/3/images/leanimt-structure.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zkspecs 2 | 3 | Specifications for various ZK and programmable cryptography protocols. 4 | 5 | See https://hackmd.io/UdMwH3nES8qTkRBOn02L8w for more context. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 zkspecs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /specs/1/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 1 3 | title: 1/COSS 4 | name: Consensus-Oriented Specification System 5 | status: draft 6 | category: Best Current Practice 7 | editor: Oskar Thoren 8 | contributors: 9 | - Pieter Hintjens 10 | - André Rebentisch 11 | - Alberto Barrionuevo 12 | - Chris Puttick 13 | - Yurii Rashkovskii 14 | - Daniel Kaiser 15 | --- 16 | 17 | This document describes a consensus-oriented specification system (COSS) for building interoperable technical specifications. 18 | COSS is based on a lightweight editorial process that seeks to engage the widest possible range of interested parties and move rapidly to consensus through working code. 19 | 20 | This specification is based on [Unprotocols 2/COSS](https://github.com/unprotocols/rfc/blob/master/2/README.md), used by the [ZeromMQ](https://rfc.zeromq.org/) project. 21 | It is equivalent except for some areas: 22 | 23 | - recommending the use of a permissive licenses, such as CC0 (with the exception of this document); 24 | - miscellaneous metadata, editor, and format/link updates; 25 | - more inheritance from the [IETF Standards Process][https://www.rfc-editor.org/rfc/rfc2026.txt], 26 | e.g. using RFC categories: Standards Track, Informational, and Best Common Practice; 27 | - standards track specifications SHOULD follow a specific structure that both streamlines editing, 28 | and helps implementers to quickly comprehend the specification 29 | - specifications MUST feature a header providing specific meta information 30 | 31 | ## License 32 | 33 | Copyright (c) 2008-24 the Editor and Contributors. 34 | 35 | This Specification is free software; 36 | you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; 37 | either version 3 of the License, or (at your option) any later version. 38 | 39 | This Specification is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 40 | without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 41 | See the GNU General Public License for more details. 42 | 43 | You should have received a copy of the GNU General Public License along with this program; 44 | if not, see http://www.gnu.org/licenses. 45 | 46 | ## Change Process 47 | 48 | This document is governed by the [1/COSS](spec/1) (COSS). 49 | 50 | ## Language 51 | 52 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in 53 | [RFC 2119](http://tools.ietf.org/html/rfc2119). 54 | 55 | ## Goals 56 | 57 | The primary goal of COSS is to facilitate the process of writing, proving, and improving new technical specifications. 58 | A "technical specification" defines a protocol, a process, an API, a use of language, a methodology, 59 | or any other aspect of a technical environment that can usefully be documented for the purposes of technical or social interoperability. 60 | 61 | COSS is intended to above all be economical and rapid, so that it is useful to small teams with little time to spend on more formal processes. 62 | 63 | Principles: 64 | 65 | * We aim for rough consensus and running code; [inspired by the IETF Tao](https://www.ietf.org/about/participate/tao/). 66 | * Specifications are small pieces, made by small teams. 67 | * Specifications should have a clearly responsible editor. 68 | * The process should be visible, objective, and accessible to anyone. 69 | * The process should clearly separate experiments from solutions. 70 | * The process should allow deprecation of old specifications. 71 | 72 | Specifications should take minutes to explain, hours to design, days to write, weeks to prove, months to become mature, and years to replace. 73 | 74 | Specifications have no special status except that accorded by the community. 75 | 76 | ## Architecture 77 | 78 | COSS is designed around fast, easy to use communications tools. 79 | Primarily, COSS uses a wiki model for editing and publishing specifications texts. 80 | 81 | * The *domain* is the conservancy for a set of specifications in a certain area. 82 | * Each domain is implemented as an Internet domain, hosting a wiki and optionally other communications tools. 83 | * Each specification is a set of wiki pages, together with comments, attached files, and other resources. 84 | * Important specifications may also exist as subdomains, i.e. child wikis. 85 | 86 | Individuals can become members of the domain by completing the necessary legal clearance. 87 | The copyright, patent, and trademark policies of the domain must be clarified in an Intellectual Property policy that applies to the domain. 88 | 89 | Specifications exist as multiple pages, one page per version of the specification (see "Branching and Merging", below), which may be assigned URIs that include an incremental number. 90 | Thus, we refer to a specification by specifying its domain, number, and short name. 91 | New versions of the same specification will have new numbers. 92 | The syntax for a specification reference is: 93 | 94 | /spec// 95 | 96 | For example, this specification is **rfc.vac.dev/spec/1/COSS**. 97 | The short form **1/COSS** may be used when referring to the specification from other specifications in the same domain. 98 | 99 | Every specification (including branches) carries a different number. 100 | 101 | ## COSS Lifecycle 102 | 103 | Every specification has an independent lifecycle that documents clearly its current status. 104 | 105 | A specification has six possible states that reflect its maturity and contractual weight: 106 | 107 | ![Lifecycle diagram](./lifecycle.png) 108 | 109 | ### Raw Specifications 110 | 111 | All new specifications are **raw** specifications. 112 | Changes to raw specifications can be unilateral and arbitrary. 113 | Those seeking to implement a raw specification should ask for it to be made a draft specification. 114 | Raw specifications have no contractual weight. 115 | 116 | ### Draft Specifications 117 | 118 | When raw specifications can be demonstrated, they become **draft** specifications. 119 | Changes to draft specifications should be done in consultation with users. 120 | Draft specifications are contracts between the editors and implementers. 121 | 122 | ### Stable Specifications 123 | 124 | When draft specifications are used by third parties, they become **stable** specifications. 125 | Changes to stable specifications should be restricted to cosmetic ones, errata and clarifications. 126 | Stable specifications are contracts between editors, implementers, and end-users. 127 | 128 | ### Deprecated Specifications 129 | 130 | When stable specifications are replaced by newer draft specifications, they become **deprecated** specifications. 131 | Deprecated specifications should not be changed except to indicate their replacements, if any. 132 | Deprecated specifications are contracts between editors, implementers and end-users. 133 | 134 | ### Retired Specifications 135 | 136 | When deprecated specifications are no longer used in products, they become **retired** specifications. 137 | Retired specifications are part of the historical record. 138 | They should not be changed except to indicate their replacements, if any. 139 | Retired specifications have no contractual weight. 140 | 141 | ### Deleted Specifications 142 | 143 | Deleted specifications are those that have not reached maturity (stable) and were discarded. 144 | They should not be used and are only kept for their historical value. 145 | Only Raw and Draft specifications can be deleted. 146 | 147 | ## Editorial control 148 | 149 | A specification MUST have a single responsible editor, 150 | the only person who SHALL change the status of the specification through the lifecycle stages. 151 | 152 | A specification MAY also have additional contributors who contribute changes to it. 153 | It is RECOMMENDED to use a process similar to [C4 process](https://github.com/unprotocols/rfc/blob/master/1/README.md) 154 | to maximize the scale and diversity of contributions. 155 | 156 | Unlike the original C4 process however, it is RECOMMENDED to use CC0 as a more permissive license alternative. 157 | We SHOULD NOT use GPL or GPL-like license. 158 | One exception is this specification, as this was the original license for this specification. 159 | 160 | The editor is responsible for accurately maintaining the state of specifications and for handling all comments on the specification. 161 | 162 | ## Branching and Merging 163 | 164 | Any member of the domain MAY branch a specification at any point. 165 | This is done by copying the existing text, and creating a new specification with the same name and content, but a new number. 166 | The ability to branch a specification is necessary in these circumstances: 167 | 168 | * To change the responsible editor for a specification, with or without the cooperation of the current responsible editor. 169 | * To rejuvenate a specification that is stable but needs functional changes. 170 | This is the proper way to make a new version of a specification that is in stable or deprecated status. 171 | * To resolve disputes between different technical opinions. 172 | 173 | The responsible editor of a branched specification is the person who makes the branch. 174 | 175 | Branches, including added contributions, are derived works and thus licensed under the same terms as the original specification. 176 | This means that contributors are guaranteed the right to merge changes made in branches back into their original specifications. 177 | 178 | Technically speaking, a branch is a *different* specification, even if it carries the same name. 179 | Branches have no special status except that accorded by the community. 180 | 181 | ## Conflict resolution 182 | 183 | COSS resolves natural conflicts between teams and vendors by allowing anyone to define a new specification. 184 | There is no editorial control process except that practised by the editor of a new specification. 185 | The administrators of a domain (moderators) may choose to interfere in editorial conflicts, 186 | and may suspend or ban individuals for behaviour they consider inappropriate. 187 | 188 | ## Specification Structure 189 | 190 | ### Meta Information 191 | 192 | Specifications MUST contain the following metadata. 193 | It is RECOMMENDED that specification metadata is specified as a YAML header (where possible). 194 | This will enable programmatic access to specification metadata. 195 | 196 | | Key | Value | Type | Example | 197 | |------------------|----------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 198 | | **shortname** | short name | string | 1/COSS | 199 | | **title** | full name | string | Consensus-Oriented Specification System | 200 | | **status** | status | string | draft | 201 | | **category** | category | string | Best Current Practice | 202 | | **tags** | 0 or several tags | list | zk-identity, anon-aadhaar | 203 | | **editor** | editor name/email | string | Oskar Thoren | 204 | | **contributors** | contributors | list | - Pieter Hintjens
- André Rebentisch
- Alberto Barrionuevo
- Chris Puttick
- Yurii Rashkovskii | 205 | 206 | 211 | 212 | ## Conventions 213 | 214 | Where possible editors and contributors are encouraged to: 215 | 216 | * Refer to and build on existing work when possible, especially IETF specifications. 217 | * Contribute to existing specifications rather than reinvent their own. 218 | * Use collaborative branching and merging as a tool for experimentation. 219 | * Use Semantic Line Breaks: https://sembr.org/. 220 | 221 | ## Appendix A. Color Coding 222 | 223 | It is RECOMMENDED to use color coding to indicate specification's status. Color coded specifications SHOULD use the following color scheme: 224 | 225 | * ![raw](https://raw.githubusercontent.com/unprotocols/rfc/master/2/raw.svg) 226 | * ![draft](https://raw.githubusercontent.com/unprotocols/rfc/master/2/draft.svg) 227 | * ![stable](https://raw.githubusercontent.com/unprotocols/rfc/master/2/stable.svg) 228 | * ![deprecated](https://raw.githubusercontent.com/unprotocols/rfc/master/2/deprecated.svg) 229 | * ![retired](https://raw.githubusercontent.com/unprotocols/rfc/master/2/retired.svg) 230 | * ![deleted](https://raw.githubusercontent.com/unprotocols/rfc/master/2/deleted.svg) 231 | 232 | 233 | -------------------------------------------------------------------------------- /specs/3/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 3 3 | title: 3/SEMAPHORE-V4 4 | name: Semaphore Protocol V4 5 | status: draft 6 | category: Standards Track 7 | tags: zero-knowledge, identity, privacy, anonymity, proof of membership, groups 8 | editor: Vivian Plasencia 9 | contributors: 10 | - Andy 11 | - Cedoor 12 | - Oskar Thoren 13 | - tags: 14 | - zero-knowledge 15 | - identity 16 | - groups 17 | - privacy 18 | --- 19 | 20 | # Semaphore V4 Specification 21 | 22 | # Change Process 23 | 24 | This document is governed by the [1/COSS](https://github.com/zkspecs/zkspecs/tree/main/specs/1) (COSS). 25 | 26 | # Language 27 | 28 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). 29 | 30 | # Abstract 31 | 32 | Semaphore V4 is a privacy-preserving, general-purpose protocol that allows users to prove they are part of a group and broadcast [signals](#message) without revealing their identity. Designed to be simple, lightweight, and highly efficient, it leverages a Merkle tree structure to minimize computational costs and provide strong security guarantees. 33 | 34 | Users generate their own Semaphore identity, which they can use to join a group structured as a Merkle tree, provided they meet the required criteria—typically proving ownership of specific credentials. Once added as a leaf, Semaphore enables them to anonymously share arbitrary [messages](#message), such as votes or endorsements, by proving their group membership within a zero-knowledge circuit, which also implicitly confirms they meet the group’s entry criteria. 35 | 36 | # Motivation 37 | 38 | Although Semaphore was created in 2019, its core motivations remain just as relevant today. Privacy continues to be a major challenge in digital identity, yet existing solutions are often either overly complex and difficult to use or, conversely, too simplistic to be practical. 39 | 40 | Despite advances in cryptographic primitives, most tools require intricate infrastructures that are hard for developers to grasp, while others rely solely on basic zero-knowledge signature verification. However, these approaches are often inefficient or lack strong security guarantees. 41 | 42 | Semaphore specifically aims to address the following challenges: 43 | 44 | - **Minimizing complexity**: Semaphore offers a simple and intuitive scheme. Users only need to create their identity and join a group with their credentials. 45 | - **Maintaining efficiency**: zk proofs (zero-knowledge proofs) can be generated and verified in under a second, ensuring fast and scalable performance. 46 | - **General-purpose design**: Semaphore is versatile and applicable to any use case where credential verification is required. 47 | - **Beyond anonymity**: In addition to preserving privacy, Semaphore provides key security guarantees, including: 48 | - **Double-signaling prevention**: Each zk proof includes a unique, deterministic nullifier to prevent reuse and ensure one-time [signaling](#signaling). 49 | - **Unlinkability**: All group members appear indistinguishable when generating zk proofs, preventing correlation across interactions. 50 | 51 | # Specification 52 | 53 | ## System Requirements 54 | 55 | Implementations MUST provide: 56 | 57 | ### 1. Identity 58 | 59 | Semaphore V4 identities use the [EdDSA](https://www.rfc-editor.org/rfc/rfc8032) signature scheme, implemented with the [Baby Jubjub](https://eips.ethereum.org/EIPS/eip-2494) elliptic curve and the [Poseidon](#poseidon-hash-function) hash function. 60 | 61 | An identity serves as a unique identifier for users and consists of an EdDSA public/private key pair along with a commitment. 62 | 63 | The private key is a secret value, generated randomly or derived from other secrets, and is used to generate the public key. The public key consists of a pair of coordinates representing a point on the elliptic curve. 64 | 65 | The identity commitment is computed as the Poseidon hash of the public key and acts as the public Semaphore identity, which is used in Semaphore groups. Instead of exposing the public key directly, it is RECOMMENDED to use the identity commitment, as it provides an additional layer of security and ensures a more compact, standardized representation of the identity. 66 | 67 | ![semaphore-v4-identity](./images/semaphore-v4-identity.svg) 68 | 69 | **Semaphore identity:** 70 | 71 | ```text 72 | { 73 | privateKey: bytes32 // The EdDSA private key 74 | secretScalar: bytes32 // The secret scalar derived from the private key. 75 | publicKey: Point // The EdDSA public key, derived from the secret scalar 76 | commitment: bytes32 // The identity commitment used as a public value in Semaphore groups 77 | } 78 | ``` 79 | 80 | The secret scalar is a value derived from the private key and used in the circuit to generate the commitment. 81 | 82 | Using the secret scalar in the circuit instead of the private key allows it to skip steps 1, 2, 3 in the generation of the public key defined in [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032#section-5.1.5), making the circuit more efficient and simple. 83 | 84 | Instead of SHA-512, the Blake 1 hash function is used to generate the secret scalar. This decision was made to maintain compatibility with the [circomlibjs implementation](https://github.com/iden3/circomlibjs/blob/main/src/eddsa.js) and the protocols that rely on it. 85 | 86 | ### 2. Group 87 | 88 | A group is a collection of identities represented in a structured format. 89 | 90 | A Semaphore group is a [Merkle Tree](#merkle-tree) in which each leaf is an identity commitment for a user. 91 | 92 | Semaphore uses the LeanIMT implementation, which is an optimized binary [Incremental Merkle Tree](#incremental-merkle-tree). The tree nodes are calculated using [Poseidon](#poseidon-hash-function). For more details on this data structure, see [Appendix A: LeanIMT](#appendix-a-leanimt). 93 | 94 | The group can support additional functions, but these are the primary ones: 95 | 96 | - Add member: Add an identity commitment to the leaves of the tree. 97 | - Update member: Update an identity commitment (a value of a leaf). 98 | - Remove member: Remove an identity commitment from the leaves. 99 | - Create proof of membership: Generate a [Merkle proof](#merkle-proof) of a leaf in the LeanIMT. 100 | 101 | ### 3. Nullifier 102 | 103 | The nullifier is a value designed to be a unique identifier for the zk proof. It is used to prevent the same zk proof from being used twice. In Semaphore, the nullifier is the Poseidon hash of the scope and secret scalar value of the user's Semaphore identity. 104 | 105 | ``` 106 | nullifier := Poseidon(scope || secret) 107 | ``` 108 | 109 | #### Scope 110 | 111 | A value used like a topic on which users can generate a valid zk proof only once. 112 | 113 | #### Secret 114 | 115 | The secret scalar derived from the identity private key. 116 | 117 | When using the same scope for an identity, the resulting nullifier remains the same because the same hash is generated. To obtain different nullifiers for the same identity (allowing users to share multiple zk proofs) users must use a different scope each time. 118 | 119 | ## Protocol Flow 120 | 121 | After users have generated their Semaphore identity and joined the group, implementations MUST: 122 | 123 | 1. Get the secret scalar from the Identity 124 | 2. Generate Merkle proof using the identity and Group 125 | 3. Generate circuit inputs 126 | 127 | Input requirements: 128 | 129 | ``` 130 | { 131 | secretScalar: bytes32 // Identity Secret Scalar 132 | merkleProofLength: uint8 // Length of Merkle proof Siblings 133 | merkleProofIndices: uint8[] // List of Indices to recompute the Merkle root 134 | merkleProofSiblings: bytes32[] // Merkle proof Siblings 135 | scope: bytes32 // Scope to generate the nullifier 136 | message: bytes32 // Message to share 137 | } 138 | ``` 139 | 140 | ## Circuit Design 141 | 142 | ### Private Inputs 143 | 144 | - `merkleProofLength`: Length of the Merkle Proof (Siblings Length) used to calculate the Merkle root. 145 | - `merkleProofSiblings`: Merkle Proof Siblings used to calculate the Merkle root. 146 | - `merkleProofIndices`: Merkle Proof Indices used to calculate the Merkle root. 147 | - `secret`: The secret is the scalar generated from the EdDSA private key. 148 | 149 | ### Public Inputs 150 | 151 | - `message`: The value the user shares when voting, confirming, sending a text message, etc. 152 | - `scope`: A value used like a topic on which users can generate a valid zk proof only once. The scope is supposed to be used to generate the nullifier. 153 | 154 | ### Circuit Operations 155 | 156 | 1. **EdDSA public key generation**: The EdDSA public key is derived from the `secret` using Baby Jubjub. The public key is a point with two coordinates (`Ax`, `Ay`). 157 | 158 | ``` 159 | (Ax, Ay) := BabyPbk()(secret) 160 | ``` 161 | 162 | 2. **Identity Commitment generation**: Calculate the hash of the public key. This hash is the Identity Commitment. 163 | 164 | ``` 165 | identityCommitment := Poseidon(Ax || Ay) 166 | ``` 167 | 168 | 3. **Proof of membership verification**: The Merkle root passed as output must be equal to that calculated within the circuit through the inputs of the Merkle proof. For more details, see [Appendix B: LeanIMT Root Circuit](#appendix-b-leanimt-root-circuit). 169 | 170 | ``` 171 | merkleRoot := LeanIMTRoot(identityCommitment, merkleProofLength, merkleProofIndices, merkleProofSiblings) 172 | ``` 173 | 174 | 4. **Nullifier generation**: The nullifier is generated by calculating the hash of the `scope` and the `secret`. 175 | 176 | ``` 177 | nullifier := Poseidon(scope || secret) 178 | ``` 179 | 180 | 5. **Dummy Square**: As the message is not really used within the circuit, the square applied to it is a way to force Circom's compiler to add a constraint and prevent its value from being changed by an attacker. More information in [this article by the Geometry team](https://geometry.xyz/notebook/groth16-malleability). This dummy square is tied to Circom and may not be necessary when using other technologies. 181 | 182 | ``` 183 | dummySquare := message * message 184 | ``` 185 | 186 | ### Outputs 187 | 188 | - `merkleRoot`: Merkle root of the LeanIMT. 189 | - `nullifier`: A value designed to be a unique identifier for the zk proof. It is used to prevent the same zk proof from being used twice. It's derived from the `scope` and `secret`. 190 | 191 | ### Component Interaction 192 | 193 | ![Semaphore V4 Circuit Colors](./images/semaphore-v4-circuit-colors.svg) 194 | 195 | ### Proof Generation 196 | 197 | #### Prover MUST 198 | 199 | - Generate the EdDSA public key using the `secret` value. 200 | - Generate the identity commitment using the EdDSA public key. 201 | - Verify proof of membership and generate the Merkle root. 202 | - Generate a unique nullifier. 203 | 204 | #### Semaphore Proof Output Format 205 | 206 | ```text 207 | { 208 | merkleTreeDepth: uint8, // Merkle Tree Depth 209 | merkleTreeRoot: bytes32, // Merkle Tree Root 210 | nullifier: bytes32, // Unique identifier for the zk proof 211 | message: bytes32, // Message to share 212 | scope: bytes32 // Scope 213 | proof: bytes // Zero-Knowledge Proof 214 | } 215 | ``` 216 | 217 | ### Proof Verification 218 | 219 | #### Verifier MUST 220 | 221 | - Validate the zero-knowledge proof provided by the user. 222 | 223 | #### Verifier SHOULD 224 | 225 | - Check if the nullifier has already been used to prevent double-signaling. 226 | 227 | #### Verifier MAY 228 | 229 | - Validate zk proofs that reference a `merkleTreeRoot` that was once valid but is no longer valid due to changes in the tree. For more details, see [Appendix C: Tree Root history](#appendix-c-tree-root-history). 230 | 231 | ## Error Handling 232 | 233 | Implementations MUST handle: 234 | 235 | - Invalid zk proof 236 | 237 | Implementations SHOULD handle: 238 | 239 | - Duplicate nullifier 240 | 241 | Error responses MUST include: 242 | 243 | - Error code 244 | - Error message 245 | - Error details (when available) 246 | 247 | # Interoperability Constraints 248 | 249 | It is RECOMMENDED to use: 250 | 251 | - EdDSA for the Identity component since it is a [zk-friendly](#zk-friendly) signature scheme. This allows the creation and verification of signatures using Semaphore identities. While other signature schemes could be used, they would not be compatible with the way Semaphore V4 identities are structured. 252 | - LeanIMT for the Group component because it efficiently supports adding, updating, and removing leaves while enabling proofs and verifications that a given leaf belongs to the tree (i.e., that an identity is part of a group). Other group representations can be used, but they will not be compatible with the current Semaphore V4 implementation. 253 | - Poseidon hash function in the circuit because it is zk-friendly. While other hash functions could be used, they would generate different outputs, making the identity commitment, group and nullifier incompatible with Semaphore V4. 254 | 255 | # Security Considerations 256 | 257 | ### Privacy and Security Assumptions 258 | 259 | The protocol assumes: 260 | 261 | - The proven security of the used proving system, and the trusted setup of the circuit if required by the proving system being used. 262 | - The proven security of the hash functions used. 263 | - The larger the group, the harder it is to distinguish individual members, as anonymity increases with the number of participants (larger anonymity set). 264 | 265 | ### Privacy and Security Best Practices 266 | 267 | - Both the private key and secret scalar SHOULD remain confidential and under the owner's control by performing identity generation off-chain on the user's device. This ensures that private identity values never leave the device, preventing identity reconstruction and unauthorized zk proof generation. 268 | - The Merkle proof (used to generate the zk proof) SHOULD be generated on the user's device to prevent exposing the identity commitment (leaf) used in the zk proof generation. 269 | - The zk proof SHOULD be performed off-chain on the user's device. Generating it on a server will allow the server to deanonymize the proof. 270 | 271 | ### Known limitations 272 | 273 | #### 1. Scalability 274 | 275 | As the number of members increases, rebuilding the tree (Semaphore Group) to generate a client-side Merkle proof becomes impractical due to memory constraints and longer processing times, ultimately degrading the user experience. 276 | 277 | [Appendix D: LeanIMT Benchmarks](#appendix-d-leanimt-benchmarks) shows that while small member sets are processed almost instantly, larger sets require significantly more time. Processing times increase from milliseconds for a few members to several minutes for millions of members. This delay makes real-time or frequent tree rebuilding infeasible for large-scale applications. 278 | 279 | **Possible solutions for scalability:** 280 | 281 | - A data structure that can limit the size of the trees, similar to the [Merkle Forest](https://github.com/Poseidon-ZKP/merkle-forest). 282 | - [Private Information Retrieval (PIR)](https://en.wikipedia.org/wiki/Private_information_retrieval). 283 | 284 | #### 2. Privacy in Group Joining 285 | 286 | Semaphore enables users to prove group membership without revealing their identity. However, joining a group typically requires sharing personal information with the group administrator, which is problematic when users want to keep data, like proof of identity or reputation, private. 287 | 288 | #### 3. Immediate Proof Generation After Joining a Group 289 | 290 | If members join a group and immediately generate a proof, their identity may be inferred, compromising privacy. 291 | 292 | **Example: Anonymous Event Feedback** 293 | Consider an event app that allows attendees to submit anonymous feedback. The app has a list of registered participants' emails but does not yet have a Semaphore group with these participants. If users generate their Semaphore identity and are added to the group right before submitting feedback, their anonymity is compromised, an observer could link new additions to generated proofs, revealing their identity. 294 | 295 | **Correct Usage** 296 | To maintain privacy, attendees need to generate their Semaphore identity and be added to the group in advance. Anonymous feedback submissions can take place once all users are included. 297 | 298 | ### Privacy Guarantees 299 | 300 | The protocol MUST guarantee: 301 | 302 | - That users cannot be deanonymized, i.e. the identity cannot be linked to the zk proof. 303 | 304 | # Implementation Notes 305 | 306 | The current [reference implementation](https://github.com/semaphore-protocol/semaphore) of the protocol is built with [Circom](https://docs.circom.io/) Groth16, and [snarkjs](https://github.com/iden3/snarkjs). 307 | 308 | ### Proof Generation 309 | 310 | ```ts 311 | // Generate the circuit input parameters using the function parameters 312 | // 1. Use the secretScalar from the identity 313 | // 2. Use the commitment from the identity and calculate the merkleProofLength, merkleProofIndices and merkleProofSiblings 314 | // 3. Call the snarkjs prove function 315 | const semaphoreProof = await generateProof(identity, group, message, scope) 316 | ``` 317 | 318 | ### Proof Verification 319 | 320 | ```ts 321 | // Call the snarkjs verify function 322 | const verified = await verifyProof(semaphoreProof) 323 | ``` 324 | 325 | # References 326 | 327 | 1. [Semaphore Whitepaper](https://semaphore.pse.dev/whitepaper-v1.pdf) 328 | 2. [Semaphore GitHub organization](https://github.com/semaphore-protocol) 329 | 3. [Semaphore repository](https://github.com/semaphore-protocol/semaphore) 330 | 331 | # Glossary 332 | 333 | ## Message 334 | 335 | In Semaphore, the term message (also known as _signal_) refers to the value a user shares when performing actions such as voting, confirming, sending a text message, and more. 336 | 337 | ## Signaling 338 | 339 | The act of sharing a message (e.g., a text message or vote). 340 | 341 | ## ZK-friendly 342 | 343 | zk-friendly refers to functions or data structures optimized for efficient computation in zero-knowledge proofs. They reduce constraints in proving systems like zk-SNARKs, lowering costs and proof sizes. Examples include Poseidon (hash function) and LeanIMT (data structure). 344 | 345 | ## Poseidon Hash Function 346 | 347 | Poseidon is a zk-friendly hash function. 348 | 349 | The [Poseidon](https://eprint.iacr.org/2019/458.pdf) implementation used by Semaphore operates over the BN254 elliptic curve and uses the POSEIDONπ − 128 instantiation. 350 | 351 | ## Binary Tree 352 | 353 | A Binary Tree is a tree data structure in which each node has at most two children, referred to as the left child and the right child. 354 | 355 | ## Incremental Merkle Tree 356 | 357 | An Incremental Merkle Tree (IMT) is a Merkle Tree (MT) designed to be updated efficiently by only appending new entries to the right-hand side. 358 | 359 | ## Merkle Tree 360 | 361 | A Merkle Tree (MT) is a tree (usually a binary tree) in which every leaf is a hash and every node that is not a leaf is the hash of its child nodes. 362 | 363 | ## Merkle Proof 364 | 365 | A Merkle proof is a proof that verifies the inclusion of a specific data element in a Merkle tree using a minimal set of hashes, allowing efficient and secure validation without revealing the entire dataset. 366 | 367 | # Appendix A: LeanIMT 368 | 369 | The LeanIMT (Lean Incremental Merkle Tree) is a Binary IMT with two fundamental properties: 370 | 371 | 1. Every node with two children is the hash of its left and right nodes. 372 | 2. Every node with one child has the same value as its child node. 373 | 374 | **Key Characteristics:** 375 | 376 | - The tree is always built from the leaves to the root. 377 | - The tree will always be balanced by construction. 378 | - The tree depth is dynamic and can increase with the insertion of new leaves. 379 | - To calculate a parent hash with two children, always start with the left child followed by the right. The order is never reversed. 380 | 381 | For more details on the LeanIMT, refer to the [LeanIMT paper](https://github.com/privacy-scaling-explorations/zk-kit/tree/main/papers/leanimt). 382 | 383 | ## Example of a LeanIMT Structure 384 | 385 | T - Tree 386 | V - Vertices (Nodes) 387 | E - Edges (Lines connecting Nodes) 388 | 389 | T = (V, E) 390 | V= {a0, a1, a2, H0, H1} 391 | E= {(a0, H0), (a1, H0), (a2, a2), (H0, H1), (a2, H1)} 392 | 393 |
394 | LeanIMT 395 |
396 | 397 | ## Insert 398 | 399 | Function to insert a new leaf into a LeanIMT. 400 | 401 | When inserting a new leaf, one of the following cases will occur at each level: 402 | 403 | 1. The new node is the left child: 404 | - The node will not be hashed. 405 | - Its value will be sent to the next level. 406 | 2. The new node is the right child: 407 | - The parent node will be the hash of the node’s sibling with itself. 408 | 409 | ## Update 410 | 411 | Function to update the value of a leaf of a LeanIMT. 412 | 413 | At each level, when updating a leaf's value, the impact depends on whether the node has a sibling: 414 | 415 | 1. No sibling: 416 | 417 | - The parent node inherits the new value directly. 418 | 419 | 2. Has a sibling: 420 | - The parent node’s value is updated as the hash of the updated node’s new value and its sibling. 421 | 422 | ## Remove 423 | 424 | Function to remove a leaf from a LeanIMT. 425 | 426 | Removing a leaf follows the same logic as the update function, but the leaf’s value is replaced with 0 (or another reserved invalid value). 427 | 428 | Effect: The parent node’s value is recalculated using this placeholder value. 429 | 430 | ## Generate Merkle Proof 431 | 432 | Function to generate a Merkle proof of a leaf in a LeanIMT. 433 | 434 | Merkle proofs verify the existence of a node in the tree. At each level, this process depends on whether the node has a sibling: 435 | 436 | 1. No Sibling: 437 | 438 | - Nothing is added to the proof at this level. 439 | - This case occurs when the node is both the last node in the level and a left node. 440 | 441 | 2. Has a Sibling: 442 | - If the node is a right child, 1 is added to the proof path, and the left sibling is stored 443 | - If the node is a left child, 0 is added to the proof path, and the right sibling is stored. 444 | 445 | # Appendix B: LeanIMT Root Circuit 446 | 447 | The LeanIMT Root circuit is designed to calculate the root of a LeanIMT given a leaf, its depth, and the necessary sibling information (aka proof of membership). A circuit is designed without the capability to iterate through a dynamic array. To address this, a parameter with the static maximum tree depth is defined (i.e. 'MAX_DEPTH'). And additionally, the circuit receives a dynamic depth as an input, which is utilized in calculating the true root of the Merkle tree. The actual depth of the Merkle tree may be equal to or less than the static maximum depth. Make sure to enforce `depth <= MAX_DEPTH` outside the circuit. 448 | 449 | This circuit is compatible with Binary Merkle Trees. 450 | 451 | # Appendix C: Tree Root History 452 | 453 | When the Merkle tree changes (due to adding, removing, or updating a member), the Merkle root also changes. If someone generates a zk proof using an outdated root (prior to the change), the proof itself remains valid, but verification will fail since the current root no longer matches the one used for proof generation. Ideally, these proofs should still be verifiable. 454 | 455 | One possible solution is to keep track of the past Merkle roots, allowing verification against those that were valid within a defined period. This ensures that proofs remain valid for a certain time even after the tree is updated. 456 | 457 | # Appendix D: LeanIMT Benchmarks 458 | 459 | The table below provides estimated build times for a LeanIMT with different numbers of members. 460 | 461 | All the benchmarks were run in an environment with these properties: 462 | 463 | **System Specifications:** 464 | 465 | Computer: MacBook Pro 466 | Chip: Apple M2 Pro 467 | Memory (RAM): 16 GB 468 | Operating System: macOS Sequoia version 15.3 469 | 470 | **Software environment:** 471 | 472 | Browser: Google Chrome Version 133.0.6943.127 (Official Build) (arm64) 473 | 474 | **Time Units:** 475 | 476 | ms: milliseconds 477 | s: seconds 478 | min: minutes 479 | 480 | | Members | Time | 481 | | --------- | ------ | 482 | | 100 | 31 ms | 483 | | 1 000 | 148 ms | 484 | | 200 000 | 24 s | 485 | | 500 000 | 1 min | 486 | | 1 000 000 | 2 min | 487 | | 2 000 000 | 4 min | 488 | | 3 000 000 | 6 min | 489 | 490 | # Copyright 491 | 492 | Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). 493 | -------------------------------------------------------------------------------- /specs/2/README.md: -------------------------------------------------------------------------------- 1 | ## Anon-Aadhaar spec raw 2 | 3 | --- 4 | 5 | slug: CS-02 6 | title: CS-02/ANON-AADHAAR-V2 7 | name: Anonymous Aadhaar Verification Protocol 8 | status: draft 9 | category: Standards Track 10 | editor: Yanis Meziane 11 | contributors: 12 | 13 | - Saleel P , Oskar Thoren 14 | - tags: 15 | - zero-knowledge 16 | - identity 17 | - privacy 18 | 19 | --- 20 | 21 | # Change Process 22 | 23 | This document is governed by the [1/COSS](../1) (COSS). 24 | 25 | # Language 26 | 27 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). 28 | 29 | # Abstract 30 | 31 | Anon Aadhaar is a zero-knowledge protocol enabling privacy-preserving verification of the [Aadhaar Secure QR Code](https://uidai.gov.in/en/ecosystem/authentication-devices-documents/qr-code-reader.html). [Aadhaar](https://en.wikipedia.org/wiki/Aadhaar) is a unique identity system in India, issued by the government, containing a 12-digit identification number linked to an individual's biometric and demographic data. The Aadhaar Secure QR code is an offline KYC process where a legitimate Aadhaar identity owner can store a QR code representing their concatenated identity and a digital signature of it. The protocol leverages this digital signature to allow proving possession of valid UIDAI-issued Aadhaar documents without revealing personal information, using RSA-SHA256 signature verification in zero-knowledge combined with selective disclosure mechanisms. 32 | 33 | # Motivation 34 | 35 | Current Aadhaar verification methods expose sensitive personal data, as the verifier of the signature requires full access to the entire identity. This approach compromises user privacy and limits the applicability of Aadhaar in decentralized and trustless systems. 36 | 37 | This protocol offers a solution for privacy-preserving and decentralized identity verification, addressing these challenges by enabling: 38 | 39 | - **Privacy-preserving verification of government-issued identity**: Ensuring that sensitive personal data is not exposed during verification. 40 | - **Selective disclosure of specific attributes**: Allowing users to reveal only the necessary attributes, such as age or residency, without disclosing the full identity. 41 | - **Prevention of identity reuse through nullifiers**: Guaranteeing that identity proofs are unique and cannot be reused fraudulently. 42 | - **Decentralized verification without trusted intermediaries**: Supporting verification on decentralized systems, such as validating proof of a valid Aadhaar on public decentralized system, like Byzantine system or a blockchain. 43 | 44 | # Specification 45 | 46 | ## System Requirements 47 | 48 | Implementations MUST provide: 49 | 50 | ### 1. SHA-256 Hashing 51 | 52 | To process the plaintext data, the SHA-256 hash of the data must be computed within the circuit. This is necessary because the Aadhaar Secure QR Code signs the hash of the data, not the plaintext itself. The protocol leverages the SHA-256 hashing function, as it is currently used in Aadhaar Secure QR Code specifications. 53 | 54 | ### 2. RSA-2048 Signature Verification 55 | 56 | The RSA-2048 signature verification must also be performed within the circuit. Constraining this process inside the circuit allows the proof to validate the correct execution of the signature verification against the specified public key. This ensures the authenticity of the signed data without exposing sensitive information. 57 | 58 | ### 3. QR Code Data Parsing and Selective Disclosure 59 | 60 | Once the signature is verified, the protocol ensures the data is authenticated. At this stage, specific identity attributes can be selectively disclosed and extracted from the QR code data. These attributes include: 61 | 62 | - Photo 63 | - Birthdate 64 | - Gender 65 | - State 66 | - Pincode 67 | - Timestamp of the signature 68 | 69 | For more details about data parsing, see [Appendix B - Data Formats](#Appendix-B-Data-Formats) 70 | 71 | ### 4. Nullifier Generation 72 | 73 | A nullifier is a core component of the protocol, designed to generate a unique identifier for the user without revealing sensitive identity attributes. The nullifier is constructed using the following formula: 74 | 75 | ``` 76 | Hash(Photo || NullifierSeed) 77 | ``` 78 | 79 | #### Photo 80 | 81 | The photo is used as it provides high entropy, reducing the risk of collision (e.g., two individuals generating the same nullifier). Using a photo also protects against dictionary attacks. For instance, a nullifier derived from simple attributes like name and birthdate could be easily recomputed, enabling tracking. 82 | 83 | #### Nullifier Seed 84 | 85 | The nullifier seed enables application- or action-specific nullifiers, ensuring that the same person generates distinct nullifiers across different applications or actions. This prevents cross-application linkage. The protocol recommends a nullifier seed of 16 bytes, allowing for \(2^{128}\) possible values. 86 | 87 | --- 88 | 89 | ## Preliminaries 90 | 91 | ### Proof Generation Environment 92 | 93 | The protocol is built with the assumption that closed servers cannot be trusted. Consequently, proof generation must occur locally on the client’s device. Sensitive data is never shared with third parties during the proof generation process, preserving user privacy. 94 | 95 | ### Nullifier Assumptions 96 | 97 | Nullifiers protect users from deanonymization by relying on the computational hardness of reversing a hash function. Only the issuer of the corresponding signature can feasibly link nullifiers, as they hold the original inputs. Additionally, since proof generation happens locally on the client, the nullifier seed is treated as public, maintaining privacy while ensuring robust linkage prevention. 98 | 99 | ### Selective Disclosure 100 | 101 | The protocol does not allow arbitrary disclosure of identity attributes but instead ensures that only group-secure attributes are revealed. This approach guarantees that even when all disclosed attributes are revealed, they belong to a sufficiently large set to prevent deanonymization. The protocol is designed for identity verification on public, immutable networks, and it prevents any sensitive data from being published publicly. 102 | 103 | ## Protocol Flow 104 | 105 | ### 1. Document Processing 106 | 107 | Implementations MUST: 108 | 109 | 1. Extract the signed message and the signature from the QR code data 110 | 2. Verify RSA-SHA256 signature 111 | 3. Parse signed data fields 112 | 4. Generate circuit inputs 113 | 114 | Input requirements: 115 | 116 | ``` 117 | { 118 | qrDataPadded: Bytes, // REQUIRED - QR code padded signed data 119 | qrDataPaddedLength: Bytes32 // REQUIRED - Length of the the QR code padded data 120 | signature: Bytes, // REQUIRED - RSA signature 121 | pubKey: Bytes, // REQUIRED - Public key 122 | nullifierSeed: Bytes32, // REQUIRED - Random seed of the nullifier 123 | signalHash: Bytes32, // OPTIONAL - Binding data 124 | ageAbove18: Boolean, // OPTIONAL - Disclosure flags 125 | gender: Boolean, // OPTIONAL - Disclosure flags 126 | state: Boolean, // OPTIONAL - Disclosure flags 127 | pincode: Boolean // OPTIONAL - Disclosure flags 128 | 129 | } 130 | ``` 131 | 132 | ## Circuit Design 133 | 134 | The current circuit design is made to optimise its implementation using Circom Groth16, but note that the same logics could be applied and implemented in other proving schemes. 135 | 136 | For more details, see [appendix A - Circuit Architecture](#Appendix-A-Circuit-Architecture) 137 | 138 | ### Inputs 139 | 140 | #### Private Inputs 141 | 142 | - `qrDataPadded: Bytes[]` - Padded signed data from QR code 143 | - `qrDataPaddedLength: BigInt` - Length of the the QR code padded data 144 | - `delimiterIndices: BigInt[]` - Set of the data delimiters 145 | - `signature: BigInt[]` - RSA signature from QR code 146 | - `pubKey: BigInt[]` - RSA public key 147 | 148 | #### Optional Private Inputs 149 | 150 | - `revealAgeAbove18: Boolean` - Flag to reveal age check 151 | - `revealGender: Boolean` - Flag to reveal gender 152 | - `revealState: Boolean` - Flag to reveal state 153 | - `revealPincode: Boolean` - Flag to reveal pincode 154 | 155 | #### Public Inputs 156 | 157 | - `nullifierSeed: BigInt` - Random seed for nullifier generation 158 | - `signalHash: BigInt` - Hash for binding external data 159 | 160 | ### Circuit Operations 161 | 162 | The circuit MUST perform the following operations in sequence: 163 | 164 | 1. **Signature Verification** 165 | a. **SHA-256 Hash** 166 | 167 | - Input: `qrDataPadded`, `qrDataPaddedLength` 168 | - Output: Hash of signed data 169 | - Purpose: Prepare data for RSA verification; Note that by inserting the plaintext data instead of the hash in the circuit let us access the clear data. 170 | 171 | b. **RSA Signature Verification** 172 | 173 | - Input: SHA-256 hash, `signature`, `pubKey` 174 | - Verification: PKCS#1 v1.5 padding check 175 | - Purpose: Ensure authentic signature of the data from the inserted public key. 176 | 177 | 2. **Field Extraction** 178 | 179 | - Input: Verified `signedData` 180 | - Operations: 181 | - Extract photo bytes 182 | - Extract timestamp 183 | - IF `revealAgeAbove18`: Extract birthdate and verify age > 18, by comparing birthdate with the timestamp 184 | - IF `revealGender`: Extract gender 185 | - IF `revealState`: Extract state 186 | - IF `revealPincode`: Extract pincode 187 | 188 | 3. **Final Computations** 189 | - Nullifier Generation: 190 | - Input: `nullifierSeed`, photo bytes 191 | - Output: `nullifier: Bytes32 := Hash(nullifierSeed | photo)` 192 | - Timestamp Conversion: 193 | - Convert extracted timestamp to UTC UNIX 194 | - Signal Constraints: 195 | - Apply constraints on `signalHash` 196 | - Hash pubKey: 197 | - `pubkeyHash := Hash(pubKey)` 198 | 199 | ### Outputs 200 | 201 | #### Required Outputs 202 | 203 | - `nullifier: Bytes32` - Unique identifier derived from seed and photo 204 | - `timestamp: Uint64` - UTC UNIX timestamp 205 | - `pubKeyHash: Bytes32` - Hash of RSA public key 206 | 207 | #### Optional Outputs (Only if requested) 208 | 209 | - `ageAbove18: Boolean` 210 | - `gender: BigInt` 211 | - `state: BigInt` 212 | - `pincode: BigInt` 213 | 214 | ### Component Interaction 215 | 216 | ![image](./anon-aadhaar-circuit.png) 217 | 218 | ### 1. Proof Generation 219 | 220 | #### **Prover MUST** 221 | 222 | - Validate the QR data signature to ensure integrity. 223 | - Extract all required fields from the Aadhaar Secure QR code. 224 | - Generate a unique `nullifier` for the user, ensuring it is consistent and non-colliding. 225 | - Create a valid zero-knowledge proof (`proof`) to encapsulate the extracted data while preserving privacy. 226 | - Reveal the hash of the `pubKey` used to verify the RSA signature 227 | 228 | #### **Prover MAY** 229 | 230 | - The prover can optionnaly reveal any relevant selective disclosure field by setting the corresponding boolean parameter to true (`ageAbove18`, `gender`, `state`, `pincode`) 231 | - The prover can optionnaly bind the proof to a specific value `signalHash`, which acts as a signature of the signal alongside the prover contraints checks 232 | 233 | #### **Circuit Constraints** 234 | 235 | - Enforce RSA signature validity against the QR data payload. 236 | - Perform field bounds checking to ensure all fields adhere to specified constraints of the secure Aadhaar QR code. 237 | 238 | ### Output Format 239 | 240 | The prover output should be structured as follows: 241 | 242 | ```json 243 | { 244 | "proof": "Bytes", // Zero-knowledge proof 245 | "pubKeyHash": "Bytes32", // RSA public key commitment 246 | "timestamp": "Uint64", // UTC Unix timestamp 247 | "nullifierSeed": "string", // Seed for nullifier generation 248 | "nullifier": "Bytes32", // Unique user identifier 249 | "signalHash": "Bytes32", // Optional transaction binding 250 | "ageAbove18": "Boolean", // Optional: Age verification 251 | "gender": "string", // Optional: Gender disclosure 252 | "pincode": "string", // Optional: Postal code 253 | "state": "string" // Optional: State of residence 254 | } 255 | ``` 256 | 257 | ### 2. Proof Verification 258 | 259 | #### **Verifier MUST** 260 | 261 | - Validate the zero-knowledge proof (`proof`) provided by the user. 262 | - Verify the RSA public key by ensuring the `pubKeyHash` corresponds to an official UIDAI public key. 263 | - Confirm the `nullifierSeed` matches the action set's initialization parameters. 264 | 265 | #### **Verifier SHOULD** 266 | 267 | - Validate the `nullifier` as a unique identifier for the user to prevent duplicate or unauthorized interactions. 268 | - Ensure the `timestamp` is within acceptable bounds, functioning as a Time-Based One-Time Password (TOTP) to confirm access to the UIDAI portal and fresh data generation. 269 | 270 | #### **Verifier MAY** 271 | 272 | - Verify the `signalHash` to bind the proof to an expected value, which helps prevent front-running by committing to an externally owned account (EOA) address. 273 | - Validate selectively disclosed attributes, such as: 274 | - `ageAbove18`: Confirm the user is over 18. 275 | - `gender`: Verify the disclosed gender. 276 | - `pincode`: Check the disclosed postal code. 277 | - `state`: Validate the disclosed state of residence. 278 | 279 | ## Error Handling 280 | 281 | Implementations MUST handle: 282 | 283 | 1. Invalid QR format 284 | 2. Invalid signature 285 | 3. Duplicate nullifier 286 | 4. Invalid proof 287 | 288 | Error responses MUST include: 289 | 290 | 1. Error code 291 | 2. Error message 292 | 3. Error details (when available) 293 | 294 | # Security Considerations 295 | 296 | ## Threat Model 297 | 298 | The protocol assumes: 299 | 300 | 1. The proven security of the used proving system, and the trusted setup of the circuit if required by the proving system being used 301 | 2. Secure and honest UIDAI RSA key, the trusting the UIDAI RSA private key will not sign non legitimate identities 302 | 303 | ## Known limitations: 304 | 305 | - Circuit Constraints Size 306 | - Aadhaar Secure QR Code Format and Nullifier Rotation 307 | 308 | **1. Circuit Size Constraints** 309 | 310 | The current reference implementation in Circom comprises approximately **1 million constraints**. The benchmarks for this implementation are as follows: 311 | 312 | - **Memory consumption at peak with bare metal Rapidsnark**: 1.4 GB 313 | - **Proving key size**: 600 MB (to be downloaded) 314 | 315 | **2. Considerations for Aadhaar Secure QR Code and Nullifier Rotation** 316 | 317 | - **QR Code Format Changes**: The protocol does not have control on the signature generation process. If the Unique Identification Authority of India (UIDAI) modifies the format of the Aadhaar Secure QR code, the protocol must be updated to accommodate the new data serialization format. The Secure QR code contains demographic information and the photograph of the Aadhaar holder, digitally signed by UIDAI to ensure security and integrity. 318 | 319 | - **Nullifier Rotation**: The nullifier is derived from the user's Aadhaar photo. UIDAI allows individuals to update their Aadhaar photograph by visiting an Aadhaar Enrolment Centre or Aadhaar Sewa Kendra. The process involves submitting a request and providing biometric details, with the update typically processed within 90 days. However, the actual time frame may vary, and users should consider the potential for generating multiple nullifiers within a short period if the photo is updated. This factor is crucial when developing use cases that require a nullifier to remain consistent for an extended duration. 320 | 321 | ## Privacy Guarantees 322 | 323 | The protocol MUST guarantee: 324 | 325 | 1. That the user cannot be deanonymized, except from the issuer of the signature. 326 | 2. That the unique identifier of the user (nullifier) cannot be used to track user's activity as soon as the nullifier seed changes accross applications. 327 | 328 | **Note**: While it cannot be enforced at the protocol level, the protocol strongly recommends that users SHOULD have access to a comprehensive UI that clearly displays what the prover is going to reveal when using selective disclosure and/or signal binding. 329 | 330 | # Implementation Notes 331 | 332 | The current [reference implementation](https://github.com/anon-aadhaar/anon-aadhaar) of the protocol is built with [Circom](https://docs.circom.io/) Groth16, and snarkjs. 333 | 334 | Basic proof generation: 335 | 336 | ```typescript 337 | // generateArgs is the component that will prepare the inputs for the circuit 338 | // https://github.com/anon-aadhaar/anon-aadhaar/blob/main/packages/core/src/generateArgs.ts 339 | const args = await generateArgs({ 340 | qrData: QRData, 341 | certificateFile: certificate, // x509 PEM certificate of the signing public key 342 | nullifierSeed: number, 343 | }); 344 | 345 | // Call to the snarkjs prove function 346 | const anonAadhaarProof = await prove(args); 347 | ``` 348 | 349 | Basic verification: 350 | 351 | ```typescript 352 | // Call to the snarkjs verify function 353 | const verified = await verify(anonAadhaarProof); 354 | ``` 355 | 356 | # References 357 | 358 | 1. [UIDAI Aadhaar Secure QR Code Specification](https://uidai.gov.in/images/resource/User_manulal_QR_Code_15032019.pdf) 359 | 2. [RSA PKCS#1 v2.1](https://www.rfc-editor.org/rfc/rfc2313) 360 | 3. [SHA-256 FIPS 180-4](https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.180-4.pdf) 361 | 362 | # Appendix A: Circuit Architecture 363 | 364 | ## Components 365 | 366 | ### 1. AadhaarQRVerifier 367 | 368 | **Description**: Verifies the Aadhaar QR data using RSA signature. 369 | 370 | **Parameters**: 371 | 372 | - `n`: RSA public key size per chunk. 373 | - `k`: Number of chunks the RSA public key is split into. 374 | - `maxDataLength`: Maximum length of the data. 375 | 376 | **Inputs**: 377 | 378 | - `qrDataPadded`: QR data without the signature; assumes elements to be bytes; remaining space is padded with 0. 379 | - `qrDataPaddedLength`: Length of padded QR data. 380 | - `delimiterIndices`: Indices of delimiters (255) in the QR text data. Includes 18 delimiters. 381 | - `signature`: RSA signature. 382 | - `pubKey`: RSA public key (of the government). 383 | - `revealAgeAbove18`: Flag to reveal if age is above 18. 384 | - `revealGender`: Flag to reveal extracted gender. 385 | - `revealPinCode`: Flag to reveal extracted pin code. 386 | - `revealState`: Flag to reveal extracted state. 387 | - `nullifierSeed`: Random value used as input to compute the nullifier. 388 | - `publicSignalHash`: Any message to commit to (as part of the proof). 389 | 390 | **Outputs**: 391 | 392 | - `pubkeyHash`: Poseidon hash of the RSA public key (after merging chunks). 393 | - `nullifier`: Unique value derived from `nullifierSeed` and Aadhaar data. 394 | - `timestamp`: Timestamp of when the data was signed (converted to Unix timestamp). 395 | - `ageAbove18`: Boolean flag indicating age is above 18; 0 if not revealed. 396 | - `gender`: Gender (`70` for Female, `77` for Male); 0 if not revealed. 397 | - `pinCode`: Pin code as integer; 0 if not revealed. 398 | - `state`: State packed as integer (reverse order); 0 if not revealed. 399 | 400 | ### 2. SignatureVerifier 401 | 402 | **Description**: Verifies the Aadhaar signature. 403 | 404 | **Parameters**: 405 | 406 | - `n`: RSA public key size per chunk. 407 | - `k`: Number of chunks the RSA public key is split into. 408 | - `maxDataLength`: Maximum length of the data. 409 | 410 | **Inputs**: 411 | 412 | - `qrDataPadded`: QR data without the signature; each number represents ASCII byte; remaining space is padded with 0. 413 | - `qrDataPaddedLength`: Length of padded QR data. 414 | - `signature`: RSA signature. 415 | - `pubKey`: RSA public key. 416 | 417 | **Output**: 418 | 419 | - `pubkeyHash`: Poseidon hash of the public key. 420 | 421 | ### 3. Nullifier 422 | 423 | **Description**: Computes the nullifier for an Aadhaar identity. 424 | 425 | **Inputs**: 426 | 427 | - `photo`: The photo of the user with SHA padding. 428 | 429 | **Output**: 430 | 431 | - `nullifier`: Computed as `hash(nullifierSeed, hash(photo[0:15]), hash(photo[16:31]))`. 432 | 433 | ### 4. TimestampExtractor 434 | 435 | **Description**: Extracts the timestamp when the QR was signed, rounded to the nearest hour. 436 | 437 | **Inputs**: 438 | 439 | - `nDelimitedData[maxDataLength]`: QR data where each delimiter is `255 * n`, where `n` is the order of the data. 440 | 441 | **Outputs**: 442 | 443 | - `timestamp`: Unix timestamp. 444 | - `year`: Year of the signature. 445 | - `month`: Month of the signature. 446 | - `day`: Day of the signature. 447 | 448 | ### 5. AgeExtractor 449 | 450 | **Description**: Extracts the date of birth from the Aadhaar QR data and returns it as a Unix timestamp. 451 | 452 | **Parameters**: 453 | 454 | - `maxDataLength`: Maximum length of the data. 455 | 456 | **Inputs**: 457 | 458 | - `nDelimitedData[maxDataLength]`: QR data where each delimiter is `255 * n`. 459 | - `startDelimiterIndex`: Index of the delimiter after which the date of birth starts. 460 | - `currentYear`: Current year to calculate age. 461 | - `currentMonth`: Current month to calculate age. 462 | - `currentDay`: Current day to calculate age. 463 | 464 | **Output**: 465 | 466 | - `out`: Unix timestamp representing the date of birth. 467 | 468 | ### 6. GenderExtractor 469 | 470 | **Description**: Extracts the gender from the Aadhaar QR data. 471 | 472 | **Inputs**: 473 | 474 | - `nDelimitedDataShiftedToDob[maxDataLength]`: QR data where each delimiter is `255 * n` shifted to the DOB index. 475 | - `startDelimiterIndex`: Index of the delimiter after which gender starts. 476 | 477 | **Output**: 478 | 479 | - `out`: Single byte number representing gender. 480 | 481 | ### 7. PinCodeExtractor 482 | 483 | **Description**: Extracts the pin code from the Aadhaar QR data. 484 | 485 | **Inputs**: 486 | 487 | - `nDelimitedData[maxDataLength]`: QR data where each delimiter is `255 * n`. 488 | - `startDelimiterIndex`: Index of the delimiter after which the pin code starts. 489 | - `endDelimiterIndex`: Index of the delimiter up to which the pin code is present. 490 | 491 | **Output**: 492 | 493 | - `out`: Pin code as integer. 494 | 495 | ### 8. PhotoExtractor 496 | 497 | **Description**: Extracts the photo from the Aadhaar QR data. 498 | 499 | **Inputs**: 500 | 501 | - `nDelimitedData[maxDataLength]`: QR data where each delimiter is `255 * n`. 502 | - `startDelimiterIndex`: Index of the delimiter after which the photo starts. 503 | - `endIndex`: Index of the last byte of the photo. 504 | 505 | **Output**: 506 | 507 | - `out`: Integer array (`int[33]`) representing the photo in big-endian order. 508 | 509 | ### 9. QRDataExtractor 510 | 511 | **Description**: Extracts the name, date, gender, and photo from the Aadhaar QR data. 512 | 513 | **Inputs**: 514 | 515 | - `data[maxDataLength]`: QR data without the signature, padded. 516 | - `qrDataPaddedLength`: Length of the padded QR data. 517 | - `delimiterIndices[17]`: Indices of the delimiters in the QR data. 518 | 519 | **Outputs**: 520 | 521 | - `name`: Single field integer representing the name in big-endian order. 522 | - `age`: Unix timestamp representing the date of birth. 523 | - `gender`: Single byte number representing gender. 524 | - `photo`: Photo of the user with SHA padding. 525 | 526 | ### 10. DigitBytesToTimestamp 527 | 528 | **Description**: Converts a date string of format `YYYYMMDDHHMMSS` to a Unix timestamp. 529 | 530 | **Parameters**: 531 | 532 | - `maxYears`: Maximum year that can be represented. 533 | - `includeHours`: Include hours (1) or round down to day (0). 534 | - `includeMinutes`: Include minutes (1) or round down to hour (0). 535 | - `includeSeconds`: Include seconds (1) or round down to minute (0). 536 | 537 | **Inputs**: 538 | 539 | - `in`: Input byte array representing the date string. 540 | 541 | **Output**: 542 | 543 | - `out`: Integer representing the Unix timestamp. 544 | 545 | ## Component Interaction 546 | 547 | ![image](https://hackmd.io/_uploads/Byb23aRfkx.png) 548 | 549 | # Appendix B: Data Formats 550 | 551 | ## QR Code Format 552 | 553 | Based on the UIDAI documentation referenced (User Manual QR Code 2019), the QR code encoding for Aadhaar involves binary data, with delimiters indicating the structure of encoded fields. Here's a technical breakdown to extract data based on delimiters (value 255): 554 | 555 | ### Steps for Data Extraction: 556 | 557 | 1. **Understand the Data Structure**: 558 | 559 | - The binary QR code data is organized into fields separated by the delimiter `255` (0xFF in hexadecimal). 560 | - Each field corresponds to a specific piece of Aadhaar information (e.g., name, gender, date of birth, etc.). 561 | 562 | 2. **Convert Binary Data to Array**: 563 | 564 | - Read the binary QR code data into a byte array. 565 | - Example: `binaryData = [byte1, byte2, ..., byteN]`. 566 | 567 | 3. **Iterate Through Byte Array**: 568 | 569 | - Loop through the byte array and identify indices where the value `255` appears. 570 | - Use these indices to fill the `delimitersIndices` array, that will help us parse the data inside of the circuit. 571 | 572 | 4. **Extract Fields**: 573 | 574 | - Use the `delimitersIndices` to slice the `qrDataPadded` array. 575 | 576 | ### **Aadhaar QR Code Data Schema (V2)** 577 | 578 | - **Key Changes in V2**: 579 | 580 | 1. The version identifier `V2` ([86, 50] in ASCII) is present before the first delimiter (`255`). 581 | 2. Mobile/email hash is removed. 582 | 3. The last 4 digits of the mobile number are included before the photo. 583 | 4. A total of 16 fields, followed by the last 4 digits of the mobile number, photo data, and a signature. 584 | 585 | - **Field Order**: 586 | | **Index** | **Field Name** | **Description** | 587 | |-----------|--------------------------------------|------------------------------------------------------| 588 | | 1 | Email/Mobile Present Bit Indicator | 0: None, 1: Email, 2: Mobile, 3: Both | 589 | | 2 | Reference ID | Last 4 digits of Aadhaar number and timestamp | 590 | | 3 | Name | Resident's full name | 591 | | 4 | Date of Birth | In `YYYY-MM-DD` format | 592 | | 5 | Gender | `M` (Male), `F` (Female), `T` (Transgender) | 593 | | 6 | Address > Care of (C/O) | Guardian/parent name | 594 | | 7 | Address > District | District name | 595 | | 8 | Address > Landmark | Landmark (if available) | 596 | | 9 | Address > House | House name/number | 597 | | 10 | Address > Location | Locality or location name | 598 | | 11 | Address > Pin code | Postal code of residence | 599 | | 12 | Address > Post office | Name of the post office | 600 | | 13 | Address > State | State/UT name | 601 | | 14 | Address > Street | Street name | 602 | | 15 | Address > Sub district | Sub-district name | 603 | | 16 | VTC (Village/Town/City) | Name of the village, town, or city | 604 | | 17 | Last 4 digits of the mobile number | Last 4 digits of registered mobile number | 605 | | 18 | Photo | Base64-encoded JPEG, from 18th `255` till end | 606 | | - | Signature | Last 256 bytes of the binary data | 607 | 608 | --- 609 | 610 | # Copyright 611 | 612 | Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). 613 | -------------------------------------------------------------------------------- /specs/4/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | slug: CS-04 4 | title: CS-04/EXCUBIAE 5 | name: Excubiae Smart Contract Framework 6 | status: draft 7 | category: Standards Track 8 | editor: Giacomo Corrias (0xjei) <0xjei@pse.dev> 9 | contributors: 10 | - ... 11 | - tags: 12 | - smart contract 13 | - gatekeeper 14 | - framework 15 | - composable 16 | - policy 17 | - checker 18 | 19 | --- 20 | 21 | # Change Process 22 | 23 | This document is governed by the [1/COSS](../1) (COSS). 24 | 25 | # Language 26 | 27 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). 28 | 29 | # Abstract 30 | Excubiae is a composable framework for implementing custom, attribute-based access control policies on EVM-compatible networks. At its core, it separates the concerns of **policy** definition (*what rules to enforce*) from policy **checking** (*how to validate those rules*), enabling flexible and reusable access control patterns. The framework's mission is to enable policy enforcement through three key components: **Policies** that define access rules, **Checkers** that validate evidence, and *enforcement* mechanisms that manage the validation flow. Built on values of modularity and reusability, Excubiae provides protocol developers with building blocks to create robust Attribute-Based Access Control (ABAC) systems. In fact, the name "[Excubiae](https://www.nihilscio.it/Manuali/Lingua%20latina/Verbi/Coniugazione_latino.aspx?verbo=excubia&lang=IT_#:~:text=1&text=excubia%20%3D%20sentinella...%20guardia,%2C%20excubia%20%2D%20Sostantivo%201%20decl.)" comes from the ancient Roman guards who kept watch and enforced access control - an apt metaphor for a system designed to protect smart contract access through configurable gatekeepers. 31 | 32 | # Motivation 33 | In the evolving blockchain ecosystem, protocols continuously generate new forms of **verifiable evidence** and **proofs** (either backed by cryptography or not). Current access control mechanisms in smart contracts are often rigid, tightly coupled, and lack interoperability, making them unsuitable for interconnection and communication. While these protocols excel at producing such evidence, integrating them into access control systems outside their standard ways of doing it (e.g., APIs / apps / libs / modules) remains challenging. Excubiae aims to bridge this gap by providing a universal framework for composing and enforcing access control policies upon verifiable attributes satisfaction (criterias), expanding and making interoperable forms of on-chain evidence, serving as a foundational layer for ABAC across the ecosystem. In fact, the framework serves multiple audiences: protocol developers integrating access control into their systems, as smart contract engineers implementing custom validation logic for access control on-chain. 34 | 35 | # Specification 36 | 37 | ## System Requirements 38 | The implementations MUST provide: 39 | 40 | ### Smart Contracts 41 | 42 | #### 1. Checker 43 | Checker contracts validate evidence against predefined rules ("verifiable attributes"). Base implementations MUST: 44 | 45 | - Provide a stateless validation mechanism through the `check()` method. 46 | - Support encoded evidence via `bytes` parameters. 47 | - Return `boolean` validation results. 48 | - Be reusable across multiple policies. 49 | 50 | Advanced implementations MUST: 51 | 52 | - Provide a stateless validation mechanism through the `check()` method, taking a supplementary parameter specifying the type of check among the following: 53 | - **PRE**: Initial validation before main enforcement. 54 | - **MAIN**: Core validation (as for base implementation). 55 | - **POST**: Final validation after main enforcement. 56 | 57 | #### 2. Policy 58 | Policy contracts define and enforce Checker rules on evidence provided by subjects. Base implementations MUST: 59 | 60 | - Define a clear target address representing the protected resource. 61 | - Track enforcement state for subjects. 62 | - Delegate validation to a designated Checker. 63 | - Emit events on successful enforcement. 64 | - Prevent unauthorized access through well-defined error conditions. 65 | 66 | Advanced implementations MUST: 67 | 68 | - Delegate validation to a designated Checker, taking a supplementary parameter specifying the type of check among the following: 69 | - **PRE**: Initial validation before main enforcement (can be skipped). 70 | - **MAIN**: Core validation (as for base implementation). 71 | - **POST**: Final validation after main enforcement (can be skipped). 72 | 73 | #### 3. Factory 74 | Factory contracts enable efficient deployment of Policies and Checkers. Implementations MUST: 75 | 76 | - Support the [minimal proxy pattern with immutable args](https://github.com/Vectorized/solady/blob/main/src/utils/LibClone.sol). 77 | - Ensure proper initialization of cloned contracts. 78 | - Enable customizable deployment parameters. 79 | 80 | --- 81 | ## Glossary 82 | This section defines key terms used throughout this specification to ensure clarity and consistency. 83 | 84 | - ABAC (Attribute-Based Access Control): A security model that grants access based on verifiable attributes rather than predefined roles. 85 | - RBAC (Role-Based Access Control): A security model that grants access based on predefined and assigned roles. 86 | - Attestation: A verifiable claim or credential issued by a trusted party, proving a subject meets certain conditions. 87 | - Checker: A smart contract responsible for validating evidence submitted by a subject. 88 | - Evidence: Cryptographic proof, attestation, or data submitted by a subject to prove eligibility for access. 89 | - EVM (Ethereum Virtual Machine): The runtime environment for executing smart contracts on Ethereum-compatible networks. 90 | - Nullifier: A mechanism to prevent replay attacks by ensuring each proof or credential is used only once. 91 | - Policy: A smart contract defining access control rules and delegating validation to checkers. 92 | - Proxy Pattern: A design pattern that allows upgradeability of smart contracts by separating storage from logic (e.g., EIP-2535 Diamond Standard). 93 | - Selective Disclosure: A privacy-preserving mechanism that allows users to reveal only necessary parts of their identity or credentials. 94 | - Subject: The entity (e.g., user, contract, or external account) requesting access to a protected resource. 95 | - Target: The entity, contract, or resource for which an access control policy is enforced. 96 | - Verifiable Attributes: Data points that can be independently verified, such as on-chain credentials, cryptographic signatures, or attestations. 97 | 98 | ## Preliminaries 99 | 100 | ### Access Control Mode 101 | Excubiae implements an Attribute-Based Access Control (ABAC) model where access decisions are based on attributes associated with the subject. This differs from Role-Based Access Control (RBAC) by allowing more flexible, fine-grained permissions based on arbitrary verifiable evidence rather than predefined roles. 102 | 103 | ### Evidence Structure 104 | Excubiae supports a flexible evidence format that enables diverse verification methods. Evidence refers to cryptographic proofs, attestations, or data that a subject provides to gain access to a protected resource. All evidence is encoded as `bytes` for future compatibility, composability, and protocol-agnostic validation. Evidence MAY take the following forms: 105 | 106 | #### 1. Basic Encoded Evidence 107 | Simple proofs such as token ownership or balance-based access. 108 | 109 | ```solidity 110 | abi.encodePacked(tokenAddress, tokenId) 111 | ``` 112 | Example: 113 | ```solidity 114 | bytes memory evidence = abi.encodePacked(0x1234..., 42); 115 | ``` 116 | 117 | #### 2. Hashed Commitments 118 | Evidence can be **hashed off-chain** and submitted on-chain to preserve privacy and reduce gas costs. 119 | 120 | ```solidity 121 | keccak256(abi.encodePacked(secretValue, salt)) 122 | ``` 123 | Example: 124 | ```solidity 125 | bytes32 evidenceHash = keccak256(abi.encodePacked(0xdeadbeef, 0xabc123)); 126 | ``` 127 | 128 | #### 3. Zero-Knowledge Proofs (ZKPs) 129 | For privacy-preserving authentication, Excubiae supports on-chain verifiable proofs (e.g., ZK-SNARKs), where subjects prove statements without revealing / selectively disclosing underlying information. 130 | 131 | Example: 132 | ```solidity 133 | struct ZKProof { 134 | bytes32 a; 135 | bytes32 b; 136 | bytes32 c; 137 | uint256 publicSignals; 138 | } 139 | ``` 140 | Usage: 141 | ```solidity 142 | ZKProof memory zkEvidence = ZKProof(a, b, c, publicSignals); 143 | bytes memory encodedProof = abi.encode(zkEvidence); 144 | ``` 145 | 146 | #### 4. Attestation-Based Evidence 147 | Verifiable credentials issued by a trusted third party, such as decentralized identifiers (DIDs) or attestations. 148 | 149 | Example: 150 | ```solidity 151 | struct Attestation { 152 | address issuer; 153 | address subject; 154 | bytes32 claimHash; 155 | bytes signature; 156 | } 157 | ``` 158 | Encoding: 159 | ```solidity 160 | bytes memory attestationData = abi.encode(Attestation(issuer, subject, claimHash, sig)); 161 | ``` 162 | 163 | #### 5. Merkle Proof-Based Access 164 | When a subject belongs to a Merkle tree-based access group, a Merkle proof can be submitted to verify inclusion. 165 | 166 | Example: 167 | ```solidity 168 | struct MerkleProof { 169 | bytes32[] proof; 170 | bytes32 root; 171 | bytes32 leaf; 172 | } 173 | ``` 174 | Usage: 175 | ```solidity 176 | bytes memory merkleEvidence = abi.encode(MerkleProof(proof, root, leaf)); 177 | ``` 178 | 179 | ### Standard Encoding and Decoding 180 | By maintaining a standardized encoding scheme, Excubiae ensures that Policies and Checkers can interpret diverse types of evidence without tightly coupling access logic to a specific authentication method. All evidence MUST be encoded using `abi.encode()` to ensure compatibility across different implementations. Policies and Checkers MUST decode evidence as needed: 181 | 182 | ```solidity 183 | // example. 184 | function validateEvidence(bytes calldata evidence) external { 185 | (address tokenAddress, uint256 tokenId) = abi.decode(evidence, (address, uint256)); 186 | } 187 | ``` 188 | 189 | ### Private Evidence 190 | The framework is designed to operate entirely on-chain, with all validation and enforcement occurring within the EVM environment. This ensures transparency and auditability. Privacy is tightly coupled with the evidence used: for example, a zero-knowledge proof brings privacy preserving verification for the prover (no disclosure of secrets) while passing a token identifier as evidence has no privacy at all. 191 | 192 | --- 193 | 194 | ## Framework Architecture 195 | 196 | ``` 197 | ┌─────────────────┐ ┌──────────────────┐ 198 | │ PolicyFactory │ │ CheckerFactory │ 199 | └─────────────────┘ └──────────────────┘ 200 | │ │ 201 | │ deploys | deploys 202 | | 203 | │ │ 204 | ▼ ▼ 205 | ┌──────────────┐ enforces ┌──────────────┐ 206 | │ Policy │ ───────────────> │ Checker │ 207 | └──────────────┘ └──────────────┘ 208 | │ │ 209 | │ protects │ checks 210 | │ │ 211 | ▼ ▼ 212 | ┌──────────────┐ ┌──────────────┐ 213 | │ Target │ │ Subject │ 214 | └──────────────┘ └──────────────┘ 215 | ``` 216 | 217 | ### Flow 218 | The system MUST implement the following flow when a subject attempts to access a protected target. Note that the following steps are generic and assumes that Checker and Policy clones have been successfully deployed and initialized from respective Factory contracts. 219 | 220 | 1. Subject provides evidence to a policy. 221 | 2. Policy delegates validation to its checker. 222 | 3. Checker verifies the evidence. 223 | 4. Policy enforces the checker's decision & keeps track of the subject. 224 | 225 | #### 1. Checker 226 | A Checker in Excubiae is responsible for validating access conditions. Think of it as the rulebook that defines what constitutes valid access - it receives evidence and determines whether it meets the specified criteria. This contract MUST remain deliberately stateless, focusing solely on validation logic. This design allows checkers to be shared across different policies and enables clear, auditable validation rules. The framework offers two checker variants: BaseChecker and AdvancedChecker. 227 | 228 | The Checker MUST be a clonable contract and MUST provide the following internal methods: 229 | - `_initialize()`: Method to initialize the clone. 230 | - Must be overridden by derived contracts to implement custom initialization logic. 231 | - Must Revert if the clone has already been initialized. 232 | - `_getAppendedBytes()`: Method to retrieve appended arguments from the clone. 233 | - MUST use the Minimal Proxy library utility to extract the arguments specified at deploy time. 234 | - MUST return the appended bytes extracted from the clone. 235 | 236 | The BaseChecker MUST provide a stateless validation mechanism through the `check()` method which takes: 237 | - `subject: address` - An address (EOA or contract) attempting to access a protected resource. 238 | - `evidence: bytes calldata` - Encoded data provided by a subject to prove they satisfy access criteria. 239 | 240 | The AdvancedChecker MUST provide a stateless validation mechanism through the `check()` method which takes: 241 | - `subject: address` - An address (EOA or contract) attempting to access a protected resource. 242 | - `evidence: bytes calldata` - Encoded data provided by a subject to prove they satisfy access criteria. 243 | - `checkType: Check` - The phase of validation to execute (PRE, MAIN, POST). 244 | 245 | #### 2. Policy 246 | A Policy acts as a gatekeeper, controlling access to protected resources through well-defined enforcement mechanisms. Think of it as a security checkpoint - it doesn't determine the rules itself, but it ensures they are properly enforced. 247 | 248 | The Policy MUST be a ownable, clonable contract and MUST provide the following internal methods: 249 | - `_initialize()`: Method to initialize the clone. 250 | - Must be overridden by derived contracts to implement custom initialization logic. 251 | - Must Revert if the clone has already been initialized. 252 | - Must transfer the ownership to the sender (`msg.sender`). 253 | - `_getAppendedBytes()`: Method to retrieve appended arguments from the clone. 254 | - MUST use the Minimal Proxy library utility to extract the arguments specified at deploy time. 255 | - MUST return the appended bytes extracted from the clone. 256 | - `setTarget(_target)`: Method to set the contract address to be protected by the policy. 257 | - MUST only be called once by the owner. 258 | - MUST revert when given `_target` is a zero address. 259 | - MUST revert when has been already set once. 260 | - SHOULD emit an event `TargetSet(_target)`. 261 | 262 | The BasePolicy MUST provide 263 | - an enforcement mechanism for the provided Checker contract through the `enforce()` method which takes: 264 | - `subject: address` - An address (EOA or contract) attempting to access a protected resource. 265 | - `evidence: bytes calldata` - Encoded data provided by a subject to prove they satisfy access criteria. 266 | - MUST revert when the check is unsuccessful (ie., evaluate to `false`). 267 | - SHOULD emit an event `Enforced(subject, target, evidence)`. 268 | - override the `_initialize()` method: 269 | - MUST decode the sender address specified at deploy time. 270 | - MUST decode the BaseChecker address specified at deploy time. 271 | - MUST transfer the ownership to the sender address. 272 | 273 | The AdvancedPolicy MUST provide 274 | - an enforcement mechanism for the provided Checker contract through the `enforce()` method which takes: 275 | - `subject: address` - An address (EOA or contract) attempting to access a protected resource. 276 | - `evidence: bytes calldata` - Encoded data provided by a subject to prove they satisfy access criteria. 277 | - `checkType: Check` - The phase of validation to execute (PRE, MAIN, POST). 278 | - SHOULD skip PRE check based on `skipPre` boolean flag configuration specified at deploy time. 279 | - SHOULD skip POST check based on `skipPost` boolean flag configuration specified at deploy time. 280 | - MUST revert when the check is unsuccessful (ie., evaluate to `false`). 281 | - SHOULD emit an event `Enforced(subject, target, evidence, checkType)`. 282 | - override the `_initialize()` method: 283 | - MUST decode the sender address specified at deploy time. 284 | - MUST decode the AdvancedChecker address specified at deploy time. 285 | - MUST decode the skipPre boolean specified at deploy time. 286 | - MUST decode the skipPost boolean specified at deploy time. 287 | - MUST transfer the ownership to the sender address. 288 | 289 | #### 3. Factory 290 | Factory contracts enable efficient deployment of Policies and Checkers. Each Factory contract MUST implement: 291 | - `IMPLEMENTATION: address` - A public state variable containing the address of the implementation contract used for cloning. This address is immutable and defines the logic contract for all clones deployed by the factory. 292 | - `_deploy(bytes memory data): address clone` - A method to deploy a new clone contract. 293 | - MUST be a minimal proxy contract with appended initialization data using the reference proxy library (e.g., [Solady's LibClone](https://github.com/Vectorized/solady/blob/main/src/utils/LibClone.sol)) 294 | - MUST emit a `CloneDeployed(address)` event upon successful deployment. 295 | 296 | ## Security Considerations 297 | Excubiae is a framework for defining and enforcing access control policies, but its security ultimately depends on the correctness and robustness of the implemented policies and checkers. The framework itself does not guarantee security—it provides a modular structure to enforce rules as defined by the developer. Implementers MUST carefully design their policies and checkers to avoid security risks such as replay attacks, insufficient validation, or incorrect assumptions about external contracts. Secure access control is achieved by selecting strong cryptographic verification methods, minimizing trust assumptions, and thoroughly testing validation mechanisms. The following is the complete list of things that what MUST be addressed: 298 | 299 | ### Prevention of Double-Enforcement Attacks 300 | Double-enforcement attacks occur when a subject attempts to leverage the same validation evidence multiple times or across different contexts. To prevent these attacks: 301 | - Track unique identifiers for evidence or validation attempts 302 | - SHOULD implement mappings that mark evidence as "spent" after first use 303 | - SHOULD consider mechanisms for cross-policy "nullifier" sharing when appropriate 304 | - CAN implement time-based expiration for enforcement status when appropriate 305 | - CAN provide mechanisms for authorized revocation of enforcement 306 | - For advanced use cases, you can implement more complex mappings and / or commitment schemes that prevent evidence reuse. 307 | - MUST track separate state for each validation phase. 308 | 309 | ### Secure Proxy Initialization 310 | The minimal proxy pattern with immutable args introduces specific security considerations: 311 | - Ensure initialization is performed in the same transaction as deployment 312 | - MUST implement secure ownership transfer during initialization 313 | - Prevent front-running attacks during deployment 314 | - Verify that critical parameters cannot be modified after initialization 315 | - Restrict deployment capabilities to authorized addresses 316 | 317 | ### Clear Separation Between Validation and State Management 318 | Maintaining separation of concerns is critical for security: 319 | - Ensure Checkers remain purely stateless for validation 320 | - Avoid side effects or state changes in Checker contracts 321 | - Implement view functions for all validation logic 322 | - Restrict all state changes to Policy contracts 323 | - Clearly document state transitions and invariants 324 | - Implement checks-effects-interactions pattern in Policy operations 325 | 326 | ### Additional Considerations 327 | Implementations SHOULD also consider: 328 | 329 | 1. **Gas Optimization** 330 | Efficient gas usage is a key consideration when implementing policies and checkers. Implementers SHOULD: 331 | - Balance security with gas efficiency by minimizing redundant on-chain computations. 332 | - Analyze gas costs for various validation scenarios to ensure feasibility for real-world use cases. 333 | - Document gas expectations for implementers to provide clarity on cost implications. 334 | 335 | 2. **Upgradeability Patterns** 336 | Policies MAY be upgradeable by utilizing proxy patterns (e.g., [EIP-2535 Diamond Standard](https://eips.ethereum.org/EIPS/eip-2535)) or by allowing governance mechanisms to deploy updated versions. 337 | - If implementing upgradeability, document security implications to avoid unforeseen risks. 338 | - Consider using transparent proxy patterns where applicable, ensuring compatibility with governance structures. 339 | - Implement secure upgrade mechanisms with appropriate time delays to allow for security reviews before changes take effect. 340 | 341 | 3. **Composability Risks** 342 | Since Excubiae is designed to integrate with multiple protocols, developers SHOULD carefully consider: 343 | - Potential for unexpected interactions with other protocols that may lead to security vulnerabilities. 344 | - Assumptions about external contracts, ensuring predictable behavior when integrating with third-party protocols. 345 | - Implementing fail-safe mechanisms for integration failures, such as timeouts or fallback execution paths. 346 | 347 | --- 348 | 349 | # Implementation Notes 350 | Excubiae is structured as a [TypeScript/Solidity monorepo](https://github.com/privacy-scaling-explorations/excubiae) using [Yarn](https://yarnpkg.com/getting-started) as its package manager. The project is organized into distinct packages and applications: 351 | 352 | ``` 353 | excubiae/ 354 | ├── packages/ 355 | │ ├── contracts/ # Framework implementation 356 | ``` 357 | 358 | The contracts package uniquely combines [Hardhat](https://hardhat.org/) and [Foundry](https://book.getfoundry.sh/) in a way that they can [coexist together](https://hardhat.org/hardhat-runner/docs/advanced/hardhat-and-foundry), offering developers flexibility in their testing approach. This dual-environment setup enables both JavaScript/TypeScript and Solidity-native testing patterns while maintaining complete coverage. 359 | 360 | The framework's core implementation resides in `packages/contracts`, structured into distinct layers: 361 | - Core contracts implementing base and advanced validation patterns, minimal proxy pattern with immutable args using [Solady's LibCLone](https://github.com/Vectorized/solady/blob/main/src/utils/LibClone.sol). 362 | - Interface definitions ensuring consistent implementation. 363 | - Test suites demonstrating usage & integration (voting use case for base and advanced scenarios). 364 | - Semaphore extensions which enforces a proof of membership for a Semaphore group with resistance to frontrunning attack vectors. 365 | 366 | ## Guidelines 367 | The following guidelines MUST be seen as a reference implementation example / guidelines and are based on the [reference implementation codebase](https://github.com/privacy-scaling-explorations/excubiae) 368 | 369 | ### Writing a Clonable Checker / Policy 370 | 371 | When implementing a policy, the first step is defining the criteria for passing validation. These criteria must be verifiable on-chain—such as token ownership, balance thresholds, or protocol-specific credentials. 372 | 373 | For example, in a voting system where voters must own a specific NFT to participate, the validation logic resides in a **Checker** contract, while a **Policy** enforces the validation result. 374 | 375 | A checker encapsulates validation logic. The [BaseERC721Checker](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721Checker.sol) is a clonable contract that verifies NFT ownership. To implement a clonable checker: 376 | - Override `_initialize()`, which is executed only once at deployment time to store immutable arguments in the contract state. 377 | - Implement `_check()`, defining the validation logic. 378 | 379 | Once the checker is in place, a **Policy** references it to enforce validation. The [BaseERC721Policy](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721Policy.sol) demonstrates how to: 380 | - Extend a base policy contract. 381 | - Provide a unique trait identifier. 382 | 383 | ```solidity 384 | abstract contract Clone is IClone { 385 | bool public initialized; 386 | 387 | function initialize() external { 388 | _initialize(); 389 | } 390 | 391 | function getAppendedBytes() external returns (bytes memory appendedBytes) { 392 | return _getAppendedBytes(); 393 | } 394 | 395 | function _initialize() internal virtual { 396 | if (initialized) revert AlreadyInitialized(); 397 | initialized = true; 398 | } 399 | 400 | function _getAppendedBytes() internal virtual returns (bytes memory appendedBytes) { 401 | return LibClone.argsOnClone(address(this)); 402 | } 403 | } 404 | 405 | abstract contract Policy is Clone, IPolicy, Ownable(msg.sender) { 406 | address public target; 407 | 408 | modifier onlyTarget() { 409 | if (msg.sender != target) revert TargetOnly(); 410 | _; 411 | } 412 | 413 | function _initialize() internal virtual override { 414 | super._initialize(); 415 | 416 | _transferOwnership(msg.sender); 417 | } 418 | 419 | function setTarget(address _target) external virtual onlyOwner { 420 | if (_target == address(0)) revert ZeroAddress(); 421 | if (target != address(0)) revert TargetAlreadySet(); 422 | 423 | target = _target; 424 | emit TargetSet(_target); 425 | } 426 | } 427 | 428 | abstract contract BaseChecker is Clone, IBaseChecker { 429 | function check(address subject, bytes calldata evidence) external view override returns (bool checked) { 430 | return _check(subject, evidence); 431 | } 432 | 433 | function _check(address subject, bytes calldata evidence) internal view virtual returns (bool checked) {} 434 | } 435 | 436 | abstract contract BasePolicy is Policy, IBasePolicy { 437 | BaseChecker public BASE_CHECKER; 438 | 439 | function _initialize() internal virtual override { 440 | super._initialize(); 441 | 442 | bytes memory data = _getAppendedBytes(); 443 | (address sender, address baseCheckerAddr) = abi.decode(data, (address, address)); 444 | 445 | _transferOwnership(sender); 446 | 447 | BASE_CHECKER = BaseChecker(baseCheckerAddr); 448 | } 449 | 450 | function enforce(address subject, bytes calldata evidence) external override onlyTarget { 451 | _enforce(subject, evidence); 452 | } 453 | 454 | function _enforce(address subject, bytes calldata evidence) internal virtual { 455 | if (!BASE_CHECKER.check(subject, evidence)) revert UnsuccessfulCheck(); 456 | 457 | emit Enforced(subject, target, evidence); 458 | } 459 | } 460 | 461 | contract BaseERC721Checker is BaseChecker { 462 | IERC721 public nft; 463 | 464 | function _initialize() internal override { 465 | super._initialize(); 466 | 467 | bytes memory data = _getAppendedBytes(); 468 | 469 | address nftAddress = abi.decode(data, (address)); 470 | 471 | nft = IERC721(nftAddress); 472 | } 473 | 474 | function _check(address subject, bytes calldata evidence) internal view override returns (bool) { 475 | super._check(subject, evidence); 476 | 477 | uint256 tokenId = abi.decode(evidence, (uint256)); 478 | 479 | return nft.ownerOf(tokenId) == subject; 480 | } 481 | } 482 | 483 | contract BaseERC721Policy is BasePolicy { 484 | function trait() external pure returns (string memory) { 485 | return "BaseERC721"; 486 | } 487 | } 488 | ``` 489 | 490 | To deploy clones dynamically, each Checker and Policy implementation requires a corresponding **Factory** contract. Examples include [BaseERC721CheckerFactory](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721CheckerFactory.sol) and [BaseERC721PolicyFactory](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721PolicyFactory.sol). 491 | 492 | Each factory must: 493 | 1. Specify the implementation contract in the constructor and pass a new instance to the `Factory()` constructor. 494 | 2. Implement a `deploy()` method that: 495 | - Encodes initialization parameters (**immutable args**). 496 | - Calls `_deploy(data)`, deploying a clone. 497 | - Initializes the clone via its `initialize()` method. 498 | 499 | ```solidity 500 | abstract contract Factory is IFactory { 501 | address public immutable IMPLEMENTATION; 502 | 503 | constructor(address _implementation) { 504 | IMPLEMENTATION = _implementation; 505 | } 506 | 507 | function _deploy(bytes memory data) internal returns (address clone) { 508 | clone = LibClone.clone(IMPLEMENTATION, data); 509 | 510 | emit CloneDeployed(clone); 511 | } 512 | } 513 | 514 | contract BaseERC721CheckerFactory is Factory { 515 | constructor() Factory(address(new BaseERC721Checker())) {} 516 | 517 | function deploy(address _nftAddress) public { 518 | bytes memory data = abi.encode(_nftAddress); 519 | 520 | address clone = super._deploy(data); 521 | 522 | BaseERC721Checker(clone).initialize(); 523 | } 524 | } 525 | 526 | contract BaseERC721PolicyFactory is Factory { 527 | constructor() Factory(address(new BaseERC721Policy())) {} 528 | 529 | function deploy(address _checkerAddr) public { 530 | bytes memory data = abi.encode(msg.sender, _checkerAddr); 531 | 532 | address clone = super._deploy(data); 533 | 534 | BaseERC721Policy(clone).initialize(); 535 | } 536 | } 537 | ``` 538 | 539 | This approach enables efficient deployments and customization at deploy time. For example, different `_nftAddress` values can be set per clone, allowing multiple NFT collections to use the same validation logic while remaining independent. 540 | 541 | ### Integrating a Policy 542 | The [BaseVoting](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseVoting.sol) contract demonstrates a complete implementation of policy integration. It shows how to: 543 | - Initialize the policy 544 | - Enforce checks before actions 545 | - Track validation state 546 | 547 | ```solidity 548 | contract BaseVoting { 549 | event Registered(address voter); 550 | event Voted(address voter, uint8 option); 551 | 552 | error NotRegistered(); 553 | error AlreadyVoted(); 554 | error InvalidOption(); 555 | 556 | BaseERC721Policy public immutable POLICY; 557 | mapping(address => bool) public registered; 558 | mapping(address => bool) public hasVoted; 559 | 560 | constructor(BaseERC721Policy _policy) { 561 | POLICY = _policy; 562 | } 563 | 564 | function register(uint256 tokenId) external { 565 | POLICY.enforce(msg.sender, abi.encode(tokenId)); 566 | 567 | registered[msg.sender] = true; 568 | 569 | emit Registered(msg.sender); 570 | } 571 | 572 | function vote(uint8 option) external { 573 | // Check registration and voting status. 574 | if (!registered[msg.sender]) revert NotRegistered(); 575 | if (hasVoted[msg.sender]) revert AlreadyVoted(); 576 | if (option >= 2) revert InvalidOption(); 577 | 578 | // Record the vote. 579 | hasVoted[msg.sender] = true; 580 | 581 | emit Voted(msg.sender, option); 582 | } 583 | } 584 | ``` 585 | 586 | #### Tracking Mechanisms to Prevent Double Enforcement 587 | Each Policy in Excubiae must implement its own tracking mechanism to prevent double enforcement. This ensures that the same proof or validation cannot be reused maliciously. The design of the tracking system may vary depending on the specific requirements of the policy. 588 | 589 | Example from [SemaphorePolicy](https://github.com/privacy-scaling-explorations/excubiae/blob/70967948b4025c3f7bbbf833c06cf5944187837d/packages/contracts/contracts/extensions/SemaphorePolicy.sol#L34): 590 | 591 | ```solidity 592 | contract SemaphorePolicy is BasePolicy { 593 | mapping(uint256 => bool) public spentNullifiers; 594 | 595 | error AlreadySpentNullifier(); 596 | 597 | function trait() external pure returns (string memory) { 598 | return "Semaphore"; 599 | } 600 | 601 | function _enforce(address subject, bytes calldata evidence) internal override { 602 | ISemaphore.SemaphoreProof memory proof = abi.decode(evidence, (ISemaphore.SemaphoreProof)); 603 | uint256 _nullifier = proof.nullifier; 604 | 605 | if (spentNullifiers[_nullifier]) revert AlreadySpentNullifier(); 606 | 607 | spentNullifiers[_nullifier] = true; 608 | 609 | super._enforce(subject, evidence); 610 | } 611 | } 612 | ``` 613 | 614 | This pattern ensures that each proof is only used once, maintaining the integrity of the access control system. 615 | 616 | --- 617 | 618 | # Copyright 619 | 620 | Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). -------------------------------------------------------------------------------- /specs/3/images/semaphore-v4-identity.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------