本文へスキップ
ecdsa.com
Rust

RustCrypto ecdsa (p256, p384, k256)

RustCrypto による純粋な Rust 実装の ECDSA です。ecdsa クレートと曲線ごとのクレート(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 クレートと曲線ごとのクレート(p256、p384、p521、secp256k1 用の k256、Brainpool のクレート群)に分けており、いずれも signature トレイト(Signer、Verifier、PrehashSigner)を実装しています。C の依存関係を持たない純粋な Rust 実装のため、WebAssembly や組み込み向けのターゲットでは定番の選択肢です。チェックでは ecdsa 0.17.0 と、p256、p384、k256 の 0.14.0 を使いました。

署名は決定的です。RFC 6979 のテストメッセージに対する P-256 署名は、RFC のテストベクトルとバイト単位で一致しました。Signature 型は 64 バイトの生の r‖s 形式で、DER は明示的な to_der() と from_der() で扱います。誤った形式を解析すると、検証が行われる前にエラーが返ります。Signer トレイトは曲線のデフォルトのハッシュ(P-256 では SHA-256)でメッセージをハッシュします。PrehashSigner::sign_prehash はダイジェストを受け取り、メッセージの SHA-256 に対して同一の署名を生成しました。

low-S の扱いは曲線によって異なり、1 つのコードベースで両方の種類の曲線を扱うときの落とし穴になります。NIST 曲線のクレートは s を計算されたまま残し(P-256 の署名 64 個のうち 29 個が high-S でした)、その検証処理は low-S と high-S のどちらも受け入れます。k256 は署名時に s を正規化し、その検証処理は有効な署名を反転させた high-S 形式を拒否しました。これは Bitcoin のルールと一致します。そのため OpenSSL、Java、クラウドの KMS が生成した secp256k1 署名は、k256 では約半分の確率で失敗します。サンプルが P-256 で示しているように、先に normalize_s() を呼び出してください。

鍵は SPKI と PKCS#8(pem と pkcs8 のフィーチャーを有効にすれば PEM と DER)、圧縮・非圧縮いずれの形式の SEC1 の点、SEC1 の秘密鍵、生のスカラーから読み込めます。Ed25519 は別の ed25519-dalek クレートにあり、ポスト量子アルゴリズムはありません。Rust で FIPS 認証済みのプリミティブやポスト量子のプリミティブが必要な場合は aws-lc-rs を参照してください。

署名が決定的なので、テストで署名が変わったら、それは本物のリグレッションの兆候です。他のライブラリとの比較にはテストベクトルと DER ⇄ 生形式の変換ツールを使ってください。

事実とその根拠となるチェック

以下の内容はドキュメントからの転記ではありません。2026年10月20日に、チェック content/libraries/verify/rustcrypto-ecdsa/run.sh がコンテナイメージ 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 は、high-S である s も含めて付録 A.2.5 を正確に再現しました。

    チェックによる記録
    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 クレートは正規化しません。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

    チェックしたクレートの中で、署名時に s を正規化するのは k256(secp256k1)だけです。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

    p256、p384、k256 クレートで P-256、P-384、secp256k1 の署名と検証ができました。P-224、P-521、Brainpool は同じプロジェクトの別のクレートです。

    チェックによる記録
    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

    SPKI の PEM と DER、圧縮形式と非圧縮形式の 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

    PKCS#8 の PEM と DER、SEC1 PEM、32 バイトの生のスカラーに対応します。

    チェックによる記録
    fact private-key-formats = pkcs8-pem, pkcs8-der, sec1-pem, raw-scalar
  • EdDSA

    none

    これらのクレートに 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

ライブラリ