Le NIST a tranché en mars 2025 : HQC devient le cinquième algorithme retenu dans son programme de cryptographie post-quantique, pensé comme un plan B pour le chiffrement de clés si ML-KEM (FIPS 203) venait à tomber. Contrairement à ML-KEM, qui repose sur les réseaux euclidiens, HQC s’appuie sur le décodage de codes correcteurs quasi-cycliques de Hamming, une famille mathématique totalement différente. Ce choix délibéré de diversification n’est pas qu’une curiosité académique : liboqs propose déjà des implémentations fonctionnelles de HQC, et les bindings Python permettent de manipuler ce mécanisme dès aujourd’hui, en laboratoire ou en prototype. Ce tutoriel vous montre, étape par étape, comment compiler liboqs, installer ses bindings Python, générer des paires de clés HQC, encapsuler et décapsuler un secret partagé, puis l’utiliser pour chiffrer de vraies données avec AES-256-GCM. Vous repartirez avec un script complet, testé, et une idée claire des compromis de taille et de performance face à ML-KEM. Ce guide s’adresse aux développeurs backend, ingénieurs sécurité et étudiants en cryptographie qui veulent toucher du concret avant que la norme ne se fige, plutôt que de se contenter de lire des spécifications abstraites. Aucune connaissance préalable en cryptographie post-quantique n’est requise : seules des bases en Python et une familiarité avec la ligne de commande suffisent pour suivre les douze étapes qui suivent.
Pourquoi le NIST a choisi un second algorithme face à ML-KEM
ML-KEM, normalisé sous FIPS 203, est aujourd’hui l’algorithme d’échange de clé post-quantique de référence. Mais s’appuyer sur un seul fondement mathématique inquiète une partie de la communauté cryptographique : si une avancée en cryptanalyse des réseaux euclidiens venait fragiliser ML-KEM, il faudrait un remplaçant prêt à l’emploi. C’est exactement le rôle que le NIST a confié à HQC en le sélectionnant comme cinquième algorithme de son programme post-quantique, aux côtés de ML-KEM, ML-DSA, SLH-DSA et FN-DSA. Les quatre premiers partagent tous, à des degrés divers, une parenté avec les réseaux euclidiens ou les fonctions de hachage. HQC rompt ce schéma en misant sur la théorie des codes correcteurs d’erreurs, un domaine mathématique étudié depuis les années 1950 pour la transmission de données, mais appliqué ici à la construction d’un mécanisme d’encapsulation de clé.
Dustin Moody, mathématicien et responsable du projet de cryptographie post-quantique au NIST, a expliqué que cette sélection visait précisément à disposer d’un standard de secours fondé sur une approche mathématique différente de celle de ML-KEM. Il a aussi souligné qu’à mesure que la compréhension des futurs ordinateurs quantiques progresse, et que de nouvelles techniques de cryptanalyse émergent, disposer d’un filet de sécurité devient indispensable si ML-KEM se révélait un jour vulnérable.
Dans la pratique, HQC n’est pas encore un standard FIPS finalisé. Un projet de norme était attendu autour de 2026, avec une finalisation visée pour 2027 selon les informations disponibles à l’automne 2026. Le RFC 9958 le confirme d’ailleurs explicitement : HQC a été sélectionné dans le cadre du projet post-quantique du NIST, mais n’a pas encore été standardisé. Cela ne change rien à l’intérêt de l’essayer dès maintenant : tester un algorithme avant sa normalisation finale permet de repérer les frictions d’intégration, de mesurer son empreinte réseau et de préparer son code à l’agilité cryptographique, sans attendre le dernier moment.
HQC face à ML-KEM : tailles de clés, ciphertexts et compromis
La première chose que l’on remarque en manipulant HQC, c’est le poids de ses objets cryptographiques. Là où ML-KEM vise la compacité, HQC hérite de la lourdeur typique des schémas basés sur les codes correcteurs. Le tableau suivant reprend les tailles publiques des deux familles, par niveau de sécurité NIST (1, 3 et 5).
| Algorithme | Niveau NIST | Clé publique | Clé secrète | Ciphertext |
|---|---|---|---|---|
| ML-KEM-512 | 1 | 800 octets | 1 632 octets | 768 octets |
| ML-KEM-768 | 3 | 1 184 octets | 2 400 octets | 1 088 octets |
| ML-KEM-1024 | 5 | 1 568 octets | 3 168 octets | 1 568 octets |
| HQC-128 | 1 | ~2 249 octets | ~2 289 octets | ~4 481 octets |
| HQC-192 | 3 | ~4 514 octets | ~4 602 octets | ~8 978 octets |
| HQC-256 | 5 | ~7 237 octets | ~7 333 octets | ~14 421 octets |
Les écarts parlent d’eux-mêmes : une clé publique HQC-128 pèse environ 2,8 fois plus qu’une clé ML-KEM-512, et son ciphertext dépasse les 4,4 Ko contre moins de 800 octets côté ML-KEM. Une mesure réseau attribuée à Cloudflare évalue à environ 7 Ko le coût combiné d’une clé publique et d’un ciphertext HQC-128, contre 1,5 à 3 Ko pour les variantes de ML-KEM. Cloudflare a conclu que HQC se montre moins favorable que ML-KEM à la fois sur la taille échangée et sur le coût de calcul, un constat qui pèse lourd pour tout protocole contraint par la taille des paquets, comme TLS sur UDP ou les objets connectés à faible bande passante.
Ce poids n’est pas un défaut de conception gratuit. Il découle directement de la sécurité recherchée par le décodage de syndrome sur des codes quasi-cycliques, une hypothèse mathématique jugée plus conservatrice par certains cryptographes que celle des réseaux euclidiens. HQC n’est donc pas pensé pour remplacer ML-KEM partout, mais pour offrir une alternative solide le jour où elle deviendrait nécessaire. Gardez cette nuance en tête tout au long de ce tutoriel : vous allez manipuler un algorithme de secours, pas encore un standard de production.
Pour un développeur qui conçoit un protocole aujourd’hui, ces chiffres se traduisent en décisions concrètes. Un objet IoT alimenté par batterie et connecté en LoRaWAN ou en NB-IoT encaissera mal un ciphertext de 14 Ko au niveau de sécurité 5, alors qu’un serveur backend échangeant des clés par lots sur une liaison filaire n’en sera presque pas affecté. C’est précisément ce genre d’arbitrage, propre à chaque contexte, que ce tutoriel vous permettra de chiffrer vous-même grâce aux scripts de benchmark des étapes suivantes.
L’écosystème Open Quantum Safe derrière HQC
Avant de passer à la pratique, il faut comprendre d’où vient le code que vous allez compiler. Open Quantum Safe (OQS) est le projet communautaire qui fédère les implémentations open source des algorithmes post-quantiques candidats ou retenus par le NIST. Il se découpe en plusieurs briques distinctes, et comprendre leur rôle respectif évite bien des confusions au moment du build.
liboqs est le cœur du projet : une bibliothèque C qui implémente les primitives cryptographiques elles-mêmes, dont HQC, ML-KEM et plusieurs autres KEM et schémas de signature. C’est cette brique que vous allez compiler aux étapes 1 et 2. Au-dessus, liboqs-python expose cette bibliothèque C sous forme de classes Python idiomatiques comme KeyEncapsulation, ce qui évite d’écrire le moindre appel natif via ctypes à la main. D’autres bindings existent pour Java, Go ou C#, mais Python reste le point d’entrée le plus rapide pour prototyper.
Open Quantum Safe classe chaque algorithme selon un système de niveaux de maturité. HQC est aujourd’hui répertorié comme Tier 2 – Supported dans la documentation du projet, un cran sous les algorithmes déjà normalisés par le NIST comme ML-KEM, qui bénéficient d’un suivi plus poussé. Concrètement, ce classement signifie que HQC est maintenu activement, testé dans l’intégration continue du projet, mais n’a pas encore reçu le même niveau d’examen que les standards FIPS publiés. C’est une information à garder en tête avant de décider où faire tourner ce code : un environnement de test ou un pipeline de recherche convient très bien, un système de production réglementé beaucoup moins.
Une dernière brique mérite d’être mentionnée même si ce tutoriel ne l’utilise pas directement : oqs-provider, qui greffe les mécanismes de liboqs dans OpenSSL 3.x sous forme de fournisseur. Elle permet de tester HQC directement dans une poignée de main TLS, sujet que l’on retrouve dans la section des conseils avancés plus bas.
Prérequis techniques et versions exactes
Avant de lancer la moindre commande, vérifiez que votre environnement coche les cases suivantes. La compilation de liboqs se fait en C, ses bindings s’installent ensuite via pip, et tout le reste du tutoriel reste du Python pur.
| Composant | Rôle | Version recommandée |
|---|---|---|
| Système | Linux (Ubuntu/Debian) ou macOS | Distribution récente avec compilateur à jour |
| Python | Exécution des scripts et bindings | 3.11 ou 3.12 |
| CMake + Ninja (ou Make) | Compilation de liboqs | Dernière version du gestionnaire de paquets |
| Compilateur C | Build de liboqs | GCC ou Clang, support C99 |
| OpenSSL | Dépendance optionnelle de liboqs | Branche 3.x |
| liboqs | Implémentation C de HQC | Dernière version du dépôt officiel open-quantum-safe/liboqs |
| liboqs-python | Bindings Python | 0.16.0 (publiée le 23 juillet 2026) |
| cryptography (PyPI) | HKDF et AES-256-GCM | Dernière version stable |
Un point mérite d’être signalé avant de commencer : HQC a connu un épisode où liboqs a désactivé le mécanisme par défaut, le temps de corriger un problème de sécurité identifié dans l’implémentation amont. Depuis, le projet a migré sa source de référence de PQClean vers le dépôt officiel pqc-hqc, mis à jour sur la spécification du 22 août 2025, et les trois paramétrages HQC-128, HQC-192 et HQC-256 sont de nouveau activés par défaut dans les versions récentes. Si votre build affiche une erreur de mécanisme non supporté, c’est presque toujours le signe d’une version de liboqs trop ancienne.
Étape 1 à 4 : compiler liboqs et installer les bindings Python
Étape 1 : cloner et préparer le dépôt liboqs
Commencez par récupérer le dépôt officiel et créez un dossier de build séparé, une pratique CMake standard qui évite de polluer les sources.
git clone --depth=1 https://github.com/open-quantum-safe/liboqs.git
cd liboqs
mkdir build && cd build
Étape 2 : configurer et compiler en mode Release
Utilisez Ninja pour accélérer la compilation et forcez explicitement le mode Release : un build en mode Debug fausse complètement vos futures mesures de performance.
cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON ..
ninja
sudo ninja install
sudo ldconfig
Étape 3 : installer les bindings Python
Créez un environnement virtuel dédié avant d’installer quoi que ce soit, puis installez liboqs-python via pip.
python3 -m venv venv-hqc
source venv-hqc/bin/activate
pip install --upgrade pip
pip install liboqs-python==0.16.0 cryptography
Étape 4 : vérifier que HQC est bien disponible
Avant d’aller plus loin, listez les mécanismes KEM activés dans votre build. Si HQC-128 n’apparaît pas dans la liste, inutile de continuer : retournez vérifier l’étape 2.
import oqs
mechanisms = oqs.get_enabled_kem_mechanisms()
hqc_mechanisms = [m for m in mechanisms if m.startswith("HQC")]
print(hqc_mechanisms)
# Sortie attendue : ['HQC-128', 'HQC-192', 'HQC-256']
Si cette commande renvoie une liste vide, vérifiez trois choses dans l’ordre : que sudo ninja install s’est bien terminé sans erreur, que votre variable d’environnement de chemin de bibliothèques pointe vers l’emplacement d’installation de liboqs, et que la version de liboqs-python installée correspond bien à la version de liboqs compilée sur votre machine.
Étape 5 à 7 : générer une paire de clés et encapsuler un secret HQC-128
Étape 5 : générer la paire de clés côté destinataire
Dans un échange HQC, le destinataire (appelons-le Bob) génère une paire de clés et transmet uniquement sa clé publique. Rien dans cette étape ne doit quitter la machine de Bob à part cette clé publique.
import oqs
kem_alg = "HQC-128"
bob = oqs.KeyEncapsulation(kem_alg)
public_key = bob.generate_keypair()
print(f"Taille de la clé publique : {len(public_key)} octets")
# Sortie attendue : Taille de la clé publique : 2249 octets
Étape 6 : encapsuler un secret côté expéditeur
L’expéditeur (Alice) ne possède que la clé publique de Bob. Elle encapsule un secret aléatoire et obtient en retour un ciphertext à transmettre, ainsi que sa propre copie du secret partagé.
alice = oqs.KeyEncapsulation(kem_alg)
ciphertext, shared_secret_alice = alice.encap_secret(public_key)
print(f"Taille du ciphertext : {len(ciphertext)} octets")
print(f"Taille du secret partagé : {len(shared_secret_alice)} octets")
# Sortie attendue :
# Taille du ciphertext : 4481 octets
# Taille du secret partagé : 64 octets
Étape 7 : transmettre le ciphertext (simulation réseau)
Dans un vrai protocole, ce ciphertext voyage sur le réseau. Pour ce tutoriel, on se contente de le passer en mémoire, mais gardez en tête sa taille (4,4 Ko environ pour HQC-128) si vous l’intégrez un jour dans un protocole UDP contraint par la taille de MTU.
# Simulation d'une transmission réseau : sérialisation puis envoi
payload = ciphertext # À remplacer par un vrai envoi socket.send() en production
assert isinstance(payload, bytes)
print(f"Prêt à transmettre {len(payload)} octets à Bob")
Étape 8 à 9 : décapsuler le secret et dériver une clé AES-256-GCM
Étape 8 : décapsuler côté destinataire
Bob reçoit le ciphertext et utilise sa clé secrète, conservée localement depuis l’étape 5, pour retrouver le même secret partagé que celui calculé par Alice.
shared_secret_bob = bob.decap_secret(ciphertext)
assert shared_secret_bob == shared_secret_alice
print("Les deux secrets partagés sont identiques.")
Si cette assertion échoue, le problème vient presque toujours d’un ciphertext tronqué ou altéré entre les étapes 6 et 8, par exemple un encodage base64 oublié ou une troncature lors d’une lecture socket incomplète. Le secret partagé brut ne doit jamais être modifié entre l’encapsulation et la décapsulation.
Étape 9 : dériver une clé AES-256-GCM via HKDF
Le secret partagé brut de HQC ne doit jamais servir directement de clé de chiffrement. Faites-le toujours passer par une fonction de dérivation comme HKDF, qui homogénéise la longueur de sortie et ajoute une séparation de domaine via le paramètre info.
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
def derive_aes_key(shared_secret: bytes) -> bytes:
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=b"hqc-128-aes-256-gcm-v1",
)
return hkdf.derive(shared_secret)
aes_key = derive_aes_key(shared_secret_bob)
print(f"Clé AES dérivée : {len(aes_key)} octets")
# Sortie attendue : Clé AES dérivée : 32 octets
Étape 10 : chiffrer et déchiffrer un message avec le secret partagé
Une fois la clé AES-256 dérivée, le reste du travail n’a plus rien de post-quantique : c’est du chiffrement authentifié classique. AES-256-GCM protège à la fois la confidentialité et l’intégrité du message, ce qui en fait un choix naturel pour sceller des données une fois l’échange de clé HQC terminé.
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def chiffrer(cle: bytes, message: bytes) -> tuple[bytes, bytes]:
aesgcm = AESGCM(cle)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, message, associated_data=None)
return nonce, ciphertext
def dechiffrer(cle: bytes, nonce: bytes, ciphertext: bytes) -> bytes:
return AESGCM(cle).decrypt(nonce, ciphertext, associated_data=None)
message = b"Rendez-vous confirme pour 14h, salle B."
nonce, ciphertext_msg = chiffrer(aes_key, message)
message_dechiffre = dechiffrer(aes_key, nonce, ciphertext_msg)
print(message_dechiffre.decode())
# Sortie attendue : Rendez-vous confirme pour 14h, salle B.
Pensez à libérer explicitement les objets natifs une fois l’échange terminé, surtout dans un script qui tourne en boucle ou dans un serveur longue durée. Les objets KeyEncapsulation de liboqs-python détiennent des ressources côté C qu’il vaut mieux relâcher proprement.
alice.free()
bob.free()
Étape 11 à 12 : passer à HQC-192/HQC-256 et comparer les performances
Étape 11 : généraliser le script à tous les niveaux de sécurité
Le code précédent fonctionne à l’identique pour HQC-192 et HQC-256, il suffit de changer la chaîne kem_alg. Transformez cette logique en fonction réutilisable pour éviter de dupliquer le code à chaque changement de niveau.
def echange_hqc(kem_alg: str) -> dict:
bob = oqs.KeyEncapsulation(kem_alg)
public_key = bob.generate_keypair()
alice = oqs.KeyEncapsulation(kem_alg)
ciphertext, secret_alice = alice.encap_secret(public_key)
secret_bob = bob.decap_secret(ciphertext)
assert secret_alice == secret_bob
resultat = {
"algorithme": kem_alg,
"taille_cle_publique": len(public_key),
"taille_ciphertext": len(ciphertext),
}
alice.free()
bob.free()
return resultat
for niveau in ["HQC-128", "HQC-192", "HQC-256"]:
print(echange_hqc(niveau))
Étape 12 : mesurer les temps d’exécution sur votre machine
Les comparatifs publiés entre HQC et ML-KEM varient selon la machine, la version de liboqs et la méthode de mesure, il est donc préférable de mesurer vous-même sur votre environnement cible plutôt que de recopier un chiffre trouvé ailleurs. Le script suivant chronomètre les trois opérations sur 100 itérations pour chaque niveau de sécurité.
import time
def benchmark(kem_alg: str, iterations: int = 100) -> dict:
temps_keygen, temps_encap, temps_decap = [], [], []
for _ in range(iterations):
t0 = time.perf_counter()
bob = oqs.KeyEncapsulation(kem_alg)
public_key = bob.generate_keypair()
t1 = time.perf_counter()
alice = oqs.KeyEncapsulation(kem_alg)
ciphertext, _ = alice.encap_secret(public_key)
t2 = time.perf_counter()
bob.decap_secret(ciphertext)
t3 = time.perf_counter()
temps_keygen.append((t1 - t0) * 1000)
temps_encap.append((t2 - t1) * 1000)
temps_decap.append((t3 - t2) * 1000)
alice.free()
bob.free()
moyenne = lambda liste: sum(liste) / len(liste)
return {
"algorithme": kem_alg,
"keygen_ms": round(moyenne(temps_keygen), 3),
"encap_ms": round(moyenne(temps_encap), 3),
"decap_ms": round(moyenne(temps_decap), 3),
}
for niveau in ["HQC-128", "HQC-192", "HQC-256"]:
print(benchmark(niveau))
Sur une machine de développement classique, vous devriez observer une tendance nette : les temps augmentent sensiblement entre HQC-128 et HQC-256, et les trois opérations restent globalement plus coûteuses que leurs équivalents ML-KEM, conformément aux observations déjà publiées sur le sujet. Le tableau ci-dessous résume les grandeurs à surveiller lors de votre propre mesure.
| Métrique | HQC-128 | HQC-192 | HQC-256 |
|---|---|---|---|
| Taille clé publique | ~2 249 o | ~4 514 o | ~7 237 o |
| Taille ciphertext | ~4 481 o | ~8 978 o | ~14 421 o |
| Coût calcul relatif | Référence | Supérieur | Plus de 4x le niveau 1 |
| Usage recommandé 2026 | Test, R&D | Test, R&D | Test, R&D |
Le projet complet : un échange HQC de bout en bout en un seul script
Voici l’assemblage final, qui réunit toutes les étapes précédentes dans un seul fichier exécutable. Il génère une paire de clés HQC-128, encapsule et décapsule un secret, dérive une clé AES-256-GCM, puis chiffre et déchiffre un message de test. C’est le squelette que vous pouvez copier dans votre propre projet et étendre.
import os
import oqs
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def derive_aes_key(shared_secret: bytes, contexte: bytes) -> bytes:
hkdf = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=contexte)
return hkdf.derive(shared_secret)
def chiffrer(cle: bytes, message: bytes) -> tuple[bytes, bytes]:
aesgcm = AESGCM(cle)
nonce = os.urandom(12)
return nonce, aesgcm.encrypt(nonce, message, associated_data=None)
def dechiffrer(cle: bytes, nonce: bytes, ciphertext: bytes) -> bytes:
return AESGCM(cle).decrypt(nonce, ciphertext, associated_data=None)
def demo_hqc(kem_alg: str = "HQC-128") -> None:
bob = oqs.KeyEncapsulation(kem_alg)
public_key = bob.generate_keypair()
alice = oqs.KeyEncapsulation(kem_alg)
ciphertext, secret_alice = alice.encap_secret(public_key)
secret_bob = bob.decap_secret(ciphertext)
assert secret_alice == secret_bob, "Les secrets partages ne correspondent pas"
contexte = f"{kem_alg}-aes-256-gcm-v1".encode()
cle_aes = derive_aes_key(secret_bob, contexte)
message = b"Message confidentiel protege par HQC et AES-256-GCM"
nonce, msg_chiffre = chiffrer(cle_aes, message)
message_clair = dechiffrer(cle_aes, nonce, msg_chiffre)
print(f"Algorithme KEM : {kem_alg}")
print(f"Taille cle publique : {len(public_key)} octets")
print(f"Taille ciphertext KEM : {len(ciphertext)} octets")
print(f"Message dechiffre : {message_clair.decode()}")
alice.free()
bob.free()
if __name__ == "__main__":
demo_hqc("HQC-128")
Exécuté tel quel, ce script affiche l’algorithme utilisé, la taille des objets cryptographiques échangés et le message déchiffré, ce qui vous permet de vérifier d’un coup d’œil que toute la chaîne fonctionne, de la génération de clé jusqu’au déchiffrement final.
Un script unique reste pratique pour valider le principe, mais il masque une réalité importante : dans un vrai usage, la génération de clé, l’encapsulation et la décapsulation se produisent sur deux machines distinctes, séparées par un réseau. Pour rendre ce tutoriel réellement complet, voici une version découpée en deux fichiers, un serveur et un client, qui communiquent par sockets TCP bruts. C’est la structure minimale d’un vrai protocole d’échange de clé HQC.
Le serveur joue le rôle de Bob. Il génère sa paire de clés, publie sa clé publique sur le réseau, attend le ciphertext d’Alice, puis décapsule le secret et l’utilise pour déchiffrer le message reçu.
# serveur.py
import socket
import struct
import oqs
HOTE, PORT = "127.0.0.1", 9443
KEM_ALG = "HQC-128"
def recevoir_bloc(conn: socket.socket) -> bytes:
taille = struct.unpack("!I", conn.recv(4))[0]
donnees = b""
while len(donnees) < taille:
donnees += conn.recv(taille - len(donnees))
return donnees
def envoyer_bloc(conn: socket.socket, donnees: bytes) -> None:
conn.sendall(struct.pack("!I", len(donnees)) + donnees)
def lancer_serveur() -> None:
bob = oqs.KeyEncapsulation(KEM_ALG)
public_key = bob.generate_keypair()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
srv.bind((HOTE, PORT))
srv.listen(1)
print(f"Serveur HQC en attente sur {HOTE}:{PORT}")
conn, _ = srv.accept()
with conn:
envoyer_bloc(conn, public_key)
ciphertext = recevoir_bloc(conn)
secret_partage = bob.decap_secret(ciphertext)
print(f"Secret partagé établi côté serveur ({len(secret_partage)} octets)")
bob.free()
if __name__ == "__main__":
lancer_serveur()
Le client joue le rôle d’Alice. Il récupère la clé publique du serveur, encapsule un secret, envoie le ciphertext, puis utilise le secret partagé obtenu localement pour la suite de la session chiffrée.
# client.py
import socket
import struct
import oqs
HOTE, PORT = "127.0.0.1", 9443
KEM_ALG = "HQC-128"
def recevoir_bloc(conn: socket.socket) -> bytes:
taille = struct.unpack("!I", conn.recv(4))[0]
donnees = b""
while len(donnees) < taille:
donnees += conn.recv(taille - len(donnees))
return donnees
def envoyer_bloc(conn: socket.socket, donnees: bytes) -> None:
conn.sendall(struct.pack("!I", len(donnees)) + donnees)
def lancer_client() -> None:
alice = oqs.KeyEncapsulation(KEM_ALG)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as conn:
conn.connect((HOTE, PORT))
public_key_serveur = recevoir_bloc(conn)
ciphertext, secret_partage = alice.encap_secret(public_key_serveur)
envoyer_bloc(conn, ciphertext)
print(f"Secret partagé établi côté client ({len(secret_partage)} octets)")
alice.free()
if __name__ == "__main__":
lancer_client()
Lancez python serveur.py dans un premier terminal, puis python client.py dans un second. Les deux processus doivent afficher un secret partagé de même longueur, preuve que l’échange a réussi à travers une vraie connexion TCP et non plus seulement en mémoire partagée au sein d’un même script. À partir de cette base, il ne reste plus qu’à brancher la dérivation HKDF et le chiffrement AES-256-GCM des étapes précédentes sur ce canal pour obtenir une session chiffrée complète de bout en bout.
Ce squelette client/serveur reste volontairement minimaliste et ne doit pas aller tel quel en production. Il lui manque deux garanties essentielles : l’authentification de la clé publique reçue, qui empêche une attaque de type intercepteur se faisant passer pour le serveur, et la protection contre le rejeu, qui empêche un ciphertext capturé d’être réutilisé plus tard. Un vrai déploiement s’appuierait plutôt sur oqs-provider à l’intérieur d’une poignée de main TLS 1.3 complète, où ces garanties sont déjà prises en charge par le protocole, plutôt que de les réimplémenter à la main au-dessus de sockets brutes.
5 pièges fréquents à éviter avec HQC et liboqs
- Utiliser le secret partagé brut comme clé de chiffrement. Le secret issu de
decap_secret()doit systématiquement passer par une KDF comme HKDF avant de servir de clé AES, sous peine d’exposer des biais structurels liés à l’algorithme. - Confondre les noms de mécanismes internes et les noms d’API. Les flags de compilation CMake référencent parfois
HQC-1,HQC-3,HQC-5, alors que l’API Python attendHQC-128,HQC-192,HQC-256. Une confusion ici déclenche une erreur de mécanisme non supporté. - Oublier de libérer les objets
KeyEncapsulation. Ce sont des wrappers autour de ressources natives en C. Dans une boucle de test ou un serveur longue durée, l’absence d’appel à.free()entraîne une fuite mémoire progressive. - Sous-estimer la taille des paquets HQC dans un protocole contraint. Avec un ciphertext proche de 14,4 Ko au niveau 5, HQC peut dépasser la taille maximale d’un seul datagramme UDP et nécessiter une fragmentation applicative que ML-KEM évite largement.
- Compiler liboqs en mode Debug par erreur. Sans
-DCMAKE_BUILD_TYPE=Releaseexplicite, vos mesures de performance seront artificiellement dégradées et inutilisables pour une vraie comparaison avec ML-KEM. - Traiter HQC comme un mécanisme de signature. HQC est un KEM, il sert uniquement à établir un secret partagé entre deux parties. Pour signer des messages de façon post-quantique, il faut se tourner vers ML-DSA, SLH-DSA ou FN-DSA, pas vers HQC.
Dépannage : 8 erreurs courantes et leurs solutions
- ModuleNotFoundError: No module named ‘oqs’ : Les bindings n’ont pas été installés dans l’environnement virtuel actif. Relancez
pip install liboqs-python==0.16.0après avoir activé le bon venv. - oqs.MechanismNotSupportedError : Le mécanisme HQC demandé n’est pas activé dans votre build de liboqs. Recompilez en vérifiant que les flags HQC sont bien actifs, ou mettez à jour vers une version plus récente du dépôt.
- OSError: liboqs.so: cannot open shared object file : La bibliothèque partagée compilée n’est pas trouvée au runtime. Sur Linux, exécutez
sudo ldconfigaprès l’installation, ou exportez la variable de chemin de bibliothèques vers le dossier d’installation. - AssertionError sur la comparaison des secrets partagés : Le ciphertext a été altéré ou tronqué entre l’encapsulation et la décapsulation, souvent à cause d’un encodage base64 oublié lors d’un transfert réseau simulé.
- Segmentation fault à la fin du script : Généralement causé par un double appel à
.free()sur le même objet, ou par l’utilisation d’un objet déjà libéré. Vérifiez l’ordre des appels de nettoyage. - Échec de build CMake faute de Ninja : Installez le paquet
ninja-buildvia votre gestionnaire de paquets, ou retirez l’option-GNinjapour revenir au générateur Makefiles par défaut. - pip install liboqs-python échoue en cherchant liboqs : Les en-têtes et bibliothèques de liboqs ne sont pas visibles par le système après l’installation. Vérifiez que
sudo ninja installs’est terminé sans erreur et que le préfixe d’installation correspond aux chemins de recherche standards. - Performances très en deçà de ce qui est attendu : Le build est probablement resté en mode Debug. Recompilez avec
-DCMAKE_BUILD_TYPE=Releaseet relancez votre benchmark pour obtenir des chiffres exploitables. - ConnectionRefusedError lors du test client/serveur : Le script serveur.py n’est pas encore démarré, ou il écoute sur un port différent de celui utilisé par client.py. Vérifiez que les deux fichiers partagent la même valeur de
PORTet lancez toujours le serveur en premier.
Conseils avancés : hybrides post-quantiques et migration
Tant que HQC n’a pas de numéro FIPS confirmé, l’utiliser seul dans un système soumis à des exigences réglementaires serait prématuré. L’approche recommandée par une partie de la communauté post-quantique consiste plutôt à combiner HQC avec un algorithme déjà normalisé, en mode hybride : on encapsule un secret avec ML-KEM et un second avec HQC, puis on les concatène via une KDF avant de dériver la clé finale. Cette construction garde la sécurité du composant normalisé tout en ajoutant une diversité mathématique de secours, sans rien perdre en cas de faille découverte sur l’un des deux.
Ce schéma de construction hybride n’est pas une invention isolée : l’IETF documente une approche similaire dans son brouillon draft-ietf-tls-hybrid-design, qui décrit comment combiner un KEM classique et un KEM post-quantique au sein d’une même poignée de main TLS. Transposer ce principe à HQC revient à l’utiliser comme composant secondaire d’un KEM combiné, jamais comme seul rempart. Concrètement, dans votre code, cela veut dire appeler encap_secret() deux fois avec deux objets KeyEncapsulation différents, puis dériver une seule clé AES à partir de la concaténation des deux secrets via HKDF, exactement comme la fonction derive_aes_key() de ce tutoriel le fait déjà pour un seul secret.
Pour qui veut aller plus loin que ce tutoriel, le projet oqs-provider d’Open Quantum Safe expose les mécanismes de liboqs comme fournisseur OpenSSL 3.x, ce qui permet d’expérimenter HQC directement dans des poignées de main TLS 1.3 de test, sans toucher au code applicatif. C’est l’environnement le plus réaliste pour mesurer l’impact de la taille des ciphertexts HQC sur la latence d’un vrai handshake, plutôt que sur un échange isolé comme celui de ce tutoriel. Le code source complet des bindings utilisés dans ce tutoriel reste par ailleurs consultable sur le dépôt GitHub de liboqs-python, notamment son dossier d’exemples qui couvre aussi les mécanismes de signature post-quantique.
Enfin, pensez l’agilité cryptographique dès la conception : exposez le choix de l’algorithme KEM comme un paramètre de configuration plutôt qu’une constante codée en dur. Le jour où le NIST publiera un projet de norme FIPS pour HQC, ou si vous préférez basculer vers HQC-256 pour un cas d’usage à très haute sensibilité, le changement ne devrait toucher qu’une ligne de configuration, pas une refonte du code.
FAQ : vos questions sur HQC en Python
HQC est-il déjà un standard officiel ?
Non. Le NIST l’a sélectionné en mars 2025 comme cinquième algorithme de son programme post-quantique, mais aucun numéro FIPS n’a encore été publié. Un projet de norme est attendu autour de 2026, avec une finalisation visée pour 2027.
Peut-on utiliser HQC en production dès maintenant ?
Pas seul, et pas pour des systèmes soumis à des contraintes réglementaires. HQC convient aujourd’hui à des environnements de test, de recherche, ou en mode hybride associé à ML-KEM.
HQC est-il plus sûr que ML-KEM ?
Ce n’est pas la bonne question. HQC repose sur une hypothèse mathématique différente de celle de ML-KEM, ce qui en fait un filet de sécurité complémentaire plutôt qu’un concurrent plus ou moins sûr. Le NIST l’a sélectionné justement pour cette diversité, pas pour remplacer ML-KEM.
Pourquoi les clés et ciphertexts HQC sont-ils si volumineux ?
Cette taille découle directement du décodage de syndrome sur des codes quasi-cycliques, la famille mathématique sur laquelle repose HQC. C’est un compromis assumé par ses concepteurs en échange d’une hypothèse de sécurité jugée conservatrice.
HQC fonctionne-t-il sous Windows ?
liboqs et ses bindings Python restent avant tout pensés pour Linux et macOS. Sous Windows, privilégiez WSL2 avec une distribution Linux pour éviter les complications de compilation native.
Faut-il recompiler liboqs à chaque mise à jour de Python ?
Non. liboqs est une bibliothèque C indépendante du langage. Seuls les bindings liboqs-python doivent correspondre à votre interpréteur Python. Une mise à jour de Python nécessite au plus une réinstallation des bindings, pas une recompilation de liboqs.
Comment migrer facilement entre HQC et ML-KEM plus tard ?
En gardant le nom du mécanisme KEM comme simple paramètre de configuration dans votre code, comme illustré dans ce tutoriel avec la fonction echange_hqc(). Le changement d’algorithme se résume alors à changer une chaîne de caractères.
Où suivre l’avancement de la standardisation de HQC ?
Le mieux reste de surveiller directement les publications du projet de cryptographie post-quantique du NIST et la page du projet Open Quantum Safe, qui documente l’état d’implémentation de HQC au fil des versions de liboqs.




