Aller au contenu
ecdsa.com
Rust

RustCrypto ecdsa (p256, p384, k256)

L’ECDSA en pur Rust de RustCrypto : la crate ecdsa et une crate par courbe (p256, p384, p521, k256…), avec RFC 6979 et r‖s brut par défaut, mais secp256k1 impose low-S à la signature comme à la vérification, contrairement aux courbes NIST.
Licence
Apache-2.0 OR MIT
Version vérifiée
0.17.0 ·
Code source
github.com/RustCrypto/signatures
Documentation
docs.rs/ecdsa/0.17.0/ecdsa/

Compatibilité en un coup d’œil

ECDSA

Courbes ECDSA
P-256P-384secp256k1
EdDSA
none
Formats de clé publique
spki-pemspki-dersec1-uncompressedsec1-compressed
Formats de clé privée
pkcs8-pempkcs8-dersec1-pemraw-scalar

Le projet RustCrypto répartit ECDSA entre une crate générique ecdsa et une crate par courbe (p256, p384, p521, k256 pour secp256k1 et les crates Brainpool), qui implémentent toutes les traits de signature (Signer, Verifier, PrehashSigner). Elles sont écrites en pur Rust, sans dépendances C, ce qui en fait le choix habituel pour WebAssembly et les cibles embarquées. Les essais ont utilisé ecdsa 0.17.0 avec p256, p384 et k256 0.14.0.

La signature est déterministe : sur P-256, la signature du message de test de la RFC 6979 a reproduit octet par octet le vecteur de la RFC. Le type Signature correspond à la forme r‖s brute de 64 octets ; le DER passe explicitement par to_der() et from_der(), et l’analyse de la mauvaise forme renvoie une erreur avant toute vérification. Le trait Signer hache le message avec la fonction de hachage par défaut de la courbe (SHA-256 pour P-256) ; PrehashSigner::sign_prehash prend un condensat et a donné une signature identique pour le SHA-256 du message.

Le traitement de low-S varie selon la courbe, et c’est là le piège quand une même base de code gère les deux familles. Les crates des courbes NIST conservent s tel qu’il a été calculé (29 des 64 signatures P-256 étaient high-S), et leurs vérificateurs acceptent les deux moitiés. k256 normalise s à la signature, et son vérificateur a rejeté la forme miroir high-S d’une signature valide, conformément aux règles de Bitcoin. Une signature secp256k1 produite par OpenSSL, Java ou un KMS cloud échoue donc dans k256 environ une fois sur deux : appelez d’abord normalize_s() dessus, comme l’exemple le montre pour P-256.

Les clés se chargent depuis SPKI et PKCS#8 (PEM et DER, avec les fonctionnalités pem et pkcs8), depuis des points SEC1 compressés ou non, des clés privées SEC1 et des scalaires bruts. Ed25519 se trouve dans la crate distincte ed25519-dalek, et il n’y a ici aucun algorithme post-quantique ; pour des primitives validées FIPS ou post-quantiques en Rust, consultez aws-lc-rs.

Comme la signature est déterministe, une signature qui change dans un test signale ici une véritable régression. Comparez avec d’autres bibliothèques à l’aide des vecteurs de test et du convertisseur DER ⇄ brut.

Les faits et les vérifications qui les fondent

Rien de ce qui suit n’est copié d’une documentation. La vérification content/libraries/verify/rustcrypto-ecdsa/run.sh a exécuté RustCrypto ecdsa (p256, p384, k256) 0.17.0 dans l’image de conteneur rust:1.99.1-trixie le 20 octobre 2026 ; la page doit correspondre à ce que la vérification a affiché, et chaque fait cite les lignes en question.

  • Encodage des signatures ECDSA

    raw

    Signature est le r‖s brut de 64 octets ; to_der() a produit une signature DER de 71 octets que from_der() a relue.

    Relevé par la vérification
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: to_der() is 71 bytes and from_der() round-trips: Ok(true)
  • Nonce ECDSA

    rfc6979

    RFC 6979 : Signer::sign sur P-256 a reproduit exactement l’annexe A.2.5, y compris sa valeur de s élevée (high-S).

    Relevé par la vérification
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: P-256 Signer::sign reproduces RFC 6979 A.2.5 including its high s: true
  • Low-S à la signature

    either

    La crate p256 ne normalise pas : 29 signatures sur 64 étaient high-S.

    Relevé par la vérification
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: high-S among 64 signatures: P-256 29, secp256k1 0
  • Low-S à la signature, par courbe

    secp256k1

    Parmi les crates testées, seule k256 (secp256k1) normalise s à la signature : aucune valeur high-S sur 64 signatures.

    Relevé par la vérification
    fact low-s-sign-by-curve = secp256k1
  • High-S à la vérification

    accepts-high-s

    Le vérificateur de p256 a accepté la signature miroir (r, n − s).

    Relevé par la vérification
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: mirrored signature verifies: P-256 true, secp256k1 false
  • High-S rejeté à la vérification, par courbe

    secp256k1

    Le vérificateur de k256 a rejeté la signature miroir high-S ; celui de P-256 l’a acceptée.

    Relevé par la vérification
    fact low-s-verify-by-curve = secp256k1
  • Ce que l’appel de signature hache

    hashes-message-or-digest

    Signer::sign hache avec SHA-256 pour P-256 ; sign_prehash appliqué au SHA-256 du message a produit la même signature, et verify_prehash l’a acceptée.

    Relevé par la vérification
    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
  • Signature ECDSA mal formée

    returns-error

    Le décodage est distinct de la vérification : Signature::from_slice sur des octets DER et Signature::from_der sur des octets bruts renvoient Err.

    Relevé par la vérification
    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 })
  • Courbes ECDSA

    P-256P-384secp256k1

    Signature et vérification réussies avec P-256, P-384 et secp256k1 via les crates p256, p384 et k256 ; P-224, P-521 et Brainpool sont des crates distinctes du même projet.

    Relevé par la vérification
    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
  • Formats de clé publique

    spki-pemspki-dersec1-uncompressedsec1-compressed

    SPKI PEM et DER, et points SEC1 compressés ou non compressés ; un x‖y de 64 octets sans le préfixe a été rejeté.

    Relevé par la vérification
    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
  • Formats de clé privée

    pkcs8-pempkcs8-dersec1-pemraw-scalar

    PKCS#8 PEM et DER, SEC1 PEM et le scalaire brut de 32 octets.

    Relevé par la vérification
    fact private-key-formats = pkcs8-pem, pkcs8-der, sec1-pem, raw-scalar
  • EdDSA

    none

    Pas d’EdDSA dans ces crates ; Ed25519 est fourni par ed25519-dalek.

    Relevé par la vérification
    fact eddsa = none
    evidence eddsa: Ed25519 lives in the separate ed25519-dalek crate, outside the ecdsa/elliptic-curves family
  • Algorithmes post-quantiques

    none

    Aucun algorithme post-quantique.

    Relevé par la vérification
    fact pqc = none

Utilisation

Signer de façon déterministe avec p256, produire les encodages brut et DER, et normaliser s pour les vérificateurs stricts.
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());
}

Le banc de test des extraits de code exécute chaque exemple tel quel dans l’image de conteneur rust:1.99.1-trixie, et l’exécution échoue si la sortie ne correspond pas.

Commande
cargo run --quiet --release
La sortie doit correspondre à cette expression régulière
raw bytes: 64, der bytes: 7[012]\nverified: true\nlow-s verified: true

Bibliothèques