git checkout -f next ./crypto

Proceeds to remove the eVRF DKG after, only keeping what's relevant to this
branch alone.
This commit is contained in:
Luke Parker
2025-08-15 17:33:22 -04:00
parent 078d6e51e5
commit 15a9cbef40
30 changed files with 318 additions and 213 deletions

View File

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

View File

@@ -28,6 +28,12 @@ macro_rules! dalek_curve {
$Point::generator() $Point::generator()
} }
fn reduce_512(mut scalar: [u8; 64]) -> Self::F {
let res = Scalar::from_bytes_mod_order_wide(&scalar);
scalar.zeroize();
res
}
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F { fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
Scalar::from_hash(Sha512::new_with_prefix(&[dst, data].concat())) Scalar::from_hash(Sha512::new_with_prefix(&[dst, data].concat()))
} }

View File

@@ -66,6 +66,12 @@ impl Ciphersuite for Ed448 {
Point::generator() Point::generator()
} }
fn reduce_512(mut scalar: [u8; 64]) -> Self::F {
let res = Self::hash_to_F(b"Ciphersuite-reduce_512", &scalar);
scalar.zeroize();
res
}
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F { fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
Scalar::wide_reduce(Self::H::digest([dst, data].concat()).as_ref().try_into().unwrap()) Scalar::wide_reduce(Self::H::digest([dst, data].concat()).as_ref().try_into().unwrap())
} }

View File

@@ -6,7 +6,7 @@ use group::ff::PrimeField;
use elliptic_curve::{ use elliptic_curve::{
generic_array::GenericArray, generic_array::GenericArray,
bigint::{NonZero, CheckedAdd, Encoding, U384}, bigint::{NonZero, CheckedAdd, Encoding, U384, U512},
hash2curve::{Expander, ExpandMsg, ExpandMsgXmd}, hash2curve::{Expander, ExpandMsg, ExpandMsgXmd},
}; };
@@ -31,6 +31,22 @@ macro_rules! kp_curve {
$lib::ProjectivePoint::GENERATOR $lib::ProjectivePoint::GENERATOR
} }
fn reduce_512(scalar: [u8; 64]) -> Self::F {
let mut modulus = [0; 64];
modulus[32 ..].copy_from_slice(&(Self::F::ZERO - Self::F::ONE).to_bytes());
let modulus = U512::from_be_slice(&modulus).checked_add(&U512::ONE).unwrap();
let mut wide =
U512::from_be_bytes(scalar).rem(&NonZero::new(modulus).unwrap()).to_be_bytes();
let mut array = *GenericArray::from_slice(&wide[32 ..]);
let res = $lib::Scalar::from_repr(array).unwrap();
wide.zeroize();
array.zeroize();
res
}
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F { fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
// While one of these two libraries does support directly hashing to the Scalar field, the // While one of these two libraries does support directly hashing to the Scalar field, the
// other doesn't. While that's probably an oversight, this is a universally working method // other doesn't. While that's probably an oversight, this is a universally working method

View File

@@ -62,6 +62,12 @@ pub trait Ciphersuite:
// While group does provide this in its API, privacy coins may want to use a custom basepoint // While group does provide this in its API, privacy coins may want to use a custom basepoint
fn generator() -> Self::G; fn generator() -> Self::G;
/// Reduce 512 bits into a uniform scalar.
///
/// If 512 bits is insufficient to perform a reduction into a uniform scalar, the ciphersuite
/// will perform a hash to sample the necessary bits.
fn reduce_512(scalar: [u8; 64]) -> Self::F;
/// Hash the provided domain-separation tag and message to a scalar. Ciphersuites MAY naively /// Hash the provided domain-separation tag and message to a scalar. Ciphersuites MAY naively
/// prefix the tag to the message, enabling transpotion between the two. Accordingly, this /// prefix the tag to the message, enabling transpotion between the two. Accordingly, this
/// function should NOT be used in any scheme where one tag is a valid substring of another /// function should NOT be used in any scheme where one tag is a valid substring of another
@@ -99,6 +105,9 @@ pub trait Ciphersuite:
} }
/// Read a canonical point from something implementing std::io::Read. /// Read a canonical point from something implementing std::io::Read.
///
/// The provided implementation is safe so long as `GroupEncoding::to_bytes` always returns a
/// canonical serialization.
#[cfg(any(feature = "alloc", feature = "std"))] #[cfg(any(feature = "alloc", feature = "std"))]
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn read_G<R: Read>(reader: &mut R) -> io::Result<Self::G> { fn read_G<R: Read>(reader: &mut R) -> io::Result<Self::G> {

View File

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

View File

@@ -35,7 +35,7 @@ impl_modulus!(
type ResidueType = Residue<FieldModulus, { FieldModulus::LIMBS }>; type ResidueType = Residue<FieldModulus, { FieldModulus::LIMBS }>;
/// A constant-time implementation of the Ed25519 field. /// A constant-time implementation of the Ed25519 field.
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Zeroize)]
pub struct FieldElement(ResidueType); pub struct FieldElement(ResidueType);
// Square root of -1. // Square root of -1.
@@ -92,7 +92,7 @@ impl Neg for FieldElement {
} }
} }
impl<'a> Neg for &'a FieldElement { impl Neg for &FieldElement {
type Output = FieldElement; type Output = FieldElement;
fn neg(self) -> Self::Output { fn neg(self) -> Self::Output {
(*self).neg() (*self).neg()

View File

@@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg"
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = ["dkg", "multisig", "threshold", "ff", "group"] keywords = ["dkg", "multisig", "threshold", "ff", "group"]
edition = "2021" edition = "2021"
rust-version = "1.79" rust-version = "1.81"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
@@ -17,7 +17,7 @@ rustdoc-args = ["--cfg", "docsrs"]
workspace = true workspace = true
[dependencies] [dependencies]
thiserror = { version = "1", default-features = false, optional = true } thiserror = { version = "2", default-features = false }
rand_core = { version = "0.6", default-features = false } rand_core = { version = "0.6", default-features = false }
@@ -42,7 +42,7 @@ ciphersuite = { path = "../ciphersuite", default-features = false, features = ["
[features] [features]
std = [ std = [
"thiserror", "thiserror/std",
"rand_core/std", "rand_core/std",

View File

@@ -98,11 +98,11 @@ fn ecdh<C: Ciphersuite>(private: &Zeroizing<C::F>, public: C::G) -> Zeroizing<C:
// Each ecdh must be distinct. Reuse of an ecdh for multiple ciphers will cause the messages to be // Each ecdh must be distinct. Reuse of an ecdh for multiple ciphers will cause the messages to be
// leaked. // leaked.
fn cipher<C: Ciphersuite>(context: &str, ecdh: &Zeroizing<C::G>) -> ChaCha20 { fn cipher<C: Ciphersuite>(context: [u8; 32], ecdh: &Zeroizing<C::G>) -> ChaCha20 {
// Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly // Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly
// TODO: https://github.com/serai-dex/serai/issues/151 // TODO: https://github.com/serai-dex/serai/issues/151
let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0.2"); let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0.2");
transcript.append_message(b"context", context.as_bytes()); transcript.append_message(b"context", context);
transcript.domain_separate(b"encryption_key"); transcript.domain_separate(b"encryption_key");
@@ -134,7 +134,7 @@ fn cipher<C: Ciphersuite>(context: &str, ecdh: &Zeroizing<C::G>) -> ChaCha20 {
fn encrypt<R: RngCore + CryptoRng, C: Ciphersuite, E: Encryptable>( fn encrypt<R: RngCore + CryptoRng, C: Ciphersuite, E: Encryptable>(
rng: &mut R, rng: &mut R,
context: &str, context: [u8; 32],
from: Participant, from: Participant,
to: C::G, to: C::G,
mut msg: Zeroizing<E>, mut msg: Zeroizing<E>,
@@ -197,7 +197,7 @@ impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
pub(crate) fn invalidate_msg<R: RngCore + CryptoRng>( pub(crate) fn invalidate_msg<R: RngCore + CryptoRng>(
&mut self, &mut self,
rng: &mut R, rng: &mut R,
context: &str, context: [u8; 32],
from: Participant, from: Participant,
) { ) {
// Invalidate the message by specifying a new key/Schnorr PoP // Invalidate the message by specifying a new key/Schnorr PoP
@@ -219,7 +219,7 @@ impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
pub(crate) fn invalidate_share_serialization<R: RngCore + CryptoRng>( pub(crate) fn invalidate_share_serialization<R: RngCore + CryptoRng>(
&mut self, &mut self,
rng: &mut R, rng: &mut R,
context: &str, context: [u8; 32],
from: Participant, from: Participant,
to: C::G, to: C::G,
) { ) {
@@ -243,7 +243,7 @@ impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
pub(crate) fn invalidate_share_value<R: RngCore + CryptoRng>( pub(crate) fn invalidate_share_value<R: RngCore + CryptoRng>(
&mut self, &mut self,
rng: &mut R, rng: &mut R,
context: &str, context: [u8; 32],
from: Participant, from: Participant,
to: C::G, to: C::G,
) { ) {
@@ -300,14 +300,14 @@ impl<C: Ciphersuite> EncryptionKeyProof<C> {
// This still doesn't mean the DKG offers an authenticated channel. The per-message keys have no // This still doesn't mean the DKG offers an authenticated channel. The per-message keys have no
// root of trust other than their existence in the assumed-to-exist external authenticated channel. // root of trust other than their existence in the assumed-to-exist external authenticated channel.
fn pop_challenge<C: Ciphersuite>( fn pop_challenge<C: Ciphersuite>(
context: &str, context: [u8; 32],
nonce: C::G, nonce: C::G,
key: C::G, key: C::G,
sender: Participant, sender: Participant,
msg: &[u8], msg: &[u8],
) -> C::F { ) -> C::F {
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Proof of Possession v0.2"); let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Proof of Possession v0.2");
transcript.append_message(b"context", context.as_bytes()); transcript.append_message(b"context", context);
transcript.domain_separate(b"proof_of_possession"); transcript.domain_separate(b"proof_of_possession");
@@ -323,9 +323,9 @@ fn pop_challenge<C: Ciphersuite>(
C::hash_to_F(b"DKG-encryption-proof_of_possession", &transcript.challenge(b"schnorr")) C::hash_to_F(b"DKG-encryption-proof_of_possession", &transcript.challenge(b"schnorr"))
} }
fn encryption_key_transcript(context: &str) -> RecommendedTranscript { fn encryption_key_transcript(context: [u8; 32]) -> RecommendedTranscript {
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2"); let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2");
transcript.append_message(b"context", context.as_bytes()); transcript.append_message(b"context", context);
transcript transcript
} }
@@ -337,58 +337,17 @@ pub(crate) enum DecryptionError {
InvalidProof, InvalidProof,
} }
// A simple box for managing encryption. // A simple box for managing decryption.
#[derive(Clone)] #[derive(Clone, Debug)]
pub(crate) struct Encryption<C: Ciphersuite> { pub(crate) struct Decryption<C: Ciphersuite> {
context: String, context: [u8; 32],
i: Option<Participant>,
enc_key: Zeroizing<C::F>,
enc_pub_key: C::G,
enc_keys: HashMap<Participant, C::G>, enc_keys: HashMap<Participant, C::G>,
} }
impl<C: Ciphersuite> fmt::Debug for Encryption<C> { impl<C: Ciphersuite> Decryption<C> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { pub(crate) fn new(context: [u8; 32]) -> Self {
fmt Self { context, enc_keys: HashMap::new() }
.debug_struct("Encryption")
.field("context", &self.context)
.field("i", &self.i)
.field("enc_pub_key", &self.enc_pub_key)
.field("enc_keys", &self.enc_keys)
.finish_non_exhaustive()
} }
}
impl<C: Ciphersuite> Zeroize for Encryption<C> {
fn zeroize(&mut self) {
self.enc_key.zeroize();
self.enc_pub_key.zeroize();
for (_, mut value) in self.enc_keys.drain() {
value.zeroize();
}
}
}
impl<C: Ciphersuite> Encryption<C> {
pub(crate) fn new<R: RngCore + CryptoRng>(
context: String,
i: Option<Participant>,
rng: &mut R,
) -> Self {
let enc_key = Zeroizing::new(C::random_nonzero_F(rng));
Self {
context,
i,
enc_pub_key: C::generator() * enc_key.deref(),
enc_key,
enc_keys: HashMap::new(),
}
}
pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
EncryptionKeyMessage { msg, enc_key: self.enc_pub_key }
}
pub(crate) fn register<M: Message>( pub(crate) fn register<M: Message>(
&mut self, &mut self,
participant: Participant, participant: Participant,
@@ -402,13 +361,109 @@ impl<C: Ciphersuite> Encryption<C> {
msg.msg msg.msg
} }
// Given a message, and the intended decryptor, and a proof for its key, decrypt the message.
// Returns None if the key was wrong.
pub(crate) fn decrypt_with_proof<E: Encryptable>(
&self,
from: Participant,
decryptor: Participant,
mut msg: EncryptedMessage<C, E>,
// There's no encryption key proof if the accusation is of an invalid signature
proof: Option<EncryptionKeyProof<C>>,
) -> Result<Zeroizing<E>, DecryptionError> {
if !msg.pop.verify(
msg.key,
pop_challenge::<C>(self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
) {
Err(DecryptionError::InvalidSignature)?;
}
if let Some(proof) = proof {
// Verify this is the decryption key for this message
proof
.dleq
.verify(
&mut encryption_key_transcript(self.context),
&[C::generator(), msg.key],
&[self.enc_keys[&decryptor], *proof.key],
)
.map_err(|_| DecryptionError::InvalidProof)?;
cipher::<C>(self.context, &proof.key).apply_keystream(msg.msg.as_mut().as_mut());
Ok(msg.msg)
} else {
Err(DecryptionError::InvalidProof)
}
}
}
// A simple box for managing encryption.
#[derive(Clone)]
pub(crate) struct Encryption<C: Ciphersuite> {
context: [u8; 32],
i: Participant,
enc_key: Zeroizing<C::F>,
enc_pub_key: C::G,
decryption: Decryption<C>,
}
impl<C: Ciphersuite> fmt::Debug for Encryption<C> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt
.debug_struct("Encryption")
.field("context", &self.context)
.field("i", &self.i)
.field("enc_pub_key", &self.enc_pub_key)
.field("decryption", &self.decryption)
.finish_non_exhaustive()
}
}
impl<C: Ciphersuite> Zeroize for Encryption<C> {
fn zeroize(&mut self) {
self.enc_key.zeroize();
self.enc_pub_key.zeroize();
for (_, mut value) in self.decryption.enc_keys.drain() {
value.zeroize();
}
}
}
impl<C: Ciphersuite> Encryption<C> {
pub(crate) fn new<R: RngCore + CryptoRng>(
context: [u8; 32],
i: Participant,
rng: &mut R,
) -> Self {
let enc_key = Zeroizing::new(C::random_nonzero_F(rng));
Self {
context,
i,
enc_pub_key: C::generator() * enc_key.deref(),
enc_key,
decryption: Decryption::new(context),
}
}
pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
EncryptionKeyMessage { msg, enc_key: self.enc_pub_key }
}
pub(crate) fn register<M: Message>(
&mut self,
participant: Participant,
msg: EncryptionKeyMessage<C, M>,
) -> M {
self.decryption.register(participant, msg)
}
pub(crate) fn encrypt<R: RngCore + CryptoRng, E: Encryptable>( pub(crate) fn encrypt<R: RngCore + CryptoRng, E: Encryptable>(
&self, &self,
rng: &mut R, rng: &mut R,
participant: Participant, participant: Participant,
msg: Zeroizing<E>, msg: Zeroizing<E>,
) -> EncryptedMessage<C, E> { ) -> EncryptedMessage<C, E> {
encrypt(rng, &self.context, self.i.unwrap(), self.enc_keys[&participant], msg) encrypt(rng, self.context, self.i, self.decryption.enc_keys[&participant], msg)
} }
pub(crate) fn decrypt<R: RngCore + CryptoRng, I: Copy + Zeroize, E: Encryptable>( pub(crate) fn decrypt<R: RngCore + CryptoRng, I: Copy + Zeroize, E: Encryptable>(
@@ -426,18 +481,18 @@ impl<C: Ciphersuite> Encryption<C> {
batch, batch,
batch_id, batch_id,
msg.key, msg.key,
pop_challenge::<C>(&self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()), pop_challenge::<C>(self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
); );
let key = ecdh::<C>(&self.enc_key, msg.key); let key = ecdh::<C>(&self.enc_key, msg.key);
cipher::<C>(&self.context, &key).apply_keystream(msg.msg.as_mut().as_mut()); cipher::<C>(self.context, &key).apply_keystream(msg.msg.as_mut().as_mut());
( (
msg.msg, msg.msg,
EncryptionKeyProof { EncryptionKeyProof {
key, key,
dleq: DLEqProof::prove( dleq: DLEqProof::prove(
rng, rng,
&mut encryption_key_transcript(&self.context), &mut encryption_key_transcript(self.context),
&[C::generator(), msg.key], &[C::generator(), msg.key],
&self.enc_key, &self.enc_key,
), ),
@@ -445,38 +500,7 @@ impl<C: Ciphersuite> Encryption<C> {
) )
} }
// Given a message, and the intended decryptor, and a proof for its key, decrypt the message. pub(crate) fn into_decryption(self) -> Decryption<C> {
// Returns None if the key was wrong. self.decryption
pub(crate) fn decrypt_with_proof<E: Encryptable>(
&self,
from: Participant,
decryptor: Participant,
mut msg: EncryptedMessage<C, E>,
// There's no encryption key proof if the accusation is of an invalid signature
proof: Option<EncryptionKeyProof<C>>,
) -> Result<Zeroizing<E>, DecryptionError> {
if !msg.pop.verify(
msg.key,
pop_challenge::<C>(&self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
) {
Err(DecryptionError::InvalidSignature)?;
}
if let Some(proof) = proof {
// Verify this is the decryption key for this message
proof
.dleq
.verify(
&mut encryption_key_transcript(&self.context),
&[C::generator(), msg.key],
&[self.enc_keys[&decryptor], *proof.key],
)
.map_err(|_| DecryptionError::InvalidProof)?;
cipher::<C>(&self.context, &proof.key).apply_keystream(msg.msg.as_mut().as_mut());
Ok(msg.msg)
} else {
Err(DecryptionError::InvalidProof)
}
} }
} }

View File

@@ -4,7 +4,6 @@
use core::fmt::{self, Debug}; use core::fmt::{self, Debug};
#[cfg(feature = "std")]
use thiserror::Error; use thiserror::Error;
use zeroize::Zeroize; use zeroize::Zeroize;
@@ -63,8 +62,7 @@ impl fmt::Display for Participant {
} }
/// Various errors possible during key generation. /// Various errors possible during key generation.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug, Error)]
#[cfg_attr(feature = "std", derive(Error))]
pub enum DkgError<B: Clone + PartialEq + Eq + Debug> { pub enum DkgError<B: Clone + PartialEq + Eq + Debug> {
/// A parameter was zero. /// A parameter was zero.
#[cfg_attr(feature = "std", error("a parameter was 0 (threshold {0}, participants {1})"))] #[cfg_attr(feature = "std", error("a parameter was 0 (threshold {0}, participants {1})"))]
@@ -205,25 +203,37 @@ mod lib {
} }
} }
/// Calculate the lagrange coefficient for a signing set. #[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub fn lagrange<F: PrimeField>(i: Participant, included: &[Participant]) -> F { pub(crate) enum Interpolation<F: Zeroize + PrimeField> {
let i_f = F::from(u64::from(u16::from(i))); Constant(Vec<F>),
Lagrange,
}
let mut num = F::ONE; impl<F: Zeroize + PrimeField> Interpolation<F> {
let mut denom = F::ONE; pub(crate) fn interpolation_factor(&self, i: Participant, included: &[Participant]) -> F {
for l in included { match self {
if i == *l { Interpolation::Constant(c) => c[usize::from(u16::from(i) - 1)],
continue; Interpolation::Lagrange => {
let i_f = F::from(u64::from(u16::from(i)));
let mut num = F::ONE;
let mut denom = F::ONE;
for l in included {
if i == *l {
continue;
}
let share = F::from(u64::from(u16::from(*l)));
num *= share;
denom *= share - i_f;
}
// Safe as this will only be 0 if we're part of the above loop
// (which we have an if case to avoid)
num * denom.invert().unwrap()
}
} }
let share = F::from(u64::from(u16::from(*l)));
num *= share;
denom *= share - i_f;
} }
// Safe as this will only be 0 if we're part of the above loop
// (which we have an if case to avoid)
num * denom.invert().unwrap()
} }
/// Keys and verification shares generated by a DKG. /// Keys and verification shares generated by a DKG.
@@ -232,6 +242,8 @@ mod lib {
pub struct ThresholdCore<C: Ciphersuite> { pub struct ThresholdCore<C: Ciphersuite> {
/// Threshold Parameters. /// Threshold Parameters.
pub(crate) params: ThresholdParams, pub(crate) params: ThresholdParams,
/// The interpolation method used.
pub(crate) interpolation: Interpolation<C::F>,
/// Secret share key. /// Secret share key.
pub(crate) secret_share: Zeroizing<C::F>, pub(crate) secret_share: Zeroizing<C::F>,
@@ -246,6 +258,7 @@ mod lib {
fmt fmt
.debug_struct("ThresholdCore") .debug_struct("ThresholdCore")
.field("params", &self.params) .field("params", &self.params)
.field("interpolation", &self.interpolation)
.field("group_key", &self.group_key) .field("group_key", &self.group_key)
.field("verification_shares", &self.verification_shares) .field("verification_shares", &self.verification_shares)
.finish_non_exhaustive() .finish_non_exhaustive()
@@ -255,6 +268,7 @@ mod lib {
impl<C: Ciphersuite> Zeroize for ThresholdCore<C> { impl<C: Ciphersuite> Zeroize for ThresholdCore<C> {
fn zeroize(&mut self) { fn zeroize(&mut self) {
self.params.zeroize(); self.params.zeroize();
self.interpolation.zeroize();
self.secret_share.zeroize(); self.secret_share.zeroize();
self.group_key.zeroize(); self.group_key.zeroize();
for share in self.verification_shares.values_mut() { for share in self.verification_shares.values_mut() {
@@ -266,16 +280,14 @@ mod lib {
impl<C: Ciphersuite> ThresholdCore<C> { impl<C: Ciphersuite> ThresholdCore<C> {
pub(crate) fn new( pub(crate) fn new(
params: ThresholdParams, params: ThresholdParams,
interpolation: Interpolation<C::F>,
secret_share: Zeroizing<C::F>, secret_share: Zeroizing<C::F>,
verification_shares: HashMap<Participant, C::G>, verification_shares: HashMap<Participant, C::G>,
) -> ThresholdCore<C> { ) -> ThresholdCore<C> {
let t = (1 ..= params.t()).map(Participant).collect::<Vec<_>>(); let t = (1 ..= params.t()).map(Participant).collect::<Vec<_>>();
ThresholdCore { let group_key =
params, t.iter().map(|i| verification_shares[i] * interpolation.interpolation_factor(*i, &t)).sum();
secret_share, ThresholdCore { params, interpolation, secret_share, group_key, verification_shares }
group_key: t.iter().map(|i| verification_shares[i] * lagrange::<C::F>(*i, &t)).sum(),
verification_shares,
}
} }
/// Parameters for these keys. /// Parameters for these keys.
@@ -304,6 +316,15 @@ mod lib {
writer.write_all(&self.params.t.to_le_bytes())?; writer.write_all(&self.params.t.to_le_bytes())?;
writer.write_all(&self.params.n.to_le_bytes())?; writer.write_all(&self.params.n.to_le_bytes())?;
writer.write_all(&self.params.i.to_bytes())?; writer.write_all(&self.params.i.to_bytes())?;
match &self.interpolation {
Interpolation::Constant(c) => {
writer.write_all(&[0])?;
for c in c {
writer.write_all(c.to_repr().as_ref())?;
}
}
Interpolation::Lagrange => writer.write_all(&[1])?,
};
let mut share_bytes = self.secret_share.to_repr(); let mut share_bytes = self.secret_share.to_repr();
writer.write_all(share_bytes.as_ref())?; writer.write_all(share_bytes.as_ref())?;
share_bytes.as_mut().zeroize(); share_bytes.as_mut().zeroize();
@@ -352,6 +373,20 @@ mod lib {
) )
}; };
let mut interpolation = [0];
reader.read_exact(&mut interpolation)?;
let interpolation = match interpolation[0] {
0 => Interpolation::Constant({
let mut res = Vec::with_capacity(usize::from(n));
for _ in 0 .. n {
res.push(C::read_F(reader)?);
}
res
}),
1 => Interpolation::Lagrange,
_ => Err(io::Error::other("invalid interpolation method"))?,
};
let secret_share = Zeroizing::new(C::read_F(reader)?); let secret_share = Zeroizing::new(C::read_F(reader)?);
let mut verification_shares = HashMap::new(); let mut verification_shares = HashMap::new();
@@ -361,6 +396,7 @@ mod lib {
Ok(ThresholdCore::new( Ok(ThresholdCore::new(
ThresholdParams::new(t, n, i).map_err(|_| io::Error::other("invalid parameters"))?, ThresholdParams::new(t, n, i).map_err(|_| io::Error::other("invalid parameters"))?,
interpolation,
secret_share, secret_share,
verification_shares, verification_shares,
)) ))
@@ -383,6 +419,7 @@ mod lib {
/// View of keys, interpolated and offset for usage. /// View of keys, interpolated and offset for usage.
#[derive(Clone)] #[derive(Clone)]
pub struct ThresholdView<C: Ciphersuite> { pub struct ThresholdView<C: Ciphersuite> {
interpolation: Interpolation<C::F>,
offset: C::F, offset: C::F,
group_key: C::G, group_key: C::G,
included: Vec<Participant>, included: Vec<Participant>,
@@ -395,6 +432,7 @@ mod lib {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt fmt
.debug_struct("ThresholdView") .debug_struct("ThresholdView")
.field("interpolation", &self.interpolation)
.field("offset", &self.offset) .field("offset", &self.offset)
.field("group_key", &self.group_key) .field("group_key", &self.group_key)
.field("included", &self.included) .field("included", &self.included)
@@ -480,12 +518,13 @@ mod lib {
included.sort(); included.sort();
let mut secret_share = Zeroizing::new( let mut secret_share = Zeroizing::new(
lagrange::<C::F>(self.params().i(), &included) * self.secret_share().deref(), self.core.interpolation.interpolation_factor(self.params().i(), &included) *
self.secret_share().deref(),
); );
let mut verification_shares = self.verification_shares(); let mut verification_shares = self.verification_shares();
for (i, share) in &mut verification_shares { for (i, share) in &mut verification_shares {
*share *= lagrange::<C::F>(*i, &included); *share *= self.core.interpolation.interpolation_factor(*i, &included);
} }
// The offset is included by adding it to the participant with the lowest ID // The offset is included by adding it to the participant with the lowest ID
@@ -496,6 +535,7 @@ mod lib {
*verification_shares.get_mut(&included[0]).unwrap() += C::generator() * offset; *verification_shares.get_mut(&included[0]).unwrap() += C::generator() * offset;
Ok(ThresholdView { Ok(ThresholdView {
interpolation: self.core.interpolation.clone(),
offset, offset,
group_key: self.group_key(), group_key: self.group_key(),
secret_share, secret_share,
@@ -528,6 +568,14 @@ mod lib {
&self.included &self.included
} }
/// Return the interpolation factor for a signer.
pub fn interpolation_factor(&self, participant: Participant) -> Option<C::F> {
if !self.included.contains(&participant) {
None?
}
Some(self.interpolation.interpolation_factor(participant, &self.included))
}
/// Return the interpolated, offset secret share. /// Return the interpolated, offset secret share.
pub fn secret_share(&self) -> &Zeroizing<C::F> { pub fn secret_share(&self) -> &Zeroizing<C::F> {
&self.secret_share &self.secret_share

View File

@@ -7,8 +7,6 @@ use std_shims::collections::HashMap;
#[cfg(feature = "std")] #[cfg(feature = "std")]
use zeroize::Zeroizing; use zeroize::Zeroizing;
#[cfg(feature = "std")]
use ciphersuite::group::ff::Field;
use ciphersuite::{ use ciphersuite::{
group::{Group, GroupEncoding}, group::{Group, GroupEncoding},
Ciphersuite, Ciphersuite,
@@ -16,7 +14,7 @@ use ciphersuite::{
use crate::DkgError; use crate::DkgError;
#[cfg(feature = "std")] #[cfg(feature = "std")]
use crate::{Participant, ThresholdParams, ThresholdCore, lagrange}; use crate::{Participant, ThresholdParams, Interpolation, ThresholdCore};
fn check_keys<C: Ciphersuite>(keys: &[C::G]) -> Result<u16, DkgError<()>> { fn check_keys<C: Ciphersuite>(keys: &[C::G]) -> Result<u16, DkgError<()>> {
if keys.is_empty() { if keys.is_empty() {
@@ -67,6 +65,7 @@ pub fn musig_key<C: Ciphersuite>(context: &[u8], keys: &[C::G]) -> Result<C::G,
let transcript = binding_factor_transcript::<C>(context, keys)?; let transcript = binding_factor_transcript::<C>(context, keys)?;
let mut res = C::G::identity(); let mut res = C::G::identity();
for i in 1 ..= keys_len { for i in 1 ..= keys_len {
// TODO: Calculate this with a multiexp
res += keys[usize::from(i - 1)] * binding_factor::<C>(transcript.clone(), i); res += keys[usize::from(i - 1)] * binding_factor::<C>(transcript.clone(), i);
} }
Ok(res) Ok(res)
@@ -104,38 +103,26 @@ pub fn musig<C: Ciphersuite>(
binding.push(binding_factor::<C>(transcript.clone(), i)); binding.push(binding_factor::<C>(transcript.clone(), i));
} }
// Multiply our private key by our binding factor // Our secret share is our private key
let mut secret_share = private_key.clone(); let secret_share = private_key.clone();
*secret_share *= binding[pos];
// Calculate verification shares // Calculate verification shares
let mut verification_shares = HashMap::new(); let mut verification_shares = HashMap::new();
// When this library offers a ThresholdView for a specific signing set, it applies the lagrange
// factor
// Since this is a n-of-n scheme, there's only one possible signing set, and one possible
// lagrange factor
// In the name of simplicity, we define the group key as the sum of all bound keys
// Accordingly, the secret share must be multiplied by the inverse of the lagrange factor, along
// with all verification shares
// This is less performant than simply defining the group key as the sum of all post-lagrange
// bound keys, yet the simplicity is preferred
let included = (1 ..= keys_len)
// This error also shouldn't be possible, for the same reasons as documented above
.map(|l| Participant::new(l).ok_or(DkgError::InvalidSigningSet))
.collect::<Result<Vec<_>, _>>()?;
let mut group_key = C::G::identity(); let mut group_key = C::G::identity();
for (l, p) in included.iter().enumerate() { for l in 1 ..= keys_len {
let bound = keys[l] * binding[l]; let key = keys[usize::from(l) - 1];
group_key += bound; group_key += key * binding[usize::from(l - 1)];
let lagrange_inv = lagrange::<C::F>(*p, &included).invert().unwrap(); // These errors also shouldn't be possible, for the same reasons as documented above
if params.i() == *p { verification_shares.insert(Participant::new(l).ok_or(DkgError::InvalidSigningSet)?, key);
*secret_share *= lagrange_inv;
}
verification_shares.insert(*p, bound * lagrange_inv);
} }
debug_assert_eq!(C::generator() * secret_share.deref(), verification_shares[&params.i()]); debug_assert_eq!(C::generator() * secret_share.deref(), verification_shares[&params.i()]);
debug_assert_eq!(musig_key::<C>(context, keys).unwrap(), group_key); debug_assert_eq!(musig_key::<C>(context, keys).unwrap(), group_key);
Ok(ThresholdCore { params, secret_share, group_key, verification_shares }) Ok(ThresholdCore::new(
params,
Interpolation::Constant(binding),
secret_share,
verification_shares,
))
} }

View File

@@ -22,9 +22,9 @@ use multiexp::{multiexp_vartime, BatchVerifier};
use schnorr::SchnorrSignature; use schnorr::SchnorrSignature;
use crate::{ use crate::{
Participant, DkgError, ThresholdParams, ThresholdCore, validate_map, Participant, DkgError, ThresholdParams, Interpolation, ThresholdCore, validate_map,
encryption::{ encryption::{
ReadWrite, EncryptionKeyMessage, EncryptedMessage, Encryption, EncryptionKeyProof, ReadWrite, EncryptionKeyMessage, EncryptedMessage, Encryption, Decryption, EncryptionKeyProof,
DecryptionError, DecryptionError,
}, },
}; };
@@ -32,10 +32,10 @@ use crate::{
type FrostError<C> = DkgError<EncryptionKeyProof<C>>; type FrostError<C> = DkgError<EncryptionKeyProof<C>>;
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn challenge<C: Ciphersuite>(context: &str, l: Participant, R: &[u8], Am: &[u8]) -> C::F { fn challenge<C: Ciphersuite>(context: [u8; 32], l: Participant, R: &[u8], Am: &[u8]) -> C::F {
let mut transcript = RecommendedTranscript::new(b"DKG FROST v0.2"); let mut transcript = RecommendedTranscript::new(b"DKG FROST v0.2");
transcript.domain_separate(b"schnorr_proof_of_knowledge"); transcript.domain_separate(b"schnorr_proof_of_knowledge");
transcript.append_message(b"context", context.as_bytes()); transcript.append_message(b"context", context);
transcript.append_message(b"participant", l.to_bytes()); transcript.append_message(b"participant", l.to_bytes());
transcript.append_message(b"nonce", R); transcript.append_message(b"nonce", R);
transcript.append_message(b"commitments", Am); transcript.append_message(b"commitments", Am);
@@ -86,15 +86,15 @@ impl<C: Ciphersuite> ReadWrite for Commitments<C> {
#[derive(Debug, Zeroize)] #[derive(Debug, Zeroize)]
pub struct KeyGenMachine<C: Ciphersuite> { pub struct KeyGenMachine<C: Ciphersuite> {
params: ThresholdParams, params: ThresholdParams,
context: String, context: [u8; 32],
_curve: PhantomData<C>, _curve: PhantomData<C>,
} }
impl<C: Ciphersuite> KeyGenMachine<C> { impl<C: Ciphersuite> KeyGenMachine<C> {
/// Create a new machine to generate a key. /// Create a new machine to generate a key.
/// ///
/// The context string should be unique among multisigs. /// The context should be unique among multisigs.
pub fn new(params: ThresholdParams, context: String) -> KeyGenMachine<C> { pub fn new(params: ThresholdParams, context: [u8; 32]) -> KeyGenMachine<C> {
KeyGenMachine { params, context, _curve: PhantomData } KeyGenMachine { params, context, _curve: PhantomData }
} }
@@ -129,11 +129,11 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
// There's no reason to spend the time and effort to make this deterministic besides a // There's no reason to spend the time and effort to make this deterministic besides a
// general obsession with canonicity and determinism though // general obsession with canonicity and determinism though
r, r,
challenge::<C>(&self.context, self.params.i(), nonce.to_bytes().as_ref(), &cached_msg), challenge::<C>(self.context, self.params.i(), nonce.to_bytes().as_ref(), &cached_msg),
); );
// Additionally create an encryption mechanism to protect the secret shares // Additionally create an encryption mechanism to protect the secret shares
let encryption = Encryption::new(self.context.clone(), Some(self.params.i), rng); let encryption = Encryption::new(self.context, self.params.i, rng);
// Step 4: Broadcast // Step 4: Broadcast
let msg = let msg =
@@ -225,7 +225,7 @@ impl<F: PrimeField> ReadWrite for SecretShare<F> {
#[derive(Zeroize)] #[derive(Zeroize)]
pub struct SecretShareMachine<C: Ciphersuite> { pub struct SecretShareMachine<C: Ciphersuite> {
params: ThresholdParams, params: ThresholdParams,
context: String, context: [u8; 32],
coefficients: Vec<Zeroizing<C::F>>, coefficients: Vec<Zeroizing<C::F>>,
our_commitments: Vec<C::G>, our_commitments: Vec<C::G>,
encryption: Encryption<C>, encryption: Encryption<C>,
@@ -274,7 +274,7 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
&mut batch, &mut batch,
l, l,
msg.commitments[0], msg.commitments[0],
challenge::<C>(&self.context, l, msg.sig.R.to_bytes().as_ref(), &msg.cached_msg), challenge::<C>(self.context, l, msg.sig.R.to_bytes().as_ref(), &msg.cached_msg),
); );
commitments.insert(l, msg.commitments.drain(..).collect::<Vec<_>>()); commitments.insert(l, msg.commitments.drain(..).collect::<Vec<_>>());
@@ -472,9 +472,10 @@ impl<C: Ciphersuite> KeyMachine<C> {
let KeyMachine { commitments, encryption, params, secret } = self; let KeyMachine { commitments, encryption, params, secret } = self;
Ok(BlameMachine { Ok(BlameMachine {
commitments, commitments,
encryption, encryption: encryption.into_decryption(),
result: Some(ThresholdCore { result: Some(ThresholdCore {
params, params,
interpolation: Interpolation::Lagrange,
secret_share: secret, secret_share: secret,
group_key: stripes[0], group_key: stripes[0],
verification_shares, verification_shares,
@@ -486,7 +487,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
/// A machine capable of handling blame proofs. /// A machine capable of handling blame proofs.
pub struct BlameMachine<C: Ciphersuite> { pub struct BlameMachine<C: Ciphersuite> {
commitments: HashMap<Participant, Vec<C::G>>, commitments: HashMap<Participant, Vec<C::G>>,
encryption: Encryption<C>, encryption: Decryption<C>,
result: Option<ThresholdCore<C>>, result: Option<ThresholdCore<C>>,
} }
@@ -505,7 +506,6 @@ impl<C: Ciphersuite> Zeroize for BlameMachine<C> {
for commitments in self.commitments.values_mut() { for commitments in self.commitments.values_mut() {
commitments.zeroize(); commitments.zeroize();
} }
self.encryption.zeroize();
self.result.zeroize(); self.result.zeroize();
} }
} }
@@ -598,14 +598,13 @@ impl<C: Ciphersuite> AdditionalBlameMachine<C> {
/// authenticated as having come from the supposed party and verified as valid. Usage of invalid /// authenticated as having come from the supposed party and verified as valid. Usage of invalid
/// commitments is considered undefined behavior, and may cause everything from inaccurate blame /// commitments is considered undefined behavior, and may cause everything from inaccurate blame
/// to panics. /// to panics.
pub fn new<R: RngCore + CryptoRng>( pub fn new(
rng: &mut R, context: [u8; 32],
context: String,
n: u16, n: u16,
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>, mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
) -> Result<Self, FrostError<C>> { ) -> Result<Self, FrostError<C>> {
let mut commitments = HashMap::new(); let mut commitments = HashMap::new();
let mut encryption = Encryption::new(context, None, rng); let mut encryption = Decryption::new(context);
for i in 1 ..= n { for i in 1 ..= n {
let i = Participant::new(i).unwrap(); let i = Participant::new(i).unwrap();
let Some(msg) = commitment_msgs.remove(&i) else { Err(DkgError::MissingParticipant(i))? }; let Some(msg) = commitment_msgs.remove(&i) else { Err(DkgError::MissingParticipant(i))? };

View File

@@ -113,6 +113,7 @@ impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<
Ok(ThresholdKeys { Ok(ThresholdKeys {
core: Arc::new(ThresholdCore::new( core: Arc::new(ThresholdCore::new(
params, params,
self.base.core.interpolation.clone(),
self.base.secret_share().clone(), self.base.secret_share().clone(),
verification_shares, verification_shares,
)), )),

View File

@@ -6,7 +6,7 @@ use rand_core::{RngCore, CryptoRng};
use ciphersuite::{group::ff::Field, Ciphersuite}; use ciphersuite::{group::ff::Field, Ciphersuite};
use crate::{Participant, ThresholdCore, ThresholdKeys, lagrange, musig::musig as musig_fn}; use crate::{Participant, ThresholdCore, ThresholdKeys, musig::musig as musig_fn};
mod musig; mod musig;
pub use musig::test_musig; pub use musig::test_musig;
@@ -43,7 +43,8 @@ pub fn recover_key<C: Ciphersuite>(keys: &HashMap<Participant, ThresholdKeys<C>>
let included = keys.keys().copied().collect::<Vec<_>>(); let included = keys.keys().copied().collect::<Vec<_>>();
let group_private = keys.iter().fold(C::F::ZERO, |accum, (i, keys)| { let group_private = keys.iter().fold(C::F::ZERO, |accum, (i, keys)| {
accum + (lagrange::<C::F>(*i, &included) * keys.secret_share().deref()) accum +
(first.core.interpolation.interpolation_factor(*i, &included) * keys.secret_share().deref())
}); });
assert_eq!(C::generator() * group_private, first.group_key(), "failed to recover keys"); assert_eq!(C::generator() * group_private, first.group_key(), "failed to recover keys");
group_private group_private

View File

@@ -14,7 +14,7 @@ use crate::{
type PedPoPEncryptedMessage<C> = EncryptedMessage<C, SecretShare<<C as Ciphersuite>::F>>; type PedPoPEncryptedMessage<C> = EncryptedMessage<C, SecretShare<<C as Ciphersuite>::F>>;
type PedPoPSecretShares<C> = HashMap<Participant, PedPoPEncryptedMessage<C>>; type PedPoPSecretShares<C> = HashMap<Participant, PedPoPEncryptedMessage<C>>;
const CONTEXT: &str = "DKG Test Key Generation"; const CONTEXT: [u8; 32] = *b"DKG Test Key Generation ";
// Commit, then return commitment messages, enc keys, and shares // Commit, then return commitment messages, enc keys, and shares
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
@@ -31,7 +31,7 @@ fn commit_enc_keys_and_shares<R: RngCore + CryptoRng, C: Ciphersuite>(
let mut enc_keys = HashMap::new(); let mut enc_keys = HashMap::new();
for i in (1 ..= PARTICIPANTS).map(Participant) { for i in (1 ..= PARTICIPANTS).map(Participant) {
let params = ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap(); let params = ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap();
let machine = KeyGenMachine::<C>::new(params, CONTEXT.to_string()); let machine = KeyGenMachine::<C>::new(params, CONTEXT);
let (machine, these_commitments) = machine.generate_coefficients(rng); let (machine, these_commitments) = machine.generate_coefficients(rng);
machines.insert(i, machine); machines.insert(i, machine);
@@ -147,14 +147,12 @@ mod literal {
// Verify machines constructed with AdditionalBlameMachine::new work // Verify machines constructed with AdditionalBlameMachine::new work
assert_eq!( assert_eq!(
AdditionalBlameMachine::new( AdditionalBlameMachine::new(CONTEXT, PARTICIPANTS, commitment_msgs.clone()).unwrap().blame(
&mut OsRng, ONE,
CONTEXT.to_string(), TWO,
PARTICIPANTS, msg.clone(),
commitment_msgs.clone() blame.clone()
) ),
.unwrap()
.blame(ONE, TWO, msg.clone(), blame.clone()),
ONE, ONE,
); );
} }

View File

@@ -28,6 +28,10 @@ impl<C: Ciphersuite> Ciphersuite for AltGenerator<C> {
C::G::generator() * <C as Ciphersuite>::hash_to_F(b"DKG Promotion Test", b"generator") C::G::generator() * <C as Ciphersuite>::hash_to_F(b"DKG Promotion Test", b"generator")
} }
fn reduce_512(scalar: [u8; 64]) -> Self::F {
<C as Ciphersuite>::reduce_512(scalar)
}
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F { fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
<C as Ciphersuite>::hash_to_F(dst, data) <C as Ciphersuite>::hash_to_F(dst, data)
} }

View File

@@ -6,7 +6,7 @@ license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dleq" repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dleq"
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021" edition = "2021"
rust-version = "1.79" rust-version = "1.81"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
@@ -18,7 +18,7 @@ workspace = true
[dependencies] [dependencies]
rustversion = "1" rustversion = "1"
thiserror = { version = "1", optional = true } thiserror = { version = "2", default-features = false, optional = true }
rand_core = { version = "0.6", default-features = false } rand_core = { version = "0.6", default-features = false }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
@@ -44,7 +44,7 @@ dalek-ff-group = { path = "../dalek-ff-group" }
transcript = { package = "flexible-transcript", path = "../transcript", features = ["recommended"] } transcript = { package = "flexible-transcript", path = "../transcript", features = ["recommended"] }
[features] [features]
std = ["rand_core/std", "zeroize/std", "digest/std", "transcript/std", "ff/std", "multiexp?/std"] std = ["thiserror?/std", "rand_core/std", "zeroize/std", "digest/std", "transcript/std", "ff/std", "multiexp?/std"]
serialize = ["std"] serialize = ["std"]
# Needed for cross-group DLEqs # Needed for cross-group DLEqs

View File

@@ -92,7 +92,7 @@ impl<G: PrimeGroup> Generators<G> {
} }
/// Error for cross-group DLEq proofs. /// Error for cross-group DLEq proofs.
#[derive(Error, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
pub enum DLEqError { pub enum DLEqError {
/// Invalid proof length. /// Invalid proof length.
#[error("invalid proof length")] #[error("invalid proof length")]

View File

@@ -37,11 +37,11 @@ pub(crate) fn challenge<T: Transcript, F: PrimeField>(transcript: &mut T) -> F {
// Get a wide amount of bytes to safely reduce without bias // Get a wide amount of bytes to safely reduce without bias
// In most cases, <=1.5x bytes is enough. 2x is still standard and there's some theoretical // In most cases, <=1.5x bytes is enough. 2x is still standard and there's some theoretical
// groups which may technically require more than 1.5x bytes for this to work as intended // groups which may technically require more than 1.5x bytes for this to work as intended
let target_bytes = ((usize::try_from(F::NUM_BITS).unwrap() + 7) / 8) * 2; let target_bytes = usize::try_from(F::NUM_BITS).unwrap().div_ceil(8) * 2;
let mut challenge_bytes = transcript.challenge(b"challenge"); let mut challenge_bytes = transcript.challenge(b"challenge");
let challenge_bytes_len = challenge_bytes.as_ref().len(); let challenge_bytes_len = challenge_bytes.as_ref().len();
// If the challenge is 32 bytes, and we need 64, we need two challenges // If the challenge is 32 bytes, and we need 64, we need two challenges
let needed_challenges = (target_bytes + (challenge_bytes_len - 1)) / challenge_bytes_len; let needed_challenges = target_bytes.div_ceil(challenge_bytes_len);
// The following algorithm should be equivalent to a wide reduction of the challenges, // The following algorithm should be equivalent to a wide reduction of the challenges,
// interpreted as concatenated, big-endian byte string // interpreted as concatenated, big-endian byte string

View File

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

View File

@@ -154,18 +154,20 @@ pub fn test_group<R: RngCore, G: Group>(rng: &mut R) {
/// Test encoding and decoding of group elements. /// Test encoding and decoding of group elements.
pub fn test_encoding<G: PrimeGroup>() { pub fn test_encoding<G: PrimeGroup>() {
let test = |point: G, msg| { let test = |point: G, msg| -> G {
let bytes = point.to_bytes(); let bytes = point.to_bytes();
let mut repr = G::Repr::default(); let mut repr = G::Repr::default();
repr.as_mut().copy_from_slice(bytes.as_ref()); repr.as_mut().copy_from_slice(bytes.as_ref());
assert_eq!(point, G::from_bytes(&repr).unwrap(), "{msg} couldn't be encoded and decoded"); let decoded = G::from_bytes(&repr).unwrap();
assert_eq!(point, decoded, "{msg} couldn't be encoded and decoded");
assert_eq!( assert_eq!(
point, point,
G::from_bytes_unchecked(&repr).unwrap(), G::from_bytes_unchecked(&repr).unwrap(),
"{msg} couldn't be encoded and decoded", "{msg} couldn't be encoded and decoded",
); );
decoded
}; };
test(G::identity(), "identity"); assert!(bool::from(test(G::identity(), "identity").is_identity()));
test(G::generator(), "generator"); test(G::generator(), "generator");
test(G::generator() + G::generator(), "(generator * 2)"); test(G::generator() + G::generator(), "(generator * 2)");
} }

View File

@@ -17,7 +17,7 @@ rustdoc-args = ["--cfg", "docsrs"]
workspace = true workspace = true
[dependencies] [dependencies]
thiserror = "1" thiserror = { version = "2", default-features = false, features = ["std"] }
rand_core = { version = "0.6", default-features = false, features = ["std"] } rand_core = { version = "0.6", default-features = false, features = ["std"] }
rand_chacha = { version = "0.3", default-features = false, features = ["std"] } rand_chacha = { version = "0.3", default-features = false, features = ["std"] }

View File

@@ -203,14 +203,15 @@ pub trait SignMachine<S>: Send + Sync + Sized {
/// SignatureMachine this SignMachine turns into. /// SignatureMachine this SignMachine turns into.
type SignatureMachine: SignatureMachine<S, SignatureShare = Self::SignatureShare>; type SignatureMachine: SignatureMachine<S, SignatureShare = Self::SignatureShare>;
/// Cache this preprocess for usage later. This cached preprocess MUST only be used once. Reuse /// Cache this preprocess for usage later.
/// of it enables recovery of your private key share. Third-party recovery of a cached preprocess ///
/// also enables recovery of your private key share, so this MUST be treated with the same /// This cached preprocess MUST only be used once. Reuse of it enables recovery of your private
/// security as your private key share. /// key share. Third-party recovery of a cached preprocess also enables recovery of your private
/// key share, so this MUST be treated with the same security as your private key share.
fn cache(self) -> CachedPreprocess; fn cache(self) -> CachedPreprocess;
/// Create a sign machine from a cached preprocess. /// Create a sign machine from a cached preprocess.
///
/// After this, the preprocess must be deleted so it's never reused. Any reuse will presumably /// After this, the preprocess must be deleted so it's never reused. Any reuse will presumably
/// cause the signer to leak their secret share. /// cause the signer to leak their secret share.
fn from_cache( fn from_cache(
@@ -219,11 +220,14 @@ pub trait SignMachine<S>: Send + Sync + Sized {
cache: CachedPreprocess, cache: CachedPreprocess,
) -> (Self, Self::Preprocess); ) -> (Self, Self::Preprocess);
/// Read a Preprocess message. Despite taking self, this does not save the preprocess. /// Read a Preprocess message.
/// It must be externally cached and passed into sign. ///
/// Despite taking self, this does not save the preprocess. It must be externally cached and
/// passed into sign.
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess>; fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess>;
/// Sign a message. /// Sign a message.
///
/// Takes in the participants' preprocess messages. Returns the signature share to be broadcast /// Takes in the participants' preprocess messages. Returns the signature share to be broadcast
/// to all participants, over an authenticated channel. The parties who participate here will /// to all participants, over an authenticated channel. The parties who participate here will
/// become the signing set for this session. /// become the signing set for this session.

View File

@@ -122,6 +122,7 @@ fn vectors_to_multisig_keys<C: Curve>(vectors: &Vectors) -> HashMap<Participant,
serialized.extend(vectors.threshold.to_le_bytes()); serialized.extend(vectors.threshold.to_le_bytes());
serialized.extend(u16::try_from(shares.len()).unwrap().to_le_bytes()); serialized.extend(u16::try_from(shares.len()).unwrap().to_le_bytes());
serialized.extend(i.to_le_bytes()); serialized.extend(i.to_le_bytes());
serialized.push(1);
serialized.extend(shares[usize::from(i) - 1].to_repr().as_ref()); serialized.extend(shares[usize::from(i) - 1].to_repr().as_ref());
for share in &verification_shares { for share in &verification_shares {
serialized.extend(share.to_bytes().as_ref()); serialized.extend(share.to_bytes().as_ref());

View File

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

View File

@@ -59,7 +59,7 @@ pub(crate) fn prep_bits<G: Group<Scalar: PrimeFieldBits>>(
for pair in pairs { for pair in pairs {
let p = groupings.len(); let p = groupings.len();
let mut bits = pair.0.to_le_bits(); let mut bits = pair.0.to_le_bits();
groupings.push(vec![0; (bits.len() + (w_usize - 1)) / w_usize]); groupings.push(vec![0; bits.len().div_ceil(w_usize)]);
for (i, mut bit) in bits.iter_mut().enumerate() { for (i, mut bit) in bits.iter_mut().enumerate() {
let mut bit = u8_from_bool(&mut bit); let mut bit = u8_from_bool(&mut bit);

View File

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

View File

@@ -31,9 +31,8 @@ fn weight<D: Send + Clone + SecureDigest, F: PrimeField>(digest: &mut DigestTran
// Derive a scalar from enough bits of entropy that bias is < 2^128 // Derive a scalar from enough bits of entropy that bias is < 2^128
// This can't be const due to its usage of a generic // This can't be const due to its usage of a generic
// Also due to the usize::try_from, yet that could be replaced with an `as` // Also due to the usize::try_from, yet that could be replaced with an `as`
// The + 7 forces it to round up
#[allow(non_snake_case)] #[allow(non_snake_case)]
let BYTES: usize = usize::try_from(((F::NUM_BITS + 128) + 7) / 8).unwrap(); let BYTES: usize = usize::try_from((F::NUM_BITS + 128).div_ceil(8)).unwrap();
let mut remaining = BYTES; let mut remaining = BYTES;

View File

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

View File

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