Implement a DLEq library

While Serai only needs the simple DLEq which was already present under 
monero, this migrates the implementation of the cross-group DLEq I 
maintain into Serai. This was to have full access to the ecosystem of 
libraries built under Serai while also ensuring support for it.

The cross_group curve, which is extremely experimental, is feature 
flagged off. So is the built in serialization functionality, as this 
should be possible to make nostd once const generics are full featured, 
yet the implemented serialization adds the additional barrier of 
std::io.
This commit is contained in:
Luke Parker
2022-06-30 05:42:29 -04:00
parent 2e168204f0
commit 5d115f1e1c
15 changed files with 854 additions and 111 deletions

View File

@@ -26,6 +26,7 @@ group = { version = "0.12", optional = true }
dalek-ff-group = { path = "../../crypto/dalek-ff-group", optional = true }
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", features = ["recommended"], optional = true }
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["ed25519"], optional = true }
dleq = { path = "../../crypto/dleq", features = ["serialize"], optional = true }
base58-monero = "1"
monero = "0.16"
@@ -38,7 +39,7 @@ reqwest = { version = "0.11", features = ["json"] }
[features]
experimental = []
multisig = ["rand_chacha", "blake2", "group", "dalek-ff-group", "transcript", "frost"]
multisig = ["rand_chacha", "blake2", "group", "dalek-ff-group", "transcript", "frost", "dleq"]
[dev-dependencies]
sha2 = "0.10"

View File

@@ -1,20 +1,15 @@
use core::convert::TryInto;
use std::{convert::TryInto, io::Cursor};
use thiserror::Error;
use rand_core::{RngCore, CryptoRng};
use group::GroupEncoding;
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE as DTable,
scalar::Scalar as DScalar,
edwards::EdwardsPoint as DPoint
};
use group::{Group, GroupEncoding};
use transcript::{Transcript, RecommendedTranscript};
use transcript::RecommendedTranscript;
use dalek_ff_group as dfg;
use crate::random_scalar;
use dleq::{Generators, DLEqProof};
#[derive(Clone, Error, Debug)]
pub enum MultisigError {
@@ -26,105 +21,34 @@ pub enum MultisigError {
InvalidKeyImage(u16)
}
// Used to prove legitimacy of key images and nonces which both involve other basepoints
#[derive(Clone)]
pub struct DLEqProof {
s: DScalar,
c: DScalar
}
#[allow(non_snake_case)]
impl DLEqProof {
fn challenge(H: &DPoint, xG: &DPoint, xH: &DPoint, rG: &DPoint, rH: &DPoint) -> DScalar {
pub(crate) fn write_dleq<R: RngCore + CryptoRng>(
rng: &mut R,
H: EdwardsPoint,
x: Scalar
) -> Vec<u8> {
let mut res = Vec::with_capacity(64);
DLEqProof::prove(
rng,
// Doesn't take in a larger transcript object due to the usage of this
// Every prover would immediately write their own DLEq proof, when they can only do so in
// the proper order if they want to reach consensus
// It'd be a poor API to have CLSAG define a new transcript solely to pass here, just to try to
// merge later in some form, when it should instead just merge xH (as it does)
let mut transcript = RecommendedTranscript::new(b"DLEq Proof");
// Bit redundant, keeps things consistent
transcript.domain_separate(b"DLEq");
// Doesn't include G which is constant, does include H which isn't, even though H manipulation
// shouldn't be possible in practice as it's independently calculated as a product of known data
transcript.append_message(b"H", &H.compress().to_bytes());
transcript.append_message(b"xG", &xG.compress().to_bytes());
transcript.append_message(b"xH", &xH.compress().to_bytes());
transcript.append_message(b"rG", &rG.compress().to_bytes());
transcript.append_message(b"rH", &rH.compress().to_bytes());
DScalar::from_bytes_mod_order_wide(
&transcript.challenge(b"challenge").try_into().expect("Blake2b512 output wasn't 64 bytes")
)
}
pub fn prove<R: RngCore + CryptoRng>(
rng: &mut R,
H: &DPoint,
secret: &DScalar
) -> DLEqProof {
let r = random_scalar(rng);
let rG = &DTable * &r;
let rH = r * H;
// We can frequently (always?) save a scalar mul if we accept xH as an arg, yet it opens room
// for incorrect data to be passed, and therefore faults, making it not worth having
// We could also return xH but... it's really micro-optimizing
let c = DLEqProof::challenge(H, &(secret * &DTable), &(secret * H), &rG, &rH);
let s = r + (c * secret);
DLEqProof { s, c }
}
pub fn verify(
&self,
H: &DPoint,
l: u16,
xG: &DPoint,
xH: &DPoint
) -> Result<(), MultisigError> {
let s = self.s;
let c = self.c;
let rG = (&s * &DTable) - (c * xG);
let rH = (s * H) - (c * xH);
if c != DLEqProof::challenge(H, &xG, &xH, &rG, &rH) {
Err(MultisigError::InvalidDLEqProof(l))?;
}
Ok(())
}
pub fn serialize(
&self
) -> Vec<u8> {
let mut res = Vec::with_capacity(64);
res.extend(self.s.to_bytes());
res.extend(self.c.to_bytes());
res
}
pub fn deserialize(
serialized: &[u8]
) -> Option<DLEqProof> {
if serialized.len() != 64 {
return None;
}
DScalar::from_canonical_bytes(serialized[0 .. 32].try_into().unwrap()).and_then(
|s| DScalar::from_canonical_bytes(serialized[32 .. 64].try_into().unwrap()).and_then(
|c| Some(DLEqProof { s, c })
)
)
}
&mut RecommendedTranscript::new(b"DLEq Proof"),
Generators::new(dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)),
dfg::Scalar(x)
).serialize(&mut res).unwrap();
res
}
#[allow(non_snake_case)]
pub(crate) fn read_dleq(
serialized: &[u8],
start: usize,
H: &DPoint,
H: EdwardsPoint,
l: u16,
xG: &DPoint
xG: dfg::EdwardsPoint
) -> Result<dfg::EdwardsPoint, MultisigError> {
if serialized.len() < start + 96 {
Err(MultisigError::InvalidDLEqProof(l))?;
@@ -132,17 +56,21 @@ pub(crate) fn read_dleq(
let bytes = (&serialized[(start + 0) .. (start + 32)]).try_into().unwrap();
// dfg ensures the point is torsion free
let other = Option::<dfg::EdwardsPoint>::from(
let xH = Option::<dfg::EdwardsPoint>::from(
dfg::EdwardsPoint::from_bytes(&bytes)).ok_or(MultisigError::InvalidDLEqProof(l)
)?;
// Ensure this is a canonical point
if other.to_bytes() != bytes {
if xH.to_bytes() != bytes {
Err(MultisigError::InvalidDLEqProof(l))?;
}
DLEqProof::deserialize(&serialized[(start + 32) .. (start + 96)])
.ok_or(MultisigError::InvalidDLEqProof(l))?
.verify(H, l, xG, &other).map_err(|_| MultisigError::InvalidDLEqProof(l))?;
let proof = DLEqProof::<dfg::EdwardsPoint>::deserialize(
&mut Cursor::new(&serialized[(start + 32) .. (start + 96)])
).map_err(|_| MultisigError::InvalidDLEqProof(l))?;
Ok(other)
let mut transcript = RecommendedTranscript::new(b"DLEq Proof");
proof.verify(&mut transcript, Generators::new(dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)), (xG, xH))
.map_err(|_| MultisigError::InvalidDLEqProof(l))?;
Ok(xH)
}

View File

@@ -19,7 +19,7 @@ use dalek_ff_group as dfg;
use crate::{
hash_to_point,
frost::{MultisigError, DLEqProof, read_dleq},
frost::{MultisigError, write_dleq, read_dleq},
ringct::clsag::{ClsagInput, Clsag}
};
@@ -133,12 +133,12 @@ impl Algorithm<Ed25519> for ClsagMultisig {
let mut serialized = Vec::with_capacity(ClsagMultisig::serialized_len());
serialized.extend((view.secret_share().0 * self.H).compress().to_bytes());
serialized.extend(DLEqProof::prove(rng, &self.H, &view.secret_share().0).serialize());
serialized.extend(write_dleq(rng, self.H, view.secret_share().0));
serialized.extend((nonces[0].0 * self.H).compress().to_bytes());
serialized.extend(&DLEqProof::prove(rng, &self.H, &nonces[0].0).serialize());
serialized.extend(write_dleq(rng, self.H, nonces[0].0));
serialized.extend((nonces[1].0 * self.H).compress().to_bytes());
serialized.extend(&DLEqProof::prove(rng, &self.H, &nonces[1].0).serialize());
serialized.extend(write_dleq(rng, self.H, nonces[1].0));
serialized
}
@@ -170,18 +170,18 @@ impl Algorithm<Ed25519> for ClsagMultisig {
self.image += read_dleq(
serialized,
cursor,
&self.H,
self.H,
l,
&view.verification_share(l).0
view.verification_share(l)
).map_err(|_| FrostError::InvalidCommitment(l))?.0;
cursor += 96;
self.transcript.append_message(b"commitment_D_H", &serialized[cursor .. (cursor + 32)]);
self.AH.0 += read_dleq(serialized, cursor, &self.H, l, &commitments[0]).map_err(|_| FrostError::InvalidCommitment(l))?;
self.AH.0 += read_dleq(serialized, cursor, self.H, l, commitments[0]).map_err(|_| FrostError::InvalidCommitment(l))?;
cursor += 96;
self.transcript.append_message(b"commitment_E_H", &serialized[cursor .. (cursor + 32)]);
self.AH.1 += read_dleq(serialized, cursor, &self.H, l, &commitments[1]).map_err(|_| FrostError::InvalidCommitment(l))?;
self.AH.1 += read_dleq(serialized, cursor, self.H, l, commitments[1]).map_err(|_| FrostError::InvalidCommitment(l))?;
Ok(())
}