9 Commits

Author SHA1 Message Date
Luke Parker
677a2e5749 Fix zeroization timeline in multiexp, cargo machete 2025-08-20 00:35:56 -04:00
Luke Parker
38bda1d586 dalek_ff_group::FieldElement: FromUniformBytes<64> 2025-08-20 00:23:39 -04:00
Luke Parker
2bc2ca6906 Implement FromUniformBytes<64> for dalek_ff_group::Scalar 2025-08-20 00:06:07 -04:00
Luke Parker
900a6612d7 Use std-shims to reduce flexible-transcript MSRV to 1.66
flexible-transcript already had a shim to support <1.66. This was irrelevant
since flexible-transcript had a MSRV of 1.73. Due to how clunky it was, it has
been removed despite theoretically enabling an even lower MSRV.
2025-08-19 23:43:26 -04:00
Luke Parker
17c1d5cd6b Tweak multiexp to Zeroize points when invoked in constant time, not just scalars 2025-08-19 22:28:59 -04:00
Luke Parker
8a1b56a928 Make the transcript dependency optional for schnorr-signatures
It's only required when aggregating.
2025-08-19 21:50:58 -04:00
Luke Parker
75964cf6da Place Schnorr signature aggregation behind a feature flag 2025-08-19 21:45:59 -04:00
Luke Parker
d407e35cee Fix Ciphersuite feature flagging 2025-08-19 21:42:25 -04:00
Luke Parker
c8ef044acb Version bump std-shims 2025-08-19 21:01:14 -04:00
24 changed files with 71 additions and 70 deletions

15
Cargo.lock generated
View File

@@ -1527,7 +1527,7 @@ dependencies = [
[[package]]
name = "ciphersuite"
version = "0.4.1"
version = "0.4.2"
dependencies = [
"dalek-ff-group",
"digest 0.10.7",
@@ -1980,7 +1980,7 @@ dependencies = [
[[package]]
name = "dalek-ff-group"
version = "0.4.2"
version = "0.4.3"
dependencies = [
"crypto-bigint",
"curve25519-dalek",
@@ -2698,14 +2698,13 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flexible-transcript"
version = "0.3.3"
version = "0.3.4"
dependencies = [
"blake2",
"digest 0.10.7",
"merlin",
"rustversion",
"sha2",
"subtle",
"std-shims",
"zeroize",
]
@@ -5119,7 +5118,7 @@ dependencies = [
[[package]]
name = "multiexp"
version = "0.4.0"
version = "0.4.1"
dependencies = [
"dalek-ff-group",
"ff",
@@ -7845,7 +7844,7 @@ dependencies = [
[[package]]
name = "schnorr-signatures"
version = "0.5.1"
version = "0.5.2"
dependencies = [
"ciphersuite",
"dalek-ff-group",
@@ -9692,7 +9691,7 @@ dependencies = [
[[package]]
name = "std-shims"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"hashbrown 0.14.5",
"rustversion",

View File

@@ -1,6 +1,6 @@
[package]
name = "std-shims"
version = "0.1.3"
version = "0.1.4"
description = "A series of std shims to make alloc more feasible"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/common/std-shims"

View File

@@ -26,7 +26,7 @@ blake2 = { version = "0.10", default-features = false, features = ["std"] }
transcript = { package = "flexible-transcript", path = "../crypto/transcript", default-features = false, features = ["std", "recommended"] }
ciphersuite = { path = "../crypto/ciphersuite", default-features = false, features = ["std"] }
schnorr = { package = "schnorr-signatures", path = "../crypto/schnorr", default-features = false, features = ["std"] }
schnorr = { package = "schnorr-signatures", path = "../crypto/schnorr", default-features = false, features = ["std", "aggregate"] }
dkg-musig = { path = "../crypto/dkg/musig", default-features = false, features = ["std"] }
frost = { package = "modular-frost", path = "../crypto/frost" }
frost-schnorrkel = { path = "../crypto/schnorrkel" }

View File

@@ -1,13 +1,13 @@
[package]
name = "ciphersuite"
version = "0.4.1"
version = "0.4.2"
description = "Ciphersuites built around ff/group"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/ciphersuite"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = ["ciphersuite", "ff", "group"]
edition = "2021"
rust-version = "1.73"
rust-version = "1.66"
[package.metadata.docs.rs]
all-features = true
@@ -24,7 +24,7 @@ rand_core = { version = "0.6", default-features = false }
zeroize = { version = "^1.5", default-features = false, features = ["derive"] }
subtle = { version = "^2.4", default-features = false }
digest = { version = "0.10", default-features = false }
digest = { version = "0.10", default-features = false, features = ["core-api"] }
transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false }
sha2 = { version = "0.10", default-features = false, optional = true }
sha3 = { version = "0.10", default-features = false, optional = true }

View File

@@ -3,11 +3,11 @@
#![cfg_attr(not(feature = "std"), no_std)]
use core::fmt::Debug;
#[cfg(any(feature = "alloc", feature = "std"))]
#[allow(unused_imports)]
use std_shims::{
prelude::*,
io::{self, Read},
};
use std_shims::prelude::*;
#[cfg(any(feature = "alloc", feature = "std"))]
use std_shims::io::{self, Read};
use rand_core::{RngCore, CryptoRng};

View File

@@ -1,6 +1,6 @@
[package]
name = "dalek-ff-group"
version = "0.4.2"
version = "0.4.3"
description = "ff/group bindings around curve25519-dalek"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dalek-ff-group"

View File

@@ -17,7 +17,7 @@ use crypto_bigint::{
impl_modulus,
};
use group::ff::{Field, PrimeField, FieldBits, PrimeFieldBits};
use group::ff::{Field, PrimeField, FieldBits, PrimeFieldBits, FromUniformBytes};
use crate::{u8_from_bool, constant_time, math_op, math};
@@ -311,6 +311,12 @@ impl FieldElement {
}
}
impl FromUniformBytes<64> for FieldElement {
fn from_uniform_bytes(bytes: &[u8; 64]) -> Self {
Self::wide_reduce(*bytes)
}
}
impl Sum<FieldElement> for FieldElement {
fn sum<I: Iterator<Item = FieldElement>>(iter: I) -> FieldElement {
let mut res = FieldElement::ZERO;

View File

@@ -30,7 +30,7 @@ use dalek::{
pub use constants::{ED25519_BASEPOINT_TABLE, RISTRETTO_BASEPOINT_TABLE};
use group::{
ff::{Field, PrimeField, FieldBits, PrimeFieldBits},
ff::{Field, PrimeField, FieldBits, PrimeFieldBits, FromUniformBytes},
Group, GroupEncoding,
prime::PrimeGroup,
};
@@ -322,6 +322,12 @@ impl PrimeFieldBits for Scalar {
}
}
impl FromUniformBytes<64> for Scalar {
fn from_uniform_bytes(bytes: &[u8; 64]) -> Self {
Self::from_bytes_mod_order_wide(bytes)
}
}
impl Sum<Scalar> for Scalar {
fn sum<I: Iterator<Item = Scalar>>(iter: I) -> Scalar {
Self(DScalar::sum(iter))

View File

@@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
edition = "2021"
rust-version = "1.73"
rust-version = "1.66"
[package.metadata.docs.rs]
all-features = true

View File

@@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/dealer"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
edition = "2021"
rust-version = "1.73"
rust-version = "1.66"
[package.metadata.docs.rs]
all-features = true

View File

@@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/recover
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
edition = "2021"
rust-version = "1.73"
rust-version = "1.66"
[package.metadata.docs.rs]
all-features = true

View File

@@ -1,6 +1,6 @@
[package]
name = "multiexp"
version = "0.4.0"
version = "0.4.2"
description = "Multiexponentiation algorithms for ff/group"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/multiexp"

View File

@@ -12,7 +12,7 @@ use crate::{multiexp, multiexp_vartime};
// Flatten the contained statements to a single Vec.
// Wrapped in Zeroizing in case any of the included statements contain private values.
#[allow(clippy::type_complexity)]
fn flat<Id: Copy + Zeroize, G: Group<Scalar: PrimeFieldBits + Zeroize> + Zeroize>(
fn flat<Id: Copy + Zeroize, G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>(
slice: &[(Id, Vec<(G::Scalar, G)>)],
) -> Zeroizing<Vec<(G::Scalar, G)>> {
Zeroizing::new(slice.iter().flat_map(|pairs| pairs.1.iter()).copied().collect::<Vec<_>>())
@@ -21,11 +21,11 @@ fn flat<Id: Copy + Zeroize, G: Group<Scalar: PrimeFieldBits + Zeroize> + Zeroize
/// A batch verifier intended to verify a series of statements are each equivalent to zero.
#[allow(clippy::type_complexity)]
#[derive(Clone, Zeroize)]
pub struct BatchVerifier<Id: Copy + Zeroize, G: Group<Scalar: PrimeFieldBits + Zeroize> + Zeroize>(
pub struct BatchVerifier<Id: Copy + Zeroize, G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>(
Zeroizing<Vec<(Id, Vec<(G::Scalar, G)>)>>,
);
impl<Id: Copy + Zeroize, G: Group<Scalar: PrimeFieldBits + Zeroize> + Zeroize>
impl<Id: Copy + Zeroize, G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>
BatchVerifier<Id, G>
{
/// Create a new batch verifier, expected to verify the following amount of statements.

View File

@@ -5,6 +5,8 @@
#[cfg(not(feature = "std"))]
#[macro_use]
extern crate alloc;
#[allow(unused_imports)]
use std_shims::prelude::*;
use std_shims::vec::Vec;
use zeroize::Zeroize;
@@ -175,7 +177,9 @@ fn algorithm(len: usize) -> Algorithm {
/// Performs a multiexponentiation, automatically selecting the optimal algorithm based on the
/// amount of pairs.
pub fn multiexp<G: Group<Scalar: PrimeFieldBits + Zeroize>>(pairs: &[(G::Scalar, G)]) -> G {
pub fn multiexp<G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>(
pairs: &[(G::Scalar, G)],
) -> G {
match algorithm(pairs.len()) {
Algorithm::Null => Group::identity(),
Algorithm::Single => pairs[0].1 * pairs[0].0,

View File

@@ -7,7 +7,7 @@ use crate::prep_bits;
// Pippenger's algorithm for multiexponentiation, as published in the SIAM Journal on Computing
// DOI: 10.1137/0209022
pub(crate) fn pippenger<G: Group<Scalar: PrimeFieldBits>>(
pub(crate) fn pippenger<G: Zeroize + Group<Scalar: PrimeFieldBits>>(
pairs: &[(G::Scalar, G)],
window: u8,
) -> G {
@@ -31,6 +31,8 @@ pub(crate) fn pippenger<G: Group<Scalar: PrimeFieldBits>>(
intermediate_sum += buckets[b];
res += intermediate_sum;
}
buckets.zeroize();
}
bits.zeroize();

View File

@@ -24,12 +24,12 @@ fn prep_tables<G: Group>(pairs: &[(G::Scalar, G)], window: u8) -> Vec<Vec<G>> {
// Straus's algorithm for multiexponentiation, as published in The American Mathematical Monthly
// DOI: 10.2307/2310929
pub(crate) fn straus<G: Group<Scalar: PrimeFieldBits + Zeroize>>(
pub(crate) fn straus<G: Zeroize + Group<Scalar: PrimeFieldBits>>(
pairs: &[(G::Scalar, G)],
window: u8,
) -> G {
let mut groupings = prep_bits(pairs, window);
let tables = prep_tables(pairs, window);
let mut tables = prep_tables(pairs, window);
let mut res = G::identity();
for b in (0 .. groupings[0].len()).rev() {
@@ -45,6 +45,7 @@ pub(crate) fn straus<G: Group<Scalar: PrimeFieldBits + Zeroize>>(
}
groupings.zeroize();
tables.zeroize();
res
}

View File

@@ -9,7 +9,7 @@ use group::Group;
use crate::BatchVerifier;
pub(crate) fn test_batch<G: Group<Scalar: PrimeFieldBits + Zeroize> + Zeroize>() {
pub(crate) fn test_batch<G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>() {
let valid = |batch: BatchVerifier<_, G>| {
assert!(batch.verify());
assert!(batch.verify_vartime());

View File

@@ -18,7 +18,7 @@ mod batch;
use batch::test_batch;
#[allow(dead_code)]
fn benchmark_internal<G: Group<Scalar: PrimeFieldBits + Zeroize>>(straus_bool: bool) {
fn benchmark_internal<G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>(straus_bool: bool) {
let runs: usize = 20;
let mut start = 0;
@@ -83,7 +83,7 @@ fn benchmark_internal<G: Group<Scalar: PrimeFieldBits + Zeroize>>(straus_bool: b
}
}
fn test_multiexp<G: Group<Scalar: PrimeFieldBits + Zeroize>>() {
fn test_multiexp<G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>() {
let test = |pairs: &[_], sum| {
// These should automatically determine the best algorithm
assert_eq!(multiexp(pairs), sum);

View File

@@ -1,6 +1,6 @@
[package]
name = "schnorr-signatures"
version = "0.5.1"
version = "0.5.2"
description = "Minimal Schnorr signatures crate hosting common code"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/schnorr"
@@ -23,7 +23,7 @@ rand_core = { version = "0.6", default-features = false }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false }
transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false, optional = true }
ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false, features = ["alloc"] }
multiexp = { path = "../multiexp", version = "0.4", default-features = false, features = ["batch"] }
@@ -39,5 +39,6 @@ dalek-ff-group = { path = "../dalek-ff-group" }
ciphersuite = { path = "../ciphersuite", features = ["ed25519"] }
[features]
std = ["std-shims/std", "rand_core/std", "zeroize/std", "transcript/std", "ciphersuite/std", "multiexp/std"]
aggregate = ["transcript"]
std = ["std-shims/std", "rand_core/std", "zeroize/std", "transcript?/std", "ciphersuite/std", "multiexp/std"]
default = ["std"]

View File

@@ -25,6 +25,7 @@ use ciphersuite::{
use multiexp::{multiexp_vartime, BatchVerifier};
/// Half-aggregation from <https://eprint.iacr.org/2021/350>.
#[cfg(feature = "aggregate")]
pub mod aggregate;
#[cfg(test)]

View File

@@ -9,10 +9,9 @@ use ciphersuite::{
};
use multiexp::BatchVerifier;
use crate::{
SchnorrSignature,
aggregate::{SchnorrAggregator, SchnorrAggregate},
};
use crate::SchnorrSignature;
#[cfg(feature = "aggregate")]
use crate::aggregate::{SchnorrAggregator, SchnorrAggregate};
mod rfc8032;
@@ -77,6 +76,7 @@ pub(crate) fn batch_verify<C: Ciphersuite>() {
}
}
#[cfg(feature = "aggregate")]
pub(crate) fn aggregate<C: Ciphersuite>() {
const DST: &[u8] = b"Schnorr Aggregator Test";
@@ -117,5 +117,6 @@ fn test() {
sign::<Ed25519>();
verify::<Ed25519>();
batch_verify::<Ed25519>();
#[cfg(feature = "aggregate")]
aggregate::<Ed25519>();
}

View File

@@ -1,13 +1,13 @@
[package]
name = "flexible-transcript"
version = "0.3.3"
version = "0.3.4"
description = "A simple transcript trait definition, along with viable options"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/transcript"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = ["transcript"]
edition = "2021"
rust-version = "1.73"
rust-version = "1.66"
[package.metadata.docs.rs]
all-features = true
@@ -17,9 +17,8 @@ rustdoc-args = ["--cfg", "docsrs"]
workspace = true
[dependencies]
rustversion = "1"
std-shims = { path = "../../common/std-shims", version = "0.1.4", default-features = false }
subtle = { version = "^2.4", default-features = false }
zeroize = { version = "^1.5", default-features = false }
digest = { version = "0.10", default-features = false, features = ["core-api"] }
@@ -32,8 +31,7 @@ sha2 = { version = "0.10", default-features = false }
blake2 = { version = "0.10", default-features = false }
[features]
std = ["subtle/std", "zeroize/std", "digest/std", "blake2?/std", "merlin?/std"]
std = ["std-shims/std", "zeroize/std", "digest/std", "blake2?/std", "merlin?/std"]
recommended = ["blake2"]
merlin = ["dep:merlin"]
tests = []
default = ["std"]

View File

@@ -2,6 +2,9 @@
#![doc = include_str!("../README.md")]
#![no_std]
#[allow(unused_imports)]
use std_shims::prelude::*;
use zeroize::Zeroize;
use digest::{
@@ -159,35 +162,12 @@ where
// These writes may be optimized out if they're never read
// Attempt to get them marked as read
#[rustversion::since(1.66)]
fn mark_read<D: Send + Clone + SecureDigest>(transcript: &DigestTranscript<D>) {
// Just get a challenge from the state
let mut challenge = core::hint::black_box(transcript.0.clone().finalize());
challenge.as_mut().zeroize();
}
#[rustversion::before(1.66)]
fn mark_read<D: Send + Clone + SecureDigest>(transcript: &mut DigestTranscript<D>) {
// Get a challenge
let challenge = transcript.0.clone().finalize();
// Attempt to use subtle's, non-exposed black_box function, by creating a Choice from this
// challenge
let mut read = 0;
for byte in challenge.as_ref() {
read ^= byte;
}
challenge.as_mut().zeroize();
// Since this Choice isn't further read, its creation may be optimized out, including its
// internal black_box
// This remains our best attempt
let mut choice = bool::from(subtle::Choice::from(read >> 7));
read.zeroize();
choice.zeroize();
}
mark_read(self)
}
}

View File

@@ -1,6 +1,8 @@
use crate::Transcript;
/// Test the sanity of a transcript.
///
/// This will panic if sanity checks fail.
pub fn test_transcript<T: Transcript<Challenge: PartialEq>>() {
// Ensure distinct names cause distinct challenges
{