ciphera 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ciphera-0.1.0/.gitignore +170 -0
- ciphera-0.1.0/LICENSE +21 -0
- ciphera-0.1.0/PKG-INFO +217 -0
- ciphera-0.1.0/README.md +164 -0
- ciphera-0.1.0/ciphera/__init__.py +34 -0
- ciphera-0.1.0/ciphera/client.py +182 -0
- ciphera-0.1.0/ciphera/exceptions.py +23 -0
- ciphera-0.1.0/ciphera/models.py +56 -0
- ciphera-0.1.0/ciphera/verifier.py +128 -0
- ciphera-0.1.0/pyproject.toml +59 -0
ciphera-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
110
|
+
.pdm.toml
|
|
111
|
+
|
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
113
|
+
__pypackages__/
|
|
114
|
+
|
|
115
|
+
# Celery stuff
|
|
116
|
+
celerybeat-schedule
|
|
117
|
+
celerybeat.pid
|
|
118
|
+
|
|
119
|
+
# SageMath parsed files
|
|
120
|
+
*.sage.py
|
|
121
|
+
|
|
122
|
+
# Environments
|
|
123
|
+
.env
|
|
124
|
+
.venv
|
|
125
|
+
env/
|
|
126
|
+
venv/
|
|
127
|
+
ENV/
|
|
128
|
+
env.bak/
|
|
129
|
+
venv.bak/
|
|
130
|
+
|
|
131
|
+
# Spyder project settings
|
|
132
|
+
.spyderproject
|
|
133
|
+
.spyproject
|
|
134
|
+
|
|
135
|
+
# Rope project settings
|
|
136
|
+
.ropeproject
|
|
137
|
+
|
|
138
|
+
# mkdocs documentation
|
|
139
|
+
/site
|
|
140
|
+
|
|
141
|
+
# mypy
|
|
142
|
+
.mypy_cache/
|
|
143
|
+
.dmypy.json
|
|
144
|
+
dmypy.json
|
|
145
|
+
|
|
146
|
+
# Pyre type checker
|
|
147
|
+
.pyre/
|
|
148
|
+
|
|
149
|
+
# pytype static type analyzer
|
|
150
|
+
.pytype/
|
|
151
|
+
|
|
152
|
+
# Ruff (linter)
|
|
153
|
+
.ruff_cache/
|
|
154
|
+
|
|
155
|
+
# Cython debug symbols
|
|
156
|
+
cython_debug/
|
|
157
|
+
|
|
158
|
+
# PyCharm
|
|
159
|
+
.idea/
|
|
160
|
+
!.idea/runConfigurations
|
|
161
|
+
|
|
162
|
+
# macOS
|
|
163
|
+
.DS_Store
|
|
164
|
+
|
|
165
|
+
# Received approval test files
|
|
166
|
+
*.received.*
|
|
167
|
+
|
|
168
|
+
# NPM
|
|
169
|
+
node_modules
|
|
170
|
+
|
ciphera-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aditya Pandey
|
|
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.
|
ciphera-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ciphera
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-Knowledge KYC for Algorand — verify KYC status on-chain without exposing personal data
|
|
5
|
+
Project-URL: Homepage, https://github.com/Aditya060806/Ciphera
|
|
6
|
+
Project-URL: Repository, https://github.com/Aditya060806/Ciphera
|
|
7
|
+
Project-URL: Issues, https://github.com/Aditya060806/Ciphera/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/Aditya060806/Ciphera#readme
|
|
9
|
+
Author-email: Aditya Pandey <adityapandey060806@gmail.com>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 Aditya Pandey
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: aadhaar,algorand,blockchain,groth16,kyc,privacy,snarkjs,zero-knowledge,zk-proof
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Topic :: Security :: Cryptography
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
|
+
Requires-Python: >=3.9
|
|
44
|
+
Requires-Dist: py-algorand-sdk>=2.6.0
|
|
45
|
+
Provides-Extra: algorand
|
|
46
|
+
Requires-Dist: py-algorand-sdk>=2.6.0; extra == 'algorand'
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# ciphera
|
|
55
|
+
|
|
56
|
+
**Zero-Knowledge KYC for Algorand** — verify KYC status on-chain without exposing personal data.
|
|
57
|
+
|
|
58
|
+
[](https://pypi.org/project/ciphera)
|
|
59
|
+
[](https://pypi.org/project/ciphera)
|
|
60
|
+
[](https://opensource.org/licenses/MIT)
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install ciphera
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## What is Ciphera?
|
|
69
|
+
|
|
70
|
+
Ciphera is a privacy-preserving zero-knowledge KYC system on Algorand. Users prove they are KYC-verified Indian adults **without revealing any personal data**. ZK proofs are generated in the browser — private inputs never leave the device.
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
### Check KYC Status (any dApp)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from ciphera import CipheraClient
|
|
78
|
+
|
|
79
|
+
client = CipheraClient() # connects to Algorand Testnet by default
|
|
80
|
+
|
|
81
|
+
# Check total registered credentials on-chain
|
|
82
|
+
status = client.verify_kyc()
|
|
83
|
+
print(status.is_verified) # True if any credentials registered
|
|
84
|
+
print(status.total_registered) # e.g. 42
|
|
85
|
+
print(status.app_id) # 756272073
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Check a Specific Nullifier
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
client = CipheraClient()
|
|
92
|
+
|
|
93
|
+
# Check if a specific nullifier is registered
|
|
94
|
+
is_verified = client.is_nullifier_registered("3b1f8a2c" + "0" * 56)
|
|
95
|
+
print(is_verified) # True / False
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Verify a ZK Proof (Issuer Backend)
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import json
|
|
102
|
+
from ciphera import verify_proof
|
|
103
|
+
|
|
104
|
+
vk = json.load(open("verification_key.json"))
|
|
105
|
+
|
|
106
|
+
result = verify_proof(
|
|
107
|
+
verification_key=vk,
|
|
108
|
+
proof=proof_dict, # from browser SDK
|
|
109
|
+
public_signals=signals_list,
|
|
110
|
+
expected_app_id=756272073 # prevents cross-app attacks
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if result.valid:
|
|
114
|
+
print(result.nullifier_hex) # 32-byte hex to register on-chain
|
|
115
|
+
print(result.public_signals.is_adult) # "1"
|
|
116
|
+
else:
|
|
117
|
+
print(result.error)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Connect to Different Networks
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from ciphera import CipheraClient
|
|
124
|
+
|
|
125
|
+
# Testnet (default)
|
|
126
|
+
client = CipheraClient()
|
|
127
|
+
|
|
128
|
+
# Mainnet
|
|
129
|
+
client = CipheraClient(network="mainnet")
|
|
130
|
+
|
|
131
|
+
# LocalNet (development)
|
|
132
|
+
client = CipheraClient(network="localnet")
|
|
133
|
+
|
|
134
|
+
# Custom endpoint
|
|
135
|
+
client = CipheraClient(
|
|
136
|
+
algod_url="https://my-algod.example.com",
|
|
137
|
+
algod_token="my-token"
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## API Reference
|
|
144
|
+
|
|
145
|
+
### `CipheraClient`
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
CipheraClient(
|
|
149
|
+
network="testnet", # "testnet" | "mainnet" | "localnet"
|
|
150
|
+
algod_url=None, # Override endpoint
|
|
151
|
+
algod_token="", # Override token
|
|
152
|
+
contract_ids=None, # Override deployed contract IDs (dict)
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Method | Returns | Description |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `verify_kyc(wallet_address=None)` | `KYCStatus` | Check on-chain KYC registry |
|
|
159
|
+
| `is_nullifier_registered(hex)` | `bool` | Direct nullifier box check |
|
|
160
|
+
| `get_credential_asa_id()` | `int` | KYCRED ASA ID |
|
|
161
|
+
| `CipheraClient.explorer_tx_url(txid)` | `str` | Allo.info TX URL |
|
|
162
|
+
| `CipheraClient.explorer_app_url(app_id)` | `str` | Allo.info app URL |
|
|
163
|
+
|
|
164
|
+
### `verify_proof(vk, proof, public_signals, expected_app_id=None)`
|
|
165
|
+
|
|
166
|
+
Verify a Groth16 ZK proof off-chain.
|
|
167
|
+
|
|
168
|
+
> **Requires Node.js** (`node` must be in PATH) as it delegates to snarkjs.
|
|
169
|
+
|
|
170
|
+
Returns `ProofResult` with `.valid`, `.nullifier_hex`, `.public_signals`, `.error`.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Models
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
@dataclass
|
|
178
|
+
class KYCStatus:
|
|
179
|
+
is_verified: bool
|
|
180
|
+
app_id: int
|
|
181
|
+
total_registered: Optional[int]
|
|
182
|
+
nullifier: Optional[str]
|
|
183
|
+
error: Optional[str]
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class ProofResult:
|
|
187
|
+
valid: bool
|
|
188
|
+
nullifier_hex: Optional[str]
|
|
189
|
+
public_signals: Optional[PublicSignals]
|
|
190
|
+
error: Optional[str]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Deployed Contracts (Algorand Testnet)
|
|
196
|
+
|
|
197
|
+
| Contract | App ID |
|
|
198
|
+
|---|---|
|
|
199
|
+
| NullifierRegistry | 756272073 |
|
|
200
|
+
| SMTRegistry | 756272075 |
|
|
201
|
+
| KYCBoxStorage | 756272299 |
|
|
202
|
+
| CredentialManager | 756281076 |
|
|
203
|
+
| KYCRED ASA | 756281102 |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Requirements
|
|
208
|
+
|
|
209
|
+
- Python 3.9+
|
|
210
|
+
- `py-algorand-sdk` (installed automatically)
|
|
211
|
+
- Node.js (only required for `verify_proof()`)
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT © [Aditya Pandey](https://github.com/Aditya060806)
|
ciphera-0.1.0/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# ciphera
|
|
2
|
+
|
|
3
|
+
**Zero-Knowledge KYC for Algorand** — verify KYC status on-chain without exposing personal data.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/ciphera)
|
|
6
|
+
[](https://pypi.org/project/ciphera)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install ciphera
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What is Ciphera?
|
|
16
|
+
|
|
17
|
+
Ciphera is a privacy-preserving zero-knowledge KYC system on Algorand. Users prove they are KYC-verified Indian adults **without revealing any personal data**. ZK proofs are generated in the browser — private inputs never leave the device.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Check KYC Status (any dApp)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from ciphera import CipheraClient
|
|
25
|
+
|
|
26
|
+
client = CipheraClient() # connects to Algorand Testnet by default
|
|
27
|
+
|
|
28
|
+
# Check total registered credentials on-chain
|
|
29
|
+
status = client.verify_kyc()
|
|
30
|
+
print(status.is_verified) # True if any credentials registered
|
|
31
|
+
print(status.total_registered) # e.g. 42
|
|
32
|
+
print(status.app_id) # 756272073
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Check a Specific Nullifier
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
client = CipheraClient()
|
|
39
|
+
|
|
40
|
+
# Check if a specific nullifier is registered
|
|
41
|
+
is_verified = client.is_nullifier_registered("3b1f8a2c" + "0" * 56)
|
|
42
|
+
print(is_verified) # True / False
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Verify a ZK Proof (Issuer Backend)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import json
|
|
49
|
+
from ciphera import verify_proof
|
|
50
|
+
|
|
51
|
+
vk = json.load(open("verification_key.json"))
|
|
52
|
+
|
|
53
|
+
result = verify_proof(
|
|
54
|
+
verification_key=vk,
|
|
55
|
+
proof=proof_dict, # from browser SDK
|
|
56
|
+
public_signals=signals_list,
|
|
57
|
+
expected_app_id=756272073 # prevents cross-app attacks
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if result.valid:
|
|
61
|
+
print(result.nullifier_hex) # 32-byte hex to register on-chain
|
|
62
|
+
print(result.public_signals.is_adult) # "1"
|
|
63
|
+
else:
|
|
64
|
+
print(result.error)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Connect to Different Networks
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from ciphera import CipheraClient
|
|
71
|
+
|
|
72
|
+
# Testnet (default)
|
|
73
|
+
client = CipheraClient()
|
|
74
|
+
|
|
75
|
+
# Mainnet
|
|
76
|
+
client = CipheraClient(network="mainnet")
|
|
77
|
+
|
|
78
|
+
# LocalNet (development)
|
|
79
|
+
client = CipheraClient(network="localnet")
|
|
80
|
+
|
|
81
|
+
# Custom endpoint
|
|
82
|
+
client = CipheraClient(
|
|
83
|
+
algod_url="https://my-algod.example.com",
|
|
84
|
+
algod_token="my-token"
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## API Reference
|
|
91
|
+
|
|
92
|
+
### `CipheraClient`
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
CipheraClient(
|
|
96
|
+
network="testnet", # "testnet" | "mainnet" | "localnet"
|
|
97
|
+
algod_url=None, # Override endpoint
|
|
98
|
+
algod_token="", # Override token
|
|
99
|
+
contract_ids=None, # Override deployed contract IDs (dict)
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| Method | Returns | Description |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `verify_kyc(wallet_address=None)` | `KYCStatus` | Check on-chain KYC registry |
|
|
106
|
+
| `is_nullifier_registered(hex)` | `bool` | Direct nullifier box check |
|
|
107
|
+
| `get_credential_asa_id()` | `int` | KYCRED ASA ID |
|
|
108
|
+
| `CipheraClient.explorer_tx_url(txid)` | `str` | Allo.info TX URL |
|
|
109
|
+
| `CipheraClient.explorer_app_url(app_id)` | `str` | Allo.info app URL |
|
|
110
|
+
|
|
111
|
+
### `verify_proof(vk, proof, public_signals, expected_app_id=None)`
|
|
112
|
+
|
|
113
|
+
Verify a Groth16 ZK proof off-chain.
|
|
114
|
+
|
|
115
|
+
> **Requires Node.js** (`node` must be in PATH) as it delegates to snarkjs.
|
|
116
|
+
|
|
117
|
+
Returns `ProofResult` with `.valid`, `.nullifier_hex`, `.public_signals`, `.error`.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Models
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
@dataclass
|
|
125
|
+
class KYCStatus:
|
|
126
|
+
is_verified: bool
|
|
127
|
+
app_id: int
|
|
128
|
+
total_registered: Optional[int]
|
|
129
|
+
nullifier: Optional[str]
|
|
130
|
+
error: Optional[str]
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class ProofResult:
|
|
134
|
+
valid: bool
|
|
135
|
+
nullifier_hex: Optional[str]
|
|
136
|
+
public_signals: Optional[PublicSignals]
|
|
137
|
+
error: Optional[str]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Deployed Contracts (Algorand Testnet)
|
|
143
|
+
|
|
144
|
+
| Contract | App ID |
|
|
145
|
+
|---|---|
|
|
146
|
+
| NullifierRegistry | 756272073 |
|
|
147
|
+
| SMTRegistry | 756272075 |
|
|
148
|
+
| KYCBoxStorage | 756272299 |
|
|
149
|
+
| CredentialManager | 756281076 |
|
|
150
|
+
| KYCRED ASA | 756281102 |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Requirements
|
|
155
|
+
|
|
156
|
+
- Python 3.9+
|
|
157
|
+
- `py-algorand-sdk` (installed automatically)
|
|
158
|
+
- Node.js (only required for `verify_proof()`)
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT © [Aditya Pandey](https://github.com/Aditya060806)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ciphera — Zero-Knowledge KYC SDK for Algorand (Python)
|
|
3
|
+
|
|
4
|
+
Install:
|
|
5
|
+
pip install ciphera
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from ciphera import CipheraClient
|
|
9
|
+
|
|
10
|
+
client = CipheraClient()
|
|
11
|
+
status = client.verify_kyc("WALLET_ADDRESS")
|
|
12
|
+
print(status.is_verified)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .client import CipheraClient
|
|
16
|
+
from .models import KYCStatus, ProofResult, PublicSignals
|
|
17
|
+
from .verifier import verify_proof
|
|
18
|
+
from .exceptions import CipheraError, ProofVerificationError, NetworkError
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
__author__ = "Aditya Pandey"
|
|
22
|
+
__license__ = "MIT"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"CipheraClient",
|
|
26
|
+
"KYCStatus",
|
|
27
|
+
"ProofResult",
|
|
28
|
+
"PublicSignals",
|
|
29
|
+
"verify_proof",
|
|
30
|
+
"CipheraError",
|
|
31
|
+
"ProofVerificationError",
|
|
32
|
+
"NetworkError",
|
|
33
|
+
"__version__",
|
|
34
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
client.py — CipheraClient for Python
|
|
3
|
+
|
|
4
|
+
Connect to Algorand and query the Ciphera KYC contracts.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from ciphera import CipheraClient
|
|
8
|
+
|
|
9
|
+
client = CipheraClient() # Testnet (default)
|
|
10
|
+
client = CipheraClient(network="mainnet") # Mainnet
|
|
11
|
+
client = CipheraClient(algod_url="http://localhost:4001", algod_token="aaaa...") # LocalNet
|
|
12
|
+
|
|
13
|
+
# Check KYC status
|
|
14
|
+
status = client.verify_kyc("WALLET_ADDRESS")
|
|
15
|
+
print(status.is_verified)
|
|
16
|
+
|
|
17
|
+
# Check a specific nullifier
|
|
18
|
+
registered = client.is_nullifier_registered("deadbeef...")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import base64
|
|
24
|
+
from typing import Optional, Dict, Any
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import algosdk
|
|
28
|
+
from algosdk.v2client import algod as algod_client
|
|
29
|
+
_ALGOSDK_AVAILABLE = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
_ALGOSDK_AVAILABLE = False
|
|
32
|
+
|
|
33
|
+
from .models import KYCStatus
|
|
34
|
+
from .exceptions import NetworkError, ContractError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Default Algorand Testnet endpoints ───────────────────────────────────────
|
|
38
|
+
_TESTNET_ALGOD = "https://testnet-api.algonode.cloud"
|
|
39
|
+
_MAINNET_ALGOD = "https://mainnet-api.algonode.cloud"
|
|
40
|
+
_LOCALNET_ALGOD = "http://localhost:4001"
|
|
41
|
+
_LOCALNET_TOKEN = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ── Deployed contract IDs on Algorand Testnet ─────────────────────────────────
|
|
45
|
+
DEFAULT_CONTRACT_IDS = {
|
|
46
|
+
"nullifier_registry": 756272073,
|
|
47
|
+
"smt_registry": 756272075,
|
|
48
|
+
"kyc_box_storage": 756272299,
|
|
49
|
+
"credential_manager": 756281076,
|
|
50
|
+
"credential_asa_id": 756281102,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CipheraClient:
|
|
55
|
+
"""
|
|
56
|
+
Python client for the Ciphera ZK-KYC system on Algorand.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
network: "testnet" (default) | "mainnet" | "localnet"
|
|
60
|
+
algod_url: Custom Algod endpoint URL (overrides network preset)
|
|
61
|
+
algod_token: Custom Algod API token (default: empty for AlgoNode)
|
|
62
|
+
contract_ids: Override deployed contract IDs (dict)
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
client = CipheraClient()
|
|
66
|
+
status = client.verify_kyc("AAAA...")
|
|
67
|
+
print(status.is_verified)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
network: str = "testnet",
|
|
73
|
+
algod_url: Optional[str] = None,
|
|
74
|
+
algod_token: str = "",
|
|
75
|
+
contract_ids: Optional[Dict[str, int]] = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
if not _ALGOSDK_AVAILABLE:
|
|
78
|
+
raise ImportError(
|
|
79
|
+
"py-algorand-sdk is required: pip install ciphera[algorand]"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Resolve endpoint
|
|
83
|
+
if algod_url:
|
|
84
|
+
url, token = algod_url, algod_token
|
|
85
|
+
elif network == "mainnet":
|
|
86
|
+
url, token = _MAINNET_ALGOD, ""
|
|
87
|
+
elif network == "localnet":
|
|
88
|
+
url, token = _LOCALNET_ALGOD, _LOCALNET_TOKEN
|
|
89
|
+
else: # testnet (default)
|
|
90
|
+
url, token = _TESTNET_ALGOD, ""
|
|
91
|
+
|
|
92
|
+
self._algod = algod_client.AlgodClient(token, url)
|
|
93
|
+
|
|
94
|
+
# Merge contract IDs
|
|
95
|
+
self._contracts = {**DEFAULT_CONTRACT_IDS, **(contract_ids or {})}
|
|
96
|
+
|
|
97
|
+
# ── Public Methods ─────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
def verify_kyc(self, wallet_address: Optional[str] = None) -> KYCStatus:
|
|
100
|
+
"""
|
|
101
|
+
Check KYC status via the on-chain NullifierRegistry.
|
|
102
|
+
|
|
103
|
+
Reads the `total_registered` global state variable.
|
|
104
|
+
A non-zero value means at least one credential has been registered.
|
|
105
|
+
|
|
106
|
+
For wallet-specific KYC, use is_nullifier_registered() with
|
|
107
|
+
the wallet's nullifier hex.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
wallet_address: Not currently used (reserved for future wallet-level checks).
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
KYCStatus with is_verified, total_registered, app_id
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
status = client.verify_kyc()
|
|
117
|
+
print(f"Registry has {status.total_registered} verified users")
|
|
118
|
+
"""
|
|
119
|
+
app_id = self._contracts["nullifier_registry"]
|
|
120
|
+
try:
|
|
121
|
+
app_info = self._algod.application_info(app_id)
|
|
122
|
+
global_state = app_info.get("params", {}).get("global-state", [])
|
|
123
|
+
|
|
124
|
+
total_registered = 0
|
|
125
|
+
for kv in global_state:
|
|
126
|
+
key = base64.b64decode(kv["key"]).decode("utf-8", errors="ignore")
|
|
127
|
+
if key == "total_registered":
|
|
128
|
+
total_registered = kv["value"].get("uint", 0)
|
|
129
|
+
|
|
130
|
+
return KYCStatus(
|
|
131
|
+
is_verified=total_registered > 0,
|
|
132
|
+
app_id=app_id,
|
|
133
|
+
total_registered=total_registered,
|
|
134
|
+
)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise NetworkError(f"Failed to query NullifierRegistry (app {app_id}): {e}") from e
|
|
137
|
+
|
|
138
|
+
def is_nullifier_registered(self, nullifier_hex: str) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
Check if a specific nullifier is registered in box storage.
|
|
141
|
+
|
|
142
|
+
More precise than verify_kyc() — directly confirms a specific
|
|
143
|
+
credential nullifier exists on-chain.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
nullifier_hex: 32-byte nullifier as hex string (64 chars, no 0x prefix)
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
True if the nullifier is registered, False otherwise
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
registered = client.is_nullifier_registered("deadbeef" * 8)
|
|
153
|
+
"""
|
|
154
|
+
app_id = self._contracts["nullifier_registry"]
|
|
155
|
+
try:
|
|
156
|
+
# Box name = b"nul_" + 32-byte nullifier
|
|
157
|
+
prefix = b"nul_"
|
|
158
|
+
nullifier_bytes = bytes.fromhex(nullifier_hex.lstrip("0x").zfill(64))
|
|
159
|
+
box_name = base64.b64encode(prefix + nullifier_bytes).decode()
|
|
160
|
+
|
|
161
|
+
result = self._algod.application_box_by_name(app_id, box_name)
|
|
162
|
+
return bool(result.get("value"))
|
|
163
|
+
except algosdk.error.AlgodHTTPError as e:
|
|
164
|
+
if "box not found" in str(e).lower() or e.code == 404:
|
|
165
|
+
return False
|
|
166
|
+
raise NetworkError(f"Box lookup failed: {e}") from e
|
|
167
|
+
except Exception as e:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
def get_credential_asa_id(self) -> int:
|
|
171
|
+
"""Return the KYCRED ASA (Algorand Standard Asset) ID."""
|
|
172
|
+
return self._contracts["credential_asa_id"]
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def explorer_tx_url(txid: str) -> str:
|
|
176
|
+
"""Return Allo.info explorer URL for a transaction."""
|
|
177
|
+
return f"https://allo.info/tx/{txid}"
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def explorer_app_url(app_id: int) -> str:
|
|
181
|
+
"""Return Allo.info explorer URL for an application."""
|
|
182
|
+
return f"https://allo.info/application/{app_id}"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
exceptions.py — Ciphera SDK custom exceptions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CipheraError(Exception):
|
|
7
|
+
"""Base exception for all Ciphera SDK errors."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProofVerificationError(CipheraError):
|
|
12
|
+
"""Raised when a ZK proof fails verification."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NetworkError(CipheraError):
|
|
17
|
+
"""Raised when Algorand network requests fail."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ContractError(CipheraError):
|
|
22
|
+
"""Raised when smart contract interactions fail."""
|
|
23
|
+
pass
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
models.py — Ciphera SDK Pydantic models
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PublicSignals:
|
|
12
|
+
"""Public signals output from the ZK circuit."""
|
|
13
|
+
nullifier: str # BN254 field element as decimal string
|
|
14
|
+
merkle_root: str # SMT root
|
|
15
|
+
app_id: str # Algorand NullifierRegistry app ID
|
|
16
|
+
is_indian: str # "1"
|
|
17
|
+
is_adult: str # "1"
|
|
18
|
+
is_kyc_verified: str # "1"
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_list(cls, signals: List[str]) -> "PublicSignals":
|
|
22
|
+
"""Parse from snarkjs publicSignals array."""
|
|
23
|
+
if len(signals) < 6:
|
|
24
|
+
raise ValueError(f"Expected >= 6 public signals, got {len(signals)}")
|
|
25
|
+
return cls(
|
|
26
|
+
nullifier=signals[0],
|
|
27
|
+
merkle_root=signals[1],
|
|
28
|
+
app_id=signals[2],
|
|
29
|
+
is_indian=signals[3],
|
|
30
|
+
is_adult=signals[4],
|
|
31
|
+
is_kyc_verified=signals[5],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def nullifier_hex(self) -> str:
|
|
36
|
+
"""Nullifier as 32-byte hex string (suitable for Algorand box keys)."""
|
|
37
|
+
return format(int(self.nullifier), '064x')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ProofResult:
|
|
42
|
+
"""Result of ZK proof verification."""
|
|
43
|
+
valid: bool
|
|
44
|
+
nullifier_hex: Optional[str] = None
|
|
45
|
+
public_signals: Optional[PublicSignals] = None
|
|
46
|
+
error: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class KYCStatus:
|
|
51
|
+
"""KYC verification status for a wallet address."""
|
|
52
|
+
is_verified: bool
|
|
53
|
+
app_id: int
|
|
54
|
+
total_registered: Optional[int] = None
|
|
55
|
+
nullifier: Optional[str] = None
|
|
56
|
+
error: Optional[str] = None
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
verifier.py — ZK proof verification for the Ciphera Python SDK.
|
|
3
|
+
|
|
4
|
+
Verifies Groth16 proofs using snarkjs via Node.js subprocess.
|
|
5
|
+
The issuer backend calls verify_proof() before registering
|
|
6
|
+
a nullifier on-chain.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import subprocess
|
|
13
|
+
import shutil
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Union, List, Dict, Any
|
|
17
|
+
|
|
18
|
+
from .models import ProofResult, PublicSignals
|
|
19
|
+
from .exceptions import ProofVerificationError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def verify_proof(
|
|
23
|
+
verification_key: Union[Dict[str, Any], str, Path],
|
|
24
|
+
proof: Dict[str, Any],
|
|
25
|
+
public_signals: List[str],
|
|
26
|
+
expected_app_id: Optional[Union[int, str]] = None,
|
|
27
|
+
) -> ProofResult:
|
|
28
|
+
"""
|
|
29
|
+
Verify a Groth16 ZK proof using snarkjs (via Node.js subprocess).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
verification_key: The Groth16 verification key (dict, JSON string, or file path).
|
|
33
|
+
proof: The Groth16 proof object from snarkjs.
|
|
34
|
+
public_signals: List of public signal strings from the circuit.
|
|
35
|
+
expected_app_id: Optional NullifierRegistry app ID to guard cross-app attacks.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
ProofResult with .valid, .nullifier_hex, .public_signals, .error
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
import json
|
|
42
|
+
vk = json.load(open("verification_key.json"))
|
|
43
|
+
result = verify_proof(vk, proof_dict, signal_list, expected_app_id=756272073)
|
|
44
|
+
if result.valid:
|
|
45
|
+
print(result.nullifier_hex)
|
|
46
|
+
"""
|
|
47
|
+
# Normalise verification_key to dict
|
|
48
|
+
if isinstance(verification_key, (str, Path)):
|
|
49
|
+
p = Path(verification_key)
|
|
50
|
+
if p.exists():
|
|
51
|
+
with open(p) as f:
|
|
52
|
+
vk_dict = json.load(f)
|
|
53
|
+
else:
|
|
54
|
+
vk_dict = json.loads(str(verification_key))
|
|
55
|
+
else:
|
|
56
|
+
vk_dict = verification_key
|
|
57
|
+
|
|
58
|
+
# Validate public signals length
|
|
59
|
+
if len(public_signals) < 6:
|
|
60
|
+
return ProofResult(
|
|
61
|
+
valid=False,
|
|
62
|
+
error=f"Expected >= 6 public signals, got {len(public_signals)}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Quick sanity checks before expensive snarkjs call
|
|
66
|
+
is_indian, is_adult, is_kyc = public_signals[3], public_signals[4], public_signals[5]
|
|
67
|
+
if is_indian != "1":
|
|
68
|
+
return ProofResult(valid=False, error="isIndian must be 1")
|
|
69
|
+
if is_adult != "1":
|
|
70
|
+
return ProofResult(valid=False, error="isAdult must be 1")
|
|
71
|
+
if is_kyc != "1":
|
|
72
|
+
return ProofResult(valid=False, error="isKYCVerified must be 1")
|
|
73
|
+
|
|
74
|
+
app_id_signal = public_signals[2]
|
|
75
|
+
if expected_app_id is not None and app_id_signal != str(expected_app_id):
|
|
76
|
+
return ProofResult(
|
|
77
|
+
valid=False,
|
|
78
|
+
error=f"appId mismatch: expected {expected_app_id}, got {app_id_signal}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Run snarkjs verification via Node.js
|
|
82
|
+
if not shutil.which("node"):
|
|
83
|
+
raise ProofVerificationError(
|
|
84
|
+
"Node.js is required for ZK proof verification. "
|
|
85
|
+
"Install from https://nodejs.org"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
js_script = """
|
|
89
|
+
const snarkjs = require('snarkjs');
|
|
90
|
+
const input = JSON.parse(process.argv[2]);
|
|
91
|
+
snarkjs.groth16.verify(input.vk, input.publicSignals, input.proof)
|
|
92
|
+
.then(valid => { process.stdout.write(JSON.stringify({valid})); })
|
|
93
|
+
.catch(e => { process.stdout.write(JSON.stringify({valid: false, error: e.message})); });
|
|
94
|
+
"""
|
|
95
|
+
payload = json.dumps({
|
|
96
|
+
"vk": vk_dict,
|
|
97
|
+
"proof": proof,
|
|
98
|
+
"publicSignals": public_signals,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
with tempfile.NamedTemporaryFile(suffix=".js", mode="w", delete=False) as f:
|
|
102
|
+
f.write(js_script)
|
|
103
|
+
script_path = f.name
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
["node", script_path, payload],
|
|
108
|
+
capture_output=True, text=True, timeout=60
|
|
109
|
+
)
|
|
110
|
+
output = json.loads(result.stdout)
|
|
111
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError) as e:
|
|
112
|
+
return ProofResult(valid=False, error=f"Verification failed: {e}")
|
|
113
|
+
finally:
|
|
114
|
+
Path(script_path).unlink(missing_ok=True)
|
|
115
|
+
|
|
116
|
+
if not output.get("valid"):
|
|
117
|
+
return ProofResult(valid=False, error=output.get("error", "Proof is invalid"))
|
|
118
|
+
|
|
119
|
+
signals = PublicSignals.from_list(public_signals)
|
|
120
|
+
return ProofResult(
|
|
121
|
+
valid=True,
|
|
122
|
+
nullifier_hex=signals.nullifier_hex,
|
|
123
|
+
public_signals=signals,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Optional type hint (Python 3.9 compat)
|
|
128
|
+
from typing import Optional
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ciphera"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Zero-Knowledge KYC for Algorand — verify KYC status on-chain without exposing personal data"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Aditya Pandey", email = "adityapandey060806@gmail.com" }
|
|
13
|
+
]
|
|
14
|
+
keywords = [
|
|
15
|
+
"algorand", "zero-knowledge", "zk-proof", "kyc",
|
|
16
|
+
"aadhaar", "privacy", "blockchain", "snarkjs", "groth16"
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Security :: Cryptography",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
]
|
|
30
|
+
requires-python = ">=3.9"
|
|
31
|
+
dependencies = [
|
|
32
|
+
"py-algorand-sdk>=2.6.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
algorand = ["py-algorand-sdk>=2.6.0"]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=7.0",
|
|
39
|
+
"pytest-asyncio>=0.23",
|
|
40
|
+
"twine>=5.0",
|
|
41
|
+
"build>=1.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/Aditya060806/Ciphera"
|
|
46
|
+
Repository = "https://github.com/Aditya060806/Ciphera"
|
|
47
|
+
Issues = "https://github.com/Aditya060806/Ciphera/issues"
|
|
48
|
+
Documentation = "https://github.com/Aditya060806/Ciphera#readme"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["ciphera"]
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.sdist]
|
|
54
|
+
include = [
|
|
55
|
+
"ciphera/",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"pyproject.toml",
|
|
59
|
+
]
|