[dependencies]p256 = { version = "=0.14.0", features = ["ecdsa", "pem"] } use p256::ecdsa::{Signature, SigningKey, signature::Signer};use p256::pkcs8::DecodePrivateKey; fn main() -> Result<(), Box<dyn std::error::Error>> { // "BEGIN PRIVATE KEY" (PKCS#8); SigningKey::from_sec1_pem reads "BEGIN EC PRIVATE KEY" let signing_key = SigningKey::from_pkcs8_pem(&std::fs::read_to_string("key.pem")?)?; let message = std::fs::read("message.txt")?; // RFC 6979: the nonce is derived from the key and the message — no RNG, same output every run let signature: Signature = signing_key.sign(&message); let signature = signature.normalize_s(); // low-S: the form Bitcoin-style verifiers insist on std::fs::write("sig.der", signature.to_der().as_bytes())?; // ASN.1 DER std::fs::write("sig.raw", signature.to_bytes())?; // r‖s, 64 bytes let hex: String = signature.to_der().as_bytes().iter().map(|b| format!("{b:02x}")).collect(); println!("DER: {hex}"); Ok(())} How it works
SigningKey::from_pkcs8_pemreadsBEGIN PRIVATE KEY; legacyBEGIN EC PRIVATE KEYfiles load withSigningKey::from_sec1_peminstead.signfrom theSignertrait hashes the message and signs; the nonce comes from RFC 6979 (an HMAC-DRBG over the key and the digest), so no random number generator is involved.normalize_s()replaces s with n − s when s is in the upper half of the group order. Both forms are valid ECDSA; low-S is the form strict verifiers require, and it never hurts lenient ones.to_der()produces ASN.1 DER for OpenSSL-style verifiers (the run checks it withopenssl dgst -verify),to_bytes()the 64-byter‖sfor JWS and WebCrypto.
Gotchas
- Deterministic is not the same as interoperable byte-for-byte: libraries with random nonces (OpenSSL, the JDK, .NET) produce different but equally valid signatures. Compare signatures across stacks by verifying them, not by diffing bytes.
p256does not normalize s by itself. Withoutnormalize_s(), about half the signatures are high-S — accepted byp256, rejected byk256(secp256k1) verifiers and by Bitcoin consensus rules.signtakes the message, not a digest. To sign a precomputed hash usePrehashSigner::sign_prehashfrom thehazmatmodule — passing a digest tosignhashes it twice.SigningKeyzeroizes its secret on drop, but the PEMStringyou read it from is ordinary memory. Keep key files out of logs, and drop or wrap the text inzeroize::Zeroizingin long-running services.