Skip to content

Commit

Permalink
Test and refactor EvenY (renamed from PositiveY) (#94)
Browse files Browse the repository at this point in the history
* rename PositiveY into EvenY; refactor it

change reexposed FROST functions to already ensure even Y keys

* add comment
  • Loading branch information
conradoplg authored Nov 28, 2023
1 parent 397f501 commit 29162a7
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 31 deletions.
137 changes: 117 additions & 20 deletions src/frost/redpallas.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
//! Rerandomized FROST with Pallas curve.
//!
//! This also re-exposes the FROST functions already parametrized with the
//! Pallas curve. Note that if you use the generic frost-core functions instead,
//! you will not get public keys with guaranteed even Y coordinate, and will
//! need to convert them using the [`EvenY`] trait; see its documentation for
//! details.
#![allow(non_snake_case)]
#![deny(missing_docs)]

Expand Down Expand Up @@ -207,7 +213,7 @@ pub type Identifier = frost::Identifier<P>;

/// FROST(Pallas, BLAKE2b-512) keys, key generation, key shares.
pub mod keys {
use alloc::collections::BTreeMap;
use alloc::{collections::BTreeMap, vec::Vec};

use super::*;

Expand All @@ -222,7 +228,12 @@ pub mod keys {
identifiers: IdentifierList,
mut rng: RNG,
) -> Result<(BTreeMap<Identifier, SecretShare>, PublicKeyPackage), Error> {
frost::keys::generate_with_dealer(max_signers, min_signers, identifiers, &mut rng)
Ok(into_even_y(frost::keys::generate_with_dealer(
max_signers,
min_signers,
identifiers,
&mut rng,
)?))
}

/// Splits an existing key into FROST shares.
Expand All @@ -238,7 +249,13 @@ pub mod keys {
identifiers: IdentifierList,
rng: &mut R,
) -> Result<(BTreeMap<Identifier, SecretShare>, PublicKeyPackage), Error> {
frost::keys::split(key, max_signers, min_signers, identifiers, rng)
Ok(into_even_y(frost::keys::split(
key,
max_signers,
min_signers,
identifiers,
rng,
)?))
}

/// Secret and public key material generated by a dealer performing
Expand Down Expand Up @@ -284,19 +301,47 @@ pub mod keys {
/// ensure that they received the correct (and same) value.
pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment<P>;

/// Trait for ensuring the group public key has a positive Y coordinate.
pub trait PositiveY {
/// Convert the given package to make sure the group public key has
/// a positive Y coordinate.
fn into_positive_y(self) -> Self;
/// Trait for ensuring the group public key has an even Y coordinate.
///
/// In the [Zcash spec][1], Orchard spend authorizing keys (which are then
/// ones where FROST applies) are generated so that their matching public
/// keys have a even Y coordinate.
///
/// This trait is used to enable this procedure, by changing the private and
/// public keys to ensure that the public key has a even Y coordinate. This
/// is done by simply negating both keys if Y is even (in a field, negating
/// is equivalent to computing p - x where p is the prime modulus. Since p
/// is odd, if x is odd then the result will be even). Fortunately this
/// works even after Shamir secret sharing, in the individual signing and
/// verifying shares, since it's linear.
///
/// [1]: https://zips.z.cash/protocol/protocol.pdf#orchardkeycomponents
pub trait EvenY {
/// Return if the given type has a group public key with an even Y
/// coordinate.
fn has_even_y(&self) -> bool;

/// Convert the given type to make sure the group public key has an even
/// Y coordinate. `is_even` can be specified if evenness was already
/// determined beforehand. Returns a boolean indicating if the original
/// type had an even Y, and a (possibly converted) value with even Y.
fn into_even_y(self, is_even: Option<bool>) -> Self;
}

impl PositiveY for PublicKeyPackage {
fn into_positive_y(self) -> Self {
let pubkey = self.verifying_key();
let pubkey_serialized = pubkey.serialize();
if pubkey_serialized[31] & 0x80 != 0 {
let pubkey = VerifyingKey::new(-pubkey.to_element());
impl EvenY for PublicKeyPackage {
fn has_even_y(&self) -> bool {
let verifying_key = self.verifying_key();
let verifying_key_serialized = verifying_key.serialize();
verifying_key_serialized[31] & 0x80 == 0
}

fn into_even_y(self, is_even: Option<bool>) -> Self {
let is_even = is_even.unwrap_or_else(|| self.has_even_y());
if !is_even {
// Negate verifying key
let verifying_key = VerifyingKey::new(-self.verifying_key().to_element());
// Recreate verifying share map with negated VerifyingShares
// values.
let verifying_shares: BTreeMap<_, _> = self
.verifying_shares()
.iter()
Expand All @@ -305,26 +350,64 @@ pub mod keys {
(*i, vs)
})
.collect();
PublicKeyPackage::new(verifying_shares, pubkey)
PublicKeyPackage::new(verifying_shares, verifying_key)
} else {
self
}
}
}

impl EvenY for SecretShare {
fn has_even_y(&self) -> bool {
let key_package: KeyPackage = self
.clone()
.try_into()
.expect("Should work; expected to be called in freshly generated SecretShares");
key_package.has_even_y()
}

fn into_even_y(self, is_even: Option<bool>) -> Self {
let is_even = is_even.unwrap_or_else(|| self.has_even_y());
if !is_even {
// Negate SigningShare
let signing_share = SigningShare::new(-self.signing_share().to_scalar());
// Negate VerifiableSecretSharingCommitment by negating each
// coefficient in it. TODO: remove serialization roundtrip
// workaround after required functions are added to frost-core
let coefficients: Vec<_> = self
.commitment()
.coefficients()
.iter()
.map(|e| <PallasBlake2b512 as Ciphersuite>::Group::serialize(&-e.value()))
.collect();
let commitments = VerifiableSecretSharingCommitment::deserialize(coefficients)
.expect("Should work since they were just serialized");
SecretShare::new(*self.identifier(), signing_share, commitments)
} else {
self
}
}
}

impl PositiveY for KeyPackage {
fn into_positive_y(self) -> Self {
impl EvenY for KeyPackage {
fn has_even_y(&self) -> bool {
let pubkey = self.verifying_key();
let pubkey_serialized = pubkey.serialize();
if pubkey_serialized[31] & 0x80 != 0 {
let pubkey = VerifyingKey::new(-pubkey.to_element());
pubkey_serialized[31] & 0x80 == 0
}

fn into_even_y(self, is_even: Option<bool>) -> Self {
let is_even = is_even.unwrap_or_else(|| self.has_even_y());
if !is_even {
// Negate all components
let verifying_key = VerifyingKey::new(-self.verifying_key().to_element());
let signing_share = SigningShare::new(-self.signing_share().to_scalar());
let verifying_share = VerifyingShare::new(-self.verifying_share().to_element());
KeyPackage::new(
*self.identifier(),
signing_share,
verifying_share,
pubkey,
verifying_key,
*self.min_signers(),
)
} else {
Expand All @@ -333,6 +416,20 @@ pub mod keys {
}
}

// Helper function which calls into_even_y() on the return values of
// keygen/split functions.
fn into_even_y(
(secret_shares, public_key_package): (BTreeMap<Identifier, SecretShare>, PublicKeyPackage),
) -> (BTreeMap<Identifier, SecretShare>, PublicKeyPackage) {
let is_even = public_key_package.has_even_y();
let public_key_package = public_key_package.into_even_y(Some(is_even));
let secret_shares = secret_shares
.iter()
.map(|(i, s)| (*i, s.clone().into_even_y(Some(is_even))))
.collect();
(secret_shares, public_key_package)
}

pub mod dkg;
pub mod repairable;
}
Expand Down
8 changes: 7 additions & 1 deletion src/frost/redpallas/keys/dkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,11 @@ pub fn part3(
round1_packages: &BTreeMap<Identifier, round1::Package>,
round2_packages: &BTreeMap<Identifier, round2::Package>,
) -> Result<(KeyPackage, PublicKeyPackage), Error> {
frost::keys::dkg::part3(round2_secret_package, round1_packages, round2_packages)
let (key_package, public_key_package) =
frost::keys::dkg::part3(round2_secret_package, round1_packages, round2_packages)?;
let is_even = public_key_package.has_even_y();
Ok((
key_package.into_even_y(Some(is_even)),
public_key_package.into_even_y(Some(is_even)),
))
}
128 changes: 118 additions & 10 deletions tests/frost_redpallas.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
#![cfg(feature = "frost")]

use frost_rerandomized::frost_core::{Ciphersuite, Group, GroupError};
use std::collections::BTreeMap;

use frost_rerandomized::frost_core::{self as frost, Ciphersuite, Group, GroupError};
use rand::thread_rng;

use reddsa::{frost::redpallas::PallasBlake2b512, orchard};
use reddsa::{
frost::redpallas::{keys::EvenY, PallasBlake2b512},
orchard,
};

#[test]
fn check_sign_with_dealer() {
let rng = thread_rng();

frost_rerandomized::frost_core::tests::ciphersuite_generic::check_sign_with_dealer::<
PallasBlake2b512,
_,
>(rng);
frost::tests::ciphersuite_generic::check_sign_with_dealer::<PallasBlake2b512, _>(rng);
}

#[test]
Expand Down Expand Up @@ -46,10 +48,7 @@ fn check_randomized_sign_with_dealer() {
fn check_sign_with_dkg() {
let rng = thread_rng();

frost_rerandomized::frost_core::tests::ciphersuite_generic::check_sign_with_dkg::<
PallasBlake2b512,
_,
>(rng);
frost::tests::ciphersuite_generic::check_sign_with_dkg::<PallasBlake2b512, _>(rng);
}

#[test]
Expand Down Expand Up @@ -78,3 +77,112 @@ fn check_deserialize_non_canonical() {
let r = <PallasBlake2b512 as Ciphersuite>::Group::deserialize(&encoded_point);
assert_eq!(r, Err(GroupError::MalformedElement));
}

#[test]
fn check_even_y_frost_core() {
let mut rng = thread_rng();

// Since there is a 50% chance of the public key having an odd Y (which
// we need to actually test), loop until we get an odd Y.
loop {
let max_signers = 5;
let min_signers = 3;
// Generate keys with frost-core function, which doesn't ensure even Y
let (shares, public_key_package) =
frost::keys::generate_with_dealer::<PallasBlake2b512, _>(
max_signers,
min_signers,
frost::keys::IdentifierList::Default,
&mut rng,
)
.unwrap();

if !public_key_package.has_even_y() {
// Test consistency of into_even_y() for PublicKeyPackage
let even_public_key_package_is_even_none = public_key_package.clone().into_even_y(None);
let even_public_key_package_is_even_false =
public_key_package.clone().into_even_y(Some(false));
assert_eq!(
even_public_key_package_is_even_false,
even_public_key_package_is_even_none
);
assert_ne!(public_key_package, even_public_key_package_is_even_false);
assert_ne!(public_key_package, even_public_key_package_is_even_none);

// Test consistency of into_even_y() for SecretShare (arbitrarily on
// the first secret share)
let secret_share = shares.first_key_value().unwrap().1.clone();
let even_secret_share_is_even_none = secret_share.clone().into_even_y(None);
let even_secret_share_is_even_false = secret_share.clone().into_even_y(Some(false));
assert_eq!(
even_secret_share_is_even_false,
even_secret_share_is_even_none
);
assert_ne!(secret_share, even_secret_share_is_even_false);
assert_ne!(secret_share, even_secret_share_is_even_none);

// Make secret shares even, then convert into KeyPackages
let key_packages_evened_before: BTreeMap<_, _> = shares
.clone()
.into_iter()
.map(|(identifier, share)| {
Ok((
identifier,
frost::keys::KeyPackage::try_from(share.into_even_y(None))?,
))
})
.collect::<Result<_, frost::Error<PallasBlake2b512>>>()
.unwrap();
// Convert into KeyPackages, then make them even
let key_packages_evened_after: BTreeMap<_, _> = shares
.into_iter()
.map(|(identifier, share)| {
Ok((
identifier,
frost::keys::KeyPackage::try_from(share)?.into_even_y(None),
))
})
.collect::<Result<_, frost::Error<PallasBlake2b512>>>()
.unwrap();
// Make sure they are equal
assert_eq!(key_packages_evened_after, key_packages_evened_before);

// Check if signing works with evened keys
frost::tests::ciphersuite_generic::check_sign(
min_signers,
key_packages_evened_after,
&mut rng,
even_public_key_package_is_even_none,
)
.unwrap();

// We managed to test it; break the loop and return
break;
}
}
}

#[test]
fn check_even_y_reddsa() {
let mut rng = thread_rng();

// Since there is a ~50% chance of having a odd Y internally, to make sure
// that odd Ys are converted to even, we test multiple times to increase
// the chance of an odd Y being generated internally
for _ in 0..16 {
let max_signers = 5;
let min_signers = 3;
// Generate keys with reexposed reddsa function, which ensures even Y
let (shares, public_key_package) =
reddsa::frost::redpallas::keys::generate_with_dealer::<_>(
max_signers,
min_signers,
frost::keys::IdentifierList::Default,
&mut rng,
)
.unwrap();

assert!(public_key_package.has_even_y());
assert!(shares.values().all(|s| s.has_even_y()));
}
}

0 comments on commit 29162a7

Please sign in to comment.