Aller au contenu
ecdsa.com
← Tous les articles

X25519MLKEM768 expliqué : échange de clés hybride TLS 1.3

Une seule key share transporte une clé ML-KEM-768 et une clé X25519, et les deux secrets entrent dans la dérivation des clés (key schedule). Voici la disposition des octets, les clients qui l’envoient déjà, comment confirmer l’hybride sur votre propre serveur, et les vieux équipements réseau qui ne le digèrent pas.

En bref. X25519MLKEM768 est un groupe d’échange de clés de TLS 1.3, de point de code 0x11EC (4588), qui exécute ML-KEM-768 et X25519 côte à côte et injecte les deux secrets partagés dans la dérivation des clés (key schedule). Il est normalisé dans la RFC 10024 (août 2026). Chrome, Firefox, les API réseau d’Apple dans les versions 26, OpenSSL 3.5, Go 1.24+ et JDK 27 le proposent par défaut. Lancez openssl s_client -connect example.com:443 -brief avec OpenSSL 3.5 et cherchez Negotiated TLS1.3 group: X25519MLKEM768. Il protège le trafic enregistré contre un futur ordinateur quantique. Il n’apporte rien à l’authentification, qui repose toujours sur des certificats RSA ou ECDSA.

Ce que dit le nom

TLS 1.3 négocie l’échange de clés au moyen de groupes nommés. Le client énumère les groupes qu’il prend en charge dans l’extension supported_groups et envoie des clés publiques pour un ou plusieurs d’entre eux dans key_share. Le serveur choisit un groupe et répond avec sa propre key share. Pendant des années, les choix réalistes étaient X25519 et ECDH sur P-256. Tous deux tombent face à l’algorithme de Shor sur un ordinateur quantique suffisamment puissant, et le trafic enregistré aujourd’hui pourra être déchiffré dès qu’une telle machine existera. C’est le scénario récolter maintenant, déchiffrer plus tard.

Un groupe hybride répond à ce problème sans tout miser sur des mathématiques nouvelles. X25519MLKEM768 exécute deux accords de clé dans une seule négociation :

  • ML-KEM-768, le mécanisme d’encapsulation de clé fondé sur les réseaux euclidiens modulaires défini dans FIPS 203 (août 2024), avec les paramètres de catégorie 3. Cette partie résiste aux attaques quantiques.
  • X25519, la fonction Diffie-Hellman sur courbe elliptique de la RFC 7748, qui a derrière elle une longue histoire de cryptanalyse et sert de filet de sécurité au composant plus récent.

La construction générique est décrite dans la RFC 9954 (Hybrid Key Exchange in TLS 1.3, juillet 2026), dont l’objectif déclaré est une sécurité assurée « même si l’on trouve un moyen de casser le chiffrement de tous les algorithmes composants sauf un ». Les groupes concrets sont définis dans la RFC 10024 (août 2026) :

GroupePoint de codeRecommandéComposants
X25519MLKEM7680x11EC (4588)OuiML-KEM-768 + X25519
SecP256r1MLKEM7680x11EB (4587)NonECDH P-256 + ML-KEM-768
SecP384r1MLKEM10240x11ED (4589)NonECDH P-384 + ML-KEM-1024

Des captures plus anciennes peuvent montrer X25519Kyber768Draft00, de point de code 0x6399 (25497). C’était l’hybride Kyber antérieur à la norme, que Chrome a livré en premier. FIPS 203 a modifié l’algorithme sur des points mineurs mais incompatibles, si bien que Chrome 131 l’a remplacé par 0x11EC et a abandonné le groupe provisoire. L’annonce de Google explique pourquoi Chrome n’a pas pris en charge les deux : « La cryptographie post-quantique est trop volumineuse pour pouvoir proposer deux prédictions de key share post-quantiques en même temps. » La RFC 10024 déclare obsolètes les points de code provisoires.

À l’intérieur de la key share

La key share hybride est une simple concaténation des valeurs des deux composants, sans préfixe de longueur à l’intérieur. Les tailles étant fixées par FIPS 203 et la RFC 7748, chaque champ peut être vérifié par sa seule longueur :

ChampStructureOctets
key_exchange du clientClé d’encapsulation ML-KEM-768 (1184) ‖ clé publique X25519 (32)1216
key_exchange du serveurTexte chiffré ML-KEM-768 (1088) ‖ clé publique X25519 (32)1120
Secret partagéSecret partagé ML-KEM (32) ‖ secret partagé X25519 (32)64

Notez l’ordre. Le nom commence par X25519, mais les octets commencent par ML-KEM. La RFC 10024 indique que l’ordre « a été inversé » par rapport à la convention de nommage « pour des raisons historiques ». Sa section 5 donne aussi une raison de conformité : la publication NIST SP 800-56C n’autorise un secret combiné dans une dérivation de clé approuvée FIPS que si la première entrée provient d’un schéma approuvé, et dans cette paire, c’est ML-KEM. Les deux hybrides sur courbes NIST placent la key share ECDH en premier pour la même raison. Un parseur qui coupe le champ à l’octet 32 au lieu de l’octet 1184 échouera à chaque fois.

Voici un vrai ClientHello d’OpenSSL 3.5.7 vers cloudflare.com, capturé avec -trace le 20 octobre 2026 (octets de clé raccourcis) :

openssl s_client -trace, ClientHello (abrégé)
Sent TLS Record
  Content Type = Handshake (22)
  Length = 1551
    ClientHello, Length=1547
        extension_type=supported_groups(10), length=18
          X25519MLKEM768 (4588)
          ecdh_x25519 (29)
          secp256r1 (P-256) (23)
          ecdh_x448 (30)
          secp384r1 (P-384) (24)
          secp521r1 (P-521) (25)
          ffdhe2048 (256)
          ffdhe3072 (257)
        extension_type=key_share(51), length=1258
            NamedGroup: X25519MLKEM768 (4588)
            key_exchange:  (len=1216): A4696B297C695DEA602FB55157BC359F...
            NamedGroup: ecdh_x25519 (29)
            key_exchange:  (len=32): 919D957E3D7E402ECF27C3DD0B38DDA0...

Le client envoie deux key shares : la key share hybride et une key share X25519 simple. C’est le comportement par défaut d’OpenSSL 3.5. Go 1.27.3 envoie la même paire, tout comme Chromium 156, qui ajoute une entrée GREASE et produit un enregistrement de 1704 octets. La JEP 527 prévoit la même paire pour JDK 27. La key share simple coûte 32 octets et permet à un serveur qui ne connaît pas 0x11EC de terminer le handshake en un seul aller-retour au lieu de demander une autre key share. Le décompte de l’extension key_share est exact : 2 octets de longueur de liste, puis 2 + 2 + 1216 pour l’entrée hybride et 2 + 2 + 32 pour X25519, soit 1258 au total. Le serveur a choisi l’hybride :

openssl s_client -trace, ServerHello (abrégé)
    ServerHello, Length=1206
        extension_type=key_share(51), length=1124
            NamedGroup: X25519MLKEM768 (4588)
            key_exchange:  (len=1120): C01BC77BB1F9985B0DE82CDD1D0DD14B...

À titre de comparaison, le même client avec -groups X25519 envoie un ClientHello de 313 octets. L’hybride ajoute environ 1,2 Ko dans chaque sens, et c’est cette taille qui est à l’origine de la plupart des problèmes de compatibilité (voir plus bas).

De deux secrets à une seule dérivation des clés

Chaque partie effectue deux opérations indépendantes :

  1. Le client génère une paire de clés ML-KEM-768 et une paire de clés X25519, puis envoie les deux clés publiques.
  2. Le serveur encapsule avec la clé ML-KEM. Il obtient un secret de 32 octets et un texte chiffré de 1088 octets. Il exécute aussi X25519 avec sa propre clé éphémère, puis envoie le texte chiffré et sa clé publique X25519.
  3. Le client décapsule le texte chiffré pour retrouver le même secret ML-KEM et termine X25519 de son côté.
  4. Les deux parties concatènent les résultats en 64 octets et utilisent cette valeur là où TLS 1.3 place normalement le secret (EC)DHE : comme entrée du HKDF-Extract qui dérive le secret de handshake (RFC 9846 §7.1, la révision de TLS 1.3 de juillet 2026, qui rend obsolète la RFC 8446).

Tout ce qui suit relève du TLS 1.3 ordinaire. Les suites de chiffrement, le certificat, CertificateVerify et les clés de trafic sont inchangés. La RFC 10024 ajoute des étapes de validation que les implémentations doivent effectuer. Elles vérifient la clé d’encapsulation comme l’exige FIPS 203, rejettent un texte chiffré de mauvaise longueur et abandonnent si le résultat de X25519 ne contient que des zéros. Chaque échec met fin au handshake avec une alerte illegal_parameter ou internal_error.

L’argument de sécurité est court. Un attaquant qui enregistre le handshake doit retrouver les deux secrets pour calculer le secret de handshake. Casser X25519 avec un futur ordinateur quantique ne suffit pas tant que ML-KEM tient. Une surprise cryptanalytique contre le jeune schéma fondé sur les réseaux euclidiens ne suffit pas tant que X25519 tient. Si vous voulez voir l’encapsulation et la décapsulation sur de vraies clés, le laboratoire ML-KEM les exécute dans votre navigateur.

Qui l’active par défaut

En octobre 2026, tous les grands clients le proposent par défaut :

LogicielPar défaut depuisSource
Chrome (ordinateur)Chrome 131, en remplacement du groupe provisoire KyberGoogle Security Blog
FirefoxPris en charge depuis Firefox 132 (TLS) et Firefox 135 (HTTP/3). La préférence security.tls.enable_kyber vaut true par défautNotes de version
Apple : URLSession et framework NetworkLes versions 26 : iOS, iPadOS, macOS Tahoe, visionOS (guide réseau), ainsi que tvOS et watchOS (guide de sécurité)Assistance Apple, Sécurité des plateformes
OpenSSL3.5.0 : groupes hybrides dans la liste par défaut, key shares par défaut X25519MLKEM768 + X25519NEWS.md
Go crypto/tlsGo 1.24, lorsque Config.CurvePreferences vaut nil. Go 1.26 ajoute les deux hybrides sur courbes NISTGo 1.24, Go 1.26
Java (JSSE)JDK 27, JEP 527 : X25519MLKEM768 en tête de la liste par défautJEP 527

Tout logiciel lié à OpenSSL 3.5 adopte lui aussi ce réglage par défaut, tant que sa configuration ne remplace pas la liste des groupes. C’est le cas des paquets de Debian 13 et de Node.js 24, qui embarque son propre OpenSSL 3.5. Sous Debian 13, le module ssl de Python a terminé un handshake avec un openssl s_server -groups X25519MLKEM768 qui n’accepte rien d’autre, et curl et nginx ont négocié l’hybride sans configuration supplémentaire. La page Sécurité TLS d’Apple signale une lacune : « les clients TLS qui utilisent les API SecureTransport ne peuvent pas utiliser TLS 1.3 », si bien que les applications qui reposent encore sur cette API n’ont pas d’échange de clés hybride.

Côté serveur, une grande part du trafic HTTPS aboutit déjà à des piles TLS qui choisissent l’hybride quand le client le propose. Une sonde lancée depuis OpenSSL 3.5.7 le 20 octobre 2026 a négocié X25519MLKEM768 avec cloudflare.com, google.com, www.amazon.com, www.microsoft.com, www.apple.com et example.com, et du X25519 simple avec github.com et letsencrypt.org.

Vérifier un serveur, un client, un navigateur

Depuis la ligne de commande. La sortie -brief d’OpenSSL 3.5 nomme directement le groupe :

openssl s_client -brief (OpenSSL 3.5.7, 20 octobre 2026)
$ openssl s_client -connect cloudflare.com:443 -servername cloudflare.com -brief </dev/null
Connecting to 104.16.132.229
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=cloudflare.com
Hash used: SHA256
Signature type: ecdsa_secp256r1_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE

Face à un serveur qui choisit un groupe classique, la sortie affiche à la place Peer Temp Key: X25519, 253 bits. Pour savoir si un serveur prend en charge l’hybride, ne proposez rien d’autre. Un serveur uniquement classique fait alors échouer le handshake :

Hybride seul, face à un serveur qui ne le prend pas en charge (abrégé)
$ openssl s_client -groups X25519MLKEM768 -connect github.com:443 -servername github.com -brief </dev/null
...:error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure:...:SSL alert number 40

N’utilisez cette sonde que pour des tests. Un vrai client qui ne propose que l’hybride sacrifie la compatibilité sans rien y gagner. curl compilé avec OpenSSL 3.5 affiche le groupe dans la ligne de connexion de son mode verbeux :

curl -v (curl 8.14.1, OpenSSL 3.5.7)
$ curl -sv -o /dev/null https://www.google.com/ 2>&1 | grep 'SSL connection'
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / id-ecPublicKey

Depuis le code. Go 1.25 et les versions ultérieures exposent le groupe négocié sous la forme ConnectionState().CurveID, et Node.js 24 le renvoie via getEphemeralKeyInfo() :

Go : afficher le groupe négocié
conn, err := tls.Dial("tcp", "cloudflare.com:443", &tls.Config{ServerName: "cloudflare.com"})
if err != nil {
	log.Fatal(err)
}
fmt.Println(conn.ConnectionState().CurveID) // X25519MLKEM768 (Go 1.27.3)
Node.js 24 : afficher le groupe négocié
import tls from "node:tls";
const socket = tls.connect({ host: "cloudflare.com", port: 443, servername: "cloudflare.com" }, () => {
  console.log(socket.getEphemeralKeyInfo()); // { type: 'TLSGroup', name: 'X25519MLKEM768', size: undefined } (Node 24.22.0)
  socket.end();
});

Dans le navigateur. Chrome affiche le groupe dans les DevTools, parmi les détails de connexion du panneau Sécurité. La même valeur est accessible par programmation sous la forme securityDetails.keyExchangeGroup dans l’événement Network.responseReceived du protocole DevTools. Chromium 156 en mode headless a signalé X25519MLKEM768 pour www.cloudflare.com et X25519 pour github.com le 20 octobre 2026. Sous macOS 26, Apple suggère nscurl --tls-diagnostics https://your.host. Pour voir ce que votre navigateur envoie, ouvrez le test TLS post-quantique du navigateur.

L’activer sur votre serveur

Avec OpenSSL 3.5 en dessous, le réglage par défaut est déjà le bon. La façon habituelle de le perdre est une liste de groupes explicite copiée d’un ancien guide de durcissement. Ce test de nginx 1.30.5 sous Debian 13 (OpenSSL 3.5.7) ne modifie que la ligne ssl_ecdh_curve et sonde avec openssl s_client -brief :

nginx 1.30.5 : le groupe suit ssl_ecdh_curve (sortie de la sonde seulement, lignes de journal nginx omises)
--- no ssl_ecdh_curve (OpenSSL default list)
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768
--- ssl_ecdh_curve X25519:prime256v1:secp384r1;
Protocol version: TLSv1.3
Peer Temp Key: X25519, 253 bits
--- ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1;
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

La correction est donc l’une des suivantes : supprimer la ligne, ou y placer X25519MLKEM768 en premier. Les anciennes versions d’OpenSSL ne connaissent pas ce nom : mettez donc d’abord à jour la bibliothèque TLS. nginx 1.24.0 sur OpenSSL 1.1.1w rejette une telle ligne (ici ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;) avec nginx: [emerg] SSL_CTX_set1_curves_list("X25519MLKEM768:X25519:prime256v1") failed. D’autres serveurs suivent le même principe sous d’autres noms : SSLOpenSSLConfCmd Groups dans Apache httpd, curves dans HAProxy, CurvePreferences dans Go, jdk.tls.namedGroups en Java. Si vous avez besoin d’un point de départ pour un serveur précis, le générateur de configurations TLS produit des configurations où les groupes hybrides sont déjà en place.

Deux autres endroits sont faciles à oublier :

  • Le serveur d’origine derrière un CDN ou un répartiteur de charge. La connexion du navigateur s’arrête en périphérie (edge), et la connexion de la périphérie vers votre serveur d’origine est une session TLS distincte, avec sa propre négociation. Le trafic enregistré sur ce second tronçon peut tout autant être récolté.
  • Le TLS interne entre services. Les services Go obtiennent l’hybride grâce à la version de la chaîne d’outils. Les services Java l’obtiennent avec JDK 27, où la JEP 527 a ajouté les groupes hybrides. Tout ce qui fige CurvePreferences, jdk.tls.namedGroups ou une autre liste de groupes explicite n’en profite pas tant que personne ne modifie cette liste.

Prédiction de key share et HelloRetryRequest

Un client TLS 1.3 doit deviner quel groupe le serveur choisira et envoyer d’emblée une key share pour ce groupe. S’il se trompe, le serveur répond par un HelloRetryRequest qui nomme le groupe voulu, et le client recommence. Cela coûte un aller-retour complet. La prédiction compte davantage avec les key shares post-quantiques, car envoyer une key share de 1216 octets « au cas où » n’est pas gratuit. Vous pouvez observer cette relance en demandant à OpenSSL de lister l’hybride, mais de n’envoyer une key share que pour X25519 :

openssl s_client -groups X25519:X25519MLKEM768 -trace (abrégé)
    ClientHello, Length=315
        extension_type=key_share(51), length=38
            NamedGroup: ecdh_x25519 (29)
    ServerHello, Length=84                      <- HelloRetryRequest
        extension_type=key_share(51), length=2
            NamedGroup: X25519MLKEM768 (4588)
    ClientHello, Length=1499
        extension_type=key_share(51), length=1222
            NamedGroup: X25519MLKEM768 (4588)
    ServerHello, Length=1206
Negotiated TLS1.3 group: X25519MLKEM768

Le serveur préférait l’hybride alors que le client avait listé X25519 en premier, il a donc demandé une relance. Dans les chaînes de groupes d’OpenSSL 3.5, un préfixe * marque les groupes qui reçoivent une key share dès le premier envoi. -groups '*X25519:*X25519MLKEM768' envoie les deux key shares, soit un ClientHello de 1535 octets, et évite la relance. Les réglages par défaut des grands clients envoient les deux pour la même raison. Un projet du groupe de travail TLS, draft-ietf-tls-key-share-prediction, permet aux serveurs de publier leurs groupes préférés dans des enregistrements DNS HTTPS, afin que les clients devinent juste du premier coup. Il était encore à l’état d’Internet-Draft en octobre 2026.

Ce qui casse : gros ClientHello et vieux équipements intermédiaires

Le ClientHello mesuré plus haut remplit un enregistrement TLS avec 1551 octets de charge utile. Avec les en-têtes de l’enregistrement, de TCP et d’IP, il ne tient plus dans un seul paquet sur un chemin dont la MTU est, comme souvent, de 1500 octets. La spécification l’a toujours permis, mais certaines implémentations supposaient que le premier read() sur le socket renvoie tout le ClientHello. Elles cassent quand l’hybride le répartit sur deux segments.

Le projet tldr.fail recense cette classe de bugs. Il décrit des serveurs « pas préparés à devoir appeler read() plus d’une fois pour lire le ClientHello entier ». Sa liste comprend des proxys d’inspection TLS, des pare-feu et des répartiteurs de charge de plusieurs fournisseurs. Certains ont depuis été corrigés, d’autres sont marqués comme inconnus. Les symptômes ressemblent à un problème réseau, pas à un problème de cryptographie : ERR_CONNECTION_RESET ou ERR_SSL_PROTOCOL_ERROR dans Chrome, des délais d’attente dépassés pendant le handshake, ou des réinitialisations juste après le ClientHello. Le bug n’est souvent visible que depuis les clients qui envoient l’hybride. Apple met en garde contre la même panne dans ses notes sur les versions 26 : les appareils peuvent ne pas atteindre certains serveurs plus anciens dont le code TLS « n’arrive pas à lire les messages ClientHello volumineux ».

Si vous rencontrez ce problème :

  1. Trouvez l’équipement qui réinitialise la connexion. Comparez openssl s_client -groups X25519 avec la liste de groupes par défaut, par le même chemin. Si seul le gros ClientHello échoue, le problème vient de la taille, pas du certificat.
  2. Corrigez ou remplacez cet équipement. Désactiver l’hybride sur les clients supprime la protection pour chaque connexion, y compris celles qui fonctionnaient.
  3. N’utilisez les réglages côté client qu’en solution provisoire. Go propose GODEBUG=tlsmlkem=0, et les notes de version de Go 1.24 le mentionnent pour les « serveurs TLS bogués qui ne gèrent pas correctement les gros enregistrements ». Chrome avait la politique d’entreprise PostQuantumKeyAgreementEnabled, que Google décrivait comme « une mesure temporaire » qui « sera supprimée quelque temps après la version 145 de Google Chrome ». Vérifiez que votre version de Chrome la respecte encore avant de bâtir un plan dessus.

Les proxys qui terminent TLS appellent une remarque de plus. Un proxy qui déchiffre le trafic effectue deux handshakes. Ce que votre ordinateur portable négocie avec le proxy ne dit rien de la connexion du proxy vers le monde extérieur. Ce tronçon n’est post-quantique que si la pile TLS du proxy elle-même prend en charge et préfère l’hybride.

Ce qu’il ne protège pas

X25519MLKEM768 protège la confidentialité contre un adversaire qui enregistre le trafic maintenant et le déchiffre plus tard. Il ne rend pas post-quantique l’authentification du handshake. Le serveur prouve toujours son identité avec un certificat RSA ou ECDSA et une signature CertificateVerify. Quelqu’un capable de falsifier ces signatures avec un ordinateur quantique pourrait usurper l’identité d’un serveur dans une connexion en direct. Cette menace n’apparaît qu’avec l’existence d’un tel ordinateur : c’est pourquoi l’échange de clés a migré en premier, tandis que les signatures font l’objet d’une migration distincte et plus lente. Les autres articles de cette série couvrent ce travail : les clés et signatures ML-DSA dans ML-DSA côté développeur, et le plan d’ensemble, inventaire et échéances compris, dans Migration post-quantique en 2026.

Il n’apporte rien non plus au trafic qui ne passe pas par TLS 1.3. Les connexions TLS 1.2, les sessions SSH et les tunnels VPN ont leurs propres échanges de clés et leurs propres voies de mise à niveau. Identifiez celles de vos connexions qui négocient encore TLS 1.2 avant de les compter comme protégées.

Sources

Toutes les sorties de commandes de cette page proviennent de conteneurs, le 20 octobre 2026 : OpenSSL 3.5.7, curl 8.14.1, Node.js 24.22.0 et le Python 3 de Debian dans node:24.22.0-trixie, Go 1.27.3 dans golang:1.27.3-trixie, nginx 1.30.5 dans nginx:1.30.5, nginx 1.24.0 avec OpenSSL 1.1.1w dans nginx:1.24.0, et Chromium 156 dans mcr.microsoft.com/playwright:v1.64.1-noble.