Saltar al contenido
ecdsa.com
Rust

RustCrypto ecdsa (p256, p384, k256)

El ECDSA en Rust puro de RustCrypto: el crate ecdsa con un crate por curva (p256, p384, p521, k256…); RFC 6979 y r‖s en bruto por defecto, pero secp256k1 aplica low-S al firmar y al verificar, y las curvas NIST no.
Licencia
Apache-2.0 OR MIT
Versión comprobada
0.17.0 ·
Código fuente
github.com/RustCrypto/signatures
Documentación
docs.rs/ecdsa/0.17.0/ecdsa/

Compatibilidad de un vistazo

ECDSA

Curvas ECDSA
P-256P-384secp256k1
EdDSA
none
Formatos de clave pública
spki-pemspki-dersec1-uncompressedsec1-compressed
Formatos de clave privada
pkcs8-pempkcs8-dersec1-pemraw-scalar

El proyecto RustCrypto divide ECDSA en un crate genérico ecdsa y un crate por curva —p256, p384, p521, k256 para secp256k1 y los crates Brainpool—, que implementan todos los traits de signature (Signer, Verifier, PrehashSigner). Son Rust puro, sin dependencias de C, lo que los convierte en la opción habitual para WebAssembly y para destinos embebidos. La comprobación usó ecdsa 0.17.0 con p256, p384 y k256 0.14.0.

Las firmas son deterministas: la firma P-256 sobre el mensaje de prueba del RFC 6979 reprodujo el vector del RFC byte a byte. El tipo Signature es la forma r‖s en bruto de 64 bytes; DER requiere llamar explícitamente a to_der() y from_der(), y analizar la forma equivocada devuelve un error antes de que se llegue a verificar nada. El trait Signer calcula el hash del mensaje con la función hash predeterminada de la curva (SHA-256 para P-256); PrehashSigner::sign_prehash recibe un resumen y dio la misma firma para el SHA-256 del mensaje.

El tratamiento de low-S depende de la curva, y ahí está la trampa cuando un mismo código base maneja ambos tipos de curva. Los crates de las curvas NIST dejan s tal como se calcula —29 de 64 firmas P-256 fueron high-S— y sus verificadores aceptan ambas mitades. k256 normaliza s al firmar, y su verificador rechazó la forma high-S reflejada de una firma válida, conforme a las reglas de Bitcoin. Por eso, una firma secp256k1 producida por OpenSSL, Java o un KMS en la nube falla en k256 aproximadamente la mitad de las veces; llama antes a normalize_s() sobre ella, como muestra el ejemplo para P-256.

Las claves se cargan desde SPKI y PKCS#8 (PEM y DER, con las características pem y pkcs8), desde puntos SEC1 en cualquiera de sus dos formas, desde claves privadas SEC1 y desde escalares en bruto. Ed25519 está en el crate independiente ed25519-dalek, y aquí no hay algoritmos poscuánticos; para primitivas con validación FIPS o poscuánticas en Rust, consulta aws-lc-rs.

Como la firma es determinista, aquí una firma que cambia en una prueba es una señal real de regresión. Compara con otras bibliotecas mediante los vectores de prueba y el conversor DER ⇄ en bruto.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/rustcrypto-ecdsa/run.sh ejecutó RustCrypto ecdsa (p256, p384, k256) 0.17.0 en la imagen de contenedor rust:1.99.1-trixie el 20 de octubre de 2026; la página debe coincidir con lo que imprimió, y cada dato cita esas líneas.

  • Codificación de la firma ECDSA

    raw

    Signature es el r‖s en bruto de 64 bytes; to_der() produjo una firma DER de 71 bytes que from_der() volvió a leer.

    Registrado por la comprobación
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: to_der() is 71 bytes and from_der() round-trips: Ok(true)
  • Nonce de ECDSA

    rfc6979

    RFC 6979: Signer::sign con P-256 reprodujo exactamente el apéndice A.2.5, incluida su s high-S.

    Registrado por la comprobación
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: P-256 Signer::sign reproduces RFC 6979 A.2.5 including its high s: true
  • Low-S al firmar

    either

    El crate p256 no normaliza: 29 de 64 firmas fueron high-S.

    Registrado por la comprobación
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: high-S among 64 signatures: P-256 29, secp256k1 0
  • Low-S al firmar, por curva

    secp256k1

    De los crates comprobados, solo k256 (secp256k1) normaliza s al firmar: ningún valor high-S en 64 firmas.

    Registrado por la comprobación
    fact low-s-sign-by-curve = secp256k1
  • High-S al verificar

    accepts-high-s

    El verificador de p256 aceptó la firma reflejada (r, n − s).

    Registrado por la comprobación
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: mirrored signature verifies: P-256 true, secp256k1 false
  • High-S rechazado al verificar, por curva

    secp256k1

    El verificador de k256 rechazó la firma high-S reflejada; el de P-256 la aceptó.

    Registrado por la comprobación
    fact low-s-verify-by-curve = secp256k1
  • De qué calcula el hash la llamada de firma

    hashes-message-or-digest

    Signer::sign calcula el hash con SHA-256 en P-256; sign_prehash sobre el SHA-256 del mensaje produjo la misma firma, y verify_prehash la aceptó.

    Registrado por la comprobación
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: sign_prehash(SHA-256(m)) equals sign(m): true
    evidence ecdsa-prehash: verify_prehash accepts it: true
  • Firma ECDSA mal formada

    returns-error

    La decodificación es independiente de la verificación: Signature::from_slice con bytes DER y Signature::from_der con bytes en bruto devuelven Err.

    Registrado por la comprobación
    fact ecdsa-malformed-signature = returns-error
    evidence ecdsa-malformed-signature: Signature::from_slice on DER bytes: Err(signature::Error { source: None })
    evidence ecdsa-malformed-signature: Signature::from_der on raw bytes: Err(signature::Error { source: None })
  • Curvas ECDSA

    P-256P-384secp256k1

    Con P-256, P-384 y secp256k1 se firmó y se verificó mediante los crates p256, p384 y k256; P-224, P-521 y Brainpool son crates aparte del mismo proyecto.

    Registrado por la comprobación
    fact ecdsa-curves = P-256, P-384, secp256k1
    evidence ecdsa-curves: checked crates: p256, p384, k256; p224, p521 and bp256/bp384 are separate crates of the same project
  • Formatos de clave pública

    spki-pemspki-dersec1-uncompressedsec1-compressed

    SPKI PEM y DER, y puntos SEC1 comprimidos o sin comprimir; se rechazó un x‖y de 64 bytes sin el prefijo.

    Registrado por la comprobación
    fact public-key-formats = spki-pem, spki-der, sec1-uncompressed, sec1-compressed
    evidence public-key-formats: 64-byte x||y: Err(signature::Error { source: None }); to_sec1_bytes() returns 65 bytes
  • Formatos de clave privada

    pkcs8-pempkcs8-dersec1-pemraw-scalar

    PKCS#8 PEM y DER, SEC1 PEM y el escalar en bruto de 32 bytes.

    Registrado por la comprobación
    fact private-key-formats = pkcs8-pem, pkcs8-der, sec1-pem, raw-scalar
  • EdDSA

    none

    Estos crates no incluyen EdDSA; Ed25519 lo proporciona ed25519-dalek.

    Registrado por la comprobación
    fact eddsa = none
    evidence eddsa: Ed25519 lives in the separate ed25519-dalek crate, outside the ecdsa/elliptic-curves family
  • Algoritmos poscuánticos

    none

    Sin algoritmos poscuánticos.

    Registrado por la comprobación
    fact pqc = none

Uso

Firma de forma determinista con p256, genera las codificaciones en bruto y DER, y normaliza s para los verificadores estrictos.
use p256::ecdsa::signature::{Signer, Verifier};
use p256::ecdsa::{Signature, SigningKey};

fn main() {
    let key = SigningKey::from_slice(&[0x11; 32]).expect("valid scalar");
    let message = b"order #1042 approved";

    // Deterministic RFC 6979, SHA-256 applied by the Signer trait.
    let signature: Signature = key.sign(message);
    let raw = signature.to_bytes(); // 64 bytes r||s, for WebCrypto and JOSE
    let der = signature.to_der(); // ASN.1 DER, for OpenSSL, Java and Go peers

    // p256 does not normalize s; do it before sending to a strict low-S verifier.
    let low_s = signature.normalize_s();

    let parsed = Signature::from_der(der.as_bytes()).expect("DER parses");
    println!("raw bytes: {}, der bytes: {}", raw.len(), der.as_bytes().len());
    println!("verified: {}", key.verifying_key().verify(message, &parsed).is_ok());
    println!("low-s verified: {}", key.verifying_key().verify(message, &low_s).is_ok());
}

El arnés de pruebas de fragmentos de código ejecuta cada ejemplo tal cual en la imagen de contenedor rust:1.99.1-trixie, y la ejecución falla si la salida no coincide.

Comando
cargo run --quiet --release
La salida debe coincidir (expresión regular)
raw bytes: 64, der bytes: 7[012]\nverified: true\nlow-s verified: true

Bibliotecas