zktally 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.
- zktally-0.1.0/.gitignore +25 -0
- zktally-0.1.0/LICENSE +21 -0
- zktally-0.1.0/PKG-INFO +185 -0
- zktally-0.1.0/README.md +136 -0
- zktally-0.1.0/pyproject.toml +90 -0
- zktally-0.1.0/src/zktally/__init__.py +202 -0
- zktally-0.1.0/src/zktally/_backend.py +237 -0
- zktally-0.1.0/src/zktally/_rng.py +97 -0
- zktally-0.1.0/src/zktally/cli/__init__.py +0 -0
- zktally-0.1.0/src/zktally/cli/main.py +477 -0
- zktally-0.1.0/src/zktally/domain.py +39 -0
- zktally-0.1.0/src/zktally/errors.py +66 -0
- zktally-0.1.0/src/zktally/primitives/__init__.py +1 -0
- zktally-0.1.0/src/zktally/primitives/curve.py +224 -0
- zktally-0.1.0/src/zktally/primitives/hash_to_curve.py +176 -0
- zktally-0.1.0/src/zktally/primitives/lsag.py +280 -0
- zktally-0.1.0/src/zktally/primitives/nizk.py +283 -0
- zktally-0.1.0/src/zktally/primitives/paillier.py +243 -0
- zktally-0.1.0/src/zktally/primitives/threshold.py +347 -0
- zktally-0.1.0/src/zktally/protocol/__init__.py +24 -0
- zktally-0.1.0/src/zktally/protocol/ballot.py +221 -0
- zktally-0.1.0/src/zktally/protocol/board.py +60 -0
- zktally-0.1.0/src/zktally/protocol/params.py +135 -0
- zktally-0.1.0/src/zktally/protocol/tally.py +251 -0
- zktally-0.1.0/src/zktally/py.typed +0 -0
- zktally-0.1.0/src/zktally/serialization/__init__.py +1 -0
- zktally-0.1.0/src/zktally/serialization/encoding.py +152 -0
- zktally-0.1.0/src/zktally/serialization/json_codec.py +350 -0
- zktally-0.1.0/src/zktally/vectors/__init__.py +0 -0
- zktally-0.1.0/src/zktally/vectors/generate.py +452 -0
- zktally-0.1.0/tests/__init__.py +0 -0
- zktally-0.1.0/tests/conformance/__init__.py +0 -0
- zktally-0.1.0/tests/conformance/test_corpus.py +243 -0
- zktally-0.1.0/tests/conftest.py +98 -0
- zktally-0.1.0/tests/property/__init__.py +0 -0
- zktally-0.1.0/tests/property/test_properties.py +156 -0
- zktally-0.1.0/tests/tamper/__init__.py +0 -0
- zktally-0.1.0/tests/tamper/test_tamper.py +261 -0
- zktally-0.1.0/tests/unit/__init__.py +0 -0
- zktally-0.1.0/tests/unit/test_cli.py +203 -0
- zktally-0.1.0/tests/unit/test_curve.py +159 -0
- zktally-0.1.0/tests/unit/test_encoding.py +96 -0
- zktally-0.1.0/tests/unit/test_lsag.py +233 -0
- zktally-0.1.0/tests/unit/test_nizk.py +178 -0
- zktally-0.1.0/tests/unit/test_paillier.py +153 -0
- zktally-0.1.0/tests/unit/test_protocol.py +321 -0
- zktally-0.1.0/tests/unit/test_serialization.py +218 -0
- zktally-0.1.0/tests/unit/test_threshold.py +148 -0
- zktally-0.1.0/tests/vectors/.omc/state/sessions/7a15ca98-f64f-4ebf-985e-89d5f877f575/last-tool-error-state.json +7 -0
- zktally-0.1.0/tests/vectors/.omc/state/sessions/7a15ca98-f64f-4ebf-985e-89d5f877f575/pre-tool-advisory-throttle.json +10 -0
- zktally-0.1.0/tests/vectors/ballot/binary-body.json +17 -0
- zktally-0.1.0/tests/vectors/encoding/worked-example.json +27 -0
- zktally-0.1.0/tests/vectors/lsag/key-images.json +30 -0
- zktally-0.1.0/tests/vectors/lsag/ring2-all-indices.json +39 -0
- zktally-0.1.0/tests/vectors/lsag/ring3-all-indices.json +53 -0
- zktally-0.1.0/tests/vectors/lsag/ring8-all-indices.json +153 -0
- zktally-0.1.0/tests/vectors/manifest.json +20 -0
- zktally-0.1.0/tests/vectors/nizk-binary/valid-v0.json +25 -0
- zktally-0.1.0/tests/vectors/nizk-binary/valid-v1.json +25 -0
- zktally-0.1.0/tests/vectors/nizk-oneofk/k2-choice0.json +19 -0
- zktally-0.1.0/tests/vectors/nizk-oneofk/k3-choice0.json +20 -0
- zktally-0.1.0/tests/vectors/paillier/aggregate.json +22 -0
- zktally-0.1.0/tests/vectors/paillier/ciphertext-validity.json +27 -0
- zktally-0.1.0/tests/vectors/paillier/encrypt-decrypt.json +47 -0
zktally-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.eggs/
|
|
8
|
+
|
|
9
|
+
# Environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
|
|
14
|
+
# Tooling caches
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.hypothesis/
|
|
19
|
+
.coverage
|
|
20
|
+
coverage.xml
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# Editors
|
|
24
|
+
.idea/
|
|
25
|
+
*.swp
|
zktally-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Muhammad Faiz A <m@faiz.at>
|
|
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.
|
zktally-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zktally
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Privacy-preserving, anonymously verifiable e-voting: linkable ring signatures, Paillier homomorphic encryption, and NIZK vote-correctness proofs. UNAUDITED ACADEMIC SOFTWARE - not for binding elections.
|
|
5
|
+
Project-URL: Homepage, https://zktally.github.io
|
|
6
|
+
Project-URL: Repository, https://github.com/ZKTally/ZKTally
|
|
7
|
+
Author: Muhammad Faiz Atharrahman
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025 Muhammad Faiz A <m@faiz.at>
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: cryptography,e-voting,paillier,ring-signature,zero-knowledge
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Science/Research
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
37
|
+
Classifier: Topic :: Security :: Cryptography
|
|
38
|
+
Classifier: Typing :: Typed
|
|
39
|
+
Requires-Python: >=3.11
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
42
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
43
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
44
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
45
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
46
|
+
Provides-Extra: fast
|
|
47
|
+
Requires-Dist: gmpy2>=2.1; extra == 'fast'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# ZKTally
|
|
51
|
+
|
|
52
|
+
Privacy-preserving, anonymously verifiable e-voting from linkable ring signatures, Paillier
|
|
53
|
+
homomorphic encryption, and NIZK vote-correctness proofs.
|
|
54
|
+
|
|
55
|
+
> **ZKTally is unaudited academic software implementing a protocol from a student research
|
|
56
|
+
> paper. It has not been reviewed by professional cryptographers and must not be used for any
|
|
57
|
+
> binding election.**
|
|
58
|
+
|
|
59
|
+
A ballot is the triple `(ciphertexts, proofs, ring signature)`:
|
|
60
|
+
|
|
61
|
+
- **Paillier encryption** hides the vote. Tallying multiplies ciphertexts and decrypts only the
|
|
62
|
+
aggregate, so no individual ballot is ever opened.
|
|
63
|
+
- **A NIZK OR-proof** shows the plaintext is a legal vote without revealing which one.
|
|
64
|
+
- **A linkable ring signature** proves the caster is *some* registered voter, and its
|
|
65
|
+
deterministic key image makes a second ballot from the same key detectable.
|
|
66
|
+
|
|
67
|
+
Wire format: `zktally/1`.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install zktally # no runtime dependencies
|
|
75
|
+
pip install "zktally[fast]" # optional gmpy2 acceleration
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Python 3.11+. The pure-stdlib path works everywhere; `gmpy2` is roughly 5x faster on Paillier
|
|
79
|
+
and must produce byte-identical results.
|
|
80
|
+
|
|
81
|
+
## Library
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from zktally import *
|
|
85
|
+
|
|
86
|
+
pk, sk = paillier_keygen(2048)
|
|
87
|
+
|
|
88
|
+
# Voters generate their own keys. Nothing else may generate them.
|
|
89
|
+
voters = [ring_keygen() for _ in range(5)]
|
|
90
|
+
ring = canonical_ring([v.public for v in voters])
|
|
91
|
+
|
|
92
|
+
params = ElectionParams(
|
|
93
|
+
election_id=new_election_id(),
|
|
94
|
+
title="Adopt the proposal?",
|
|
95
|
+
ballot_type="binary", k=1, candidates=("Yes",),
|
|
96
|
+
public_key=pk, trust_model="single-authority", ring=ring,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
board = Board(params=params)
|
|
100
|
+
for choice, voter in zip([1, 0, 1, 1, 0], voters):
|
|
101
|
+
board.append(cast_ballot(choice, params, voter, ring.index(voter.public)))
|
|
102
|
+
|
|
103
|
+
print(tally(board, sk).totals) # (3,)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Multi-candidate elections use `ballot_type="one-of-k"` with `k >= 2`, which proves each column
|
|
107
|
+
binary *and* proves the columns sum to exactly one.
|
|
108
|
+
|
|
109
|
+
## Command line
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
zktally keygen --bits 3072 [--threshold 3-of-5] --out keys/
|
|
113
|
+
zktally election new --title "…" --type binary|one-of-k --candidates a,b,c \
|
|
114
|
+
--key keys/public.json --out params.json
|
|
115
|
+
zktally voterkey --out voter.json # voter-side only
|
|
116
|
+
zktally register --params params.json --add voter*.json
|
|
117
|
+
zktally vote --params params.json --key voter.json --choice 1 --out ballot.json
|
|
118
|
+
zktally board --params params.json --add ballot*.json --out board.json
|
|
119
|
+
zktally verify --board board.json [--explain]
|
|
120
|
+
zktally tally --board board.json --shares s1.json,s2.json --out result.json
|
|
121
|
+
zktally audit --board board.json --result result.json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`zktally audit` takes **no secret material**. It re-verifies every ballot and every partial
|
|
125
|
+
decryption from public data alone, which is what makes public verifiability checkable rather
|
|
126
|
+
than asserted.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Security properties
|
|
131
|
+
|
|
132
|
+
✅ achieved · ⚠️ conditional · ❌ not provided
|
|
133
|
+
|
|
134
|
+
| Property | | Conditions |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| Ballot secrecy | ⚠️ | Against anyone below quorum. Not against `t` colluding authorities, nor against a dishonest dealer during v1 setup |
|
|
137
|
+
| Voter anonymity | ⚠️ | Anonymity set is the ring, or the sub-ring when partitioned. Defeated by traffic analysis |
|
|
138
|
+
| Double-vote prevention | ✅ | Key image bound into every challenge; first-wins resolution |
|
|
139
|
+
| Ballot-stuffing resistance | ✅ | Ring fixed by the election parameters, never read from a ballot |
|
|
140
|
+
| Vote well-formedness | ✅ | Binary / 1-of-k NIZK plus mandatory ciphertext-domain validation |
|
|
141
|
+
| Public verifiability of ballots | ✅ | Anyone re-verifies every proof and signature from public data |
|
|
142
|
+
| Public verifiability of the tally | ✅ | Threshold mode only; partial decryptions carry correctness proofs |
|
|
143
|
+
| Non-malleability | ✅ | Election context bound into every transcript; proofs bound into the signed message |
|
|
144
|
+
| Eligibility | ⚠️ | Cryptographically enforced *given* an honest registrar |
|
|
145
|
+
| **Receipt-freeness** | ❌ | The voter holds the encryption randomness, which reconstructs their ciphertext. Structural, not a bug |
|
|
146
|
+
| **Coercion resistance** | ❌ | Not implemented. Needs fake credentials or an untappable channel |
|
|
147
|
+
| Everlasting privacy | ❌ | Ciphertexts are public forever; a future break reveals every vote |
|
|
148
|
+
| Side-channel resistance | ❌ | Not constant-time, and pure-Python `int` arithmetic cannot be made so |
|
|
149
|
+
| Post-quantum security | ❌ | Paillier and secp256k1 both fall to Shor |
|
|
150
|
+
|
|
151
|
+
Under `trust_model="single-authority"` the key holder can decrypt any individual ballot and the
|
|
152
|
+
published total carries no proof. `zktally audit` reports that decryption as **not proved**
|
|
153
|
+
rather than verified.
|
|
154
|
+
|
|
155
|
+
**v1 uses a trusted dealer** for threshold setup: that party transiently knows the factors, so
|
|
156
|
+
ballot privacy rests on them destroying that material rather than on the threshold itself.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
uv venv && uv pip install -e ".[dev]"
|
|
162
|
+
pytest # full suite
|
|
163
|
+
pytest -m "not slow" # skip real-parameter keygen
|
|
164
|
+
ruff check . && ruff format --check .
|
|
165
|
+
mypy
|
|
166
|
+
zktally vectors generate --seed 00…00 --out tests/vectors/
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Test suites: `unit/`, `property/` (Hypothesis), `conformance/` (the shared vector corpus, run
|
|
170
|
+
under both numeric backends), and `tamper/` — one test per known attack, each asserting the
|
|
171
|
+
exact rejection reason.
|
|
172
|
+
|
|
173
|
+
The corpus in `tests/vectors/` is generated here and mirrored into the TypeScript port, which
|
|
174
|
+
is validated against it. It has exactly one source; hand-editing the mirror is a CI failure.
|
|
175
|
+
|
|
176
|
+
## Relationship to the paper
|
|
177
|
+
|
|
178
|
+
This implementation is normative where it and the originating paper disagree. The paper states
|
|
179
|
+
the second OR-proof branch as a discrete-log relation that does not match its own prover, omits
|
|
180
|
+
`R_i` and the ring from the ring-signature challenge, leaves the curve, hash, hash-to-curve and
|
|
181
|
+
point encoding unpinned, and claims receipt-freeness that the construction does not provide.
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT. See [LICENSE](LICENSE).
|
zktally-0.1.0/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# ZKTally
|
|
2
|
+
|
|
3
|
+
Privacy-preserving, anonymously verifiable e-voting from linkable ring signatures, Paillier
|
|
4
|
+
homomorphic encryption, and NIZK vote-correctness proofs.
|
|
5
|
+
|
|
6
|
+
> **ZKTally is unaudited academic software implementing a protocol from a student research
|
|
7
|
+
> paper. It has not been reviewed by professional cryptographers and must not be used for any
|
|
8
|
+
> binding election.**
|
|
9
|
+
|
|
10
|
+
A ballot is the triple `(ciphertexts, proofs, ring signature)`:
|
|
11
|
+
|
|
12
|
+
- **Paillier encryption** hides the vote. Tallying multiplies ciphertexts and decrypts only the
|
|
13
|
+
aggregate, so no individual ballot is ever opened.
|
|
14
|
+
- **A NIZK OR-proof** shows the plaintext is a legal vote without revealing which one.
|
|
15
|
+
- **A linkable ring signature** proves the caster is *some* registered voter, and its
|
|
16
|
+
deterministic key image makes a second ballot from the same key detectable.
|
|
17
|
+
|
|
18
|
+
Wire format: `zktally/1`.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install zktally # no runtime dependencies
|
|
26
|
+
pip install "zktally[fast]" # optional gmpy2 acceleration
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Python 3.11+. The pure-stdlib path works everywhere; `gmpy2` is roughly 5x faster on Paillier
|
|
30
|
+
and must produce byte-identical results.
|
|
31
|
+
|
|
32
|
+
## Library
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from zktally import *
|
|
36
|
+
|
|
37
|
+
pk, sk = paillier_keygen(2048)
|
|
38
|
+
|
|
39
|
+
# Voters generate their own keys. Nothing else may generate them.
|
|
40
|
+
voters = [ring_keygen() for _ in range(5)]
|
|
41
|
+
ring = canonical_ring([v.public for v in voters])
|
|
42
|
+
|
|
43
|
+
params = ElectionParams(
|
|
44
|
+
election_id=new_election_id(),
|
|
45
|
+
title="Adopt the proposal?",
|
|
46
|
+
ballot_type="binary", k=1, candidates=("Yes",),
|
|
47
|
+
public_key=pk, trust_model="single-authority", ring=ring,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
board = Board(params=params)
|
|
51
|
+
for choice, voter in zip([1, 0, 1, 1, 0], voters):
|
|
52
|
+
board.append(cast_ballot(choice, params, voter, ring.index(voter.public)))
|
|
53
|
+
|
|
54
|
+
print(tally(board, sk).totals) # (3,)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Multi-candidate elections use `ballot_type="one-of-k"` with `k >= 2`, which proves each column
|
|
58
|
+
binary *and* proves the columns sum to exactly one.
|
|
59
|
+
|
|
60
|
+
## Command line
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
zktally keygen --bits 3072 [--threshold 3-of-5] --out keys/
|
|
64
|
+
zktally election new --title "…" --type binary|one-of-k --candidates a,b,c \
|
|
65
|
+
--key keys/public.json --out params.json
|
|
66
|
+
zktally voterkey --out voter.json # voter-side only
|
|
67
|
+
zktally register --params params.json --add voter*.json
|
|
68
|
+
zktally vote --params params.json --key voter.json --choice 1 --out ballot.json
|
|
69
|
+
zktally board --params params.json --add ballot*.json --out board.json
|
|
70
|
+
zktally verify --board board.json [--explain]
|
|
71
|
+
zktally tally --board board.json --shares s1.json,s2.json --out result.json
|
|
72
|
+
zktally audit --board board.json --result result.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`zktally audit` takes **no secret material**. It re-verifies every ballot and every partial
|
|
76
|
+
decryption from public data alone, which is what makes public verifiability checkable rather
|
|
77
|
+
than asserted.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Security properties
|
|
82
|
+
|
|
83
|
+
✅ achieved · ⚠️ conditional · ❌ not provided
|
|
84
|
+
|
|
85
|
+
| Property | | Conditions |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| Ballot secrecy | ⚠️ | Against anyone below quorum. Not against `t` colluding authorities, nor against a dishonest dealer during v1 setup |
|
|
88
|
+
| Voter anonymity | ⚠️ | Anonymity set is the ring, or the sub-ring when partitioned. Defeated by traffic analysis |
|
|
89
|
+
| Double-vote prevention | ✅ | Key image bound into every challenge; first-wins resolution |
|
|
90
|
+
| Ballot-stuffing resistance | ✅ | Ring fixed by the election parameters, never read from a ballot |
|
|
91
|
+
| Vote well-formedness | ✅ | Binary / 1-of-k NIZK plus mandatory ciphertext-domain validation |
|
|
92
|
+
| Public verifiability of ballots | ✅ | Anyone re-verifies every proof and signature from public data |
|
|
93
|
+
| Public verifiability of the tally | ✅ | Threshold mode only; partial decryptions carry correctness proofs |
|
|
94
|
+
| Non-malleability | ✅ | Election context bound into every transcript; proofs bound into the signed message |
|
|
95
|
+
| Eligibility | ⚠️ | Cryptographically enforced *given* an honest registrar |
|
|
96
|
+
| **Receipt-freeness** | ❌ | The voter holds the encryption randomness, which reconstructs their ciphertext. Structural, not a bug |
|
|
97
|
+
| **Coercion resistance** | ❌ | Not implemented. Needs fake credentials or an untappable channel |
|
|
98
|
+
| Everlasting privacy | ❌ | Ciphertexts are public forever; a future break reveals every vote |
|
|
99
|
+
| Side-channel resistance | ❌ | Not constant-time, and pure-Python `int` arithmetic cannot be made so |
|
|
100
|
+
| Post-quantum security | ❌ | Paillier and secp256k1 both fall to Shor |
|
|
101
|
+
|
|
102
|
+
Under `trust_model="single-authority"` the key holder can decrypt any individual ballot and the
|
|
103
|
+
published total carries no proof. `zktally audit` reports that decryption as **not proved**
|
|
104
|
+
rather than verified.
|
|
105
|
+
|
|
106
|
+
**v1 uses a trusted dealer** for threshold setup: that party transiently knows the factors, so
|
|
107
|
+
ballot privacy rests on them destroying that material rather than on the threshold itself.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
uv venv && uv pip install -e ".[dev]"
|
|
113
|
+
pytest # full suite
|
|
114
|
+
pytest -m "not slow" # skip real-parameter keygen
|
|
115
|
+
ruff check . && ruff format --check .
|
|
116
|
+
mypy
|
|
117
|
+
zktally vectors generate --seed 00…00 --out tests/vectors/
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Test suites: `unit/`, `property/` (Hypothesis), `conformance/` (the shared vector corpus, run
|
|
121
|
+
under both numeric backends), and `tamper/` — one test per known attack, each asserting the
|
|
122
|
+
exact rejection reason.
|
|
123
|
+
|
|
124
|
+
The corpus in `tests/vectors/` is generated here and mirrored into the TypeScript port, which
|
|
125
|
+
is validated against it. It has exactly one source; hand-editing the mirror is a CI failure.
|
|
126
|
+
|
|
127
|
+
## Relationship to the paper
|
|
128
|
+
|
|
129
|
+
This implementation is normative where it and the originating paper disagree. The paper states
|
|
130
|
+
the second OR-proof branch as a discrete-log relation that does not match its own prover, omits
|
|
131
|
+
`R_i` and the ring from the ring-signature challenge, leaves the curve, hash, hash-to-curve and
|
|
132
|
+
point encoding unpinned, and claims receipt-freeness that the construction does not provide.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zktally"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Privacy-preserving, anonymously verifiable e-voting: linkable ring signatures, Paillier homomorphic encryption, and NIZK vote-correctness proofs. UNAUDITED ACADEMIC SOFTWARE - not for binding elections."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Muhammad Faiz Atharrahman" }]
|
|
13
|
+
keywords = ["cryptography", "e-voting", "paillier", "ring-signature", "zero-knowledge"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Security :: Cryptography",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
fast = ["gmpy2>=2.1"]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=8.0",
|
|
30
|
+
"pytest-cov>=5.0",
|
|
31
|
+
"hypothesis>=6.100",
|
|
32
|
+
"ruff>=0.6",
|
|
33
|
+
"mypy>=1.11",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
zktally = "zktally.cli.main:main"
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://zktally.github.io"
|
|
41
|
+
Repository = "https://github.com/ZKTally/ZKTally"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/zktally"]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.sdist]
|
|
47
|
+
include = ["src/", "tests/", "README.md", "LICENSE"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 100
|
|
51
|
+
target-version = "py311"
|
|
52
|
+
src = ["src", "tests"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "SIM", "RUF", "S"]
|
|
56
|
+
ignore = [
|
|
57
|
+
"S101", # assert is used for internal invariants
|
|
58
|
+
"N806", # crypto pseudocode uses uppercase locals (N, L, R, A, C)
|
|
59
|
+
"N803", # ... and uppercase arguments
|
|
60
|
+
"E741", # `l` is not used; `L` is a spec symbol
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint.per-file-ignores]
|
|
64
|
+
"tests/*" = ["S", "N"]
|
|
65
|
+
"src/zktally/vectors/*" = ["S311"]
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
python_version = "3.11"
|
|
69
|
+
strict = true
|
|
70
|
+
warn_unreachable = true
|
|
71
|
+
disallow_any_explicit = false
|
|
72
|
+
files = ["src/zktally"]
|
|
73
|
+
|
|
74
|
+
[[tool.mypy.overrides]]
|
|
75
|
+
module = "gmpy2.*"
|
|
76
|
+
ignore_missing_imports = true
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
testpaths = ["tests"]
|
|
80
|
+
addopts = "-q --strict-markers"
|
|
81
|
+
markers = [
|
|
82
|
+
"slow: tests that take more than a few seconds (keygen, large rings)",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[tool.coverage.run]
|
|
86
|
+
source = ["src/zktally"]
|
|
87
|
+
branch = true
|
|
88
|
+
|
|
89
|
+
[tool.coverage.report]
|
|
90
|
+
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError"]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""ZKTally - privacy-preserving, anonymously verifiable e-voting.
|
|
2
|
+
|
|
3
|
+
A ballot is the triple ``(ciphertexts, proofs, ring signature)``:
|
|
4
|
+
|
|
5
|
+
* **Paillier** homomorphic encryption hides each vote, and lets the tally be
|
|
6
|
+
computed by multiplying ciphertexts so no individual ballot is ever opened.
|
|
7
|
+
* A **NIZK OR-proof** shows the plaintext is a valid vote without revealing it.
|
|
8
|
+
* A **linkable ring signature** proves the caster is *some* registered voter,
|
|
9
|
+
while a deterministic key image makes a second ballot from the same key
|
|
10
|
+
detectable.
|
|
11
|
+
|
|
12
|
+
.. warning::
|
|
13
|
+
|
|
14
|
+
ZKTally is unaudited academic software implementing a protocol from a
|
|
15
|
+
student research paper. It has not been reviewed by professional
|
|
16
|
+
cryptographers and must not be used for any binding election.
|
|
17
|
+
|
|
18
|
+
Quick start::
|
|
19
|
+
|
|
20
|
+
from zktally import *
|
|
21
|
+
|
|
22
|
+
pk, sk = paillier_keygen(2048)
|
|
23
|
+
voters = [ring_keygen() for _ in range(5)]
|
|
24
|
+
ring = canonical_ring([v.public for v in voters])
|
|
25
|
+
params = ElectionParams(
|
|
26
|
+
election_id=new_election_id(), title="Referendum",
|
|
27
|
+
ballot_type="binary", k=1, candidates=("Yes",),
|
|
28
|
+
public_key=pk, trust_model="single-authority", ring=ring,
|
|
29
|
+
)
|
|
30
|
+
board = Board(params=params)
|
|
31
|
+
for i, voter in enumerate(voters):
|
|
32
|
+
board.append(cast_ballot(1, params, voter, ring.index(voter.public)))
|
|
33
|
+
print(tally(board, sk).totals)
|
|
34
|
+
|
|
35
|
+
Anything under ``primitives`` or ``_backend`` is private and may change without
|
|
36
|
+
a major version bump.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from .domain import SPEC_VERSION
|
|
42
|
+
from .errors import (
|
|
43
|
+
ConfigurationError,
|
|
44
|
+
DecodingError,
|
|
45
|
+
InvalidCiphertextError,
|
|
46
|
+
InvalidPlaintextError,
|
|
47
|
+
InvalidPointError,
|
|
48
|
+
InvalidRingError,
|
|
49
|
+
InvalidShareError,
|
|
50
|
+
UnsupportedVersionError,
|
|
51
|
+
ValidationError,
|
|
52
|
+
ZKTallyError,
|
|
53
|
+
)
|
|
54
|
+
from .primitives.curve import Point
|
|
55
|
+
from .primitives.lsag import (
|
|
56
|
+
MAX_RING,
|
|
57
|
+
RingSignature,
|
|
58
|
+
VoterKey,
|
|
59
|
+
canonical_ring,
|
|
60
|
+
compute_key_image,
|
|
61
|
+
lsag_link,
|
|
62
|
+
lsag_sign,
|
|
63
|
+
lsag_verify,
|
|
64
|
+
public_key_of,
|
|
65
|
+
ring_hash,
|
|
66
|
+
ring_keygen,
|
|
67
|
+
subring_index,
|
|
68
|
+
)
|
|
69
|
+
from .primitives.nizk import (
|
|
70
|
+
BinaryProof,
|
|
71
|
+
NthRootProof,
|
|
72
|
+
OneOfKBallot,
|
|
73
|
+
prove_binary,
|
|
74
|
+
prove_nth_root,
|
|
75
|
+
prove_one_of_k,
|
|
76
|
+
verify_binary,
|
|
77
|
+
verify_nth_root,
|
|
78
|
+
verify_one_of_k,
|
|
79
|
+
)
|
|
80
|
+
from .primitives.paillier import (
|
|
81
|
+
PaillierPrivateKey,
|
|
82
|
+
PaillierPublicKey,
|
|
83
|
+
aggregate,
|
|
84
|
+
decrypt,
|
|
85
|
+
encrypt,
|
|
86
|
+
encrypt_with,
|
|
87
|
+
is_valid_ciphertext,
|
|
88
|
+
paillier_keygen,
|
|
89
|
+
)
|
|
90
|
+
from .primitives.threshold import (
|
|
91
|
+
KeyShare,
|
|
92
|
+
PartialDecryption,
|
|
93
|
+
VerificationKeys,
|
|
94
|
+
combine_shares,
|
|
95
|
+
partial_decrypt,
|
|
96
|
+
threshold_keygen,
|
|
97
|
+
verify_partial,
|
|
98
|
+
)
|
|
99
|
+
from .protocol.ballot import (
|
|
100
|
+
Ballot,
|
|
101
|
+
BallotVerdict,
|
|
102
|
+
RejectReason,
|
|
103
|
+
cast_ballot,
|
|
104
|
+
verify_ballot,
|
|
105
|
+
)
|
|
106
|
+
from .protocol.board import Board
|
|
107
|
+
from .protocol.params import ElectionParams, election_context, new_election_id
|
|
108
|
+
from .protocol.tally import Rejection, TallyAudit, TallyResult, audit_tally, tally, verify_tally
|
|
109
|
+
from .serialization.json_codec import (
|
|
110
|
+
ballot_from_dict,
|
|
111
|
+
ballot_to_dict,
|
|
112
|
+
board_from_json,
|
|
113
|
+
board_to_json,
|
|
114
|
+
params_from_dict,
|
|
115
|
+
params_to_dict,
|
|
116
|
+
tally_from_dict,
|
|
117
|
+
tally_to_dict,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
__version__ = "0.1.0"
|
|
121
|
+
|
|
122
|
+
DISCLAIMER = (
|
|
123
|
+
"ZKTally is unaudited academic software implementing a protocol from a student "
|
|
124
|
+
"research paper. It has not been reviewed by professional cryptographers and must "
|
|
125
|
+
"not be used for any binding election."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
__all__ = [
|
|
129
|
+
"DISCLAIMER",
|
|
130
|
+
"MAX_RING",
|
|
131
|
+
"SPEC_VERSION",
|
|
132
|
+
"Ballot",
|
|
133
|
+
"BallotVerdict",
|
|
134
|
+
"BinaryProof",
|
|
135
|
+
"Board",
|
|
136
|
+
"ConfigurationError",
|
|
137
|
+
"DecodingError",
|
|
138
|
+
"ElectionParams",
|
|
139
|
+
"InvalidCiphertextError",
|
|
140
|
+
"InvalidPlaintextError",
|
|
141
|
+
"InvalidPointError",
|
|
142
|
+
"InvalidRingError",
|
|
143
|
+
"InvalidShareError",
|
|
144
|
+
"KeyShare",
|
|
145
|
+
"NthRootProof",
|
|
146
|
+
"OneOfKBallot",
|
|
147
|
+
"PaillierPrivateKey",
|
|
148
|
+
"PaillierPublicKey",
|
|
149
|
+
"PartialDecryption",
|
|
150
|
+
"Point",
|
|
151
|
+
"RejectReason",
|
|
152
|
+
"Rejection",
|
|
153
|
+
"RingSignature",
|
|
154
|
+
"TallyAudit",
|
|
155
|
+
"TallyResult",
|
|
156
|
+
"UnsupportedVersionError",
|
|
157
|
+
"ValidationError",
|
|
158
|
+
"VerificationKeys",
|
|
159
|
+
"VoterKey",
|
|
160
|
+
"ZKTallyError",
|
|
161
|
+
"__version__",
|
|
162
|
+
"aggregate",
|
|
163
|
+
"audit_tally",
|
|
164
|
+
"ballot_from_dict",
|
|
165
|
+
"ballot_to_dict",
|
|
166
|
+
"board_from_json",
|
|
167
|
+
"board_to_json",
|
|
168
|
+
"canonical_ring",
|
|
169
|
+
"cast_ballot",
|
|
170
|
+
"combine_shares",
|
|
171
|
+
"compute_key_image",
|
|
172
|
+
"decrypt",
|
|
173
|
+
"election_context",
|
|
174
|
+
"encrypt",
|
|
175
|
+
"encrypt_with",
|
|
176
|
+
"is_valid_ciphertext",
|
|
177
|
+
"lsag_link",
|
|
178
|
+
"lsag_sign",
|
|
179
|
+
"lsag_verify",
|
|
180
|
+
"new_election_id",
|
|
181
|
+
"paillier_keygen",
|
|
182
|
+
"params_from_dict",
|
|
183
|
+
"params_to_dict",
|
|
184
|
+
"partial_decrypt",
|
|
185
|
+
"prove_binary",
|
|
186
|
+
"prove_nth_root",
|
|
187
|
+
"prove_one_of_k",
|
|
188
|
+
"public_key_of",
|
|
189
|
+
"ring_hash",
|
|
190
|
+
"ring_keygen",
|
|
191
|
+
"subring_index",
|
|
192
|
+
"tally",
|
|
193
|
+
"tally_from_dict",
|
|
194
|
+
"tally_to_dict",
|
|
195
|
+
"threshold_keygen",
|
|
196
|
+
"verify_ballot",
|
|
197
|
+
"verify_binary",
|
|
198
|
+
"verify_nth_root",
|
|
199
|
+
"verify_one_of_k",
|
|
200
|
+
"verify_partial",
|
|
201
|
+
"verify_tally",
|
|
202
|
+
]
|