跳到主要内容
ecdsa.com
Rust

ring

Brian Smith 开发的 ring,rustls 和众多 Web 服务器底层的 Rust 密码学库:API 刻意保持精简,由算法常量决定签名编码,私钥只支持 PKCS#8,曲线为 P-256、P-384 和 Ed25519。
许可证
Apache-2.0 AND ISC
检查的版本
0.17.14 ·
源代码
github.com/briansmith/ring
文档
docs.rs/ring/0.17.14/ring/

兼容性概览

ECDSAEdDSA

ECDSA 曲线
P-256P-384
EdDSA
Ed25519
公钥格式
sec1-uncompressed
私钥格式
pkcs8-der

ring 只提供少量算法,采用源自 BoringSSL 的汇编代码和防误用的 API:无法自行选择 nonce,无法对预先计算的摘要签名,也无法加载不常见格式的密钥。这让它既快又不容易用错,也正因如此,工作量主要花在转换来自其他技术栈的密钥和签名上。

编码是算法的一部分:ECDSA_P256_SHA256_FIXED_SIGNING 生成 64 字节的原始 r‖s,ECDSA_P256_SHA256_ASN1_SIGNING 生成 DER,验证也有对应的 _FIXED 和 _ASN1 常量。用另一个常量检查签名只会报错 Unspecified,因此用 ASN1 常量验证 JWS 签名——或者用 FIXED 验证 OpenSSL 签名——看起来与伪造签名完全一样。示例展示了这两种结果。签名使用随机 nonce,s 没有经过规范化,验证时两个半区都接受。

私钥只能以 PKCS#8 DER 读取,而且 Ed25519 密钥必须是包含公钥的 PKCS#8 v2:OpenSSL 和大多数工具写出的 48 字节 v1 形式被 Ed25519KeyPair::from_pkcs8 拒绝,报错 VersionNotSupported,而 from_pkcs8_maybe_unchecked 和 from_seed_unchecked 接受了它。PEM、SEC1 和 JWK 私钥需要先转换,例如使用 JWK 转换器。公钥是裸 SEC1 点:public_key() 返回 65 字节的未压缩形式,不接受压缩点。

曲线列表只有 P-256 和 P-384,外加 Ed25519;没有 P-521,没有 secp256k1,也没有后量子算法。需要这些算法或 FIPS 认证的项目会转向 aws-lc-rs,它提供与 ring 兼容的 API。

要查看 ring 生成的签名采用哪种编码,可以把它粘贴到 DER ⇄ 原始格式转换器中。

事实及其依据的检查

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

  • ECDSA 签名编码

    der-or-raw

    由算法常量选择编码:*_FIXED 签名和验证 64 字节的原始 r‖s,*_ASN1 则对应 DER;用另一个常量无法验证签名。

    检查记录
    fact ecdsa-signature-encoding = der-or-raw
    evidence ecdsa-signature-encoding: FIXED signs 64 bytes; ASN1 output starts with 0x30 (a DER SEQUENCE)
    evidence ecdsa-signature-encoding: a signature verifies under the other encoding's constant: false
  • ECDSA 的 nonce

    random

    随机 nonce:同一条消息的两个签名不同,也没有确定性选项。

    检查记录
    fact ecdsa-nonce = random
  • 签名时的 low-S

    either

    s 没有经过规范化:64 个签名中出现了 high-S 值。

    检查记录
    fact ecdsa-low-s-sign = either
  • 验证时的 high-S

    accepts-high-s

    FIXED 验证器接受了把 s 替换为 n − s 的签名。

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
  • 签名调用对哪些内容做哈希

    hashes-message

    消息按算法规定的方式计算哈希;没有对预先计算的摘要签名的 API。

    检查记录
    fact ecdsa-prehash = hashes-message
    evidence ecdsa-prehash: the API has no entry point for signing a precomputed digest
  • 格式错误的 ECDSA 签名

    returns-error

    对于格式错误或不正确的签名,verify() 返回 Err(Unspecified),不提供任何细节。

    检查记录
    fact ecdsa-malformed-signature = returns-error
    evidence ecdsa-malformed-signature: verify() of 3 bytes: Err(Unspecified)
  • ECDSA 曲线

    P-256P-384

    仅支持 P-256 和 P-384。

    检查记录
    fact ecdsa-curves = P-256, P-384
    evidence ecdsa-curves: ring defines ECDSA_P256_* and ECDSA_P384_* algorithms only
  • EdDSA

    Ed25519

    Ed25519。

    检查记录
    fact eddsa = Ed25519
  • Ed25519 PKCS #8 v1 密钥

    rejected-by-from-pkcs8

    48 字节的 PKCS#8 v1 Ed25519 密钥被 from_pkcs8 拒绝,报错 VersionNotSupported;from_pkcs8_maybe_unchecked 和 from_seed_unchecked 接受了它。generate_pkcs8 写出 83 字节的 v2 密钥。

    检查记录
    fact ed25519-pkcs8-v1 = rejected-by-from-pkcs8
    evidence ed25519-pkcs8-v1: generate_pkcs8 writes 83 bytes (PKCS#8 v2 with the public key); a 48-byte v1 key: from_pkcs8 Err(KeyRejected("VersionNotSupported")), from_pkcs8_maybe_unchecked ok true, from_seed_unchecked ok true
  • 后量子算法

    none

    不支持后量子算法。

    检查记录
    fact pqc = none
  • 公钥格式

    sec1-uncompressed

    公钥是 SEC1 未压缩点(P-256 为 65 字节);使用压缩点失败,也不能解析 SPKI、PEM 或 JWK。

    检查记录
    fact public-key-formats = sec1-uncompressed
    evidence public-key-formats: public_key() returns 65 bytes (uncompressed SEC1); no SPKI, PEM or JWK parsing
  • 私钥格式

    pkcs8-der

    私钥只能从 PKCS#8 DER 加载。

    检查记录
    fact private-key-formats = pkcs8-der

用法

用 FIXED(原始格式)常量签名,并展示只有对应的验证常量才接受该签名。
use ring::rand::SystemRandom;
use ring::signature::{self, EcdsaKeyPair, KeyPair, UnparsedPublicKey};

fn main() {
    let rng = SystemRandom::new();
    // ring only reads PKCS#8 DER private keys.
    let pkcs8 = EcdsaKeyPair::generate_pkcs8(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
    let message = b"order #1042 approved";

    // The algorithm constant fixes the encoding: FIXED = raw r||s (JWS, WebCrypto),
    // ASN1 = DER (OpenSSL, Java). Verify with the matching constant.
    let fixed = EcdsaKeyPair::from_pkcs8(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng).unwrap();
    let raw = fixed.sign(&rng, message).unwrap();
    let public = fixed.public_key().as_ref(); // 65-byte uncompressed SEC1 point

    let ok = UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_FIXED, public).verify(message, raw.as_ref()).is_ok();
    let wrong = UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_ASN1, public).verify(message, raw.as_ref()).is_ok();
    println!("raw bytes: {}, verified: {ok}, verified as DER: {wrong}", raw.as_ref().len());
}

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

命令
cargo run --quiet --release
输出必须匹配(正则表达式)
raw bytes: 64, verified: true, verified as DER: false

库