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.
Files changed (64) hide show
  1. zktally-0.1.0/.gitignore +25 -0
  2. zktally-0.1.0/LICENSE +21 -0
  3. zktally-0.1.0/PKG-INFO +185 -0
  4. zktally-0.1.0/README.md +136 -0
  5. zktally-0.1.0/pyproject.toml +90 -0
  6. zktally-0.1.0/src/zktally/__init__.py +202 -0
  7. zktally-0.1.0/src/zktally/_backend.py +237 -0
  8. zktally-0.1.0/src/zktally/_rng.py +97 -0
  9. zktally-0.1.0/src/zktally/cli/__init__.py +0 -0
  10. zktally-0.1.0/src/zktally/cli/main.py +477 -0
  11. zktally-0.1.0/src/zktally/domain.py +39 -0
  12. zktally-0.1.0/src/zktally/errors.py +66 -0
  13. zktally-0.1.0/src/zktally/primitives/__init__.py +1 -0
  14. zktally-0.1.0/src/zktally/primitives/curve.py +224 -0
  15. zktally-0.1.0/src/zktally/primitives/hash_to_curve.py +176 -0
  16. zktally-0.1.0/src/zktally/primitives/lsag.py +280 -0
  17. zktally-0.1.0/src/zktally/primitives/nizk.py +283 -0
  18. zktally-0.1.0/src/zktally/primitives/paillier.py +243 -0
  19. zktally-0.1.0/src/zktally/primitives/threshold.py +347 -0
  20. zktally-0.1.0/src/zktally/protocol/__init__.py +24 -0
  21. zktally-0.1.0/src/zktally/protocol/ballot.py +221 -0
  22. zktally-0.1.0/src/zktally/protocol/board.py +60 -0
  23. zktally-0.1.0/src/zktally/protocol/params.py +135 -0
  24. zktally-0.1.0/src/zktally/protocol/tally.py +251 -0
  25. zktally-0.1.0/src/zktally/py.typed +0 -0
  26. zktally-0.1.0/src/zktally/serialization/__init__.py +1 -0
  27. zktally-0.1.0/src/zktally/serialization/encoding.py +152 -0
  28. zktally-0.1.0/src/zktally/serialization/json_codec.py +350 -0
  29. zktally-0.1.0/src/zktally/vectors/__init__.py +0 -0
  30. zktally-0.1.0/src/zktally/vectors/generate.py +452 -0
  31. zktally-0.1.0/tests/__init__.py +0 -0
  32. zktally-0.1.0/tests/conformance/__init__.py +0 -0
  33. zktally-0.1.0/tests/conformance/test_corpus.py +243 -0
  34. zktally-0.1.0/tests/conftest.py +98 -0
  35. zktally-0.1.0/tests/property/__init__.py +0 -0
  36. zktally-0.1.0/tests/property/test_properties.py +156 -0
  37. zktally-0.1.0/tests/tamper/__init__.py +0 -0
  38. zktally-0.1.0/tests/tamper/test_tamper.py +261 -0
  39. zktally-0.1.0/tests/unit/__init__.py +0 -0
  40. zktally-0.1.0/tests/unit/test_cli.py +203 -0
  41. zktally-0.1.0/tests/unit/test_curve.py +159 -0
  42. zktally-0.1.0/tests/unit/test_encoding.py +96 -0
  43. zktally-0.1.0/tests/unit/test_lsag.py +233 -0
  44. zktally-0.1.0/tests/unit/test_nizk.py +178 -0
  45. zktally-0.1.0/tests/unit/test_paillier.py +153 -0
  46. zktally-0.1.0/tests/unit/test_protocol.py +321 -0
  47. zktally-0.1.0/tests/unit/test_serialization.py +218 -0
  48. zktally-0.1.0/tests/unit/test_threshold.py +148 -0
  49. zktally-0.1.0/tests/vectors/.omc/state/sessions/7a15ca98-f64f-4ebf-985e-89d5f877f575/last-tool-error-state.json +7 -0
  50. zktally-0.1.0/tests/vectors/.omc/state/sessions/7a15ca98-f64f-4ebf-985e-89d5f877f575/pre-tool-advisory-throttle.json +10 -0
  51. zktally-0.1.0/tests/vectors/ballot/binary-body.json +17 -0
  52. zktally-0.1.0/tests/vectors/encoding/worked-example.json +27 -0
  53. zktally-0.1.0/tests/vectors/lsag/key-images.json +30 -0
  54. zktally-0.1.0/tests/vectors/lsag/ring2-all-indices.json +39 -0
  55. zktally-0.1.0/tests/vectors/lsag/ring3-all-indices.json +53 -0
  56. zktally-0.1.0/tests/vectors/lsag/ring8-all-indices.json +153 -0
  57. zktally-0.1.0/tests/vectors/manifest.json +20 -0
  58. zktally-0.1.0/tests/vectors/nizk-binary/valid-v0.json +25 -0
  59. zktally-0.1.0/tests/vectors/nizk-binary/valid-v1.json +25 -0
  60. zktally-0.1.0/tests/vectors/nizk-oneofk/k2-choice0.json +19 -0
  61. zktally-0.1.0/tests/vectors/nizk-oneofk/k3-choice0.json +20 -0
  62. zktally-0.1.0/tests/vectors/paillier/aggregate.json +22 -0
  63. zktally-0.1.0/tests/vectors/paillier/ciphertext-validity.json +27 -0
  64. zktally-0.1.0/tests/vectors/paillier/encrypt-decrypt.json +47 -0
@@ -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).
@@ -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
+ ]