Criptografía de Clave Pública

La criptografía de clave pública (asimétrica) usa un par de claves en lugar de un secreto compartido: lo que la clave privada firma, la clave pública correspondiente lo verifica; lo que se sella con una clave pública, solo la clave privada correspondiente lo abre. La clave pública puede entregarse a cualquiera, incluido un adversario, sin debilitar nada.

 

RSA

 

La generación de claves busca números primos grandes y es realmente lenta, varios segundos para una clave de 2048 bits; nunca la ejecute en un hilo de interfaz. Use OAEP para cifrado y PSS para firmas, ambos incluyen un elemento aleatorizado que PKCS#1 v1.5 no tiene. La firma/verificación PKCS#1 v1.5 se proporciona en sgcCrypto_RSA para interoperar con formatos que la especifican (JWS RS256 es el caso habitual); prefiera PSS para cualquier cosa nueva.

 


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;

 

El cifrado PKCS#1 v1.5 también está disponible, el modo que Java llama RSA/NONE/PKCS1Padding. Está aquí para interoperar con formatos que lo exigen y para nada más: el relleno v1.5 es exactamente lo que ataca Bleichenbacher, así que OAEP sigue siendo la opción correcta para cualquier cosa nueva. El texto en claro está limitado al tamaño del módulo menos 11 bytes, 245 bytes con una clave 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 devuelve False para todo tipo de fallo de decodificación y sigue el mismo camino en cada uno de ellos, porque distinguirlos es precisamente lo que necesita un oráculo de relleno.

 

OAEP con un hash MGF1 independiente

 

OAEP usa un hash en dos sitios, sobre la etiqueta y dentro de la función de generación de máscara MGF1, y los dos no tienen por qué ser el mismo. Ahí es donde Java muerde. SunJCE interpreta OAEPWithSHA256AndMGF1Padding como SHA-256 para el resumen con MGF1 todavía en SHA-1, mientras que Bouncy Castle interpreta la misma cadena como SHA-256 para ambos, de modo que un texto cifrado que produce uno de ellos resulta indescifrable para el otro. Las sobrecargas de cinco parámetros fijan los dos hashes de forma independiente, que es lo que puede exigir hablar con un interlocutor Java. La forma de cuatro parámetros no cambia y pasa aHash para ambos.

 


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;

 

Una discrepancia de hash es indistinguible de un texto cifrado corrupto, así que un aMGFHash incorrecto se traduce en un simple False sin nada que indique cuál de los dos está mal. Ambos interlocutores tienen que acordarlo de antemano.

 

Las claves se exportan a PKCS#1 y SubjectPublicKeyInfo DER, y a PEM:

 


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

 

PKCS#8 es el otro contenedor de clave privada, el que se escribe como BEGIN PRIVATE KEY, y es lo que esperan Java y .NET; el par anterior emite en su lugar el PKCS#1 más antiguo, BEGIN RSA PRIVATE KEY. Ninguno de los dos contenedores está cifrado, así que lo que se obtiene es material de clave desnudo y no debe llegar a ningún registro ni copia de seguridad.

 


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

 

Importar una clave RSA

 

Hasta que existieron estas funciones, un TsgcRSAPrivateKey solo podía salir de sgcRSA_GenerateKey, lo que significaba que una clave guardada en disco no se podía usar en absoluto. Las rutinas de importación leen ambos contenedores de clave privada, PKCS#1 RSAPrivateKey y PKCS#8 PrivateKeyInfo, y ambas codificaciones de clave pública, PKCS#1 RSAPublicKey y X.509 SubjectPublicKeyInfo, en DER o en PEM. Un PKCS#8 cifrado, la forma BEGIN ENCRYPTED PRIVATE KEY, no está soportado y devuelve False. Los parámetros CRT se recalculan cuando el archivo los omite, de modo que una clave importada sigue ejecutándose a velocidad 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;

 

Un certificado no es una clave pública, así que entregue a sgcRSA_ImportPublicKeyDER el SubjectPublicKeyInfo extraído del certificado, no el certificado en sí. sgcRSA_CheckPrivateKey verifica la coherencia interna de cualquier clave que provenga de otro sitio, que n = p*q, que e*d sea congruente con 1, y que los parámetros CRT coincidan.

 

Ed25519 y Ed448 (firmas)

 

Los nonces de EdDSA se derivan de forma determinista a partir de la clave privada y del mensaje, de modo que no interviene ningún generador de números aleatorios al firmar y no existe posibilidad de que un nonce repetido filtre la clave, a diferencia de ECDSA (vea más abajo). sgcCrypto_Ed25519 es la primitiva de firma/verificación que usa el tipo de credencial EdDSA de WebAuthn en otras partes de sgcWebSockets; espera que quien la llama ya conozca la clave pública de 32 bytes (WebAuthn se la entrega desde la credencial) y no expone una llamada independiente de generación de claves o de derivación de clave pública. sgcCrypto_Ed448 es el firmante de propósito general, con aproximadamente 224 bits de seguridad frente a 128 de Ed25519, a costa de una clave y una firma mayores y una firma más 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 y X448 (acuerdo de claves)

 

Diffie-Hellman sobre curvas de Montgomery: ambas partes generan un par de claves, intercambian claves públicas, y cada una calcula el mismo secreto compartido a partir de su propia clave privada y de la clave pública de la otra parte. Pase siempre la salida en bruto por una KDF (vea Hash de Contraseñas y Derivación de Claves, HKDF) antes de usarla para cifrar cualquier cosa; la salida ECDH en bruto no es en sí misma una clave adecuada. X448 ofrece aproximadamente 224 bits de seguridad frente a 128 de X25519, a costa de una clave mayor y aproximadamente el triple de cómputo.

 


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 devuelve False para una clave de la otra parte de orden bajo, el pequeño conjunto de claves públicas que fuerzan el secreto compartido a todo ceros con independencia de su propia clave privada. Compruebe siempre el resultado en lugar de confiar en la salida incondicionalmente.

 

ECIES (cifrado de caja sellada)

 

Combina X25519, HKDF-SHA256 y XChaCha20-Poly1305 en una única llamada que cifra un mensaje para la clave pública de un destinatario, sin necesidad de ningún secreto compartido previo. aContext vincula el mensaje sellado a un propósito o a un paso del protocolo, abrirlo bajo un contexto distinto falla, lo cual impide que un mensaje sellado de un intercambio se reproduzca en otro.

 


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 en curvas con nombre

 

Dos unidades independientes cubren las curvas de Weierstrass cortas, divididas según la familia de curva que implementan:

 

sgcCrypto_EC cubre las curvas NIST (P-256, P-384, P-521) y está orientada a claves PEM y firmas JWS, la forma que esperan las bibliotecas JWT (ES256 y similares).

 

sgcCrypto_ECCurves cubre secp256k1 (la curva de Bitcoin y Ethereum) y las curvas brainpoolP256r1 / P384r1 / P512r1 de RFC 5639, trabajando directamente con bytes de clave y de firma en bruto en lugar de PEM.

 

TsgcECCurve en sgcCrypto_ECCurves ahora nombra siete curvas en lugar de cuatro: eccSecp256k1, las tres curvas Brainpool, y eccP256, eccP384 y eccP521 para NIST P-256, P-384 y P-521. Las tres NIST antes solo eran alcanzables a través de la API PEM y JWS de sgcCrypto_EC, de modo que una aplicación que tuviera una clave P-256 como bytes en bruto tenía que escribir PEM primero. Las llamadas de generación, firma, verificación, ECDH y compresión sobre clave en bruto de más abajo ahora cubren las siete. Recurra a sgcCrypto_EC cuando la tarea sea JWS, y a sgcCrypto_ECCurves cuando el material de clave ya sean bytes.

 

Ambas unidades declaran un tipo llamado TsgcECCurve y una rutina llamada sgcEC_GenerateKeyPair, con significados distintos en cada unidad. No añada ambas unidades a la misma cláusula uses y haga referencia a cualquiera de los dos nombres sin cualificar; cualifique con el nombre de la unidad (sgcCrypto_ECCurves.sgcEC_GenerateKeyPair) si una única unidad necesita genuinamente ambas.

 


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

 

Las firmas ECDSA aquí usan nonces deterministas de RFC 6979: el nonce se deriva de la clave privada y del mensaje en lugar de extraerse de un generador de números aleatorios, lo que elimina la causa más común por la que despliegues reales han perdido una clave privada ECDSA, un nonce repetido o parcialmente predecible. Las firmas llevan la normalización low-S que exigen Bitcoin (BIP 62) y Ethereum; la verificación acepta cualquiera de las dos formas.

 

Firmas ECDSA codificadas en DER

 

Una firma, dos codificaciones, y elegir la equivocada es uno de los errores de integración más comunes en esta área. El par en bruto R || S, cada mitad del tamaño del cuerpo de la curva, es lo que llevan JOSE y WebAuthn. Una SEQUENCE DER { r, s } es lo que llevan X.509, CMS y TLS. sgcECDSA_SignHash de más arriba produce la forma en bruto, las llamadas de abajo producen y consumen la forma DER, y los dos conversores traducen entre ellas sin volver a firmar 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 en la forma de clave en bruto solo elige el HMAC detrás del nonce de RFC 6979, no hashea nada por usted, así que pase sgcEC_DefaultHash(aCurve) cuando no haya motivo para elegir otra cosa. sgcECDSA_RawToDER devuelve un array vacío cuando el par no son exactamente dos mitades del tamaño del cuerpo, y sgcECDSA_DERToRaw devuelve False ante un DER malformado o un valor que no encaja en la curva, así que ambas son seguras de apuntar a entrada no confiable.

 

Exportar e importar claves EC

 

Tres contenedores, y la etiqueta PEM es lo que los distingue: un SEC1 ECPrivateKey es BEGIN EC PRIVATE KEY, un PKCS#8 PrivateKeyInfo es BEGIN PRIVATE KEY, y un X.509 SubjectPublicKeyInfo es BEGIN PUBLIC KEY. Java y .NET quieren PKCS#8, OpenSSL y la mayoría de las herramientas de línea de comandos leen cualquiera de los dos. Ninguna de las dos formas privadas está cifrada, así que el resultado es tan secreto como el propio 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;

 

Las rutinas de importación leen la curva del propio archivo, razón por la cual aCurve se devuelve como parámetro out en lugar de pasarse como entrada. Una curva que la unidad no implementa es un fallo de negociación y no un error del programa, así que devuelven False y nunca lanzan una excepción, lo que las hace seguras ante entrada no confiable. El punto público se recalcula a partir del escalar cuando el archivo lo omite, y se valida contra la curva antes de devolverlo. Las rutinas de exportación sí lanzan, porque un material de clave que no encaja con la curva es un error de quien la llama y no un dato de entrada.

 

El OID de curva con nombre en notación decimal con puntos que se incluye en cada uno de esos contenedores está disponible directamente, y es lo que usan internamente las rutinas de exportación e importación:

 


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

 

Valide siempre la clave pública de la otra parte

 

Antes de usar cualquier clave pública que no haya generado usted mismo, ya sea para ECDH, verificación ECDSA o ECIES, valide que realmente es un punto sobre la curva con sgcEC_ValidatePublicKey. Un punto que no está sobre la curva puede filtrar bits de su propia clave privada a través de la aritmética usada para procesarlo.

 

Firmas Schnorr (BIP-340)

 

Solo secp256k1, y esto es lo que usan Taproot, Nostr y Lightning. No es un sustituto directo del ECDSA anterior, porque hay dos cosas que difieren. Las claves públicas son x-only, 32 bytes con la coordenada y implícitamente par, de modo que una clave BIP-340 no es intercambiable con las formas comprimida de 33 bytes o sin comprimir de 65 bytes que produce el resto de esta página. Y el desafío hashea la clave pública junto con el nonce y el mensaje, lo que vincula la firma a la clave de una forma en que una firma ECDSA no lo hace.

 


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 son 32 bytes de aleatoriedad auxiliar mezclados en la derivación del nonce. Es defensa en profundidad frente a ataques de fallo, no el nonce en sí: el esquema sigue siendo seguro con todo ceros, y un array vacío se trata como 32 bytes cero para que los vectores de prueba de BIP-340 se reproduzcan exactamente. El uso real debería pasar aleatoriedad fresca. sgcSchnorr_TaggedHash se expone porque BIP-341 y BIP-342 construyen etiquetas adicionales sobre la misma construcción.