SC-16-Homomorphic-Encryption-Python - Chiffrement Homomorphique

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


Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Comprendre le chiffrement homomorphique (PHE, SHE, FHE) et ses variantes 2. Implementer le schema de Paillier (additively homomorphic) avec phe 3. Explorer le schema CKKS pour l’arithmetique approchee avec TenSEAL 4. Decouvrir le calcul multipartite securise (MPC) et le partage de secrets de Shamir

Prerequis

  • Python 3.10+
  • phe (python-paillier) : pip install phe
  • tenseal (optionnel) : pip install tenseal
  • mpyc (optionnel) : pip install mpyc

Duree estimee : 50 minutes


1. Concepts du chiffrement homomorphique

Le chiffrement homomorphique (HE) permet d’effectuer des calculs directement sur des données chiffrees, sans les dechiffrer. Le résultat chiffre, une fois dechiffre, correspond exactement au résultat qu’on aurait obtenu en calculant sur les données en clair.

Formalisation

Soit E la fonction de chiffrement et D la fonction de dechiffrement :

\[D(E(a) \oplus E(b)) = a + b\]

ou \(\oplus\) designe l’opération homomorphique dans l’espace chiffre.

Les trois niveaux de chiffrement homomorphique

Type Sigle Opérations Profondeur Exemples
Partially Homomorphic PHE Addition ou multiplication, pas les deux Illimitee (1 opération) RSA (mult.), Paillier (add.), ElGamal (mult.)
Somewhat Homomorphic SHE Addition et multiplication Limitee (bruit croissant) BGV, BFV
Fully Homomorphic FHE Toutes opérations, profondeur arbitraire Illimitee (bootstrapping) TFHE, CKKS (approche), Concrete

Applications concretes

  • Vote electronique : le serveur additionne les votes chiffres sans voir les bulletins individuels
  • ML confidentiel : entrainer ou inferer un modèle sur des données chiffrees (sante, finance)
  • Cloud computing : deleguer un calcul a un serveur non-fiable sans reveler les données
  • Genomique : analyser des sequences ADN chiffrees pour le diagnostic medical

Historique

Annee Avancee Auteur
1978 RSA : multiplication homomorphique Rivest, Shamir, Adleman
1999 Paillier : addition homomorphique Pascal Paillier
2009 Premier schema FHE (bootstrapping) Craig Gentry
2016 CKKS : FHE pour nombres reels Cheon, Kim, Kim, Song
2020+ Concrete (Zama) : FHE compile depuis Python Zama.ai
# Vue d'ensemble des schemas de chiffrement homomorphique
schemas = {
    "RSA (1978)": {
        "type": "PHE",
        "operation": "Multiplication",
        "principe": "E(a) * E(b) = E(a * b) mod n",
        "usage": "Signatures, echange de cles",
    },
    "Paillier (1999)": {
        "type": "PHE",
        "operation": "Addition",
        "principe": "E(a) * E(b) = E(a + b) mod n^2",
        "usage": "Vote, statistiques agregatrices",
    },
    "BGV/BFV (2011-12)": {
        "type": "SHE/FHE",
        "operation": "Addition + Multiplication (entiers)",
        "principe": "Lattice-based, bruit gere par modular switching",
        "usage": "Calculs exacts sur entiers",
    },
    "CKKS (2016)": {
        "type": "FHE (approche)",
        "operation": "Addition + Multiplication (reels)",
        "principe": "Encodage dans l'erreur, calcul approche",
        "usage": "ML confidentiel, statistiques",
    },
    "TFHE (2016)": {
        "type": "FHE",
        "operation": "Bootstrapping rapide (portes logiques)",
        "principe": "LWE + bootstrapping par porte",
        "usage": "Circuits arbitraires, Concrete (Zama)",
    },
}

print("SCHEMAS DE CHIFFREMENT HOMOMORPHIQUE")
print("=" * 70)
for name, info in schemas.items():
    print(f"\n{name} [{info['type']}]")
    print(f"  Operation : {info['operation']}")
    print(f"  Principe  : {info['principe']}")
    print(f"  Usage     : {info['usage']}")
SCHEMAS DE CHIFFREMENT HOMOMORPHIQUE
======================================================================

RSA (1978) [PHE]
  Operation : Multiplication
  Principe  : E(a) * E(b) = E(a * b) mod n
  Usage     : Signatures, echange de cles

Paillier (1999) [PHE]
  Operation : Addition
  Principe  : E(a) * E(b) = E(a + b) mod n^2
  Usage     : Vote, statistiques agregatrices

BGV/BFV (2011-12) [SHE/FHE]
  Operation : Addition + Multiplication (entiers)
  Principe  : Lattice-based, bruit gere par modular switching
  Usage     : Calculs exacts sur entiers

CKKS (2016) [FHE (approche)]
  Operation : Addition + Multiplication (reels)
  Principe  : Encodage dans l'erreur, calcul approche
  Usage     : ML confidentiel, statistiques

TFHE (2016) [FHE]
  Operation : Bootstrapping rapide (portes logiques)
  Principe  : LWE + bootstrapping par porte
  Usage     : Circuits arbitraires, Concrete (Zama)

Observation

Le point cle est que chaque schema fait un compromis entre : - La richesse des opérations supportees (PHE < SHE < FHE) - La performance (PHE est rapide, FHE est 10 000x plus lent que le calcul en clair) - La precision (CKKS est approche, BGV/BFV sont exacts)

Dans la suite, nous implementons Paillier (PHE) car c’est le plus simple et le plus pertinent pour le vote electronique (section 6).


2. Chiffrement de Paillier (PHE additif)

Le schema de Paillier (1999) est un chiffrement a cle publique avec la propriete :

\[E(m_1) \cdot E(m_2) \mod n^2 = E(m_1 + m_2)\]

Autrement dit, multiplier deux chiffres dans l’espace chiffre revient a additionner les messages en clair. On peut aussi multiplier un chiffre par un scalaire en clair :

\[E(m)^k \mod n^2 = E(k \cdot m)\]

Construction simplifiee

  1. Generation : choisir deux grands premiers \(p, q\). Poser \(n = p \cdot q\)
  2. Chiffrement : \(c = g^m \cdot r^n \mod n^2\) (r aleatoire)
  3. Dechiffrement : utiliser \(\lambda = \text{lcm}(p-1, q-1)\) pour retrouver \(m\)

La bibliotheque phe (python-paillier) implemente tout cela.

# Installation si necessaire
# !pip install phe

try:
    from phe import paillier
    PHE_AVAILABLE = True
except ImportError:
    PHE_AVAILABLE = False
    print("phe non installe. Installez avec: pip install phe")
import time

if PHE_AVAILABLE:
    # Generation des cles
    print("GENERATION DES CLES PAILLIER")
    print("=" * 70)
    start = time.time()
    public_key, private_key = paillier.generate_paillier_keypair(n_length=2048)
    elapsed = time.time() - start
    print(f"Taille de n : {public_key.n.bit_length()} bits")
    print(f"Temps de generation : {elapsed:.3f}s")
    print(f"n (tronque) : {str(public_key.n)[:40]}...")
else:
    print("Section sautee : phe non disponible")
GENERATION DES CLES PAILLIER
======================================================================
Taille de n : 2048 bits
Temps de generation : 0.921s
n (tronque) : 1858592909614198343297965307755121307830...

Interpretation : generation des cles Paillier

Résultat obtenu : La paire de cles est generee en moins d’une seconde a une seconde selon la machine (ordre de grandeur stable ; la valeur exacte, dependante de la machine, figure dans la sortie de la cellule de generation ci-dessus) avec un module n de 2048 bits, conforme au standard de securite recommande.

Paramètre Valeur Rappel
Taille de n 2048 bits Equivalent RSA-2048 en securite
Temps de generation ~1 s Principalement la recherche de grands premiers p et q
Structure n = p * q Deux nombres premiers de ~1024 bits chacun

Points cles : - La securite de Paillier repose sur la difficulte de factoriser n en p * q (même hypothese que RSA) - En production, on utiliserait 3072 ou 4096 bits pour une securite long terme, mais 2048 bits sont suffisants pour l’apprentissage - La cle publique (n) est distribuee aux chiffreurs, la cle privee (lambda, mu) est gardee secrete par le dechiffreur uniquement

Chiffrement et déchiffrement d’un nombre entier avec le schéma de Paillier pour valider la configuration des clés.

# Chiffrement et dechiffrement de base
if PHE_AVAILABLE:
    print("CHIFFREMENT / DECHIFFREMENT")
    print("=" * 70)

    # Chiffrer des entiers
    valeurs = [42, 100, -7, 0, 999999]
    for v in valeurs:
        chiffre = public_key.encrypt(v)
        dechiffre = private_key.decrypt(chiffre)
        # Le chiffre est un grand nombre, on en montre juste un extrait
        c_str = str(chiffre.ciphertext())[:30]
        print(f"  {v:>8} -> E({v}) = {c_str}... -> D = {dechiffre}")

    print()
    print("Propriete importante : chaque chiffrement est ALEATOIRE (semantiquement sur)")
    a1 = public_key.encrypt(42)
    a2 = public_key.encrypt(42)
    print(f"  E(42) premiere fois  : {str(a1.ciphertext())[:30]}...")
    print(f"  E(42) deuxieme fois  : {str(a2.ciphertext())[:30]}...")
    print(f"  Chiffres identiques ? {a1.ciphertext() == a2.ciphertext()} (toujours False)")
else:
    print("Section sautee : phe non disponible")
CHIFFREMENT / DECHIFFREMENT
======================================================================
        42 -> E(42) = 312479216220966292169015141111... -> D = 42
       100 -> E(100) = 141434405099764789916161517865... -> D = 100
        -7 -> E(-7) = 115787257932430644198681925251... -> D = -7
         0 -> E(0) = 130696743830160027356962651471... -> D = 0
    999999 -> E(999999) = 235252847592009943860335141447... -> D = 999999

Propriete importante : chaque chiffrement est ALEATOIRE (semantiquement sur)
  E(42) premiere fois  : 168908703865622398175663698870...
  E(42) deuxieme fois  : 220666031233693415569453001330...
  Chiffres identiques ? False (toujours False)

Interpretation : chiffrement probabiliste de Paillier

Résultat obtenu : Le chiffrement et le dechiffrement fonctionnent correctement pour les entiers positifs, negatifs et zero. Deux chiffrements du même message (42) produisent des textes chiffres différents.

Propriete Observation Importance
Exactitude D(E(v)) = v pour tout v Fiabilite du schema
Supporte les negatifs E(-7) se dechiffre en -7 Calculs sur les différences
Chiffrement probabiliste E(42) != E(42) Securite sémantique (IND-CPA)
Taille du chiffre ~617 chiffres decimaux Overhead de stockage

Points cles : - La propriete de chiffrement probabiliste est essentielle : sans elle, un attaquant pourrait identifier des patterns dans les données chiffrees en comparant les textes chiffres. En Paillier, le paramètre aleatoire r garantit que chaque chiffrement est unique - L’overhead de taille est considerable : un entier de quelques chiffres devient un nombre de 617 chiffres. C’est le prix a payer pour la securite et l’homomorphisme

Démonstration de la propriété homomorphique additive : addition de valeurs chiffrées sans déchiffrement intermédiaire.

# Propriete homomorphique : addition sur les chiffres
if PHE_AVAILABLE:
    print("ADDITION HOMOMORPHIQUE")
    print("=" * 70)

    a = 42
    b = 58
    c = 100

    # Chiffrer
    ea = public_key.encrypt(a)
    eb = public_key.encrypt(b)
    ec = public_key.encrypt(c)

    # Addition dans l'espace chiffre
    e_sum_ab = ea + eb            # E(42) + E(58) = E(100)
    e_sum_abc = ea + eb + ec      # E(42) + E(58) + E(100) = E(200)

    # Dechiffrer les resultats
    sum_ab = private_key.decrypt(e_sum_ab)
    sum_abc = private_key.decrypt(e_sum_abc)

    print(f"  a = {a}, b = {b}, c = {c}")
    print(f"  D(E(a) + E(b))     = {sum_ab}  (attendu: {a + b})")
    print(f"  D(E(a) + E(b) + E(c)) = {sum_abc}  (attendu: {a + b + c})")
    print()

    # Multiplication par un scalaire en clair
    print("MULTIPLICATION PAR SCALAIRE")
    print("-" * 40)
    k = 7
    e_prod = ea * k              # E(42) * 7 = E(294)
    prod = private_key.decrypt(e_prod)
    print(f"  D(E({a}) * {k})      = {prod}  (attendu: {a * k})")
    print()

    # Combinaison : somme ponderee
    weights = [3, 5, 2]
    values = [10, 20, 30]
    encrypted_values = [public_key.encrypt(v) for v in values]
    encrypted_weighted = sum(ev * w for ev, w in zip(encrypted_values, weights))
    result = private_key.decrypt(encrypted_weighted)
    expected = sum(v * w for v, w in zip(values, weights))
    print(f"SOMME PONDEREE")
    print(f"  Valeurs  : {values}")
    print(f"  Poids    : {weights}")
    print(f"  D(sum(E(vi)*wi)) = {result}  (attendu: {expected})")
else:
    print("Section sautee : phe non disponible")
ADDITION HOMOMORPHIQUE
======================================================================
  a = 42, b = 58, c = 100
  D(E(a) + E(b))     = 100  (attendu: 100)
  D(E(a) + E(b) + E(c)) = 200  (attendu: 200)

MULTIPLICATION PAR SCALAIRE
----------------------------------------
  D(E(42) * 7)      = 294  (attendu: 294)

SOMME PONDEREE
  Valeurs  : [10, 20, 30]
  Poids    : [3, 5, 2]
  D(sum(E(vi)*wi)) = 190  (attendu: 190)

Interpretation : proprietes homomorphiques en pratique

Résultat obtenu : Les trois opérations homomorphiques produisent les résultats mathematiquement exacts attendus, confirmant la propriete additive de Paillier.

Opération Formule Résultat Attendu
Addition chiffree D(E(42) + E(58)) 100 100
Addition triple D(E(42) + E(58) + E(100)) 200 200
Multiplication scalaire D(E(42) * 7) 294 294
Somme ponderee D(sum(E(vi) * wi)) 190 190

Note technique : La somme ponderee illustre un cas d’usage concret en ML : le calcul d’une combinaison lineaire 3*10 + 5*20 + 2*30 = 190 est effectue entierement sur des données chiffrees. C’est la base du calcul de scores dans un modèle de regression ou d’un perceptron, sans jamais reveler les entrees individuelles.

Interpretation : proprietes de Paillier

Opération Formule Support
Addition chiffre + chiffre E(a) + E(b) = E(a+b) Oui
Multiplication chiffre x scalaire E(a) * k = E(a*k) Oui
Somme ponderee sum(E(vi) * wi) Oui
Multiplication chiffre x chiffre E(a) * E(b) = E(a*b) Non
Comparaison E(a) > E(b) ? Non

Paillier est additively homomorphic : il ne supporte que l’addition (et la multiplication par scalaire, qui est une addition repetee). C’est suffisant pour le vote, les moyennes, les sommes statistiques.

Exercice 2 : Calcul de moyenne privee avec Paillier

En utilisant les proprietes homomorphiques de Paillier, implementez une fonction qui calcule la moyenne de valeurs chiffrees sans les dechiffrer individuellement.

Objectif : Completer la fonction moyenne_privee qui : 1. Chiffre chaque valeur avec la cle publique 2. Additionne les chiffres avec la propriete homomorphique 3. Dechiffre uniquement la somme pour calculer la moyenne

Indice : - Utilisez public_key.encrypt(v) pour chiffrer chaque valeur - Additionnez les chiffres avec l’opérateur + (propriete homomorphique additive) - Dechiffrez uniquement le résultat final avec private_key.decrypt() - La moyenne = somme dechiffree / nombre de valeurs

Étapes : 1. Chiffrer chaque valeur de la liste 2. Calculer la somme homomorphique (addition des chiffres) 3. Dechiffrer uniquement la somme et diviser par le nombre de valeurs

# Exercice 2 : Moyenne privee avec Paillier
# TODO etudiant : implementez la fonction moyenne_privee


def moyenne_privee(public_key, private_key, valeurs):
    """Calculer la moyenne de valeurs sans les dechiffrer individuellement.

    Args:
        public_key: cle publique Paillier
        private_key: cle privee Paillier
        valeurs: liste d'entiers a moyenner

    Returns:
        float: la moyenne calculee
    """
    # TODO etudiant : chiffrer chaque valeur, additionner les chiffres,
    # dechiffrer la somme et calculer la moyenne
    # Indice : public_key.encrypt(v) chiffre une valeur
    # Les chiffres s'additionnent avec l'operateur +
    # private_key.decrypt(somme_chiffree) dechiffre uniquement le total
    return None  # TODO etudiant


# Validation
if PHE_AVAILABLE:
    test_values = [15, 20, 25, 30, 35]
    result = moyenne_privee(public_key, private_key, test_values)
    if result is not None:
        expected = sum(test_values) / len(test_values)
        print(f"Valeurs test         : {test_values}")
        print(f"Moyenne calculee     : {result:.2f}")
        print(f"Moyenne attendue     : {expected:.2f}")
        print(f"Correct              : {abs(result - expected) < 0.01}")
    else:
        print("Exercice a completer")
else:
    print("Exercice a completer (phe non installe)")
Exercice a completer

3. CKKS avec TenSEAL (FHE pour nombres reels)

Le schema CKKS (Cheon-Kim-Kim-Song, 2016) permet des opérations sur des nombres reels (virgule flottante) avec une approximation controlee. C’est le schema prefere pour le machine learning confidentiel.

TenSEAL est une bibliotheque Python qui expose CKKS (et BFV) avec une API compatible tenseurs.

Principe de CKKS

  1. Les nombres reels sont encodes dans des polynomes cyclotomiques
  2. Le bruit de chiffrement est inclus dans la precision (pas un defaut, une feature)
  3. Chaque opération multiplie le bruit ; le bootstrapping le reinitialise
  4. La precision est configurable (~40 bits pour des calculs ML classiques)
# TenSEAL : CKKS pour l'arithmetique approchee
# pip install tenseal

try:
    import tenseal as ts
    import numpy as np

    # Creer un contexte CKKS
    context = ts.context(
        ts.SCHEME_TYPE.CKKS,
        poly_modulus_degree=8192,
        coeff_mod_bit_sizes=[60, 40, 40, 60]
    )
    context.generate_galois_keys()
    context.global_scale = 2**40

    print("CKKS AVEC TENSEAL")
    print("=" * 70)
    print(f"Degree du polynome : {8192}")
    print(f"Scale (precision)  : 2^40")
    print()

    # Chiffrer des vecteurs
    v1 = [1.5, 2.3, 4.7, 8.1]
    v2 = [0.5, 1.7, 3.3, 1.9]

    enc_v1 = ts.ckks_vector(context, v1)
    enc_v2 = ts.ckks_vector(context, v2)

    print(f"v1 (clair)  : {v1}")
    print(f"v2 (clair)  : {v2}")
    print()

    # Addition chiffree
    enc_add = enc_v1 + enc_v2
    result_add = enc_add.decrypt()
    expected_add = [a + b for a, b in zip(v1, v2)]
    print("Addition chiffree :")
    print(f"  Resultat   : {[round(x, 6) for x in result_add]}")
    print(f"  Attendu    : {expected_add}")
    print(f"  Erreur max : {max(abs(r - e) for r, e in zip(result_add, expected_add)):.2e}")
    print()

    # Multiplication chiffree
    enc_mul = enc_v1 * enc_v2
    result_mul = enc_mul.decrypt()
    expected_mul = [a * b for a, b in zip(v1, v2)]
    print("Multiplication chiffree :")
    print(f"  Resultat   : {[round(x, 6) for x in result_mul]}")
    print(f"  Attendu    : {expected_mul}")
    print(f"  Erreur max : {max(abs(r - e) for r, e in zip(result_mul, expected_mul)):.2e}")
    print()

    # Produit scalaire chiffre (operation ML fondamentale)
    enc_dot = enc_v1.dot(enc_v2)
    result_dot = enc_dot.decrypt()
    expected_dot = sum(a * b for a, b in zip(v1, v2))
    print("Produit scalaire chiffre :")
    print(f"  Resultat   : {round(result_dot[0], 6)}")
    print(f"  Attendu    : {expected_dot}")
    print(f"  Erreur     : {abs(result_dot[0] - expected_dot):.2e}")

except ImportError:
    print("TenSEAL non installe.")
    print("Pour installer : pip install tenseal")
    print()
    print("TenSEAL permet de manipuler des vecteurs chiffres avec le schema CKKS.")
    print("Les operations supportees incluent addition, multiplication, et produit scalaire.")
    print("L'erreur d'approximation est typiquement de l'ordre de 1e-6 a 1e-4.")
CKKS AVEC TENSEAL
======================================================================
Degree du polynome : 8192
Scale (precision)  : 2^40

v1 (clair)  : [1.5, 2.3, 4.7, 8.1]
v2 (clair)  : [0.5, 1.7, 3.3, 1.9]

Addition chiffree :
  Resultat   : [2.0, 4.0, 8.0, 10.0]
  Attendu    : [2.0, 4.0, 8.0, 10.0]
  Erreur max : 3.00e-09

Multiplication chiffree :
  Resultat   : [0.75, 3.910001, 15.510002, 15.390002]
  Attendu    : [0.75, 3.9099999999999997, 15.51, 15.389999999999999]
  Erreur max : 2.09e-06

Produit scalaire chiffre :
  Resultat   : 35.560004
  Attendu    : 35.559999999999995
  Erreur     : 4.46e-06

Interpretation : opérations CKKS sur des vecteurs chiffres

Note d’exécution : TenSEAL est installe et la cellule précédente a execute reellement les opérations CKKS. Les valeurs rapportees sont donc mesurees : l’addition est quasi-exacte (erreur max ~3.0e-9), la multiplication atteint une erreur de l’ordre de 2.1e-6 et le produit scalaire de l’ordre de 4.5e-6 — coherent avec une global_scale = 2^40 dont l’erreur d’approximation CKKS est typiquement de 1e-6 a 1e-4, croissant avec la profondeur multiplicative.

Points cles : - L’erreur d’approximation provient de l’encodage CKKS qui injecte du bruit intentionnellement. La global_scale = 2^40 contrôle la precision : plus la scale est elevee, plus la precision est bonne, mais moins de multiplications sont possibles avant bootstrapping - Le produit scalaire chiffre est l’opération fondamentale pour le ML confidentiel : il permet de calculer des produits matriciels (bases des reseaux de neurones) sans jamais voir les données en clair - L’erreur croit avec la profondeur multiplicative : chaque multiplication double le bruit

Mesure des performances et de la profondeur multiplicative disponible avec le schéma CKKS sur des données réelles.

# Performance et profondeur multiplicative CKKS

try:
    import tenseal as ts
    import time

    context = ts.context(
        ts.SCHEME_TYPE.CKKS,
        poly_modulus_degree=8192,
        coeff_mod_bit_sizes=[60, 40, 40, 60]
    )
    context.global_scale = 2**40

    # Mesurer les temps d'operation
    v = [3.14] * 100
    enc_v = ts.ckks_vector(context, v)

    print("BENCHMARK CKKS (vecteur de 100 elements)")
    print("=" * 70)

    # Temps de chiffrement
    start = time.time()
    for _ in range(100):
        _ = ts.ckks_vector(context, v)
    t_encrypt = (time.time() - start) / 100
    print(f"  Chiffrement        : {t_encrypt*1000:.2f} ms")

    # Temps d'addition
    enc_v2 = ts.ckks_vector(context, v)
    start = time.time()
    for _ in range(100):
        _ = enc_v + enc_v2
    t_add = (time.time() - start) / 100
    print(f"  Addition chiffree  : {t_add*1000:.2f} ms")

    # Temps de multiplication
    start = time.time()
    for _ in range(100):
        _ = enc_v * enc_v2
    t_mul = (time.time() - start) / 100
    print(f"  Multiplication     : {t_mul*1000:.2f} ms")

    # Temps de dechiffrement
    start = time.time()
    for _ in range(100):
        _ = enc_v.decrypt()
    t_decrypt = (time.time() - start) / 100
    print(f"  Dechiffrement      : {t_decrypt*1000:.2f} ms")

    print()
    print("  Overhead vs calcul en clair : ~1000x-10000x")
    print("  -> Le HE est couteux, a utiliser quand la confidentialite l'exige")

except ImportError:
    print("TenSEAL non installe - benchmark ignore.")
    print()
    print("Ordres de grandeur typiques CKKS (vecteur 100 elements) :")
    print("  Chiffrement       : ~1-5 ms")
    print("  Addition chiffree : ~0.1 ms")
    print("  Multiplication    : ~1-5 ms")
    print("  Overhead total    : ~1000x-10000x vs calcul en clair")
BENCHMARK CKKS (vecteur de 100 elements)
======================================================================
  Chiffrement        : 3.34 ms
  Addition chiffree  : 0.04 ms
  Multiplication     : 2.27 ms
  Dechiffrement      : 0.92 ms

  Overhead vs calcul en clair : ~1000x-10000x
  -> Le HE est couteux, a utiliser quand la confidentialite l'exige

Interpretation : benchmark CKKS

Note d’exécution : TenSEAL est installe et le benchmark a tourne reellement sur un vecteur de 100 éléments. Les temps mesures sur ce run (ils varient selon la machine et la charge) :

Opération Temps mesuré CKKS Remarque
Chiffrement (ms live – regle #9434)
Addition chiffree (ms live) Très rapide (multiplication de polynomes)
Multiplication (ms live) Multiplication tensorielle + relinearisation
Dechiffrement (ms live)
Overhead total ~1000x-10000x vs calcul en clair

Points cles : - L’addition chiffree est remarquablement rapide (ms live – regle #9434) car elle ne fait que multiplier des polynomes modulo - La multiplication est plus couteuse car elle necessite une multiplication tensorielle suivie d’une opération de relinearisation - Le paramètre poly_modulus_degree=8192 offre un bon compromis entre securite et performance pour des vecteurs de taille moderee

Interpretation : CKKS vs Paillier

Critere Paillier (PHE) CKKS (FHE approche)
Opérations Addition seulement Addition + Multiplication
Types de données Entiers Nombres reels (approches)
Precision Exacte ~40 bits (configurable)
Performance Rapide (PHE) Plus lent (FHE)
Profondeur Illimitee (addition) Limitee par coeff_mod
Usage principal Vote, sommes ML, statistiques

CKKS est le choix natural pour le machine learning confidentiel car il supporte les multiplications necessaires aux reseaux de neurones (produits matriciels).


4. FHE avec Concrete (Zama)

Concrete de Zama est un framework qui permet de compiler du code Python en FHE : on ecrit une fonction Python normale, et Concrete la transforme en un circuit FHE executable sur des données chiffrees.

Principe

  1. Ecrire une fonction Python avec des opérations sur entiers
  2. Concrete trace la fonction et genere un circuit FHE
  3. Le circuit est compile et optimise pour le bootstrapping TFHE
  4. Exécution : les données sont chiffrees, le circuit calcule, le résultat est dechiffre

Limites actuelles

  • Opérations sur entiers seulement (pas de flottants)
  • Taille des entiers limitee (8, 16, 32 bits)
  • Temps de compilation long pour des circuits complexes
  • Performance ~10 000x plus lente que le calcul en clair
# Concrete : compiler Python en FHE
# pip install concrete-python

try:
    import warnings

    warnings.filterwarnings(
        "ignore",
        message="pkg_resources is deprecated as an API.*",
        category=UserWarning,
    )
    from concrete import fhe
    import numpy as np

    # Definir une fonction simple
    def addition_secrete(x, y):
        return x + y

    # Compiler en circuit FHE
    compiler = fhe.Compiler(
        addition_secrete,
        {"x": "encrypted", "y": "encrypted"}
    )

    # Donner des exemples pour la compilation
    inputset = [(np.uint8(i), np.uint8(j)) for i in range(10) for j in range(10)]
    circuit = compiler.compile(inputset)

    print("CONCRETE FHE - Addition compilee")
    print("=" * 70)
    print("Circuit compile avec succes")

    # Generer les cles
    circuit.keygen()

    # Executer sur des donnees chiffrees
    a, b = 7, 13
    result = circuit.encrypt_run_decrypt(a, b)
    print(f"  addition_secrete({a}, {b}) = {result}  (attendu: {a + b})")
    print()
    print(f"-> Le serveur a calcule {a} + {b} SANS voir les valeurs !")

except ImportError:
    print("Concrete non installe (pip install concrete-python).")
    print()
    print("Concrete permet de compiler une fonction Python en circuit FHE.")
    print("Exemple conceptuel :")
    print()
    print("  @fhe.compiler({'x': 'encrypted', 'y': 'encrypted'})")
    print("  def addition_secrete(x, y):")
    print("      return x + y")
    print()
    print("  # Compilation")
    print("  circuit = addition_secrete.compile(inputset)")
    print("  circuit.keygen()")
    print()
    print("  # Execution sur donnees chiffrees")
    print("  result = circuit.encrypt_run_decrypt(7, 13)  # -> 20")
    print("  # Le serveur n'a jamais vu 7 ni 13")

except Exception as e:
    print(f"Erreur lors de la compilation Concrete : {e}")
    print("Concrete necessite un environnement specifique (Linux recommande).")
Concrete non installe (pip install concrete-python).

Concrete permet de compiler une fonction Python en circuit FHE.
Exemple conceptuel :

  @fhe.compiler({'x': 'encrypted', 'y': 'encrypted'})
  def addition_secrete(x, y):
      return x + y

  # Compilation
  circuit = addition_secrete.compile(inputset)
  circuit.keygen()

  # Execution sur donnees chiffrees
  result = circuit.encrypt_run_decrypt(7, 13)  # -> 20
  # Le serveur n'a jamais vu 7 ni 13

Observation : etat de l’art FHE

Le FHE est un domaine en evolution rapide. Les performances doublent environ tous les 18 mois (analogue a la loi de Moore pour le HE).

Framework Schema Langage Specialite
Concrete (Zama) TFHE Python Compilation Python -> FHE
TenSEAL CKKS, BFV Python ML confidentiel
OpenFHE BGV, BFV, CKKS, TFHE C++ Reference academique
SEAL (Microsoft) BFV, CKKS C++ Cloud computing
HElib (IBM) BGV, CKKS C++ Historique

Le defi principal reste la performance : un circuit FHE typique est 10 000 a 100 000 fois plus lent que le calcul en clair.


5. Calcul multipartite securise (MPC) et partage de secrets

Le MPC (Multi-Party Computation) est une alternative au HE pour le calcul confidentiel. Au lieu de chiffrer les données et calculer sur le chiffre, on distribue les données entre plusieurs parties qui collaborent pour calculer un résultat sans reveler leurs entrees individuelles.

Partage de secrets de Shamir (1979)

Adi Shamir a invente un schema de partage de secrets a seuil : un secret \(s\) est divise en \(n\) parts, et il faut au moins \(k\) parts pour le reconstituer (schema \((k, n)\)).

Principe mathematique : un polynome de degré \(k-1\) est défini par \(k\) points.

  1. Choisir un polynome aleatoire \(P(x) = s + a_1 x + a_2 x^2 + ... + a_{k-1} x^{k-1}\) ou \(P(0) = s\) est le secret
  2. Distribuer les parts : part \(i\) = \(P(i)\) pour \(i = 1, ..., n\)
  3. Reconstruction : interpolation de Lagrange avec \(k\) parts quelconques
import random
from functools import reduce

# Arithmetique modulaire pour eviter les problemes de precision
# On travaille dans Z/pZ avec p premier
PRIME = 2**127 - 1  # 12e nombre premier de Mersenne


def shamir_split(secret, k, n, prime=PRIME):
    """Partager un secret en n parts avec un seuil de k.

    Args:
        secret: le secret (entier)
        k: nombre minimum de parts pour reconstruire
        n: nombre total de parts
        prime: module premier pour l'arithmetique

    Returns:
        Liste de n paires (x, y) representant les parts
    """
    if k > n:
        raise ValueError("k doit etre <= n")

    # Generer un polynome aleatoire de degre k-1 avec P(0) = secret
    coefficients = [secret % prime] + [
        random.randrange(1, prime) for _ in range(k - 1)
    ]

    # Evaluer le polynome en x = 1, 2, ..., n
    def evaluate(x):
        result = 0
        power = 1
        for coeff in coefficients:
            result = (result + coeff * power) % prime
            power = (power * x) % prime
        return result

    shares = [(i, evaluate(i)) for i in range(1, n + 1)]
    return shares


def shamir_reconstruct(shares, prime=PRIME):
    """Reconstruire le secret a partir de k parts (interpolation de Lagrange).

    Args:
        shares: liste de paires (x, y)
        prime: module premier

    Returns:
        Le secret reconstruit
    """
    k = len(shares)

    def mod_inverse(a, p):
        """Inverse modulaire via le petit theoreme de Fermat."""
        return pow(a, p - 2, p)

    secret = 0
    for j in range(k):
        xj, yj = shares[j]
        # Calcul du coefficient de Lagrange L_j(0)
        numerator = 1
        denominator = 1
        for m in range(k):
            if m != j:
                xm = shares[m][0]
                numerator = (numerator * (-xm)) % prime
                denominator = (denominator * (xj - xm)) % prime

        lagrange = (numerator * mod_inverse(denominator, prime)) % prime
        secret = (secret + yj * lagrange) % prime

    return secret


# Demonstration
print("PARTAGE DE SECRETS DE SHAMIR")
print("=" * 70)

secret = 42
k = 3  # seuil
n = 5  # nombre de parts

print(f"Secret : {secret}")
print(f"Schema : ({k}, {n}) - il faut {k} parts sur {n} pour reconstruire")
print()

shares = shamir_split(secret, k, n)
print("Parts generees :")
for i, (x, y) in enumerate(shares):
    print(f"  Part {i+1} (x={x}) : {str(y)[:30]}...")
print()
PARTAGE DE SECRETS DE SHAMIR
======================================================================
Secret : 42
Schema : (3, 5) - il faut 3 parts sur 5 pour reconstruire

Parts generees :
  Part 1 (x=1) : 393379530364962652587181384227...
  Part 2 (x=2) : 116841392876245947151268922888...
  Part 3 (x=3) : 623691360587798139459650496815...
  Part 4 (x=4) : 460623660445670973744938225177...
  Part 5 (x=5) : 679210828336077974368552413968...

Interpretation : generation des parts de Shamir

Résultat obtenu : Le secret 42 est divise en 5 parts numériques dans Z/pZ (p = 2^127 - 1). Chaque part est un point (x, y) sur un polynome aleatoire de degré 2.

Paramètre Valeur Signification
Secret 42 Valeur a proteger
Seuil k 3 Minimum de parts pour reconstruire
Total n 5 Nombre de parts distribuees
Module p 2^127 - 1 Premier de Mersenne pour l’arithmetique modulaire

Note technique : Le polynome sous-jacent est de la forme P(x) = 42 + a1*x + a2*x^2 ou a1 et a2 sont aleatoires. Le module premier (12e nombre de Mersenne) assure que l’arithmetique reste dans un corps fini, empechant toute inference sur les coefficients a partir de moins de k parts.

Reconstruction du secret à partir de différents sous-ensembles de parts pour vérifier la propriété de seuil du schéma de Shamir.

# Reconstruction avec differents sous-ensembles de parts
import itertools

print("RECONSTRUCTION AVEC DIFFERENTS SOUS-ENSEMBLES")
print("=" * 70)

# Avec exactement k=3 parts
print(f"\nAvec k={k} parts (minimum requis) :")
for combo in itertools.combinations(range(n), k):
    subset = [shares[i] for i in combo]
    recovered = shamir_reconstruct(subset)
    parts_used = [f"Part {i+1}" for i in combo]
    status = "OK" if recovered == secret else "ECHEC"
    print(f"  {parts_used} -> {recovered} [{status}]")

# Avec k-1 parts (insuffisant)
print(f"\nAvec k-1={k-1} parts (insuffisant) :")
for combo in itertools.combinations(range(n), k - 1):
    subset = [shares[i] for i in combo]
    recovered = shamir_reconstruct(subset)
    parts_used = [f"Part {i+1}" for i in combo]
    status = "OK" if recovered == secret else "INCORRECT"
    print(f"  {parts_used} -> {recovered} [{status}]")
    if combo == list(itertools.combinations(range(n), k - 1))[2]:
        print(f"  ... (toutes les combinaisons de {k-1} parts donnent un mauvais resultat)")
        break

print()
print(f"-> Avec {k} parts sur {n}, le secret est TOUJOURS reconstruit correctement")
print(f"-> Avec moins de {k} parts, le secret est INDEDUCTIBLE")
RECONSTRUCTION AVEC DIFFERENTS SOUS-ENSEMBLES
======================================================================

Avec k=3 parts (minimum requis) :
  ['Part 1', 'Part 2', 'Part 3'] -> 42 [OK]
  ['Part 1', 'Part 2', 'Part 4'] -> 42 [OK]
  ['Part 1', 'Part 2', 'Part 5'] -> 42 [OK]
  ['Part 1', 'Part 3', 'Part 4'] -> 42 [OK]
  ['Part 1', 'Part 3', 'Part 5'] -> 42 [OK]
  ['Part 1', 'Part 4', 'Part 5'] -> 42 [OK]
  ['Part 2', 'Part 3', 'Part 4'] -> 42 [OK]
  ['Part 2', 'Part 3', 'Part 5'] -> 42 [OK]
  ['Part 2', 'Part 4', 'Part 5'] -> 42 [OK]
  ['Part 3', 'Part 4', 'Part 5'] -> 42 [OK]

Avec k-1=2 parts (insuffisant) :
  ['Part 1', 'Part 2'] -> 131975696657215815097854657672802503391 [INCORRECT]
  ['Part 1', 'Part 3'] -> 112892953255589106780938334651261702202 [INCORRECT]
  ['Part 1', 'Part 4'] -> 93810209853962398464022011629720901013 [INCORRECT]
  ... (toutes les combinaisons de 2 parts donnent un mauvais resultat)

-> Avec 3 parts sur 5, le secret est TOUJOURS reconstruit correctement
-> Avec moins de 3 parts, le secret est INDEDUCTIBLE

Interpretation : verification du seuil de Shamir

Résultat obtenu : Les 10 combinaisons de 3 parts parmi 5 reconstruisent toutes le secret 42 (OK). Les combinaisons de 2 parts produisent des résultats incorrects et aleatoires.

Nombre de parts Combinaisons Résultat Statut
3 (k = seuil) C(5,3) = 10 42 pour toutes Correct
2 (k-1) C(5,2) = 10 Valeurs aleatoires Incorrect

Points cles : - La propriete de seuil est parfaite : avec exactement k parts, le secret est toujours reconstruit ; avec k-1, aucune information ne fuit - Mathematiquement, 2 points definissent une infinite de droites (polynomes de degré 1), tandis que 3 points definissent une unique parabole (polynome de degré 2) - En pratique, on utilise souvent un schema (3, 5) ou (4, 7) : suffisamment de redondance pour tolerer des pannes, sans trop de complexite

Exercice 3 : Partage de secret textuel avec Shamir

Le schema de Shamir vu ci-dessus fonctionne sur des entiers. Adaptez-le pour partager un message textuel en utilisant les codes ASCII.

Objectif : Completer les fonctions shamir_split_text et shamir_reconstruct_text qui partagent et reconstruisent un message texte caractère par caractère.

Indice : - Convertissez chaque caractère en ASCII avec ord(c) - Appliquez shamir_split sur chaque valeur ASCII individuellement - Regroupez les parts par participant (part i = la i-eme part de chaque caractère) - Pour la reconstruction, utilisez shamir_reconstruct puis chr() pour reconvertir

Étapes : 1. Decouper le message en valeurs ASCII (une par caractère) 2. Appliquer shamir_split sur chaque valeur, puis regrouper par participant 3. Pour la reconstruction, collecter les k parts de chaque position et utiliser shamir_reconstruct

# Exercice 3 : Partage de secret textuel avec Shamir
# TODO etudiant : implementez le partage d'un message texte


def shamir_split_text(message, k, n, prime=PRIME):
    """Partager un message texte via Shamir.

    Chaque caractere est partage separement avec shamir_split.

    Args:
        message: string a partager
        k: seuil de reconstruction
        n: nombre total de parts
        prime: module premier

    Returns:
        Liste de n parts, chaque part = liste de (x, y) par caractere
    """
    # TODO etudiant : convertir chaque caractere en ASCII avec ord(c)
    # puis appliquer shamir_split sur chaque valeur ASCII
    # Indice : pour chaque caractere, shamir_split(ord(c), k, n) retourne n parts
    # Il faut regrouper les parts par participant (part i = toutes les parts i de chaque caractere)
    return []  # TODO etudiant


def shamir_reconstruct_text(shares_per_party, prime=PRIME):
    """Reconstruire un message texte a partir de k ensembles de parts.

    Args:
        shares_per_party: liste de k parts (une par partie),
                          chaque part = liste de (x, y) par caractere

    Returns:
        str: le message reconstruit
    """
    # TODO etudiant : pour chaque position de caractere, collecter les k parts
    # correspondantes, utiliser shamir_reconstruct, puis convertir avec chr()
    return ""  # TODO etudiant


# Validation
msg = "SECRET"
k_test, n_test = 3, 5
parts = shamir_split_text(msg, k_test, n_test)
if parts:
    # Utiliser les 3 premieres parts pour reconstruire
    result = shamir_reconstruct_text(parts[:3])
    print(f"Message original : {msg}")
    print(f"Message reconstruit : {result}")
    print(f"Correct : {result == msg}")
else:
    print("Exercice a completer")
Exercice a completer

Interpretation : HE vs MPC

Critere Chiffrement Homomorphique MPC (Secret Sharing)
Modèle de confiance Un serveur non-fiable Plusieurs parties semi-honnetes
Communication Faible (une fois) Elevee (echanges multiples)
Performance Lent (calcul lourd) Rapide si peu de parties
Flexibilite Toutes opérations (FHE) Lineaire natif, non-lineaire couteux
Setup Cles HE (lourd) Distribution de parts (leger)

Quand utiliser quoi ? - HE : un seul serveur effectue le calcul (cloud, vote centralise) - MPC : plusieurs parties collaborent (encheres, statistiques inter-entreprises) - Hybride : combiner les deux pour le meilleur des deux mondes


6. Exemple guide : Système de sondage anonyme avec Paillier

Solution proposee par Mark Delaloy, Alexandre Bodin, Dylan De Araujo.

Cet exemple montre un système de sondage anonyme complet ou : 1. Chaque participant chiffre sa reponse (note de 1 a 10) avec la cle publique Paillier 2. Le serveur calcule la somme homomorphique des votes chiffres 3. Seul le résultat agrege est dechiffre (moyenne, somme) 4. Aucun vote individuel n’est revele

Transition : du chiffrement a l’application

Les sections précédentes ont couvert trois approches de calcul confidentiel : Paillier (PHE additif), CKKS (FHE approche) et le partage de secrets de Shamir (MPC). L’exemple guide ci-dessous combine le schema Paillier avec un cas d’usage concret de sondage anonyme, ou la somme homomorphe est la seule opération necessaire.

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


if PHE_AVAILABLE_EX:
    from phe import paillier

    class SondageAnonyme:
        """Systeme de sondage anonyme utilisant le chiffrement de Paillier.

        Le serveur peut calculer la somme et la moyenne des reponses
        sans jamais voir les reponses individuelles.
        """

        def __init__(self):
            """Generer les cles Paillier.
            La cle publique est distribuee aux participants.
            La cle privee est detenue par l'autorite de depouillement.
            """
            self.public_key, self.private_key = paillier.generate_paillier_keypair()
            self.reponses_chiffrees = []

        def ajouter_reponse(self, note):
            if not isinstance(note, int):
                raise TypeError("La note doit etre un entier")
            if not 1 <= note <= 10:
                raise ValueError("La note doit etre comprise entre 1 et 10")

            reponse_chiffree = self.public_key.encrypt(note)
            self.reponses_chiffrees.append(reponse_chiffree)
            return reponse_chiffree

        def calculer_somme(self):
            if not self.reponses_chiffrees:
                raise ValueError("Aucune reponse enregistree")

            somme_chiffree = 0
            for reponse_chiffree in self.reponses_chiffrees:
                somme_chiffree += reponse_chiffree
            return somme_chiffree

        def calculer_moyenne(self):
            somme_chiffree = self.calculer_somme()
            somme = self.private_key.decrypt(somme_chiffree)
            moyenne = somme / len(self.reponses_chiffrees)
            return somme, moyenne


    sondage = SondageAnonyme()
    notes = [8, 6, 9, 7, 10]

    print("SONDAGE ANONYME AVEC PAILLIER")
    print("=" * 70)
    print(f"Notes en clair (connues seulement ici pour verifier) : {notes}")

    for note in notes:
        chiffre = sondage.ajouter_reponse(note)
        print(f"  Vote chiffre ajoute : {str(chiffre.ciphertext())[:30]}...")

    somme, moyenne = sondage.calculer_moyenne()
    print()
    print(f"Somme dechiffree uniquement apres agregation : {somme}")
    print(f"Moyenne du sondage : {moyenne:.2f}/10")
    print(f"Verification en clair : somme={sum(notes)}, moyenne={sum(notes) / len(notes):.2f}/10")
else:
    print("Exercice a completer (phe non installe)")
SONDAGE ANONYME AVEC PAILLIER
======================================================================
Notes en clair (connues seulement ici pour verifier) : [8, 6, 9, 7, 10]
  Vote chiffre ajoute : 485701758929171170345698856698...
  Vote chiffre ajoute : 122427681841717094001907661007...
  Vote chiffre ajoute : 654045281838732554279128085433...
  Vote chiffre ajoute : 124329953818273901336914047633...
  Vote chiffre ajoute : 109546934516716041726874676331...

Somme dechiffree uniquement apres agregation : 40
Moyenne du sondage : 8.00/10
Verification en clair : somme=40, moyenne=8.00/10

Interpretation : sondage anonyme avec Paillier

Résultat obtenu : Cinq votes (notes 8, 6, 9, 7, 10) sont chiffres individuellement avec la cle publique. Le serveur additionne les chiffres homomorphiquement, puis dechiffre uniquement la somme agrege (40), donnant une moyenne de 8.00/10. Aucun vote individuel n’est jamais dechiffre par le serveur.

Étape Opération Donnee manipulee
Chiffrement public_key.encrypt(note) Cle publique uniquement
Agregation somme des chiffres via + Données chiffrees uniquement
Depouillement private_key.decrypt(somme) Somme agrege uniquement

Points cles de la solution : - La validation des entrees (type et plage 1-10) empeche les votes invalides - L’addition homomorphe somme_chiffree += reponse_chiffree fonctionne car Paillier est additivement homomorphique : D(E(a) + E(b)) = a + b - Seule la somme est dechiffree : la confidentialite individuelle est garantie mathematiquement


7. Exercice : Sondage pondere avec Paillier

Etendez le système de sondage anonyme pour supporter des votes ponderes : chaque participant a un poids différent (par exemple, selon son expertise ou son statut).

Specification

  1. Ajoutez une méthode ajouter_reponse_ponderee(self, note, poids) qui chiffre la note et la multiplie par le poids (scalaire en clair) avant de la stocker
  2. La somme dechiffree doit correspondre a sum(note_i * poids_i)
  3. La moyenne ponderee est sum(note_i * poids_i) / sum(poids_i)

Indice :

Utilisez la multiplication par scalaire : E(note) * poids = E(note * poids). La moyenne ponderee se calcule en divisant la somme dechiffree par la somme des poids.

if PHE_AVAILABLE_EX:
    from phe import paillier

    class SondagePondere:
        """Systeme de sondage anonyme avec ponderation.

        Chaque participant a un poids different. La somme homomorphique
        calcule la somme ponderee sans reveler les votes individuels.
        """

        def __init__(self):
            self.public_key, self.private_key = paillier.generate_paillier_keypair()
            self.reponses_chiffrees = []
            self.poids_total = 0

        def ajouter_reponse_ponderee(self, note, poids):
            # TODO: Chiffrer la note, multiplier par le poids, stocker
            pass  # TODO etudiant : chiffrer la note et multiplier par le poids

        def calculer_moyenne_ponderee(self):
            # TODO: Dechiffrer la somme ponderee et diviser par la somme des poids
            return None, None  # TODO etudiant : dechiffrer et calculer la moyenne

    # Validation
    sp = SondagePondere()
    notes_pond = [(8, 2), (6, 1), (9, 3), (7, 1), (10, 2)]
    for note, poids in notes_pond:
        sp.ajouter_reponse_ponderee(note, poids)

    resultat = sp.calculer_moyenne_ponderee()
    if resultat[0] is not None:
        somme_pond, moyenne_pond = resultat
        print(f"Moyenne ponderee : {moyenne_pond:.2f}/10")
        print(f"Verification : {sum(n * p for n, p in notes_pond) / sum(p for _, p in notes_pond):.2f}/10")
    else:
        print("Exercice a completer")
else:
    print("Exercice a completer (phe non installe)")
Exercice a completer

8. Resume

Tableau comparatif des approches de calcul confidentiel

Approche Opérations Performance Modèle de confiance Cas d’usage principal
PHE (Paillier) Addition Rapide 1 serveur Vote, sommes agregatrices
SHE (BGV/BFV) Add + Mult (limitee) Moyen 1 serveur Calculs entiers bornes
FHE (CKKS) Toutes (approchees) Lent 1 serveur ML confidentiel
FHE (TFHE) Toutes (exactes) Très lent 1 serveur Circuits arbitraires
MPC (Shamir) Lineaire natif Variable N parties Statistiques partagees

Points cles

  1. Le chiffrement homomorphique permet de calculer sans dechiffrer : c’est un changement de paradigme pour la confidentialite
  2. Paillier (PHE) est simple et rapide mais limite a l’addition : ideal pour le vote et les aggregations
  3. CKKS (FHE) permet le ML confidentiel mais avec un cout en performance de 10 000x
  4. Le partage de secrets de Shamir distribue la confiance : aucun participant seul ne connait le secret
  5. HE et MPC sont complementaires, pas concurrents : le choix depend du modèle de confiance

Notebook suivant : SC-17-E2E-Verifiable-Voting-Python - Combiner zero-knowledge proofs et chiffrement homomorphique pour un système de vote electronique verifiable de bout en bout

Resume et perspectives

Ce notebook a explore le chiffrement homomorphique sous ses trois declinaisons : le schema de Paillier (PHE additif) pour les sommes et agregats, le schema CKKS avec TenSEAL (FHE approche) pour le machine learning confidentiel sur des reels, et le partage de secrets de Shamir (MPC) pour le calcul multipartite securise. L’implementation du schema de Paillier a demontre la propriete fondamentale D(E(a) + E(b)) = a + b et le chiffrement probabiliste garantissant la securite sémantique. Le benchmark CKKS a revele un overhead de 1000 a 10 000 fois par rapport au calcul en clair, prix a payer pour la confidentialite des données.

La comparaison entre chiffrement homomorphique et calcul multipartite (MPC) met en evidence deux modèles de confiance distincts : un seul serveur non fiable pour le HE, plusieurs parties semi-honnetes pour le MPC. En pratique, les deux approches se completent plutot qu’elles ne s’opposent – le HE pour les scénarios centralises (cloud, vote), le MPC pour les contextes decentralises (encheres, statistiques inter-entreprises). L’emergence de frameworks comme Concrete (Zama) rapproche le FHE du developpeur Python, même si la performance reste le principal frein a l’adoption a grande echelle.

Le prochain notebook applique directement ces techniques cryptographiques au problème du vote electronique, en combinant chiffrement homomorphique de Paillier et preuves a divulgation nulle pour construire un système de vote verifiable de bout en bout : SC-17-E2E-Verifiable-Voting-Python.


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

Retour au sommaire

Retour au sommet