electionguard installe (optionnel, pour la partie SOTA)
Duree estimee : 70 minutes
1. Le problème du vote electronique
Le vote electronique doit satisfaire des proprietes contradictoires en apparence :
Propriete
Description
Tension
Anonymat
Personne ne sait ce que j’ai vote
vs Verifiabilite
Verifiabilite individuelle
Je peux verifier que mon vote a ete compte
vs Anonymat
Verifiabilite universelle
N’importe qui peut verifier le decompte
vs Anonymat
Eligibilite
Seuls les electeurs autorises votent
vs Anonymat
Unicite
Un electeur = un vote
vs Anonymat
Resistance a la coercition
Impossible de prouver pour qui on a vote
vs Verifiabilite
La solution : le chiffrement homomorphique permet de compter les votes sans les dechiffrer individuellement, et les preuves a divulgation nulle (ZKP) permettent de verifier qu’un bulletin est valide sans reveler son contenu.
Historique
Annee
Système
Contribution
2008
Helios (Ben Adida)
Premier système E2E verifiable grand public
2012+
Belenios (INRIA)
Preuves formelles en Coq, plus fort garanties
2019
ElectionGuard (Microsoft)
SDK Python production-ready, threshold decryption
2. Chiffrement des bulletins (Paillier)
Chaque electeur chiffre son vote avec Paillier (additively homomorphic). Un vote pour le candidat i est represente comme un vecteur one-hot : - Candidat A : [1, 0, 0] - Candidat B : [0, 1, 0] - Candidat C : [0, 0, 1]
Chaque composante est chiffree individuellement.
try:from phe import paillier PHE_AVAILABLE =TrueexceptImportError: PHE_AVAILABLE =Falseprint("phe non installe. Installez avec: pip install phe")import hashlibimport jsonif PHE_AVAILABLE:# Autorite electorale : genere les cles public_key, private_key = paillier.generate_paillier_keypair(n_length=1024)print("AUTORITE ELECTORALE")print("="*60)print(f"Cle publique (n) : {str(public_key.n)[:40]}...")print(f"Cle publique distribuee a tous les electeurs")print(f"Cle privee conservee par l'autorite (ou repartie via threshold)")print()# Candidats candidates = ["Alice Dupont", "Bob Martin", "Charlie Durand"]print(f"Candidats : {candidates}")print()else:print("Section sautee : phe non disponible")
AUTORITE ELECTORALE
============================================================
Cle publique (n) : 1110929763904247198491966478154407896244...
Cle publique distribuee a tous les electeurs
Cle privee conservee par l'autorite (ou repartie via threshold)
Candidats : ['Alice Dupont', 'Bob Martin', 'Charlie Durand']
Interpretation : Generation des cles
L’autorite electorale genere une paire de cles Paillier :
Élément
Rôle
Distribution
Cle publique (n)
Chiffrer les bulletins
Distribuee a tous les electeurs
Cle privee
Dechiffrer les résultats agreges
Conservee par l’autorite (ou k-of-n gardiens)
Representation des votes : chaque vote est un vecteur one-hot (un 1 et des 0). Par exemple avec 3 candidats, voter pour Alice = [1, 0, 0]. Cette representation permet l’addition homomorphique composante par composante pour obtenir le decompte sans dechiffrer les bulletins individuels.
Point cle : en production, la cle privee est repartie via Shamir secret sharing entre N gardiens (threshold k-of-n), eliminant le point de defaillance unique.
Simulation du processus de vote pour 7 électeurs avec chiffrement homomorphique de chaque bulletin.
# Simulation de 7 electeursimport randomrandom.seed(42) # reproductibiliteif PHE_AVAILABLE: voters = [ {"name": "Electeur 1", "choice": 0}, # vote Alice {"name": "Electeur 2", "choice": 1}, # vote Bob {"name": "Electeur 3", "choice": 0}, # vote Alice {"name": "Electeur 4", "choice": 2}, # vote Charlie {"name": "Electeur 5", "choice": 0}, # vote Alice {"name": "Electeur 6", "choice": 1}, # vote Bob {"name": "Electeur 7", "choice": 2}, # vote Charlie ]# Chaque electeur chiffre son bulletin encrypted_ballots = []print("CHIFFREMENT DES BULLETINS")print("="*60)for voter in voters:# Creer le vecteur one-hot plaintext = [0] *len(candidates) plaintext[voter["choice"]] =1# Chiffrer chaque composante encrypted = [public_key.encrypt(v) for v in plaintext] encrypted_ballots.append(encrypted)# Le bulletin chiffre ne revele rienprint(f" {voter['name']} : bulletin chiffre (3 ciphertexts de ~{len(str(encrypted[0].ciphertext()))} digits)")print(f"\n-> {len(encrypted_ballots)} bulletins chiffres, aucun vote individuel visible")else:print("Section sautee : phe non disponible")
Interpretation : securite semantique des bulletins
Chaque bulletin chiffre est un vecteur de 3 ciphertexts Paillier. Même l’autorite ne peut pas deviner le contenu d’un bulletin individuel sans utiliser la cle privee. Le chiffrement est semantiquement securise : deux chiffrements du même message sont différents (grace a l’aleatoire).
3. Decompte homomorphique
Grace a la propriete additive de Paillier, on peut additionner les bulletins chiffres sans les dechiffrer. Seul le total est ensuite dechiffre.
# Addition homomorphique des bulletins# (SOTA Prong-A #3801 : la barre ASCII devisualisation est remplacee par un vrai# graphique matplotlib ; le decompte homomorphique Paillier, lui, est inchange.)if PHE_AVAILABLE:print("DECOMPTE HOMOMORPHIQUE")print("="*60)# Initialiser les totaux chiffres a 0 tally_encrypted = [public_key.encrypt(0) for _ in candidates]# Additionner tous les bulletinsfor ballot in encrypted_ballots:for i inrange(len(candidates)): tally_encrypted[i] = tally_encrypted[i] + ballot[i]print("Totaux chiffres calcules (sans dechiffrer aucun bulletin individuel)")print()# Seul le total est dechiffre par l'autorite results = [private_key.decrypt(t) for t in tally_encrypted]print("RESULTATS :")print("-"*40)for candidate, votes inzip(candidates, results):print(f" {candidate:20s} : {votes} votes")print(f" {'Total':20s} : {sum(results)} votes")print()# Verification manuelle expected = [0] *len(candidates)for v in voters: expected[v["choice"]] +=1assert results == expected, "Le decompte homomorphique doit correspondre au decompte manuel"print("-> Verification : le decompte homomorphique correspond au decompte en clair")# Visualisation : repartition des voix dechiffrees (matplotlib, remplace l'ASCII).# Rendu via savefig + IPython.display.Image (backend Agg headless : plt.show()# n'affiche rien en sortie et emet un avertissement ; savefig garantit un PNG).import matplotlib matplotlib.use("Agg")import matplotlib.pyplot as pltimport iofrom IPython.display import Image, display fig, ax = plt.subplots(figsize=(7, 3)) palette = ["#2980b9", "#27ae60", "#c0392b", "#8e44ad", "#d35400", "#16a085"] ax.barh(range(len(candidates)), results, color=palette[:len(candidates)]) ax.set_yticks(range(len(candidates))) ax.set_yticklabels(candidates) ax.invert_yaxis() ax.set_xlabel("Nombre de voix") ax.set_title("Decompte homomorphique Paillier - resultats dechiffres") ax.set_xlim(0, max(results) +1)for i, v inenumerate(results): ax.text(v +0.05, i, str(v), va="center", fontweight="bold") plt.tight_layout() buf = io.BytesIO() fig.savefig(buf, format="png", dpi=110, bbox_inches="tight") buf.seek(0) display(Image(data=buf.getvalue())) plt.close(fig)else:print("Section sautee : phe non disponible")
DECOMPTE HOMOMORPHIQUE
============================================================
Totaux chiffres calcules (sans dechiffrer aucun bulletin individuel)
RESULTATS :
----------------------------------------
Alice Dupont : 3 votes
Bob Martin : 2 votes
Charlie Durand : 2 votes
Total : 7 votes
-> Verification : le decompte homomorphique correspond au decompte en clair
Interpretation : decompte sur donnees chiffrees
Le decompte s’est fait entierement sur des données chiffrees : - Aucun bulletin individuel n’a ete dechiffre - Seul le résultat agrege a ete revele - L’anonymat de chaque electeur est preserve
Mais comment garantir que les bulletins sont valides (exactement un 1 et des 0) ? C’est la que les ZKP interviennent.
3b. Exercice : Simulation d’une attaque par bourrage d’urne
Objectif
Implementez une fonction detect_stuffing qui detecte une tentative de bourrage d’urne (ballot stuffing) en verifiant que le nombre de bulletins correspond au nombre d’electeurs enregistres et qu’aucun electeur n’a vote deux fois.
Contexte
Le bulletin board est public mais comment garantir qu’un electeur malveillant n’a pas soumis plusieurs bulletins ? La detection repose sur les voter_id (ou les ballot_hash uniques).
Indice :
Verifiez que le nombre de bulletins ne depasse pas le nombre d’electeurs autorises
Utilisez un set() pour verifier que les voter_id sont tous uniques
La fonction retourne un dictionnaire avec les résultats de chaque verification
def detect_stuffing(voters_list, bulletin_board_entries):"""Detecter une tentative de bourrage d'urne.TODO etudiant : implementez les verifications suivantes : 1. Le nombre de bulletins <= nombre d'electeurs enregistres 2. Les voter_id dans le bulletin board sont tous uniques (pas de doublon) 3. Chaque voter_id correspond a un electeur enregistre Args: voters_list: liste des electeurs autorises (dict avec 'name') bulletin_board_entries: liste d'entrees du bulletin board Returns: dict avec les cles 'count_ok', 'no_duplicates', 'all_registered', 'clean' """# TODO etudiant : implementez la detectionreturn {"count_ok": None, "no_duplicates": None, "all_registered": None, "clean": False}print("Exercice a completer : implementez detect_stuffing")
Exercice a completer : implementez detect_stuffing
4. Preuve de validite du bulletin (ZKP simplifiee)
Chaque electeur doit prouver que son bulletin chiffre contient un vote valide (exactement un 1 parmi les composantes) sans reveler pour qui il a vote.
Nous implementons ici une version simplifiee basee sur la somme des composantes. Un bulletin valide a une somme de composantes = 1 (un seul candidat choisi).
import hashlibdef compute_ballot_hash(encrypted_ballot):"""Calculer un identifiant unique pour un bulletin chiffre.""" data ="|".join(str(e.ciphertext()) for e in encrypted_ballot)return hashlib.sha256(data.encode()).hexdigest()def prove_valid_ballot(public_key, encrypted_ballot, plaintext_vote, num_candidates):"""Generer une preuve simplifiee que le bulletin est valide. Preuve que sum(composantes) = 1 en utilisant la propriete homomorphique. Le verifieur peut additionner les ciphertexts et verifier que le resultat dechiffre a 1, MAIS sans connaitre la cle privee. On utilise donc un commitment scheme. """# La preuve contient :# 1. La somme chiffree des composantes sum_encrypted = encrypted_ballot[0]for e in encrypted_ballot[1:]: sum_encrypted = sum_encrypted + e# 2. Un hash du bulletin pour identification ballot_hash = compute_ballot_hash(encrypted_ballot)return {"ballot_hash": ballot_hash,"sum_encrypted": sum_encrypted,"num_candidates": num_candidates, }def verify_ballot_proof(private_key, proof):"""Verifier la preuve (par l'autorite). Note: dans un vrai systeme, la verification se fait sans cle privee via des preuves Sigma / Chaum-Pedersen. Ici on simplifie. """ decrypted_sum = private_key.decrypt(proof["sum_encrypted"])return decrypted_sum ==1# Generer et verifier les preuvesprint("PREUVES DE VALIDITE DES BULLETINS")print("="*60)all_valid =Trueproofs = []for i, (voter, ballot) inenumerate(zip(voters, encrypted_ballots)): proof = prove_valid_ballot(public_key, ballot, voter["choice"], len(candidates)) valid = verify_ballot_proof(private_key, proof) proofs.append(proof) status ="VALIDE"if valid else"INVALIDE"print(f" {voter['name']} : {status} (sum=1, ballot_hash={proof['ballot_hash'][:16]}...)")ifnot valid: all_valid =Falseprint(f"\n-> Tous les bulletins valides : {all_valid}")# Demonstration : un bulletin invalide serait detecteprint("\nDEMONSTRATION : bulletin invalide")fake_ballot = [public_key.encrypt(1), public_key.encrypt(1), public_key.encrypt(0)] # 2 votes !fake_proof = prove_valid_ballot(public_key, fake_ballot, 0, len(candidates))fake_valid = verify_ballot_proof(private_key, fake_proof)print(f" Bulletin avec 2 votes : {'VALIDE'if fake_valid else'REJETE'}")print(" -> Un electeur ne peut pas voter deux fois dans un bulletin")
PREUVES DE VALIDITE DES BULLETINS
============================================================
Electeur 1 : VALIDE (sum=1, ballot_hash=32513bb5118f1b3e...)
Electeur 2 : VALIDE (sum=1, ballot_hash=50dd6fa0a31daf29...)
Electeur 3 : VALIDE (sum=1, ballot_hash=4cacb82627dfab1f...)
Electeur 4 : VALIDE (sum=1, ballot_hash=e2429a131b828e4d...)
Electeur 5 : VALIDE (sum=1, ballot_hash=bbb57163e2646ea6...)
Electeur 6 : VALIDE (sum=1, ballot_hash=b383bf20c6c6643b...)
Electeur 7 : VALIDE (sum=1, ballot_hash=e701911bd4382482...)
-> Tous les bulletins valides : True
DEMONSTRATION : bulletin invalide
Bulletin avec 2 votes : REJETE
-> Un electeur ne peut pas voter deux fois dans un bulletin
Interpretation : ZKP simplifiee vs preuves non-interactives
Dans notre version simplifiee, l’autorite verifie les preuves avec sa cle privee. Dans un vrai système (Helios, ElectionGuard), les preuves sont des ZKP non-interactives (Chaum-Pedersen / Sigma protocols) verifiables par n’importe qui sans cle privee.
C’est la différence entre notre demo pedagogique et un système production-ready.
5. Bulletin board public
Le bulletin board est un registre public ou tous les bulletins chiffres et leurs preuves sont publies. N’importe qui peut : - Verifier que son bulletin est present - Verifier les preuves de validite de tous les bulletins - Recompter le total homomorphique
# Construction du bulletin boardif PHE_AVAILABLE: bulletin_board = []for i, (voter, ballot, proof) inenumerate(zip(voters, encrypted_ballots, proofs)): entry = {"voter_id": hashlib.sha256(voter["name"].encode()).hexdigest()[:16],"ballot_hash": proof["ballot_hash"],"proof_valid": verify_ballot_proof(private_key, proof),"encrypted_ballot": ballot, # les ciphertexts } bulletin_board.append(entry)print("BULLETIN BOARD PUBLIC")print("="*60)print(" Voter ID | Ballot Hash | Proof")print("-"*60)for entry in bulletin_board: status ="OK"if entry["proof_valid"] else"FAIL" vid = entry["voter_id"] bh = entry["ballot_hash"][:16]print(f" {vid:16s} | {bh}... | {status}")print(f"\nTotal : {len(bulletin_board)} bulletins publies")else:print("Section sautee : phe non disponible")
BULLETIN BOARD PUBLIC
============================================================
Voter ID | Ballot Hash | Proof
------------------------------------------------------------
1a54b754d2b221c9 | 32513bb5118f1b3e... | OK
33306133a66fb4ce | 50dd6fa0a31daf29... | OK
d0b6d105a02f9bbe | 4cacb82627dfab1f... | OK
cf36812b5d407363 | e2429a131b828e4d... | OK
acf16ea3fd4efc79 | bbb57163e2646ea6... | OK
085e2a76a924550d | b383bf20c6c6643b... | OK
515b232255e4d399 | e701911bd4382482... | OK
Total : 7 bulletins publies
Interpretation : Bulletin board
Le bulletin board est le registre public de l’election :
Information publiee
Forme
Verifiable par
Voter ID
SHA-256 du nom (pseudonymise)
L’electeur peut s’identifier
Ballot hash
SHA-256 des ciphertexts
L’electeur retrouve son bulletin
Preuve de validite
Verification sum=1
Quiconque (si ZKP non-interactive)
Ciphertexts
3 valeurs Paillier par bulletin
Quiconque peut recompter
Point cle : le bulletin board est append-only (on ajoute, on ne retire jamais). Sur une blockchain, c’est garanti par l’immutabilite de la chaîne. Sur un serveur web, c’est garanti par des signatures numériques et un log public.
Vérification individuelle permettant à chaque électeur de confirmer que son bulletin est bien enregistré sur le tableau d’affichage public.
# Verification individuelle : un electeur verifie que son bulletin est presentif PHE_AVAILABLE:print("VERIFICATION INDIVIDUELLE")print("="*60)# L'electeur 3 verifie my_voter = voters[2] my_ballot = encrypted_ballots[2] my_hash = compute_ballot_hash(my_ballot)# Chercher dans le bulletin board found =Falsefor entry in bulletin_board:if entry["ballot_hash"] == my_hash: found =Truebreakprint(f"Electeur : {my_voter['name']}")print(f"Mon ballot hash : {my_hash[:32]}...")print(f"Present dans le bulletin board : {'OUI'if found else'NON'}")print()print("-> L'electeur peut verifier que son vote a bien ete enregistre")print("-> MAIS il ne peut pas prouver a un tiers pour qui il a vote")print(" (le hash ne revele pas le contenu, et le chiffrement est randomise)")else:print("Section sautee : phe non disponible")
VERIFICATION INDIVIDUELLE
============================================================
Electeur : Electeur 3
Mon ballot hash : 4cacb82627dfab1f47ac5af546258cf4...
Present dans le bulletin board : OUI
-> L'electeur peut verifier que son vote a bien ete enregistre
-> MAIS il ne peut pas prouver a un tiers pour qui il a vote
(le hash ne revele pas le contenu, et le chiffrement est randomise)
Interpretation : les proprietes du bulletin board
Le bulletin board fournit la transparence :
Propriete
Mécanisme
Verifiabilite individuelle
L’electeur conserve le hash de son bulletin
Verifiabilite universelle
Quiconque peut reverifier le decompte homomorphique
Anonymat
Le hash ne revele pas le vote, le chiffrement est randomise
Resistance a la coercition
L’electeur ne peut pas prouver son vote a un tiers
Ce modèle est exactement celui utilise par Helios (2008) et ses successeurs.
5b. Exercice : Verification universelle du bulletin board
Objectif
Implementez la fonction universal_verification qui permet a n’importe quel observateur de verifier l’integrite complete du bulletin board sans connaitre la cle privee.
La verification doit confirmer que : 1. Tous les bulletins ont des preuves valides 2. Le decompte homomorphique correspond aux totaux attendus 3. Aucun bulletin n’est duplique (hash uniques)
Indice :
Pour verifier la preuve de chaque bulletin sans cle privee (dans un vrai système), on utiliserait des preuves ZKP non-interactives. Ici, vous simulerez la verification en verifiant que chaque ballot_hash est unique et en recomptant les sommes chiffrees. Utilisez un set() pour detecter les doublons et iterez sur le bulletin_board.
def universal_verification(bulletin_board, expected_total_votes):"""Verifier l'integrite complete du bulletin board publiquement.TODO etudiant : implementez les 3 verifications : 1. Tous les ballot_hash sont uniques (pas de doublons) 2. Toutes les preuves de validite sont OK 3. Le nombre total de bulletins correspond au nombre attendu Args: bulletin_board: liste d'entrees du bulletin board expected_total_votes: nombre attendu de bulletins Returns: dict avec les cles 'duplicates_ok', 'proofs_ok', 'count_ok', 'all_valid' """# TODO etudiant : implementez la verificationreturn {"duplicates_ok": None, "proofs_ok": None, "count_ok": None, "all_valid": False}print("Exercice a completer : implementez universal_verification")
Exercice a completer : implementez universal_verification
6. ElectionGuard - Le SOTA
ElectionGuard (Microsoft, 2019) est le système de vote E2E verifiable le plus avance, disponible en open-source avec un SDK Python.
Différences avec notre construction a la main
Aspect
Notre demo
ElectionGuard
Chiffrement
Paillier (additif)
ElGamal exponentiel (additif + ZKP)
ZKP bulletins
Verification avec cle privee
Chaum-Pedersen non-interactive
Dechiffrement
Cle privee unique
Threshold (k-of-n gardiens)
Bulletin board
Liste Python
Artefacts JSON standardises
Audit
Manuel
Outils de verification automatises
Architecture ElectionGuard
1. Configuration : Manifest (candidats, règles) + Gardiens (threshold keys)
2. Chiffrement : Chaque bulletin chiffre avec ElGamal + ZKP Chaum-Pedersen
3. Publication : Bulletin board avec tous les ciphertexts + preuves
4. Decompte : Addition homomorphique des ciphertexts
5. Dechiffrement : k-of-n gardiens revelent le total (threshold decryption)
6. Verification : Quiconque peut reverifier avec les artefacts publics
Threshold Decryption
La cle privee est repartie entre N gardiens (ex: 5 commissaires electoraux). Il faut au moins k gardiens (ex: 3) pour dechiffrer le résultat. Aucun gardien seul ne peut dechiffrer quoi que ce soit.
# ElectionGuard SDK (optionnel - necessite pip install electionguard)try:from electionguard.election import CiphertextElectionContextprint("ElectionGuard installe et disponible") ELECTIONGUARD_AVAILABLE =TrueexceptImportError:print("ElectionGuard non installe.")print("Pour l'installer : pip install electionguard")print()print("Note : ElectionGuard necessite pydantic<2 (conflit avec web3>=7)")print("Installer dans un venv separe si necessaire.") ELECTIONGUARD_AVAILABLE =FalseexceptExceptionas e:# ElectionGuard a des bugs internes avec Python 3.12+ (mutable dataclass defaults)print(f"ElectionGuard installe mais incompatible avec ce Python : {type(e).__name__}")print(f" {e}")print()print("La section suivante montre le code ElectionGuard a titre d'illustration.") ELECTIONGUARD_AVAILABLE =False
ElectionGuard non installe.
Pour l'installer : pip install electionguard
Note : ElectionGuard necessite pydantic<2 (conflit avec web3>=7)
Installer dans un venv separe si necessaire.
Démonstration illustrative du SDK ElectionGuard pour le vote vérifié de bout en bout.
# Code ElectionGuard illustratif# (s'execute uniquement si la library est installee)if ELECTIONGUARD_AVAILABLE:# Cet exemple simplifie est base sur la documentation officielle# https://www.electionguard.vote/print("Configuration d'une election ElectionGuard...")print("(voir la documentation officielle pour un exemple complet)")else:# Simuler la structure conceptuelleprint("SIMULATION CONCEPTUELLE ElectionGuard")print("="*60)print()print("1. MANIFEST (definition de l'election) :") manifest = {"election_scope_id": "election-municipale-2026","type": "general","candidates": ["Alice Dupont", "Bob Martin", "Charlie Durand"],"contests": [{"id": "maire", "type": "plurality", "max_selections": 1}],"guardians": {"count": 5, "quorum": 3}, }for k, v in manifest.items():print(f" {k}: {v}")print()print("2. CEREMONY DES GARDIENS (generation des cles threshold) :")print(" 5 gardiens generent chacun une paire de cles")print(" Ils partagent des fragments de cle (Shamir secret sharing)")print(" La cle publique jointe est publiee")print(" -> Quorum de 3/5 necessaire pour dechiffrer")print()print("3. CHIFFREMENT DES BULLETINS :")print(" Chaque bulletin chiffre avec ElGamal exponentiel")print(" + preuve Chaum-Pedersen de validite (0 ou 1 par candidat)")print(" + preuve que la somme des selections = 1")print()print("4. DECOMPTE :")print(" Addition homomorphique des ciphertexts ElGamal")print(" Dechiffrement par 3+ gardiens (threshold)")print(" Publication des preuves de dechiffrement correct")print()print("5. VERIFICATION E2E :")print(" N'importe qui telecharge les artefacts et verifie :")print(" - Toutes les preuves de bulletin sont valides")print(" - Le decompte correspond a la somme des ciphertexts")print(" - Le dechiffrement est correct (preuve DLOG)")
SIMULATION CONCEPTUELLE ElectionGuard
============================================================
1. MANIFEST (definition de l'election) :
election_scope_id: election-municipale-2026
type: general
candidates: ['Alice Dupont', 'Bob Martin', 'Charlie Durand']
contests: [{'id': 'maire', 'type': 'plurality', 'max_selections': 1}]
guardians: {'count': 5, 'quorum': 3}
2. CEREMONY DES GARDIENS (generation des cles threshold) :
5 gardiens generent chacun une paire de cles
Ils partagent des fragments de cle (Shamir secret sharing)
La cle publique jointe est publiee
-> Quorum de 3/5 necessaire pour dechiffrer
3. CHIFFREMENT DES BULLETINS :
Chaque bulletin chiffre avec ElGamal exponentiel
+ preuve Chaum-Pedersen de validite (0 ou 1 par candidat)
+ preuve que la somme des selections = 1
4. DECOMPTE :
Addition homomorphique des ciphertexts ElGamal
Dechiffrement par 3+ gardiens (threshold)
Publication des preuves de dechiffrement correct
5. VERIFICATION E2E :
N'importe qui telecharge les artefacts et verifie :
- Toutes les preuves de bulletin sont valides
- Le decompte correspond a la somme des ciphertexts
- Le dechiffrement est correct (preuve DLOG)
Interpretation : Architecture ElectionGuard
Étape
Mécanisme
Securite
Configuration
Manifest JSON + gardiens (Shamir secret sharing)
Quorum 3/5, aucun gardien seul ne peut agir
Chiffrement
ElGamal exponentiel + Chaum-Pedersen
Preuve non-interactive de validite du bulletin
Decompte
Addition homomorphique des ciphertexts
Votes individuels jamais dechiffres
Dechiffrement
Threshold (k-of-n gardiens)
Coordination de 3 gardiens requise
Verification
Artefacts publics telechargeables
N’importe qui peut recompter independamment
Points cles : - Le threshold decryption elimine le point de defaillance unique : même si 2 gardiens sur 5 sont compromis, le système reste sur - ElGamal exponentiel (vs Paillier) est choisi pour sa compatibilite avec les preuves Chaum-Pedersen sur courbes elliptiques - Les artefacts de verification sont des fichiers JSON standardises, auditables par des outils tiers
Comparaison historique
Critere
Helios (2008)
Belenios (2012+)
ElectionGuard (2019)
Chiffrement
ElGamal
ElGamal
ElGamal
ZKP
Sigma protocols
Sigma protocols
Chaum-Pedersen
Preuves formelles
Non
Oui (Coq)
Non (mais audite)
Threshold
Non (trustee unique)
Oui
Oui (k-of-n)
SDK
JavaScript
OCaml
Python
Usage reel
Elections universitaires
Elections CNRS
Pilotes US
Open source
Oui
Oui
Oui (Microsoft)
ElectionGuard est le plus accessible grace a son SDK Python et sa documentation complete.
7. Exemple guide : Vote preferentiel verifiable
Solution proposee par Clovis Lefebvre, Evariste Balvay.
Principe du vote preferentiel
Au lieu de voter pour un seul candidat, chaque electeur classe les candidats par ordre de préférence. Si aucun candidat n’a la majorite absolue au premier tour, le candidat avec le moins de votes de premier choix est elimine, et ses votes sont redistribues selon les seconds choix, etc.
Representation cryptographique
Le bulletin preferentiel est encode comme une matrice de permutation N x N ou entry[i][j] = 1 si le candidat i est classe en position j. Chaque ligne a exactement un 1 (chaque candidat a un rang) et chaque colonne aussi (chaque rang est attribue a un seul candidat).
La classe RankedBallot ci-dessous construit cette matrice et chiffre chaque élément avec Paillier.
# Exemple guide : Vote preferentiel chiffretry:from phe import paillier as _paillier_check PHE_AVAILABLE_EX =TrueexceptImportError: PHE_AVAILABLE_EX =Falseimport hashlibif PHE_AVAILABLE_EX:from phe import paillier# Configuration candidates_ex = ["Alice", "Bob", "Charlie"] pk, sk = paillier.generate_paillier_keypair(n_length=1024)class RankedBallot:"""Bulletin de vote preferentiel chiffre. Representation : matrice N x N ou entry[i][j] = 1 si le candidat i est classe en position j (0-indexed), 0 sinon. Chaque ligne a exactement un 1, chaque colonne aussi (permutation). """def__init__(self, public_key, ranking):"""Creer un bulletin chiffre a partir d'un classement. Args: public_key: cle publique Paillier ranking: liste d'indices de candidats par preference ex: [2, 0, 1] = Charlie 1er, Alice 2eme, Bob 3eme """self.n_candidates =len(ranking)# Construire la matrice de permutation matrix = [[0] *self.n_candidates for _ inrange(self.n_candidates)]for rank, candidate_idx inenumerate(ranking): matrix[candidate_idx][rank] =1self.encrypted_matrix = [[public_key.encrypt(matrix[i][j])for j inrange(self.n_candidates)]for i inrange(self.n_candidates)]# --- Validation --- ranking = [2, 0, 1] # Charlie 1er, Alice 2eme, Bob 3eme ballot = RankedBallot(pk, ranking)# Verifier la matrice dechiffreeprint("VALIDATION DU BULLETIN PREFERENTIEL")print("="*60)print(f"Classement : {ranking}")print(f" = {candidates_ex[ranking[0]]} 1er, {candidates_ex[ranking[1]]} 2e, {candidates_ex[ranking[2]]} 3e")print()# Dechiffrer et afficher la matriceprint("Matrice de permutation dechiffree :")print(" Rang1 Rang2 Rang3")for i inrange(ballot.n_candidates): row = [sk.decrypt(ballot.encrypted_matrix[i][j]) for j inrange(ballot.n_candidates)]print(f" {candidates_ex[i]:8s}{row[0]}{row[1]}{row[2]}")# Verifier les contraintes de permutation valid =Truefor i inrange(ballot.n_candidates): row_sum =sum(sk.decrypt(ballot.encrypted_matrix[i][j]) for j inrange(ballot.n_candidates))if row_sum !=1: valid =Falsefor j inrange(ballot.n_candidates): col_sum =sum(sk.decrypt(ballot.encrypted_matrix[i][j]) for i inrange(ballot.n_candidates))if col_sum !=1: valid =Falseprint(f"\nMatrice de permutation valide (sommes lignes/colonnes = 1) : {valid}")else:print("Exemple guide non executable (phe non installe)")
VALIDATION DU BULLETIN PREFERENTIEL
============================================================
Classement : [2, 0, 1]
= Charlie 1er, Alice 2e, Bob 3e
Matrice de permutation dechiffree :
Rang1 Rang2 Rang3
Alice 0 1 0
Bob 0 0 1
Charlie 1 0 0
Matrice de permutation valide (sommes lignes/colonnes = 1) : True
Interpretation de l’exemple guide
Élément
Description
Matrice N x N
Represente le classement complet de chaque candidat
Ligne i
Position du candidat i (exactement un 1 par ligne)
Colonne j
Quel candidat est en position j (exactement un 1 par colonne)
Chiffrement Paillier
Chaque élément est chiffre independamment
Validation : la matrice dechiffree verifie les contraintes de permutation (somme de chaque ligne et chaque colonne = 1). En production, cette verification se fait via des preuves ZKP (Chaum-Pedersen) sans dechiffrer.
8. Exercice : Decompte homomorphique preferentiel
Objectif
Implementer une méthode de decompte homomorphique pour un ensemble de bulletins preferentiels chiffres. Le decompte doit extraire le premier choix de chaque bulletin en utilisant uniquement les opérations homomorphiques.
A implementer
Completez la méthode count_first_choices de la classe RankedTally ci-dessous.
Indice : la première colonne de la matrice chiffree contient les premiers choix. Utilisez l’addition homomorphique pour sommer les ciphertexts de la première colonne.
class RankedTally:"""Decompte homomorphique de bulletins preferentiels."""def__init__(self, public_key, private_key, n_candidates):self.public_key = public_keyself.private_key = private_keyself.n_candidates = n_candidatesdef count_first_choices(self, ballots):"""Extraire le decompte des premiers choix via addition homomorphique.TODO etudiant : pour chaque candidat i, sommer homomorphiquement la premiere colonne (rang 0) de chaque bulletin. Retourner une liste decompte [votes_candidat_0, votes_candidat_1, ...]. Args: ballots: liste d'objets RankedBallot Returns: liste de comptes dechiffres """# TODO etudiant : implementez count_first_choicesreturn [None] *self.n_candidatesprint("Exercice a completer : implementez count_first_choices dans RankedTally")
Exercice a completer : implementez count_first_choices dans RankedTally
9. Resume : les composants de la chaine de confiance
Composant
Technique
Rôle
Anonymat
Chiffrement Paillier / ElGamal
Personne ne voit les votes individuels
Decompte
Addition homomorphique
Compter sans dechiffrer
Validite
Preuves ZKP (Chaum-Pedersen)
Verifier qu’un bulletin est bien forme
Transparence
Bulletin board public
Tout le monde peut verifier
Resilience
Threshold decryption (k-of-n)
Aucun gardien seul ne peut dechiffrer
Verification
Artefacts publics
N’importe qui peut recompter
Vote preferentiel
Matrice de permutation chiffree
Classement complet sans reveler les choix
Points cles
Le vote E2E verifiable resout le paradoxe anonymat/verifiabilite grace a la cryptographie
Le chiffrement homomorphique permet le decompte sur des données chiffrees
Les ZKP garantissent la validite des bulletins sans reveler leur contenu
ElectionGuard (Microsoft, 2019) est le SOTA avec un SDK Python
L’integration blockchain (bulletin board sur Ethereum) ajoute l’immutabilite
Le vote preferentiel etend le schema one-hot en matrice de permutation chiffree
Ce notebook a montre comment construire un système de vote electronique verifiable de bout en bout en combinant le chiffrement homomorphique (Paillier) et les preuves a divulgation nulle. Nous avons chiffre les bulletins sous forme de vecteurs one-hot, effectue le decompte par addition homomorphique sans jamais dechiffrer les votes individuels, genere des preuves de validite des bulletins (somme = 1), et implemente un bulletin board publiquement verifiable. La comparaison avec ElectionGuard (Microsoft) a permis de mesurer l’ecart entre notre construction pedagogique et les systèmes production-ready, notamment le chiffrement ElGamal exponentiel, les preuves Chaum-Pedersen non-interactives et le dechiffrement a seuil (k-of-n gardiens).
Le paradoxe du vote electronique – concilier anonymat et verifiabilite – trouve sa resolution dans une architecture ou les bulletins sont chiffres (anonymat), le decompte est homomorphique (confidentialite des votes individuels), les preuves ZKP garantissent la validite (integrite), et le bulletin board public assure la transparence (verifiabilite universelle). Le dechiffrement a seuil elimine le point de defaillance unique : aucun gardien seul ne peut trahir le secret du vote. L’integration blockchain, en tant que bulletin board immutable, renforce encore cette garantie structurelle.
Le prochain notebook change de domaine pour explorer les chaînes alternatives a Ethereum, en commencant par Vyper, un langage de smart contracts a la syntaxe Python-like et a la philosophie securitaire : SC-18-Vyper-Python.