Pular para o conteúdo
ecdsa.com
Rust

RustCrypto ecdsa (p256, p384, k256)

O ECDSA em Rust puro do RustCrypto: o crate ecdsa com um crate por curva (p256, p384, p521, k256…) — RFC 6979 e r‖s bruto por padrão, mas a secp256k1 é low-S nos dois lados, e as curvas do NIST não.
Licença
Apache-2.0 OR MIT
Versão verificada
0.17.0 ·
Código-fonte
github.com/RustCrypto/signatures
Documentação
docs.rs/ecdsa/0.17.0/ecdsa/

Visão geral da compatibilidade

ECDSA

Curvas ECDSA
P-256P-384secp256k1
EdDSA
none
Formatos de chave pública
spki-pemspki-dersec1-uncompressedsec1-compressed
Formatos de chave privada
pkcs8-pempkcs8-dersec1-pemraw-scalar

O projeto RustCrypto divide o ECDSA em um crate genérico ecdsa e um crate por curva — p256, p384, p521, k256 para secp256k1 e os crates Brainpool —, e todos implementam os traits de signature (Signer, Verifier, PrehashSigner). São Rust puro, sem dependências de C, o que faz deles a escolha habitual para WebAssembly e alvos embarcados. O teste usou ecdsa 0.17.0 com p256, p384 e k256 0.14.0.

A assinatura é determinística: a assinatura P-256 sobre a mensagem de teste da RFC 6979 reproduziu o vetor de teste da RFC byte a byte. O tipo Signature é a forma r‖s bruta de 64 bytes; o DER exige chamadas explícitas a to_der() e from_der(), e decodificar a forma errada retorna um erro antes de qualquer verificação. O trait Signer calcula o hash da mensagem com o hash padrão da curva (SHA-256 para P-256); PrehashSigner::sign_prehash recebe um digest e produziu a assinatura idêntica para o SHA-256 da mensagem.

O tratamento de low-S muda conforme a curva, e essa é a armadilha quando uma mesma base de código lida com os dois tipos de curva. Os crates das curvas do NIST mantêm o s como foi calculado — 29 de 64 assinaturas P-256 eram high-S — e seus verificadores aceitam as duas metades. O k256 normaliza o s ao assinar, e seu verificador rejeitou a forma high-S espelhada de uma assinatura válida, de acordo com as regras do Bitcoin. Por isso, uma assinatura secp256k1 produzida pelo OpenSSL, pelo Java ou por um KMS de nuvem falha no k256 em cerca de metade das vezes; chame normalize_s() nela antes, como o exemplo mostra para P-256.

As chaves são carregadas a partir de SPKI e PKCS#8 (PEM e DER, com as features pem e pkcs8), de pontos SEC1 em qualquer das duas formas, de chaves privadas SEC1 e de escalares brutos. O Ed25519 fica no crate separado ed25519-dalek, e aqui não há algoritmos pós-quânticos; para primitivas com validação FIPS ou pós-quânticas em Rust, veja o aws-lc-rs.

Como a assinatura é determinística, uma assinatura que muda em um teste é, aqui, um sinal real de regressão. Compare com outras bibliotecas usando vetores de teste e o conversor DER ⇄ bruto.

Os fatos e as verificações por trás deles

Nada abaixo foi copiado da documentação. A verificação content/libraries/verify/rustcrypto-ecdsa/run.sh executou RustCrypto ecdsa (p256, p384, k256) 0.17.0 na imagem de contêiner rust:1.99.1-trixie em 20 de outubro de 2026; a página precisa corresponder ao que a verificação imprimiu, e cada fato lista essas linhas.

  • Codificação da assinatura ECDSA

    raw

    Signature é o r‖s bruto de 64 bytes; to_der() produziu uma assinatura DER de 71 bytes que from_der() leu de volta.

    Registrado pela verificação
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: to_der() is 71 bytes and from_der() round-trips: Ok(true)
  • Nonce do ECDSA

    rfc6979

    RFC 6979: Signer::sign na P-256 reproduziu exatamente o apêndice A.2.5, inclusive o s high-S do vetor.

    Registrado pela verificação
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: P-256 Signer::sign reproduces RFC 6979 A.2.5 including its high s: true
  • Low-S ao assinar

    either

    O crate p256 não normaliza: 29 de 64 assinaturas eram high-S.

    Registrado pela verificação
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: high-S among 64 signatures: P-256 29, secp256k1 0
  • Low-S ao assinar, por curva

    secp256k1

    Entre os crates testados, só o k256 (secp256k1) normaliza o s ao assinar: nenhum valor high-S em 64 assinaturas.

    Registrado pela verificação
    fact low-s-sign-by-curve = secp256k1
  • High-S ao verificar

    accepts-high-s

    O verificador do p256 aceitou a assinatura espelhada (r, n − s).

    Registrado pela verificação
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: mirrored signature verifies: P-256 true, secp256k1 false
  • Rejeição de high-S ao verificar, por curva

    secp256k1

    O verificador do k256 rejeitou a assinatura high-S espelhada; o da P-256 a aceitou.

    Registrado pela verificação
    fact low-s-verify-by-curve = secp256k1
  • Entrada do hash na chamada de assinatura

    hashes-message-or-digest

    Signer::sign calcula o hash com SHA-256 na P-256; sign_prehash sobre o SHA-256 da mensagem produziu a mesma assinatura, e verify_prehash a aceitou.

    Registrado pela verificação
    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
  • Assinatura ECDSA malformada

    returns-error

    A decodificação é separada da verificação: Signature::from_slice com bytes DER e Signature::from_der com bytes brutos retornam Err.

    Registrado pela verificação
    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 })
  • Curvas ECDSA

    P-256P-384secp256k1

    Assinatura e verificação funcionaram com P-256, P-384 e secp256k1 nos crates p256, p384 e k256; P-224, P-521 e Brainpool são crates separados do mesmo projeto.

    Registrado pela verificação
    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
  • Formatos de chave pública

    spki-pemspki-dersec1-uncompressedsec1-compressed

    SPKI PEM e DER e pontos SEC1 comprimidos ou não comprimidos; um x‖y de 64 bytes sem o prefixo foi rejeitado.

    Registrado pela verificação
    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
  • Formatos de chave privada

    pkcs8-pempkcs8-dersec1-pemraw-scalar

    PKCS#8 PEM e DER, SEC1 PEM e o escalar bruto de 32 bytes.

    Registrado pela verificação
    fact private-key-formats = pkcs8-pem, pkcs8-der, sec1-pem, raw-scalar
  • EdDSA

    none

    Sem EdDSA nestes crates; o Ed25519 é fornecido pelo ed25519-dalek.

    Registrado pela verificação
    fact eddsa = none
    evidence eddsa: Ed25519 lives in the separate ed25519-dalek crate, outside the ecdsa/elliptic-curves family
  • Algoritmos pós-quânticos

    none

    Sem algoritmos pós-quânticos.

    Registrado pela verificação
    fact pqc = none

Uso

Assine de forma determinística com p256, produza as codificações bruta e DER e normalize o s para verificadores estritos.
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());
}

O executor de trechos de código roda cada exemplo literalmente na imagem de contêiner rust:1.99.1-trixie, e a execução falha se a saída não corresponder.

Comando
cargo run --quiet --release
A saída deve corresponder a (expressão regular)
raw bytes: 64, der bytes: 7[012]\nverified: true\nlow-s verified: true

Bibliotecas