Skip to content
ecdsa.com
Rust

RustCrypto ecdsa (p256, p384, k256)

RustCrypto's pure-Rust ECDSA: the ecdsa crate with one crate per curve (p256, p384, p521, k256…) — RFC 6979 and raw r‖s by default, but secp256k1 is low-S on both sides while the NIST curves are not.
License
Apache-2.0 OR MIT
Checked version
0.17.0 ·
Source
github.com/RustCrypto/signatures
Documentation
docs.rs/ecdsa/0.17.0/ecdsa/

Compatibility at a glance

ECDSA

ECDSA curves
P-256P-384secp256k1
EdDSA
none
Public key formats
spki-pemspki-dersec1-uncompressedsec1-compressed
Private key formats
pkcs8-pempkcs8-dersec1-pemraw-scalar

The RustCrypto project splits ECDSA into a generic ecdsa crate and a crate per curve — p256, p384, p521, k256 for secp256k1 and the Brainpool crates — all implementing the signature traits (Signer, Verifier, PrehashSigner). They are pure Rust with no C dependencies, which makes them the usual choice for WebAssembly and embedded targets. The check used ecdsa 0.17.0 with p256, p384 and k256 0.14.0.

Signing is deterministic: the P-256 signature over the RFC 6979 test message reproduced the RFC's vector byte for byte. The Signature type is the 64-byte raw r‖s form; DER is an explicit to_der() and from_der(), and parsing the wrong form returns an error before any verification happens. The Signer trait hashes the message with the curve's default hash (SHA-256 for P-256); PrehashSigner::sign_prehash takes a digest and gave the identical signature for SHA-256 of the message.

Low-S handling differs by curve, which is the trap when one code base handles both. The NIST curve crates keep s as computed — 29 of 64 P-256 signatures were high-S — and their verifiers accept both halves. k256 normalizes s when signing and its verifier rejected the mirrored high-S form of a valid signature, matching Bitcoin's rules. A secp256k1 signature produced by OpenSSL, Java or a cloud KMS therefore fails in k256 about half the time; call normalize_s() on it first, as the sample shows for P-256.

Keys load from SPKI and PKCS#8 (PEM and DER, with the pem and pkcs8 features), SEC1 points in either form, SEC1 private keys and raw scalars. Ed25519 lives in the separate ed25519-dalek crate, and there are no post-quantum algorithms here; for FIPS-validated or post-quantum primitives in Rust see aws-lc-rs.

Because signing is deterministic, a changed signature in a test is a real regression signal here. Compare against other libraries with test vectors and the DER ⇄ raw converter.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/rustcrypto-ecdsa/run.sh ran RustCrypto ecdsa (p256, p384, k256) 0.17.0 in the container image rust:1.99.1-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    raw

    Signature is the 64-byte raw r‖s; to_der() produced a 71-byte DER signature that from_der() read back.

    Recorded by the check
    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 on P-256 reproduced appendix A.2.5 exactly, including its high s.

    Recorded by the check
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: P-256 Signer::sign reproduces RFC 6979 A.2.5 including its high s: true
  • Low-S when signing

    either

    The p256 crate does not normalize: 29 of 64 signatures were high-S.

    Recorded by the check
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: high-S among 64 signatures: P-256 29, secp256k1 0
  • Low-S when signing, by curve

    secp256k1

    Among the checked crates only k256 (secp256k1) normalizes s when signing: no high-S value in 64 signatures.

    Recorded by the check
    fact low-s-sign-by-curve = secp256k1
  • High-S when verifying

    accepts-high-s

    The p256 verifier accepted the mirrored signature (r, n − s).

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: mirrored signature verifies: P-256 true, secp256k1 false
  • High-S rejected when verifying, by curve

    secp256k1

    The k256 verifier rejected the mirrored high-S signature; P-256 accepted it.

    Recorded by the check
    fact low-s-verify-by-curve = secp256k1
  • What the signing call hashes

    hashes-message-or-digest

    Signer::sign hashes with SHA-256 for P-256; sign_prehash over SHA-256 of the message produced the same signature, and verify_prehash accepted it.

    Recorded by the check
    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
  • Malformed ECDSA signature

    returns-error

    Decoding is separate from verifying: Signature::from_slice on DER bytes and Signature::from_der on raw bytes return Err.

    Recorded by the check
    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 curves

    P-256P-384secp256k1

    P-256, P-384 and secp256k1 signed and verified with the p256, p384 and k256 crates; P-224, P-521 and Brainpool are separate crates of the same project.

    Recorded by the check
    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
  • Public key formats

    spki-pemspki-dersec1-uncompressedsec1-compressed

    SPKI PEM and DER, and SEC1 points compressed or uncompressed; a 64-byte x‖y without the prefix was rejected.

    Recorded by the check
    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
  • Private key formats

    pkcs8-pempkcs8-dersec1-pemraw-scalar

    PKCS#8 PEM and DER, SEC1 PEM and the raw 32-byte scalar.

    Recorded by the check
    fact private-key-formats = pkcs8-pem, pkcs8-der, sec1-pem, raw-scalar
  • EdDSA

    none

    No EdDSA in these crates; Ed25519 is provided by ed25519-dalek.

    Recorded by the check
    fact eddsa = none
    evidence eddsa: Ed25519 lives in the separate ed25519-dalek crate, outside the ecdsa/elliptic-curves family
  • Post-quantum algorithms

    none

    No post-quantum algorithms.

    Recorded by the check
    fact pqc = none

Usage

Sign deterministically with p256, produce raw and DER encodings, and normalize s for strict verifiers.
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());
}

The snippet harness runs every sample verbatim in the container image rust:1.99.1-trixie, and the run fails unless the output matches.

Command
cargo run --quiet --release
Output must match (regular expression)
raw bytes: 64, der bytes: 7[012]\nverified: true\nlow-s verified: true

Libraries