Zum Inhalt springen
ecdsa.com
Rust

RustCrypto ecdsa (p256, p384, k256)

ECDSA in reinem Rust von RustCrypto: das Crate ecdsa mit je einem Crate pro Kurve (p256, p384, p521, k256 …) – standardmäßig RFC 6979 und rohes r‖s, aber secp256k1 setzt Low-S beim Signieren wie beim Prüfen durch, die NIST-Kurven dagegen nicht.
Lizenz
Apache-2.0 OR MIT
Geprüfte Version
0.17.0 ·
Quellcode
github.com/RustCrypto/signatures
Dokumentation
docs.rs/ecdsa/0.17.0/ecdsa/

Kompatibilität auf einen Blick

ECDSA

ECDSA-Kurven
P-256P-384secp256k1
EdDSA
none
Formate öffentlicher Schlüssel
spki-pemspki-dersec1-uncompressedsec1-compressed
Formate privater Schlüssel
pkcs8-pempkcs8-dersec1-pemraw-scalar

Das Projekt RustCrypto teilt ECDSA in ein generisches Crate ecdsa und je ein Crate pro Kurve auf – p256, p384, p521, k256 für secp256k1 und die Brainpool-Crates –, die alle die Traits des Crates signature implementieren (Signer, Verifier, PrehashSigner). Die Crates sind reines Rust ohne C-Abhängigkeiten und deshalb die übliche Wahl für WebAssembly und Embedded-Ziele. Für die Prüfung wurden ecdsa 0.17.0 sowie p256, p384 und k256 0.14.0 verwendet.

Das Signieren ist deterministisch: Die P-256-Signatur über die Testnachricht aus RFC 6979 reproduzierte den Vektor des RFC Byte für Byte. Der Typ Signature ist die 64 Byte lange rohe r‖s-Form; DER erfordert explizit to_der() und from_der(), und das Parsen der falschen Form gibt einen Fehler zurück, bevor überhaupt geprüft wird. Der Trait Signer hasht die Nachricht mit dem Standard-Hash der Kurve (SHA-256 bei P-256); PrehashSigner::sign_prehash nimmt einen Digest entgegen und lieferte für den SHA-256-Hash der Nachricht die identische Signatur.

Der Umgang mit Low-S hängt von der Kurve ab – genau das ist die Falle, wenn eine Codebasis beide Kurvenarten verarbeitet. Die Crates für die NIST-Kurven lassen s so, wie es berechnet wurde – 29 von 64 P-256-Signaturen waren High-S –, und ihre Verifizierer akzeptieren beide Hälften. k256 normalisiert s beim Signieren, und sein Verifizierer lehnte die gespiegelte High-S-Form einer gültigen Signatur ab, wie es die Regeln von Bitcoin vorsehen. Eine secp256k1-Signatur aus OpenSSL, Java oder einem Cloud-KMS scheitert in k256 deshalb etwa in der Hälfte der Fälle; rufe vorher normalize_s() für sie auf, wie es das Beispiel für P-256 zeigt.

Schlüssel lassen sich aus SPKI und PKCS#8 laden (PEM und DER, mit den Features pem und pkcs8), aus SEC1-Punkten in beiden Formen, aus privaten Schlüsseln im SEC1-Format und aus rohen Skalaren. Ed25519 liegt im separaten Crate ed25519-dalek, und Post-Quanten-Algorithmen gibt es hier nicht; für FIPS-validierte oder Post-Quanten-Primitive in Rust sieh dir aws-lc-rs an.

Weil das Signieren deterministisch ist, ist eine geänderte Signatur in einem Test hier ein echtes Anzeichen für eine Regression. Vergleiche mit anderen Bibliotheken anhand von Testvektoren und mit dem Konverter DER ⇄ Raw.

Fakten und die Prüfungen dahinter

Nichts im Folgenden ist aus einer Dokumentation abgeschrieben. Das Prüfskript content/libraries/verify/rustcrypto-ecdsa/run.sh hat RustCrypto ecdsa (p256, p384, k256) 0.17.0 am 20. Oktober 2026 im Container-Image rust:1.99.1-trixie ausgeführt; die Seite muss mit seiner Ausgabe übereinstimmen, und jeder Fakt führt die zugehörigen Zeilen auf.

  • Kodierung von ECDSA-Signaturen

    raw

    Signature ist das 64 Byte lange rohe r‖s; to_der() erzeugte eine DER-Signatur mit 71 Byte, die from_der() wieder einlas.

    Von der Prüfung aufgezeichnet
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: to_der() is 71 bytes and from_der() round-trips: Ok(true)
  • ECDSA-Nonce

    rfc6979

    RFC 6979: Signer::sign reproduzierte auf P-256 Anhang A.2.5 exakt, einschließlich des dortigen High-S-Werts.

    Von der Prüfung aufgezeichnet
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: P-256 Signer::sign reproduces RFC 6979 A.2.5 including its high s: true
  • Low-S beim Signieren

    either

    Das Crate p256 normalisiert nicht: 29 von 64 Signaturen waren High-S.

    Von der Prüfung aufgezeichnet
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: high-S among 64 signatures: P-256 29, secp256k1 0
  • Low-S beim Signieren, nach Kurve

    secp256k1

    Unter den geprüften Crates normalisiert nur k256 (secp256k1) s beim Signieren: kein High-S-Wert unter 64 Signaturen.

    Von der Prüfung aufgezeichnet
    fact low-s-sign-by-curve = secp256k1
  • High-S bei der Prüfung

    accepts-high-s

    Der Verifizierer von p256 akzeptierte die gespiegelte Signatur (r, n − s).

    Von der Prüfung aufgezeichnet
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: mirrored signature verifies: P-256 true, secp256k1 false
  • Ablehnung von High-S bei der Prüfung, nach Kurve

    secp256k1

    Der Verifizierer von k256 lehnte die gespiegelte High-S-Signatur ab; P-256 akzeptierte sie.

    Von der Prüfung aufgezeichnet
    fact low-s-verify-by-curve = secp256k1
  • Was der Signaturaufruf hasht

    hashes-message-or-digest

    Signer::sign hasht bei P-256 mit SHA-256; sign_prehash über den SHA-256-Hash der Nachricht erzeugte dieselbe Signatur, und verify_prehash akzeptierte sie.

    Von der Prüfung aufgezeichnet
    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
  • Fehlerhafte ECDSA-Signatur

    returns-error

    Dekodieren und Prüfen sind getrennt: Signature::from_slice mit DER-Bytes und Signature::from_der mit rohen Bytes geben Err zurück.

    Von der Prüfung aufgezeichnet
    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 })
  • ECDSA-Kurven

    P-256P-384secp256k1

    Signieren und Prüfen funktionierten mit P-256, P-384 und secp256k1 über die Crates p256, p384 und k256; P-224, P-521 und Brainpool sind separate Crates desselben Projekts.

    Von der Prüfung aufgezeichnet
    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
  • Formate öffentlicher Schlüssel

    spki-pemspki-dersec1-uncompressedsec1-compressed

    SPKI als PEM und DER sowie SEC1-Punkte, komprimiert oder unkomprimiert; ein 64 Byte langes x‖y ohne das Präfix wurde abgelehnt.

    Von der Prüfung aufgezeichnet
    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
  • Formate privater Schlüssel

    pkcs8-pempkcs8-dersec1-pemraw-scalar

    PKCS#8 als PEM und DER, SEC1 als PEM und der rohe 32-Byte-Skalar.

    Von der Prüfung aufgezeichnet
    fact private-key-formats = pkcs8-pem, pkcs8-der, sec1-pem, raw-scalar
  • EdDSA

    none

    Kein EdDSA in diesen Crates; Ed25519 stellt ed25519-dalek bereit.

    Von der Prüfung aufgezeichnet
    fact eddsa = none
    evidence eddsa: Ed25519 lives in the separate ed25519-dalek crate, outside the ecdsa/elliptic-curves family
  • Post-Quanten-Algorithmen

    none

    Keine Post-Quanten-Algorithmen.

    Von der Prüfung aufgezeichnet
    fact pqc = none

Verwendung

Signiere deterministisch mit p256, erzeuge die rohe und die DER-Kodierung und normalisiere s für strikte Verifizierer.
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());
}

Die Snippet-Testumgebung führt jedes Beispiel unverändert im Container-Image rust:1.99.1-trixie aus, und der Lauf schlägt fehl, wenn die Ausgabe nicht übereinstimmt.

Befehl
cargo run --quiet --release
Ausgabe muss passen (regulärer Ausdruck)
raw bytes: 64, der bytes: 7[012]\nverified: true\nlow-s verified: true

Bibliotheken