Перейти к содержимому
ecdsa.com
Rust

RustCrypto ecdsa (p256, p384, k256)

ECDSA на чистом Rust от RustCrypto: крейт ecdsa и отдельный крейт для каждой кривой (p256, p384, p521, k256…) — по умолчанию RFC 6979 и raw 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, k256 для secp256k1 и крейты Brainpool, и все они реализуют трейты крейта signature (Signer, Verifier, PrehashSigner). Они написаны на чистом Rust без зависимостей на C, поэтому их обычно выбирают для WebAssembly и встраиваемых систем. Для проверки использовались ecdsa 0.17.0 и p256, p384 и k256 версии 0.14.0.

Подписи детерминированы: подпись P-256 тестового сообщения из RFC 6979 побайтово совпала с тестовым вектором из RFC. Тип Signature — это 64-байтовая форма raw r‖s; для DER есть явные to_der() и from_der(), а разбор подписи не в той форме возвращает ошибку ещё до проверки. Трейт Signer хеширует сообщение хеш-функцией кривой по умолчанию (SHA-256 для P-256); PrehashSigner::sign_prehash принимает дайджест и для хеша SHA-256 сообщения выдал идентичную подпись.

Обработка low-S зависит от кривой, и в этом ловушка, когда одна кодовая база работает с обоими семействами кривых. Крейты кривых NIST оставляют s таким, каким оно получилось при вычислении (29 из 64 подписей P-256 оказались high-S), а их код проверки принимает s из обеих половин диапазона. k256 нормализует s при подписании, а его код проверки отклонил зеркальную форму high-S верной подписи, что соответствует правилам Bitcoin. Поэтому подпись secp256k1, созданная OpenSSL, Java или облачным KMS, примерно в половине случаев не проходит проверку в k256; сначала вызовите для неё normalize_s(), как пример показывает для P-256.

Ключи загружаются из SPKI и PKCS#8 (PEM и DER, с feature-флагами pem и pkcs8), из точек SEC1 в любой форме, из закрытых ключей SEC1 и из скаляров в формате raw. Ed25519 находится в отдельном крейте ed25519-dalek, а постквантовых алгоритмов здесь нет; если для Rust нужны примитивы, сертифицированные по FIPS, или постквантовые примитивы, смотрите aws-lc-rs.

Поскольку подписание детерминированное, изменившаяся подпись в тесте здесь действительно указывает на регрессию. Сравнивайте результаты с другими библиотеками с помощью тестовых векторов и конвертера DER ⇄ raw.

Факты и проверки, на которых они основаны

Ничего из приведённого ниже не скопировано из документации. 20 октября 2026 г. проверка content/libraries/verify/rustcrypto-ecdsa/run.sh запустила RustCrypto ecdsa (p256, p384, k256) 0.17.0 в образе контейнера rust:1.99.1-trixie; содержимое страницы должно совпадать с выводом проверки, а у каждого факта приведены соответствующие строки вывода.

  • Кодировка подписи ECDSA

    raw

    Signature — 64-байтовое значение raw 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)
  • Nonce в ECDSA

    rfc6979

    RFC 6979: Signer::sign на P-256 в точности воспроизвёл подпись из приложения A.2.5, в том числе её значение s в форме 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 не нормализует s: 29 из 64 подписей оказались 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

    Из проверенных крейтов только 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

    Signer::sign для P-256 хеширует сообщение SHA-256; sign_prehash от хеша SHA-256 сообщения выдал ту же подпись, и 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

    Декодирование отделено от проверки: Signature::from_slice для байтов DER и Signature::from_der для байтов raw возвращают 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 прошли успешно; 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-байтовый скаляр в формате raw.

    Что записала проверка
    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, получите кодировки raw и 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

Библиотеки