3.8.5 Let the caller pass in a DST for the aggregation hash function

Also moves the aggregator over to Digest. While a bit verbose for this context,
as all appended items were fixed length, it's length prefixing is solid and
the API is pleasant. The downside is the additional dependency which is
in tree and quite compact.
This commit is contained in:
Luke Parker 2023-03-02 09:29:37 -05:00
parent 8b7e7b1a1c
commit 530671795a
No known key found for this signature in database
3 changed files with 42 additions and 43 deletions

View file

@ -18,11 +18,11 @@ rand_core = "0.6"
zeroize = { version = "1.5", features = ["zeroize_derive"] }
digest = "0.10"
transcript = { package = "flexible-transcript", path = "../transcript", version = "0.2" }
group = "0.12"
ciphersuite = { path = "../ciphersuite", version = "0.1" }
multiexp = { path = "../multiexp", version = "0.2", features = ["batch"] }
ciphersuite = { path = "../ciphersuite", version = "0.1" }
[dev-dependencies]
hex = "0.4"

View file

@ -2,32 +2,27 @@ use std::io::{self, Read, Write};
use zeroize::Zeroize;
use digest::Digest;
use transcript::{Transcript, SecureDigest, DigestTranscript};
use group::{
ff::{Field, PrimeField},
Group, GroupEncoding,
prime::PrimeGroup,
};
use multiexp::multiexp_vartime;
use ciphersuite::Ciphersuite;
use crate::SchnorrSignature;
fn digest<D: Digest>() -> D {
D::new_with_prefix(b"Schnorr Aggregate")
}
// Performs a big-endian modular reduction of the hash value
// This is used by the below aggregator to prevent mutability
// Only an 128-bit scalar is needed to offer 128-bits of security against malleability per
// 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: Digest, F: PrimeField>(digest: D) -> F {
let bytes = digest.finalize();
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);
let mut res = F::zero();
@ -44,12 +39,6 @@ fn scalar_from_digest<D: Digest, F: PrimeField>(digest: D) -> F {
res
}
fn digest_yield<D: Digest, F: PrimeField>(digest: D, i: usize) -> F {
scalar_from_digest(digest.chain_update(
u32::try_from(i).expect("more than 4 billion signatures in aggregate").to_le_bytes(),
))
}
/// Aggregate Schnorr signature as defined in <https://eprint.iacr.org/2021/350>.
#[allow(non_snake_case)]
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
@ -96,23 +85,31 @@ impl<C: Ciphersuite> SchnorrAggregate<C> {
/// Perform signature verification.
///
/// This challenge must be properly crafted, which means being binding to the public key, nonce,
/// and any message. Failure to do so will let a malicious adversary to forge signatures for
/// Challenges must be properly crafted, which means being binding to the public key, nonce, and
/// any message. Failure to do so will let a malicious adversary to forge signatures for
/// different keys/messages.
///
/// The DST used here must prevent a collision with whatever hash function produced the
/// challenges.
#[must_use]
pub fn verify<D: Clone + Digest>(&self, keys_and_challenges: &[(C::G, C::F)]) -> bool {
pub fn verify<D: Clone + SecureDigest>(
&self,
dst: &'static [u8],
keys_and_challenges: &[(C::G, C::F)],
) -> bool {
if self.Rs.len() != keys_and_challenges.len() {
return false;
}
let mut digest = digest::<D>();
for (key, challenge) in keys_and_challenges {
digest.update(challenge.to_repr().as_ref());
let mut digest = DigestTranscript::<D>::new(dst);
digest.domain_separate(b"signatures");
for (_, challenge) in keys_and_challenges {
digest.append_message(b"challenge", challenge.to_repr());
}
let mut pairs = Vec::with_capacity((2 * keys_and_challenges.len()) + 1);
for (i, (key, challenge)) in keys_and_challenges.iter().enumerate() {
let z = digest_yield(digest.clone(), i);
let z = scalar_from_digest(&mut digest);
pairs.push((z, self.Rs[i]));
pairs.push((z * challenge, *key));
}
@ -123,31 +120,30 @@ impl<C: Ciphersuite> SchnorrAggregate<C> {
#[allow(non_snake_case)]
#[derive(Clone, Debug, Zeroize)]
pub struct SchnorrAggregator<D: Clone + Digest, C: Ciphersuite> {
digest: D,
pub struct SchnorrAggregator<D: Clone + SecureDigest, C: Ciphersuite> {
digest: DigestTranscript<D>,
sigs: Vec<SchnorrSignature<C>>,
}
impl<D: Clone + Digest, C: Ciphersuite> Default for SchnorrAggregator<D, C> {
fn default() -> Self {
Self { digest: digest(), sigs: vec![] }
}
}
impl<D: Clone + Digest, C: Ciphersuite> SchnorrAggregator<D, C> {
impl<D: Clone + SecureDigest, C: Ciphersuite> SchnorrAggregator<D, C> {
/// Create a new aggregator.
pub fn new() -> Self {
Self::default()
///
/// The DST used here must prevent a collision with whatever hash function produced the
/// challenges.
pub fn new(dst: &'static [u8]) -> Self {
let mut res = Self { digest: DigestTranscript::<D>::new(dst), sigs: vec![] };
res.digest.domain_separate(b"signatures");
res
}
/// Aggregate a signature.
pub fn aggregate(&mut self, public_key: C::G, challenge: C::F, sig: SchnorrSignature<C>) {
self.digest.update(challenge.to_repr().as_ref());
pub fn aggregate(&mut self, challenge: C::F, sig: SchnorrSignature<C>) {
self.digest.append_message(b"challenge", challenge.to_repr());
self.sigs.push(sig);
}
/// Complete aggregation, returning None if none were aggregated.
pub fn complete(self) -> Option<SchnorrAggregate<C>> {
pub fn complete(mut self) -> Option<SchnorrAggregate<C>> {
if self.sigs.is_empty() {
return None;
}
@ -156,7 +152,7 @@ impl<D: Clone + Digest, C: Ciphersuite> SchnorrAggregator<D, C> {
SchnorrAggregate { Rs: Vec::with_capacity(self.sigs.len()), s: C::F::zero() };
for i in 0 .. self.sigs.len() {
aggregate.Rs.push(self.sigs[i].R);
aggregate.s += self.sigs[i].s * digest_yield::<_, C::F>(self.digest.clone(), i);
aggregate.s += self.sigs[i].s * scalar_from_digest::<_, C::F>(&mut self.digest);
}
Some(aggregate)
}

View file

@ -79,15 +79,17 @@ pub(crate) fn batch_verify<C: Ciphersuite>() {
}
pub(crate) fn aggregate<C: Ciphersuite>() {
const DST: &[u8] = b"Schnorr Aggregator Test";
// Create 5 signatures
let mut keys = vec![];
let mut challenges = vec![];
let mut aggregator = SchnorrAggregator::<Sha256, C>::new();
let mut aggregator = SchnorrAggregator::<Sha256, C>::new(DST);
for i in 0 .. 5 {
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
challenges.push(C::random_nonzero_F(&mut OsRng));
aggregator.aggregate(
C::generator() * keys[i].deref(),
challenges[i],
SchnorrSignature::<C>::sign(
&keys[i],
@ -101,12 +103,13 @@ pub(crate) fn aggregate<C: Ciphersuite>() {
let aggregate =
SchnorrAggregate::<C>::read::<&[u8]>(&mut aggregate.serialize().as_ref()).unwrap();
assert!(aggregate.verify::<Sha256>(
DST,
keys
.iter()
.map(|key| C::generator() * key.deref())
.zip(challenges.iter().cloned())
.collect::<Vec<_>>()
.as_ref()
.as_ref(),
));
}