Skip to content

Commit

Permalink
p521: initial ecdsa support with FIPS 186-4 test vectors (#956)
Browse files Browse the repository at this point in the history
Initial `ecdsa` feature and test vectors to ensure it's working.

Due to P-521's unusual modulus size, we can't use the upstream
`ecdsa::{SigningKey, VerifyingKey}` types (although we are able to use
the generic implementation of ECDSA). The `ecdsa` crate currently
includes bounds for the digest size in several places, including the
RFC6979 implementation, which are incompatible with P-521, which uses a
66-byte serialized scalar size along with SHA-521, which emits a 64-byte
digest.

To work around this, newtypes for `SigningKey` and `VerifyingKey` have
been added. They largely wrap the inner types, but don't use RFC6979 and
instead randomly generate `R` each time signing is performed (using an
on-by-default `getrandom` feature which enables `rand_core::OsRng`).

FIPS 186-4 test vectors adapted from `SigGen.txt` in
`186-4ecdsatestvectors.zip` from:

https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/digital-signatures
  • Loading branch information
tarcieri authored Nov 9, 2023
1 parent 7862950 commit 62a4091
Show file tree
Hide file tree
Showing 7 changed files with 430 additions and 13 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ NOTE: Most crates have field/point arithmetic implementations gated under the

| Name | Curve | `arithmetic`? | Crates.io | Documentation | Build Status |
|-----------|--------------------|---------------|-------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| [`bign256`] | bign-curve256v1 || [![crates.io](https://img.shields.io/crates/v/bign256.svg)](https://crates.io/crates/bign256) | [![Documentation](https://docs.rs/bign256/badge.svg)](https://docs.rs/bign256) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/bign256/badge.svg?branch=master&event=push) |
| [`bign256`] | bign-curve256v1 | | [![crates.io](https://img.shields.io/crates/v/bign256.svg)](https://crates.io/crates/bign256) | [![Documentation](https://docs.rs/bign256/badge.svg)](https://docs.rs/bign256) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/bign256/badge.svg?branch=master&event=push) |
| [`bp256`] | brainpoolP256r1/t1 | 🚧 | [![crates.io](https://img.shields.io/crates/v/bp256.svg)](https://crates.io/crates/bp256) | [![Documentation](https://docs.rs/bp256/badge.svg)](https://docs.rs/bp256) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/bp256/badge.svg?branch=master&event=push) |
| [`bp384`] | brainpoolP384r1/t1 | 🚧 | [![crates.io](https://img.shields.io/crates/v/bp384.svg)](https://crates.io/crates/bp384) | [![Documentation](https://docs.rs/bp384/badge.svg)](https://docs.rs/bp384) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/bp384/badge.svg?branch=master&event=push) |
| [`k256`] | [secp256k1] || [![crates.io](https://img.shields.io/crates/v/k256.svg)](https://crates.io/crates/k256) | [![Documentation](https://docs.rs/k256/badge.svg)](https://docs.rs/k256) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/k256/badge.svg?branch=master&event=push) |
| [`p192`] | [NIST P-192] || [![crates.io](https://img.shields.io/crates/v/p192.svg)](https://crates.io/crates/p192) | [![Documentation](https://docs.rs/p192/badge.svg)](https://docs.rs/p192) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/p192/badge.svg?branch=master&event=push) |
| [`p224`] | [NIST P-224] || [![crates.io](https://img.shields.io/crates/v/p224.svg)](https://crates.io/crates/p224) | [![Documentation](https://docs.rs/p224/badge.svg)](https://docs.rs/p224) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/p224/badge.svg?branch=master&event=push) |
| [`p256`] | [NIST P-256] || [![crates.io](https://img.shields.io/crates/v/p256.svg)](https://crates.io/crates/p256) | [![Documentation](https://docs.rs/p256/badge.svg)](https://docs.rs/p256) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/p256/badge.svg?branch=master&event=push) |
| [`p384`] | [NIST P-384] || [![crates.io](https://img.shields.io/crates/v/p384.svg)](https://crates.io/crates/p384) | [![Documentation](https://docs.rs/p384/badge.svg)](https://docs.rs/p384) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/p384/badge.svg?branch=master&event=push) |
| [`p521`] | [NIST P-521] | 🚧 | [![crates.io](https://img.shields.io/crates/v/p521.svg)](https://crates.io/crates/p521) | [![Documentation](https://docs.rs/p521/badge.svg)](https://docs.rs/p521) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/p521/badge.svg?branch=master&event=push) |
| [`sm2`] | [SM2] || [![crates.io](https://img.shields.io/crates/v/sm2.svg)](https://crates.io/crates/sm2) | [![Documentation](https://docs.rs/sm2/badge.svg)](https://docs.rs/sm2) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/sm2/badge.svg?branch=master&event=push) |
| [`k256`] | [secp256k1] | | [![crates.io](https://img.shields.io/crates/v/k256.svg)](https://crates.io/crates/k256) | [![Documentation](https://docs.rs/k256/badge.svg)](https://docs.rs/k256) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/k256/badge.svg?branch=master&event=push) |
| [`p192`] | [NIST P-192] | | [![crates.io](https://img.shields.io/crates/v/p192.svg)](https://crates.io/crates/p192) | [![Documentation](https://docs.rs/p192/badge.svg)](https://docs.rs/p192) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/p192/badge.svg?branch=master&event=push) |
| [`p224`] | [NIST P-224] | | [![crates.io](https://img.shields.io/crates/v/p224.svg)](https://crates.io/crates/p224) | [![Documentation](https://docs.rs/p224/badge.svg)](https://docs.rs/p224) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/p224/badge.svg?branch=master&event=push) |
| [`p256`] | [NIST P-256] | | [![crates.io](https://img.shields.io/crates/v/p256.svg)](https://crates.io/crates/p256) | [![Documentation](https://docs.rs/p256/badge.svg)](https://docs.rs/p256) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/p256/badge.svg?branch=master&event=push) |
| [`p384`] | [NIST P-384] | | [![crates.io](https://img.shields.io/crates/v/p384.svg)](https://crates.io/crates/p384) | [![Documentation](https://docs.rs/p384/badge.svg)](https://docs.rs/p384) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/p384/badge.svg?branch=master&event=push) |
| [`p521`] | [NIST P-521] | | [![crates.io](https://img.shields.io/crates/v/p521.svg)](https://crates.io/crates/p521) | [![Documentation](https://docs.rs/p521/badge.svg)](https://docs.rs/p521) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/p521/badge.svg?branch=master&event=push) |
| [`sm2`] | [SM2] | | [![crates.io](https://img.shields.io/crates/v/sm2.svg)](https://crates.io/crates/sm2) | [![Documentation](https://docs.rs/sm2/badge.svg)](https://docs.rs/sm2) | ![build](https://github.com/RustCrypto/elliptic-curves/workflows/sm2/badge.svg?branch=master&event=push) |

🚧: curve arithmetic implementation under construction

Expand Down
17 changes: 12 additions & 5 deletions p521/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,36 @@ edition = "2021"
rust-version = "1.65"

[dependencies]
base16ct = "0.2"
elliptic-curve = { version = "0.13", default-features = false, features = ["hazmat", "sec1"] }
sha2 = { version = "0.10", optional = true, default-features = false }

# optional dependencies
ecdsa-core = { version = "0.16.5", package = "ecdsa", optional = true, default-features = false, features = ["der"] }
hex-literal = { version = "0.4", optional = true }
primeorder = { version = "0.13.3", optional = true, path = "../primeorder" }
base16ct = "0.2.0"
rand_core = { version = "0.6", optional = true, default-features = false }
sha2 = { version = "0.10", optional = true, default-features = false }

[dev-dependencies]
ecdsa-core = { version = "0.16", package = "ecdsa", default-features = false, features = ["dev"] }
hex-literal = "0.4"
primeorder = { version = "0.13.3", features = ["dev"], path = "../primeorder" }
rand_core = { version = "0.6", features = ["getrandom"] }

[features]
default = ["arithmetic", "pem", "std"]
alloc = ["elliptic-curve/alloc"]
std = ["alloc", "elliptic-curve/std"]
default = ["arithmetic", "ecdsa", "getrandom", "pem", "std"]
alloc = ["ecdsa-core?/alloc", "elliptic-curve/alloc"]
std = ["alloc", "ecdsa-core?/std", "elliptic-curve/std"]

arithmetic = ["dep:primeorder"]
digest = ["ecdsa-core/digest", "ecdsa-core/hazmat"]
ecdh = ["arithmetic", "elliptic-curve/ecdh"]
ecdsa = ["arithmetic", "ecdsa-core/signing", "ecdsa-core/verifying", "sha512"]
getrandom = ["rand_core/getrandom"]
jwk = ["elliptic-curve/jwk"]
pem = ["elliptic-curve/pem", "pkcs8"]
pkcs8 = ["elliptic-curve/pkcs8"]
sha512 = ["digest", "dep:sha2"]
test-vectors = ["dep:hex-literal"]
voprf = ["elliptic-curve/voprf", "dep:sha2"]

Expand Down
249 changes: 249 additions & 0 deletions p521/src/ecdsa.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
//! Elliptic Curve Digital Signature Algorithm (ECDSA)
//!
//! This module contains support for computing and verifying ECDSA signatures.
//! To use it, you will need to enable one of the two following Cargo features:
//!
//! - `ecdsa-core`: provides only the [`Signature`] type (which represents an
//! ECDSA/P-521 signature). Does not require the `arithmetic` feature. This is
//! useful for 3rd-party crates which wish to use the `Signature` type for
//! interoperability purposes (particularly in conjunction with the
//! [`signature::Signer`] trait. Example use cases for this include other
//! software implementations of ECDSA/P-521 and wrappers for cloud KMS
//! services or hardware devices (HSM or crypto hardware wallet).
//! - `ecdsa`: provides `ecdsa-core` features plus the [`SigningKey`] and
//! [`VerifyingKey`] types which natively implement ECDSA/P-521 signing and
//! verification.
//!
//! ## Signing/Verification Example
//!
//! This example requires the `ecdsa` Cargo feature is enabled:
//!
//! ```
//! # #[cfg(feature = "ecdsa")]
//! # {
//! use p521::ecdsa::{signature::Signer, Signature, SigningKey};
//! use rand_core::OsRng; // requires 'getrandom' feature
//!
//! // Signing
//! let signing_key = SigningKey::random(&mut OsRng); // Serialize with `::to_bytes()`
//! let message = b"ECDSA proves knowledge of a secret number in the context of a single message";
//! let signature: Signature = signing_key.sign(message);
//!
//! // Verification
//! use p521::ecdsa::{signature::Verifier, VerifyingKey};
//!
//! let verifying_key = VerifyingKey::from(&signing_key); // Serialize with `::to_encoded_point()`
//! assert!(verifying_key.verify(message, &signature).is_ok());
//! # }
//! ```

// TODO(tarcieri): use RFC6979 + upstream types from the `ecdsa` crate

pub use ecdsa_core::signature::{self, Error, Result};

#[cfg(feature = "ecdsa")]
use {
crate::{AffinePoint, EncodedPoint, FieldBytes, NonZeroScalar, Scalar},
ecdsa_core::{
hazmat::{bits2field, sign_prehashed, SignPrimitive, VerifyPrimitive},
signature::{
hazmat::{PrehashVerifier, RandomizedPrehashSigner},
rand_core::CryptoRngCore,
RandomizedSigner, Verifier,
},
},
elliptic_curve::Field,
sha2::{Digest, Sha512},
};

#[cfg(all(feature = "ecdsa", feature = "getrandom"))]
use {
ecdsa_core::signature::{hazmat::PrehashSigner, Signer},
rand_core::OsRng,
};

use super::NistP521;

/// ECDSA/P-521 signature (fixed-size)
pub type Signature = ecdsa_core::Signature<NistP521>;

/// ECDSA/P-521 signature (ASN.1 DER encoded)
pub type DerSignature = ecdsa_core::der::Signature<NistP521>;

#[cfg(feature = "ecdsa")]
impl SignPrimitive<NistP521> for Scalar {}

#[cfg(feature = "ecdsa")]
impl VerifyPrimitive<NistP521> for AffinePoint {}

/// ECDSA/P-521 signing key
#[cfg(feature = "ecdsa")]
#[derive(Clone)]
pub struct SigningKey(ecdsa_core::SigningKey<NistP521>);

#[cfg(feature = "ecdsa")]
impl SigningKey {
/// Generate a cryptographically random [`SigningKey`].
pub fn random(rng: &mut impl CryptoRngCore) -> Self {
ecdsa_core::SigningKey::<NistP521>::random(rng).into()
}

/// Initialize signing key from a raw scalar serialized as a byte array.
pub fn from_bytes(bytes: &FieldBytes) -> Result<Self> {
ecdsa_core::SigningKey::<NistP521>::from_bytes(bytes).map(Into::into)
}

/// Initialize signing key from a raw scalar serialized as a byte slice.
pub fn from_slice(bytes: &[u8]) -> Result<Self> {
ecdsa_core::SigningKey::<NistP521>::from_slice(bytes).map(Into::into)
}

/// Serialize this [`SigningKey`] as bytes
pub fn to_bytes(&self) -> FieldBytes {
self.0.to_bytes()
}

/// Borrow the secret [`NonZeroScalar`] value for this key.
///
/// # ⚠️ Warning
///
/// This value is key material.
///
/// Please treat it with the care it deserves!
pub fn as_nonzero_scalar(&self) -> &NonZeroScalar {
self.0.as_nonzero_scalar()
}

/// Get the [`VerifyingKey`] which corresponds to this [`SigningKey`].
#[cfg(feature = "verifying")]
pub fn verifying_key(&self) -> VerifyingKey {
VerifyingKey::from(self)
}
}

#[cfg(feature = "ecdsa")]
impl From<ecdsa_core::SigningKey<NistP521>> for SigningKey {
fn from(inner: ecdsa_core::SigningKey<NistP521>) -> SigningKey {
SigningKey(inner)
}
}

#[cfg(all(feature = "ecdsa", feature = "getrandom"))]
impl PrehashSigner<Signature> for SigningKey {
fn sign_prehash(&self, prehash: &[u8]) -> Result<Signature> {
self.sign_prehash_with_rng(&mut OsRng, prehash)
}
}

#[cfg(feature = "ecdsa")]
impl RandomizedPrehashSigner<Signature> for SigningKey {
fn sign_prehash_with_rng(
&self,
rng: &mut impl CryptoRngCore,
prehash: &[u8],
) -> Result<Signature> {
let z = bits2field::<NistP521>(prehash)?;
let k = Scalar::random(rng);
sign_prehashed(self.0.as_nonzero_scalar().as_ref(), k, &z).map(|sig| sig.0)
}
}

#[cfg(feature = "ecdsa")]
impl RandomizedSigner<Signature> for SigningKey {
fn try_sign_with_rng(&self, rng: &mut impl CryptoRngCore, msg: &[u8]) -> Result<Signature> {
self.sign_prehash_with_rng(rng, &Sha512::digest(msg))
}
}

#[cfg(all(feature = "ecdsa", feature = "getrandom"))]
impl Signer<Signature> for SigningKey {
fn try_sign(&self, msg: &[u8]) -> Result<Signature> {
self.try_sign_with_rng(&mut OsRng, msg)
}
}

/// ECDSA/P-521 verification key (i.e. public key)
#[cfg(feature = "ecdsa")]
#[derive(Clone)]
pub struct VerifyingKey(ecdsa_core::VerifyingKey<NistP521>);

#[cfg(feature = "ecdsa")]
impl VerifyingKey {
/// Initialize [`VerifyingKey`] from a SEC1-encoded public key.
pub fn from_sec1_bytes(bytes: &[u8]) -> Result<Self> {
ecdsa_core::VerifyingKey::<NistP521>::from_sec1_bytes(bytes).map(Into::into)
}

/// Initialize [`VerifyingKey`] from an affine point.
///
/// Returns an [`Error`] if the given affine point is the additive identity
/// (a.k.a. point at infinity).
pub fn from_affine(affine: AffinePoint) -> Result<Self> {
ecdsa_core::VerifyingKey::<NistP521>::from_affine(affine).map(Into::into)
}

/// Initialize [`VerifyingKey`] from an [`EncodedPoint`].
pub fn from_encoded_point(public_key: &EncodedPoint) -> Result<Self> {
ecdsa_core::VerifyingKey::<NistP521>::from_encoded_point(public_key).map(Into::into)
}

/// Serialize this [`VerifyingKey`] as a SEC1 [`EncodedPoint`], optionally
/// applying point compression.
pub fn to_encoded_point(&self, compress: bool) -> EncodedPoint {
self.0.to_encoded_point(compress)
}

/// Borrow the inner [`AffinePoint`] for this public key.
pub fn as_affine(&self) -> &AffinePoint {
self.0.as_affine()
}
}

#[cfg(feature = "ecdsa")]
impl From<&SigningKey> for VerifyingKey {
fn from(signing_key: &SigningKey) -> VerifyingKey {
Self::from(*signing_key.0.verifying_key())
}
}

#[cfg(feature = "ecdsa")]
impl From<ecdsa_core::VerifyingKey<NistP521>> for VerifyingKey {
fn from(inner: ecdsa_core::VerifyingKey<NistP521>) -> VerifyingKey {
VerifyingKey(inner)
}
}

#[cfg(feature = "ecdsa")]
impl PrehashVerifier<Signature> for VerifyingKey {
fn verify_prehash(&self, prehash: &[u8], signature: &Signature) -> Result<()> {
self.0.verify_prehash(prehash, signature)
}
}

#[cfg(feature = "ecdsa")]
impl Verifier<Signature> for VerifyingKey {
fn verify(&self, msg: &[u8], signature: &Signature) -> Result<()> {
self.verify_prehash(&Sha512::digest(msg), signature)
}
}

#[cfg(all(test, feature = "ecdsa", feature = "getrandom"))]
mod tests {
// TODO(tarcieri): RFC6979 support + test vectors

mod sign {
use crate::{test_vectors::ecdsa::ECDSA_TEST_VECTORS, NistP521};
ecdsa_core::new_signing_test!(NistP521, ECDSA_TEST_VECTORS);
}

mod verify {
use crate::{test_vectors::ecdsa::ECDSA_TEST_VECTORS, NistP521};
ecdsa_core::new_verification_test!(NistP521, ECDSA_TEST_VECTORS);
}

// TODO(tarcieri): wycheproof test vectors
// mod wycheproof {
// use crate::NistP521;
// ecdsa_core::new_wycheproof_test!(wycheproof, "wycheproof", NistP521);
// }
}
7 changes: 7 additions & 0 deletions p521/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ pub mod arithmetic;
#[cfg(feature = "ecdh")]
pub mod ecdh;

#[cfg(feature = "ecdsa-core")]
pub mod ecdsa;

#[cfg(any(feature = "test-vectors", test))]
pub mod test_vectors;

Expand Down Expand Up @@ -85,6 +88,10 @@ pub type FieldBytes = elliptic_curve::FieldBytes<NistP521>;

impl FieldBytesEncoding<NistP521> for U576 {}

/// Non-zero NIST P-521 scalar field element.
#[cfg(feature = "arithmetic")]
pub type NonZeroScalar = elliptic_curve::NonZeroScalar<NistP521>;

/// NIST P-521 public key.
#[cfg(feature = "arithmetic")]
pub type PublicKey = elliptic_curve::PublicKey<NistP521>;
Expand Down
2 changes: 2 additions & 0 deletions p521/src/test_vectors.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! secp521r1 test vectors.

#[cfg(test)]
pub mod ecdsa;
pub mod group;
Loading

0 comments on commit 62a4091

Please sign in to comment.