Skip to content
ecdsa.com

Verify a signature · Rust

How to verify an ECDSA signature in Rust

To verify an ECDSA signature in Rust, the RustCrypto p256 crate is the pure-Rust standard: VerifyingKey::from_public_key_pem loads the key, Signature::from_der or from_slice reads the signature, and verify checks it. The signature type makes the encoding explicit — from_der for ASN.1 DER (OpenSSL, Java), from_slice for the raw r‖s of JWS and WebCrypto.

Tested with p256 (RustCrypto) 0.14.0 (Rust 1.99.1, ecdsa 0.17.0).

Cargo.toml — p256 with ECDSA and PEM support
[dependencies]
p256 = { version = "=0.14.0", features = ["ecdsa", "pem"] }
src/bin/verify.rs — verify a DER signature
use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
use p256::pkcs8::DecodePublicKey;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// "BEGIN PUBLIC KEY" (SPKI); a key on any other curve is rejected right here
let public_key = VerifyingKey::from_public_key_pem(&std::fs::read_to_string("pub.pem")?)?;
let message = std::fs::read("message.txt")?;
// ASN.1 DER, as OpenSSL, Java and Go produce it; malformed bytes fail to parse
let signature = Signature::from_der(&std::fs::read("sig.der")?)?;
// verify() hashes the message with SHA-256 itself
match public_key.verify(&message, &signature) {
Ok(()) => println!("valid"),
Err(_) => println!("INVALID"),
}
Ok(())
}
raw r‖s signatures: same key, Signature::from_slice
// r‖s from a JWS or WebCrypto: exactly 64 bytes on P-256, no DER framing
let raw = Signature::from_slice(&std::fs::read("sig.raw")?)?;
println!("{}", if public_key.verify(&message, &raw).is_ok() { "valid" } else { "INVALID" });

How it works

  1. VerifyingKey::from_public_key_pem comes from the DecodePublicKey trait and reads SPKI (BEGIN PUBLIC KEY). The type is curve-specific: a P-384 key fails to load as p256::ecdsa::VerifyingKey.
  2. Signature::from_der parses strict ASN.1 DER and checks that r and s are valid scalars; Signature::from_slice takes exactly 64 bytes of r‖s.
  3. verify from the Verifier trait hashes the message with SHA-256 — the hash bound to P-256 in this crate — and returns Ok(()) or an opaque signature::Error.
  4. Both encodings parse into the same Signature value, so one verify call serves both; the run checks DER, raw, and a message with one changed byte.

Gotchas

  • Errors are deliberately opaque: a malformed signature, a 70-byte buffer passed to from_slice and a wrong signature all display as signature error. Log which step failed (parse or verify) yourself.
  • p256 accepts high-S signatures, but k256 (secp256k1) rejects them in verify, following Bitcoin's rule. A signature that verifies with p256 code can fail with k256 code purely because of s — call normalize_s() on the signer side.
  • With ring (the crypto behind rustls), UnparsedPublicKey expects the bare 65-byte SEC1 point, not SPKI: passing the 91-byte SPKI DER fails with Unspecified. Pick ECDSA_P256_SHA256_ASN1 for DER and ECDSA_P256_SHA256_FIXED for r‖s.
  • Loading a BEGIN CERTIFICATE file with from_public_key_pem fails: parse the certificate (for example with x509-parser) and take its SubjectPublicKeyInfo.

Related recipes