















| | | | | Mottó: A déjà vu többnyire hiba a Mátrixban. /The Matrix, 1999/ | | 2026. február 10. | | | | | Post-Quantum Cryptography: ML-DSA, ML-KEM - ezeknél szükséges a "hiba a Mátrixban" | | | | | | FIGYELEM! A jelen bejegyzésben lehetnek még pontatlanságok. Ezeket igyekszem javítani, ha érkeznek visszajelzések!
Senki nem tudja, hogy mikor jelennek meg - nyilvánosan, vagy akár a háttérben, titokban - az olyan kvantumszámítógépek, amelyek veszélyt jelentenek a jelenleg használt kriptográfiára. Szinte naponta kapunk újabb és újabb híreket arról, hogy mennyi fizikai és mennyi logikai qubitnél tartanak, illetve, hogy milyen kvantumhiba-javító algoritmusok, optimalizációk érhetők el. Így viszont valóban nehéz azt megmondani, hogy 5 év múlva vagy 50 év múlva érkezik-e el az idő. Azt viszont fontos látni, hogy előbb-utóbb elérkezik ez az idő és - még ha az aláírások, felülhitelesítések kapcsán rá is érünk az utolsó előtti pillanatig - a rejtjelezés, titkosítás igazából már a mai napon igényelné a váltást. Sokszor lehet hallani a "harvest/store now, decrypt later" szállóigét, ami már előrevetíti, hogy aki nem vált minél hamarabb, arról annál több dolog kerül(het)/derül(het) majd ki egy esetleges post-quantum WikiLeaks oldalon.

Hol tartunk most? Utoljára az Ibtv. (2013. évi L. törvény) kapcsán foglalkoztam a témával (amibe 2022-07-01-én váratlanul bekerült a "poszt-kvantumtitkosítás" fogalom, és aztán azt átvette a 2024. évi LXIX. törvény is), azóta viszont - 2024-08-13-án - megjelentek a NIST FIPS specifikációk és szépen halad az adatstruktúrák kidolgozása is. Halad, de sok esetben még nem véglegesedett. Éppen ezért a jelen összefoglalóban maradnék a NIST FIPS által leírtak bemutatásánál:HASH-BASED A NIST szabványok között lenyomat-alapú (hash-based) algoritmus csak egy található. Ennél kizárólag a hash-képzés a mögöttes logika, amit nem érint végzetesen a kvantumszámítógép. És mivel csak hash-képzés van (azaz nem kell pl. prímtényezőket keresgélni, mint RSA törésénél), emiatt nem is kell aggodalmaskodni, mint akár a rácsoknál (lattice) hogy pl. miért pont olyan konstansokat állított be az NSA NIST, amilyeneket?! Cserébe viszont - sajnos -, csak aláírásra lehet használni... a rejtjelezéshez, titkosításhoz másra van szükség.
a) NIST FIPS 205 - SLH-DSA (SPHINCS+) A NIST FIPS 205 a lenyomat-alapú (hash-based) aláírásról szól.
// key generation openssl genpkey -algorithm slh-dsa-sha2-256f -out PQC_secret_key_end_signature.txt -outpubkey PQC_public_key_end_signature.txt openssl req -new -key PQC_secret_key_end_signature.txt -out PQC_public_key_certificate_request_end_signature.txt -subj "/CN=Aron Szabo" openssl x509 -in PQC_public_key_certificate_request_end_signature.txt -out PQC_public_key_certificate_end_signature.crt -req -CAkey PQC_secret_key_intermediate.txt -days 3652 -extfile test_end_signature.ext -sha256 -CA PQC_public_key_certificate_intermediate.crt -CAcreateserial openssl pkcs12 -export -in PQC_public_key_certificate_end_signature.crt -inkey PQC_secret_key_end_signature.txt -out PQC_secret_keystore_encrypted_end_signature_1234.p12 // signature creation openssl pkeyutl -sign -in message.txt -out signature.txt -keyform PEM -inkey PQC_secret_key_end_signature.txt // signature verification openssl pkeyutl -verify -in message.txt -sigfile signature.txt -pubin -keyform PEM -inkey PQC_public_key_end_signature.txt
A teszt adatok létrehozásához meg kellett adni az algoritmust és annak paramétereit. Ezek határozzák meg a biztonsági szintet, a kulcsméreteket, illetve a kódolt adat (aláírás) méretét.
SLH-DSA-SHA2/SHAKE-128s/f - NIST Security Level 1 (~ AES-128) SLH-DSA-SHA2/SHAKE-192s/f - NIST Security Level 3 (~ AES-192) SLH-DSA-SHA2/SHAKE-256s/f - NIST Security Level 5 (~ AES-256) strength | secret key | public key | signature | hash | small/fast ------------+------------+------------+-------------------+------------+----------- SLH-DSA-128 | 64 bytes | 32 bytes | 7856/17088 bytes | SHA2/SHAKE | s/f SLH-DSA-192 | 96 bytes | 48 bytes | 16224/35664 bytes | SHA2/SHAKE | s/f SLH-DSA-256 | 128 bytes | 64 bytes | 29792/49856 bytes | SHA2/SHAKE | s/f
Az OpenSSL parancsoknál az slh-dsa-sha2-256f került megadásra, így a legerősebb, 128 byte (1024 bit) méretű titkos kulcs és 64 byte (512 bit) méretű nyilvános kulcs, illetve 49856 byte (398848 bit) méretű kódolt adat (aláírás) jött létre. Érdemes hozzátenni, hogy ez a gyors (fast) változatra igaz. A méretek nagysága talán akkor látható a legjobban, ha mellettük megemlítjük, hogy a jelenleg használt ECC-alapú aláírás - NIST P-256 görbés ECDSA esetén - csak 64 byte (512 bit), az RSA-alapú pedig 384 byte (3072 bit). A "quantum-safe" algoritmusok közül a legkisebb méretű aláírást a FALCON algoritmussal lehet létrehozni, de a legalacsonyabb biztonsági szinten az is 666 byte (5328 bit), a legmagasabbon pedig 1280 byte (10240 bit).
  



Fejlesztői, üzemeltetői szemmel nézve... Korábban - jóval korábban, amikor még nem voltak ezek szabványosítva - már foglalkoztam a hash-alapú aláírás témával: a 2015-ös Hacktivity konferencián mutattam be egy ilyen algoritmus - a Lamport-Diffie-Winternitz-Merkle (LDWM) - működését, saját kódokkal. Ez ma már Leighton-Micali Hash-Based Signatures név alatt érhető el az IETF RFC 8554 specifikációban. A hash-alapú aláírásokra általánosságban az jellemző, hogy a tanúsítványuk, pontosabban a kulcsuk lejárata nem időhöz kötődik, hanem tranzakciószámhoz. Ennek az az oka, hogy egy - nagy, de akkor is - korlátos számú esetben lehet csak használni egy adott kulcsot. A humán folyamatokhoz ez is bőven elegendő lehet, a gépi folyamatoknál viszont a korábban megszokottakhoz képest más üzemeltetést, odafigyelést igényel(ne). De ha még ezt a - tranzakciószámra vonatkozó - nagyon magas határértéket nem is nézzük (hiszen elvileg pont ettől jobb a "stateless" megoldás, mint a "stateful"), a hatalmas méretű aláírások is korlátozzák az alkalmazhatóságát, főleg IoT környezeteknél (NIST Security Level 5 szinten az aláírás lehet akár 49856 byte is, ami jóval több, mint a legerősebb ECDSA-nál a 132 byte). A méret mellett pedig az aláírás létrehozásához és ellenőrzéséhez szükséges idő is nagyobb, hiszen itt hash-láncokat kell végigszámolgatni. Egy átlagos számítógépen - Intel Core i7 vagy AMD Ryzen 7 - az SLH-DSA-SHA2-256s változat esetén egy tranzakció időtartama lehet akár 5000 ms is. Ezen okok miatt valószínűleg nem is ez a megoldás lesz a nyerő pl. egy Apache web server SSL/TLS csatorna felépítésénél, ahol a háttérben aláírási műveleteket is végre kell hajtani. A hash-alapú aláírásoknak azonban óriási előnye az, hogy nem kell aggodalmaskodni a mögöttes matematikán, azaz azon, hogy az NP-nehéz problémára vajon mikor találnak törést. Itt ugyanis kizárólag az egyirányú hash-függvények állnak a háttérben. Éppen emiatt a magas biztonság miatt a nem időkritikus és nem méretkritikus esetekben ajánlott a használatuk: nem csak humán folyamatoknál, de akár code signing műveletekhez, illetve archivált adatok hitelességének megőrzéséhez is jó lehet.
LATTICE-BASED A NIST szabványok között rács-alapú (lattice-based) algoritmus kettő található. Ezeknek a matematikai nehézséget az adja, hogy sok dimenziós térben kell egy ponthoz megtalálni a legközelebbit. A rácspontról ugyanis a hozzáadott hiba/zaj miatt "leugrik" a pont és csak az képes gyorsan megtalálni/visszafejteni az eredeti értéket, aki ismeri a titkos kulcsot, amivel a hiba javítható. Mindenki más számára - legyen szó hagyományos vagy akár kvantumszámítógépről - a hiba javítása és a legközelebbi rácspont megtalálása matematikailag nehéz feladat a Closest Vector Problem (CVP) miatt, ami visszavezethető a Shortest Vector Problem (SVP) problémára. A rács-alapú algoritmusoknál tehát más értelmet nyer az, hogy van "hiba a Mátrixban": itt pont a szándékosan elhelyezett, titkos hiba az, ami biztosítja a rendszerben az adatok bizalmasságát, sértetlenségét, hitelességét. Ennek a matematikai okaival a matematikusok foglalkoznak (Kutas Péter publikációiról, illetve Hornyai Alex magyarázó előadásáról beraktam linket alább, de amúgyis érdemes tudni, hogy a lattice-based cryptography témakörnél "magyarok vannak a rácsok mögött": Lovász László, Ajtai Miklós, Babai László), engem - rendszertervezőként, fejlesztőként - inkább az érdekel, hogy mi a folyamata a kulcsok generálásának, az aláírási/rejtjelezési műveletek elvégzésének és ezekhez milyen paramétereket kell használni és azoknál mikre kell figyelni. Ehhez pedig először érdemes valamilyen segédalkalmazással legyártani a teszt adatokat aztán pedig végig kell bogarászni a NIST leírásokat, a könnyebb érthetőség érdekében pedig kis számokkal végiglépkedni a folyamatokon. Szerencsére, az OpenSSL 3.5.0+ változatai - mint a Java 24, Bouncy Castle 1.81 vagy .NET 10.0 rc1 - már beépítve tartalmazzák a PQC algoritmusokat (nem szükséges az oqs-provider kódjait becsatolni), így a megszokott parancsokkal pár lépésben el tudjuk végezni a tanúsítványok - Root CA, Intermediate CA és a végfelhasználói tanúsítványok - kiadását és az aláírási/rejtjelezési műveleteket.
a) NIST FIPS 204 - ML-DSA (CRYSTALS-DILITHIUM) A NIST FIPS 204 a rács-alapú (lattice-based) aláírásról szól.
// key generation openssl genpkey -algorithm mldsa87 -out PQC_secret_key_end_signature.txt -outpubkey PQC_public_key_end_signature.txt openssl req -new -key PQC_secret_key_end_signature.txt -out PQC_public_key_certificate_request_end_signature.txt -subj "/CN=Aron Szabo" openssl x509 -in PQC_public_key_certificate_request_end_signature.txt -out PQC_public_key_certificate_end_signature.crt -req -CAkey PQC_secret_key_intermediate.txt -days 3652 -extfile test_end_signature.ext -sha256 -CA PQC_public_key_certificate_intermediate.crt -CAcreateserial openssl pkcs12 -export -in PQC_public_key_certificate_end_signature.crt -inkey PQC_secret_key_end_signature.txt -out PQC_secret_keystore_encrypted_end_signature_1234.p12 // signature creation openssl pkeyutl -sign -in message.txt -out signature.txt -keyform PEM -inkey PQC_secret_key_end_signature.txt // signature verification openssl pkeyutl -verify -in message.txt -sigfile signature.txt -pubin -keyform PEM -inkey PQC_public_key_end_signature.txt
A teszt adatok létrehozásához meg kellett adni az algoritmust és annak paramétereit. Ezek határozzák meg a biztonsági szintet, a kulcsméreteket, illetve a kódolt adat (aláírás) méretét.
mldsa44 = ML-DSA-44 - NIST Security Level 2 (~ AES-128) mldsa65 = ML-DSA-65 - NIST Security Level 3 (~ AES-192) mldsa87 = ML-DSA-87 - NIST Security Level 5 (~ AES-256) strength | secret key | public key | signature ------------+------------+------------+----------- ML-DSA-44 | 2560 bytes | 1312 bytes | 2420 bytes ML-DSA-65 | 4032 bytes | 1952 bytes | 3309 bytes ML-DSA-87 | 4896 bytes | 2592 bytes | 4627 bytes
Az OpenSSL parancsoknál az mldsa87 került megadásra, így a legerősebb, 4896 byte (39168 bit) méretű titkos kulcs és 2592 byte (20736 bit) méretű nyilvános kulcs, illetve 4627 byte (37016 bit) méretű kódolt adat (aláírás) jött létre.
  



public key (2592 bytes) = (32 bytes public seed for A (rho) || 2560 bytes part of public value (t1, HighBits)) secret key (32 bytes) + (4896 bytes) = (32 bytes master seed (zeta)) + (32 bytes public seed for A (rho) || 32 bytes secret seed for K || 64 bytes public key hash (tr) || 2016 bytes part of public key (t0, LowBits) || 1280 bytes secret value (s1) || 1472 bytes secret noise/error (s2)) signature (4627 bytes) = (64 bytes challenge (c) || 4480 bytes encoded hash (z) || 83 bytes Hint vector (h))
Vessük össze a legyártott teszt adatokat a NIST FIPS 204 specifikációban leírtakkal és nézzük meg, hogy ezekkel miképp lehet aláírást létrehozni és ellenőrizni! Az alábbi példánál kis számok szerepelnek, de ahol lehetett, ott megadtam a szabványban szereplő - mldsa87 szintre vonatkozó - értékeket is.
| // key generation | n = 1 | // public constant polynomial degree // mldsa87 n = 256 | q = 23 | // public constant modulus // mldsa87 q = 8380417 = 2^23 - 2^13 + 1 | d = 13 | // public constant scaling factor for compression of public key (d least significant bits, dropped from t) in Power2Round() for HighBits and LowBits // mldsa87 d = 13 | h = 83 bytes | // public constant Hint vector // mldsa87 h = 75 bytes concatenated index bytes of polynomials + 8 bytes ending index of each polynomial | A = [[6], [5]] | // public matrix, calculated (rank k, column l) // mldsa87 k = 8, l = 7 | s1 = 2 | // secret value, calculated // mldsa87 s1 = 1280 bytes (part of 4896 bytes) // secret key = (32 bytes master seed (zeta)) + (32 bytes public seed for A (rho), 32 bytes secret seed for K, 64 bytes public key hash (tr), 2016 bytes part of public key (t0), (s1), (s2)) | s2 = [[1], [2]] | // secret noise/error, calculated // mldsa87 s2 = 1472 bytes (part of 4896 bytes) // secret key = (32 bytes master seed (zeta)) + (32 bytes public seed for A (rho), 32 bytes secret seed for K, 64 bytes public key hash (tr), 2016 bytes part of public key (t0), (s1), (s2)) | t = [[13], [12]] | // public value, calculated // mldsa87 t = 2560 bytes (part of 2592 bytes) // public key = (32 bytes public seed for A (rho), 2560 bytes part of public value (t1, HighBits)) // t1 = ((A1 x s1) + s2) mod q = 6 x 2 + 1 mod 23 = 13 // t2 = ((A2 x s1) + s2) mod q = 5 x 2 + 2 mod 23 = 12 | | // signature creation | M' = .. | // public message // mldsa87 M' = 0x01 || 1 byte length(ctx) || ctx || OID || H(M) | y = 10 | // secret ephemeral random masking value (with 16 bits Kappa nonce/counter) // mldsa87 y = ExpandMask(SHAKE-256(K || 32 bytes 0x00/random || H(tr || M')), Kappa) | w = [[14], [4]] | // secret ephemeral commitment, calculated, truncated (2^d HighBits) // mldsa87 w = A x y mod q // w1 = A1 x y mod q = 6 x 10 mod 23 = 14 // w2 = A2 x y mod q = 5 x 10 mod 23 = 4 | c = 3 | // public challenge, calculated // mldsa87 c = H(H(tr + M') || HighBits(w)) | z = 16 | // public value, calculated // mldsa87 z = 4480 bytes (part of 4627 bytes) // signature = 4627 bytes = (64 bytes challenge (c), 4480 bytes encoded hash (z), 83 bytes Hint vector (h)) // z = y + c x s1 = 10 + 3 x 2 = 16 | | // signature verification | w' = [[11], [21]] | // public ephemeral commitment, calculated, truncated (2^d HighBits) // mldsa87 w' = (A x z - c x t) mod q // w1' = (A1 x z - c x t1) mod q = (6 x 16 - 3 x 13) mod 23 = 11 // w2' = (A2 x z - c x t2) mod q = (5 x 16 - 3 x 12) mod 23 = 21 | c' = 3 | // public challenge, calculated, corrected and truncated (Hint vector and return HighBits) // mldsa87 c' = H(H(tr + M') || UseHint(h, w')) | c' ? c | // comparison, equals? true/false // mldsa87 probability of meeting all acceptance criteria is 15%-25%, so 4-7 re-calculations (with new y) are required in general |
Fejlesztői, üzemeltetői szemmel nézve... Az első dolog, ami feltűnik, hogy egy - az összes "jósági" feltételnek megfelelő - aláírás létrehozása valószínűleg többszöri lefutásra lehetséges csak (pl. a tapasztalatok alapján ML-DSA-87 esetében átlagosan 4-7 iteráció is szükséges lehet). Vannak biztonsági szempontok is (ld. "rejection sampling" és a méretkorlátok), de a lényeg, hogy a "segítő" bitek (Hint vector) révén az ellenőrzés során kiszámolt, zajos w' értéket ki kell hozni úgy, hogy azonos legyen a létrehozás során számolt w értékkel. Ha csak kicsit is eltérnek, akkor ezek lenyomata már teljesen más lesz (a hash-függvények lavina-hatása miatt). Az eltérés emiatt hibát jelent, így újra meg kell próbálni az aláírás létrehozását egy következő iterációban. Ha tehát majd performancia értékeket nézünk egy pl. smart card vagy HSM kulcstárolónál, akkor valószínűleg másképp kell értelmezni a megadott számokat.
A második dolog, hogy még ha ugyanaz is az összes paraméter, kulcs, sőt, még az üzenet is, akkor a tranzakció egyediségét végső soron az y kiszámításához használt Kappa számláló adja (ami mindig 0-ról indul), illetve a 32 byte "töltelék". A NIST FIPS 204 megengedi, hogy ez a "töltelék" rögzítetten csak 32 byte 0x00 legyen ("deterministic"), de jobb esetben ez véletlenszerű adattal van feltöltve ("hedged"). Ez - többek között - azt is befolyásolja, hogy ha ugyanazt az M' üzenetet ugyanazzal a kulccsal írjuk alá, akkor ugyanaz lesz-e z aláírás értéke is vagy sem. A PKCS#1 padding-es RSA-alapú aláírásoknál ez így volt korábban is ("deterministic"), de az ECC-alapú ECDSA aláírásoknál már ez nem volt igaz, hiszen ott aláírásonként is került be véletlenszám ("hedged"). A hibakeresésnél okozhat nehézséget, ha mindig más aláírás-értéket kapunk ugyanazon bemenő adatok esetén, de a nagyobb biztonság érdekében ezt be kell vállalni (ahogy az ECDSA aláírásoknál is ezt már korábban megszoktuk).
A harmadik dolog is kapcsolódik ehhez az y értékhez. Fontos látni az y képletében, hogy az felhasználja az adott M' üzenet lenyomatát. Ez miért érdekes? Azért érdekes, mert ha y csak egy - az adott M' üzenet lenyomatától független - hagyományos véletlenszám lenne, akkor érzékeny lenne a "nonce reuse" támadásra, amiről már pl. az ECDSA és a Sony PlayStation 3 kapcsán sokszor hallottunk. (És ez az, ami miatt pl. egy Thales HSM kulcstárolónál meg kell venni a kibővített licenszt és be kell kapcsolni az emelt szintű entrópia használatát, ha egyébként nem támogatja az IETF RFC 6979 szabványban leírt "deterministic" megoldást.) Annak a lényege, hogy két eltérő M' üzenet, és két azonos y véletlenszám esetén két eltérő aláírás keletkezik és ezek különbségéből kifejezhető az s1 titkos kulcs. Az y képletéből viszont látszik, hogy két eltérő M' üzenet esetén nem lehet két azonos y véletlenszám, sem "hedged", sem "deterministic" esetben. De... Az y kiszámítása csak az aláírás létrehozásánál számít, az aláírás ellenőrzésénél (pl. az ellenőrző oldalán futó másik kódnál) nem történik meg. Ez azt jelenti, hogy lehet azzal trükközni, hogy egy kendácsolt 3rd party library suttyomban - egyáltalán nem NIST FIPS 204 szabványos módon - csak egy gyenge minőségű véletlenszámot generál az y változónak. Érdemes lesz tehát az auditált kriptográfiai megoldásoknál maradni - legalább a forráskód-szintű, pl. Common Criteria EAL4 tanúsítottaknál -, amik ellenőrzötten jól - NIST FIPS 204 alapján - számítják ki az y értékét! Így lehet elkerülni, hogy a Sony PlayStation 3-hoz hasonló módon kiszivárogjon a titkos kulcsunk az alábbi képlet megoldásával:
z1 = y + c1 x s1 z2 = y + c2 x s1 (z1 - z2) = (y + c1 x s1) - (y + c2 x s1) s1 = (z1 - z2)/(c1 - c2)
A negyedik dolog is a performancia értékekhez kapcsolódik. Legalábbis, az EJBCA fejlesztők (Keyfactor/PrimeKey) által végzett teszteknél kiderült, hogy jelentős különbség van a teljesítményekben a különböző HSM kulcstárolóknál attól függően, hogy a teljes üzeneten végzett lenyomatképzés belül - a HSM-ben - vagy kívül - a környezetben - hajtódik-e végre ML-DSA esetén. Ez elsőre furcsán hangzik, hiszen az RSA és ECDSA aláírásoknál mindig csak a lenyomat került elküldésre a kulcstároló felé, azaz még a környezet számolta ki azt. És ennek részben pont az volt az oka, hogy egy pl. több GB-os videó állományt nagyon lassan lehetett volna áttolni egy kis chipkártyán. De még egy HSM is megizzadt volna vele. Ezen felül van azonban egy másik ok is, amiért ez nem így volt: a jogi oldal. Vannak olyan szektorok, ahol az érzékeny adatok nem kerülhetnek ki a felhasználó környezetéből - pl. banktitok vagy ügyvédi titok miatt, de akár még a GDPR-t is emlegethetném - és ilyenkor csatolt aláírást kell létrehozni. Ez jellemzően úgy történik ilyenkor, hogy még az ügyfél oldalán - pl. a banki dokumentumkezelő rendszerben - készül el a lenyomat és a 3rd party aláírás-szolgáltató a megkapott lenyomatot írja alá (azaz nem a teljes dokumentumot), majd ezt küldi vissza a banknak. De, ha ehelyett be kell küldeni a teljes aláírandó adatot a kulcstárolóba - a HSM-be vagy akár a kis chipkártyába -, akkor az egyrészt baromi lassúvá teszi a folyamatot (és emiatt az ügyfél elégedetlen lesz), másrészt sérülnek a titoktartási kötelezettségek (és emiatt az ügyfél még elégedetlenebb lesz). Éppen ezért furcsa számomra, hogy egyáltalán létezik ez az "external mu" témakör (és nem alapértelmezett ez a működési mód), ahol egyébként a "mu" értéke nem más, mint a nyilvános kulcs lenyomatának és az üzenet lenyomatának (meg még néhány adatnak) az összefűzése és ennek az egésznek a lenyomata. Ez szükséges az y kiszámításához is: H(tr || M')), ami jobban kifejtve HASH(HASH(public key) || .. || HASH(message)). Az "external mu" fontosságát mutatja, hogy külön oszlopban szerepel a HSM-ek táblázatában a Keyfactor/PrimeKey oldalán. Néhányan ugyan támogatják ezt a funkciót (mint az i4p Trident HSM), de a táblázat készítésekor a Thales Luna még nem tudta (csak az újabb változatoknál írnak erről). Erre a paraméterre tehát érdemes figyelni, amikor valaki rendszert tervez és a jogi követelményeknek, illetve a performancia kritériumoknak meg kell felelni!
b) NIST FIPS 203 - ML-KEM (CRYSTALS-KYBER) A NIST FIPS 203 a rács-alapú (lattice-based) (kulcs)rejtjelezésről szól.
// key generation - note: .csr can not be signed by encryption secret key openssl genpkey -algorithm mlkem1024 -out PQC_secret_key_end_encryption.txt -outpubkey PQC_public_key_end_encryption.txt openssl x509 -new -force_pubkey PQC_public_key_end_encryption.txt -out PQC_public_key_certificate_end_encryption.crt -subj "/CN=Aron Szabo" -CAkey PQC_secret_key_intermediate.txt -days 3652 -extfile test_end_encryption.ext -sha256 -CA PQC_public_key_certificate_intermediate.crt -CAcreateserial openssl pkcs12 -export -in PQC_public_key_certificate_end_encryption.crt -inkey PQC_secret_key_end_encryption.txt -out PQC_secret_keystore_encrypted_end_encryption_1234.p12 // data encryption openssl pkeyutl -encap -inkey PQC_public_key_end_encryption.txt -secret shared_secret_generated.txt -out shared_secret_encrypted.txt -pubin // data decryption openssl pkeyutl -decap -inkey PQC_secret_key_end_encryption.txt -secret shared_secret_decrypted.txt -in shared_secret_encrypted.txt
A teszt adatok létrehozásához meg kellett adni az algoritmust és annak paramétereit. Ezek határozzák meg a biztonsági szintet, a kulcsméreteket, illetve a kódolt adat (rejtjel) méretét.
mlkem512 = ML-KEM-512 - NIST Security Level 1 (~ AES-128) mlkem768 = ML-KEM-768 - NIST Security Level 3 (~ AES-192) mlkem1024 = ML-KEM-1024 - NIST Security Level 5 (~ AES-256) strength | secret key | public key | ciphertext | shared ------------+------------+------------+------------+--------- ML-KEM-512 | 1632 bytes | 800 bytes | 768 bytes | 32 bytes ML-KEM-768 | 2400 bytes | 1184 bytes | 1088 bytes | 32 bytes ML-KEM-1024 | 3168 bytes | 1568 bytes | 1568 bytes | 32 bytes
Az OpenSSL parancsoknál az mlkem1024 került megadásra, így a legerősebb, 3168 byte (25344 bit) méretű titkos kulcs és 1568 byte (12544 bit) méretű nyilvános kulcs, illetve 1568 byte (12544 bit) méretű kódolt adat (rejtjel) jött létre.
  



public key (1568 bytes) = (1536 bytes public value (t) || 32 bytes public value (rho)) secret key (64 bytes) + (3168 bytes) = (32 bytes master seed (d) || 32 bytes rejection seed (z)) + (1536 bytes secret value (s) || 1568 bytes public key (ek) || 32 bytes public key hash || 32 bytes rejection seed (z)) ciphertext (1568 bytes) = (1408 bytes (c1) || 160 bytes (c2))
Vessük össze a legyártott teszt adatokat a NIST FIPS 203 specifikációban leírtakkal és nézzük meg, hogy ezekkel miképp lehet rejtjelezett kulcsot létrehozni és egyeztetni! Az alábbi példánál kis számok szerepelnek, de ahol lehetett, ott megadtam a szabványban szereplő - mlkem1024 szintre vonatkozó - értékeket is.
| // key generation | n = 1 | // public constant polynomial degree // mlkem1024 n = 256 | q = 11 | // public constant modulus // mlkem1024 q = 3329 = 2^8 x 13 + 1 | d = 32 bytes | // secret master seed, calculated // mlkem1024 d = 32 bytes | z = 32 bytes | // secret rejection seed, calculated // mlkem1024 z = 32 bytes | rho = 32 bytes | // public seed for A, calculated // mlkem1024 rho = 32 bytes | A = [[2, 3], [1, 5]] | // public matrix, calculated (rank k, column k) // mlkem1024 k = 4 | s = [[1], [2]] | // secret value, calculated // mlkem1024 s = 1536 bytes (part of 3168 bytes) // secret key = (32 bytes master seed (d), 32 bytes rejection seed (z)) + (1536 bytes secret value (s), 1568 bytes public key (ek), 32 bytes public key hash, 32 bytes rejection seed (z)) | e = [[1], [0]] | // secret noise/error, calculated // mlkem1024 e <- H(d) | t = [[9], [0]] | // public value, calculated // mlkem1024 t = 1536 bytes = A x s + e mod q // public key = (1536 bytes public value (t), 32 bytes public value (rho)) // t1 = (2 x 1 + 3 x 2) + 1 mod 11 = 9 // t2 = (1 x 1 + 5 x 2) + 0 mod 11 = 0 | | // data encryption/encapsulation | m = 1 | // secret message, calculated // mlkem1024 m = 32 bytes | K = 32 bytes key | // secret shared secret key, calculated // mlkem1024 (K, r) <- G(m || H(ek)) | r = [[1], [0]] | // secret random seed, calculated // mlkem1024 (K, r) <- G(m || H(ek)) | y = [[1], [0]] | // secret random, calculated // mlkem1024 y <- H(r) | e1 = [[0], [1]] | // secret random, calculated // mlkem1024 e1 <- H(r) | e2 = 1 | // secret random, calculated // mlkem1024 e2 <- H(r) | u = [[2], [4]] | // public value, calculated // mlkem1024 u = A^T x y + e1 // u1 = 2 x 1 + 1 x 0 + 0 = 2 // u2 = 3 x 1 + 5 x 0 + 1 = 4 | dcomp = 1 | // public constant (for Compress(), Decompress()) // mlkem1024 dcomp < 12 | mu = 6 | // secret value, calculated // mlkem1024 mu = Decompress(m) = round((q / 2^dcomp) x (m)) // mu = round((11 / 2^1) x 1) = 6 | v = [[5]] | // public value, calculated // mlkem1024 v = t^T x y + e2 + mu mod q // v1 = 9 x 1 + 0 x 0 + 1 + 6 mod 11 = 5 | c1 = [[0], [1]] | // public value, calculated // mlkem1024 c1 = Compress(u) = round((2^dcomp / q) x (u)) mod 2^dcomp // c1u1 = round((2^1 / 11) x 2) mod 2^1 = 0 // c1u2 = round((2^1 / 11) x 4) mod 2^1 = 1 | c2 = [[1]] | // public value, calculated // mlkem1024 c2 = Compress(v) = round((2^dcomp / q) x (v)) mod 2^dcomp // c2v1 = round((2^1 / 11) x 5) mod 2^1 = 1 | c = (c1 || c2) | // public value, calculated // mlkem1024 c = 1568 bytes = (c1 || c2) // ciphertext = (1408 bytes (c1), 160 bytes (c2)) | | // data decryption/decapsulation | u' = [[0], [6]] | // public value, calculated // mlkem1024 u' = Decompress(c1) = round((q / 2^dcomp) x (c1)) // c1u1' = round((11 / 2^1) x 0) = 0 // c1u2' = round((11 / 2^1) x 1) = 6 | v' = [[6]] | // public value, calculated // mlkem1024 v' = Decompress(c2) = round((q / 2^dcomp) x (c2)) // c2v1' = round((11 / 2^1) x 1) = 6 | w = [[5]] | // secret value, calculated // mlkem1024 w = v' - s^T x u' mod q // w = 6 - ((1 x 0 + 2 x 6)) mod 11 = 5 | m' = 1 | // secret value, calculated // mlkem1024 m' = Compress(w) = round((2^dcomp / q) x (w)) mod 2^dcomp // m' = round((2^1 / 11) x 5) mod 2^1 = 1 | K' = 32 bytes key | // secret shared secret key, calculated // mlkem1024 (K', r') <- G(m' || H(ek)) | c1' = [[0], [1]] | // public value, calculated // mlkem1024 c1' = Compress(u') = round((2^dcomp / q) x (u')) mod 2^dcomp // c1u1' = round((2^1 / 11) x 0) mod 2^1 = 0 // c1u2 = round((2^1 / 11) x 6) mod 2^1 = 1 | c2' = [[1]] | // public value, calculated // mlkem1024 c2' = Compress(v') = round((2^dcomp / q) x (v')) mod 2^dcomp // c2v1' = round((2^1 / 11) x 6) mod 2^1 = 1 | c' = (c1' || c2') | // public value, calculated // mlkem1024 c' = (c1' || c2') | c' ? c | // comparison, equals? true/false // mlkem1024 re-encryption check ensures that ciphertext was not tampered and K' = K can be used as shared secret key |
Fejlesztői, üzemeltetői szemmel nézve... Az első dolog, amit érdemes megemlíteni, hogy az ML-KEM egy "deterministic" lefutású algoritmus: azonos m üzenet - és azonos ek nyilvános kulcs - adatok esetén azonos K "shared secret key" - és azonos r véletlenszám - jön létre. A többi lépésnél/rétegben van ugyan "randomness", de eddig a pontig ugyanúgy kell tudni elvinni a folyamatot. Ennek oka az, hogy a "re-encryption" visszaellenőrzésnél ugyanazt az adatot kell tudni előállítani, így titkos véletlenszámokat - vagy egyéb nem "deterministic" adatokat - nem lehet használni, nyilvános véletlenszámra meg nincs szükség eddig a pontig.
A második dolog, ami gyakran elhangzik, hogy az ML-KEM egy kizárólag kulcsrejtjelező algoritmus, nem pedig egy aszimmetrikus rejtjelező algoritmus. És valóban: az m üzenet neve ugyan arra utalhat, hogy esetleg itt lehet megadni a nyílt szöveget, de valójában ez egy titkos véletlenszám, amiből - lenyomatképzés révén - közvetlenül áll elő az egyeztetendő adatrejtjelező (pl. AES) kulcs. (Kell is hozzá a jó entrópia, de kérdés, hogy az alsóbb rétegekben tud-e pl. /dev/qrandom vagy haveged/HAVEGE - HArdware Volatile Entropy Gathering and Expansion - modult használni forrásként?) Ezzel szemben az RSA képes akár közvetlenül is rejtjelezni a nyílt szöveget, de... Tény, hogy az esetek túlnyomó többségében ez nem így történik, hanem hybrid módon: az RSA is csak az AES kulcsot rejtjelezi és az AES fogja a nyílt szöveget rejtjelezni. Ilyen szempontból tehát mindegy, hogy az ML-KEM tud-e közvetlenül nyílt szöveget rejtjelezni vagy sem, mert általában erre nincs is szükség. Ha pedig valakinek mégis szüksége van rá, akkor jó hír, hogy néhány módosítás - "Fujisaki-Okamoto (FO) transform" eltávolítása - révén akár még erre is jó (bár, a ténylegesen biztonságos alkalmazhatósága még erősen kutatási fázisban van), úgyhogy alkalmas lehet "homomorphic encryption" funkció megvalósítására is pl. e-voting rendszereknél vagy statisztikai célú, egészségügyi adatok feldolgozásánál. Itt fontos megemlíteni, hogy a NIST ugyan nem tervezett foglalkozni a témával, azonban az ISO szabványosító szervezetnél megszületett ISO/IEC DIS 28033-3 név alatt egy "quantum-safe" megoldás, a CKKS (Cheon, Kim, Kim, Song) modellen alapuló "fully homomorphic encryption" (FHE).
A harmadik dolog, hogy az ML-KEM algoritmusnál is van olyan rész, ami egy NIST FIPS 203 szabvány szerinti működésnél nem fordulhatna elő, de a nem bevizsgált, "kitudjamilyen" kódoknál megeshet: ha ugyanis azonos r véletlenszámmal és azonos nyilvános kulccsal kódolnánk m üzeneteket, akkor pl. az azonos c rejtjelezett szövegek alapján tudhatnánk, hogy azonosak az m nyílt szövegek is, de különböző c rejtjelezett szövegek esetén is, az eltérések mértéke alapján lehetne következtetni a különböző m nyílt szövegek közötti eltérésekre is. Ez akkor veszélyes leginkább, amikor ismert az egyik m nyílt szöveg és az ahhoz tartozó c rejtjelezett szöveg. (Halkan megjegyzem, hogy ez majdnem pont olyan eset, mint amikor CTR vagy GCM módban használjuk az AES algoritmust és elkövetjük a "nonce reuse" hibát, mint ahogy azt a WPA2-t is érintő KRACK esetében is láttuk.) Az eltérés ismerete pedig már adhat támpontot ahhoz, hogy milyen tartományban érdemes pl. "brute-force" támadással próbálkozni. Egy biztonságosnak számító rendszernél ilyen információra tehát nem szabad tudni következtetni kizárólag a rejtjelezett szöveg alapján. Szerencsére, ha az elvárt módon készül az r, akkor csak kicsit is különböző m nyílt szöveg esetén teljesen eltérő lesz a c rejtjelezett szöveg (a hash-függvények lavina-hatása miatt). Fontos hangsúlyozni, hogy ha ugyanazt a sérülékeny kódot használják "encapsulation" és "decapsulation" során is, akkor a rossz minőségű r létrehozása és használata észrevétlen marad, a külső - megfigyelő - fél viszont ezen "backdoor" alapján már tud információt szerezni a c rejtjelezett szövegekről. Itt is el lehet mondani, hogy érdemes lesz tehát az auditált kriptográfiai megoldásoknál maradni - legalább a forráskód-szintű, pl. Common Criteria EAL4 tanúsítottaknál -, amik ellenőrzötten jól - NIST FIPS 203 alapján - számítják ki az r értékét!
A negyedik dolog, hogy a hybrid módban alkalmazandó ML-KEM algoritmus megjelenése leginkább az SSL/TLS protokollnál érdekes. Nagy talány volt azonban az elején, hogy miképp kerül bevezetésre egy RSA-szerű kulcsrejtjelező megoldás egy olyan verzióba (TLS v1.3), aminél már pont kivezették ezt és csak a DH-szerű kulcsegyeztető megoldásokat hagyták meg (a Perfect Forward Secrecy biztosítása érdekében). A - jelenleg még nem végleges - specifikációk alapján a "quantum-safe" ML-KEM és a hagyományos, elliptikus görbés Diffie-Hellman algoritmus hybrid módon kerül alkalmazásra: a Curve25519 görbén alapuló ECDH (X25519) algoritmus adja az adatrejtjelezésre használandó pl. AES kulcs származtatásához - a lenyomatképzésen alapuló függvényhez - a bemeneti adatok egyik felét és az ML-KEM algoritmus a másik felét. A csomagméretek optimalizációja miatt ennél a kombinációnál a NIST Security Level 3 szintnek megfelelő X25519MLKEM768 változatot kezdték el használni. A NIST Security Level 5 szinthez a SecP384r1MLKEM1024 (NIST P-384 görbe és ML-KEM-1024) kombináció jelent meg az IETF specifikációiban. A hybrid megoldás miatt érdemes lesz figyelni a paraméterek megfelelő beállítására pl. a tanúsítvány keyUsage mezőjénél is (keyEncipherment/keyAgreement). Ez a CA és web server üzemeltetőket is érinti majd.

Mire várunk még? A NIST FIPS specifikációk ugyan leírják az algoritmusok működését és listázzák a paramétereket, de hogy ezek az algoritmusok milyen azonosítókat kapnak az XML/JSON-világban, illetve hogy ezek a paraméterek miképp fognak megjelenni a különböző adatstruktúrákban, azt még el kell dönteni (simán összefűzve/konkatenálva? vagy szét lesznek szedve? ASN.1 körítést kapnak? vagy másféle konténerbe kerülnek?). Ezeknek a szabványosítása jelenleg zajlik, ezért addig még nem lehet "quantum-safe" módon rejtjelezett és aláírt JSON-alapú (JWE/JWS-réteges JWT) vagy XML-alapú (XML Signature/Encryption) adatokat, üzeneteket gyártani. De a munka halad, és van is, aminek a szabványosítását már lezárták: elsőként a CMS (Cryptographic Message Syntax) adatstruktúra lett véglegesítve (IETF RFC 9882, IETF RFC 9814). Ezt használják pl. a PDF-be ágyazott aláírásoknál is (PAdES). És ez pontosan az, amit a DÁP eAláírás is támogat, azaz itt már - akár a mai napon - át lehetne váltani a "quantum-safe" megoldásra...
Mik a fejlemények? A technológiaváltás ütemezésére először az NSA (National Security Agency) adott ki útmutatót 2022-09-07-én. Abban céldátumként a 2033 szerepelt, de néhány speciális területen már 2030-ra át kellene állni a "quantum-safe" megoldásokra. A 2026-03-25-én megjelent Google ütemterv még ennél is szigorúbb lett: az átállás céldátumaként 2029 lett megadva. Mindeközben jelentek meg írások más témák kapcsán is. Az egyik ilyen érdekesség a Cloudflare korábbi munkatársától származik: egy 2026-04-20-i bejegyzésében Filippo Valsorda megkérdőjelezte azt, hogy valóban veszélyben lennének-e az AES-128 bites kulccsal rejtjelezett adatok (a Grover-algoritmus miatt), ha beköszönt a "quantum era". A bizonytalanság miatt azonban én inkább maradnék az AES-256 bites megoldásnál... A kvantumszámítógépek fejlődésével együtt néha jelentek meg ilyen-olyan kalkulációk is, amelyek arról szóltak, hogy milyen algoritmusok, milyen kulcsméretei, mekkora veszélyben vannak. A "nagyok" közül a Microsoft írt erről még 2017-ben. Az akkori kalkulációk alapján az látszott, hogy a jelenleg használt, ECC-alapú kulcsokhoz elegendő 2330 logikai qubit, míg az RSA-alapúakhoz sokkal több, 6146 logikai qubit szükséges.For example, our simulation of the point addition quantum circuit for the NIST standardized curve P-256 needs 2330 logical qubits [...]. In comparison, Shor’s factoring algorithm for a 3072-bit modulus needs 6146 qubits [...].
/ Microsoft, 2017, Quantum Resource Estimates for Computing Elliptic Curve Discrete Logarithms, https://eprint.iacr.org/2017/598.pdf / Ezek régebbi számítások voltak, ezért volt fontos, hogy 2026-ban érkezett a Caltech-től egy friss anyag. A számok ugyan mások, de a lényeg ugyanaz: az ECC-alapú kulcsok töréséhez szignifikánsan kevesebb logikai qubit szükséges, mint az RSA-alapúakhoz. A Caltech tanulmánya szerint az ECC-alapúakhoz 1450 logikai qubit kell, míg az RSA-alapúakhoz 6144 logikai qubit (itt eltérés, hogy a Caltech 2048 bites RSA kulcsokról írt, míg a Microsoftnál 3072 bites RSA kulcsok szerepeltek).In this work, we propose FTQC architectures based on reconfigurable atom arrays [...] that Shor’s algorithm can be implemented at cryptographically relevant scales using as few as 10,000 atomic qubits [...] our most time-efficient architectures can potentially enable runtimes of 10 days for ECC–256 with ≈ 26,000 qubits, and 97 days for RSA–2048 with ≈ 102,000 qubits [...] FIG. 3. Resources required for Shor’s algorithm [...] RSA-2048 6,144 log. qubits [...] ECC-256 1,450 log. qubits
/ Caltech/Oratomic, 2026, Shor’s algorithm is possible with as few as 10,000 reconfigurable atomic qubits, https://arxiv.org/pdf/2603.28627 / Az ECC-alapú kulcsokhoz szükséges 1450 logikai qubit aztán megjelent a Google 2026-os tanulmányában is.we do not expect meaningful scaling challenges between a quantum computer with 1200 logical qubits and one with 1450, so, [...] we assume that first-generation fast-clock CRQCs may be able to solve ECDLP on secp256k1 and similar elliptic curves in about 9 minutes on average.
/ Google, 2026, Securing Elliptic Curve Cryptocurrencies against Quantum Vulnerabilities: Resource Estimates and Mitigations, https://quantumai.google/static/site-assets/downloads/cryptocurrency-whitepaper.pdf / Az algoritmusokkal és kulcsméreteikkel azért érdemes foglalkozni, mert az aláírások, felülhitelesítések kapcsán mindig azt emlegetjük, hogy azokkal ráérünk az utolsó előtti pillanatig, de érdemes tisztában lenni azzal, hogy ez a pillanat sokkal hamarabb jön el az ECC-t használóknál, mint az RSA-t használóknál! Ez pedig azért számíthat, mert pont az utóbbi években az látszott, hogy ahol lehetett, RSA-alapúakról átváltottak ECC-alapú megoldásokra még akkor is, ha egyébként erre nem volt szükség. Ahol viszont meg van kötve a fejlesztők keze, az az IoT világ, ahol a méretek miatt muszáj ECC-alapú kulcsokat használni aláíráshoz. Az IoT világban tehát már most érdemes figyelembe venni, hogy mivel jár, ha "quantum-safe" működést is támogatni kell, hogy tudják megugrani a jóval nagyobb méretű aláírásokra váltást. A jelenlegi NIST P-256 görbés ECDSA aláírás mérete 64 byte, de a legkisebb "quantum-safe" FALCON esetében sem lehet 666 byte alatt megúszni. Ez viszont 10-szeres méretnövekedést jelent!
Kapcsolódó anyagok:
| | | vissza | | | | | |
|