From bc481e75bc4e06e7e62bfa115654ea2aa10d7c7b Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 4 Aug 2025 01:17:19 +0200 Subject: [PATCH 1/2] wip: finish ecdsa impl for brainpool --- Cargo.lock | 4 ++++ bp256/Cargo.toml | 9 +++++++- bp256/src/r1/ecdsa.rs | 50 +++++++++++++++++++++++++++++++++++++++---- bp256/tests/ecdsa.rs | 36 +++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 bp256/tests/ecdsa.rs diff --git a/Cargo.lock b/Cargo.lock index c022e4b19..9e97d4896 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,8 +145,12 @@ version = "0.7.0-pre" dependencies = [ "ecdsa", "elliptic-curve", + "hex-literal", "primefield", "primeorder", + "proptest", + "rand_chacha", + "rand_core", "sha2", ] diff --git a/bp256/Cargo.toml b/bp256/Cargo.toml index 147a7d086..f627e48f8 100644 --- a/bp256/Cargo.toml +++ b/bp256/Cargo.toml @@ -23,7 +23,7 @@ primeorder = { version = "=0.14.0-pre.7", optional = true } sha2 = { version = "0.11.0-rc.0", optional = true, default-features = false } [features] -default = ["pkcs8", "std"] +default = ["pkcs8", "std", "ecdsa"] alloc = ["ecdsa?/alloc", "elliptic-curve/alloc", "primeorder?/alloc"] std = ["alloc", "ecdsa?/std", "elliptic-curve/std"] @@ -32,6 +32,13 @@ pkcs8 = ["ecdsa/pkcs8", "elliptic-curve/pkcs8"] serde = ["ecdsa/serde", "elliptic-curve/serde"] sha256 = ["ecdsa/digest", "ecdsa/hazmat", "sha2"] arithmetic = ["dep:primefield", "dep:primeorder"] +ecdsa = ["arithmetic", "ecdsa/signing", "ecdsa/verifying", "sha256"] + +[dev-dependencies] +hex-literal = "1.0.0" +proptest = "1" +rand_chacha = "0.9.0" +rand_core = "0.9.3" [package.metadata.docs.rs] all-features = true diff --git a/bp256/src/r1/ecdsa.rs b/bp256/src/r1/ecdsa.rs index 5dc2d03a3..591222d92 100644 --- a/bp256/src/r1/ecdsa.rs +++ b/bp256/src/r1/ecdsa.rs @@ -1,17 +1,59 @@ //! Elliptic Curve Digital Signature Algorithm (ECDSA) +//! +//! This module contains support for computing and verifying ECDSA signatures. +//! To use it, you will need to enable the following Cargo feature: `ecdsa` +//! +//! ## Signing/Verification Example +//! +//! This example requires the `ecdsa` Cargo feature is enabled: +//! +//! ``` +//! # #[cfg(feature = "ecdsa")] +//! # { +//! use bp256::{ +//! r1::ecdsa::{SigningKey, Signature, signature::Signer}, +//! }; +//! use rand_core::OsRng; // requires 'os_rng' feature +//! +//! // Signing +//! let signing_key = SigningKey::try_from_rng(&mut OsRng).unwrap(); // 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 bp256::r1::ecdsa::{VerifyingKey, signature::Verifier}; +//! +//! let verifying_key = VerifyingKey::from(&signing_key); // Serialize with `::to_encoded_point()` +//! assert!(verifying_key.verify(message, &signature).is_ok()); +//! # } +//! ``` -pub use super::BrainpoolP256r1; +pub use ecdsa::{ + RecoveryId, + signature::{self, Error}, +}; -/// ECDSA/brainpoolP256r1 signature (fixed-size) +use super::BrainpoolP256r1; +use ecdsa::EcdsaCurve; + +/// ECDSA/Brainpool-256r1 signature (fixed-size) pub type Signature = ecdsa::Signature; -/// ECDSA/brainpoolP256r1 signature (ASN.1 DER encoded) +/// ECDSA/Brainpool-256r1 signature (ASN.1 DER encoded) pub type DerSignature = ecdsa::der::Signature; -impl ecdsa::EcdsaCurve for BrainpoolP256r1 { +impl EcdsaCurve for BrainpoolP256r1 { const NORMALIZE_S: bool = false; } +/// ECDSA/Brainpool-256r1 signing key +#[cfg(feature = "ecdsa")] +pub type SigningKey = ecdsa::SigningKey; + +/// ECDSA/Brainpool-256r1 verification key (i.e. public key) +#[cfg(feature = "ecdsa")] +pub type VerifyingKey = ecdsa::VerifyingKey; + #[cfg(feature = "sha256")] impl ecdsa::hazmat::DigestAlgorithm for BrainpoolP256r1 { type Digest = sha2::Sha256; diff --git a/bp256/tests/ecdsa.rs b/bp256/tests/ecdsa.rs new file mode 100644 index 000000000..5bcb55ea7 --- /dev/null +++ b/bp256/tests/ecdsa.rs @@ -0,0 +1,36 @@ +//! ECDSA tests. + +#![cfg(feature = "ecdsa")] + +use bp256::r1::ecdsa::{ + SigningKey, VerifyingKey, + signature::{Signer, Verifier}, +}; +use proptest::prelude::*; +use rand_chacha::ChaCha8Rng; +use rand_core::SeedableRng; + +prop_compose! { + fn signing_key()(seed in any::<[u8; 32]>()) -> SigningKey { + let mut rng = ChaCha8Rng::from_seed(seed); + SigningKey::try_from_rng(&mut rng).unwrap() + } +} + +proptest! { + #[test] + fn recover_from_msg(sk in signing_key()) { + let msg = b"example"; + let (signature, v) = sk.sign_recoverable(msg).unwrap(); + let recovered_vk = VerifyingKey::recover_from_msg(msg, &signature, v).unwrap(); + prop_assert_eq!(sk.verifying_key(), &recovered_vk); + } + + #[test] + fn sign_roundtrip(sk in signing_key()) { + let msg = b"example"; + let (signature, _v) = sk.try_sign(msg).unwrap(); + let vk = sk.verifying_key(); + prop_assert!(vk.verify(msg, &signature).is_ok()); + } +} From 9f42848b3a3cbc463911955d898ba0d62f031014 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 4 Aug 2025 10:43:21 +0200 Subject: [PATCH 2/2] test: add wyhceproof tests --- Cargo.lock | 1 + bp256/Cargo.toml | 2 ++ bp256/src/r1/ecdsa.rs | 8 ++++++++ bp256/src/r1/test_vectors/data/wycheproof.blb | Bin 0 -> 20313 bytes 4 files changed, 11 insertions(+) create mode 100644 bp256/src/r1/test_vectors/data/wycheproof.blb diff --git a/Cargo.lock b/Cargo.lock index 9e97d4896..892faa07b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,7 @@ dependencies = [ name = "bp256" version = "0.7.0-pre" dependencies = [ + "blobby", "ecdsa", "elliptic-curve", "hex-literal", diff --git a/bp256/Cargo.toml b/bp256/Cargo.toml index f627e48f8..4a4c240ea 100644 --- a/bp256/Cargo.toml +++ b/bp256/Cargo.toml @@ -35,6 +35,8 @@ arithmetic = ["dep:primefield", "dep:primeorder"] ecdsa = ["arithmetic", "ecdsa/signing", "ecdsa/verifying", "sha256"] [dev-dependencies] +blobby = "0.3" +ecdsa = { version = "0.17.0-rc.4", features = ["dev"] } hex-literal = "1.0.0" proptest = "1" rand_chacha = "0.9.0" diff --git a/bp256/src/r1/ecdsa.rs b/bp256/src/r1/ecdsa.rs index 591222d92..28696d4d0 100644 --- a/bp256/src/r1/ecdsa.rs +++ b/bp256/src/r1/ecdsa.rs @@ -58,3 +58,11 @@ pub type VerifyingKey = ecdsa::VerifyingKey; impl ecdsa::hazmat::DigestAlgorithm for BrainpoolP256r1 { type Digest = sha2::Sha256; } + +#[cfg(all(test, feature = "ecdsa"))] +mod tests { + mod wycheproof { + use crate::BrainpoolP256r1; + ecdsa::new_wycheproof_test!(wycheproof, "wycheproof", BrainpoolP256r1); + } +} diff --git a/bp256/src/r1/test_vectors/data/wycheproof.blb b/bp256/src/r1/test_vectors/data/wycheproof.blb new file mode 100644 index 0000000000000000000000000000000000000000..4d796322bf2cac108772f277b705d0b5343770dd GIT binary patch literal 20313 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