Skip to content
ecdsa.com
Rust

ring

Brian Smith's ring, the Rust crypto library under rustls and many web servers: a deliberately small API where the algorithm constant fixes the signature encoding, private keys are PKCS#8 only, and the curves are P-256, P-384 and Ed25519.
License
Apache-2.0 AND ISC
Checked version
0.17.14 ·
Source
github.com/briansmith/ring
Documentation
docs.rs/ring/0.17.14/ring/

Compatibility at a glance

ECDSAEdDSA

ECDSA curves
P-256P-384
EdDSA
Ed25519
Public key formats
sec1-uncompressed

ring offers a short list of algorithms with BoringSSL-derived assembly and a misuse-resistant API: there is no way to choose a nonce, sign a precomputed digest or load a key in an unusual format. That makes it fast and hard to hold wrong, and it is why converting keys and signatures from other stacks is where the work goes.

The encoding is part of the algorithm: ECDSA_P256_SHA256_FIXED_SIGNING produces 64-byte raw r‖s and ECDSA_P256_SHA256_ASN1_SIGNING DER, with matching _FIXED and _ASN1 verification constants. A signature checked under the other constant simply fails with Unspecified, so a JWS signature verified with the ASN1 constant — or an OpenSSL signature with FIXED — looks exactly like a forgery. The sample shows both outcomes. Signatures use a random nonce, s is not normalized, and verification accepts both halves.

Private keys are read only as PKCS#8 DER, and Ed25519 keys must be PKCS#8 v2 with the public key included: the 48-byte v1 form that OpenSSL and most tools write was rejected by Ed25519KeyPair::from_pkcs8 with VersionNotSupported, while from_pkcs8_maybe_unchecked and from_seed_unchecked accepted it. PEM, SEC1 and JWK private keys need converting first, for example with the JWK converter. Public keys are bare SEC1 points: public_key() returns the 65-byte uncompressed form, and compressed points are not accepted.

The curve list ends at P-256 and P-384 plus Ed25519; there is no P-521, no secp256k1 and no post-quantum algorithm. Projects that need those, or FIPS validation, move to aws-lc-rs, which offers a ring-compatible API.

To check what encoding a signature from ring is in, paste it into the DER ⇄ raw converter.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/ring/run.sh ran ring 0.17.14 in the container image rust:1.99.1-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    der-or-raw

    The algorithm constant chooses the encoding: *_FIXED signs and verifies 64-byte raw r‖s, *_ASN1 DER; a signature does not verify under the other constant.

    Recorded by the check
    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

    Random nonce: two signatures of the same message differ, and there is no deterministic option.

    Recorded by the check
    fact ecdsa-nonce = random
  • Low-S when signing

    either

    s is not normalized: high-S values appeared among 64 signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = either
  • High-S when verifying

    accepts-high-s

    The FIXED verifier accepted the signature with s replaced by n − s.

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
  • What the signing call hashes

    hashes-message

    The message is hashed as the algorithm says; there is no API for signing a precomputed digest.

    Recorded by the check
    fact ecdsa-prehash = hashes-message
    evidence ecdsa-prehash: the API has no entry point for signing a precomputed digest
  • Malformed ECDSA signature

    returns-error

    verify() returns Err(Unspecified) for a malformed or wrong signature, with no detail.

    Recorded by the check
    fact ecdsa-malformed-signature = returns-error
    evidence ecdsa-malformed-signature: verify() of 3 bytes: Err(Unspecified)
  • ECDSA curves

    P-256P-384

    P-256 and P-384 only.

    Recorded by the check
    fact ecdsa-curves = P-256, P-384
    evidence ecdsa-curves: ring defines ECDSA_P256_* and ECDSA_P384_* algorithms only
  • EdDSA

    Ed25519

    Ed25519.

    Recorded by the check
    fact eddsa = Ed25519
  • Ed25519 PKCS #8 v1 keys

    rejected-by-from-pkcs8

    A 48-byte PKCS#8 v1 Ed25519 key was refused by from_pkcs8 with VersionNotSupported; from_pkcs8_maybe_unchecked and from_seed_unchecked accepted it. generate_pkcs8 writes 83-byte v2 keys.

    Recorded by the check
    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
  • Post-quantum algorithms

    none

    No post-quantum algorithms.

    Recorded by the check
    fact pqc = none
  • Public key formats

    sec1-uncompressed

    Public keys are uncompressed SEC1 points (65 bytes for P-256); compressed points failed, and there is no SPKI, PEM or JWK parsing.

    Recorded by the check
    fact public-key-formats = sec1-uncompressed
    evidence public-key-formats: public_key() returns 65 bytes (uncompressed SEC1); no SPKI, PEM or JWK parsing
  • Private key formats

    pkcs8-der

    Private keys load only from PKCS#8 DER.

    Recorded by the check
    fact private-key-formats = pkcs8-der

Usage

Sign with the FIXED (raw) constant and show that only the matching verification constant accepts the signature.
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());
}

The snippet harness runs every sample verbatim in the container image rust:1.99.1-trixie, and the run fails unless the output matches.

Command
cargo run --quiet --release
Output must match (regular expression)
raw bytes: 64, verified: true, verified as DER: false

Libraries