Ein ausreichend großer Quantencomputer würde RSA, X25519/X448 und ECDSA/ECDH auf jeder Kurve der Seite Public-Key-Kryptografie brechen: Keine dieser mathematischen Grundlagen widersteht Shors Algorithmus. Er muss heute nicht existieren, um heute ein Problem zu sein, ein Angreifer kann verschlüsselten Datenverkehr jetzt aufzeichnen und ihn entschlüsseln, sobald eine solche Maschine existiert, eine Strategie, die üblicherweise „harvest now, decrypt later" genannt wird. Alles, was über Jahre, nicht Tage, vertraulich bleiben muss, sollte bereits zu einem Post-Quanten-Schlüsselaustausch wechseln.
NIST hat drei Verfahren standardisiert, alle hier implementiert, und alle ausschließlich auf SHAKE aufgebaut (siehe Hashing & Nachrichtenauthentifizierung), sodass ihre Sicherheit nur davon abhängt, dass der Hash solide ist: keine Gitter- oder Zahlentheorie-Annahme, die gebrochen werden könnte.
| Verfahren | Zweck | Standard | Unit |
| ML-KEM | Schlüsselkapselung, ersetzt Diffie-Hellman | FIPS 203 | sgcCrypto_MLKEM |
| ML-DSA | Signaturen, gitterbasiert, die Standardwahl | FIPS 204 | sgcCrypto_MLDSA |
| SLH-DSA | Signaturen, ausschließlich hashbasiert, konservative Rückfalllösung | FIPS 205 | sgcCrypto_SLHDSA |
ML-KEM und die hybride Konstruktion (empfohlen)
ML-KEM ersetzt einen Diffie-Hellman-Austausch: Der Sender kapselt ein gemeinsames Geheimnis an den öffentlichen Schlüssel des Empfängers, der Empfänger entkapselt es mit dem privaten Schlüssel, und beide erhalten dieselben 32 Byte. Es gibt drei Parametersätze, größer bedeutet eine größere Sicherheitsmarge und größere Schlüssel.
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;
Die Entkapselung gemäß FIPS 203 schlägt nie fehl.
Ein manipulierter oder bösartiger Chiffretext löst keinen Fehler aus, sondern gibt stillschweigend ein anderes Geheimnis zurück, abgeleitet aus einem impliziten Ablehnungswert, der in den privaten Schlüssel eingebacken ist. Stattdessen einen Fehler zurückzugeben, würde einem Angreifer ein Entschlüsselungsorakel liefern, um den privaten Schlüssel zu untersuchen; der Standard vermeidet das bewusst durch dieses Design. Was auch immer das abgeleitete Geheimnis schützt, muss die Abweichung selbst erkennen, zum Beispiel indem ein AEAD-Tag weiter unten in der Verarbeitung nicht verifiziert.
Was heute tatsächlich eingesetzt werden sollte, ist die hybride Konstruktion: X25519 kombiniert mit ML-KEM-768, sodass das gemeinsame Geheimnis von beiden abhängt. Sie bleibt sicher, falls ML-KEM jemals durch einen kryptoanalytischen Fortschritt gebrochen wird, und sie bleibt sicher, falls ein Quantencomputer X25519 bricht; ein Angreifer benötigt beides gleichzeitig, um zu versagen.
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 und die hybriden TLS-1.3-Gruppen
sgcCrypto_MLKEM_Hybrid enthält drei Hybrid-Familien. Die oben genannten sgcHybrid_-Funktionen sind das ursprüngliche sgcCrypto-Format und bleiben aus Kompatibilitätsgründen erhalten. X-Wing ist der standardisierte Allzweck-Hybrid-KEM, X25519 kombiniert mit ML-KEM-768, und er ist derjenige, den Sie außerhalb von TLS in neuem Code verwenden sollten. Die dritte Familie erzeugt die hybriden TLS-1.3-Key-Shares und sonst nichts.
X-Wing (draft-connolly-cfrg-xwing-kem-10) speichert nur ein einziges Geheimnis: Der private Schlüssel ist ein 32 Byte langer Seed, und jede Operation expandiert ihn zu dem ML-KEM-768- und X25519-Material, das sie benötigt. Der öffentliche Schlüssel ist 1216 Byte groß, der Chiffretext 1120 Byte und das gemeinsame Geheimnis 32 Byte. Die Kapselung führt die Prüfung des Kapselungsschlüssels gemäß FIPS 203 durch und löst einen Fehler aus, wenn der öffentliche Schlüssel sie nicht besteht.
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;
Die hybriden TLS-1.3-Key-Shares aus RFC 10024 sind X25519MLKEM768, SecP256r1MLKEM768 und SecP384r1MLKEM1024, IANA-Codepoints 0x11EC, 0x11EB und 0x11ED. Das sind keine KEMs, die Sie eigenständig einsetzen können. Das gemeinsame Geheimnis ist eine einfache Verkettung der beiden Hälften, was nur deshalb sicher ist, weil TLS es zusammen mit dem Handshake-Transkript in seinen Schlüsselplan einspeist. Verwenden Sie sie also ausschließlich, um einen TLS-Key-Share aufzubauen. Die Engine Native TLS bietet standardmäßig X25519MLKEM768 zuerst an, sodass ein Client oder ein Server, der sie verwendet, bereits ohne zusätzlichen Code einen Post-Quanten-Schlüsselaustausch aushandelt.
Die hybriden Gruppen gibt es nur in TLS 1.3. Die native Engine spricht auch TLS 1.2, und eine Verbindung, die darauf zurückfällt, weil die Gegenstelle kein TLS 1.3 kann, verwendet eine klassische Gruppe, X25519, secp256r1 oder secp384r1, ohne Post-Quanten-Schutz. Setzen Sie TLSOptions.Version auf einem Client oder SSLOptions.Version auf einem Server auf tls1_3, und listen Sie in NativeTLS_Options.Groups nur hybride Gruppen auf, wenn der Schlüsselaustausch immer Post-Quanten sein muss.
Die Post-Quanten-TLS-Demo, Demos\18.Cryptography\08.PostQuantum_TLS, hat eine Auswahl der TLS-Version mit den Optionen TLS 1.3 und TLS 1.2, nur TLS 1.3 und nur TLS 1.2, sowie eine Cipher-Suite-Auswahl, und ihre Statuszeile zeigt die ausgehandelte Version, Gruppe und Cipher Suite. Die Server-Demo kann mit einem ML-DSA-44-Zertifikat signieren, das nur TLS-1.3-Clients bedient, weil TLS 1.2 kein ML-DSA-Signaturschema hat.
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;
Der Client hält aPrivateState geheim, übergibt ihn unverändert an sgcTLSHybrid_ClientSharedSecret und löscht ihn anschließend; sein Aufbau ist nicht Teil der API. Die beiden booleschen Funktionen geben bei jeder ungültigen Eingabe der Gegenstelle False zurück und lassen alle Ausgaben leer: ein Key-Share falscher Länge, ein ML-KEM-Kapselungsschlüssel, der die Prüfung gemäß FIPS 203 nicht besteht, ein Punkt, der nicht auf der Kurve liegt, oder ein X25519-Ergebnis, das ausschließlich aus Nullen besteht. RFC 10024 verlangt, dass die Verbindung in jedem dieser Fälle mit einem illegal_parameter-Alert abgebrochen wird.
Alles, was auf X25519 aufbaut, also die sgcHybrid_-Funktionen, X-Wing und die Gruppe X25519MLKEM768, wird in einem SGC_CRYPTO_FIPS-Build herauskompiliert, siehe FIPS-Modus. Die beiden SecP-Gruppen bleiben erhalten, weil P-256, P-384 und ML-KEM allesamt zugelassene Algorithmen sind. Rufen Sie sgcTLSHybrid_Available auf, bevor Sie eine Gruppe anbieten oder akzeptieren, statt anzunehmen, dass sie vorhanden ist.
ML-DSA (empfohlen für Signaturen)
Das Signieren zieht einen Maskierungsvektor und versucht, eine Signatur zu erstellen, wobei ein Neustart erfolgt, wenn der Versuch Informationen über den Schlüssel preisgeben würde; die erwartete Anzahl an Versuchen ist klein, aber nicht fest, sodass das Signieren eine variable Zeit benötigt. Das liegt am Verfahren selbst und ist kein Seitenkanal auf den Schlüssel. Signaturen sind groß: 2420 Byte für ML-DSA-44 gegenüber 64 bei Ed25519, und öffentliche Schlüssel 1312 Byte gegenüber 32, kalkulieren Sie diesen Unterschied also ein, bevor Sie es in einem größenempfindlichen Protokoll einsetzen.
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 (konservative Rückfalllösung)
SLH-DSA beruht ausschließlich auf der Hash-Funktion: kein Gitterproblem, das jemals, zu irgendeinem Zeitpunkt in der Zukunft, gebrochen werden könnte. Diese Garantie ist teuer. Eine Signatur reicht von 7856 Byte (die kleinste, SLH-DSA-SHAKE-128s) bis 49856 Byte (die größte, SLH-DSA-SHAKE-256f), und das Signieren eines s-Parametersatzes (small) durchläuft Hunderttausende von Hash-Operationen und kann Dutzende Sekunden dauern; die f-Sätze (fast) signieren in etwa ein bis sieben Sekunden bei ungefähr doppelter Signaturgröße. Die Verifikation ist für jeden Parametersatz schnell, was sich für einen Selten-signieren-, oft-verifizieren-Anwendungsfall wie Firmware- oder Codesignierung besser eignet als für ein interaktives Protokoll.
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;
Schlüsselformate: SubjectPublicKeyInfo, PKCS#8 und PEM
Die rohen Byte-Folgen, die die FIPS-Algorithmen entgegennehmen, sind kein Speicherformat, und keine andere Implementierung wird sie lesen. Um einen Schlüssel auf die Festplatte zu schreiben, ihn in ein Zertifikat zu legen oder ihn an eine andere Bibliothek zu übergeben, exportieren Sie ihn in den Standard-Containern: ein X.509 SubjectPublicKeyInfo für einen öffentlichen Schlüssel, ein PKCS#8 OneAsymmetricKey für einen privaten Schlüssel und eine PEM-Umhüllung (RFC 7468) um beide, beschriftet mit PUBLIC KEY und PRIVATE KEY. Die drei Verfahren stellen dieselben acht Funktionen bereit, jedes gemäß dem RFC, der seine Kodierung festlegt.
| Verfahren | Container-RFC | DER-Funktionen | PEM-Funktionen |
| 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 |
Der Algorithmusbezeichner trägt den NIST-Objektbezeichner ohne Parameter, sodass der Parametersatz mit dem Schlüssel mitreist und der Import ihn zurückgibt. sgcMLDSA_OID, sgcMLKEM_OID und sgcSLHDSA_OID liefern den Punkt-String, und die passenden ParamsFromOID-Funktionen bilden einen solchen wieder zurück.
Drei Formen des privaten Schlüssels bei ML-DSA und ML-KEM.
Die Exportfunktionen nehmen einen Parameter aFormat vom Typ TsgcPQCPrivateKeyFormat entgegen, deklariert in sgcCrypto_DER. pqkfSeed, der Standardwert, schreibt nur den Seed, 32 Byte bei ML-DSA und die 64 Byte d || z bei ML-KEM; das ist die Form, die beide RFCs empfehlen, und sie ist mit Abstand die kleinste. pqkfExpanded schreibt den expandierten privaten FIPS-Schlüssel. pqkfBoth schreibt beide und löst einen Fehler aus, wenn der expandierte Schlüssel nicht die Expansion des Seeds ist, was die RFCs als fehlerhaften Schlüssel bezeichnen.
Ein Seed lässt sich nachträglich nicht aus einem expandierten Schlüssel zurückgewinnen, fordern Sie ihn also bei der Erzeugung mit sgcMLDSA_GenerateKeyPairAndSeed oder sgcMLKEM_GenerateKeyPairAndSeed an, wenn Sie die empfohlene Form speichern wollen. SLH-DSA hat eine einzige Form des privaten Schlüssels, SK.seed || SK.prf || PK.seed || PK.root, sein Export nimmt daher keinen Format-Parameter entgegen.
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;
Der Import validiert, er vertraut nicht.
Jede Importfunktion gibt False zurück, statt einen Fehler auszulösen, und sie gibt False zurück bei fehlerhaftem DER, bei nachfolgenden Daten, bei einem unbekannten Objektbezeichner, bei Parametern, die vorhanden sind, obwohl sie fehlen müssen, und bei einem Schlüssel, dessen Länge nicht zum Parametersatz passt. Über die Struktur hinaus muss ein öffentlicher ML-KEM-Schlüssel die Modulus-Prüfung gemäß FIPS 203 bestehen, ein expandierter privater ML-KEM-Schlüssel muss die Hash-Prüfung und den paarweisen Konsistenztest bestehen, und ein expandierter privater ML-DSA-Schlüssel wird zu seinem öffentlichen Schlüssel zurückgerechnet, sodass die gespeicherten t0 und tr beide damit übereinstimmen müssen. Ein eingebetteter öffentlicher Schlüssel, der nicht zum privaten Schlüssel passt, wird abgelehnt. Wenn ein privater Schlüssel einen Seed enthält, kommen alle drei Ausgaben gefüllt zurück, ein Aufrufer muss ihn also nie gesondert expandieren.
sgcSLHDSA_ImportPrivateKeyInfo und sgcSLHDSA_ImportPrivateKeyPEM nehmen einen zusätzlichen Parameter aVerify entgegen, standardmäßig True, der PK.root aus dem privaten Seed neu berechnet und einen Schlüssel ablehnt, dessen gespeicherte Root davon abweicht. Diese Neuberechnung kostet so viel wie eine Schlüsselerzeugung, also Sekunden bei den s-Parametersätzen, übergeben Sie False daher nur bei Schlüsseln, die Ihre eigene Anwendung erzeugt hat oder denen sie bereits vertraut. Der Objektbezeichner, die Längen und die Struktur werden auch bei False geprüft.
Das sind die Container, die der Rest der Bibliothek liest. Ein privater PKCS#8-ML-DSA-Schlüssel ist das, womit der JWT-Client signiert, ein SubjectPublicKeyInfo ist das, womit der JWT-Server validiert, und beides ist das, was Zertifikate & PKI in ein Zertifikat oder einen Certificate Signing Request legt.
Einen Parametersatz wählen
Jeder Größenaufruf (sgcMLKEM_PublicKeySize, sgcMLDSA_SignatureSize, sgcSLHDSA_SignatureSize und ihre PrivateKeySize-Gegenstücke) nimmt dieselbe Parameter-Enumeration wie die Operation selbst entgegen, sodass ein Protokoll seine Puffer dimensionieren kann, bevor überhaupt Kryptografie stattfindet:
| Parametersatz | Öffentlicher Schlüssel | Privater Schlüssel | Chiffretext / Signatur |
| 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 |
Alle Größenangaben sind in Byte. Das gemeinsame ML-KEM-Geheimnis ist unabhängig vom Parametersatz immer 32 Byte lang (sgcMLKEM_SharedSecretSize).