Wystarczająco duży komputer kwantowy złamałby RSA, X25519/X448 oraz ECDSA/ECDH na każdej krzywej ze strony Kryptografia klucza publicznego: żadna z tych matematycznych podstaw nie opiera się algorytmowi Shora. Nie musi on istnieć dziś, aby stanowić dzisiejszy problem, przeciwnik może już teraz nagrywać zaszyfrowany ruch i odszyfrować go, gdy taka maszyna powstanie, strategia zwykle nazywana „zbierz teraz, odszyfruj później” (harvest now, decrypt later). Wszystko, co musi pozostać poufne przez lata, a nie dni, powinno już przechodzić na postkwantową wymianę kluczy.
NIST ustandaryzował trzy schematy, wszystkie zaimplementowane tutaj, i wszystkie zbudowane wyłącznie na SHAKE (patrz Haszowanie i uwierzytelnianie wiadomości), więc ich bezpieczeństwo opiera się wyłącznie na solidności tego skrótu: żadnego założenia teorii krat ani teorii liczb do złamania.
| Schemat | Przeznaczenie | Standard | Jednostka |
| ML-KEM | Enkapsulacja klucza, zastępuje Diffie-Hellman | FIPS 203 | sgcCrypto_MLKEM |
| ML-DSA | Podpisy, oparte na kratach, wybór domyślny | FIPS 204 | sgcCrypto_MLDSA |
| SLH-DSA | Podpisy, wyłącznie oparte na skrótach, konserwatywna alternatywa awaryjna | FIPS 205 | sgcCrypto_SLHDSA |
ML-KEM i konstrukcja hybrydowa (zalecana)
ML-KEM zastępuje wymianę Diffie-Hellman: nadawca enkapsuluje sekret współdzielony do klucza publicznego odbiorcy, odbiorca dekapsuluje go kluczem prywatnym, a obie strony kończą z tymi samymi 32 bajtami. Występuje w trzech zestawach parametrów, gdzie większy oznacza większy margines bezpieczeństwa i większe klucze.
procedure sgcMLKEM_GenerateKeyPair(aParams: TsgcMLKEMParams; out aPublicKey, aPrivateKey: TBytes);
procedure sgcMLKEM_Encapsulate(aParams: TsgcMLKEMParams; const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcMLKEM_Decapsulate(aParams: TsgcMLKEMParams; const aPrivateKey, aCiphertext: TBytes): TBytes;
var
oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
sgcMLKEM_GenerateKeyPair(mlkem768, oPub, oPriv);
sgcMLKEM_Encapsulate(mlkem768, oPub, oCiphertext, oSecretA); { sender }
oSecretB := sgcMLKEM_Decapsulate(mlkem768, oPriv, oCiphertext); { recipient }
{ oSecretA = oSecretB }
end;
Dekapsulacja FIPS 203 nigdy nie kończy się niepowodzeniem.
Uszkodzony lub złośliwy szyfrogram nie zgłasza błędu, lecz po cichu zwraca inny sekret, wyprowadzony z niejawnej wartości odrzucenia zaszytej w kluczu prywatnym. Zwrócenie błędu zamiast tego dałoby atakującemu wyrocznię odszyfrowania do sondowania klucza prywatnego; standard z założenia tego unika. Cokolwiek chroni wyprowadzony sekret, musi samodzielnie wykryć niezgodność, na przykład przez niepowodzenie weryfikacji tagu AEAD dalej w łańcuchu.
To, co warto faktycznie wdrożyć już dziś, to konstrukcja hybrydowa: X25519 połączony z ML-KEM-768, tak aby sekret współdzielony zależał od obu. Pozostaje bezpieczna, jeśli ML-KEM zostanie kiedyś złamany przez postęp kryptoanalityczny, i pozostaje bezpieczna, jeśli komputer kwantowy złamie X25519; atakujący musiałby złamać oba naraz.
procedure sgcHybrid_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcHybrid_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcHybrid_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;
X-Wing i hybrydowe grupy TLS 1.3
sgcCrypto_MLKEM_Hybrid zawiera trzy rodziny konstrukcji hybrydowych. Funkcje sgcHybrid_ powyżej to pierwotny format sgcCrypto, zachowany dla zgodności. X-Wing to ustandaryzowany hybrydowy KEM ogólnego przeznaczenia, X25519 połączony z ML-KEM-768, i to jego należy używać w nowym kodzie poza TLS. Trzecia rodzina buduje hybrydowe udziały klucza TLS 1.3 i nic poza tym.
X-Wing (draft-connolly-cfrg-xwing-kem-10) przechowuje tylko jeden sekret: kluczem prywatnym jest 32-bajtowe ziarno, a każda operacja rozwija je w potrzebny jej materiał ML-KEM-768 i X25519. Klucz publiczny ma 1216 bajtów, szyfrogram 1120 bajtów, a sekret współdzielony 32 bajty. Enkapsulacja wykonuje kontrolę klucza enkapsulacji z FIPS 203 i zgłasza wyjątek, gdy klucz publiczny jej nie przechodzi.
procedure sgcXWing_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcXWing_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcXWing_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;
var
oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
sgcXWing_GenerateKeyPair(oPub, oPriv);
sgcXWing_Encapsulate(oPub, oCiphertext, oSecretA); { sender }
if sgcXWing_Decapsulate(oPriv, oCiphertext, oSecretB) then
{ oSecretA = oSecretB };
end;
Hybrydowe udziały klucza TLS 1.3 z RFC 10024 to X25519MLKEM768, SecP256r1MLKEM768 i SecP384r1MLKEM1024, o punktach kodowych IANA 0x11EC, 0x11EB i 0x11ED. Nie są to KEM-y, których można używać samodzielnie. Sekret współdzielony jest zwykłym połączeniem obu połówek, co jest bezpieczne wyłącznie dlatego, że TLS podaje go do swojego harmonogramu kluczy razem z transkryptem uzgadniania, dlatego używaj ich do zbudowania udziału klucza TLS i do niczego innego. Silnik Natywny TLS domyślnie oferuje X25519MLKEM768 jako pierwszy, więc klient albo serwer, który go używa, już negocjuje postkwantową wymianę kluczy bez dodatkowego kodu.
Grupy hybrydowe istnieją wyłącznie w TLS 1.3. Silnik natywny obsługuje również TLS 1.2, a połączenie, które przechodzi na tę wersję, ponieważ druga strona nie potrafi TLS 1.3, używa klasycznej grupy, X25519, secp256r1 lub secp384r1, bez żadnej ochrony postkwantowej. Aby wymiana kluczy była zawsze postkwantowa, ustaw TLSOptions.Version na kliencie lub SSLOptions.Version na serwerze na tls1_3, i wymień w NativeTLS_Options.Groups wyłącznie grupy hybrydowe.
Demo Post-Quantum TLS, Demos\18.Cryptography\08.PostQuantum_TLS, ma selektor wersji TLS z opcjami TLS 1.3 i TLS 1.2, tylko TLS 1.3 oraz tylko TLS 1.2, a także selektor zestawu szyfrów, a jego linia stanu pokazuje wynegocjowaną wersję, grupę i zestaw szyfrów. Demo serwera może podpisywać certyfikatem ML-DSA-44, który obsługuje wyłącznie klientów TLS 1.3, ponieważ TLS 1.2 nie ma schematu podpisu ML-DSA.
type
TsgcTLSHybridGroup = (thgX25519MLKEM768, thgSecP256r1MLKEM768, thgSecP384r1MLKEM1024);
function sgcTLSHybrid_GroupID(aGroup: TsgcTLSHybridGroup): Word;
function sgcTLSHybrid_GroupName(aGroup: TsgcTLSHybridGroup): string;
function sgcTLSHybrid_Available(aGroup: TsgcTLSHybridGroup): Boolean;
procedure sgcTLSHybrid_ClientKeyShare(aGroup: TsgcTLSHybridGroup; out aKeyShare, aPrivateState: TBytes);
function sgcTLSHybrid_ServerKeyShare(aGroup: TsgcTLSHybridGroup; const aClientKeyShare: TBytes;
out aKeyShare, aSharedSecret: TBytes): Boolean;
function sgcTLSHybrid_ClientSharedSecret(aGroup: TsgcTLSHybridGroup;
const aPrivateState, aServerKeyShare: TBytes; out aSharedSecret: TBytes): Boolean;
Klient trzyma aPrivateState w tajemnicy, przekazuje go bez zmian do sgcTLSHybrid_ClientSharedSecret i następnie czyści; jego układ nie jest częścią API. Obie funkcje typu Boolean zwracają False, z wszystkimi wynikami pustymi, przy każdych nieprawidłowych danych drugiej strony: udziale klucza o złej długości, kluczu enkapsulacji ML-KEM, który nie przechodzi kontroli FIPS 203, punkcie leżącym poza krzywą albo wyniku X25519 złożonym z samych zer. RFC 10024 wymaga, aby w każdym z tych przypadków połączenie zostało przerwane alertem illegal_parameter.
Wszystko, co jest zbudowane na X25519, czyli funkcje sgcHybrid_, X-Wing oraz grupa X25519MLKEM768, jest usuwane przy kompilacji z SGC_CRYPTO_FIPS, patrz Tryb FIPS. Obie grupy SecP pozostają, ponieważ P-256, P-384 i ML-KEM są algorytmami zatwierdzonymi. Wywołaj sgcTLSHybrid_Available przed zaoferowaniem albo przyjęciem grupy, zamiast zakładać, że jest dostępna.
ML-DSA (zalecany do podpisów)
Podpisywanie losuje wektor maskujący i próbuje zbudować podpis, ponawiając próbę, gdy dana próba ujawniłaby informacje o kluczu; oczekiwana liczba prób jest niewielka, ale nieustalona, więc podpisywanie zajmuje zmienną ilość czasu. Jest to nieodłączna cecha schematu, a nie kanał boczny dotyczący klucza. Podpisy są duże: 2420 bajtów dla ML-DSA-44 wobec 64 dla Ed25519, a klucze publiczne 1312 bajtów wobec 32, więc przed wdrożeniem w protokole wrażliwym na rozmiar trzeba uwzględnić tę różnicę w budżecie.
procedure sgcMLDSA_GenerateKeyPair(aParams: TsgcMLDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcMLDSA_Sign(aParams: TsgcMLDSAParams; const aPrivateKey, aMessage, aContext: TBytes): TBytes;
function sgcMLDSA_Verify(aParams: TsgcMLDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;
var
oPub, oPriv, oSignature, oEmpty: TBytes;
begin
sgcMLDSA_GenerateKeyPair(mldsa44, oPub, oPriv);
oSignature := sgcMLDSA_Sign(mldsa44, oPriv, oMessage, oEmpty);
if sgcMLDSA_Verify(mldsa44, oPub, oMessage, oSignature, oEmpty) then
{ valid };
end;
SLH-DSA (konserwatywna alternatywa awaryjna)
SLH-DSA opiera się wyłącznie na funkcji skrótu: żadnego problemu z teorii krat do złamania, nigdy, w żadnym momencie w przyszłości. Ta gwarancja jest kosztowna. Podpis waha się od 7856 bajtów (najmniejszy, SLH-DSA-SHAKE-128s) do 49856 bajtów (największy, SLH-DSA-SHAKE-256f), a podpisywanie w zestawie parametrów s (small, mały) przechodzi przez setki tysięcy operacji skrótu i może zająć dziesiątki sekund; zestawy f (fast, szybki) podpisują w około jednej do siedmiu sekund, kosztem mniej więcej dwukrotnie większego podpisu. Weryfikacja jest szybka dla każdego zestawu parametrów, co pasuje bardziej do przypadku użycia rzadkiego podpisywania i częstej weryfikacji, jak podpisywanie firmware albo kodu, niż do protokołu interaktywnego.
procedure sgcSLHDSA_GenerateKeyPair(aParams: TsgcSLHDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcSLHDSA_Sign(aParams: TsgcSLHDSAParams; const aPrivateKey, aMessage, aContext: TBytes;
aDeterministic: Boolean = True): TBytes;
function sgcSLHDSA_Verify(aParams: TsgcSLHDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;
Formaty kluczy: SubjectPublicKeyInfo, PKCS#8 i PEM
Surowe ciągi bajtów, które przyjmują algorytmy FIPS, nie są formatem przechowywania i żadna inna implementacja ich nie odczyta. Aby zapisać klucz na dysku, umieścić go w certyfikacie albo przekazać innej bibliotece, wyeksportuj go w standardowych kontenerach: X.509 SubjectPublicKeyInfo dla klucza publicznego, PKCS#8 OneAsymmetricKey dla klucza prywatnego oraz otoczkę PEM (RFC 7468) wokół każdego z nich, z etykietą PUBLIC KEY i PRIVATE KEY. Wszystkie trzy schematy udostępniają te same osiem funkcji, każda zgodnie z RFC, który definiuje jej kodowanie.
| Schemat | RFC kontenera | Funkcje DER | Funkcje PEM |
| ML-DSA | RFC 9881 | sgcMLDSA_ExportSubjectPublicKeyInfo, sgcMLDSA_ImportSubjectPublicKeyInfo, sgcMLDSA_ExportPrivateKeyInfo, sgcMLDSA_ImportPrivateKeyInfo | sgcMLDSA_ExportPublicKeyPEM, sgcMLDSA_ImportPublicKeyPEM, sgcMLDSA_ExportPrivateKeyPEM, sgcMLDSA_ImportPrivateKeyPEM |
| ML-KEM | RFC 9935 | sgcMLKEM_ExportSubjectPublicKeyInfo, sgcMLKEM_ImportSubjectPublicKeyInfo, sgcMLKEM_ExportPrivateKeyInfo, sgcMLKEM_ImportPrivateKeyInfo | sgcMLKEM_ExportPublicKeyPEM, sgcMLKEM_ImportPublicKeyPEM, sgcMLKEM_ExportPrivateKeyPEM, sgcMLKEM_ImportPrivateKeyPEM |
| SLH-DSA | RFC 9909 | sgcSLHDSA_ExportSubjectPublicKeyInfo, sgcSLHDSA_ImportSubjectPublicKeyInfo, sgcSLHDSA_ExportPrivateKeyInfo, sgcSLHDSA_ImportPrivateKeyInfo | sgcSLHDSA_ExportPublicKeyPEM, sgcSLHDSA_ImportPublicKeyPEM, sgcSLHDSA_ExportPrivateKeyPEM, sgcSLHDSA_ImportPrivateKeyPEM |
Identyfikator algorytmu niesie identyfikator obiektu NIST bez parametrów, więc zestaw parametrów podróżuje razem z kluczem, a import go zwraca. sgcMLDSA_OID, sgcMLKEM_OID i sgcSLHDSA_OID zwracają postać z kropkami, a odpowiadające im funkcje ParamsFromOID odwzorowują ją z powrotem.
Trzy postacie klucza prywatnego dla ML-DSA i ML-KEM.
Funkcje eksportu przyjmują parametr aFormat typu TsgcPQCPrivateKeyFormat, zadeklarowany w sgcCrypto_DER. pqkfSeed, wartość domyślna, zapisuje samo ziarno, 32 bajty dla ML-DSA i 64-bajtowe d || z dla ML-KEM; jest to postać zalecana przez oba RFC i zdecydowanie najmniejsza. pqkfExpanded zapisuje rozwinięty klucz prywatny FIPS. pqkfBoth zapisuje obie postacie i zgłasza wyjątek, gdy klucz rozwinięty nie jest rozwinięciem ziarna, co RFC nazywają kluczem zniekształconym.
Ziarna nie da się później odtworzyć z klucza rozwiniętego, więc poproś o nie już przy generowaniu, funkcją sgcMLDSA_GenerateKeyPairAndSeed albo sgcMLKEM_GenerateKeyPairAndSeed, jeśli zamierzasz przechowywać zalecaną postać. SLH-DSA ma jedną postać klucza prywatnego, SK.seed || SK.prf || PK.seed || PK.root, więc jego eksport nie przyjmuje parametru formatu.
function sgcMLDSA_ExportPrivateKeyPEM(aParams: TsgcMLDSAParams; const aSeed, aPrivateKey: TBytes;
aFormat: TsgcPQCPrivateKeyFormat = pqkfSeed): string;
function sgcMLDSA_ImportPrivateKeyPEM(const aPEM: string; out aParams: TsgcMLDSAParams;
out aSeed, aPrivateKey, aPublicKey: TBytes): Boolean;
var
vParams: TsgcMLDSAParams;
oSeed, oPub, oPriv: TBytes;
vPublicPEM, vPrivatePEM: string;
begin
sgcMLDSA_GenerateKeyPairAndSeed(mldsa65, oSeed, oPub, oPriv);
vPublicPEM := sgcMLDSA_ExportPublicKeyPEM(mldsa65, oPub);
vPrivatePEM := sgcMLDSA_ExportPrivateKeyPEM(mldsa65, oSeed, oPriv, pqkfSeed);
{ reading it back: the parameter set comes from the key itself }
if sgcMLDSA_ImportPrivateKeyPEM(vPrivatePEM, vParams, oSeed, oPriv, oPub) then
{ vParams = mldsa65, and oPriv and oPub are the expansion of oSeed };
end;
Import weryfikuje, nie ufa.
Każda funkcja importu zwraca False, zamiast zgłaszać wyjątek, i zwraca False przy zniekształconym DER, przy danych pozostałych za strukturą, przy nieznanym identyfikatorze obiektu, przy parametrach obecnych tam, gdzie muszą być nieobecne, oraz przy kluczu, którego długość nie odpowiada zestawowi parametrów. Poza strukturą, klucz publiczny ML-KEM musi przejść kontrolę modułu z FIPS 203, rozwinięty klucz prywatny ML-KEM musi przejść kontrolę skrótu oraz test spójności pary kluczy, a rozwinięty klucz prywatny ML-DSA jest odtwarzany do postaci klucza publicznego, tak że zapisane t0 i tr muszą być z nim zgodne. Osadzony klucz publiczny, który nie pasuje do klucza prywatnego, zostaje odrzucony. Gdy klucz prywatny zawiera ziarno, wszystkie trzy wyniki wracają wypełnione, więc wywołujący nigdy nie musi rozwijać go osobno.
sgcSLHDSA_ImportPrivateKeyInfo i sgcSLHDSA_ImportPrivateKeyPEM przyjmują dodatkowy parametr aVerify, domyślnie True, który przelicza PK.root z prywatnego ziarna i odrzuca klucz, którego zapisany korzeń się różni. To przeliczenie kosztuje tyle, co wygenerowanie klucza, czyli sekundy dla zestawów parametrów s, dlatego przekazuj False wyłącznie dla kluczy wygenerowanych przez Twoją własną aplikację albo już przez nią zaufanych. Identyfikator obiektu, długości i struktura są sprawdzane także wtedy, gdy ma wartość False.
To są kontenery, które odczytuje reszta biblioteki. Kluczem prywatnym ML-DSA w formacie PKCS#8 podpisuje klient JWT, kluczem SubjectPublicKeyInfo weryfikuje serwer JWT, a oba trafiają przez Certyfikaty i PKI do certyfikatu albo do żądania podpisania certyfikatu.
Wybór zestawu parametrów
Każde wywołanie rozmiaru (sgcMLKEM_PublicKeySize, sgcMLDSA_SignatureSize, sgcSLHDSA_SignatureSize i odpowiadające im PrivateKeySize) przyjmuje ten sam wyliczeniowy parametr co sama operacja, dzięki czemu protokół może ustalić rozmiar buforów przed wykonaniem jakiejkolwiek operacji kryptograficznej:
| Zestaw parametrów | Klucz publiczny | Klucz prywatny | Szyfrogram / Podpis |
| ML-KEM-512 | 800 | 1632 | 768 |
| ML-KEM-768 | 1184 | 2400 | 1088 |
| ML-KEM-1024 | 1568 | 3168 | 1568 |
| ML-DSA-44 | 1312 | 2560 | 2420 |
| ML-DSA-65 | 1952 | 4032 | 3309 |
| ML-DSA-87 | 2592 | 4896 | 4627 |
| SLH-DSA-SHAKE-128s | 32 | 64 | 7856 |
| SLH-DSA-SHAKE-128f | 32 | 64 | 17088 |
| SLH-DSA-SHAKE-192s | 48 | 96 | 16224 |
| SLH-DSA-SHAKE-192f | 48 | 96 | 35664 |
| SLH-DSA-SHAKE-256s | 64 | 128 | 29792 |
| SLH-DSA-SHAKE-256f | 64 | 128 | 49856 |
Wszystkie rozmiary są podane w bajtach. Sekret współdzielony ML-KEM ma zawsze 32 bajty, niezależnie od zestawu parametrów (sgcMLKEM_SharedSecretSize).