Remove Monero as a dependency

Introduces missing CLSAG checks. The only difference now should be the 
additional rejection of torsioned points, which is relevant to 
https://github.com/serai-dex/serai/issues/25. Considering this is only 
currently used for FROST verification, this should be fine.

Closes https://github.com/serai-dex/serai/issues/19 by making it 
irrelevant.

Increases priority of https://github.com/serai-dex/serai/issues/68, as 
now it's used for the BP generators which are done at first-proof.

Also merges BP's stricter hash_to_point with the library's, since CLSAG 
has the same bound.
This commit is contained in:
Luke Parker 2022-07-26 03:25:57 -04:00
parent ee29f6d6d8
commit 696da8228e
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
14 changed files with 33 additions and 403 deletions

1
Cargo.lock generated
View file

@ -4569,7 +4569,6 @@ version = "0.1.0"
dependencies = [
"base58-monero 1.0.0",
"blake2",
"cc",
"curve25519-dalek 3.2.0",
"dalek-ff-group",
"dleq-serai",

View file

@ -6,9 +6,6 @@ license = "MIT"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
[build-dependencies]
cc = "1.0"
[dependencies]
hex-literal = "0.3"
lazy_static = "1"
@ -45,7 +42,6 @@ monero = "0.16"
reqwest = { version = "0.11", features = ["json"] }
[features]
experimental = []
multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"]
[dev-dependencies]

View file

@ -1,52 +0,0 @@
use std::process::Command;
fn main() {
if !Command::new("git")
.args(&["submodule", "update", "--init", "--recursive"])
.status()
.unwrap()
.success()
{
panic!("git failed to init submodules");
}
println!("cargo:rerun-if-changed=c/wrapper.cpp");
#[rustfmt::skip]
cc::Build::new()
.static_flag(true)
.warnings(false)
.extra_warnings(false)
.flag("-Wno-deprecated-declarations")
.include("c/monero/external/supercop/include")
.include("c/monero/contrib/epee/include")
.include("c/monero/src")
.include("c/monero/build/release/generated_include")
.define("AUTO_INITIALIZE_EASYLOGGINGPP", None)
.include("c/monero/external/easylogging++")
.file("c/monero/external/easylogging++/easylogging++.cc")
.file("c/monero/src/common/aligned.c")
.file("c/monero/src/common/perf_timer.cpp")
.include("c/monero/src/crypto")
.file("c/monero/src/crypto/crypto-ops-data.c")
.file("c/monero/src/crypto/crypto-ops.c")
.file("c/monero/src/crypto/keccak.c")
.file("c/monero/src/crypto/hash.c")
.include("c/monero/src/ringct")
.file("c/monero/src/ringct/rctCryptoOps.c")
.file("c/monero/src/ringct/rctTypes.cpp")
.file("c/monero/src/ringct/rctOps.cpp")
.file("c/monero/src/ringct/multiexp.cc")
.file("c/monero/src/ringct/bulletproofs.cc")
.file("c/monero/src/ringct/rctSigs.cpp")
.file("c/wrapper.cpp")
.compile("wrapper");
println!("cargo:rustc-link-lib=wrapper");
println!("cargo:rustc-link-lib=stdc++");
}

@ -1 +0,0 @@
Subproject commit 424e4de16b98506170db7b0d7d87a79ccf541744

View file

@ -1,156 +0,0 @@
#include <mutex>
#include "ringct/bulletproofs.h"
#include "ringct/rctSigs.h"
typedef std::lock_guard<std::mutex> lock;
std::mutex rng_mutex;
uint8_t rng_entropy[64];
extern "C" {
void rng(uint8_t* seed) {
// Set the first half to the seed
memcpy(rng_entropy, seed, 32);
// Set the second half to the hash of a DST to ensure a lack of collisions
crypto::cn_fast_hash("RNG_entropy_seed", 16, (char*) &rng_entropy[32]);
}
}
extern "C" void monero_wide_reduce(uint8_t* value);
namespace crypto {
void generate_random_bytes_not_thread_safe(size_t n, void* value) {
size_t written = 0;
while (written != n) {
uint8_t hash[32];
crypto::cn_fast_hash(rng_entropy, 64, (char*) hash);
// Step the RNG by setting the latter half to the most recent result
// Does not leak the RNG, even if the values are leaked (which they are
// expected to be) due to the first half remaining constant and
// undisclosed
memcpy(&rng_entropy[32], hash, 32);
size_t next = n - written;
if (next > 32) {
next = 32;
}
memcpy(&((uint8_t*) value)[written], hash, next);
written += next;
}
}
void random32_unbiased(unsigned char *bytes) {
uint8_t value[64];
generate_random_bytes_not_thread_safe(64, value);
monero_wide_reduce(value);
memcpy(bytes, value, 32);
}
}
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);
}
uint8_t* c_generate_bp(uint8_t* seed, uint8_t len, uint64_t* a, uint8_t* m) {
lock guard(rng_mutex);
rng(seed);
rct::keyV masks;
std::vector<uint64_t> amounts;
masks.resize(len);
amounts.resize(len);
for (uint8_t i = 0; i < len; i++) {
memcpy(masks[i].bytes, m + (i * 32), 32);
amounts[i] = a[i];
}
rct::Bulletproof bp = rct::bulletproof_PROVE(amounts, masks);
std::stringstream ss;
binary_archive<true> ba(ss);
::serialization::serialize(ba, bp);
uint8_t* res = (uint8_t*) calloc(ss.str().size(), 1);
memcpy(res, ss.str().data(), ss.str().size());
return res;
}
bool c_verify_bp(
uint8_t* seed,
uint s_len,
uint8_t* s,
uint8_t c_len,
uint8_t* c
) {
// BPs are batch verified which use RNG based weights to ensure individual
// integrity
// That's why this must also have control over RNG, to prevent interrupting
// multisig signing while not using known seeds. Considering this doesn't
// actually define a batch, and it's only verifying a single BP,
// it'd probably be fine, but...
lock guard(rng_mutex);
rng(seed);
rct::Bulletproof bp;
std::stringstream ss;
std::string str;
str.assign((char*) s, (size_t) s_len);
ss << str;
binary_archive<false> ba(ss);
::serialization::serialize(ba, bp);
if (!ss.good()) {
return false;
}
bp.V.resize(c_len);
for (uint8_t i = 0; i < c_len; i++) {
memcpy(bp.V[i].bytes, &c[i * 32], 32);
}
try { return rct::bulletproof_VERIFY(bp); } catch(...) { return false; }
}
bool c_verify_clsag(
uint s_len,
uint8_t* s,
uint8_t k_len,
uint8_t* k,
uint8_t* I,
uint8_t* p,
uint8_t* m
) {
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;
}
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);
}
memcpy(clsag.I.bytes, I, 32);
rct::key pseudo_out;
memcpy(pseudo_out.bytes, p, 32);
rct::key msg;
memcpy(msg.bytes, m, 32);
try {
return verRctCLSAGSimple(msg, clsag, keys, pseudo_out);
} catch(...) { return false; }
}
}

View file

@ -1,10 +1,6 @@
use std::slice;
use lazy_static::lazy_static;
use rand_core::{RngCore, CryptoRng};
use subtle::ConstantTimeEq;
use tiny_keccak::{Hasher, Keccak};
use curve25519_dalek::{
@ -38,26 +34,6 @@ lazy_static! {
static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&H);
}
// Function from libsodium our subsection of Monero relies on. Implementing it here means we don't
// need to link against libsodium
#[no_mangle]
unsafe extern "C" fn crypto_verify_32(a: *const u8, b: *const u8) -> isize {
isize::from(slice::from_raw_parts(a, 32).ct_eq(slice::from_raw_parts(b, 32)).unwrap_u8()) - 1
}
// Offer a wide reduction to C. Our seeded RNG prevented Monero from defining an unbiased scalar
// generation function, and in order to not use Monero code (which would require propagating its
// license), the function was rewritten. It was rewritten with wide reduction, instead of rejection
// sampling however, hence the need for this function
#[no_mangle]
unsafe extern "C" fn monero_wide_reduce(value: *mut u8) {
let res =
Scalar::from_bytes_mod_order_wide(std::slice::from_raw_parts(value, 64).try_into().unwrap());
for (i, b) in res.to_bytes().iter().enumerate() {
value.add(i).write(*b);
}
}
#[allow(non_snake_case)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Commitment {
@ -95,5 +71,11 @@ pub fn hash(data: &[u8]) -> [u8; 32] {
}
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
Scalar::from_bytes_mod_order(hash(data))
let scalar = Scalar::from_bytes_mod_order(hash(data));
// Monero will explicitly error in this case
// This library acknowledges its practical impossibility of it occurring, and doesn't bother to
// code in logic to handle it. That said, if it ever occurs, something must happen in order to
// not generate/verify a proof we believe to be valid when it isn't
assert!(scalar != Scalar::zero(), "ZERO HASH: {:?}", data);
scalar
}

View file

@ -28,13 +28,7 @@ fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
}
fn hash_to_scalar(data: &[u8]) -> Scalar {
let scalar = Scalar(dalek_hash(data));
// Monero will explicitly retry on these cases, as them occurring breaks the proof
// This library acknowledges their practical impossibility of them occurring, and doesn't bother
// to code in logic to handle it. That said, if they ever occur, something must happen in order
// to not generate a proof we believe to be valid when it isn't
assert!(!bool::from(scalar.is_zero()), "ZERO HASH: {:?}", data);
scalar
Scalar(dalek_hash(data))
}
fn generator(i: usize) -> EdwardsPoint {

View file

@ -54,44 +54,6 @@ impl Bulletproofs {
Ok(prove(rng, outputs))
}
#[must_use]
pub fn verify<R: RngCore + CryptoRng>(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool {
if commitments.len() > 16 {
return false;
}
let mut seed = [0; 32];
rng.fill_bytes(&mut seed);
let mut serialized = Vec::with_capacity((9 + (2 * self.L.len())) * 32);
self.serialize(&mut serialized).unwrap();
let commitments: Vec<[u8; 32]> = commitments
.iter()
.map(|commitment| (commitment * Scalar::from(8u8).invert()).compress().to_bytes())
.collect();
unsafe {
#[link(name = "wrapper")]
extern "C" {
fn c_verify_bp(
seed: *const u8,
serialized_len: usize,
serialized: *const u8,
commitments_len: u8,
commitments: *const [u8; 32],
) -> bool;
}
c_verify_bp(
seed.as_ptr(),
serialized.len(),
serialized.as_ptr(),
u8::try_from(commitments.len()).unwrap(),
commitments.as_ptr(),
)
}
}
fn serialize_core<W: std::io::Write, F: Fn(&[EdwardsPoint], &mut W) -> std::io::Result<()>>(
&self,
w: &mut W,

View file

@ -7,7 +7,7 @@ use rand_core::{RngCore, CryptoRng};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE,
scalar::Scalar,
traits::VartimePrecomputedMultiscalarMul,
traits::{IsIdentity, VartimePrecomputedMultiscalarMul},
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
};
@ -29,10 +29,14 @@ lazy_static! {
pub enum ClsagError {
#[error("internal error ({0})")]
InternalError(String),
#[error("invalid ring")]
InvalidRing,
#[error("invalid ring member (member {0}, ring size {1})")]
InvalidRingMember(u8, u8),
#[error("invalid commitment")]
InvalidCommitment,
#[error("invalid key image")]
InvalidImage,
#[error("invalid D")]
InvalidD,
#[error("invalid s")]
@ -72,7 +76,6 @@ impl ClsagInput {
#[allow(clippy::large_enum_variant)]
enum Mode {
Sign(usize, EdwardsPoint, EdwardsPoint),
#[cfg(feature = "experimental")]
Verify(Scalar),
}
@ -150,7 +153,6 @@ fn core(
c = hash_to_scalar(&to_hash);
}
#[cfg(feature = "experimental")]
Mode::Verify(c1) => {
start = 0;
end = n;
@ -259,17 +261,31 @@ impl Clsag {
res
}
// Not extensively tested nor guaranteed to have expected parity with Monero
#[cfg(feature = "experimental")]
pub fn rust_verify(
pub fn verify(
&self,
ring: &[[EdwardsPoint; 2]],
I: &EdwardsPoint,
pseudo_out: &EdwardsPoint,
msg: &[u8; 32],
) -> Result<(), ClsagError> {
let (_, c1) =
core(ring, I, pseudo_out, msg, &self.D.mul_by_cofactor(), &self.s, Mode::Verify(self.c1));
// Preliminary checks. s, c1, and points must also be encoded canonically, which isn't checked
// here
if ring.len() == 0 {
Err(ClsagError::InvalidRing)?;
}
if ring.len() != self.s.len() {
Err(ClsagError::InvalidS)?;
}
if I.is_identity() {
Err(ClsagError::InvalidImage)?;
}
let D = self.D.mul_by_cofactor();
if D.is_identity() {
Err(ClsagError::InvalidD)?;
}
let (_, c1) = core(ring, I, pseudo_out, msg, &D, &self.s, Mode::Verify(self.c1));
if c1 != self.c1 {
Err(ClsagError::InvalidC1)?;
}
@ -289,58 +305,4 @@ impl Clsag {
pub fn deserialize<R: std::io::Read>(decoys: usize, r: &mut R) -> std::io::Result<Clsag> {
Ok(Clsag { s: read_raw_vec(read_scalar, decoys, r)?, c1: read_scalar(r)?, D: read_point(r)? })
}
pub fn verify(
&self,
ring: &[[EdwardsPoint; 2]],
I: &EdwardsPoint,
pseudo_out: &EdwardsPoint,
msg: &[u8; 32],
) -> Result<(), ClsagError> {
// Serialize it to pass the struct to Monero without extensive FFI
let mut serialized = Vec::with_capacity(1 + ((self.s.len() + 2) * 32));
write_varint(&self.s.len().try_into().unwrap(), &mut serialized).unwrap();
self.serialize(&mut serialized).unwrap();
let I_bytes = I.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();
unsafe {
// Uses Monero's C verification function to ensure compatibility with Monero
#[link(name = "wrapper")]
extern "C" {
pub(crate) fn c_verify_clsag(
serialized_len: usize,
serialized: *const u8,
ring_size: u8,
ring: *const u8,
I: *const u8,
pseudo_out: *const u8,
msg: *const u8,
) -> bool;
}
if c_verify_clsag(
serialized.len(),
serialized.as_ptr(),
u8::try_from(ring.len())
.map_err(|_| ClsagError::InternalError("too large ring".to_string()))?,
ring_bytes.as_ptr(),
I_bytes.as_ptr(),
pseudo_out_bytes.as_ptr(),
msg.as_ptr(),
) {
Ok(())
} else {
Err(ClsagError::InvalidC1)
}
}
}
}

View file

@ -7,23 +7,8 @@ use dalek_ff_group::field::FieldElement;
use crate::hash;
pub(crate) fn raw_hash_to_point(mut bytes: [u8; 32]) -> EdwardsPoint {
unsafe {
#[link(name = "wrapper")]
extern "C" {
fn c_hash_to_point(key: *const u8);
}
c_hash_to_point(bytes.as_mut_ptr());
}
CompressedEdwardsY::from_slice(&bytes).decompress().unwrap()
}
// This works without issue. It's also 140 times slower (@ 3.5ms), and despite checking it passes
// for all branches, there still could be *some* discrepancy somewhere. There's no reason to use it
// unless we're trying to purge that section of the C static library, which we aren't right now
#[allow(dead_code)]
pub(crate) fn rust_hash_to_point(bytes: [u8; 32]) -> EdwardsPoint {
pub(crate) fn raw_hash_to_point(bytes: [u8; 32]) -> EdwardsPoint {
#[allow(non_snake_case)]
let A = FieldElement::from(486662u64);

View file

@ -1,21 +0,0 @@
use rand::rngs::OsRng;
use crate::{Commitment, random_scalar, ringct::bulletproofs::Bulletproofs};
#[test]
fn bulletproofs() {
// Create Bulletproofs for all possible output quantities
for i in 1 .. 17 {
let commitments =
(1 ..= i).map(|i| Commitment::new(random_scalar(&mut OsRng), i)).collect::<Vec<_>>();
assert!(Bulletproofs::new(&mut OsRng, &commitments)
.unwrap()
.verify(&mut OsRng, &commitments.iter().map(Commitment::calculate).collect::<Vec<_>>()));
}
// Check it errors if we try to create too many
assert!(
Bulletproofs::new(&mut OsRng, &[Commitment::new(random_scalar(&mut OsRng), 1); 17]).is_err()
);
}

View file

@ -74,8 +74,6 @@ fn clsag() {
)
.swap_remove(0);
clsag.verify(&ring, &image, &pseudo_out, &msg).unwrap();
#[cfg(feature = "experimental")]
clsag.rust_verify(&ring, &image, &pseudo_out, &msg).unwrap();
}
}

View file

@ -1,16 +0,0 @@
use rand::rngs::OsRng;
use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
use crate::{
random_scalar,
ringct::hash_to_point::{hash_to_point as c_hash_to_point, rust_hash_to_point},
};
#[test]
fn hash_to_point() {
for _ in 0 .. 50 {
let point = &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE;
assert_eq!(rust_hash_to_point(point.compress().to_bytes()), c_hash_to_point(point));
}
}

View file

@ -1,4 +1,2 @@
mod hash_to_point;
mod clsag;
mod bulletproofs;
mod address;