SC-17-E2E-Verifiable-Voting-Python - Vote Electronique Verifiable

Navigation : Sommaire | << Précédent | Suivant >>


Objectifs d’apprentissage

  1. Comprendre le paradoxe du vote electronique (anonymat + verifiabilite)
  2. Construire un système de vote a la main avec Paillier + ZKP
  3. Implementer un bulletin board publiquement verifiable
  4. Decouvrir ElectionGuard (Microsoft), le SOTA en vote E2E verifiable
  5. Relier ces techniques aux smart contracts

Prerequis

  • SC-15 et SC-16 completes
  • phe (python-paillier) installe
  • pycryptodome installe
  • 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 = True
except ImportError:
    PHE_AVAILABLE = False
    print("phe non installe. Installez avec: pip install phe")
import hashlib
import json

if 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 electeurs
import random
random.seed(42)  # reproductibilite

if 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 rien
        print(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")
CHIFFREMENT DES BULLETINS
============================================================
  Electeur 1 : bulletin chiffre (3 ciphertexts de ~616 digits)
  Electeur 2 : bulletin chiffre (3 ciphertexts de ~616 digits)
  Electeur 3 : bulletin chiffre (3 ciphertexts de ~616 digits)
  Electeur 4 : bulletin chiffre (3 ciphertexts de ~617 digits)
  Electeur 5 : bulletin chiffre (3 ciphertexts de ~617 digits)
  Electeur 6 : bulletin chiffre (3 ciphertexts de ~616 digits)
  Electeur 7 : bulletin chiffre (3 ciphertexts de ~615 digits)

-> 7 bulletins chiffres, aucun vote individuel visible

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 bulletins
    for ballot in encrypted_ballots:
        for i in range(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 in zip(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"]] += 1
    assert 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 plt
    import io
    from 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 in enumerate(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 detection
    return {"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 hashlib

def 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 preuves
print("PREUVES DE VALIDITE DES BULLETINS")
print("=" * 60)

all_valid = True
proofs = []
for i, (voter, ballot) in enumerate(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]}...)")
    if not valid:
        all_valid = False

print(f"\n-> Tous les bulletins valides : {all_valid}")

# Demonstration : un bulletin invalide serait detecte
print("\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 board
if PHE_AVAILABLE:
    bulletin_board = []
    for i, (voter, ballot, proof) in enumerate(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 present
if 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 = False
    for entry in bulletin_board:
        if entry["ballot_hash"] == my_hash:
            found = True
            break

    print(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 verification
    return {"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 CiphertextElectionContext
    print("ElectionGuard installe et disponible")
    ELECTIONGUARD_AVAILABLE = True
except ImportError:
    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 = False
except Exception as 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 conceptuelle
    print("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 chiffre

try:
    from phe import paillier as _paillier_check
    PHE_AVAILABLE_EX = True
except ImportError:
    PHE_AVAILABLE_EX = False

import hashlib

if 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 _ in range(self.n_candidates)]
            for rank, candidate_idx in enumerate(ranking):
                matrix[candidate_idx][rank] = 1
            self.encrypted_matrix = [[public_key.encrypt(matrix[i][j])
                                      for j in range(self.n_candidates)]
                                     for i in range(self.n_candidates)]

    # --- Validation ---
    ranking = [2, 0, 1]  # Charlie 1er, Alice 2eme, Bob 3eme
    ballot = RankedBallot(pk, ranking)

    # Verifier la matrice dechiffree
    print("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 matrice
    print("Matrice de permutation dechiffree :")
    print("        Rang1  Rang2  Rang3")
    for i in range(ballot.n_candidates):
        row = [sk.decrypt(ballot.encrypted_matrix[i][j]) for j in range(ballot.n_candidates)]
        print(f"  {candidates_ex[i]:8s}  {row[0]}      {row[1]}      {row[2]}")

    # Verifier les contraintes de permutation
    valid = True
    for i in range(ballot.n_candidates):
        row_sum = sum(sk.decrypt(ballot.encrypted_matrix[i][j]) for j in range(ballot.n_candidates))
        if row_sum != 1:
            valid = False
    for j in range(ballot.n_candidates):
        col_sum = sum(sk.decrypt(ballot.encrypted_matrix[i][j]) for i in range(ballot.n_candidates))
        if col_sum != 1:
            valid = False

    print(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_key
        self.private_key = private_key
        self.n_candidates = n_candidates

    def 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_choices
        return [None] * self.n_candidates


print("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

Notebook suivant : SC-18-Vyper-Python - Smart contracts en Python-like

Conclusion et perspectives

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.


Navigation : Sommaire | << Précédent | Suivant >>

Retour au sommaire

Retour au sommet