From ee67f36ba9362f41b5a46c7be99ea2ef398075fa Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 21 Nov 2019 00:31:10 +0000 Subject: [PATCH 1/5] Move verify_batch() to new batch module. --- src/ed25519.rs | 127 ++----------------------------------------------- src/lib.rs | 2 + 2 files changed, 6 insertions(+), 123 deletions(-) diff --git a/src/ed25519.rs b/src/ed25519.rs index 7cfca3f..7c55a65 100644 --- a/src/ed25519.rs +++ b/src/ed25519.rs @@ -7,9 +7,8 @@ // Authors: // - isis agora lovecruft -//! ed25519 keypairs and batch verification. +//! ed25519 keypairs. -#[allow(unused_imports)] use core::default::Default; use rand::{CryptoRng, RngCore}; @@ -19,139 +18,21 @@ use serde::de::Error as SerdeError; #[cfg(feature = "serde")] use serde::de::Visitor; #[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -#[cfg(feature = "serde")] -use serde::{Deserializer, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub use sha2::Sha512; use curve25519_dalek::digest::generic_array::typenum::U64; pub use curve25519_dalek::digest::Digest; -#[cfg(all(feature = "batch", any(feature = "alloc", feature = "std")))] -use curve25519_dalek::constants; -#[cfg(all(feature = "batch", any(feature = "alloc", feature = "std")))] -use curve25519_dalek::edwards::EdwardsPoint; -#[cfg(all(feature = "batch", any(feature = "alloc", feature = "std")))] -use curve25519_dalek::scalar::Scalar; - +#[cfg(all(feature = "batch", any(feature = "std", feature = "alloc")))] +pub use crate::batch::*; pub use crate::constants::*; pub use crate::errors::*; pub use crate::public::*; pub use crate::secret::*; pub use crate::signature::*; -/// Verify a batch of `signatures` on `messages` with their respective `public_keys`. -/// -/// # Inputs -/// -/// * `messages` is a slice of byte slices, one per signed message. -/// * `signatures` is a slice of `Signature`s. -/// * `public_keys` is a slice of `PublicKey`s. -/// * `csprng` is an implementation of `Rng + CryptoRng`. -/// -/// # Returns -/// -/// * A `Result` whose `Ok` value is an emtpy tuple and whose `Err` value is a -/// `SignatureError` containing a description of the internal error which -/// occured. -/// -/// # Examples -/// -/// ``` -/// extern crate ed25519_dalek; -/// extern crate rand; -/// -/// use ed25519_dalek::verify_batch; -/// use ed25519_dalek::Keypair; -/// use ed25519_dalek::PublicKey; -/// use ed25519_dalek::Signature; -/// use rand::rngs::OsRng; -/// -/// # fn main() { -/// let mut csprng = OsRng{}; -/// let keypairs: Vec = (0..64).map(|_| Keypair::generate(&mut csprng)).collect(); -/// let msg: &[u8] = b"They're good dogs Brant"; -/// let messages: Vec<&[u8]> = (0..64).map(|_| msg).collect(); -/// let signatures: Vec = keypairs.iter().map(|key| key.sign(&msg)).collect(); -/// let public_keys: Vec = keypairs.iter().map(|key| key.public).collect(); -/// -/// let result = verify_batch(&messages[..], &signatures[..], &public_keys[..]); -/// assert!(result.is_ok()); -/// # } -/// ``` -#[cfg(all(feature = "batch", any(feature = "alloc", feature = "std")))] -#[allow(non_snake_case)] -pub fn verify_batch( - messages: &[&[u8]], - signatures: &[Signature], - public_keys: &[PublicKey], -) -> Result<(), SignatureError> -{ - if signatures.len() != messages.len() || - signatures.len() != public_keys.len() || - public_keys.len() != messages.len() { - return Err(SignatureError(InternalError::ArrayLengthError{ - name_a: "signatures", length_a: signatures.len(), - name_b: "messages", length_b: messages.len(), - name_c: "public_keys", length_c: public_keys.len(), - })); - } - - #[cfg(feature = "alloc")] - use alloc::vec::Vec; - #[cfg(feature = "std")] - use std::vec::Vec; - - use core::iter::once; - use rand::{Rng, thread_rng}; - - use curve25519_dalek::traits::IsIdentity; - use curve25519_dalek::traits::VartimeMultiscalarMul; - - // Select a random 128-bit scalar for each signature. - let zs: Vec = signatures - .iter() - .map(|_| Scalar::from(thread_rng().gen::())) - .collect(); - - // Compute the basepoint coefficient, ∑ s[i]z[i] (mod l) - let B_coefficient: Scalar = signatures - .iter() - .map(|sig| sig.s) - .zip(zs.iter()) - .map(|(s, z)| z * s) - .sum(); - - // Compute H(R || A || M) for each (signature, public_key, message) triplet - let hrams = (0..signatures.len()).map(|i| { - let mut h: Sha512 = Sha512::default(); - h.input(signatures[i].R.as_bytes()); - h.input(public_keys[i].as_bytes()); - h.input(&messages[i]); - Scalar::from_hash(h) - }); - - // Multiply each H(R || A || M) by the random value - let zhrams = hrams.zip(zs.iter()).map(|(hram, z)| hram * z); - - let Rs = signatures.iter().map(|sig| sig.R.decompress()); - let As = public_keys.iter().map(|pk| Some(pk.1)); - let B = once(Some(constants::ED25519_BASEPOINT_POINT)); - - // Compute (-∑ z[i]s[i] (mod l)) B + ∑ z[i]R[i] + ∑ (z[i]H(R||A||M)[i] (mod l)) A[i] = 0 - let id = EdwardsPoint::optional_multiscalar_mul( - once(-B_coefficient).chain(zs.iter().cloned()).chain(zhrams), - B.chain(Rs).chain(As), - ).ok_or_else(|| SignatureError(InternalError::VerifyError))?; - - if id.is_identity() { - Ok(()) - } else { - Err(SignatureError(InternalError::VerifyError)) - } -} - /// An ed25519 keypair. #[derive(Debug, Default)] // we derive Default in order to use the clear() method in Drop pub struct Keypair { diff --git a/src/lib.rs b/src/lib.rs index 92f1d45..bce5edf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -247,6 +247,8 @@ extern crate rand; extern crate serde; extern crate sha2; +#[cfg(all(feature = "batch", any(feature = "std", feature = "alloc")))] +mod batch; mod constants; mod ed25519; mod errors; From 85a218ac402bece6778540ec1376a944dfb0c2d0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 22 Nov 2019 23:29:14 +0000 Subject: [PATCH 2/5] Implement deterministic batch verification and synthetic nonce generation. --- Cargo.toml | 10 ++- src/batch.rs | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 3 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 src/batch.rs diff --git a/Cargo.toml b/Cargo.toml index 09b8ef2..1c8b73c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,8 @@ features = ["nightly", "batch"] [dependencies] clear_on_drop = { version = "0.2" } -curve25519-dalek = { version = "1", default-features = false } +curve25519-dalek = { version = "2.0.0-alpha.1", default-features = false } +merlin = { version = "1", default-features = false, optional = true } rand = { version = "0.7", default-features = false, optional = true } serde = { version = "1.0", optional = true } sha2 = { version = "0.8", default-features = false } @@ -46,11 +47,14 @@ default = ["std", "u64_backend"] std = ["curve25519-dalek/std", "sha2/std", "rand/std"] alloc = ["curve25519-dalek/alloc", "rand/alloc"] nightly = ["curve25519-dalek/nightly", "clear_on_drop/nightly", "rand/nightly"] -batch = ["rand"] +batch = ["merlin", "rand"] +# This feature enables deterministic batch verification. +batch_deterministic = ["merlin", "rand"] asm = ["sha2/asm"] # This features turns off stricter checking for scalar malleability in signatures legacy_compatibility = [] -yolocrypto = ["curve25519-dalek/yolocrypto"] u64_backend = ["curve25519-dalek/u64_backend"] u32_backend = ["curve25519-dalek/u32_backend"] +# Deprecated curve25519-dalek feature, use "simd_backend" instead: avx2_backend = ["curve25519-dalek/avx2_backend"] +simd_backend = ["curve25519-dalek/simd_backend"] \ No newline at end of file diff --git a/src/batch.rs b/src/batch.rs new file mode 100644 index 0000000..7368ccf --- /dev/null +++ b/src/batch.rs @@ -0,0 +1,203 @@ +// -*- mode: rust; -*- +// +// This file is part of ed25519-dalek. +// Copyright (c) 2017-2019 isis lovecruft +// See LICENSE for licensing information. +// +// Authors: +// - isis agora lovecruft + +//! Batch signature verification. + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; +#[cfg(feature = "std")] +use std::vec::Vec; + +use core::iter::once; + +use curve25519_dalek::constants; +use curve25519_dalek::edwards::EdwardsPoint; +use curve25519_dalek::scalar::Scalar; +use curve25519_dalek::traits::IsIdentity; +use curve25519_dalek::traits::VartimeMultiscalarMul; + +pub use curve25519_dalek::digest::Digest; + +use merlin::Transcript; + +#[cfg(all(feature = "batch", not(feature = "batch_deterministic")))] +use rand::{Rng, thread_rng}; +#[cfg(all(not(feature = "batch"), feature = "batch_deterministic"))] +use rand::{CryptoRng, RngCore}; + +use sha2::Sha512; + +use crate::errors::InternalError; +use crate::errors::SignatureError; +use crate::public::PublicKey; +use crate::signature::Signature; + +trait BatchTranscript { + fn append_hrams(&mut self, hrams: &Vec); +} + +impl BatchTranscript for Transcript { + /// Add all the computed `H(R||A||M)`s to the protocol transcript. + /// + /// Each is also prefixed with their index in the vector. + fn append_hrams(&mut self, hrams: &Vec) { + for (i, hram) in hrams.iter().enumerate() { + self.append_u64(b"", i as u64); + self.append_message(b"hram", hram.as_bytes()); + } + } +} + +/// An implementation of `rand_core::RngCore` which does nothing, to provide +/// purely deterministic transcript-based nonces, rather than synthetically +/// random nonces. +#[cfg(all(not(feature = "batch"), feature = "batch_deterministic"))] +struct ZeroRng {} + +#[cfg(all(not(feature = "batch"), feature = "batch_deterministic"))] +impl rand_core::RngCore for ZeroRng { + fn next_u32(&mut self) -> u32 { + rand_core::impls::next_u32_via_fill(self) + } + + fn next_u64(&mut self) -> u64 { + rand_core::impls::next_u64_via_fill(self) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + self.fill_bytes(dest); + Ok(()) + } +} + +#[cfg(all(not(feature = "batch"), feature = "batch_deterministic"))] +impl rand_core::CryptoRng for ZeroRng {} + +#[cfg(all(not(feature = "batch"), feature = "batch_deterministic"))] +fn zero_rng() -> ZeroRng { + ZeroRng +} + +/// Verify a batch of `signatures` on `messages` with their respective `public_keys`. +/// +/// # Inputs +/// +/// * `messages` is a slice of byte slices, one per signed message. +/// * `signatures` is a slice of `Signature`s. +/// * `public_keys` is a slice of `PublicKey`s. +/// * `csprng` is an implementation of `Rng + CryptoRng`. +/// +/// # Returns +/// +/// * A `Result` whose `Ok` value is an emtpy tuple and whose `Err` value is a +/// `SignatureError` containing a description of the internal error which +/// occured. +/// +/// # Examples +/// +/// ``` +/// extern crate ed25519_dalek; +/// extern crate rand; +/// +/// use ed25519_dalek::verify_batch; +/// use ed25519_dalek::Keypair; +/// use ed25519_dalek::PublicKey; +/// use ed25519_dalek::Signature; +/// use rand::rngs::OsRng; +/// +/// # fn main() { +/// let mut csprng = OsRng{}; +/// let keypairs: Vec = (0..64).map(|_| Keypair::generate(&mut csprng)).collect(); +/// let msg: &[u8] = b"They're good dogs Brant"; +/// let messages: Vec<&[u8]> = (0..64).map(|_| msg).collect(); +/// let signatures: Vec = keypairs.iter().map(|key| key.sign(&msg)).collect(); +/// let public_keys: Vec = keypairs.iter().map(|key| key.public).collect(); +/// +/// let result = verify_batch(&messages[..], &signatures[..], &public_keys[..]); +/// assert!(result.is_ok()); +/// # } +/// ``` +#[cfg(all(any(feature = "batch", feature = "batch_deterministic"), + any(feature = "alloc", feature = "std")))] +#[allow(non_snake_case)] +pub fn verify_batch( + messages: &[&[u8]], + signatures: &[Signature], + public_keys: &[PublicKey], +) -> Result<(), SignatureError> +{ + // Return an Error if any of the vectors were not the same size as the others. + if signatures.len() != messages.len() || + signatures.len() != public_keys.len() || + public_keys.len() != messages.len() { + return Err(SignatureError(InternalError::ArrayLengthError{ + name_a: "signatures", length_a: signatures.len(), + name_b: "messages", length_b: messages.len(), + name_c: "public_keys", length_c: public_keys.len(), + })); + } + + // Compute H(R || A || M) for each (signature, public_key, message) triplet + let hrams: Vec = (0..signatures.len()).map(|i| { + let mut h: Sha512 = Sha512::default(); + h.input(signatures[i].R.as_bytes()); + h.input(public_keys[i].as_bytes()); + h.input(&messages[i]); + Scalar::from_hash(h) + }).collect(); + + // Build a PRNG based on a transcript of the H(R || A || M)s seen thus far. + // This provides synthethic randomness in the default configuration, and + // purely deterministic in the case of compiling with the + // "batch_deterministic" feature. + let transcript: Transcript = Transcript::new(b"ed25519 batch verification"); + + transcript.append_hrams(&hrams); + + #[cfg(all(feature = "batch", not(feature = "batch_deterministic")))] + let mut prng = transcript.build_rng().finalize(&mut thread_rng()); + #[cfg(all(not(feature = "batch"), feature = "batch_deterministic"))] + let mut prng = transcript.build_rng().finalize(&mut zero_rng()); + + // Select a random 128-bit scalar for each signature. + let zs: Vec = signatures + .iter() + .map(|_| Scalar::from(thread_rng().gen::())) + .collect(); + + + // Compute the basepoint coefficient, ∑ s[i]z[i] (mod l) + let B_coefficient: Scalar = signatures + .iter() + .map(|sig| sig.s) + .zip(zs.iter()) + .map(|(s, z)| z * s) + .sum(); + + // Multiply each H(R || A || M) by the random value + let zhrams = hrams.iter().zip(zs.iter()).map(|(hram, z)| hram * z); + + let Rs = signatures.iter().map(|sig| sig.R.decompress()); + let As = public_keys.iter().map(|pk| Some(pk.1)); + let B = once(Some(constants::ED25519_BASEPOINT_POINT)); + + // Compute (-∑ z[i]s[i] (mod l)) B + ∑ z[i]R[i] + ∑ (z[i]H(R||A||M)[i] (mod l)) A[i] = 0 + let id = EdwardsPoint::optional_multiscalar_mul( + once(-B_coefficient).chain(zs.iter().cloned()).chain(zhrams), + B.chain(Rs).chain(As), + ).ok_or_else(|| SignatureError(InternalError::VerifyError))?; + + if id.is_identity() { + Ok(()) + } else { + Err(SignatureError(InternalError::VerifyError)) + } +} diff --git a/src/lib.rs b/src/lib.rs index bce5edf..097e784 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -241,6 +241,8 @@ extern crate std; extern crate alloc; extern crate clear_on_drop; extern crate curve25519_dalek; +#[cfg(all(any(feature = "batch", feature = "batch_deterministic"), any(feature = "std", feature = "alloc")))] +extern crate merlin; #[cfg(any(feature = "batch", feature = "std", feature = "alloc", test))] extern crate rand; #[cfg(feature = "serde")] From bd6a8977297922dccd5a2ac94cb4be819d731232 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 22 Nov 2019 23:40:46 +0000 Subject: [PATCH 3/5] Actually use the transcript PRNG. --- src/batch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/batch.rs b/src/batch.rs index 7368ccf..d8c513c 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -170,7 +170,7 @@ pub fn verify_batch( // Select a random 128-bit scalar for each signature. let zs: Vec = signatures .iter() - .map(|_| Scalar::from(thread_rng().gen::())) + .map(|_| Scalar::from(prng.gen::())) .collect(); From ec551145e966894fd320c11eb7d1cdc9922d3b1a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 22 Nov 2019 23:21:01 +0000 Subject: [PATCH 4/5] Add message lengths into nonce generator protocol transcript. --- src/batch.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/batch.rs b/src/batch.rs index d8c513c..2cde684 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -40,6 +40,7 @@ use crate::signature::Signature; trait BatchTranscript { fn append_hrams(&mut self, hrams: &Vec); + fn append_message_lengths(&mut self, message_lengths: &Vec); } impl BatchTranscript for Transcript { @@ -48,10 +49,18 @@ impl BatchTranscript for Transcript { /// Each is also prefixed with their index in the vector. fn append_hrams(&mut self, hrams: &Vec) { for (i, hram) in hrams.iter().enumerate() { + // XXX add message length into transcript self.append_u64(b"", i as u64); self.append_message(b"hram", hram.as_bytes()); } } + + fn append_message_lengths(&mut self, message_lengths: &Vec) { + for (i, len) in message_lengths.iter().enumerate() { + self.append_u64(b"", i as u64); + self.append_u64(b"mlen", len as u64); + } + } } /// An implementation of `rand_core::RngCore` which does nothing, to provide @@ -154,6 +163,9 @@ pub fn verify_batch( Scalar::from_hash(h) }).collect(); + // Collect the message lengths to add into the transcript. + let message_lengths: Vec = messages.iter().map(|i| i.len()).collect(); + // Build a PRNG based on a transcript of the H(R || A || M)s seen thus far. // This provides synthethic randomness in the default configuration, and // purely deterministic in the case of compiling with the @@ -161,6 +173,7 @@ pub fn verify_batch( let transcript: Transcript = Transcript::new(b"ed25519 batch verification"); transcript.append_hrams(&hrams); + transcript.append_message_lengths(&message_lengths); #[cfg(all(feature = "batch", not(feature = "batch_deterministic")))] let mut prng = transcript.build_rng().finalize(&mut thread_rng()); From 1be2a65777ffc198d8fbca419a1178a9e9f1c08b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 23 Nov 2019 01:34:13 +0000 Subject: [PATCH 5/5] Maybe I should try compiling my code before showing other cryptographers? lol --- Cargo.toml | 7 ++++--- src/batch.rs | 21 +++++++++++++++------ src/lib.rs | 4 +++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1c8b73c..c0ab84f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,9 @@ features = ["nightly", "batch"] [dependencies] clear_on_drop = { version = "0.2" } curve25519-dalek = { version = "2.0.0-alpha.1", default-features = false } -merlin = { version = "1", default-features = false, optional = true } +merlin = { version = "1", default-features = false, optional = true, git = "https://github.com/isislovecruft/merlin", branch = "develop" } rand = { version = "0.7", default-features = false, optional = true } +rand_core = { version = "0.5", default-features = false, optional = true } serde = { version = "1.0", optional = true } sha2 = { version = "0.8", default-features = false } @@ -49,7 +50,7 @@ alloc = ["curve25519-dalek/alloc", "rand/alloc"] nightly = ["curve25519-dalek/nightly", "clear_on_drop/nightly", "rand/nightly"] batch = ["merlin", "rand"] # This feature enables deterministic batch verification. -batch_deterministic = ["merlin", "rand"] +batch_deterministic = ["merlin", "rand", "rand_core"] asm = ["sha2/asm"] # This features turns off stricter checking for scalar malleability in signatures legacy_compatibility = [] @@ -57,4 +58,4 @@ u64_backend = ["curve25519-dalek/u64_backend"] u32_backend = ["curve25519-dalek/u32_backend"] # Deprecated curve25519-dalek feature, use "simd_backend" instead: avx2_backend = ["curve25519-dalek/avx2_backend"] -simd_backend = ["curve25519-dalek/simd_backend"] \ No newline at end of file +simd_backend = ["curve25519-dalek/simd_backend"] diff --git a/src/batch.rs b/src/batch.rs index 2cde684..a778934 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -26,10 +26,11 @@ pub use curve25519_dalek::digest::Digest; use merlin::Transcript; +use rand::Rng; #[cfg(all(feature = "batch", not(feature = "batch_deterministic")))] -use rand::{Rng, thread_rng}; +use rand::thread_rng; #[cfg(all(not(feature = "batch"), feature = "batch_deterministic"))] -use rand::{CryptoRng, RngCore}; +use rand_core; use sha2::Sha512; @@ -58,7 +59,7 @@ impl BatchTranscript for Transcript { fn append_message_lengths(&mut self, message_lengths: &Vec) { for (i, len) in message_lengths.iter().enumerate() { self.append_u64(b"", i as u64); - self.append_u64(b"mlen", len as u64); + self.append_u64(b"mlen", *len as u64); } } } @@ -79,7 +80,15 @@ impl rand_core::RngCore for ZeroRng { rand_core::impls::next_u64_via_fill(self) } - fn fill_bytes(&mut self, dest: &mut [u8]) { } + /// A no-op function which leaves the destination bytes for randomness unchanged. + /// + /// In this case, the internal merlin code is initialising the destination + /// by doing `[0u8; …]`, which means that when we call + /// `merlin::TranscriptRngBuilder.finalize()`, rather than rekeying the + /// STROBE state based on external randomness, we're doing an + /// `ENC_{state}(00000000000000000000000000000000)` operation, which is + /// identical to the STROBE `MAC` operation. + fn fill_bytes(&mut self, _dest: &mut [u8]) { } fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { self.fill_bytes(dest); @@ -92,7 +101,7 @@ impl rand_core::CryptoRng for ZeroRng {} #[cfg(all(not(feature = "batch"), feature = "batch_deterministic"))] fn zero_rng() -> ZeroRng { - ZeroRng + ZeroRng {} } /// Verify a batch of `signatures` on `messages` with their respective `public_keys`. @@ -170,7 +179,7 @@ pub fn verify_batch( // This provides synthethic randomness in the default configuration, and // purely deterministic in the case of compiling with the // "batch_deterministic" feature. - let transcript: Transcript = Transcript::new(b"ed25519 batch verification"); + let mut transcript: Transcript = Transcript::new(b"ed25519 batch verification"); transcript.append_hrams(&hrams); transcript.append_message_lengths(&message_lengths); diff --git a/src/lib.rs b/src/lib.rs index 097e784..32aff4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -249,7 +249,7 @@ extern crate rand; extern crate serde; extern crate sha2; -#[cfg(all(feature = "batch", any(feature = "std", feature = "alloc")))] +#[cfg(all(any(feature = "batch", feature = "batch_deterministic"), any(feature = "std", feature = "alloc")))] mod batch; mod constants; mod ed25519; @@ -260,3 +260,5 @@ mod signature; // Export everything public in ed25519. pub use crate::ed25519::*; +#[cfg(all(any(feature = "batch", feature = "batch_deterministic"), any(feature = "std", feature = "alloc")))] +pub use crate::batch::*;