Crittografia a chiave pubblica

La crittografia a chiave pubblica (asimmetrica) utilizza una coppia di chiavi invece di un segreto condiviso: qualsiasi cosa firmata dalla chiave privata viene verificata dalla chiave pubblica corrispondente; qualsiasi cosa sigillata per una chiave pubblica può essere aperta solo dalla chiave privata corrispondente. La chiave pubblica può essere consegnata a chiunque, compreso un avversario, senza indebolire nulla.

RSA

La generazione delle chiavi cerca numeri primi grandi ed è genuinamente lenta, diversi secondi per una chiave a 2048 bit; non eseguirla mai su un thread di UI. Utilizzare OAEP per la cifratura e PSS per le firme, entrambi includono un elemento randomizzato che manca a PKCS#1 v1.5. La firma/verifica PKCS#1 v1.5 è fornita in sgcCrypto_RSA per interoperare con formati che la specificano (JWS RS256 è il caso comune); preferire PSS per qualsiasi cosa nuova.


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;

È fornita anche la cifratura PKCS#1 v1.5, la modalità che Java chiama RSA/NONE/PKCS1Padding. È qui solo per interoperare con formati che la impongono e per nient'altro: il padding v1.5 è esattamente ciò che l'attacco di Bleichenbacher prende di mira, quindi OAEP resta la scelta giusta per qualsiasi cosa nuova. Il plaintext è limitato alla dimensione del modulo meno 11 byte, 245 byte con una chiave a 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 restituisce False per ogni tipo di errore di decodifica e segue lo stesso percorso per ciascuno di essi, perché distinguerli è esattamente ciò di cui ha bisogno un oracolo di padding.

OAEP con un hash MGF1 separato

OAEP utilizza un hash in due punti, sull'etichetta e all'interno della funzione di generazione della maschera MGF1, e i due non devono necessariamente coincidere. È qui che Java morde. SunJCE interpreta OAEPWithSHA256AndMGF1Padding come SHA-256 per il digest con MGF1 ancora su SHA-1, mentre Bouncy Castle interpreta la stessa stringa come SHA-256 per entrambi, quindi un ciphertext prodotto da uno dei due non è decifrabile dall'altro. I due overload a cinque parametri fissano i due hash in modo indipendente, ciò che può essere necessario per colloquiare con un peer Java. La forma a quattro parametri resta invariata e passa aHash per entrambi.


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: hash dell'etichetta SHA-256, MGF1 su SHA-1. }
  oCipher := sgcRSA_OAEP_Encrypt(sgcRSA_PublicKey(oKey), oPlain, nil, rhSHA256, rhSHA1);
end;

Una mancata corrispondenza di hash è indistinguibile da un ciphertext corrotto, quindi un aMGFHash errato viene restituito come un semplice False, senza alcuna indicazione su quale dei due sia sbagliato. Entrambi i peer devono concordarlo in anticipo.

Le chiavi vengono esportate in PKCS#1 e SubjectPublicKeyInfo DER, e in PEM:


function sgcRSA_ExportPrivateKeyPEM(const aKey: TsgcRSAPrivateKey): string;
function sgcRSA_ExportPublicKeyPEM(const aKey: TsgcRSAPublicKey): string;

PKCS#8 è l'altro contenitore per chiavi private, quello scritto come BEGIN PRIVATE KEY, ed è ciò che Java e .NET si aspettano; la coppia sopra emette invece il più datato PKCS#1 BEGIN RSA PRIVATE KEY. Nessuno dei due contenitori è cifrato, quindi ciò che si ottiene è materiale di chiave allo stato grezzo, che non deve finire in nessun luogo raggiungibile da un log o da un backup.


function sgcRSA_ExportPrivateKeyPKCS8DER(const aKey: TsgcRSAPrivateKey): TBytes;
function sgcRSA_ExportPrivateKeyPKCS8PEM(const aKey: TsgcRSAPrivateKey): string;

Importazione di una chiave RSA

Prima che queste esistessero, una TsgcRSAPrivateKey poteva provenire soltanto da sgcRSA_GenerateKey, il che significava che una chiave salvata su disco non poteva essere utilizzata affatto. Le routine di importazione leggono entrambi i contenitori di chiave privata, PKCS#1 RSAPrivateKey e PKCS#8 PrivateKeyInfo, ed entrambe le codifiche di chiave pubblica, PKCS#1 RSAPublicKey e X.509 SubjectPublicKeyInfo, sia come DER che come PEM. Un PKCS#8 cifrato, la forma BEGIN ENCRYPTED PRIVATE KEY, non è supportato e restituisce False. I parametri CRT vengono ricalcolati quando il file li omette, quindi una chiave importata funziona comunque alla massima velocità.


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 certificato non è una chiave pubblica, quindi passare a sgcRSA_ImportPublicKeyDER il SubjectPublicKeyInfo estratto dal certificato, non il certificato stesso. sgcRSA_CheckPrivateKey verifica la coerenza interna di qualsiasi chiave proveniente da altrove, cioè che n = p*q, che e*d sia congruente a 1, e che i parametri CRT corrispondano.

Ed25519 e Ed448 (firme)

I nonce EdDSA sono derivati deterministicamente dalla chiave privata e dal messaggio, quindi non è coinvolto alcun generatore di numeri casuali nella firma e non esiste la possibilità che un nonce ripetuto riveli la chiave, a differenza di ECDSA (vedere sotto). sgcCrypto_Ed25519 è la primitiva di verifica/firma utilizzata dal tipo di credenziale WebAuthn EdDSA altrove in sgcWebSockets; presuppone che il chiamante conosca già la chiave pubblica a 32 byte (WebAuthn la fornisce dalla credenziale) e non espone una chiamata separata per la generazione della chiave o la derivazione della chiave pubblica. sgcCrypto_Ed448 è il firmatario general purpose, con circa 224 bit di sicurezza contro i 128 di Ed25519, a costo di una chiave e una firma più grandi e di una firma più lenta.


{ 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 e X448 (key agreement)

Diffie-Hellman su curve di Montgomery: entrambe le parti generano una coppia di chiavi, si scambiano le chiavi pubbliche, e ciascuna calcola lo stesso segreto condiviso a partire dalla propria chiave privata e dalla chiave pubblica dell'altra parte. Eseguire sempre l'output grezzo attraverso una KDF (vedere Hashing delle password e derivazione delle chiavi, HKDF) prima di utilizzarlo per cifrare qualsiasi cosa; l'output ECDH grezzo non è di per sé una chiave adatta. X448 offre circa 224 bit di sicurezza contro i 128 di X25519, a costo di una chiave più grande e di circa tre volte il calcolo.


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 restituisce False per una chiave peer di ordine basso, il piccolo insieme di chiavi pubbliche che forzano il segreto condiviso a tutti zero indipendentemente dalla propria chiave privata. Verificare sempre il risultato anziché fidarsi incondizionatamente dell'output.

ECIES (cifratura sealed box)

Combina X25519, HKDF-SHA256 e XChaCha20-Poly1305 in un'unica chiamata che cifra un messaggio verso la chiave pubblica di un destinatario, senza bisogno di alcun segreto condiviso preesistente. aContext vincola il messaggio sigillato a uno scopo o a un passaggio di protocollo, l'apertura con un contesto diverso fallisce, il che impedisce che un messaggio sigillato di uno scambio venga riproposto in un altro.


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 su curve con nome

Due unit separate coprono le curve short Weierstrass, suddivise in base alla famiglia di curve che implementano:

sgcCrypto_EC copre le curve NIST (P-256, P-384, P-521) ed è orientata verso chiavi PEM e firme JWS, la forma che le librerie JWT si aspettano (ES256 e simili).

sgcCrypto_ECCurves copre secp256k1 (la curva di Bitcoin ed Ethereum) e le curve brainpoolP256r1 / P384r1 / P512r1 dell'RFC 5639, lavorando direttamente su byte grezzi di chiave e firma anziché su PEM.

TsgcECCurve in sgcCrypto_ECCurves ora nomina sette curve anziché quattro: eccSecp256k1, le tre curve Brainpool, ed eccP256, eccP384 ed eccP521 per NIST P-256, P-384 e P-521. Le tre curve NIST erano in precedenza raggiungibili solo tramite l'API PEM e JWS di sgcCrypto_EC, quindi un'applicazione che deteneva una chiave P-256 come byte grezzi doveva prima scrivere un PEM. Le chiamate di generazione, firma, verifica, ECDH e compressione su chiave raw riportate sotto coprono ora tutte e sette. Rivolgersi a sgcCrypto_EC quando il compito è JWS, e a sgcCrypto_ECCurves quando il materiale di chiave è già in byte.

Entrambe le unit dichiarano un tipo di nome TsgcECCurve e una routine di nome sgcEC_GenerateKeyPair, con significati diversi in ciascuna unit. Non aggiungere entrambe le unit alla stessa clausola uses e fare riferimento a uno dei due nomi senza qualificarlo; qualificarlo con il nome dell'unit (sgcCrypto_ECCurves.sgcEC_GenerateKeyPair) se un'unica unit ha davvero bisogno di entrambe.


{ 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;

Le firme ECDSA qui utilizzano nonce deterministici RFC 6979: il nonce viene derivato dalla chiave privata e dal messaggio anziché essere estratto da un generatore di numeri casuali, il che elimina la causa più comune in assoluto con cui le implementazioni reali hanno perso una chiave privata ECDSA, ovvero un nonce ripetuto o parzialmente prevedibile. Le firme portano la normalizzazione low-S richiesta da Bitcoin (BIP 62) ed Ethereum; la verifica accetta entrambe le forme.

Firme ECDSA codificate in DER

Una firma, due codifiche, e scegliere quella sbagliata è tra i bug di integrazione più comuni in quest'area. La coppia raw R || S, ciascuna metà pari alla dimensione del campo della curva, è ciò che JOSE e WebAuthn trasportano. Una DER SEQUENCE { r, s } è ciò che X.509, CMS e TLS trasportano. sgcECDSA_SignHash sopra produce la forma raw, le chiamate sotto producono e consumano la forma DER, e i due convertitori traducono dall'una all'altra senza rifirmare nulla.


{ sgcCrypto_ECCurves: chiavi raw, un digest che il chiamante ha già calcolato. }
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: chiavi PEM, esegue l'hash di aData stesso con lo SHA della curva. }
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 nella forma a chiave raw sceglie soltanto l'HMAC dietro il nonce RFC 6979, non esegue alcun hash per conto proprio, quindi passare sgcEC_DefaultHash(aCurve) quando non c'è motivo di scegliere altrimenti. sgcECDSA_RawToDER restituisce un array vuoto quando la coppia non è esattamente costituita da due metà della dimensione del campo, e sgcECDSA_DERToRaw restituisce False su un DER malformato o su un valore che non si adatta alla curva, quindi entrambe sono sicure da puntare su input non fidato.

Esportazione e importazione di chiavi EC

Tre contenitori, ed è l'etichetta PEM a distinguerli: una SEC1 ECPrivateKey è BEGIN EC PRIVATE KEY, una PKCS#8 PrivateKeyInfo è BEGIN PRIVATE KEY, e una X.509 SubjectPublicKeyInfo è BEGIN PUBLIC KEY. Java e .NET vogliono PKCS#8, OpenSSL e la maggior parte degli strumenti a riga di comando leggono entrambe. Nessuna delle due forme private è cifrata, quindi il risultato è segreto quanto lo scalare stesso.


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;

Le routine di importazione leggono la curva dal file, motivo per cui aCurve viene restituito come parametro out anziché essere passato in ingresso. Una curva che l'unit non implementa è un fallimento di negoziazione, non un bug, quindi restituiscono False e non sollevano mai un'eccezione, il che le rende sicure su input non fidato. Il punto pubblico viene ricalcolato dallo scalare quando il file lo omette, e convalidato rispetto alla curva prima di essere restituito. Le routine di esportazione invece sollevano un'eccezione, perché materiale di chiave che non corrisponde alla curva è un bug del chiamante, non un input.

L'OID della curva con nome puntato, che va in ciascuno di questi contenitori, è disponibile direttamente, ed è ciò che le routine di esportazione e importazione utilizzano internamente:


function sgcEC_CurveOID(aCurve: TsgcECCurve): string;
function sgcEC_CurveFromOID(const aDotted: string; out aCurve: TsgcECCurve): Boolean;

Convalidare sempre la chiave pubblica di un peer

Prima di utilizzare una chiave pubblica non generata da sé stessi, sia per ECDH, verifica ECDSA o ECIES, convalidare che sia effettivamente un punto sulla curva con sgcEC_ValidatePublicKey. Un punto che non si trova sulla curva può rivelare bit della propria chiave privata attraverso l'aritmetica utilizzata per elaborarlo.

Firme Schnorr (BIP-340)

Solo secp256k1, ed è ciò che utilizzano Taproot, Nostr e Lightning. Non è un sostituto immediato dell'ECDSA descritto sopra, perché due aspetti differiscono. Le chiavi pubbliche sono x-only, 32 byte con la coordinata y implicitamente pari, quindi una chiave BIP-340 non è intercambiabile con le forme compresse a 33 byte o non compresse a 65 byte che il resto di questa pagina produce. Inoltre la challenge esegue l'hash della chiave pubblica insieme al nonce e al messaggio, il che vincola la firma alla chiave in un modo in cui una firma ECDSA non lo fa.


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 byte, x-only }
  oSignature := sgcSchnorr_Sign(oPrivateKey, oMessage, sgcRandomBytes(32));
  if sgcSchnorr_Verify(oPublicKey, oMessage, oSignature) then
    { valid };
end;

aAuxRand è costituito da 32 byte di casualità ausiliaria mescolati nella derivazione del nonce. È una difesa in profondità contro gli attacchi fault, non il nonce stesso: lo schema resta sicuro anche con tutti zeri, e un array vuoto viene trattato come 32 byte zero, in modo che i vettori di test BIP-340 si riproducano esattamente. L'uso reale dovrebbe passare casualità fresca. sgcSchnorr_TaggedHash è esposta perché BIP-341 e BIP-342 costruiscono ulteriori tag sulla stessa costruzione.