Move the Monero create to coins/

Includes misc bug fixes
This commit is contained in:
Luke Parker
2022-04-27 00:09:05 -04:00
parent 79f39c4433
commit df4be9ca0c
19 changed files with 13 additions and 3 deletions

View File

@@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
rand_core = "0.6"
digest = "0.10"
subtle = "2.4"

View File

@@ -5,6 +5,7 @@ use core::{
};
use rand_core::RngCore;
use digest::{consts::U64, Digest};
use subtle::{Choice, CtOption, ConstantTimeEq, ConditionallySelectable};
@@ -146,6 +147,14 @@ impl PrimeField for Scalar {
fn root_of_unity() -> Self { unimplemented!() }
}
impl Scalar {
pub fn from_hash<D: Digest<OutputSize = U64>>(hash: D) -> Scalar {
let mut output = [0u8; 64];
output.copy_from_slice(&hash.finalize());
Scalar(DScalar::from_bytes_mod_order_wide(&output))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct EdwardsPoint(pub DPoint);
pub const ED25519_BASEPOINT_POINT: EdwardsPoint = EdwardsPoint(constants::ED25519_BASEPOINT_POINT);

View File

@@ -1 +0,0 @@
c/.build

View File

@@ -1,31 +0,0 @@
[package]
name = "monero-sign"
version = "0.1.0"
description = "Implementation of Monero transaction signing in Rust"
license = "MIT"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
[dependencies]
lazy_static = "1"
thiserror = "1"
rand_core = "0.6"
tiny-keccak = { version = "2.0", features = ["keccak"] }
blake2 = "0.10"
curve25519-dalek = { version = "3.2", features = ["std", "simd_backend"] }
ff = { version = "0.11", optional = true }
group = { version = "0.11", optional = true }
dalek-ff-group = { path = "../dalek-ff-group", optional = true }
frost = { path = "../frost", optional = true }
monero = "0.16.0" # Locked to this specific patch version due to a bug we compensate for
[features]
multisig = ["ff", "group", "dalek-ff-group", "frost"]
[dev-dependencies]
rand = "0.8"

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,89 +0,0 @@
use std::process::Command;
use std::env;
use std::path::Path;
fn main() {
if !Command::new("git").args(&["submodule", "update", "--init", "--recursive"]).status().unwrap().success() {
panic!("git failed to init submodules");
}
if !Command ::new("mkdir").args(&["-p", ".build"])
.current_dir(&Path::new("c")).status().unwrap().success() {
panic!("failed to create a directory to track build progress");
}
let out_dir = &env::var("OUT_DIR").unwrap();
// Use a file to signal if Monero was already built, as that should never be rebuilt
// If the signaling file was deleted, run this script again to rebuild Monero though
// TODO: Move this signaling file into OUT_DIR once Monero is built statically successfully
println!("cargo:rerun-if-changed=c/.build/monero");
if !Path::new("c/.build/monero").exists() {
if !Command::new("cmake").args(&["cmake", "-DCMAKE_BUILD_TYPE=Release", "-DBUILD_SHARED_LIBS=1", "."])
.current_dir(&Path::new("c/monero")).status().unwrap().success() {
panic!("cmake failed to generate Monero's build scripts");
}
if !Command::new("make").arg(format!("-j{}", &env::var("THREADS").unwrap_or("2".to_string())))
.current_dir(&Path::new("c/monero")).status().unwrap().success() {
panic!("make failed to build Monero. Please check your dependencies");
}
if !Command::new("touch").arg("monero")
.current_dir(&Path::new("c/.build")).status().unwrap().success() {
panic!("failed to create a file to label Monero as built");
}
}
println!("cargo:rerun-if-env-changed=OUT_DIR");
if !Path::new(
&format!(
"c/monero/src/crypto/{}cncrypto.{}",
&env::consts::DLL_PREFIX,
&env::consts::DLL_EXTENSION
)
).exists() {
for (folder, lib) in [
("crypto", "cncrypto"),
("device", "device"),
("ringct", "ringct_basic"),
("ringct", "ringct")
] {
if !Command::new("cp").args(&[
&format!(
"c/monero/src/{}/{}{}.{}",
folder,
&env::consts::DLL_PREFIX,
lib,
&env::consts::DLL_EXTENSION
),
out_dir
]).status().unwrap().success() {
panic!("Failed to cp {}", lib);
}
}
println!("cargo:rerun-if-changed=c/wrapper.c");
if !Command::new("g++").args(&[
"-O3", "-Wall", "-shared", "-std=c++14", "-fPIC",
"-Imonero/contrib/epee/include", "-Imonero/src",
"wrapper.c", "-o", &format!(
"{}/{}wrapper.{}",
out_dir,
&env::consts::DLL_PREFIX,
&env::consts::DLL_EXTENSION
),
&format!("-L{}", out_dir),
"-ldevice", "-lringct_basic", "-lringct"
]).current_dir(&Path::new("c")).status().unwrap().success() {
panic!("g++ failed to build the wrapper");
}
}
println!("cargo:rustc-link-search={}", out_dir);
println!("cargo:rustc-link-lib=cncrypto");
println!("cargo:rustc-link-lib=device");
println!("cargo:rustc-link-lib=ringct_basic");
println!("cargo:rustc-link-lib=ringct");
println!("cargo:rustc-link-lib=wrapper");
}

View File

@@ -1,42 +0,0 @@
#include "device/device_default.hpp"
#include "ringct/rctSigs.h"
extern "C" {
void c_hash_to_point(uint8_t* point) {
rct::key key_point;
ge_p3 e_p3;
memcpy(key_point.bytes, point, 32);
rct::hash_to_p3(e_p3, key_point);
ge_p3_tobytes(point, &e_p3);
}
bool c_verify_clsag(uint s_len, uint8_t* s, uint8_t* I, uint8_t k_len, uint8_t* k, uint8_t* m, uint8_t* p) {
rct::clsag clsag;
std::stringstream ss;
std::string str;
str.assign((char*) s, (size_t) s_len);
ss << str;
binary_archive<false> ba(ss);
::serialization::serialize(ba, clsag);
if (!ss.good()) {
return false;
}
memcpy(clsag.I.bytes, I, 32);
rct::key msg;
memcpy(msg.bytes, m, 32);
rct::ctkeyV keys;
keys.resize(k_len);
for (uint8_t i = 0; i < k_len; i++) {
memcpy(keys[i].dest.bytes, &k[(i * 2) * 32], 32);
memcpy(keys[i].mask.bytes, &k[((i * 2) + 1) * 32], 32);
}
rct::key pseudo_out;
memcpy(pseudo_out.bytes, p, 32);
return verRctCLSAGSimple(msg, clsag, keys, pseudo_out);
}
}

View File

@@ -1,241 +0,0 @@
use rand_core::{RngCore, CryptoRng};
use blake2::{Digest, Blake2b512};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE,
scalar::Scalar,
traits::VartimePrecomputedMultiscalarMul,
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation}
};
use monero::{
consensus::Encodable,
util::ringct::{Key, Clsag}
};
use crate::{SignError, c_verify_clsag, random_scalar, commitment, hash_to_scalar, hash_to_point};
#[cfg(feature = "multisig")]
mod multisig;
#[cfg(feature = "multisig")]
pub use multisig::Multisig;
// Ring with both the index we're signing for and the data needed to rebuild its commitment
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct SemiSignableRing {
ring: Vec<[EdwardsPoint; 2]>,
i: usize,
randomness: Scalar,
amount: u64
}
pub(crate) fn validate_sign_args(
ring: Vec<[EdwardsPoint; 2]>,
i: u8,
private_key: Option<&Scalar>, // Option as multisig won't have access to this
randomness: &Scalar,
amount: u64
) -> Result<SemiSignableRing, SignError> {
let n = ring.len();
if n > u8::MAX.into() {
Err(SignError::InternalError("max ring size in this library is u8 max".to_string()))?;
}
if i >= (n as u8) {
Err(SignError::InvalidRingMember(i, n as u8))?;
}
let i: usize = i.into();
// Validate the secrets match these ring members
if private_key.is_some() && (ring[i][0] != (private_key.unwrap() * &ED25519_BASEPOINT_TABLE)) {
Err(SignError::InvalidSecret(0))?;
}
if ring[i][1] != commitment(&randomness, amount) {
Err(SignError::InvalidSecret(1))?;
}
Ok(SemiSignableRing { ring, i, randomness: *randomness, amount })
}
#[allow(non_snake_case)]
pub(crate) fn sign_core(
rand_source: [u8; 64],
image: EdwardsPoint,
ssr: &SemiSignableRing,
msg: &[u8; 32],
A: EdwardsPoint,
AH: EdwardsPoint
) -> (Clsag, Scalar, Scalar, Scalar, Scalar, EdwardsPoint) {
let n = ssr.ring.len();
let i: usize = ssr.i.into();
let C_out;
let mut P = vec![];
P.reserve_exact(n);
let mut C = vec![];
C.reserve_exact(n);
let mut C_non_zero = vec![];
C_non_zero.reserve_exact(n);
let z;
let mut next_rand = rand_source;
next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap();
{
let a = Scalar::from_bytes_mod_order_wide(&next_rand);
next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap();
C_out = commitment(&a, ssr.amount);
for member in &ssr.ring {
P.push(member[0]);
C_non_zero.push(member[1]);
C.push(C_non_zero[C_non_zero.len() - 1] - C_out);
}
z = ssr.randomness - a;
}
let H = hash_to_point(&P[i]);
let mut D = H * z;
// Doesn't use a constant time table as dalek takes longer to generate those then they save
let images_precomp = VartimeEdwardsPrecomputation::new(&[image, D]);
D = Scalar::from(8 as u8).invert() * D;
let mut to_hash = vec![];
to_hash.reserve_exact(((2 * n) + 4) * 32);
const PREFIX: &str = "CLSAG_";
const AGG_0: &str = "CLSAG_agg_0";
const ROUND: &str = "round";
to_hash.extend(AGG_0.bytes());
to_hash.extend([0; 32 - AGG_0.len()]);
for j in 0 .. n {
to_hash.extend(P[j].compress().to_bytes());
}
for j in 0 .. n {
to_hash.extend(C_non_zero[j].compress().to_bytes());
}
to_hash.extend(image.compress().to_bytes());
let D_bytes = D.compress().to_bytes();
to_hash.extend(D_bytes);
to_hash.extend(C_out.compress().to_bytes());
let mu_P = hash_to_scalar(&to_hash);
to_hash[AGG_0.len() - 1] = '1' as u8;
let mu_C = hash_to_scalar(&to_hash);
to_hash.truncate(((2 * n) + 1) * 32);
to_hash.reserve_exact(((2 * n) + 5) * 32);
for j in 0 .. ROUND.len() {
to_hash[PREFIX.len() + j] = ROUND.as_bytes()[j] as u8;
}
to_hash.extend(C_out.compress().to_bytes());
to_hash.extend(msg);
to_hash.extend(A.compress().to_bytes());
to_hash.extend(AH.compress().to_bytes());
let mut c = hash_to_scalar(&to_hash);
let mut c1 = Scalar::zero();
let mut j = (i + 1) % n;
if j == 0 {
c1 = c;
}
let mut s = vec![];
s.resize(n, Scalar::zero());
while j != i {
s[j] = Scalar::from_bytes_mod_order_wide(&next_rand);
next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap();
let c_p = mu_P * c;
let c_c = mu_C * c;
let L = (&s[j] * &ED25519_BASEPOINT_TABLE) + (c_p * P[j]) + (c_c * C[j]);
let PH = hash_to_point(&P[j]);
// Shouldn't be an issue as all of the variables in this vartime statement are public
let R = (s[j] * PH) + images_precomp.vartime_multiscalar_mul(&[c_p, c_c]);
to_hash.truncate(((2 * n) + 3) * 32);
to_hash.extend(L.compress().to_bytes());
to_hash.extend(R.compress().to_bytes());
c = hash_to_scalar(&to_hash);
j = (j + 1) % n;
if j == 0 {
c1 = c;
}
}
(
Clsag {
s: s.iter().map(|s| Key { key: s.to_bytes() }).collect(),
c1: Key { key: c1.to_bytes() },
D: Key { key: D_bytes }
},
c, mu_C, z, mu_P,
C_out
)
}
#[allow(non_snake_case)]
pub fn sign<R: RngCore + CryptoRng>(
rng: &mut R,
image: EdwardsPoint,
msg: [u8; 32],
ring: Vec<[EdwardsPoint; 2]>,
i: u8,
private_key: &Scalar,
randomness: &Scalar,
amount: u64
) -> Result<(Clsag, EdwardsPoint), SignError> {
let ssr = validate_sign_args(ring, i, Some(private_key), randomness, amount)?;
let a = random_scalar(rng);
let mut rand_source = [0; 64];
rng.fill_bytes(&mut rand_source);
let (mut clsag, c, mu_C, z, mu_P, C_out) = sign_core(
rand_source,
image,
&ssr,
&msg,
&a * &ED25519_BASEPOINT_TABLE, a * hash_to_point(&ssr.ring[ssr.i][0])
);
clsag.s[i as usize] = Key { key: (a - (c * ((mu_C * z) + (mu_P * private_key)))).to_bytes() };
Ok((clsag, C_out))
}
// Uses Monero's C verification function to ensure compatibility with Monero
pub fn verify(
clsag: &Clsag,
image: EdwardsPoint,
msg: &[u8; 32],
ring: &[[EdwardsPoint; 2]],
pseudo_out: EdwardsPoint
) -> Result<(), SignError> {
// Workaround for the fact monero-rs doesn't include the length of clsag.s in clsag encoding
// despite it being part of clsag encoding. Reason for the patch version pin
let mut serialized = vec![clsag.s.len() as u8];
clsag.consensus_encode(&mut serialized).unwrap();
let image_bytes = image.compress().to_bytes();
let mut ring_bytes = vec![];
for member in ring {
ring_bytes.extend(&member[0].compress().to_bytes());
ring_bytes.extend(&member[1].compress().to_bytes());
}
let pseudo_out_bytes = pseudo_out.compress().to_bytes();
let success;
unsafe {
success = c_verify_clsag(
serialized.len(), serialized.as_ptr(), image_bytes.as_ptr(),
ring.len() as u8, ring_bytes.as_ptr(), msg.as_ptr(), pseudo_out_bytes.as_ptr()
);
}
if success { Ok(()) } else { Err(SignError::InvalidSignature) }
}

View File

@@ -1,211 +0,0 @@
use rand_core::{RngCore, CryptoRng};
use blake2::{Digest, Blake2b512};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE,
scalar::Scalar,
edwards::EdwardsPoint
};
use dalek_ff_group as dfg;
use group::Group;
use frost::{Curve, FrostError, algorithm::Algorithm, sign::ParamsView};
use monero::util::ringct::{Key, Clsag};
use crate::{
SignError,
hash_to_point,
frost::{Ed25519, DLEqProof},
clsag::{SemiSignableRing, validate_sign_args, sign_core, verify}
};
#[allow(non_snake_case)]
#[derive(Clone, Debug)]
struct ClsagSignInterim {
c: Scalar,
mu_C: Scalar,
z: Scalar,
mu_P: Scalar,
clsag: Clsag,
C_out: EdwardsPoint
}
#[allow(non_snake_case)]
#[derive(Clone, Debug)]
pub struct Multisig {
b: Vec<u8>,
AH: dfg::EdwardsPoint,
image: EdwardsPoint,
ssr: SemiSignableRing,
msg: [u8; 32],
interim: Option<ClsagSignInterim>
}
impl Multisig {
pub fn new(
image: EdwardsPoint,
msg: [u8; 32],
ring: Vec<[EdwardsPoint; 2]>,
i: u8,
randomness: &Scalar,
amount: u64
) -> Result<Multisig, SignError> {
let ssr = validate_sign_args(ring, i, None, randomness, amount)?;
Ok(
Multisig {
b: vec![],
AH: dfg::EdwardsPoint::identity(),
image,
ssr,
msg,
interim: None
}
)
}
}
impl Algorithm<Ed25519> for Multisig {
type Signature = (Clsag, EdwardsPoint);
fn context(&self) -> Vec<u8> {
let mut context = self.image.compress().to_bytes().to_vec();
for pair in &self.ssr.ring {
context.extend(&pair[0].compress().to_bytes());
context.extend(&pair[1].compress().to_bytes());
}
context.extend(&u8::try_from(self.ssr.i).unwrap().to_le_bytes());
context.extend(&self.msg);
context
}
// We arguably don't have to commit to at all thanks to xG and yG being committed to, both of
// those being proven to have the same scalar as xH and yH, yet it doesn't hurt
fn addendum_commit_len() -> usize {
64
}
fn preprocess_addendum<R: RngCore + CryptoRng>(
rng: &mut R,
view: &ParamsView<Ed25519>,
nonces: &[dfg::Scalar; 2]
) -> Vec<u8> {
#[allow(non_snake_case)]
let H = hash_to_point(&view.group_key().0);
let h0 = nonces[0].0 * H;
let h1 = nonces[1].0 * H;
// 32 + 32 + 64 + 64
let mut serialized = Vec::with_capacity(192);
serialized.extend(h0.compress().to_bytes());
serialized.extend(h1.compress().to_bytes());
serialized.extend(&DLEqProof::prove(rng, &nonces[0].0, &H, &h0).serialize());
serialized.extend(&DLEqProof::prove(rng, &nonces[1].0, &H, &h1).serialize());
serialized
}
fn process_addendum(
&mut self,
_: &ParamsView<Ed25519>,
l: usize,
commitments: &[dfg::EdwardsPoint; 2],
p: &dfg::Scalar,
serialized: &[u8]
) -> Result<(), FrostError> {
if serialized.len() != 192 {
// Not an optimal error but...
Err(FrostError::InvalidCommitmentQuantity(l, 6, serialized.len() / 32))?;
}
let alt = &hash_to_point(&self.ssr.ring[self.ssr.i][0]);
let h0 = <Ed25519 as Curve>::G_from_slice(&serialized[0 .. 32]).map_err(|_| FrostError::InvalidCommitment(l))?;
DLEqProof::deserialize(&serialized[64 .. 128]).ok_or(FrostError::InvalidCommitment(l))?.verify(
&alt,
&commitments[0],
&h0
).map_err(|_| FrostError::InvalidCommitment(l))?;
let h1 = <Ed25519 as Curve>::G_from_slice(&serialized[32 .. 64]).map_err(|_| FrostError::InvalidCommitment(l))?;
DLEqProof::deserialize(&serialized[128 .. 192]).ok_or(FrostError::InvalidCommitment(l))?.verify(
&alt,
&commitments[1],
&h1
).map_err(|_| FrostError::InvalidCommitment(l))?;
self.b.extend(&l.to_le_bytes());
self.b.extend(&serialized[0 .. 64]);
self.AH += h0 + (h1 * p);
Ok(())
}
fn sign_share(
&mut self,
view: &ParamsView<Ed25519>,
nonce_sum: dfg::EdwardsPoint,
nonce: dfg::Scalar,
_: &[u8]
) -> dfg::Scalar {
// Use everyone's commitments to derive a random source all signers can agree upon
// Cannot be manipulated to effect and all signers must, and will, know this
let rand_source = Keccak::v512()
.chain("clsag_randomness")
.chain(&self.b)
.finalize()
.as_slice()
.try_into()
.unwrap();
#[allow(non_snake_case)]
let (clsag, c, mu_C, z, mu_P, C_out) = sign_core(
rand_source,
self.image,
&self.ssr,
&self.msg,
nonce_sum.0,
self.AH.0
);
let share = dfg::Scalar(nonce.0 - (c * (mu_P * view.secret_share().0)));
self.interim = Some(ClsagSignInterim { c, mu_C, z, mu_P, clsag, C_out });
share
}
fn verify(
&self,
_: dfg::EdwardsPoint,
_: dfg::EdwardsPoint,
sum: dfg::Scalar
) -> Option<Self::Signature> {
let interim = self.interim.as_ref().unwrap();
// Subtract the randomness's presence, which is done once and not fractionalized among shares
let s = sum.0 - (interim.c * (interim.mu_C * interim.z));
let mut clsag = interim.clsag.clone();
clsag.s[self.ssr.i] = Key { key: s.to_bytes() };
if verify(&clsag, self.image, &self.ssr.ring, &self.msg, interim.C_out).is_ok() {
return Some((clsag, interim.C_out));
}
return None;
}
fn verify_share(
&self,
verification_share: dfg::EdwardsPoint,
nonce: dfg::EdwardsPoint,
share: dfg::Scalar,
) -> bool {
let interim = self.interim.as_ref().unwrap();
return (&share.0 * &ED25519_BASEPOINT_TABLE) == (
nonce.0 - (interim.c * (interim.mu_P * verification_share.0))
);
}
}

View File

@@ -1,186 +0,0 @@
use core::convert::TryInto;
use rand_core::{RngCore, CryptoRng};
use blake2::{Digest, Blake2b512};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE as DTable,
traits::VartimeMultiscalarMul,
scalar::Scalar as DScalar,
edwards::EdwardsPoint as DPoint
};
use dalek_ff_group::EdwardsPoint;
use ff::PrimeField;
use group::Group;
use dalek_ff_group as dfg;
use frost::{CurveError, Curve};
use crate::{SignError, random_scalar};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Ed25519;
impl Curve for Ed25519 {
type F = dfg::Scalar;
type G = dfg::EdwardsPoint;
type T = &'static dfg::EdwardsBasepointTable;
fn id() -> String {
"Ed25519".to_string()
}
fn id_len() -> u8 {
Self::id().len() as u8
}
fn generator() -> Self::G {
Self::G::generator()
}
fn generator_table() -> Self::T {
&dfg::ED25519_BASEPOINT_TABLE
}
fn multiexp_vartime(scalars: &[Self::F], points: &[Self::G]) -> Self::G {
EdwardsPoint(DPoint::vartime_multiscalar_mul(scalars, points))
}
fn hash_msg(msg: &[u8]) -> Vec<u8> {
Blake2b512::digest(msg)
}
fn hash_to_F(data: &[u8]) -> Self::F {
dfg::Scalar::from_hash(Blake2b512::new().chain(data))
}
fn F_len() -> usize {
32
}
fn G_len() -> usize {
32
}
fn F_from_le_slice(slice: &[u8]) -> Result<Self::F, CurveError> {
let scalar = Self::F::from_repr(
slice.try_into().map_err(|_| CurveError::InvalidLength(32, slice.len()))?
);
if scalar.is_some().unwrap_u8() == 1 {
Ok(scalar.unwrap())
} else {
Err(CurveError::InvalidScalar)
}
}
fn G_from_slice(slice: &[u8]) -> Result<Self::G, CurveError> {
let point = dfg::CompressedEdwardsY::new(
slice.try_into().map_err(|_| CurveError::InvalidLength(32, slice.len()))?
).decompress();
if point.is_some() {
let point = point.unwrap();
// Ban torsioned points
if !point.is_torsion_free() {
Err(CurveError::InvalidPoint)?
}
Ok(point)
} else {
Err(CurveError::InvalidPoint)
}
}
fn F_to_le_bytes(f: &Self::F) -> Vec<u8> {
f.to_repr().to_vec()
}
fn G_to_bytes(g: &Self::G) -> Vec<u8> {
g.compress().to_bytes().to_vec()
}
}
// Used to prove legitimacy in several locations
#[derive(Clone)]
pub struct DLEqProof {
s: DScalar,
c: DScalar
}
#[allow(non_snake_case)]
impl DLEqProof {
pub fn prove<R: RngCore + CryptoRng>(
rng: &mut R,
secret: &DScalar,
H: &DPoint,
alt: &DPoint
) -> DLEqProof {
let r = random_scalar(rng);
let R1 = &DTable * &r;
let R2 = r * H;
let c = DScalar::from_hash(
Blake2b512::new()
.chain(R1.compress().to_bytes())
.chain(R2.compress().to_bytes())
.chain((secret * &DTable).compress().to_bytes())
.chain(alt.compress().to_bytes())
);
let s = r + (c * secret);
DLEqProof { s, c }
}
pub fn verify(
&self,
H: &DPoint,
primary: &DPoint,
alt: &DPoint
) -> Result<(), SignError> {
let s = self.s;
let c = self.c;
let R1 = (&s * &DTable) - (c * primary);
let R2 = (s * H) - (c * alt);
let expected_c = DScalar::from_hash(
Blake2b512::new()
.chain(R1.compress().to_bytes())
.chain(R2.compress().to_bytes())
.chain(primary.compress().to_bytes())
.chain(alt.compress().to_bytes())
);
// Take the opportunity to ensure a lack of torsion in key images/randomness commitments
if (!primary.is_torsion_free()) || (!alt.is_torsion_free()) || (c != expected_c) {
Err(SignError::InvalidDLEqProof)?;
}
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;
}
Some(
DLEqProof {
s: DScalar::from_bytes_mod_order(serialized[0 .. 32].try_into().unwrap()),
c: DScalar::from_bytes_mod_order(serialized[32 .. 64].try_into().unwrap())
}
)
}
}

View File

@@ -1,16 +0,0 @@
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE,
scalar::Scalar,
edwards::EdwardsPoint
};
use crate::hash_to_point;
#[cfg(feature = "multisig")]
mod multisig;
#[cfg(feature = "multisig")]
pub use crate::key_image::multisig::{Package, multisig};
pub fn single(secret: &Scalar) -> EdwardsPoint {
secret * hash_to_point(&(secret * &ED25519_BASEPOINT_TABLE))
}

View File

@@ -1,75 +0,0 @@
use rand_core::{RngCore, CryptoRng};
use curve25519_dalek::edwards::EdwardsPoint;
use dalek_ff_group::Scalar;
use frost::{MultisigKeys, sign::lagrange};
use crate::{SignError, hash_to_point, frost::{Ed25519, DLEqProof}};
#[derive(Clone)]
#[allow(non_snake_case)]
pub struct Package {
// Don't serialize
H: EdwardsPoint,
i: usize,
// Serialize
image: EdwardsPoint,
proof: DLEqProof
}
#[allow(non_snake_case)]
pub fn multisig<R: RngCore + CryptoRng>(
rng: &mut R,
keys: &MultisigKeys<Ed25519>,
included: &[usize]
) -> Package {
let i = keys.params().i();
let secret = (keys.secret_share() * lagrange::<Scalar>(i, included)).0;
let H = hash_to_point(&keys.group_key().0);
let image = secret * H;
// Includes a proof. Since:
// sum(lagranged_secrets) = group_private
// group_private * G = output_key
// group_private * H = key_image
// Then sum(lagranged_secrets * H) = key_image
// lagranged_secret * G is known. lagranged_secret * H is being sent
// Any discrete log equality proof confirms the same secret was used,
// forming a valid key_image share
Package { H, i, image, proof: DLEqProof::prove(rng, &secret, &H, &image) }
}
#[allow(non_snake_case)]
impl Package {
pub fn resolve(
self,
shares: Vec<Option<(EdwardsPoint, Package)>>
) -> Result<EdwardsPoint, SignError> {
let mut included = vec![self.i];
for i in 1 .. shares.len() {
if shares[i].is_some() {
included.push(i);
}
}
let mut image = self.image;
for i in 0 .. shares.len() {
if shares[i].is_none() {
continue;
}
let (other, shares) = shares[i].as_ref().unwrap();
let other = other * lagrange::<Scalar>(i, &included).0;
// Verify their proof
let share = shares.image;
shares.proof.verify(&self.H, &other, &share).map_err(|_| SignError::InvalidKeyImage(i))?;
// Add their share to the image
image += share;
}
Ok(image)
}
}

View File

@@ -1,82 +0,0 @@
use lazy_static::lazy_static;
use thiserror::Error;
use rand_core::{RngCore, CryptoRng};
use tiny_keccak::{Hasher, Keccak};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE,
scalar::Scalar,
edwards::{EdwardsPoint, EdwardsBasepointTable, CompressedEdwardsY}
};
use monero::util::key;
#[cfg(feature = "multisig")]
pub mod frost;
pub mod key_image;
pub mod clsag;
#[link(name = "wrapper")]
extern "C" {
fn c_hash_to_point(point: *const u8);
pub(crate) fn c_verify_clsag(
serialized_len: usize, serialized: *const u8, I: *const u8,
ring_size: u8, ring: *const u8, msg: *const u8, pseudo_out: *const u8
) -> bool;
}
#[derive(Error, Debug)]
pub enum SignError {
#[error("internal error ({0})")]
InternalError(String),
#[error("invalid discrete log equality proof")]
InvalidDLEqProof,
#[error("invalid key image {0}")]
InvalidKeyImage(usize),
#[error("invalid ring member (member {0}, ring size {1})")]
InvalidRingMember(u8, u8),
#[error("invalid secret for ring (index {0})")]
InvalidSecret(u8),
#[error("invalid commitment {0}")]
InvalidCommitment(usize),
#[error("invalid share {0}")]
InvalidShare(usize),
#[error("invalid signature")]
InvalidSignature
}
// Allows using a modern rand as dalek's is notoriously dated
pub fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
let mut r = [0; 64];
rng.fill_bytes(&mut r);
Scalar::from_bytes_mod_order_wide(&r)
}
lazy_static! {
static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&key::H.point.decompress().unwrap());
}
// aG + bH
pub fn commitment(randomness: &Scalar, amount: u64) -> EdwardsPoint {
(randomness * &ED25519_BASEPOINT_TABLE) + (&Scalar::from(amount) * &*H_TABLE)
}
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
let mut keccak = Keccak::v256();
keccak.update(data);
let mut res = [0; 32];
keccak.finalize(&mut res);
Scalar::from_bytes_mod_order(res)
}
pub fn hash_to_point(point: &EdwardsPoint) -> EdwardsPoint {
let mut bytes = point.compress().to_bytes();
unsafe {
c_hash_to_point(bytes.as_mut_ptr());
}
CompressedEdwardsY::from_slice(&bytes).decompress().unwrap()
}

View File

@@ -1,155 +0,0 @@
use rand::{RngCore, rngs::OsRng};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
use monero_sign::{SignError, random_scalar, commitment, key_image, clsag};
#[cfg(feature = "multisig")]
use ::frost::sign;
#[cfg(feature = "multisig")]
mod frost;
#[cfg(feature = "multisig")]
use crate::frost::generate_keys;
#[cfg(feature = "multisig")]
const THRESHOLD: usize = 5;
#[cfg(feature = "multisig")]
const PARTICIPANTS: usize = 8;
const RING_INDEX: u8 = 3;
const RING_LEN: u64 = 11;
const AMOUNT: u64 = 1337;
#[test]
fn test_single() -> Result<(), SignError> {
let msg = [1; 32];
let mut secrets = [Scalar::zero(), Scalar::zero()];
let mut ring = vec![];
for i in 0 .. RING_LEN {
let dest = random_scalar(&mut OsRng);
let a = random_scalar(&mut OsRng);
let amount;
if i == RING_INDEX.into() {
secrets = [dest, a];
amount = AMOUNT;
} else {
amount = OsRng.next_u64();
}
let mask = commitment(&a, amount);
ring.push([&dest * &ED25519_BASEPOINT_TABLE, mask]);
}
let image = key_image::single(&secrets[0]);
let (clsag, pseudo_out) = clsag::sign(
&mut OsRng,
image,
msg,
ring.clone(),
RING_INDEX,
&secrets[0],
&secrets[1],
AMOUNT
)?;
clsag::verify(&clsag, image, &msg, &ring, pseudo_out)?;
Ok(())
}
#[cfg(feature = "multisig")]
#[test]
fn test_multisig() -> Result<(), SignError> {
let (keys, group_private) = generate_keys(THRESHOLD, PARTICIPANTS);
let t = keys[0].params().t();
let mut images = vec![];
images.resize(PARTICIPANTS + 1, None);
let included = (1 ..= THRESHOLD).collect::<Vec<usize>>();
for i in &included {
let i = *i;
images[i] = Some(
(
keys[0].verification_shares()[i].0,
key_image::multisig(&mut OsRng, &keys[i - 1], &included)
)
);
}
let msg = [1; 32];
images.push(None);
let ki_used = images.swap_remove(1).unwrap().1;
let image = ki_used.resolve(images).unwrap();
let randomness = random_scalar(&mut OsRng);
let mut ring = vec![];
for i in 0 .. RING_LEN {
let dest;
let a;
let amount;
if i != RING_INDEX.into() {
dest = random_scalar(&mut OsRng);
a = random_scalar(&mut OsRng);
amount = OsRng.next_u64();
} else {
dest = group_private.0;
a = randomness;
amount = AMOUNT;
}
let mask = commitment(&a, amount);
ring.push([&dest * &ED25519_BASEPOINT_TABLE, mask]);
}
let mut machines = vec![];
let mut commitments = Vec::with_capacity(PARTICIPANTS + 1);
commitments.resize(PARTICIPANTS + 1, None);
for i in 1 ..= t {
machines.push(
sign::StateMachine::new(
sign::Params::new(
clsag::Multisig::new(image, msg, ring.clone(), RING_INDEX, &randomness, AMOUNT).unwrap(),
keys[i - 1].clone(),
&(1 ..= t).collect::<Vec<usize>>()
).unwrap()
)
);
commitments[i] = Some(machines[i - 1].preprocess(&mut OsRng).unwrap());
}
let mut shares = Vec::with_capacity(PARTICIPANTS + 1);
shares.resize(PARTICIPANTS + 1, None);
for i in 1 ..= t {
shares[i] = Some(
machines[i - 1].sign(
&commitments
.iter()
.enumerate()
.map(|(idx, value)| if idx == i { None } else { value.to_owned() })
.collect::<Vec<Option<Vec<u8>>>>(),
&vec![]
).unwrap()
);
}
let mut signature = None;
for i in 1 ..= t {
// Multisig does call verify to ensure integrity upon complete, before checking individual key
// shares. For FROST Schnorr, it's cheaper. For CLSAG, it may be more expensive? Yet it ensures
// we have usable signatures, not just signatures we think are usable
let sig = machines[i - 1].complete(
&shares
.iter()
.enumerate()
.map(|(idx, value)| if idx == i { None } else { value.to_owned() })
.collect::<Vec<Option<Vec<u8>>>>()
).unwrap();
if signature.is_none() {
signature = Some(sig.clone());
}
// Check the commitment out and the non-decoy s scalar are identical to every other signature
assert_eq!(sig.1, signature.as_ref().unwrap().1);
assert_eq!(sig.0.s[RING_INDEX as usize], signature.as_ref().unwrap().0.s[RING_INDEX as usize]);
}
Ok(())
}

View File

@@ -1,65 +0,0 @@
#![cfg(feature = "multisig")]
use std::rc::Rc;
use rand::rngs::OsRng;
use ff::Field;
use dalek_ff_group::{ED25519_BASEPOINT_TABLE, Scalar};
use frost::{
MultisigParams, MultisigKeys,
key_gen,
sign::lagrange
};
use monero_sign::frost::Ed25519;
pub fn generate_keys(t: usize, n: usize) -> (Vec<Rc<MultisigKeys<Ed25519>>>, Scalar) {
let mut params = vec![];
let mut machines = vec![];
let mut commitments = vec![vec![]];
for i in 1 ..= n {
params.push(
MultisigParams::new(t, n, i).unwrap()
);
machines.push(
key_gen::StateMachine::<Ed25519>::new(
params[i - 1],
"monero-sign-rs test suite".to_string()
)
);
commitments.push(machines[i - 1].generate_coefficients(&mut OsRng).unwrap());
}
let mut secret_shares = vec![];
for i in 1 ..= n {
secret_shares.push(
machines[i - 1].generate_secret_shares(
&mut OsRng,
commitments
.iter()
.enumerate()
.map(|(idx, commitments)| if idx == i { vec![] } else { commitments.to_vec() })
.collect()
).unwrap()
);
}
let mut keys = vec![];
for i in 1 ..= n {
let mut our_secret_shares = vec![vec![]];
our_secret_shares.extend(
secret_shares.iter().map(|shares| shares[i].clone()).collect::<Vec<Vec<u8>>>()
);
keys.push(Rc::new(machines[i - 1].complete(our_secret_shares).unwrap().clone()));
}
let mut group_private = Scalar::zero();
for i in 0 .. t {
group_private += keys[i].secret_share() * lagrange::<Scalar>(i + 1, &(1 ..= t).collect::<Vec<usize>>());
}
assert_eq!(&ED25519_BASEPOINT_TABLE * group_private, keys[0].group_key());
(keys, group_private)
}

View File

@@ -1,36 +0,0 @@
#![cfg(feature = "multisig")]
use rand::rngs::OsRng;
use monero_sign::{SignError, key_image};
mod frost;
use crate::frost::generate_keys;
#[test]
fn test() -> Result<(), SignError> {
let (keys, group_private) = generate_keys(3, 5);
let image = key_image::single(&group_private);
let mut packages = vec![];
packages.resize(5 + 1, None);
let included = vec![1, 3, 4];
for i in &included {
let i = *i;
packages[i] = Some(
(
keys[0].verification_shares()[i].0,
key_image::multisig(&mut OsRng, &keys[i - 1], &included)
)
);
}
for i in included {
let mut packages = packages.clone();
packages.push(None);
let package = packages.swap_remove(i).unwrap().1;
assert_eq!(image, package.resolve(packages).unwrap());
}
Ok(())
}