Public key-cryptografie (asymmetrische cryptografie) gebruikt een sleutelpaar in plaats van een gedeeld geheim: wat de privésleutel ondertekent, verifieert de bijbehorende publieke sleutel; wat aan een publieke sleutel wordt verzegeld, opent alleen de bijbehorende privésleutel. De publieke sleutel kan aan wie dan ook worden gegeven, inclusief een tegenstander, zonder dat dit iets verzwakt.
RSA
Sleutelgeneratie zoekt naar grote priemgetallen en is echt traag, meerdere seconden voor een sleutel van 2048 bit; voer het nooit uit op een UI-thread. Gebruik OAEP voor versleuteling en PSS voor handtekeningen, beide bevatten een gerandomiseerd element dat PKCS#1 v1.5 mist. PKCS#1 v1.5 sign/verify wordt aangeboden in sgcCrypto_RSA voor interoperabiliteit met formaten die het voorschrijven (JWS RS256 is het gangbare geval); geef de voorkeur aan PSS voor iets nieuws.
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;
PKCS#1 v1.5-versleuteling wordt ook aangeboden, de modus die Java RSA/NONE/PKCS1Padding noemt. Deze is er voor interoperabiliteit met formaten die het voorschrijven en voor niets anders: v1.5-padding is precies waar de Bleichenbacher-aanval op mikt, dus OAEP blijft de juiste keuze voor iets nieuws. De platte tekst is beperkt tot de grootte van de modulus minus 11 bytes, 245 bytes bij een sleutel van 2048 bit.
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 geeft False terug voor elke soort decoderingsfout en doorloopt daarbij telkens hetzelfde pad, omdat het onderscheiden ervan precies is wat een padding-oracle nodig heeft.
OAEP met een aparte MGF1-hash
OAEP gebruikt een hash op twee plaatsen, over het label en binnen de MGF1 mask generation function, en de twee hoeven niet dezelfde te zijn. Daar bijt Java. SunJCE leest OAEPWithSHA256AndMGF1Padding als SHA-256 voor de digest met MGF1 nog steeds op SHA-1, terwijl Bouncy Castle dezelfde string leest als SHA-256 voor beide, zodat een ciphertext die de ene produceert door de andere niet te ontsleutelen is. De vijf-parameteroverloads leggen de twee hashes onafhankelijk van elkaar vast, wat nodig kan zijn om met een Java-peer te communiceren. De vier-parametervorm is ongewijzigd en geeft aHash door voor beide.
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;
Een hash-mismatch is niet te onderscheiden van een corrupte ciphertext, dus een verkeerde aMGFHash komt terug als een simpele False zonder enige aanwijzing welke van de twee fout is. Beide peers moeten dit vooraf afspreken.
Sleutels worden geëxporteerd naar PKCS#1 en SubjectPublicKeyInfo DER, en naar PEM:
function sgcRSA_ExportPrivateKeyPEM(const aKey: TsgcRSAPrivateKey): string;
function sgcRSA_ExportPublicKeyPEM(const aKey: TsgcRSAPublicKey): string;
PKCS#8 is de andere privésleutelcontainer, degene die geschreven wordt als BEGIN PRIVATE KEY, en dat is wat Java en .NET verwachten; het paar hierboven levert in plaats daarvan de oudere PKCS#1 BEGIN RSA PRIVATE KEY. Geen van beide containers is versleuteld, dus wat terugkomt is kaal sleutelmateriaal en hoort nergens thuis waar een log of een back-up bij kan.
function sgcRSA_ExportPrivateKeyPKCS8DER(const aKey: TsgcRSAPrivateKey): TBytes;
function sgcRSA_ExportPrivateKeyPKCS8PEM(const aKey: TsgcRSAPrivateKey): string;
Een RSA-sleutel importeren
Voordat deze bestonden kon een TsgcRSAPrivateKey alleen uit sgcRSA_GenerateKey komen, wat betekende dat een op schijf opgeslagen sleutel helemaal niet kon worden gebruikt. De importroutines lezen beide privésleutelcontainers, PKCS#1 RSAPrivateKey en PKCS#8 PrivateKeyInfo, en beide publieke-sleutelcoderingen, PKCS#1 RSAPublicKey en X.509 SubjectPublicKeyInfo, als DER of als PEM. Een versleutelde PKCS#8, de vorm BEGIN ENCRYPTED PRIVATE KEY, wordt niet ondersteund en geeft False terug. De CRT-parameters worden herberekend wanneer het bestand ze weglaat, zodat een geïmporteerde sleutel nog steeds op volle snelheid draait.
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;
Een certificaat is geen publieke sleutel, geef sgcRSA_ImportPublicKeyDER dus de SubjectPublicKeyInfo die uit het certificaat is gehaald, niet het certificaat zelf. sgcRSA_CheckPrivateKey verifieert de interne consistentie van alles wat van elders afkomstig is: dat n = p*q, dat e*d congruent is aan 1, en dat de CRT-parameters overeenkomen.
Ed25519 en Ed448 (handtekeningen)
EdDSA-nonces worden deterministisch afgeleid van de privésleutel en het bericht, zodat er geen generator voor willekeurige getallen betrokken is bij het ondertekenen en er geen mogelijkheid bestaat dat een herhaalde nonce de sleutel lekt, in tegenstelling tot ECDSA (zie hieronder). sgcCrypto_Ed25519 is de verify/sign-primitief die wordt gebruikt door het WebAuthn EdDSA-credentialtype elders in sgcWebSockets; het verwacht dat de aanroeper de publieke sleutel van 32 bytes al kent (WebAuthn geeft deze aan u door vanuit de credential) en biedt geen aparte aanroep voor sleutelgeneratie of publieke-sleutelafleiding. sgcCrypto_Ed448 is de general purpose ondertekenaar, met ruwweg 224 bits beveiliging tegenover 128 voor Ed25519, ten koste van een grotere sleutel en handtekening en tragere ondertekening.
{ 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 en X448 (sleuteluitwisseling)
Diffie-Hellman op Montgomery-curven: beide zijden genereren een sleutelpaar, wisselen publieke sleutels uit, en berekenen elk hetzelfde gedeelde geheim vanuit hun eigen privésleutel en de publieke sleutel van de andere zijde. Stuur de raw uitvoer altijd eerst door een KDF (zie Wachtwoordhashing & sleutelafleiding, HKDF) voordat u deze gebruikt om iets te versleutelen; de raw ECDH-uitvoer is zelf geen geschikte sleutel. X448 biedt ruwweg 224 bits beveiliging tegenover 128 voor X25519, ten koste van een grotere sleutel en ongeveer drie keer zoveel rekenwerk.
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 geeft False terug voor een low-order peersleutel, de kleine verzameling publieke sleutels die het gedeelde geheim naar allemaal nullen forceren ongeacht uw eigen privésleutel. Controleer altijd het resultaat in plaats van de uitvoer onvoorwaardelijk te vertrouwen.
ECIES (sealed box-versleuteling)
Combineert X25519, HKDF-SHA256 en XChaCha20-Poly1305 in één enkele aanroep die een bericht versleutelt naar de publieke sleutel van een ontvanger, zonder dat er vooraf een gedeeld geheim nodig is. aContext bindt het verzegelde bericht aan een doel of protocolstap; openen onder een andere context mislukt, wat voorkomt dat een verzegeld bericht uit de ene uitwisseling wordt herhaald in een andere.
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 op benoemde curven
Twee afzonderlijke units behandelen de short Weierstrass-curven, gesplitst naar de curvefamilie die ze implementeren:
sgcCrypto_EC behandelt de NIST-curven (P-256, P-384, P-521) en is gericht op PEM-sleutels en JWS-handtekeningen, de vorm die JWT-bibliotheken verwachten (ES256 en vergelijkbare).
sgcCrypto_ECCurves behandelt secp256k1 (de curve van Bitcoin en Ethereum) en de brainpoolP256r1- / P384r1- / P512r1-curven van RFC 5639, en werkt rechtstreeks met raw sleutel- en handtekeningbytes in plaats van PEM.
TsgcECCurve in sgcCrypto_ECCurves noemt nu zeven curven in plaats van vier: eccSecp256k1, de drie Brainpool-curven, en eccP256, eccP384 en eccP521 voor NIST P-256, P-384 en P-521. De drie NIST-curven waren voorheen alleen bereikbaar via de PEM- en JWS-API van sgcCrypto_EC, zodat een toepassing die een P-256-sleutel als raw bytes had, eerst PEM moest schrijven. De onderstaande raw-sleutelaanroepen voor genereren, ondertekenen, verifiëren, ECDH en comprimeren dekken nu alle zeven. Grijp naar sgcCrypto_EC wanneer de taak JWS is, en naar sgcCrypto_ECCurves wanneer het sleutelmateriaal al bytes is.
Beide units declareren een type met de naam TsgcECCurve en een routine met de naam sgcEC_GenerateKeyPair, met een andere betekenis in elke unit. Voeg niet beide units toe aan dezelfde uses-clausule en verwijs niet ongekwalificeerd naar een van beide namen; kwalificeer met de unitnaam (sgcCrypto_ECCurves.sgcEC_GenerateKeyPair) als een enkele unit echt beide nodig heeft.
{ 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;
De ECDSA-handtekeningen hier gebruiken deterministische nonces volgens RFC 6979: de nonce wordt afgeleid van de privésleutel en het bericht in plaats van getrokken uit een generator voor willekeurige getallen, wat de meest voorkomende manier wegneemt waarop echte implementaties een ECDSA-privésleutel zijn kwijtgeraakt, namelijk een herhaalde of gedeeltelijk voorspelbare nonce. Handtekeningen dragen de low-S-normalisatie die Bitcoin (BIP 62) en Ethereum vereisen; verificatie accepteert beide vormen.
DER-gecodeerde ECDSA-handtekeningen
Eén handtekening, twee coderingen, en de verkeerde kiezen is een van de meest voorkomende integratiefouten op dit gebied. Het raw paar R || S, elke helft de veldgrootte van de curve, is wat JOSE en WebAuthn dragen. Een DER SEQUENCE { r, s } is wat X.509, CMS en TLS dragen. sgcECDSA_SignHash hierboven produceert de raw vorm, de onderstaande aanroepen produceren en verwerken de DER-vorm, en de twee converters vertalen ertussen zonder iets opnieuw te ondertekenen.
{ 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 kiest in de raw-sleutelvorm alleen de HMAC achter de RFC 6979-nonce, het hasht niets voor u, geef dus sgcEC_DefaultHash(aCurve) door wanneer er geen reden is om anders te kiezen. sgcECDSA_RawToDER geeft een lege array terug wanneer het paar niet precies uit twee helften van de veldgrootte bestaat, en sgcECDSA_DERToRaw geeft False terug bij misvormde DER of bij een waarde die niet op de curve past, zodat beide veilig zijn te gebruiken op niet-vertrouwde invoer.
EC-sleutels exporteren en importeren
Drie containers, en het PEM-label is wat ze uit elkaar houdt: een SEC1 ECPrivateKey is BEGIN EC PRIVATE KEY, een PKCS#8 PrivateKeyInfo is BEGIN PRIVATE KEY, en een X.509 SubjectPublicKeyInfo is BEGIN PUBLIC KEY. Java en .NET willen PKCS#8, OpenSSL en de meeste command line-tools lezen beide. Geen van beide privévormen is versleuteld, dus het resultaat is even geheim als de scalar zelf.
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;
De importroutines lezen de curve uit het bestand, en dat is waarom aCurve terugkomt als een out-parameter in plaats van te worden meegegeven. Een curve die de unit niet implementeert is een onderhandelingsfout en geen bug, dus geven ze False terug en gooien ze nooit een exception, wat ze veilig maakt op niet-vertrouwde invoer. Het publieke punt wordt herberekend uit de scalar wanneer het bestand het weglaat, en gevalideerd tegen de curve voordat het wordt teruggegeven. De exportroutines gooien in plaats daarvan wel een exception, omdat sleutelmateriaal dat niet bij de curve past een fout van de aanroeper is en geen invoer.
De OID in punt-notatie van de benoemde curve die in elk van deze containers terechtkomt, is rechtstreeks beschikbaar, en is wat de export- en importroutines intern gebruiken:
function sgcEC_CurveOID(aCurve: TsgcECCurve): string;
function sgcEC_CurveFromOID(const aDotted: string; out aCurve: TsgcECCurve): Boolean;
Valideer altijd de publieke sleutel van een peer
Voordat u een publieke sleutel gebruikt die u niet zelf heeft gegenereerd, of het nu voor ECDH, ECDSA-verificatie of ECIES is, valideert u met sgcEC_ValidatePublicKey dat het daadwerkelijk een punt op de curve is. Een punt dat niet op de curve ligt, kan via de rekenkunde die wordt gebruikt om het te verwerken, bits van uw eigen privésleutel lekken.
Schnorr-handtekeningen (BIP-340)
Alleen secp256k1, en dit is wat Taproot, Nostr en Lightning gebruiken. Het is geen drop-in vervanging voor de ECDSA hierboven, omdat twee dingen verschillen. Publieke sleutels zijn x-only, 32 bytes met de y-coördinaat impliciet even, zodat een BIP-340-sleutel niet verwisselbaar is met de 33 byte gecomprimeerde of de 65 byte ongecomprimeerde vormen die de rest van deze pagina produceert. En de challenge hasht de publieke sleutel samen met de nonce en het bericht, wat de handtekening aan de sleutel bindt op een manier die een ECDSA-handtekening niet doet.
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 is 32 bytes aanvullende willekeurigheid die in de nonce-afleiding wordt gemengd. Het is een extra verdedigingslaag tegen foutaanvallen, niet de nonce zelf: het schema blijft veilig als deze volledig uit nullen bestaat, en een lege array wordt behandeld als 32 nulbytes, zodat de BIP-340-testvectoren exact worden gereproduceerd. Echt gebruik moet verse willekeur meegeven. sgcSchnorr_TaggedHash wordt blootgesteld omdat BIP-341 en BIP-342 verdere tags op dezelfde constructie bouwen.