















| | | | | 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), 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. 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). 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.
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), 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 már beépítve tartalmazzák a PQC algoritmusokat megvalósító oqs-provider kódjait, í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 csak - egyáltalán nem NIST FIPS 204 szabványos módon - 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 2 (~ 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. 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.
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...
Kapcsolódó anyagok:
| | | vissza | | | | | |
|