mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-20 17:54:38 +00:00
3.8.6 Correct transcript to scalar derivation
Replaces the externally passed in Digest with C::H since C is available.
This commit is contained in:
parent
530671795a
commit
97374a3e24
6 changed files with 54 additions and 36 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -912,6 +912,7 @@ dependencies = [
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"ff",
|
"ff",
|
||||||
"ff-group-tests",
|
"ff-group-tests",
|
||||||
|
"flexible-transcript",
|
||||||
"group",
|
"group",
|
||||||
"hex",
|
"hex",
|
||||||
"k256",
|
"k256",
|
||||||
|
@ -7493,13 +7494,14 @@ dependencies = [
|
||||||
name = "schnorr-signatures"
|
name = "schnorr-signatures"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"blake2",
|
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
"digest 0.10.6",
|
"flexible-transcript",
|
||||||
"group",
|
"group",
|
||||||
|
"hex",
|
||||||
"multiexp",
|
"multiexp",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
|
"sha2 0.10.6",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ zeroize = { version = "1.5", features = ["zeroize_derive"] }
|
||||||
subtle = "2"
|
subtle = "2"
|
||||||
|
|
||||||
digest = "0.10"
|
digest = "0.10"
|
||||||
|
transcript = { package = "flexible-transcript", path = "../transcript", version = "0.2" }
|
||||||
sha2 = { version = "0.10", optional = true }
|
sha2 = { version = "0.10", optional = true }
|
||||||
sha3 = { version = "0.10", optional = true }
|
sha3 = { version = "0.10", optional = true }
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,8 @@ use rand_core::{RngCore, CryptoRng};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
|
|
||||||
use digest::{core_api::BlockSizeUser, Digest};
|
use digest::{core_api::BlockSizeUser, Digest, HashMarker};
|
||||||
|
use transcript::SecureDigest;
|
||||||
|
|
||||||
use group::{
|
use group::{
|
||||||
ff::{Field, PrimeField, PrimeFieldBits},
|
ff::{Field, PrimeField, PrimeFieldBits},
|
||||||
|
@ -49,7 +50,7 @@ pub trait Ciphersuite: Clone + Copy + PartialEq + Eq + Debug + Zeroize {
|
||||||
type G: Group<Scalar = Self::F> + GroupOps + PrimeGroup + Zeroize + ConstantTimeEq;
|
type G: Group<Scalar = Self::F> + GroupOps + PrimeGroup + Zeroize + ConstantTimeEq;
|
||||||
/// Hash algorithm used with this curve.
|
/// Hash algorithm used with this curve.
|
||||||
// Requires BlockSizeUser so it can be used within Hkdf which requies that.
|
// Requires BlockSizeUser so it can be used within Hkdf which requies that.
|
||||||
type H: Clone + BlockSizeUser + Digest;
|
type H: Clone + BlockSizeUser + Digest + HashMarker + SecureDigest;
|
||||||
|
|
||||||
/// ID for this curve.
|
/// ID for this curve.
|
||||||
const ID: &'static [u8];
|
const ID: &'static [u8];
|
||||||
|
|
|
@ -17,7 +17,6 @@ rand_core = "0.6"
|
||||||
|
|
||||||
zeroize = { version = "1.5", features = ["zeroize_derive"] }
|
zeroize = { version = "1.5", features = ["zeroize_derive"] }
|
||||||
|
|
||||||
digest = "0.10"
|
|
||||||
transcript = { package = "flexible-transcript", path = "../transcript", version = "0.2" }
|
transcript = { package = "flexible-transcript", path = "../transcript", version = "0.2" }
|
||||||
|
|
||||||
group = "0.12"
|
group = "0.12"
|
||||||
|
|
|
@ -13,28 +13,49 @@ use ciphersuite::Ciphersuite;
|
||||||
|
|
||||||
use crate::SchnorrSignature;
|
use crate::SchnorrSignature;
|
||||||
|
|
||||||
// Performs a big-endian modular reduction of the hash value
|
// Returns a unbiased scalar weight to use on a signature in order to prevent malleability
|
||||||
// This is used by the below aggregator to prevent mutability
|
fn weight<D: Clone + SecureDigest, F: PrimeField>(digest: &mut DigestTranscript<D>) -> F {
|
||||||
// Only an 128-bit scalar is needed to offer 128-bits of security against malleability per
|
let mut bytes = digest.challenge(b"aggregation_weight");
|
||||||
// https://cr.yp.to/badbatch/badbatch-20120919.pdf
|
|
||||||
// Accordingly, while a 256-bit hash used here with a 256-bit ECC will have bias, it shouldn't be
|
|
||||||
// an issue
|
|
||||||
fn scalar_from_digest<D: Clone + SecureDigest, F: PrimeField>(
|
|
||||||
digest: &mut DigestTranscript<D>,
|
|
||||||
) -> F {
|
|
||||||
let bytes = digest.challenge(b"aggregation_weight");
|
|
||||||
debug_assert_eq!(bytes.len() % 8, 0);
|
debug_assert_eq!(bytes.len() % 8, 0);
|
||||||
|
// This should be guaranteed thanks to SecureDigest
|
||||||
|
debug_assert!(bytes.len() >= 32);
|
||||||
|
|
||||||
let mut res = F::zero();
|
let mut res = F::zero();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < bytes.len() {
|
|
||||||
if i != 0 {
|
// Derive a scalar from enough bits of entropy that bias is < 2^128
|
||||||
for _ in 0 .. 8 {
|
// 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`
|
||||||
|
// The + 7 forces it to round up
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let BYTES: usize = usize::try_from(((F::NUM_BITS + 128) + 7) / 8).unwrap();
|
||||||
|
|
||||||
|
let mut remaining = BYTES;
|
||||||
|
|
||||||
|
// We load bits in as u64s
|
||||||
|
const WORD_LEN_IN_BITS: usize = 64;
|
||||||
|
const WORD_LEN_IN_BYTES: usize = WORD_LEN_IN_BITS / 8;
|
||||||
|
|
||||||
|
let mut first = true;
|
||||||
|
while i < remaining {
|
||||||
|
// Shift over the already loaded bits
|
||||||
|
if !first {
|
||||||
|
for _ in 0 .. WORD_LEN_IN_BITS {
|
||||||
res += res;
|
res += res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res += F::from(u64::from_be_bytes(bytes[i .. (i + 8)].try_into().unwrap()));
|
first = false;
|
||||||
i += 8;
|
|
||||||
|
// Add the next 64 bits
|
||||||
|
res += F::from(u64::from_be_bytes(bytes[i .. (i + WORD_LEN_IN_BYTES)].try_into().unwrap()));
|
||||||
|
i += WORD_LEN_IN_BYTES;
|
||||||
|
|
||||||
|
// If we've exhausted this challenge, get another
|
||||||
|
if i == bytes.len() {
|
||||||
|
bytes = digest.challenge(b"aggregation_weight_continued");
|
||||||
|
remaining -= i;
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
@ -92,16 +113,12 @@ impl<C: Ciphersuite> SchnorrAggregate<C> {
|
||||||
/// The DST used here must prevent a collision with whatever hash function produced the
|
/// The DST used here must prevent a collision with whatever hash function produced the
|
||||||
/// challenges.
|
/// challenges.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn verify<D: Clone + SecureDigest>(
|
pub fn verify(&self, dst: &'static [u8], keys_and_challenges: &[(C::G, C::F)]) -> bool {
|
||||||
&self,
|
|
||||||
dst: &'static [u8],
|
|
||||||
keys_and_challenges: &[(C::G, C::F)],
|
|
||||||
) -> bool {
|
|
||||||
if self.Rs.len() != keys_and_challenges.len() {
|
if self.Rs.len() != keys_and_challenges.len() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut digest = DigestTranscript::<D>::new(dst);
|
let mut digest = DigestTranscript::<C::H>::new(dst);
|
||||||
digest.domain_separate(b"signatures");
|
digest.domain_separate(b"signatures");
|
||||||
for (_, challenge) in keys_and_challenges {
|
for (_, challenge) in keys_and_challenges {
|
||||||
digest.append_message(b"challenge", challenge.to_repr());
|
digest.append_message(b"challenge", challenge.to_repr());
|
||||||
|
@ -109,7 +126,7 @@ impl<C: Ciphersuite> SchnorrAggregate<C> {
|
||||||
|
|
||||||
let mut pairs = Vec::with_capacity((2 * keys_and_challenges.len()) + 1);
|
let mut pairs = Vec::with_capacity((2 * keys_and_challenges.len()) + 1);
|
||||||
for (i, (key, challenge)) in keys_and_challenges.iter().enumerate() {
|
for (i, (key, challenge)) in keys_and_challenges.iter().enumerate() {
|
||||||
let z = scalar_from_digest(&mut digest);
|
let z = weight(&mut digest);
|
||||||
pairs.push((z, self.Rs[i]));
|
pairs.push((z, self.Rs[i]));
|
||||||
pairs.push((z * challenge, *key));
|
pairs.push((z * challenge, *key));
|
||||||
}
|
}
|
||||||
|
@ -120,18 +137,18 @@ impl<C: Ciphersuite> SchnorrAggregate<C> {
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[derive(Clone, Debug, Zeroize)]
|
#[derive(Clone, Debug, Zeroize)]
|
||||||
pub struct SchnorrAggregator<D: Clone + SecureDigest, C: Ciphersuite> {
|
pub struct SchnorrAggregator<C: Ciphersuite> {
|
||||||
digest: DigestTranscript<D>,
|
digest: DigestTranscript<C::H>,
|
||||||
sigs: Vec<SchnorrSignature<C>>,
|
sigs: Vec<SchnorrSignature<C>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Clone + SecureDigest, C: Ciphersuite> SchnorrAggregator<D, C> {
|
impl<C: Ciphersuite> SchnorrAggregator<C> {
|
||||||
/// Create a new aggregator.
|
/// Create a new aggregator.
|
||||||
///
|
///
|
||||||
/// The DST used here must prevent a collision with whatever hash function produced the
|
/// The DST used here must prevent a collision with whatever hash function produced the
|
||||||
/// challenges.
|
/// challenges.
|
||||||
pub fn new(dst: &'static [u8]) -> Self {
|
pub fn new(dst: &'static [u8]) -> Self {
|
||||||
let mut res = Self { digest: DigestTranscript::<D>::new(dst), sigs: vec![] };
|
let mut res = Self { digest: DigestTranscript::<C::H>::new(dst), sigs: vec![] };
|
||||||
res.digest.domain_separate(b"signatures");
|
res.digest.domain_separate(b"signatures");
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
@ -152,7 +169,7 @@ impl<D: Clone + SecureDigest, C: Ciphersuite> SchnorrAggregator<D, C> {
|
||||||
SchnorrAggregate { Rs: Vec::with_capacity(self.sigs.len()), s: C::F::zero() };
|
SchnorrAggregate { Rs: Vec::with_capacity(self.sigs.len()), s: C::F::zero() };
|
||||||
for i in 0 .. self.sigs.len() {
|
for i in 0 .. self.sigs.len() {
|
||||||
aggregate.Rs.push(self.sigs[i].R);
|
aggregate.Rs.push(self.sigs[i].R);
|
||||||
aggregate.s += self.sigs[i].s * scalar_from_digest::<_, C::F>(&mut self.digest);
|
aggregate.s += self.sigs[i].s * weight::<_, C::F>(&mut self.digest);
|
||||||
}
|
}
|
||||||
Some(aggregate)
|
Some(aggregate)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ use core::ops::Deref;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
use rand_core::OsRng;
|
use rand_core::OsRng;
|
||||||
|
|
||||||
use sha2::Sha256;
|
|
||||||
|
|
||||||
use group::{ff::Field, Group};
|
use group::{ff::Field, Group};
|
||||||
use multiexp::BatchVerifier;
|
use multiexp::BatchVerifier;
|
||||||
|
|
||||||
|
@ -84,7 +82,7 @@ pub(crate) fn aggregate<C: Ciphersuite>() {
|
||||||
// Create 5 signatures
|
// Create 5 signatures
|
||||||
let mut keys = vec![];
|
let mut keys = vec![];
|
||||||
let mut challenges = vec![];
|
let mut challenges = vec![];
|
||||||
let mut aggregator = SchnorrAggregator::<Sha256, C>::new(DST);
|
let mut aggregator = SchnorrAggregator::<C>::new(DST);
|
||||||
for i in 0 .. 5 {
|
for i in 0 .. 5 {
|
||||||
keys.push(Zeroizing::new(C::random_nonzero_F(&mut OsRng)));
|
keys.push(Zeroizing::new(C::random_nonzero_F(&mut OsRng)));
|
||||||
// In practice, this MUST be a secure challenge binding to the nonce, key, and any message
|
// In practice, this MUST be a secure challenge binding to the nonce, key, and any message
|
||||||
|
@ -102,7 +100,7 @@ pub(crate) fn aggregate<C: Ciphersuite>() {
|
||||||
let aggregate = aggregator.complete().unwrap();
|
let aggregate = aggregator.complete().unwrap();
|
||||||
let aggregate =
|
let aggregate =
|
||||||
SchnorrAggregate::<C>::read::<&[u8]>(&mut aggregate.serialize().as_ref()).unwrap();
|
SchnorrAggregate::<C>::read::<&[u8]>(&mut aggregate.serialize().as_ref()).unwrap();
|
||||||
assert!(aggregate.verify::<Sha256>(
|
assert!(aggregate.verify(
|
||||||
DST,
|
DST,
|
||||||
keys
|
keys
|
||||||
.iter()
|
.iter()
|
||||||
|
|
Loading…
Reference in a new issue