Projet personnel open source

Implémenter MLS (RFC 9420) en Rust : une messagerie E2E

Ce que j'ai appris en implémentant MLS (RFC 9420) en Rust avec OpenMLS sur Whispee, ma messagerie chiffrée de bout en bout open source.

Mon rôle : Auteur.

Le problème

Le 20 août 2026, premier commit de Whispee : je voulais voir ce que coûte vraiment MLS une fois sorti du RFC. Tu connais le double ratchet de Signal. Il est excellent pour une conversation à deux : chaque message fait tourner les clés, et chaque clé de message est jetée après usage. Le jour où tu veux chiffrer un groupe, tu découvres le prix : ces protocoles sont pairwise, chaque membre entretient une session avec chacun des autres. À dix membres ça passe, à quelques centaines ça ne tient plus.

C’est le point de départ de MLS, le Messaging Layer Security, publié par l’IETF en juillet 2023 sous le numéro RFC 9420. Ses auteurs viennent de Cisco, Inria, Meta, Google et Oxford. Le protocole est conçu pour des groupes de deux à plusieurs milliers de membres, avec forward secrecy et post-compromise security.

Le mécanisme central, c’est un arbre binaire de clés, le ratchet tree ou TreeKEM. Ajouter ou retirer un membre coûte de l’ordre de log N opérations au lieu de N. Et le protocole ne fait pas tout : le RFC 9750, qui décrit l’architecture, délègue à ton application un Delivery Service pour router les messages et un Authentication Service pour lier une identité à une clé publique. Ces deux pièces, c’est toi qui les écris.

L’approche

Whispee est un projet personnel, open source sous AGPLv3 : une messagerie chiffrée de bout en bout construite sur MLS, avec un seul cœur Rust pour le web, le desktop et le mobile. Ce n’est pas un produit client, c’est mon terrain d’expérimentation Rust et crypto, et je le présente comme tel.

Le dépôt compte 434 commits en 18 jours calendaires, du 20 août au 6 septembre 2026, et deux releases, v0.1.0 et v0.2.0.

Ce que MLS a demandé de construireQuatre blocs, du cœur crypto vers les usagesLe cœur MLSOpenMLS et les KeyPackages à usage uniqueLe delivery serviceun serveur aveugle au contenuUn cœur Rust pour toutes les plateformeswasm et Tauri 2Les appels chiffrésLiveKit avec E2EE activé

Le cœur MLS avec OpenMLS

Je n’ai pas réimplémenté le RFC. Le crate crates/crypto-core embarque OpenMLS 0.8.1, la seule implémentation Rust mature que j’aie trouvée, et SECURITY.md le désigne comme « the only production crypto path ». Une réimplémentation pédagogique de X3DH et du double ratchet existe dans crates/ratchet-lab, mais elle n’est jamais importée par du code exécuté en production.

La ciphersuite est fixée à MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519. Ce n’est pas un choix, c’est la seule que le RFC 9420 rend obligatoire, et le commentaire dans identity.rs le dit tel quel. Pour rejoindre un groupe, chaque appareil publie des KeyPackages, à usage unique : la clé d’initialisation est consommée à la première utilisation, et le client maintient un stock qu’il réapprovisionne via replenish_key_packages.

Le point que le code documente le plus sérieusement, c’est la validation d’un KeyPackage reçu. Si un client ne le vérifie pas, un serveur malveillant peut en forger un et usurper une identité. Même chose pour l’état MLS : réutiliser un état hors de son epoch rejoue des clés déjà consommées et détruit la forward secrecy. Ce sont des commentaires dans le code, pas une doc à part, et c’est là qu’ils servent.

Un delivery service qui ne voit rien

Le delivery service, c’est crates/server, en Axum sur PostgreSQL. Il relaie des enveloppes chiffrées et des numéros de séquence, authentifie chaque session par signature, et c’est tout. L’en-tête de gateway.rs le résume : « No content, exactly as with the SSE stream ». Le serveur ne voit jamais un message en clair, ni pour le stocker, ni pour le router.

Pour les frames de signalisation, il vérifie un MAC de groupe : la preuve qu’un membre a émis, sans lier le frame à une identité. C’est un choix de ne pas enregistrer ce que le serveur pourrait pourtant déduire de la session. L’attestation d’appareil, dans crates/attest et server/auth.rs, tient le rôle d’Authentication Service que le RFC 9750 laisse à l’application, pour la liaison entre un appareil et sa clé.

Un cœur Rust pour toutes les plateformes

La question que je voulais trancher : est-ce qu’un seul code MLS peut servir le web, le desktop et le mobile sans trois implémentations ? Chez Whispee, crypto-core, client et wire sont consommés de deux façons. En natif par apps/desktop, une application Tauri 2 qui cible le desktop, Android et iOS depuis un seul tauri.conf.json, avec des workflows CI android.yml et ios.yml pour le prouver.

Et en wasm pour le web : crates/crypto-wasm expose le même cœur via wasm-bindgen, packagé par wasm-pack, chargé par le frontend React/Vite. Il n’existe aucune implémentation JavaScript du protocole dans le dépôt. Une extension navigateur complète le tout. Précision utile : Tauri n’a rien à voir avec wasm, il embarque le Rust natif ; seule la cible web passe par le module wasm.

Des appels chiffrés avec LiveKit

Les appels audio et vidéo passent par WebRTC via un SFU compatible LiveKit, et le chiffrement de bout en bout est activé côté client : ExternalE2EEKeyProvider, un worker e2ee-worker dédié, puis room.setE2EEEnabled(true). Le SFU relaie des frames qu’il ne peut pas lire.

Côté serveur, call.rs mint le token de session avec canPublishData: false, volontairement. Le commentaire l’explique : la conversation a déjà son propre canal chiffré, inutile d’en ouvrir un second à travers le SFU.

Stack et choix techniques

Un workspace Rust de 8 crates : crypto-core, crypto-wasm, client, wire, server, attest, transparency et ratchet-lab. React et Vite pour le web, Tauri 2 pour desktop et mobile, PostgreSQL derrière le serveur. Le tout est public sur github.com/Sycatle/whispee.

La toolchain est épinglée en 1.95.0 dans rust-toolchain.toml, et le commentaire dit pourquoi : un projet qui publie des releases reproductibles et signées ne peut pas accepter qu’un build du même source donne des octets différents deux jours plus tard. Les scripts release.sh et verify-release.sh font partie du périmètre de sécurité déclaré. Pour un projet crypto, Rust est le choix qui me laisse le moins de place à l’à-peu-près : le commentaire d’identity.rs sur le replay d’un état hors epoch est là parce que le compilateur ne l’attrape pas, Rust réduit l’à-peu-près sans le supprimer, et le même code compile vers wasm et vers ARM.

Résultat

Ce sont des chiffres de dépôt, relevés le 20 septembre 2026. Ils disent la taille, la discipline de test et la portée plateforme, pas un usage : Whispee est un projet d’apprentissage et de démonstration.

IndicateurValeur
Commits (20/08 → 06/09/2026)434
Releasesv0.1.0, v0.2.0
Lignes de code Rust28 905
Lignes de code TypeScript49 355
Crates du workspace8
Tests Rust512
Workflows CI4
Plateformes couvertesWeb, desktop, Android, iOS, extension navigateur

À côté du code, le dépôt porte un THREAT-MODEL.md, un SECURITY-PROPERTIES.md et un PROTOCOL.md. Sur un protocole comme MLS, écrire ce que le système promet et ce qu’il ne promet pas m’a pris autant de soin que le code.

Questions fréquentes

MLS (RFC 9420) vs le double ratchet de Signal : quelle différence pour un développeur ?

Le double ratchet est pairwise : une session par paire de membres. MLS maintient un arbre de clés par groupe, avec des epochs successives, et l’ajout ou le retrait d’un membre coûte log N. En contrepartie, tu dois écrire toi-même le delivery service et l’authentification, le RFC 9750 ne te les donne pas.

Qu’est-ce que le delivery service peut voir, concrètement ?

Sur Whispee : des enveloppes chiffrées, des numéros de séquence, des signatures de session et l’appartenance à un groupe. Pas le contenu, et un frame de signal qui n’est pas lié à une identité par le MAC de groupe. Ce que tu laisses voir à ton serveur est une décision d’architecture, pas un détail d’implémentation.

OpenMLS compile-t-il vraiment en wasm ?

Oui. crates/crypto-wasm est un cdylib wasm-bindgen construit sur le même crypto-core que l’application native, et c’est ce binaire que le frontend web charge. Les tests marqués #[wasm_bindgen_test] font partie des 512 du dépôt.

Peut-on utiliser Whispee pour de vrai ?

Non, et le README le dit avant tout le reste : « received no external audit and will not receive one. […] For communications that actually matter: use Signal. » Un protocole E2E correct sur le papier échoue sur des détails que seul un audit fait remonter. Écrire cette phrase en tête de projet fait partie du travail, au même titre que la ciphersuite.


Si ton produit a besoin de chiffrement de bout en bout, d’un cœur Rust partagé entre web, desktop et mobile, ou simplement d’un backend Rust qui tient, c’est ce que je construis en SaaS sur mesure.

Un projet à concrétiser ?

Parlons-en. Je peux t'aider à transformer une idée en produit livré et itéré.

Un appel de 30 minutes, sans engagement. Je réponds sous 24 h. ★ Avis vérifiés sur Trustpilot