
Foto: FlyD / Unsplash (Unsplash License)
Post-quantum crypto w praktyce: ML-KEM krok po kroku
Hybrid ML-KEM-768 + X25519 w nginxie i OpenSSH na Debianie 12.
Dlaczego post-quantum, dlaczego teraz
NIST sfinalizował standardy post-kwantowe w sierpniu 2024 (FIPS 203, 204, 205). ML-KEM (dawniej Kyber) zastępuje klasyczne ECDH w wymianie kluczy, a ML-DSA (Dilithium) - podpisy. W 2026 najwięksi gracze (Cloudflare, Google, AWS) mają już PQ w produkcji, więc czas, żeby polskie zespoły też przestały udawać, że problemu nie ma.
W tym artykule pokażę minimalny przykład wymiany kluczy ML-KEM-768 w Pythonie z użyciem biblioteki pyoqs (wrapper na liboqs 0.10), a potem przejdę do tego, jak włączyć PQ w nginxie i OpenSSH na Debianie.
Threat model: "harvest now, decrypt later"
Klasyczny argument - atakujący zbiera dziś szyfrowany ruch i czeka aż za 10-15 lat duży komputer kwantowy złamie ECDH. Dla danych o długim okresie ważności (medyczne, prawnicze, IP) to realne ryzyko już teraz. Dlatego najpierw migrujemy wymianę kluczy, a podpisy w drugiej kolejności - fałszywy podpis sprzed lat i tak jest bezużyteczny.
Instalacja środowiska
sudo apt install -y cmake ninja-build libssl-dev
git clone --depth 1 https://github.com/open-quantum-safe/liboqs
cd liboqs && mkdir build && cd build
cmake -GNinja -DOQS_BUILD_ONLY_LIB=ON ..
ninja && sudo ninja install
sudo ldconfig
pip install oqs-python==0.10.0
Minimalna wymiana kluczy
from oqs import KeyEncapsulation
ALG = "ML-KEM-768" # NIST level 3, ~AES-192 equivalent
# Strona A (server) generuje parę kluczy
with KeyEncapsulation(ALG) as alice:
public_key = alice.generate_keypair()
# Strona B (client) szyfruje shared secret kluczem publicznym A
with KeyEncapsulation(ALG) as bob:
ciphertext, shared_b = bob.encap_secret(public_key)
shared_a = alice.decap_secret(ciphertext)
assert shared_a == shared_b
print(f"Shared secret length: {len(shared_a)} bytes")
Co jest istotne:
- klucz publiczny ML-KEM-768 ma 1184 bajty, ciphertext 1088 bajtów - to znacznie więcej niż 32 bajty ECDH X25519
- shared secret ma 32 bajty, więc jest drop-in dla istniejących KDF-ów
- operacje są bardzo szybkie (~50 µs na nowoczesnym CPU)
Hybrid mode: nie wyrzucaj ECDH
W produkcji nie zastępujemy klasycznej krypto - łączymy ją z PQ. Jeśli któryś z algorytmów się sypnie, drugi nadal trzyma. nginx 1.27+ z OpenSSL 3.5 obsługuje to natywnie:
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
Po reloadzie sprawdź w Wiresharku rozszerzenie key_share w ClientHello - powinieneś zobaczyć grupę X25519MLKEM768 (kod 0x11ec).
OpenSSH
OpenSSH 9.6+ ma sntrup761x25519-sha512@openssh.com od 2023, ale od wersji 9.8 dostępne jest też mlkem768x25519-sha256. W sshd_config:
KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512@openssh.com,curve25519-sha256
Uwaga: starsze klienty (PuTTY < 0.81, OpenSSH < 9.0) wynegocjują fallback > na curve25519 - to OK podczas migracji, ale po roku należy te dwa ostatnie > usunąć.
Pułapki, na które się nadziałem
- MTU: większy ClientHello (3+ KB) bywa fragmentowany; jeśli widzisz zerwane TLS pod load balancerem, sprawdź
MSS clamping - HSM: większość komercyjnych HSM-ów (luty 2026) nadal nie wspiera ML-KEM w hardware - softwarowy fallback jest OK, ale to nie to samo
- Audyt: liboqs to projekt open-source, nie ma jeszcze FIPS 140-3 validation dla wszystkich kombinacji; jeżeli musisz mieć FIPS, użyj OpenSSL 3.5 z odpowiednim providerem
Co dalej
W kolejnym artykule pokażę, jak przepiąć Wireguard na PQ z użyciem Rosenpass i jak to się skaluje przy 200+ peerach.

// udostępnij
