La cryptographie à clé publique (asymétrique) utilise une paire de clés au lieu d'un secret partagé : tout ce que la clé privée signe, la clé publique correspondante le vérifie ; tout ce qui est scellé pour une clé publique, seule la clé privée correspondante peut l'ouvrir. La clé publique peut être remise à n'importe qui, y compris à un adversaire, sans rien affaiblir.
RSA
La génération de clé recherche de grands nombres premiers et est réellement lente, plusieurs secondes pour une clé de 2048 bits ; ne l'exécutez jamais sur un thread d'interface utilisateur. Utilisez OAEP pour le chiffrement et PSS pour les signatures, tous deux incluent un élément aléatoire qui manque à PKCS#1 v1.5. La signature/vérification PKCS#1 v1.5 est fournie dans sgcCrypto_RSA pour interopérer avec des formats qui la spécifient (le RS256 de JWS est le cas courant) ; privilégiez PSS pour tout ce qui est nouveau.
function sgcRSA_GenerateKey(aBits: Integer): TsgcRSAPrivateKey;
function sgcRSA_PublicKey(const aPrivateKey: TsgcRSAPrivateKey): TsgcRSAPublicKey;
function sgcRSA_OAEP_Encrypt(const aKey: TsgcRSAPublicKey; const aPlain, aLabel: TBytes; aHash: TsgcRSAHash): TBytes;
function sgcRSA_OAEP_Decrypt(const aKey: TsgcRSAPrivateKey; const aCipher, aLabel: TBytes; aHash: TsgcRSAHash; out aPlain: TBytes): Boolean;
function sgcRSA_PSS_Sign(const aKey: TsgcRSAPrivateKey; const aData: TBytes; aHash: TsgcRSAHash; aSaltLen: Integer = -1): TBytes;
function sgcRSA_PSS_Verify(const aKey: TsgcRSAPublicKey; const aData, aSignature: TBytes; aHash: TsgcRSAHash; aSaltLen: Integer = -1): Boolean;
var
oKey: TsgcRSAPrivateKey;
oSignature: TBytes;
begin
oKey := sgcRSA_GenerateKey(2048);
oSignature := sgcRSA_PSS_Sign(oKey, oData, rhSHA256);
if sgcRSA_PSS_Verify(sgcRSA_PublicKey(oKey), oData, oSignature, rhSHA256) then
{ valid };
end;
Le chiffrement PKCS#1 v1.5 est également fourni, le mode que Java nomme RSA/NONE/PKCS1Padding. Il est présent ici pour interopérer avec des formats qui l'imposent et pour rien d'autre : le remplissage v1.5 est exactement ce que cible l'attaque de Bleichenbacher, aussi OAEP demeure-t-il le bon choix pour tout ce qui est nouveau. Le texte en clair est limité à la taille du module moins 11 octets, soit 245 octets avec une clé de 2048 bits.
function sgcRSA_PKCS1_Encrypt(const aKey: TsgcRSAPublicKey; const aPlain: TBytes): TBytes;
function sgcRSA_PKCS1_Decrypt(const aKey: TsgcRSAPrivateKey; const aCipher: TBytes; out aPlain: TBytes): Boolean;
sgcRSA_PKCS1_Decrypt renvoie False pour chaque type d'échec de décodage et emprunte le même chemin pour chacun d'eux, car les distinguer est précisément ce dont un oracle de remplissage a besoin.
OAEP avec un hachage MGF1 distinct
OAEP utilise un hachage à deux endroits, sur le label et à l'intérieur de la fonction de génération de masque MGF1, et les deux ne sont pas obligés d'être identiques. C'est là que Java pose problème. SunJCE interprète OAEPWithSHA256AndMGF1Padding comme SHA-256 pour l'empreinte avec MGF1 resté sur SHA-1, tandis que Bouncy Castle interprète la même chaîne comme SHA-256 pour les deux, si bien qu'un texte chiffré produit par l'un est indéchiffrable par l'autre. Les surcharges à cinq paramètres fixent les deux hachages indépendamment, ce que peut exiger un dialogue avec un pair Java. La forme à quatre paramètres est inchangée et transmet aHash pour les deux.
function sgcRSA_OAEP_Encrypt(const aKey: TsgcRSAPublicKey; const aPlain, aLabel: TBytes; aHash, aMGFHash: TsgcRSAHash): TBytes; overload;
function sgcRSA_OAEP_Decrypt(const aKey: TsgcRSAPrivateKey; const aCipher, aLabel: TBytes; aHash, aMGFHash: TsgcRSAHash; out aPlain: TBytes): Boolean; overload;
var
oKey: TsgcRSAPrivateKey;
oCipher: TBytes;
begin
{ SunJCE OAEPWithSHA256AndMGF1Padding: SHA-256 label hash, MGF1 on SHA-1. }
oCipher := sgcRSA_OAEP_Encrypt(sgcRSA_PublicKey(oKey), oPlain, nil, rhSHA256, rhSHA1);
end;
Une discordance de hachage est indiscernable d'un texte chiffré corrompu, si bien qu'un aMGFHash erroné revient sous la forme d'un simple False, sans rien indiquer lequel des deux est fautif. Les deux parties doivent s'accorder dessus à l'avance.
Les clés s'exportent en PKCS#1 et en SubjectPublicKeyInfo DER, ainsi qu'en PEM :
function sgcRSA_ExportPrivateKeyPEM(const aKey: TsgcRSAPrivateKey): string;
function sgcRSA_ExportPublicKeyPEM(const aKey: TsgcRSAPublicKey): string;
PKCS#8 est l'autre conteneur de clé privée, celui écrit sous la forme BEGIN PRIVATE KEY, et c'est celui qu'attendent Java et .NET ; la paire ci-dessus émet à la place l'ancien BEGIN RSA PRIVATE KEY de PKCS#1. Aucun des deux conteneurs n'est chiffré, si bien que ce qui en ressort est du matériel de clé nu, qui ne doit se trouver nulle part où un journal ou une sauvegarde pourrait l'atteindre.
function sgcRSA_ExportPrivateKeyPKCS8DER(const aKey: TsgcRSAPrivateKey): TBytes;
function sgcRSA_ExportPrivateKeyPKCS8PEM(const aKey: TsgcRSAPrivateKey): string;
Importer une clé RSA
Jusqu'à leur apparition, une TsgcRSAPrivateKey ne pouvait provenir que de sgcRSA_GenerateKey, ce qui signifiait qu'une clé enregistrée sur disque ne pouvait pas être réutilisée du tout. Les routines d'import lisent les deux conteneurs de clé privée, PKCS#1 RSAPrivateKey et PKCS#8 PrivateKeyInfo, ainsi que les deux encodages de clé publique, PKCS#1 RSAPublicKey et X.509 SubjectPublicKeyInfo, en DER ou en PEM. Un PKCS#8 chiffré, la forme BEGIN ENCRYPTED PRIVATE KEY, n'est pas pris en charge et renvoie False. Les paramètres CRT sont recalculés lorsque le fichier les omet, si bien qu'une clé importée tourne toujours à pleine vitesse.
function sgcRSA_ImportPrivateKeyDER(const aDER: TBytes; out aKey: TsgcRSAPrivateKey): Boolean;
function sgcRSA_ImportPrivateKeyPEM(const aPEM: string; out aKey: TsgcRSAPrivateKey): Boolean;
function sgcRSA_ImportPublicKeyDER(const aDER: TBytes; out aKey: TsgcRSAPublicKey): Boolean;
function sgcRSA_ImportPublicKeyPEM(const aPEM: string; out aKey: TsgcRSAPublicKey): Boolean;
var
oKey: TsgcRSAPrivateKey;
oSignature: TBytes;
begin
if sgcRSA_ImportPrivateKeyPEM(Memo1.Text, oKey) then
oSignature := sgcRSA_PSS_Sign(oKey, oData, rhSHA256);
end;
Un certificat n'est pas une clé publique, donnez donc à sgcRSA_ImportPublicKeyDER le SubjectPublicKeyInfo extrait du certificat, et non le certificat lui-même. sgcRSA_CheckPrivateKey vérifie la cohérence interne de tout ce qui provient d'ailleurs, que n = p*q, que e*d est congru à 1, et que les paramètres CRT correspondent.
Ed25519 et Ed448 (signatures)
Les nonces EdDSA sont dérivés de manière déterministe à partir de la clé privée et du message, de sorte qu'aucun générateur de nombres aléatoires n'intervient dans la signature et qu'il n'y a aucune possibilité qu'un nonce répété ne divulgue la clé, contrairement à ECDSA (voir ci-dessous). sgcCrypto_Ed25519 est la primitive de signature/vérification utilisée par le type d'identifiant WebAuthn EdDSA ailleurs dans sgcWebSockets ; elle suppose que l'appelant connaît déjà la clé publique de 32 octets (WebAuthn vous la fournit à partir de l'identifiant) et n'expose pas d'appel séparé de génération de clé ou de dérivation de clé publique. sgcCrypto_Ed448 est le signataire généraliste, avec environ 224 bits de sécurité contre 128 pour Ed25519, au prix d'une clé et d'une signature plus grandes et d'une signature plus lente.
{ Ed25519: caller supplies the 32-byte seed and, to verify, the 32-byte public key. }
function sgcEd25519_Sign(const aPrivateSeed, aMessage: TBytes): TBytes;
function sgcEd25519_Verify(const aPublicKey, aMessage, aSignature: TBytes): Boolean;
{ Ed448: 57-byte keys, 114-byte signatures, optional context string. }
procedure sgcEd448_GenerateKeyPair(out aPrivateKey, aPublicKey: TBytes);
function sgcEd448_Sign(const aPrivateKey, aMessage, aContext: TBytes): TBytes;
function sgcEd448_Verify(const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;
X25519 et X448 (accord de clé)
Diffie-Hellman sur des courbes de Montgomery : les deux parties génèrent une paire de clés, échangent leurs clés publiques, et chacune calcule le même secret partagé à partir de sa propre clé privée et de la clé publique de l'autre partie. Faites toujours passer la sortie brute par un KDF (voir Hachage de mots de passe & dérivation de clé, HKDF) avant de l'utiliser pour chiffrer quoi que ce soit ; la sortie ECDH brute n'est pas en elle-même une clé appropriée. X448 offre environ 224 bits de sécurité contre 128 pour X25519, au prix d'une clé plus grande et d'environ trois fois plus de calcul.
function sgcX25519_PublicKey(const aPrivateKey: TBytes): TBytes;
function sgcX25519_SharedSecret(const aPrivateKey, aPeerPublicKey: TBytes; out aSecret: TBytes): Boolean;
function sgcX448_PublicKey(const aPrivateKey: TBytes): TBytes;
function sgcX448_SharedSecret(const aPrivateKey, aPeerPublicKey: TBytes; out aSecret: TBytes): Boolean;
var
oAlicePriv, oBobPriv, oAlicePub, oBobPub, oSecretA, oSecretB: TBytes;
begin
oAlicePriv := sgcRandomBytes(32);
oBobPriv := sgcRandomBytes(32);
oAlicePub := sgcX25519_PublicKey(oAlicePriv);
oBobPub := sgcX25519_PublicKey(oBobPriv);
sgcX25519_SharedSecret(oAlicePriv, oBobPub, oSecretA);
sgcX25519_SharedSecret(oBobPriv, oAlicePub, oSecretB);
{ oSecretA = oSecretB }
end;
SharedSecret renvoie False pour une clé de pair d'ordre faible, le petit ensemble de clés publiques qui forcent le secret partagé à être entièrement à zéro quelle que soit votre propre clé privée. Vérifiez toujours le résultat plutôt que de faire confiance à la sortie sans condition.
ECIES (chiffrement scellé)
Combine X25519, HKDF-SHA256 et XChaCha20-Poly1305 en un seul appel qui chiffre un message pour la clé publique d'un destinataire, sans qu'aucun secret partagé préalable ne soit nécessaire. aContext lie le message scellé à un usage ou une étape de protocole, l'ouvrir avec un contexte différent échoue, ce qui empêche qu'un message scellé d'un échange ne soit rejoué dans un autre.
procedure sgcECIES_GenerateKeyPair(out aPrivateKey, aPublicKey: TBytes);
function sgcECIES_Seal(const aRecipientPublicKey, aPlain, aAAD, aContext: TBytes): TBytes;
function sgcECIES_Open(const aPrivateKey, aSealed, aAAD, aContext: TBytes; out aPlain: TBytes): Boolean;
ECDSA / ECDH sur courbes nommées
Deux unités distinctes couvrent les courbes de Weierstrass courtes, réparties selon la famille de courbes qu'elles implémentent :
sgcCrypto_EC couvre les courbes NIST (P-256, P-384, P-521) et est orientée autour des clés PEM et des signatures JWS, la forme attendue par les bibliothèques JWT (ES256 et similaires).
sgcCrypto_ECCurves couvre secp256k1 (la courbe de Bitcoin et d'Ethereum) et les courbes brainpoolP256r1 / P384r1 / P512r1 de la RFC 5639, en travaillant directement sur les octets bruts des clés et des signatures plutôt qu'en PEM.
TsgcECCurve dans sgcCrypto_ECCurves nomme désormais sept courbes plutôt que quatre : eccSecp256k1, les trois courbes Brainpool, et eccP256, eccP384 et eccP521 pour NIST P-256, P-384 et P-521. Les trois courbes NIST n'étaient auparavant accessibles que via l'API PEM et JWS de sgcCrypto_EC, si bien qu'une application détenant une clé P-256 sous forme d'octets bruts devait d'abord écrire du PEM. Les appels bruts de génération, signature, vérification, ECDH et compression ci-dessous couvrent désormais les sept. Utilisez sgcCrypto_EC lorsque la tâche est du JWS, et sgcCrypto_ECCurves lorsque le matériel de clé est déjà sous forme d'octets.
Les deux unités déclarent un type nommé TsgcECCurve et une routine nommée sgcEC_GenerateKeyPair, avec des significations différentes dans chaque unité. N'ajoutez pas les deux unités à la même clause uses en référençant l'un ou l'autre nom sans le qualifier ; qualifiez avec le nom de l'unité (sgcCrypto_ECCurves.sgcEC_GenerateKeyPair) si une seule unité a réellement besoin des deux.
{ sgcCrypto_ECCurves: secp256k1 and Brainpool, RFC 6979 deterministic nonces,
low-S normalized signatures (BIP 62). }
procedure sgcEC_GenerateKeyPair(aCurve: TsgcECCurve; out aPrivateKey, aPublicKey: TBytes);
function sgcECDSA_SignHash(aCurve: TsgcECCurve; const aPrivateKey, aHash: TBytes; aHashKind: TsgcECHash = echSHA256): TBytes;
function sgcECDSA_VerifyHash(aCurve: TsgcECCurve; const aPublicKey, aHash, aSignature: TBytes): Boolean;
function sgcECDH_SharedSecret(aCurve: TsgcECCurve; const aPrivateKey, aPeerPublicKey: TBytes; out aSecret: TBytes): Boolean;
function sgcEC_ValidatePublicKey(aCurve: TsgcECCurve; const aPublicKey: TBytes): Boolean;
function sgcEC_Compress(aCurve: TsgcECCurve; const aPublicKey: TBytes): TBytes;
function sgcEC_Decompress(aCurve: TsgcECCurve; const aCompressed: TBytes): TBytes;
Les signatures ECDSA utilisent ici les nonces déterministes de la RFC 6979 : le nonce est dérivé de la clé privée et du message plutôt que tiré d'un générateur de nombres aléatoires, ce qui élimine la cause la plus courante par laquelle des déploiements réels ont perdu une clé privée ECDSA, un nonce répété ou partiellement prévisible. Les signatures portent la normalisation low-S qu'exigent Bitcoin (BIP 62) et Ethereum ; la vérification accepte les deux formes.
Signatures ECDSA encodées en DER
Une signature, deux encodages, et se tromper de choix compte parmi les bugs d'intégration les plus courants dans ce domaine. La paire brute R || S, chaque moitié faisant la taille du corps de la courbe, est ce que transportent JOSE et WebAuthn. Une SEQUENCE { r, s } en DER est ce que transportent X.509, CMS et TLS. sgcECDSA_SignHash ci-dessus produit la forme brute, les appels ci-dessous produisent et consomment la forme DER, et les deux convertisseurs traduisent de l'une à l'autre sans avoir à resigner quoi que ce soit.
{ sgcCrypto_ECCurves: raw keys, a digest the caller has already computed. }
function sgcECDSA_SignDER(aCurve: TsgcECCurve; const aPrivateKey, aHash: TBytes; aHashKind: TsgcECHash): TBytes; overload;
function sgcECDSA_VerifyDER(aCurve: TsgcECCurve; const aPublicKey, aHash, aDERSignature: TBytes): Boolean; overload;
function sgcECDSA_RawToDER(aCurve: TsgcECCurve; const aRaw: TBytes): TBytes;
function sgcECDSA_DERToRaw(aCurve: TsgcECCurve; const aDER: TBytes; out aRaw: TBytes): Boolean;
{ sgcCrypto_EC: PEM keys, hashes aData itself with the curve SHA. }
function sgcECDSA_SignDER(const aPrivateKeyPEM: string; const aData: TBytes; aCurveBits: Integer): TBytes; overload;
function sgcECDSA_VerifyDER(const aPublicPoint, aData, aDERSignature: TBytes; aCurveBits: Integer): Boolean; overload;
var
oRaw, oDER: TBytes;
begin
oRaw := sgcECDSA_SignHash(eccP256, oPrivateKey, oDigest, echSHA256);
oDER := sgcECDSA_RawToDER(eccP256, oRaw);
end;
aHashKind dans la forme à clé brute ne sélectionne que le HMAC derrière le nonce RFC 6979, il ne hache rien pour vous, passez donc sgcEC_DefaultHash(aCurve) lorsqu'il n'y a aucune raison de choisir autrement. sgcECDSA_RawToDER renvoie un tableau vide lorsque la paire n'est pas exactement composée de deux moitiés de la taille du corps, et sgcECDSA_DERToRaw renvoie False sur un DER malformé ou sur une valeur qui ne correspond pas à la courbe, les deux sont donc sûres à utiliser sur une entrée non fiable.
Exporter et importer des clés EC
Trois conteneurs, et c'est l'étiquette PEM qui permet de les distinguer : un SEC1 ECPrivateKey est BEGIN EC PRIVATE KEY, un PKCS#8 PrivateKeyInfo est BEGIN PRIVATE KEY, et un X.509 SubjectPublicKeyInfo est BEGIN PUBLIC KEY. Java et .NET veulent du PKCS#8, OpenSSL et la plupart des outils en ligne de commande lisent l'un ou l'autre. Aucune des deux formes privées n'est chiffrée, le résultat est donc aussi secret que le scalaire lui-même.
function sgcEC_ExportPrivateKeySEC1DER(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): TBytes;
function sgcEC_ExportPrivateKeyPKCS8DER(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): TBytes;
function sgcEC_ExportSubjectPublicKeyInfo(aCurve: TsgcECCurve; const aPublicKey: TBytes): TBytes;
function sgcEC_ExportPrivateKeySEC1PEM(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): string;
function sgcEC_ExportPrivateKeyPKCS8PEM(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): string;
function sgcEC_ExportPublicKeyPEM(aCurve: TsgcECCurve; const aPublicKey: TBytes): string;
function sgcEC_ImportPrivateKeyDER(const aDER: TBytes; out aCurve: TsgcECCurve; out aPrivateKey, aPublicKey: TBytes): Boolean;
function sgcEC_ImportPrivateKeyPEM(const aPEM: string; out aCurve: TsgcECCurve; out aPrivateKey, aPublicKey: TBytes): Boolean;
function sgcEC_ImportPublicKeyDER(const aDER: TBytes; out aCurve: TsgcECCurve; out aPublicKey: TBytes): Boolean;
function sgcEC_ImportPublicKeyPEM(const aPEM: string; out aCurve: TsgcECCurve; out aPublicKey: TBytes): Boolean;
var
oCurve: TsgcECCurve;
oPrivateKey, oPublicKey: TBytes;
begin
if sgcEC_ImportPrivateKeyPEM(Memo1.Text, oCurve, oPrivateKey, oPublicKey) then
Memo2.Text := sgcEC_ExportPrivateKeyPKCS8PEM(oCurve, oPrivateKey, oPublicKey);
end;
Les routines d'import lisent la courbe dans le fichier, ce qui explique pourquoi aCurve revient sous forme de paramètre out plutôt que d'être passée en entrée. Une courbe que l'unité n'implémente pas est un échec de négociation plutôt qu'un bug, elles renvoient donc False et ne lèvent jamais d'exception, ce qui les rend sûres sur une entrée non fiable. Le point public est recalculé à partir du scalaire lorsque le fichier l'omet, et validé par rapport à la courbe avant d'être renvoyé. Les routines d'export lèvent une exception à la place, car du matériel de clé qui ne correspond pas à la courbe est un bug de l'appelant, pas une entrée.
L'OID de la courbe nommée, sous forme pointée, qui entre dans chacun de ces conteneurs est disponible directement, et c'est ce qu'utilisent en interne les routines d'export et d'import :
function sgcEC_CurveOID(aCurve: TsgcECCurve): string;
function sgcEC_CurveFromOID(const aDotted: string; out aCurve: TsgcECCurve): Boolean;
Validez toujours la clé publique d'un pair
Avant d'utiliser une clé publique que vous n'avez pas générée vous-même, que ce soit pour ECDH, la vérification ECDSA ou ECIES, vérifiez qu'il s'agit bien d'un point sur la courbe avec sgcEC_ValidatePublicKey. Un point qui n'est pas sur la courbe peut divulguer des bits de votre propre clé privée par le biais de l'arithmétique utilisée pour le traiter.
Signatures Schnorr (BIP-340)
secp256k1 uniquement, et c'est ce qu'utilisent Taproot, Nostr et Lightning. Ce n'est pas un remplacement direct de l'ECDSA ci-dessus, car deux choses diffèrent. Les clés publiques sont x-only, sur 32 octets, la coordonnée y étant implicitement supposée paire, si bien qu'une clé BIP-340 n'est pas interchangeable avec les formes compressées de 33 octets ou non compressées de 65 octets produites par le reste de cette page. Et le défi hache la clé publique conjointement avec le nonce et le message, ce qui lie la signature à la clé d'une manière qu'une signature ECDSA ne fait pas.
function sgcSchnorr_PublicKey(const aPrivateKey: TBytes): TBytes;
function sgcSchnorr_Sign(const aPrivateKey, aMessage, aAuxRand: TBytes): TBytes;
function sgcSchnorr_Verify(const aPublicKey, aMessage, aSignature: TBytes): Boolean;
function sgcSchnorr_TaggedHash(const aTag: string; const aData: TBytes): TBytes;
var
oPrivateKey, oPublicKey, oSignature: TBytes;
begin
oPrivateKey := sgcRandomBytes(32);
oPublicKey := sgcSchnorr_PublicKey(oPrivateKey); { 32 bytes, x-only }
oSignature := sgcSchnorr_Sign(oPrivateKey, oMessage, sgcRandomBytes(32));
if sgcSchnorr_Verify(oPublicKey, oMessage, oSignature) then
{ valid };
end;
aAuxRand est constitué de 32 octets d'aléa auxiliaire mélangés dans la dérivation du nonce. C'est de la défense en profondeur contre les attaques par faute, pas le nonce lui-même : le schéma reste sûr même s'il ne contient que des zéros, et un tableau vide est traité comme 32 octets nuls afin que les vecteurs de test du BIP-340 se reproduisent exactement. Un usage réel devrait passer de l'aléa frais. sgcSchnorr_TaggedHash est exposée car les BIP-341 et BIP-342 construisent d'autres tags sur la même construction.