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 homomorphiqueschemas = {"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
Generation : choisir deux grands premiers \(p, q\). Poser \(n = p \cdot q\)
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 phetry:from phe import paillier PHE_AVAILABLE =TrueexceptImportError: PHE_AVAILABLE =Falseprint("phe non installe. Installez avec: pip install phe")import timeif PHE_AVAILABLE:# Generation des clesprint("GENERATION DES CLES PAILLIER")print("="*70) start = time.time() public_key, private_key = paillier.generate_paillier_keypair(n_length=2048) elapsed = time.time() - startprint(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 baseif 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 chiffresif 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 clairprint("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 inzip(encrypted_values, weights)) result = private_key.decrypt(encrypted_weighted) expected =sum(v * w for v, w inzip(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")
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_priveedef 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 totalreturnNone# TODO etudiant# Validationif PHE_AVAILABLE: test_values = [15, 20, 25, 30, 35] result = moyenne_privee(public_key, private_key, test_values)if result isnotNone: 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
Les nombres reels sont encodes dans des polynomes cyclotomiques
Le bruit de chiffrement est inclus dans la precision (pas un defaut, une feature)
Chaque opération multiplie le bruit ; le bootstrapping le reinitialise
La precision est configurable (~40 bits pour des calculs ML classiques)
# TenSEAL : CKKS pour l'arithmetique approchee# pip install tensealtry:import tenseal as tsimport 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**40print("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 inzip(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 inzip(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 inzip(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 inzip(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 inzip(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}")exceptImportError: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.")
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 CKKStry:import tenseal as tsimport 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 _ inrange(100): _ = ts.ckks_vector(context, v) t_encrypt = (time.time() - start) /100print(f" Chiffrement : {t_encrypt*1000:.2f} ms")# Temps d'addition enc_v2 = ts.ckks_vector(context, v) start = time.time()for _ inrange(100): _ = enc_v + enc_v2 t_add = (time.time() - start) /100print(f" Addition chiffree : {t_add*1000:.2f} ms")# Temps de multiplication start = time.time()for _ inrange(100): _ = enc_v * enc_v2 t_mul = (time.time() - start) /100print(f" Multiplication : {t_mul*1000:.2f} ms")# Temps de dechiffrement start = time.time()for _ inrange(100): _ = enc_v.decrypt() t_decrypt = (time.time() - start) /100print(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")exceptImportError: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
Ecrire une fonction Python avec des opérations sur entiers
Concrete trace la fonction et genere un circuit FHE
Le circuit est compile et optimise pour le bootstrapping TFHE
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-pythontry:import warnings warnings.filterwarnings("ignore", message="pkg_resources is deprecated as an API.*", category=UserWarning, )from concrete import fheimport numpy as np# Definir une fonction simpledef 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 inrange(10) for j inrange(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 !")exceptImportError: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")exceptExceptionas 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.
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
Distribuer les parts : part \(i\) = \(P(i)\) pour \(i = 1, ..., n\)
Reconstruction : interpolation de Lagrange avec \(k\) parts quelconques
import randomfrom functools importreduce# Arithmetique modulaire pour eviter les problemes de precision# On travaille dans Z/pZ avec p premierPRIME =2**127-1# 12e nombre premier de Mersennedef 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:raiseValueError("k doit etre <= n")# Generer un polynome aleatoire de degre k-1 avec P(0) = secret coefficients = [secret % prime] + [ random.randrange(1, prime) for _ inrange(k -1) ]# Evaluer le polynome en x = 1, 2, ..., ndef evaluate(x): result =0 power =1for coeff in coefficients: result = (result + coeff * power) % prime power = (power * x) % primereturn result shares = [(i, evaluate(i)) for i inrange(1, n +1)]return sharesdef 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."""returnpow(a, p -2, p) secret =0for j inrange(k): xj, yj = shares[j]# Calcul du coefficient de Lagrange L_j(0) numerator =1 denominator =1for m inrange(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) % primereturn secret# Demonstrationprint("PARTAGE DE SECRETS DE SHAMIR")print("="*70)secret =42k =3# seuiln =5# nombre de partsprint(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) inenumerate(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 partsimport itertoolsprint("RECONSTRUCTION AVEC DIFFERENTS SOUS-ENSEMBLES")print("="*70)# Avec exactement k=3 partsprint(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)")breakprint()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 textedef 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 etudiantdef 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# Validationmsg ="SECRET"k_test, n_test =3, 5parts = 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 =TrueexceptImportError: PHE_AVAILABLE_EX =Falseif PHE_AVAILABLE_EX:from phe import paillierclass 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):ifnotisinstance(note, int):raiseTypeError("La note doit etre un entier")ifnot1<= note <=10:raiseValueError("La note doit etre comprise entre 1 et 10") reponse_chiffree =self.public_key.encrypt(note)self.reponses_chiffrees.append(reponse_chiffree)return reponse_chiffreedef calculer_somme(self):ifnotself.reponses_chiffrees:raiseValueError("Aucune reponse enregistree") somme_chiffree =0for reponse_chiffree inself.reponses_chiffrees: somme_chiffree += reponse_chiffreereturn somme_chiffreedef 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)")
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
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
La somme dechiffree doit correspondre a sum(note_i * poids_i)
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 paillierclass 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 =0def ajouter_reponse_ponderee(self, note, poids):# TODO: Chiffrer la note, multiplier par le poids, stockerpass# TODO etudiant : chiffrer la note et multiplier par le poidsdef calculer_moyenne_ponderee(self):# TODO: Dechiffrer la somme ponderee et diviser par la somme des poidsreturnNone, 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] isnotNone: somme_pond, moyenne_pond = resultatprint(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
Le chiffrement homomorphique permet de calculer sans dechiffrer : c’est un changement de paradigme pour la confidentialite
Paillier (PHE) est simple et rapide mais limite a l’addition : ideal pour le vote et les aggregations
CKKS (FHE) permet le ML confidentiel mais avec un cout en performance de 10 000x
Le partage de secrets de Shamir distribue la confiance : aucun participant seul ne connait le secret
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.