Metadata-Version: 2.5
Name: zktally
Version: 0.1.0
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.
Project-URL: Homepage, https://zktally.github.io
Project-URL: Repository, https://github.com/ZKTally/ZKTally
Author: Muhammad Faiz Atharrahman
License: MIT License
        
        Copyright (c) 2025 Muhammad Faiz A <m@faiz.at>
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: cryptography,e-voting,paillier,ring-signature,zero-knowledge
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: fast
Requires-Dist: gmpy2>=2.1; extra == 'fast'
Description-Content-Type: text/markdown

# ZKTally

Privacy-preserving, anonymously verifiable e-voting from linkable ring signatures, Paillier
homomorphic encryption, and NIZK vote-correctness proofs.

> **ZKTally is unaudited academic software implementing a protocol from a student research
> paper. It has not been reviewed by professional cryptographers and must not be used for any
> binding election.**

A ballot is the triple `(ciphertexts, proofs, ring signature)`:

- **Paillier encryption** hides the vote. Tallying multiplies ciphertexts and decrypts only the
  aggregate, so no individual ballot is ever opened.
- **A NIZK OR-proof** shows the plaintext is a legal vote without revealing which one.
- **A linkable ring signature** proves the caster is *some* registered voter, and its
  deterministic key image makes a second ballot from the same key detectable.

Wire format: `zktally/1`.

---

## Install

```bash
pip install zktally                 # no runtime dependencies
pip install "zktally[fast]"         # optional gmpy2 acceleration
```

Python 3.11+. The pure-stdlib path works everywhere; `gmpy2` is roughly 5x faster on Paillier
and must produce byte-identical results.

## Library

```python
from zktally import *

pk, sk = paillier_keygen(2048)

# Voters generate their own keys. Nothing else may generate them.
voters = [ring_keygen() for _ in range(5)]
ring = canonical_ring([v.public for v in voters])

params = ElectionParams(
    election_id=new_election_id(),
    title="Adopt the proposal?",
    ballot_type="binary", k=1, candidates=("Yes",),
    public_key=pk, trust_model="single-authority", ring=ring,
)

board = Board(params=params)
for choice, voter in zip([1, 0, 1, 1, 0], voters):
    board.append(cast_ballot(choice, params, voter, ring.index(voter.public)))

print(tally(board, sk).totals)      # (3,)
```

Multi-candidate elections use `ballot_type="one-of-k"` with `k >= 2`, which proves each column
binary *and* proves the columns sum to exactly one.

## Command line

```bash
zktally keygen   --bits 3072 [--threshold 3-of-5] --out keys/
zktally election new --title "…" --type binary|one-of-k --candidates a,b,c \
                     --key keys/public.json --out params.json
zktally voterkey --out voter.json                      # voter-side only
zktally register --params params.json --add voter*.json
zktally vote     --params params.json --key voter.json --choice 1 --out ballot.json
zktally board    --params params.json --add ballot*.json --out board.json
zktally verify   --board board.json [--explain]
zktally tally    --board board.json --shares s1.json,s2.json --out result.json
zktally audit    --board board.json --result result.json
```

`zktally audit` takes **no secret material**. It re-verifies every ballot and every partial
decryption from public data alone, which is what makes public verifiability checkable rather
than asserted.

---

## Security properties

✅ achieved · ⚠️ conditional · ❌ not provided

| Property | | Conditions |
|---|---|---|
| Ballot secrecy | ⚠️ | Against anyone below quorum. Not against `t` colluding authorities, nor against a dishonest dealer during v1 setup |
| Voter anonymity | ⚠️ | Anonymity set is the ring, or the sub-ring when partitioned. Defeated by traffic analysis |
| Double-vote prevention | ✅ | Key image bound into every challenge; first-wins resolution |
| Ballot-stuffing resistance | ✅ | Ring fixed by the election parameters, never read from a ballot |
| Vote well-formedness | ✅ | Binary / 1-of-k NIZK plus mandatory ciphertext-domain validation |
| Public verifiability of ballots | ✅ | Anyone re-verifies every proof and signature from public data |
| Public verifiability of the tally | ✅ | Threshold mode only; partial decryptions carry correctness proofs |
| Non-malleability | ✅ | Election context bound into every transcript; proofs bound into the signed message |
| Eligibility | ⚠️ | Cryptographically enforced *given* an honest registrar |
| **Receipt-freeness** | ❌ | The voter holds the encryption randomness, which reconstructs their ciphertext. Structural, not a bug |
| **Coercion resistance** | ❌ | Not implemented. Needs fake credentials or an untappable channel |
| Everlasting privacy | ❌ | Ciphertexts are public forever; a future break reveals every vote |
| Side-channel resistance | ❌ | Not constant-time, and pure-Python `int` arithmetic cannot be made so |
| Post-quantum security | ❌ | Paillier and secp256k1 both fall to Shor |

Under `trust_model="single-authority"` the key holder can decrypt any individual ballot and the
published total carries no proof. `zktally audit` reports that decryption as **not proved**
rather than verified.

**v1 uses a trusted dealer** for threshold setup: that party transiently knows the factors, so
ballot privacy rests on them destroying that material rather than on the threshold itself.

## Development

```bash
uv venv && uv pip install -e ".[dev]"
pytest                              # full suite
pytest -m "not slow"                # skip real-parameter keygen
ruff check . && ruff format --check .
mypy
zktally vectors generate --seed 00…00 --out tests/vectors/
```

Test suites: `unit/`, `property/` (Hypothesis), `conformance/` (the shared vector corpus, run
under both numeric backends), and `tamper/` — one test per known attack, each asserting the
exact rejection reason.

The corpus in `tests/vectors/` is generated here and mirrored into the TypeScript port, which
is validated against it. It has exactly one source; hand-editing the mirror is a CI failure.

## Relationship to the paper

This implementation is normative where it and the originating paper disagree. The paper states
the second OR-proof branch as a discrete-log relation that does not match its own prover, omits
`R_i` and the ring from the ring-signature challenge, leaves the curve, hash, hash-to-curve and
point encoding unpinned, and claims receipt-freeness that the construction does not provide.

## License

MIT. See [LICENSE](LICENSE).
