The Crypto E-Voting API implements a textbook RSA blind signature scheme adapted for electronic voting. The protocol ensures that the administrator can certify a ballot’s authenticity without ever seeing its content, that ballots are encrypted before reaching the anonymizer, and that every ballot can be verified at counting time using two independent checks. All cryptographic primitives live inDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/Crypto-Project-ENSTA/back-end/llms.txt
Use this file to discover all available pages before exploring further.
app/utils/crypto.py.
Overview
RSA blind signatures give you three guarantees simultaneously:- Authenticity — only the administrator (private-key holder) can produce a valid signature, so the counter can reject any ballot that was not legitimately issued.
- Secrecy — the administrator signs a blinded value and never sees the original ballot, so it cannot link signatures to voters.
- Verifiability — anyone with the administrator’s public key can confirm that
s^e ≡ m (mod N), and the commissioner can confirm that the N2 fingerprint was legitimately generated.
RSA blind signatures were first described by David Chaum in 1982 and are the classical building block for privacy-preserving voting and digital cash schemes.
Credential generation
Before voting begins the system generates two independent nonces for each voter: N1 and N2. Both nonces are produced bygenerate_nonce, which draws from a 36-character alphabet (A–Z and 0–9) using Python’s secrets.choice — a cryptographically secure random source:
| Nonce | Stored as | Purpose |
|---|---|---|
| N1 | Plaintext in credentials.n1 | One-time submission token; consumed on ballot submission |
| N2 | SHA-256 hash in credentials.hash_n2 | Ballot fingerprint; verified at counting time |
hash_n2:
Storing only the SHA-256 hash of N2 means the database never holds the plaintext receipt. Even if the
credentials table were leaked, an attacker could not reconstruct any voter’s N2 value to forge a ballot.Ballot creation
A ballot is represented byVoterBallotDTO, which packages the vote choice, the voter’s N2 nonce, and a fresh random component:
create_ballot populates random_bits with a URL-safe token to ensure uniqueness across ballots even when the vote choice and N2 are identical:
to_int() serialises the ballot string — e.g. (CandidateA,AF15GH258ZQP,abc123...) — to bytes using UTF-8, then interprets those bytes as a big-endian integer. This integer m is the value that travels through the RSA operations.
The resulting integer must satisfy
m < N (the RSA modulus) for the arithmetic to hold. mask_ballot raises a ValueError if this constraint is violated.Blind signature protocol
The blind signature protocol runs in four sub-steps.VotingSystemService.get_blind_signed_ballot orchestrates all of them.
Step 1 — Mask the ballot (voter side)
mask_ballot blinds the integer ballot m using the administrator’s RSA public key (e, N):
k is chosen uniformly at random from [2, N-1] subject to gcd(k, N) = 1. The masked value m' is computationally indistinguishable from a random element of ℤ_N, so the administrator learns nothing about m.
Formula: m' = m · k^e mod N
Step 2 — Administrator signs the masked ballot
sign_masked_ballot applies the administrator’s RSA private exponent d to the masked ballot:
s' = (m')^d mod N
The administrator computes s' without knowing k or m. It returns only s'.
Step 3 — Unmask the signature (voter side)
unmask_signed_ballot removes the blinding factor by multiplying s' by the modular inverse of k:
s = s' · k⁻¹ mod N
The unblinding works because of the homomorphic property of RSA:
s' = (m')^d = (m · k^e)^d = m^d · k^(ed) = m^d · k (mod N)Multiplying by k⁻¹ cancels the blinding factor, leaving s = m^d mod N — a standard RSA signature on the original message.Ballot encryption
After unblinding, the voter encrypts the signed ballots with the counter’s RSA public key (e_c, N_c) before submission:
c = s^e_c mod N_c
The anonymizer receives and stores c. Only the counter — the holder of the matching private key — can decrypt it.
Encrypting the signed ballot before submission ensures that the anonymizer cannot inspect ballot content. The anonymizer’s only role is to verify N1 and forward the opaque ciphertext to the
votes table.Verification at counting time
CounterService.process_all_votes runs two independent checks on every ballot after decryption.
Phase 1 — RSA decryption
s = c^d_c mod N_c
Check 1 — Administrator signature verification
verify_signature re-applies the administrator’s public exponent to the decrypted value and attempts to decode the result as a UTF-8 ballot string:
m = s^e_admin mod N_admin
If the recovered string cannot be split into exactly three comma-separated parts (vote, n2, random_bits) the ballot is marked INVALID_SIGNATURE.
Check 2 — N2 fingerprint verification
The extractedn2 value is hashed and looked up in the commissioner’s credential store:
credentials table is marked INVALID_N2. This rejects any ballot carrying a fabricated or reused N2.