跳到主要内容
ecdsa.com
Rust

RustCrypto ecdsa (p256, p384, k256)

RustCrypto 的纯 Rust ECDSA 实现:ecdsa crate 加上每条曲线各一个 crate(p256、p384、p521、k256……)——默认使用 RFC 6979 和原始 r‖s,但 secp256k1 在签名和验证两端都执行 low-S,NIST 曲线则不然。
许可证
Apache-2.0 OR MIT
检查的版本
0.17.0 ·
源代码
github.com/RustCrypto/signatures
文档
docs.rs/ecdsa/0.17.0/ecdsa/

兼容性概览

ECDSA

ECDSA 曲线
P-256P-384secp256k1
EdDSA
none
公钥格式
spki-pemspki-dersec1-uncompressedsec1-compressed
私钥格式
pkcs8-pempkcs8-dersec1-pemraw-scalar

RustCrypto 项目把 ECDSA 拆分为通用的 ecdsa crate 和每条曲线各一个 crate——p256、p384、p521、用于 secp256k1 的 k256 以及 Brainpool 系列 crate——它们都实现了 signature crate 中的 trait(Signer、Verifier、PrehashSigner)。这些 crate 是纯 Rust 实现,没有 C 依赖,因此通常是 WebAssembly 和嵌入式目标的首选。本次检查使用的是 ecdsa 0.17.0,以及 0.14.0 版的 p256、p384 和 k256。

签名是确定性的:对 RFC 6979 测试消息的 P-256 签名逐字节复现了 RFC 中的向量。Signature 类型是 64 字节的原始 r‖s 形式;DER 需要显式调用 to_der() 和 from_der(),解析了错误的形式时,会在任何验证发生之前返回错误。Signer trait 用曲线的默认哈希算法(P-256 为 SHA-256)对消息计算哈希;PrehashSigner::sign_prehash 接受摘要,对消息的 SHA-256 摘要给出了完全相同的签名。

low-S 的处理方式因曲线而异,同一套代码同时处理两类曲线时,这就是陷阱所在。NIST 曲线的 crate 保留计算得到的 s——64 个 P-256 签名中有 29 个是 high-S——其验证器对处于高、低两个半区的 s 都接受。k256 签名时会对 s 做规范化,其验证器拒绝了有效签名经镜像得到的 high-S 形式,这与比特币的规则一致。因此,由 OpenSSL、Java 或云 KMS 生成的 secp256k1 签名大约有一半会在 k256 中验证失败;应先对它调用 normalize_s(),示例中对 P-256 签名演示了这一做法。

密钥可以从 SPKI 和 PKCS#8(PEM 和 DER,需启用 pem 和 pkcs8 feature)、两种形式的 SEC1 点、SEC1 私钥和原始标量加载。Ed25519 位于单独的 ed25519-dalek crate 中,这里也没有后量子算法;在 Rust 中需要经过 FIPS 验证的原语或后量子原语时,可参阅 aws-lc-rs。

由于签名是确定性的,测试中签名发生变化在这里是真正的回归信号。可以用测试向量和 DER ⇄ 原始格式转换器与其他库对比。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/rustcrypto-ecdsa/run.sh 于2026年10月20日在容器镜像 rust:1.99.1-trixie 中运行了 RustCrypto ecdsa (p256, p384, k256) 0.17.0;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    raw

    Signature 是 64 字节的原始 r‖s;to_der() 生成了 71 字节的 DER 签名,from_der() 能将其读回。

    检查记录
    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:P-256 上的 Signer::sign 精确复现了附录 A.2.5,连其中的 high-S 值也完全一致。

    检查记录
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: P-256 Signer::sign reproduces RFC 6979 A.2.5 including its high s: true
  • 签名时的 low-S

    either

    p256 crate 不做规范化:64 个签名中有 29 个是 high-S。

    检查记录
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: high-S among 64 signatures: P-256 29, secp256k1 0
  • 签名时的 low-S(按曲线)

    secp256k1

    在检查过的 crate 中,只有 k256(secp256k1)在签名时对 s 做规范化:64 个签名中没有 high-S 值。

    检查记录
    fact low-s-sign-by-curve = secp256k1
  • 验证时的 high-S

    accepts-high-s

    p256 的验证器接受了镜像签名 (r, n − s)。

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: mirrored signature verifies: P-256 true, secp256k1 false
  • 验证时拒绝 high-S(按曲线)

    secp256k1

    k256 的验证器拒绝了镜像得到的 high-S 签名;P-256 则接受了它。

    检查记录
    fact low-s-verify-by-curve = secp256k1
  • 签名调用对哪些内容做哈希

    hashes-message-or-digest

    对于 P-256,Signer::sign 用 SHA-256 计算哈希;对消息的 SHA-256 摘要调用 sign_prehash 生成了相同的签名,verify_prehash 也接受了它。

    检查记录
    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
  • 格式错误的 ECDSA 签名

    returns-error

    解码与验证是分开的:对 DER 字节调用 Signature::from_slice,或对原始字节调用 Signature::from_der,都会返回 Err。

    检查记录
    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 曲线

    P-256P-384secp256k1

    P-256、P-384 和 secp256k1 分别用 p256、p384 和 k256 crate 完成了签名与验证;P-224、P-521 和 Brainpool 是同一项目中的独立 crate。

    检查记录
    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
  • 公钥格式

    spki-pemspki-dersec1-uncompressedsec1-compressed

    PEM 和 DER 格式的 SPKI,以及压缩或未压缩的 SEC1 点;不带前缀的 64 字节 x‖y 被拒绝。

    检查记录
    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
  • 私钥格式

    pkcs8-pempkcs8-dersec1-pemraw-scalar

    PEM 和 DER 格式的 PKCS#8、SEC1 PEM 以及 32 字节的原始标量。

    检查记录
    fact private-key-formats = pkcs8-pem, pkcs8-der, sec1-pem, raw-scalar
  • EdDSA

    none

    这些 crate 中没有 EdDSA;Ed25519 由 ed25519-dalek 提供。

    检查记录
    fact eddsa = none
    evidence eddsa: Ed25519 lives in the separate ed25519-dalek crate, outside the ecdsa/elliptic-curves family
  • 后量子算法

    none

    不支持后量子算法。

    检查记录
    fact pqc = none

用法

用 p256 确定性地签名,生成原始编码和 DER 编码,并为严格的验证方规范化 s。
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());
}

代码片段测试工具会在容器镜像 rust:1.99.1-trixie 中原样运行每个示例,只要输出不匹配,运行就会失败。

命令
cargo run --quiet --release
输出必须匹配(正则表达式)
raw bytes: 64, der bytes: 7[012]\nverified: true\nlow-s verified: true

库