A criptografia de chave pública (assimétrica) usa um par de chaves em vez de um segredo compartilhado: o que a chave privada assina, a chave pública correspondente verifica; o que é selado para uma chave pública, apenas a chave privada correspondente abre. A chave pública pode ser entregue a qualquer pessoa, inclusive a um adversário, sem enfraquecer nada.
RSA
A geração de chave busca primos grandes e é genuinamente lenta, vários segundos para uma chave de 2048 bits; nunca a execute em uma thread de UI. Use OAEP para criptografia e PSS para assinaturas, ambos incluem um elemento aleatorizado que o PKCS#1 v1.5 não tem. A assinatura/verificação PKCS#1 v1.5 é fornecida em sgcCrypto_RSA para interoperar com formatos que a especificam (o RS256 do JWS é o caso comum); prefira PSS para qualquer coisa nova.
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;
A criptografia PKCS#1 v1.5 também é fornecida, o modo que o Java chama de RSA/NONE/PKCS1Padding. Ela está aqui para interoperar com formatos que a exigem e para mais nada: o padding v1.5 é exatamente o alvo do ataque de Bleichenbacher, então o OAEP continua sendo a escolha certa para qualquer coisa nova. O texto plano fica limitado ao tamanho do módulo menos 11 bytes, 245 bytes com uma chave 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 retorna False para todo tipo de falha de decodificação e segue o mesmo caminho em cada uma delas, porque distingui-las é exatamente o que um oráculo de padding precisa.
OAEP com um hash de MGF1 separado
O OAEP usa um hash em dois lugares, sobre o label e dentro da função de geração de máscara MGF1, e os dois não precisam ser o mesmo. É aí que o Java morde. O SunJCE lê OAEPWithSHA256AndMGF1Padding como SHA-256 para o digest com o MGF1 ainda em SHA-1, enquanto o Bouncy Castle lê a mesma string como SHA-256 para os dois, de modo que um texto cifrado produzido por um deles é indecifrável pelo outro. As cinco sobrecargas de parâmetros fixam os dois hashes de forma independente, o que conversar com um peer Java pode exigir. A forma de quatro parâmetros permanece inalterada e passa aHash para os dois.
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;
Uma incompatibilidade de hash é indistinguível de um texto cifrado corrompido, de modo que um aMGFHash errado volta como um simples False, sem nada que diga qual dos dois está errado. Os dois lados precisam concordar sobre isso com antecedência.
As chaves são exportadas para PKCS#1 e SubjectPublicKeyInfo DER, e para PEM:
function sgcRSA_ExportPrivateKeyPEM(const aKey: TsgcRSAPrivateKey): string;
function sgcRSA_ExportPublicKeyPEM(const aKey: TsgcRSAPublicKey): string;
O PKCS#8 é o outro contêiner de chave privada, o que é escrito como BEGIN PRIVATE KEY, e é o que o Java e o .NET esperam; o par acima emite, em vez disso, o PKCS#1 mais antigo, BEGIN RSA PRIVATE KEY. Nenhum dos dois contêineres é criptografado, então o que volta é material de chave nu, e não deve estar em nenhum lugar que um log ou um backup possa alcançar.
function sgcRSA_ExportPrivateKeyPKCS8DER(const aKey: TsgcRSAPrivateKey): TBytes;
function sgcRSA_ExportPrivateKeyPKCS8PEM(const aKey: TsgcRSAPrivateKey): string;
Importando uma chave RSA
Até estas existirem, uma TsgcRSAPrivateKey só podia vir de sgcRSA_GenerateKey, o que significava que uma chave salva em disco não podia ser usada de forma alguma. As rotinas de importação leem os dois contêineres de chave privada, o PKCS#1 RSAPrivateKey e o PKCS#8 PrivateKeyInfo, e as duas codificações de chave pública, o PKCS#1 RSAPublicKey e o X.509 SubjectPublicKeyInfo, seja como DER ou como PEM. Um PKCS#8 criptografado, a forma BEGIN ENCRYPTED PRIVATE KEY, não é suportado e retorna False. Os parâmetros CRT são recalculados quando o arquivo os omite, de modo que uma chave importada continua rodando na velocidade máxima.
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;
Um certificado não é uma chave pública, então passe a sgcRSA_ImportPublicKeyDER o SubjectPublicKeyInfo extraído do certificado, não o certificado em si. sgcRSA_CheckPrivateKey verifica a consistência interna de qualquer coisa que tenha vindo de outro lugar, que n = p*q, que e*d seja congruente a 1, e que os parâmetros CRT correspondam.
Ed25519 e Ed448 (assinaturas)
Os nonces do EdDSA são derivados deterministicamente a partir da chave privada e da mensagem, de modo que não há nenhum gerador de números aleatórios envolvido na assinatura e nenhuma possibilidade de um nonce repetido vazar a chave, diferente do ECDSA (veja abaixo). sgcCrypto_Ed25519 é a primitiva de assinar/verificar usada pelo tipo de credencial EdDSA do WebAuthn em outras partes do sgcWebSockets; ela espera que o chamador já conheça a chave pública de 32 bytes (o WebAuthn entrega essa chave a partir da credencial) e não expõe uma chamada separada de geração de chave ou de derivação de chave pública. sgcCrypto_Ed448 é o assinador de propósito geral, com aproximadamente 224 bits de segurança contra 128 do Ed25519, ao custo de uma chave e uma assinatura maiores e de uma assinatura mais 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 (acordo de chave)
Diffie-Hellman em curvas de Montgomery: os dois lados geram um par de chaves, trocam as chaves públicas, e cada um calcula o mesmo segredo compartilhado a partir da sua própria chave privada e da chave pública do outro lado. Sempre passe a saída bruta por um KDF (veja Hashing de Senhas & Derivação de Chaves, HKDF) antes de usá-la para criptografar qualquer coisa; a saída bruta do ECDH não é, por si só, uma chave adequada. O X448 oferece aproximadamente 224 bits de segurança contra 128 do X25519, ao custo de uma chave maior e de cerca de três vezes mais computação.
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 retorna False para uma chave de par de baixa ordem, o pequeno conjunto de chaves públicas que força o segredo compartilhado a ficar todo zerado independentemente da sua própria chave privada. Sempre verifique o resultado em vez de confiar incondicionalmente na saída.
ECIES (criptografia sealed box)
Combina X25519, HKDF-SHA256 e XChaCha20-Poly1305 em uma única chamada que criptografa uma mensagem para a chave pública de um destinatário, sem nenhum segredo compartilhado prévio necessário. aContext vincula a mensagem selada a um propósito ou a uma etapa de protocolo, abrir sob um contexto diferente falha, o que impede que uma mensagem selada de uma troca seja reproduzida em outra.
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 em curvas nomeadas
Duas units separadas cobrem as curvas de Weierstrass curtas, divididas por qual família de curvas cada uma implementa:
sgcCrypto_EC cobre as curvas NIST (P-256, P-384, P-521) e é orientada em torno de chaves PEM e assinaturas JWS, o formato que as bibliotecas JWT esperam (ES256 e semelhantes).
sgcCrypto_ECCurves cobre secp256k1 (a curva do Bitcoin e do Ethereum) e as curvas brainpoolP256r1 / P384r1 / P512r1 da RFC 5639, trabalhando diretamente sobre bytes brutos de chave e de assinatura em vez de PEM.
TsgcECCurve em sgcCrypto_ECCurves agora nomeia sete curvas em vez de quatro: eccSecp256k1, as três curvas Brainpool, e eccP256, eccP384 e eccP521 para as NIST P-256, P-384 e P-521. As três NIST antes só eram alcançáveis através da API de PEM e JWS de sgcCrypto_EC, de modo que uma aplicação que tivesse uma chave P-256 como bytes brutos precisava primeiro escrever um PEM. As chamadas abaixo de geração de chave bruta, assinatura, verificação, ECDH e compressão agora cobrem as sete. Recorra a sgcCrypto_EC quando a tarefa for JWS, e a sgcCrypto_ECCurves quando o material de chave já estiver em bytes.
As duas units declaram um tipo chamado TsgcECCurve e uma rotina chamada sgcEC_GenerateKeyPair, com significados diferentes em cada unit. Não adicione as duas units à mesma cláusula uses e referencie qualquer um dos nomes sem qualificação; qualifique com o nome da unit (sgcCrypto_ECCurves.sgcEC_GenerateKeyPair) se uma única unit realmente precisar das duas.
{ 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;
As assinaturas ECDSA aqui usam nonces determinísticos da RFC 6979: o nonce é derivado da chave privada e da mensagem em vez de vir de um gerador de números aleatórios, o que elimina a forma mais comum pela qual implantações reais já perderam uma chave privada ECDSA, um nonce repetido ou parcialmente previsível. As assinaturas carregam a normalização low-S que o Bitcoin (BIP 62) e o Ethereum exigem; a verificação aceita qualquer uma das duas formas.
Assinaturas ECDSA codificadas em DER
Uma assinatura, duas codificações, e escolher a errada está entre os bugs de integração mais comuns nesta área. O par bruto R || S, cada metade do tamanho do campo da curva, é o que o JOSE e o WebAuthn carregam. Um DER SEQUENCE { r, s } é o que o X.509, o CMS e o TLS carregam. sgcECDSA_SignHash, acima, produz a forma bruta; as chamadas abaixo produzem e consomem a forma DER, e os dois conversores traduzem entre elas sem reassinar nada.
{ 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, na forma de chave bruta, só escolhe o HMAC por trás do nonce da RFC 6979, ele não faz hash de nada para você, então passe sgcEC_DefaultHash(aCurve) quando não houver motivo para escolher outra coisa. sgcECDSA_RawToDER retorna um array vazio quando o par não é exatamente duas metades do tamanho do campo, e sgcECDSA_DERToRaw retorna False em um DER malformado ou em um valor que não cabe na curva, de modo que os dois são seguros para apontar para entrada não confiável.
Exportando e importando chaves EC
Três contêineres, e o rótulo do PEM é o que os diferencia: um SEC1 ECPrivateKey é BEGIN EC PRIVATE KEY, um PKCS#8 PrivateKeyInfo é BEGIN PRIVATE KEY, e um X.509 SubjectPublicKeyInfo é BEGIN PUBLIC KEY. O Java e o .NET querem PKCS#8; o OpenSSL e a maioria das ferramentas de linha de comando leem qualquer um dos dois. Nenhuma das formas privadas é criptografada, então o resultado é tão secreto quanto o próprio escalar.
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;
As rotinas de importação leem a curva a partir do arquivo, e é por isso que aCurve volta como um parâmetro out em vez de ser passado na chamada. Uma curva que a unit não implementa é uma falha de negociação e não um bug, então elas retornam False e nunca disparam uma exceção, o que as torna seguras com entrada não confiável. O ponto público é recalculado a partir do escalar quando o arquivo o omite, e validado em relação à curva antes de ser devolvido. As rotinas de exportação, em vez disso, disparam uma exceção, porque material de chave que não corresponde à curva é um bug do chamador, e não uma entrada.
O OID de curva nomeada, no formato pontilhado, que vai dentro de cada um desses contêineres está disponível diretamente, e é o que as rotinas de exportação e importação usam internamente:
function sgcEC_CurveOID(aCurve: TsgcECCurve): string;
function sgcEC_CurveFromOID(const aDotted: string; out aCurve: TsgcECCurve): Boolean;
Sempre valide a chave pública de um peer
Antes de usar qualquer chave pública que você não gerou você mesmo, seja para ECDH, verificação ECDSA ou ECIES, valide que ela é de fato um ponto na curva com sgcEC_ValidatePublicKey. Um ponto que não está na curva pode vazar bits da sua própria chave privada através da aritmética usada para processá-lo.
Assinaturas Schnorr (BIP-340)
Apenas secp256k1, e é isso que o Taproot, o Nostr e o Lightning usam. Não é um substituto direto para o ECDSA acima, porque duas coisas são diferentes. As chaves públicas são x-only, 32 bytes com a coordenada y implicitamente par, então uma chave BIP-340 não é intercambiável com as formas comprimida de 33 bytes ou não comprimida de 65 bytes que o restante desta página produz. E o desafio faz hash da chave pública junto com o nonce e a mensagem, o que vincula a assinatura à chave de um jeito que uma assinatura ECDSA não faz.
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 são 32 bytes de aleatoriedade auxiliar misturados na derivação do nonce. É defesa em profundidade contra ataques de falha, não o nonce em si: o esquema permanece seguro mesmo com ele todo zerado, e um array vazio é tratado como 32 bytes zero, de modo que os vetores de teste do BIP-340 sejam reproduzidos exatamente. Um uso real deve passar aleatoriedade fresca. sgcSchnorr_TaggedHash é exposta porque a BIP-341 e a BIP-342 constroem tags adicionais sobre a mesma construção.