mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Remove dleq, dkg-promote, dkg-pedpop per #597
Does not move them to a new repository at this time.
This commit is contained in:
7
.github/workflows/crypto-tests.yml
vendored
7
.github/workflows/crypto-tests.yml
vendored
@@ -35,17 +35,10 @@ jobs:
|
|||||||
-p ciphersuite-kp256 \
|
-p ciphersuite-kp256 \
|
||||||
-p multiexp \
|
-p multiexp \
|
||||||
-p schnorr-signatures \
|
-p schnorr-signatures \
|
||||||
-p dleq \
|
|
||||||
-p generalized-bulletproofs \
|
|
||||||
-p generalized-bulletproofs-circuit-abstraction \
|
|
||||||
-p ec-divisors \
|
|
||||||
-p generalized-bulletproofs-ec-gadgets \
|
|
||||||
-p dkg \
|
-p dkg \
|
||||||
-p dkg-recovery \
|
-p dkg-recovery \
|
||||||
-p dkg-dealer \
|
-p dkg-dealer \
|
||||||
-p dkg-promote \
|
|
||||||
-p dkg-musig \
|
-p dkg-musig \
|
||||||
-p dkg-pedpop \
|
|
||||||
-p dkg-evrf \
|
-p dkg-evrf \
|
||||||
-p modular-frost \
|
-p modular-frost \
|
||||||
-p frost-schnorrkel
|
-p frost-schnorrkel
|
||||||
|
|||||||
62
Cargo.lock
generated
62
Cargo.lock
generated
@@ -1562,15 +1562,6 @@ dependencies = [
|
|||||||
"generic-array 0.14.7",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block-padding"
|
|
||||||
version = "0.3.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array 0.14.7",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bls12_381"
|
name = "bls12_381"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -2801,38 +2792,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dkg-pedpop"
|
|
||||||
version = "0.6.0"
|
|
||||||
dependencies = [
|
|
||||||
"chacha20",
|
|
||||||
"ciphersuite 0.4.2",
|
|
||||||
"dalek-ff-group",
|
|
||||||
"dkg",
|
|
||||||
"dleq",
|
|
||||||
"flexible-transcript",
|
|
||||||
"multiexp",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
"schnorr-signatures",
|
|
||||||
"thiserror 2.0.16",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dkg-promote"
|
|
||||||
version = "0.6.1"
|
|
||||||
dependencies = [
|
|
||||||
"ciphersuite 0.4.2",
|
|
||||||
"dalek-ff-group",
|
|
||||||
"dkg",
|
|
||||||
"dkg-recovery",
|
|
||||||
"dleq",
|
|
||||||
"flexible-transcript",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
"thiserror 2.0.16",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dkg-recovery"
|
name = "dkg-recovery"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -2843,25 +2802,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dleq"
|
|
||||||
version = "0.4.1"
|
|
||||||
dependencies = [
|
|
||||||
"blake2",
|
|
||||||
"dalek-ff-group",
|
|
||||||
"digest 0.10.7",
|
|
||||||
"ff",
|
|
||||||
"flexible-transcript",
|
|
||||||
"group",
|
|
||||||
"hex-literal",
|
|
||||||
"k256",
|
|
||||||
"multiexp",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
"rustversion",
|
|
||||||
"thiserror 2.0.16",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dockertest"
|
name = "dockertest"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -4574,7 +4514,6 @@ version = "0.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-padding",
|
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -10259,7 +10198,6 @@ dependencies = [
|
|||||||
"ciphersuite 0.4.2",
|
"ciphersuite 0.4.2",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
"dkg",
|
"dkg",
|
||||||
"dleq",
|
|
||||||
"embedwards25519",
|
"embedwards25519",
|
||||||
"flexible-transcript",
|
"flexible-transcript",
|
||||||
"minimal-ed448",
|
"minimal-ed448",
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ members = [
|
|||||||
|
|
||||||
"crypto/multiexp",
|
"crypto/multiexp",
|
||||||
"crypto/schnorr",
|
"crypto/schnorr",
|
||||||
"crypto/dleq",
|
|
||||||
|
|
||||||
"crypto/evrf/secq256k1",
|
"crypto/evrf/secq256k1",
|
||||||
"crypto/evrf/embedwards25519",
|
"crypto/evrf/embedwards25519",
|
||||||
@@ -41,9 +40,7 @@ members = [
|
|||||||
"crypto/dkg",
|
"crypto/dkg",
|
||||||
"crypto/dkg/recovery",
|
"crypto/dkg/recovery",
|
||||||
"crypto/dkg/dealer",
|
"crypto/dkg/dealer",
|
||||||
"crypto/dkg/promote",
|
|
||||||
"crypto/dkg/musig",
|
"crypto/dkg/musig",
|
||||||
"crypto/dkg/pedpop",
|
|
||||||
"crypto/dkg/evrf",
|
"crypto/dkg/evrf",
|
||||||
"crypto/frost",
|
"crypto/frost",
|
||||||
"crypto/schnorrkel",
|
"crypto/schnorrkel",
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "dkg-pedpop"
|
|
||||||
version = "0.6.0"
|
|
||||||
description = "The PedPoP distributed key generation protocol"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/pedpop"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
thiserror = { version = "2", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["std", "zeroize_derive"] }
|
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../transcript", version = "^0.3.3", default-features = false, features = ["std", "recommended"] }
|
|
||||||
chacha20 = { version = "0.9", default-features = false, features = ["std", "zeroize"] }
|
|
||||||
|
|
||||||
multiexp = { path = "../../multiexp", version = "0.4", default-features = false, features = ["std"] }
|
|
||||||
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false, features = ["std"] }
|
|
||||||
schnorr = { package = "schnorr-signatures", path = "../../schnorr", version = "^0.5.1", default-features = false, features = ["std"] }
|
|
||||||
dleq = { path = "../../dleq", version = "^0.4.1", default-features = false, features = ["std", "serialize"] }
|
|
||||||
|
|
||||||
dkg = { path = "../", version = "0.6", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
|
||||||
dalek-ff-group = { path = "../../dalek-ff-group", default-features = false }
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021-2025 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.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Distributed Key Generation - PedPoP
|
|
||||||
|
|
||||||
This implements the PedPoP distributed key generation protocol for the
|
|
||||||
[`dkg`](https://docs.rs/dkg) crate's types.
|
|
||||||
|
|
||||||
This crate was originally part of the `dkg` crate, which was
|
|
||||||
[audited by Cypher Stack in March 2023](
|
|
||||||
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
|
||||||
), culminating in commit
|
|
||||||
[669d2dbffc1dafb82a09d9419ea182667115df06](
|
|
||||||
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
|
||||||
). Any subsequent changes have not undergone auditing.
|
|
||||||
@@ -1,506 +0,0 @@
|
|||||||
use core::{ops::Deref, fmt};
|
|
||||||
use std::{io, collections::HashMap};
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use chacha20::{
|
|
||||||
cipher::{crypto_common::KeyIvInit, StreamCipher},
|
|
||||||
Key as Cc20Key, Nonce as Cc20Iv, ChaCha20,
|
|
||||||
};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use ciphersuite::group::ff::Field;
|
|
||||||
use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
|
||||||
use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
use schnorr::SchnorrSignature;
|
|
||||||
use dleq::DLEqProof;
|
|
||||||
|
|
||||||
use dkg::{Participant, ThresholdParams};
|
|
||||||
|
|
||||||
mod sealed {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub trait ReadWrite: Sized {
|
|
||||||
fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self>;
|
|
||||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()>;
|
|
||||||
|
|
||||||
fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut buf = vec![];
|
|
||||||
self.write(&mut buf).unwrap();
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Message: Clone + PartialEq + Eq + fmt::Debug + Zeroize + ReadWrite {}
|
|
||||||
impl<M: Clone + PartialEq + Eq + fmt::Debug + Zeroize + ReadWrite> Message for M {}
|
|
||||||
|
|
||||||
pub trait Encryptable: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite {}
|
|
||||||
impl<E: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite> Encryptable for E {}
|
|
||||||
}
|
|
||||||
pub(crate) use sealed::*;
|
|
||||||
|
|
||||||
/// Wraps a message with a key to use for encryption in the future.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct EncryptionKeyMessage<C: Ciphersuite, M: Message> {
|
|
||||||
msg: M,
|
|
||||||
enc_key: C::G,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Doesn't impl ReadWrite so that doesn't need to be imported
|
|
||||||
impl<C: Ciphersuite, M: Message> EncryptionKeyMessage<C, M> {
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
|
|
||||||
Ok(Self { msg: M::read(reader, params)?, enc_key: C::read_G(reader)? })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
self.msg.write(writer)?;
|
|
||||||
writer.write_all(self.enc_key.to_bytes().as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut buf = vec![];
|
|
||||||
self.write(&mut buf).unwrap();
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn enc_key(&self) -> C::G {
|
|
||||||
self.enc_key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An encrypted message, with a per-message encryption key enabling revealing specific messages
|
|
||||||
/// without side effects.
|
|
||||||
#[derive(Clone, Zeroize)]
|
|
||||||
pub struct EncryptedMessage<C: Ciphersuite, E: Encryptable> {
|
|
||||||
key: C::G,
|
|
||||||
// Also include a proof-of-possession for the key.
|
|
||||||
// If this proof-of-possession wasn't here, Eve could observe Alice encrypt to Bob with key X,
|
|
||||||
// then send Bob a message also claiming to use X.
|
|
||||||
// While Eve's message would fail to meaningfully decrypt, Bob would then use this to create a
|
|
||||||
// blame argument against Eve. When they do, they'd reveal bX, revealing Alice's message to Bob.
|
|
||||||
// This is a massive side effect which could break some protocols, in the worst case.
|
|
||||||
// While Eve can still reuse their own keys, causing Bob to leak all messages by revealing for
|
|
||||||
// any single one, that's effectively Eve revealing themselves, and not considered relevant.
|
|
||||||
pop: SchnorrSignature<C>,
|
|
||||||
msg: Zeroizing<E>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ecdh<C: Ciphersuite>(private: &Zeroizing<C::F>, public: C::G) -> Zeroizing<C::G> {
|
|
||||||
Zeroizing::new(public * private.deref())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Each ecdh must be distinct. Reuse of an ecdh for multiple ciphers will cause the messages to be
|
|
||||||
// leaked.
|
|
||||||
fn cipher<C: Ciphersuite>(context: [u8; 32], ecdh: &Zeroizing<C::G>) -> ChaCha20 {
|
|
||||||
// Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly
|
|
||||||
// TODO: https://github.com/serai-dex/serai/issues/151
|
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0.2");
|
|
||||||
transcript.append_message(b"context", context);
|
|
||||||
|
|
||||||
transcript.domain_separate(b"encryption_key");
|
|
||||||
|
|
||||||
let mut ecdh = ecdh.to_bytes();
|
|
||||||
transcript.append_message(b"shared_key", ecdh.as_ref());
|
|
||||||
ecdh.as_mut().zeroize();
|
|
||||||
|
|
||||||
let zeroize = |buf: &mut [u8]| buf.zeroize();
|
|
||||||
|
|
||||||
let mut key = Cc20Key::default();
|
|
||||||
let mut challenge = transcript.challenge(b"key");
|
|
||||||
key.copy_from_slice(&challenge[.. 32]);
|
|
||||||
zeroize(challenge.as_mut());
|
|
||||||
|
|
||||||
// Since the key is single-use, it doesn't matter what we use for the IV
|
|
||||||
// The issue is key + IV reuse. If we never reuse the key, we can't have the opportunity to
|
|
||||||
// reuse a nonce
|
|
||||||
// Use a static IV in acknowledgement of this
|
|
||||||
let mut iv = Cc20Iv::default();
|
|
||||||
// The \0 is to satisfy the length requirement (12), not to be null terminated
|
|
||||||
iv.copy_from_slice(b"DKG IV v0.2\0");
|
|
||||||
|
|
||||||
// ChaCha20 has the same commentary as the transcript regarding ZAlloc
|
|
||||||
// TODO: https://github.com/serai-dex/serai/issues/151
|
|
||||||
let res = ChaCha20::new(&key, &iv);
|
|
||||||
zeroize(key.as_mut());
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encrypt<R: RngCore + CryptoRng, C: Ciphersuite, E: Encryptable>(
|
|
||||||
rng: &mut R,
|
|
||||||
context: [u8; 32],
|
|
||||||
from: Participant,
|
|
||||||
to: C::G,
|
|
||||||
mut msg: Zeroizing<E>,
|
|
||||||
) -> EncryptedMessage<C, E> {
|
|
||||||
/*
|
|
||||||
The following code could be used to replace the requirement on an RNG here.
|
|
||||||
It's just currently not an issue to require taking in an RNG here.
|
|
||||||
let last = self.last_enc_key.to_bytes();
|
|
||||||
self.last_enc_key = C::hash_to_F(b"encryption_base", last.as_ref());
|
|
||||||
let key = C::hash_to_F(b"encryption_key", last.as_ref());
|
|
||||||
last.as_mut().zeroize();
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Generate a new key for this message, satisfying cipher's requirement of distinct keys per
|
|
||||||
// message, and enabling revealing this message without revealing any others
|
|
||||||
let key = Zeroizing::new(C::random_nonzero_F(rng));
|
|
||||||
cipher::<C>(context, &ecdh::<C>(&key, to)).apply_keystream(msg.as_mut().as_mut());
|
|
||||||
|
|
||||||
let pub_key = C::generator() * key.deref();
|
|
||||||
let nonce = Zeroizing::new(C::random_nonzero_F(rng));
|
|
||||||
let pub_nonce = C::generator() * nonce.deref();
|
|
||||||
EncryptedMessage {
|
|
||||||
key: pub_key,
|
|
||||||
pop: SchnorrSignature::sign(
|
|
||||||
&key,
|
|
||||||
nonce,
|
|
||||||
pop_challenge::<C>(context, pub_nonce, pub_key, from, msg.deref().as_ref()),
|
|
||||||
),
|
|
||||||
msg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
key: C::read_G(reader)?,
|
|
||||||
pop: SchnorrSignature::<C>::read(reader)?,
|
|
||||||
msg: Zeroizing::new(E::read(reader, params)?),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
writer.write_all(self.key.to_bytes().as_ref())?;
|
|
||||||
self.pop.write(writer)?;
|
|
||||||
self.msg.write(writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut buf = vec![];
|
|
||||||
self.write(&mut buf).unwrap();
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn invalidate_pop(&mut self) {
|
|
||||||
self.pop.s += C::F::ONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn invalidate_msg<R: RngCore + CryptoRng>(
|
|
||||||
&mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
context: [u8; 32],
|
|
||||||
from: Participant,
|
|
||||||
) {
|
|
||||||
// Invalidate the message by specifying a new key/Schnorr PoP
|
|
||||||
// This will cause all initial checks to pass, yet a decrypt to gibberish
|
|
||||||
let key = Zeroizing::new(C::random_nonzero_F(rng));
|
|
||||||
let pub_key = C::generator() * key.deref();
|
|
||||||
let nonce = Zeroizing::new(C::random_nonzero_F(rng));
|
|
||||||
let pub_nonce = C::generator() * nonce.deref();
|
|
||||||
self.key = pub_key;
|
|
||||||
self.pop = SchnorrSignature::sign(
|
|
||||||
&key,
|
|
||||||
nonce,
|
|
||||||
pop_challenge::<C>(context, pub_nonce, pub_key, from, self.msg.deref().as_ref()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumes the encrypted message is a secret share.
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn invalidate_share_serialization<R: RngCore + CryptoRng>(
|
|
||||||
&mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
context: [u8; 32],
|
|
||||||
from: Participant,
|
|
||||||
to: C::G,
|
|
||||||
) {
|
|
||||||
use ciphersuite::group::ff::PrimeField;
|
|
||||||
|
|
||||||
let mut repr = <C::F as PrimeField>::Repr::default();
|
|
||||||
for b in repr.as_mut() {
|
|
||||||
*b = 255;
|
|
||||||
}
|
|
||||||
// Tries to guarantee the above assumption.
|
|
||||||
assert_eq!(repr.as_ref().len(), self.msg.as_ref().len());
|
|
||||||
// Checks that this isn't over a field where this is somehow valid
|
|
||||||
assert!(!bool::from(C::F::from_repr(repr).is_some()));
|
|
||||||
|
|
||||||
self.msg.as_mut().as_mut().copy_from_slice(repr.as_ref());
|
|
||||||
*self = encrypt(rng, context, from, to, self.msg.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumes the encrypted message is a secret share.
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn invalidate_share_value<R: RngCore + CryptoRng>(
|
|
||||||
&mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
context: [u8; 32],
|
|
||||||
from: Participant,
|
|
||||||
to: C::G,
|
|
||||||
) {
|
|
||||||
use ciphersuite::group::ff::PrimeField;
|
|
||||||
|
|
||||||
// Assumes the share isn't randomly 1
|
|
||||||
let repr = C::F::ONE.to_repr();
|
|
||||||
self.msg.as_mut().as_mut().copy_from_slice(repr.as_ref());
|
|
||||||
*self = encrypt(rng, context, from, to, self.msg.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A proof that the provided encryption key is a legitimately derived shared key for some message.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct EncryptionKeyProof<C: Ciphersuite> {
|
|
||||||
key: Zeroizing<C::G>,
|
|
||||||
dleq: DLEqProof<C::G>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> EncryptionKeyProof<C> {
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
Ok(Self { key: Zeroizing::new(C::read_G(reader)?), dleq: DLEqProof::read(reader)? })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
writer.write_all(self.key.to_bytes().as_ref())?;
|
|
||||||
self.dleq.write(writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut buf = vec![];
|
|
||||||
self.write(&mut buf).unwrap();
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn invalidate_key(&mut self) {
|
|
||||||
*self.key += C::generator();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn invalidate_dleq(&mut self) {
|
|
||||||
let mut buf = vec![];
|
|
||||||
self.dleq.write(&mut buf).unwrap();
|
|
||||||
// Adds one to c since this is serialized c, s
|
|
||||||
// Adding one to c will leave a validly serialized c
|
|
||||||
// Adding one to s may leave an invalidly serialized s
|
|
||||||
buf[0] = buf[0].wrapping_add(1);
|
|
||||||
self.dleq = DLEqProof::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This doesn't need to take the msg. It just doesn't hurt as an extra layer.
|
|
||||||
// This still doesn't mean the DKG offers an authenticated channel. The per-message keys have no
|
|
||||||
// root of trust other than their existence in the assumed-to-exist external authenticated channel.
|
|
||||||
fn pop_challenge<C: Ciphersuite>(
|
|
||||||
context: [u8; 32],
|
|
||||||
nonce: C::G,
|
|
||||||
key: C::G,
|
|
||||||
sender: Participant,
|
|
||||||
msg: &[u8],
|
|
||||||
) -> C::F {
|
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Proof of Possession v0.2");
|
|
||||||
transcript.append_message(b"context", context);
|
|
||||||
|
|
||||||
transcript.domain_separate(b"proof_of_possession");
|
|
||||||
|
|
||||||
transcript.append_message(b"nonce", nonce.to_bytes());
|
|
||||||
transcript.append_message(b"key", key.to_bytes());
|
|
||||||
// This is sufficient to prevent the attack this is meant to stop
|
|
||||||
transcript.append_message(b"sender", sender.to_bytes());
|
|
||||||
// This, as written above, doesn't hurt
|
|
||||||
transcript.append_message(b"message", msg);
|
|
||||||
// While this is a PoK and a PoP, it's called a PoP here since the important part is its owner
|
|
||||||
// Elsewhere, where we use the term PoK, the important part is that it isn't some inverse, with
|
|
||||||
// an unknown to anyone discrete log, breaking the system
|
|
||||||
C::hash_to_F(b"DKG-encryption-proof_of_possession", &transcript.challenge(b"schnorr"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encryption_key_transcript(context: [u8; 32]) -> RecommendedTranscript {
|
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2");
|
|
||||||
transcript.append_message(b"context", context);
|
|
||||||
transcript
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
|
|
||||||
pub(crate) enum DecryptionError {
|
|
||||||
#[error("accused provided an invalid signature")]
|
|
||||||
InvalidSignature,
|
|
||||||
#[error("accuser provided an invalid decryption key")]
|
|
||||||
InvalidProof,
|
|
||||||
}
|
|
||||||
|
|
||||||
// A simple box for managing decryption.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct Decryption<C: Ciphersuite> {
|
|
||||||
context: [u8; 32],
|
|
||||||
enc_keys: HashMap<Participant, C::G>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> Decryption<C> {
|
|
||||||
pub(crate) fn new(context: [u8; 32]) -> Self {
|
|
||||||
Self { context, enc_keys: HashMap::new() }
|
|
||||||
}
|
|
||||||
pub(crate) fn register<M: Message>(
|
|
||||||
&mut self,
|
|
||||||
participant: Participant,
|
|
||||||
msg: EncryptionKeyMessage<C, M>,
|
|
||||||
) -> M {
|
|
||||||
assert!(
|
|
||||||
!self.enc_keys.contains_key(&participant),
|
|
||||||
"Re-registering encryption key for a participant"
|
|
||||||
);
|
|
||||||
self.enc_keys.insert(participant, msg.enc_key);
|
|
||||||
msg.msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a message, and the intended decryptor, and a proof for its key, decrypt the message.
|
|
||||||
// Returns None if the key was wrong.
|
|
||||||
pub(crate) fn decrypt_with_proof<E: Encryptable>(
|
|
||||||
&self,
|
|
||||||
from: Participant,
|
|
||||||
decryptor: Participant,
|
|
||||||
mut msg: EncryptedMessage<C, E>,
|
|
||||||
// There's no encryption key proof if the accusation is of an invalid signature
|
|
||||||
proof: Option<EncryptionKeyProof<C>>,
|
|
||||||
) -> Result<Zeroizing<E>, DecryptionError> {
|
|
||||||
if !msg.pop.verify(
|
|
||||||
msg.key,
|
|
||||||
pop_challenge::<C>(self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
|
|
||||||
) {
|
|
||||||
Err(DecryptionError::InvalidSignature)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(proof) = proof {
|
|
||||||
// Verify this is the decryption key for this message
|
|
||||||
proof
|
|
||||||
.dleq
|
|
||||||
.verify(
|
|
||||||
&mut encryption_key_transcript(self.context),
|
|
||||||
&[C::generator(), msg.key],
|
|
||||||
&[self.enc_keys[&decryptor], *proof.key],
|
|
||||||
)
|
|
||||||
.map_err(|_| DecryptionError::InvalidProof)?;
|
|
||||||
|
|
||||||
cipher::<C>(self.context, &proof.key).apply_keystream(msg.msg.as_mut().as_mut());
|
|
||||||
Ok(msg.msg)
|
|
||||||
} else {
|
|
||||||
Err(DecryptionError::InvalidProof)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A simple box for managing encryption.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct Encryption<C: Ciphersuite> {
|
|
||||||
context: [u8; 32],
|
|
||||||
i: Participant,
|
|
||||||
enc_key: Zeroizing<C::F>,
|
|
||||||
enc_pub_key: C::G,
|
|
||||||
decryption: Decryption<C>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> fmt::Debug for Encryption<C> {
|
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt
|
|
||||||
.debug_struct("Encryption")
|
|
||||||
.field("context", &self.context)
|
|
||||||
.field("i", &self.i)
|
|
||||||
.field("enc_pub_key", &self.enc_pub_key)
|
|
||||||
.field("decryption", &self.decryption)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> Zeroize for Encryption<C> {
|
|
||||||
fn zeroize(&mut self) {
|
|
||||||
self.enc_key.zeroize();
|
|
||||||
self.enc_pub_key.zeroize();
|
|
||||||
for (_, mut value) in self.decryption.enc_keys.drain() {
|
|
||||||
value.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> Encryption<C> {
|
|
||||||
pub(crate) fn new<R: RngCore + CryptoRng>(
|
|
||||||
context: [u8; 32],
|
|
||||||
i: Participant,
|
|
||||||
rng: &mut R,
|
|
||||||
) -> Self {
|
|
||||||
let enc_key = Zeroizing::new(C::random_nonzero_F(rng));
|
|
||||||
Self {
|
|
||||||
context,
|
|
||||||
i,
|
|
||||||
enc_pub_key: C::generator() * enc_key.deref(),
|
|
||||||
enc_key,
|
|
||||||
decryption: Decryption::new(context),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
|
|
||||||
EncryptionKeyMessage { msg, enc_key: self.enc_pub_key }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn register<M: Message>(
|
|
||||||
&mut self,
|
|
||||||
participant: Participant,
|
|
||||||
msg: EncryptionKeyMessage<C, M>,
|
|
||||||
) -> M {
|
|
||||||
self.decryption.register(participant, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn encrypt<R: RngCore + CryptoRng, E: Encryptable>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
participant: Participant,
|
|
||||||
msg: Zeroizing<E>,
|
|
||||||
) -> EncryptedMessage<C, E> {
|
|
||||||
encrypt(rng, self.context, self.i, self.decryption.enc_keys[&participant], msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn decrypt<R: RngCore + CryptoRng, I: Copy + Zeroize, E: Encryptable>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
batch: &mut BatchVerifier<I, C::G>,
|
|
||||||
// Uses a distinct batch ID so if this batch verifier is reused, we know its the PoP aspect
|
|
||||||
// which failed, and therefore to use None for the blame
|
|
||||||
batch_id: I,
|
|
||||||
from: Participant,
|
|
||||||
mut msg: EncryptedMessage<C, E>,
|
|
||||||
) -> (Zeroizing<E>, EncryptionKeyProof<C>) {
|
|
||||||
msg.pop.batch_verify(
|
|
||||||
rng,
|
|
||||||
batch,
|
|
||||||
batch_id,
|
|
||||||
msg.key,
|
|
||||||
pop_challenge::<C>(self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let key = ecdh::<C>(&self.enc_key, msg.key);
|
|
||||||
cipher::<C>(self.context, &key).apply_keystream(msg.msg.as_mut().as_mut());
|
|
||||||
(
|
|
||||||
msg.msg,
|
|
||||||
EncryptionKeyProof {
|
|
||||||
key,
|
|
||||||
dleq: DLEqProof::prove(
|
|
||||||
rng,
|
|
||||||
&mut encryption_key_transcript(self.context),
|
|
||||||
&[C::generator(), msg.key],
|
|
||||||
&self.enc_key,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn into_decryption(self) -> Decryption<C> {
|
|
||||||
self.decryption
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,683 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
// This crate requires `dleq` which doesn't support no-std via std-shims
|
|
||||||
// #![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
use core::{marker::PhantomData, ops::Deref, fmt};
|
|
||||||
use std::{
|
|
||||||
io::{self, Read, Write},
|
|
||||||
collections::HashMap,
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
use multiexp::{multiexp_vartime, BatchVerifier};
|
|
||||||
use ciphersuite::{
|
|
||||||
group::{
|
|
||||||
ff::{Field, PrimeField},
|
|
||||||
Group, GroupEncoding,
|
|
||||||
},
|
|
||||||
Ciphersuite,
|
|
||||||
};
|
|
||||||
|
|
||||||
use schnorr::SchnorrSignature;
|
|
||||||
|
|
||||||
pub use dkg::*;
|
|
||||||
|
|
||||||
mod encryption;
|
|
||||||
pub use encryption::*;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
/// Errors possible during key generation.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
|
||||||
pub enum PedPoPError<C: Ciphersuite> {
|
|
||||||
/// An incorrect amount of participants was provided.
|
|
||||||
#[error("incorrect amount of participants (expected {expected}, found {found})")]
|
|
||||||
IncorrectAmountOfParticipants { expected: usize, found: usize },
|
|
||||||
/// An invalid proof of knowledge was provided.
|
|
||||||
#[error("invalid proof of knowledge (participant {0})")]
|
|
||||||
InvalidCommitments(Participant),
|
|
||||||
/// An invalid DKG share was provided.
|
|
||||||
#[error("invalid share (participant {participant}, blame {blame})")]
|
|
||||||
InvalidShare { participant: Participant, blame: Option<EncryptionKeyProof<C>> },
|
|
||||||
/// A participant was missing.
|
|
||||||
#[error("missing participant {0}")]
|
|
||||||
MissingParticipant(Participant),
|
|
||||||
/// An error propagated from the underlying `dkg` crate.
|
|
||||||
#[error("error from dkg ({0})")]
|
|
||||||
DkgError(DkgError),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate a map of values to have the expected included participants
|
|
||||||
fn validate_map<T, C: Ciphersuite>(
|
|
||||||
map: &HashMap<Participant, T>,
|
|
||||||
included: &[Participant],
|
|
||||||
ours: Participant,
|
|
||||||
) -> Result<(), PedPoPError<C>> {
|
|
||||||
if (map.len() + 1) != included.len() {
|
|
||||||
Err(PedPoPError::IncorrectAmountOfParticipants {
|
|
||||||
expected: included.len(),
|
|
||||||
found: map.len() + 1,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for included in included {
|
|
||||||
if *included == ours {
|
|
||||||
if map.contains_key(included) {
|
|
||||||
Err(PedPoPError::DkgError(DkgError::DuplicatedParticipant(*included)))?;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !map.contains_key(included) {
|
|
||||||
Err(PedPoPError::MissingParticipant(*included))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
fn challenge<C: Ciphersuite>(context: [u8; 32], l: Participant, R: &[u8], Am: &[u8]) -> C::F {
|
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG PedPoP v0.2");
|
|
||||||
transcript.domain_separate(b"schnorr_proof_of_knowledge");
|
|
||||||
transcript.append_message(b"context", context);
|
|
||||||
transcript.append_message(b"participant", l.to_bytes());
|
|
||||||
transcript.append_message(b"nonce", R);
|
|
||||||
transcript.append_message(b"commitments", Am);
|
|
||||||
C::hash_to_F(b"DKG-PedPoP-proof_of_knowledge-0", &transcript.challenge(b"schnorr"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The commitments message, intended to be broadcast to all other parties.
|
|
||||||
///
|
|
||||||
/// Every participant should only provide one set of commitments to all parties. If any
|
|
||||||
/// participant sends multiple sets of commitments, they are faulty and should be presumed
|
|
||||||
/// malicious. As this library does not handle networking, it is unable to detect if any
|
|
||||||
/// participant is so faulty. That responsibility lies with the caller.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct Commitments<C: Ciphersuite> {
|
|
||||||
commitments: Vec<C::G>,
|
|
||||||
cached_msg: Vec<u8>,
|
|
||||||
sig: SchnorrSignature<C>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> ReadWrite for Commitments<C> {
|
|
||||||
fn read<R: Read>(reader: &mut R, params: ThresholdParams) -> io::Result<Self> {
|
|
||||||
let mut commitments = Vec::with_capacity(params.t().into());
|
|
||||||
let mut cached_msg = vec![];
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut read_G = || -> io::Result<C::G> {
|
|
||||||
let mut buf = <C::G as GroupEncoding>::Repr::default();
|
|
||||||
reader.read_exact(buf.as_mut())?;
|
|
||||||
let point = C::read_G(&mut buf.as_ref())?;
|
|
||||||
cached_msg.extend(buf.as_ref());
|
|
||||||
Ok(point)
|
|
||||||
};
|
|
||||||
|
|
||||||
for _ in 0 .. params.t() {
|
|
||||||
commitments.push(read_G()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Commitments { commitments, cached_msg, sig: SchnorrSignature::read(reader)? })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
writer.write_all(&self.cached_msg)?;
|
|
||||||
self.sig.write(writer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State machine to begin the key generation protocol.
|
|
||||||
#[derive(Debug, Zeroize)]
|
|
||||||
pub struct KeyGenMachine<C: Ciphersuite> {
|
|
||||||
params: ThresholdParams,
|
|
||||||
context: [u8; 32],
|
|
||||||
_curve: PhantomData<C>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> KeyGenMachine<C> {
|
|
||||||
/// Create a new machine to generate a key.
|
|
||||||
///
|
|
||||||
/// The context should be unique among multisigs.
|
|
||||||
pub fn new(params: ThresholdParams, context: [u8; 32]) -> KeyGenMachine<C> {
|
|
||||||
KeyGenMachine { params, context, _curve: PhantomData }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start generating a key according to the PedPoP DKG specification present in the FROST paper.
|
|
||||||
///
|
|
||||||
/// Returns a commitments message to be sent to all parties over an authenticated channel. If any
|
|
||||||
/// party submits multiple sets of commitments, they MUST be treated as malicious.
|
|
||||||
pub fn generate_coefficients<R: RngCore + CryptoRng>(
|
|
||||||
self,
|
|
||||||
rng: &mut R,
|
|
||||||
) -> (SecretShareMachine<C>, EncryptionKeyMessage<C, Commitments<C>>) {
|
|
||||||
let t = usize::from(self.params.t());
|
|
||||||
let mut coefficients = Vec::with_capacity(t);
|
|
||||||
let mut commitments = Vec::with_capacity(t);
|
|
||||||
let mut cached_msg = vec![];
|
|
||||||
|
|
||||||
for i in 0 .. t {
|
|
||||||
// Step 1: Generate t random values to form a polynomial with
|
|
||||||
coefficients.push(Zeroizing::new(C::random_nonzero_F(&mut *rng)));
|
|
||||||
// Step 3: Generate public commitments
|
|
||||||
commitments.push(C::generator() * coefficients[i].deref());
|
|
||||||
cached_msg.extend(commitments[i].to_bytes().as_ref());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Provide a proof of knowledge
|
|
||||||
let r = Zeroizing::new(C::random_nonzero_F(rng));
|
|
||||||
let nonce = C::generator() * r.deref();
|
|
||||||
let sig = SchnorrSignature::<C>::sign(
|
|
||||||
&coefficients[0],
|
|
||||||
// This could be deterministic as the PoK is a singleton never opened up to cooperative
|
|
||||||
// discussion
|
|
||||||
// There's no reason to spend the time and effort to make this deterministic besides a
|
|
||||||
// general obsession with canonicity and determinism though
|
|
||||||
r,
|
|
||||||
challenge::<C>(self.context, self.params.i(), nonce.to_bytes().as_ref(), &cached_msg),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Additionally create an encryption mechanism to protect the secret shares
|
|
||||||
let encryption = Encryption::new(self.context, self.params.i(), rng);
|
|
||||||
|
|
||||||
// Step 4: Broadcast
|
|
||||||
let msg =
|
|
||||||
encryption.registration(Commitments { commitments: commitments.clone(), cached_msg, sig });
|
|
||||||
(
|
|
||||||
SecretShareMachine {
|
|
||||||
params: self.params,
|
|
||||||
context: self.context,
|
|
||||||
coefficients,
|
|
||||||
our_commitments: commitments,
|
|
||||||
encryption,
|
|
||||||
},
|
|
||||||
msg,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn polynomial<F: PrimeField + Zeroize>(
|
|
||||||
coefficients: &[Zeroizing<F>],
|
|
||||||
l: Participant,
|
|
||||||
) -> Zeroizing<F> {
|
|
||||||
let l = F::from(u64::from(u16::from(l)));
|
|
||||||
// This should never be reached since Participant is explicitly non-zero
|
|
||||||
assert!(l != F::ZERO, "zero participant passed to polynomial");
|
|
||||||
let mut share = Zeroizing::new(F::ZERO);
|
|
||||||
for (idx, coefficient) in coefficients.iter().rev().enumerate() {
|
|
||||||
*share += coefficient.deref();
|
|
||||||
if idx != (coefficients.len() - 1) {
|
|
||||||
*share *= l;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
share
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The secret share message, to be sent to the party it's intended for over an authenticated
|
|
||||||
/// channel.
|
|
||||||
///
|
|
||||||
/// If any participant sends multiple secret shares to another participant, they are faulty.
|
|
||||||
// This should presumably be written as SecretShare(Zeroizing<F::Repr>).
|
|
||||||
// It's unfortunately not possible as F::Repr doesn't have Zeroize as a bound.
|
|
||||||
// The encryption system also explicitly uses Zeroizing<M> so it can ensure anything being
|
|
||||||
// encrypted is within Zeroizing. Accordingly, internally having Zeroizing would be redundant.
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
|
||||||
pub struct SecretShare<F: PrimeField>(F::Repr);
|
|
||||||
impl<F: PrimeField> AsRef<[u8]> for SecretShare<F> {
|
|
||||||
fn as_ref(&self) -> &[u8] {
|
|
||||||
self.0.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<F: PrimeField> AsMut<[u8]> for SecretShare<F> {
|
|
||||||
fn as_mut(&mut self) -> &mut [u8] {
|
|
||||||
self.0.as_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<F: PrimeField> fmt::Debug for SecretShare<F> {
|
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt.debug_struct("SecretShare").finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<F: PrimeField> Zeroize for SecretShare<F> {
|
|
||||||
fn zeroize(&mut self) {
|
|
||||||
self.0.as_mut().zeroize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Still manually implement ZeroizeOnDrop to ensure these don't stick around.
|
|
||||||
// We could replace Zeroizing<M> with a bound M: ZeroizeOnDrop.
|
|
||||||
// Doing so would potentially fail to highlight the expected behavior with these and remove a layer
|
|
||||||
// of depth.
|
|
||||||
impl<F: PrimeField> Drop for SecretShare<F> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<F: PrimeField> ZeroizeOnDrop for SecretShare<F> {}
|
|
||||||
|
|
||||||
impl<F: PrimeField> ReadWrite for SecretShare<F> {
|
|
||||||
fn read<R: Read>(reader: &mut R, _: ThresholdParams) -> io::Result<Self> {
|
|
||||||
let mut repr = F::Repr::default();
|
|
||||||
reader.read_exact(repr.as_mut())?;
|
|
||||||
Ok(SecretShare(repr))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
writer.write_all(self.0.as_ref())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advancement of the key generation state machine.
|
|
||||||
#[derive(Zeroize)]
|
|
||||||
pub struct SecretShareMachine<C: Ciphersuite> {
|
|
||||||
params: ThresholdParams,
|
|
||||||
context: [u8; 32],
|
|
||||||
coefficients: Vec<Zeroizing<C::F>>,
|
|
||||||
our_commitments: Vec<C::G>,
|
|
||||||
encryption: Encryption<C>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> fmt::Debug for SecretShareMachine<C> {
|
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt
|
|
||||||
.debug_struct("SecretShareMachine")
|
|
||||||
.field("params", &self.params)
|
|
||||||
.field("context", &self.context)
|
|
||||||
.field("our_commitments", &self.our_commitments)
|
|
||||||
.field("encryption", &self.encryption)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> SecretShareMachine<C> {
|
|
||||||
/// Verify the data from the previous round (canonicity, PoKs, message authenticity)
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
fn verify_r1<R: RngCore + CryptoRng>(
|
|
||||||
&mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
|
||||||
) -> Result<HashMap<Participant, Vec<C::G>>, PedPoPError<C>> {
|
|
||||||
validate_map(
|
|
||||||
&commitment_msgs,
|
|
||||||
&self.params.all_participant_indexes().collect::<Vec<_>>(),
|
|
||||||
self.params.i(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut batch = BatchVerifier::<Participant, C::G>::new(commitment_msgs.len());
|
|
||||||
let mut commitments = HashMap::new();
|
|
||||||
for l in self.params.all_participant_indexes() {
|
|
||||||
let Some(msg) = commitment_msgs.remove(&l) else { continue };
|
|
||||||
let mut msg = self.encryption.register(l, msg);
|
|
||||||
|
|
||||||
if msg.commitments.len() != self.params.t().into() {
|
|
||||||
Err(PedPoPError::InvalidCommitments(l))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Validate each proof of knowledge
|
|
||||||
// This is solely the prep step for the latter batch verification
|
|
||||||
msg.sig.batch_verify(
|
|
||||||
rng,
|
|
||||||
&mut batch,
|
|
||||||
l,
|
|
||||||
msg.commitments[0],
|
|
||||||
challenge::<C>(self.context, l, msg.sig.R.to_bytes().as_ref(), &msg.cached_msg),
|
|
||||||
);
|
|
||||||
|
|
||||||
commitments.insert(l, msg.commitments.drain(..).collect::<Vec<_>>());
|
|
||||||
}
|
|
||||||
|
|
||||||
batch.verify_vartime_with_vartime_blame().map_err(PedPoPError::InvalidCommitments)?;
|
|
||||||
|
|
||||||
commitments.insert(self.params.i(), self.our_commitments.drain(..).collect());
|
|
||||||
Ok(commitments)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Continue generating a key.
|
|
||||||
///
|
|
||||||
/// Takes in everyone else's commitments. Returns a HashMap of encrypted secret shares to be sent
|
|
||||||
/// over authenticated channels to their relevant counterparties.
|
|
||||||
///
|
|
||||||
/// If any participant sends multiple secret shares to another participant, they are faulty.
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
pub fn generate_secret_shares<R: RngCore + CryptoRng>(
|
|
||||||
mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
commitments: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
|
||||||
) -> Result<
|
|
||||||
(KeyMachine<C>, HashMap<Participant, EncryptedMessage<C, SecretShare<C::F>>>),
|
|
||||||
PedPoPError<C>,
|
|
||||||
> {
|
|
||||||
let commitments = self.verify_r1(&mut *rng, commitments)?;
|
|
||||||
|
|
||||||
// Step 1: Generate secret shares for all other parties
|
|
||||||
let mut res = HashMap::new();
|
|
||||||
for l in self.params.all_participant_indexes() {
|
|
||||||
// Don't insert our own shares to the byte buffer which is meant to be sent around
|
|
||||||
// An app developer could accidentally send it. Best to keep this black boxed
|
|
||||||
if l == self.params.i() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut share = polynomial(&self.coefficients, l);
|
|
||||||
let share_bytes = Zeroizing::new(SecretShare::<C::F>(share.to_repr()));
|
|
||||||
share.zeroize();
|
|
||||||
res.insert(l, self.encryption.encrypt(rng, l, share_bytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate our own share
|
|
||||||
let share = polynomial(&self.coefficients, self.params.i());
|
|
||||||
self.coefficients.zeroize();
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
KeyMachine { params: self.params, secret: share, commitments, encryption: self.encryption },
|
|
||||||
res,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advancement of the the secret share state machine.
|
|
||||||
///
|
|
||||||
/// This machine will 'complete' the protocol, by a local perspective. In order to be secure,
|
|
||||||
/// the parties must confirm having successfully completed the protocol (an effort out of scope to
|
|
||||||
/// this library), yet this is modeled by one more state transition (BlameMachine).
|
|
||||||
pub struct KeyMachine<C: Ciphersuite> {
|
|
||||||
params: ThresholdParams,
|
|
||||||
secret: Zeroizing<C::F>,
|
|
||||||
commitments: HashMap<Participant, Vec<C::G>>,
|
|
||||||
encryption: Encryption<C>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> fmt::Debug for KeyMachine<C> {
|
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt
|
|
||||||
.debug_struct("KeyMachine")
|
|
||||||
.field("params", &self.params)
|
|
||||||
.field("commitments", &self.commitments)
|
|
||||||
.field("encryption", &self.encryption)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> Zeroize for KeyMachine<C> {
|
|
||||||
fn zeroize(&mut self) {
|
|
||||||
self.params.zeroize();
|
|
||||||
self.secret.zeroize();
|
|
||||||
for commitments in self.commitments.values_mut() {
|
|
||||||
commitments.zeroize();
|
|
||||||
}
|
|
||||||
self.encryption.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the exponent for a given participant and apply it to a series of commitments
|
|
||||||
// Initially used with the actual commitments to verify the secret share, later used with
|
|
||||||
// stripes to generate the verification shares
|
|
||||||
fn exponential<C: Ciphersuite>(i: Participant, values: &[C::G]) -> Vec<(C::F, C::G)> {
|
|
||||||
let i = C::F::from(u16::from(i).into());
|
|
||||||
let mut res = Vec::with_capacity(values.len());
|
|
||||||
(0 .. values.len()).fold(C::F::ONE, |exp, l| {
|
|
||||||
res.push((exp, values[l]));
|
|
||||||
exp * i
|
|
||||||
});
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
fn share_verification_statements<C: Ciphersuite>(
|
|
||||||
target: Participant,
|
|
||||||
commitments: &[C::G],
|
|
||||||
mut share: Zeroizing<C::F>,
|
|
||||||
) -> Vec<(C::F, C::G)> {
|
|
||||||
// This can be insecurely linearized from n * t to just n using the below sums for a given
|
|
||||||
// stripe. Doing so uses naive addition which is subject to malleability. The only way to
|
|
||||||
// ensure that malleability isn't present is to use this n * t algorithm, which runs
|
|
||||||
// per sender and not as an aggregate of all senders, which also enables blame
|
|
||||||
let mut values = exponential::<C>(target, commitments);
|
|
||||||
|
|
||||||
// Perform the share multiplication outside of the multiexp to minimize stack copying
|
|
||||||
// While the multiexp BatchVerifier does zeroize its flattened multiexp, and itself, it still
|
|
||||||
// converts whatever we give to an iterator and then builds a Vec internally, welcoming copies
|
|
||||||
let neg_share_pub = C::generator() * -*share;
|
|
||||||
share.zeroize();
|
|
||||||
values.push((C::F::ONE, neg_share_pub));
|
|
||||||
|
|
||||||
values
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Hash, Debug, Zeroize)]
|
|
||||||
enum BatchId {
|
|
||||||
Decryption(Participant),
|
|
||||||
Share(Participant),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> KeyMachine<C> {
|
|
||||||
/// Calculate our share given the shares sent to us.
|
|
||||||
///
|
|
||||||
/// Returns a BlameMachine usable to determine if faults in the protocol occurred.
|
|
||||||
///
|
|
||||||
/// This will error on, and return a blame proof for, the first-observed case of faulty behavior.
|
|
||||||
pub fn calculate_share<R: RngCore + CryptoRng>(
|
|
||||||
mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
mut shares: HashMap<Participant, EncryptedMessage<C, SecretShare<C::F>>>,
|
|
||||||
) -> Result<BlameMachine<C>, PedPoPError<C>> {
|
|
||||||
validate_map(
|
|
||||||
&shares,
|
|
||||||
&self.params.all_participant_indexes().collect::<Vec<_>>(),
|
|
||||||
self.params.i(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut batch = BatchVerifier::new(shares.len());
|
|
||||||
let mut blames = HashMap::new();
|
|
||||||
for (l, share_bytes) in shares.drain() {
|
|
||||||
let (mut share_bytes, blame) =
|
|
||||||
self.encryption.decrypt(rng, &mut batch, BatchId::Decryption(l), l, share_bytes);
|
|
||||||
let share =
|
|
||||||
Zeroizing::new(Option::<C::F>::from(C::F::from_repr(share_bytes.0)).ok_or_else(|| {
|
|
||||||
PedPoPError::InvalidShare { participant: l, blame: Some(blame.clone()) }
|
|
||||||
})?);
|
|
||||||
share_bytes.zeroize();
|
|
||||||
*self.secret += share.deref();
|
|
||||||
|
|
||||||
blames.insert(l, blame);
|
|
||||||
batch.queue(
|
|
||||||
rng,
|
|
||||||
BatchId::Share(l),
|
|
||||||
share_verification_statements::<C>(self.params.i(), &self.commitments[&l], share),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
batch.verify_with_vartime_blame().map_err(|id| {
|
|
||||||
let (l, blame) = match id {
|
|
||||||
BatchId::Decryption(l) => (l, None),
|
|
||||||
BatchId::Share(l) => (l, Some(blames.remove(&l).unwrap())),
|
|
||||||
};
|
|
||||||
PedPoPError::InvalidShare { participant: l, blame }
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Stripe commitments per t and sum them in advance. Calculating verification shares relies on
|
|
||||||
// these sums so preprocessing them is a massive speedup
|
|
||||||
// If these weren't just sums, yet the tables used in multiexp, this would be further optimized
|
|
||||||
// As of right now, each multiexp will regenerate them
|
|
||||||
let mut stripes = Vec::with_capacity(usize::from(self.params.t()));
|
|
||||||
for t in 0 .. usize::from(self.params.t()) {
|
|
||||||
stripes.push(self.commitments.values().map(|commitments| commitments[t]).sum());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate each user's verification share
|
|
||||||
let mut verification_shares = HashMap::new();
|
|
||||||
for i in self.params.all_participant_indexes() {
|
|
||||||
verification_shares.insert(
|
|
||||||
i,
|
|
||||||
if i == self.params.i() {
|
|
||||||
C::generator() * self.secret.deref()
|
|
||||||
} else {
|
|
||||||
multiexp_vartime(&exponential::<C>(i, &stripes))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let KeyMachine { commitments, encryption, params, secret } = self;
|
|
||||||
Ok(BlameMachine {
|
|
||||||
commitments,
|
|
||||||
encryption: encryption.into_decryption(),
|
|
||||||
result: Some(
|
|
||||||
ThresholdKeys::new(params, Interpolation::Lagrange, secret, verification_shares)
|
|
||||||
.map_err(PedPoPError::DkgError)?,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A machine capable of handling blame proofs.
|
|
||||||
pub struct BlameMachine<C: Ciphersuite> {
|
|
||||||
commitments: HashMap<Participant, Vec<C::G>>,
|
|
||||||
encryption: Decryption<C>,
|
|
||||||
result: Option<ThresholdKeys<C>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> fmt::Debug for BlameMachine<C> {
|
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt
|
|
||||||
.debug_struct("BlameMachine")
|
|
||||||
.field("commitments", &self.commitments)
|
|
||||||
.field("encryption", &self.encryption)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> Zeroize for BlameMachine<C> {
|
|
||||||
fn zeroize(&mut self) {
|
|
||||||
for commitments in self.commitments.values_mut() {
|
|
||||||
commitments.zeroize();
|
|
||||||
}
|
|
||||||
self.result.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> BlameMachine<C> {
|
|
||||||
/// Mark the protocol as having been successfully completed, returning the generated keys.
|
|
||||||
/// This should only be called after having confirmed, with all participants, successful
|
|
||||||
/// completion.
|
|
||||||
///
|
|
||||||
/// Confirming successful completion is not necessarily as simple as everyone reporting their
|
|
||||||
/// completion. Everyone must also receive everyone's report of completion, entering into the
|
|
||||||
/// territory of consensus protocols. This library does not handle that nor does it provide any
|
|
||||||
/// tooling to do so. This function is solely intended to force users to acknowledge they're
|
|
||||||
/// completing the protocol, not processing any blame.
|
|
||||||
pub fn complete(self) -> ThresholdKeys<C> {
|
|
||||||
self.result.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn blame_internal(
|
|
||||||
&self,
|
|
||||||
sender: Participant,
|
|
||||||
recipient: Participant,
|
|
||||||
msg: EncryptedMessage<C, SecretShare<C::F>>,
|
|
||||||
proof: Option<EncryptionKeyProof<C>>,
|
|
||||||
) -> Participant {
|
|
||||||
let share_bytes = match self.encryption.decrypt_with_proof(sender, recipient, msg, proof) {
|
|
||||||
Ok(share_bytes) => share_bytes,
|
|
||||||
// If there's an invalid signature, the sender did not send a properly formed message
|
|
||||||
Err(DecryptionError::InvalidSignature) => return sender,
|
|
||||||
// Decryption will fail if the provided ECDH key wasn't correct for the given message
|
|
||||||
Err(DecryptionError::InvalidProof) => return recipient,
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(share) = Option::<C::F>::from(C::F::from_repr(share_bytes.0)) else {
|
|
||||||
// If this isn't a valid scalar, the sender is faulty
|
|
||||||
return sender;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If this isn't a valid share, the sender is faulty
|
|
||||||
if !bool::from(
|
|
||||||
multiexp_vartime(&share_verification_statements::<C>(
|
|
||||||
recipient,
|
|
||||||
&self.commitments[&sender],
|
|
||||||
Zeroizing::new(share),
|
|
||||||
))
|
|
||||||
.is_identity(),
|
|
||||||
) {
|
|
||||||
return sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The share was canonical and valid
|
|
||||||
recipient
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given an accusation of fault, determine the faulty party (either the sender, who sent an
|
|
||||||
/// invalid secret share, or the receiver, who claimed a valid secret share was invalid). No
|
|
||||||
/// matter which, prevent completion of the machine, forcing an abort of the protocol.
|
|
||||||
///
|
|
||||||
/// The message should be a copy of the encrypted secret share from the accused sender to the
|
|
||||||
/// accusing recipient. This message must have been authenticated as actually having come from
|
|
||||||
/// the sender in question.
|
|
||||||
///
|
|
||||||
/// In order to enable detecting multiple faults, an `AdditionalBlameMachine` is returned, which
|
|
||||||
/// can be used to determine further blame. These machines will process the same blame statements
|
|
||||||
/// multiple times, always identifying blame. It is the caller's job to ensure they're unique in
|
|
||||||
/// order to prevent multiple instances of blame over a single incident.
|
|
||||||
pub fn blame(
|
|
||||||
self,
|
|
||||||
sender: Participant,
|
|
||||||
recipient: Participant,
|
|
||||||
msg: EncryptedMessage<C, SecretShare<C::F>>,
|
|
||||||
proof: Option<EncryptionKeyProof<C>>,
|
|
||||||
) -> (AdditionalBlameMachine<C>, Participant) {
|
|
||||||
let faulty = self.blame_internal(sender, recipient, msg, proof);
|
|
||||||
(AdditionalBlameMachine(self), faulty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A machine capable of handling an arbitrary amount of additional blame proofs.
|
|
||||||
#[derive(Debug, Zeroize)]
|
|
||||||
pub struct AdditionalBlameMachine<C: Ciphersuite>(BlameMachine<C>);
|
|
||||||
impl<C: Ciphersuite> AdditionalBlameMachine<C> {
|
|
||||||
/// Create an AdditionalBlameMachine capable of evaluating Blame regardless of if the caller was
|
|
||||||
/// a member in the DKG protocol.
|
|
||||||
///
|
|
||||||
/// Takes in the parameters for the DKG protocol and all of the participant's commitment
|
|
||||||
/// messages.
|
|
||||||
///
|
|
||||||
/// This constructor assumes the full validity of the commitment messages. They must be fully
|
|
||||||
/// authenticated as having come from the supposed party and verified as valid. Usage of invalid
|
|
||||||
/// commitments is considered undefined behavior, and may cause everything from inaccurate blame
|
|
||||||
/// to panics.
|
|
||||||
pub fn new(
|
|
||||||
context: [u8; 32],
|
|
||||||
n: u16,
|
|
||||||
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
|
||||||
) -> Result<Self, PedPoPError<C>> {
|
|
||||||
let mut commitments = HashMap::new();
|
|
||||||
let mut encryption = Decryption::new(context);
|
|
||||||
for i in 1 ..= n {
|
|
||||||
let i = Participant::new(i).unwrap();
|
|
||||||
let Some(msg) = commitment_msgs.remove(&i) else { Err(PedPoPError::MissingParticipant(i))? };
|
|
||||||
commitments.insert(i, encryption.register(i, msg).commitments);
|
|
||||||
}
|
|
||||||
Ok(AdditionalBlameMachine(BlameMachine { commitments, encryption, result: None }))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given an accusation of fault, determine the faulty party (either the sender, who sent an
|
|
||||||
/// invalid secret share, or the receiver, who claimed a valid secret share was invalid).
|
|
||||||
///
|
|
||||||
/// The message should be a copy of the encrypted secret share from the accused sender to the
|
|
||||||
/// accusing recipient. This message must have been authenticated as actually having come from
|
|
||||||
/// the sender in question.
|
|
||||||
///
|
|
||||||
/// This will process the same blame statement multiple times, always identifying blame. It is
|
|
||||||
/// the caller's job to ensure they're unique in order to prevent multiple instances of blame
|
|
||||||
/// over a single incident.
|
|
||||||
pub fn blame(
|
|
||||||
&self,
|
|
||||||
sender: Participant,
|
|
||||||
recipient: Participant,
|
|
||||||
msg: EncryptedMessage<C, SecretShare<C::F>>,
|
|
||||||
proof: Option<EncryptionKeyProof<C>>,
|
|
||||||
) -> Participant {
|
|
||||||
self.0.blame_internal(sender, recipient, msg, proof)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng, OsRng};
|
|
||||||
|
|
||||||
use dalek_ff_group::Ristretto;
|
|
||||||
use ciphersuite::Ciphersuite;
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
const THRESHOLD: u16 = 3;
|
|
||||||
const PARTICIPANTS: u16 = 5;
|
|
||||||
|
|
||||||
/// Clone a map without a specific value.
|
|
||||||
fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
|
||||||
map: &HashMap<K, V>,
|
|
||||||
without: &K,
|
|
||||||
) -> HashMap<K, V> {
|
|
||||||
let mut res = map.clone();
|
|
||||||
res.remove(without).unwrap();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
type PedPoPEncryptedMessage<C> = EncryptedMessage<C, SecretShare<<C as Ciphersuite>::F>>;
|
|
||||||
type PedPoPSecretShares<C> = HashMap<Participant, PedPoPEncryptedMessage<C>>;
|
|
||||||
|
|
||||||
const CONTEXT: [u8; 32] = *b"DKG Test Key Generation ";
|
|
||||||
|
|
||||||
// Commit, then return commitment messages, enc keys, and shares
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
fn commit_enc_keys_and_shares<R: RngCore + CryptoRng, C: Ciphersuite>(
|
|
||||||
rng: &mut R,
|
|
||||||
) -> (
|
|
||||||
HashMap<Participant, KeyMachine<C>>,
|
|
||||||
HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
|
||||||
HashMap<Participant, C::G>,
|
|
||||||
HashMap<Participant, PedPoPSecretShares<C>>,
|
|
||||||
) {
|
|
||||||
let mut machines = HashMap::new();
|
|
||||||
let mut commitments = HashMap::new();
|
|
||||||
let mut enc_keys = HashMap::new();
|
|
||||||
for i in (1 ..= PARTICIPANTS).map(|i| Participant::new(i).unwrap()) {
|
|
||||||
let params = ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap();
|
|
||||||
let machine = KeyGenMachine::<C>::new(params, CONTEXT);
|
|
||||||
let (machine, these_commitments) = machine.generate_coefficients(rng);
|
|
||||||
machines.insert(i, machine);
|
|
||||||
|
|
||||||
commitments.insert(
|
|
||||||
i,
|
|
||||||
EncryptionKeyMessage::read::<&[u8]>(&mut these_commitments.serialize().as_ref(), params)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
enc_keys.insert(i, commitments[&i].enc_key());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut secret_shares = HashMap::new();
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.map(|(l, machine)| {
|
|
||||||
let (machine, mut shares) =
|
|
||||||
machine.generate_secret_shares(rng, clone_without(&commitments, &l)).unwrap();
|
|
||||||
let shares = shares
|
|
||||||
.drain()
|
|
||||||
.map(|(l, share)| {
|
|
||||||
(
|
|
||||||
l,
|
|
||||||
EncryptedMessage::read::<&[u8]>(
|
|
||||||
&mut share.serialize().as_ref(),
|
|
||||||
// Only t/n actually matters, so hardcode i to 1 here
|
|
||||||
ThresholdParams::new(THRESHOLD, PARTICIPANTS, Participant::new(1).unwrap()).unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
secret_shares.insert(l, shares);
|
|
||||||
(l, machine)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
(machines, commitments, enc_keys, secret_shares)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_secret_shares<C: Ciphersuite>(
|
|
||||||
shares: &HashMap<Participant, PedPoPSecretShares<C>>,
|
|
||||||
recipient: Participant,
|
|
||||||
) -> PedPoPSecretShares<C> {
|
|
||||||
let mut our_secret_shares = HashMap::new();
|
|
||||||
for (i, shares) in shares {
|
|
||||||
if recipient == *i {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
our_secret_shares.insert(*i, shares[&recipient].clone());
|
|
||||||
}
|
|
||||||
our_secret_shares
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fully perform the PedPoP key generation algorithm.
|
|
||||||
fn pedpop_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
|
||||||
rng: &mut R,
|
|
||||||
) -> HashMap<Participant, ThresholdKeys<C>> {
|
|
||||||
let (mut machines, _, _, secret_shares) = commit_enc_keys_and_shares::<_, C>(rng);
|
|
||||||
|
|
||||||
let mut verification_shares = None;
|
|
||||||
let mut group_key = None;
|
|
||||||
machines
|
|
||||||
.drain()
|
|
||||||
.map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let these_keys = machine.calculate_share(rng, our_secret_shares).unwrap().complete();
|
|
||||||
|
|
||||||
// Verify the verification_shares are agreed upon
|
|
||||||
if verification_shares.is_none() {
|
|
||||||
verification_shares = Some(
|
|
||||||
these_keys
|
|
||||||
.params()
|
|
||||||
.all_participant_indexes()
|
|
||||||
.map(|i| (i, these_keys.original_verification_share(i)))
|
|
||||||
.collect::<HashMap<_, _>>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
verification_shares.as_ref().unwrap(),
|
|
||||||
&these_keys
|
|
||||||
.params()
|
|
||||||
.all_participant_indexes()
|
|
||||||
.map(|i| (i, these_keys.original_verification_share(i)))
|
|
||||||
.collect::<HashMap<_, _>>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify the group keys are agreed upon
|
|
||||||
if group_key.is_none() {
|
|
||||||
group_key = Some(these_keys.group_key());
|
|
||||||
}
|
|
||||||
assert_eq!(group_key.unwrap(), these_keys.group_key());
|
|
||||||
|
|
||||||
(i, these_keys)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
const ONE: Participant = Participant::new(1).unwrap();
|
|
||||||
const TWO: Participant = Participant::new(2).unwrap();
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pedpop() {
|
|
||||||
let _ = core::hint::black_box(pedpop_gen::<_, Ristretto>(&mut OsRng));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_blame(
|
|
||||||
commitment_msgs: &HashMap<Participant, EncryptionKeyMessage<Ristretto, Commitments<Ristretto>>>,
|
|
||||||
machines: Vec<BlameMachine<Ristretto>>,
|
|
||||||
msg: &PedPoPEncryptedMessage<Ristretto>,
|
|
||||||
blame: &Option<EncryptionKeyProof<Ristretto>>,
|
|
||||||
) {
|
|
||||||
for machine in machines {
|
|
||||||
let (additional, blamed) = machine.blame(ONE, TWO, msg.clone(), blame.clone());
|
|
||||||
assert_eq!(blamed, ONE);
|
|
||||||
// Verify additional blame also works
|
|
||||||
assert_eq!(additional.blame(ONE, TWO, msg.clone(), blame.clone()), ONE);
|
|
||||||
|
|
||||||
// Verify machines constructed with AdditionalBlameMachine::new work
|
|
||||||
assert_eq!(
|
|
||||||
AdditionalBlameMachine::new(CONTEXT, PARTICIPANTS, commitment_msgs.clone()).unwrap().blame(
|
|
||||||
ONE,
|
|
||||||
TWO,
|
|
||||||
msg.clone(),
|
|
||||||
blame.clone()
|
|
||||||
),
|
|
||||||
ONE,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Write a macro which expands to the following
|
|
||||||
#[test]
|
|
||||||
fn invalid_encryption_pop_blame() {
|
|
||||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
|
||||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
|
||||||
|
|
||||||
// Mutate the PoP of the encrypted message from 1 to 2
|
|
||||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_pop();
|
|
||||||
|
|
||||||
let mut blame = None;
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.filter_map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
|
||||||
if i == TWO {
|
|
||||||
assert_eq!(
|
|
||||||
machine.err(),
|
|
||||||
Some(PedPoPError::InvalidShare { participant: ONE, blame: None })
|
|
||||||
);
|
|
||||||
// Explicitly declare we have a blame object, which happens to be None since invalid PoP
|
|
||||||
// is self-explainable
|
|
||||||
blame = Some(None);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(machine.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_ecdh_blame() {
|
|
||||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
|
||||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
|
||||||
|
|
||||||
// Mutate the share to trigger a blame event
|
|
||||||
// Mutates from 2 to 1, as 1 is expected to end up malicious for test_blame to pass
|
|
||||||
// While here, 2 is malicious, this is so 1 creates the blame proof
|
|
||||||
// We then malleate 1's blame proof, so 1 ends up malicious
|
|
||||||
// Doesn't simply invalidate the PoP as that won't have a blame statement
|
|
||||||
// By mutating the encrypted data, we do ensure a blame statement is created
|
|
||||||
secret_shares
|
|
||||||
.get_mut(&TWO)
|
|
||||||
.unwrap()
|
|
||||||
.get_mut(&ONE)
|
|
||||||
.unwrap()
|
|
||||||
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
|
||||||
|
|
||||||
let mut blame = None;
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.filter_map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
|
||||||
if i == ONE {
|
|
||||||
blame = Some(match machine.err() {
|
|
||||||
Some(PedPoPError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
|
||||||
_ => panic!(),
|
|
||||||
});
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(machine.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
blame.as_mut().unwrap().as_mut().unwrap().invalidate_key();
|
|
||||||
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should be largely equivalent to the prior test
|
|
||||||
#[test]
|
|
||||||
fn invalid_dleq_blame() {
|
|
||||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
|
||||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
|
||||||
|
|
||||||
secret_shares
|
|
||||||
.get_mut(&TWO)
|
|
||||||
.unwrap()
|
|
||||||
.get_mut(&ONE)
|
|
||||||
.unwrap()
|
|
||||||
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
|
||||||
|
|
||||||
let mut blame = None;
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.filter_map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
|
||||||
if i == ONE {
|
|
||||||
blame = Some(match machine.err() {
|
|
||||||
Some(PedPoPError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
|
||||||
_ => panic!(),
|
|
||||||
});
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(machine.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
blame.as_mut().unwrap().as_mut().unwrap().invalidate_dleq();
|
|
||||||
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_share_serialization_blame() {
|
|
||||||
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
|
||||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
|
||||||
|
|
||||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_serialization(
|
|
||||||
&mut OsRng,
|
|
||||||
CONTEXT,
|
|
||||||
ONE,
|
|
||||||
enc_keys[&TWO],
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut blame = None;
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.filter_map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
|
||||||
if i == TWO {
|
|
||||||
blame = Some(match machine.err() {
|
|
||||||
Some(PedPoPError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
|
||||||
_ => panic!(),
|
|
||||||
});
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(machine.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_share_value_blame() {
|
|
||||||
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
|
||||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
|
||||||
|
|
||||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_value(
|
|
||||||
&mut OsRng,
|
|
||||||
CONTEXT,
|
|
||||||
ONE,
|
|
||||||
enc_keys[&TWO],
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut blame = None;
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.filter_map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
|
||||||
if i == TWO {
|
|
||||||
blame = Some(match machine.err() {
|
|
||||||
Some(PedPoPError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
|
||||||
_ => panic!(),
|
|
||||||
});
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(machine.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "dkg-promote"
|
|
||||||
version = "0.6.1"
|
|
||||||
description = "Promotions for keys from the dkg crate"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/promote"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.80"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
thiserror = { version = "2", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../transcript", version = "^0.3.2", default-features = false, features = ["std", "recommended"] }
|
|
||||||
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false, features = ["std"] }
|
|
||||||
dleq = { path = "../../dleq", version = "^0.4.1", default-features = false, features = ["std", "serialize"] }
|
|
||||||
|
|
||||||
dkg = { path = "../", version = "0.6.1", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["std", "zeroize_derive"] }
|
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
|
||||||
dalek-ff-group = { path = "../../dalek-ff-group" }
|
|
||||||
dkg-recovery = { path = "../recovery", default-features = false, features = ["std"] }
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021-2025 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.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Distributed Key Generation - Promote
|
|
||||||
|
|
||||||
This crate implements 'promotions' for keys from the
|
|
||||||
[`dkg`](https://docs.rs/dkg) crate. A promotion takes a set of keys and maps it
|
|
||||||
to a different `Ciphersuite`.
|
|
||||||
|
|
||||||
This crate was originally part of the `dkg` crate, which was
|
|
||||||
[audited by Cypher Stack in March 2023](
|
|
||||||
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
|
||||||
), culminating in commit
|
|
||||||
[669d2dbffc1dafb82a09d9419ea182667115df06](
|
|
||||||
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
|
||||||
). Any subsequent changes have not undergone auditing.
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
// This crate requires `dleq` which doesn't support no-std via std-shims
|
|
||||||
// #![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
use core::{marker::PhantomData, ops::Deref};
|
|
||||||
use std::{
|
|
||||||
io::{self, Read, Write},
|
|
||||||
collections::HashMap,
|
|
||||||
};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
use dleq::DLEqProof;
|
|
||||||
|
|
||||||
pub use dkg::*;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
/// Errors encountered when promoting keys.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
|
||||||
pub enum PromotionError {
|
|
||||||
/// Invalid participant identifier.
|
|
||||||
#[error("invalid participant (1 <= participant <= {n}, yet participant is {participant})")]
|
|
||||||
InvalidParticipant {
|
|
||||||
/// The total amount of participants.
|
|
||||||
n: u16,
|
|
||||||
/// The specified participant.
|
|
||||||
participant: Participant,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// An incorrect amount of participants was specified.
|
|
||||||
#[error("incorrect amount of participants. {t} <= amount <= {n}, yet amount is {amount}")]
|
|
||||||
IncorrectAmountOfParticipants {
|
|
||||||
/// The threshold required.
|
|
||||||
t: u16,
|
|
||||||
/// The total amount of participants.
|
|
||||||
n: u16,
|
|
||||||
/// The amount of participants specified.
|
|
||||||
amount: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Participant provided an invalid proof.
|
|
||||||
#[error("invalid proof {0}")]
|
|
||||||
InvalidProof(Participant),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript<G: GroupEncoding>(key: &G, i: Participant) -> RecommendedTranscript {
|
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG Generator Promotion v0.2");
|
|
||||||
transcript.append_message(b"group_key", key.to_bytes());
|
|
||||||
transcript.append_message(b"participant", i.to_bytes());
|
|
||||||
transcript
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Proof of valid promotion to another generator.
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct GeneratorProof<C: Ciphersuite> {
|
|
||||||
share: C::G,
|
|
||||||
proof: DLEqProof<C::G>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> GeneratorProof<C> {
|
|
||||||
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
writer.write_all(self.share.to_bytes().as_ref())?;
|
|
||||||
self.proof.write(writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: Read>(reader: &mut R) -> io::Result<GeneratorProof<C>> {
|
|
||||||
Ok(GeneratorProof {
|
|
||||||
share: <C as Ciphersuite>::read_G(reader)?,
|
|
||||||
proof: DLEqProof::read(reader)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut buf = vec![];
|
|
||||||
self.write(&mut buf).unwrap();
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Promote a set of keys from one generator to another, where the elliptic curve is the same.
|
|
||||||
///
|
|
||||||
/// Since the Ciphersuite trait additionally specifies a generator, this provides an O(n) way to
|
|
||||||
/// update the generator used with keys. This outperforms the key generation protocol which is
|
|
||||||
/// exponential.
|
|
||||||
pub struct GeneratorPromotion<C1: Ciphersuite, C2: Ciphersuite> {
|
|
||||||
base: ThresholdKeys<C1>,
|
|
||||||
proof: GeneratorProof<C1>,
|
|
||||||
_c2: PhantomData<C2>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<C1, C2> {
|
|
||||||
/// Begin promoting keys from one generator to another.
|
|
||||||
///
|
|
||||||
/// Returns a proof this share was properly promoted.
|
|
||||||
pub fn promote<R: RngCore + CryptoRng>(
|
|
||||||
rng: &mut R,
|
|
||||||
base: ThresholdKeys<C1>,
|
|
||||||
) -> (GeneratorPromotion<C1, C2>, GeneratorProof<C1>) {
|
|
||||||
// Do a DLEqProof for the new generator
|
|
||||||
let proof = GeneratorProof {
|
|
||||||
share: C2::generator() * base.original_secret_share().deref(),
|
|
||||||
proof: DLEqProof::prove(
|
|
||||||
rng,
|
|
||||||
&mut transcript(&base.original_group_key(), base.params().i()),
|
|
||||||
&[C1::generator(), C2::generator()],
|
|
||||||
base.original_secret_share(),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
(GeneratorPromotion { base, proof, _c2: PhantomData::<C2> }, proof)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Complete promotion by taking in the proofs from all other participants.
|
|
||||||
pub fn complete(
|
|
||||||
self,
|
|
||||||
proofs: &HashMap<Participant, GeneratorProof<C1>>,
|
|
||||||
) -> Result<ThresholdKeys<C2>, PromotionError> {
|
|
||||||
let params = self.base.params();
|
|
||||||
if proofs.len() != (usize::from(params.n()) - 1) {
|
|
||||||
Err(PromotionError::IncorrectAmountOfParticipants {
|
|
||||||
t: params.n(),
|
|
||||||
n: params.n(),
|
|
||||||
amount: proofs.len() + 1,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
for i in proofs.keys().copied() {
|
|
||||||
if u16::from(i) > params.n() {
|
|
||||||
Err(PromotionError::InvalidParticipant { n: params.n(), participant: i })?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut verification_shares = HashMap::new();
|
|
||||||
verification_shares.insert(params.i(), self.proof.share);
|
|
||||||
for i in 1 ..= params.n() {
|
|
||||||
let i = Participant::new(i).unwrap();
|
|
||||||
if i == params.i() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let proof = proofs.get(&i).unwrap();
|
|
||||||
proof
|
|
||||||
.proof
|
|
||||||
.verify(
|
|
||||||
&mut transcript(&self.base.original_group_key(), i),
|
|
||||||
&[C1::generator(), C2::generator()],
|
|
||||||
&[self.base.original_verification_share(i), proof.share],
|
|
||||||
)
|
|
||||||
.map_err(|_| PromotionError::InvalidProof(i))?;
|
|
||||||
verification_shares.insert(i, proof.share);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(
|
|
||||||
ThresholdKeys::new(
|
|
||||||
params,
|
|
||||||
self.base.interpolation().clone(),
|
|
||||||
self.base.original_secret_share().clone(),
|
|
||||||
verification_shares,
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
use core::marker::PhantomData;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use dalek_ff_group::Ristretto;
|
|
||||||
use ciphersuite::{
|
|
||||||
group::{ff::Field, Group},
|
|
||||||
Ciphersuite,
|
|
||||||
};
|
|
||||||
|
|
||||||
use dkg::*;
|
|
||||||
use dkg_recovery::recover_key;
|
|
||||||
use crate::{GeneratorPromotion, GeneratorProof};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
struct AltGenerator<C: Ciphersuite> {
|
|
||||||
_curve: PhantomData<C>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> Ciphersuite for AltGenerator<C> {
|
|
||||||
type F = C::F;
|
|
||||||
type G = C::G;
|
|
||||||
type H = C::H;
|
|
||||||
|
|
||||||
const ID: &'static [u8] = b"Alternate Ciphersuite";
|
|
||||||
|
|
||||||
fn generator() -> Self::G {
|
|
||||||
C::G::generator() * <C as Ciphersuite>::hash_to_F(b"DKG Promotion Test", b"generator")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
|
||||||
<C as Ciphersuite>::hash_to_F(dst, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clone a map without a specific value.
|
|
||||||
pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
|
||||||
map: &HashMap<K, V>,
|
|
||||||
without: &K,
|
|
||||||
) -> HashMap<K, V> {
|
|
||||||
let mut res = map.clone();
|
|
||||||
res.remove(without).unwrap();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test promotion of threshold keys to another generator
|
|
||||||
#[test]
|
|
||||||
fn test_generator_promotion() {
|
|
||||||
// Generate a set of `ThresholdKeys`
|
|
||||||
const PARTICIPANTS: u16 = 5;
|
|
||||||
let keys: [ThresholdKeys<_>; PARTICIPANTS as usize] = {
|
|
||||||
let shares: [<Ristretto as Ciphersuite>::F; PARTICIPANTS as usize] =
|
|
||||||
core::array::from_fn(|_| <Ristretto as Ciphersuite>::F::random(&mut OsRng));
|
|
||||||
let verification_shares = (0 .. PARTICIPANTS)
|
|
||||||
.map(|i| {
|
|
||||||
(
|
|
||||||
Participant::new(i + 1).unwrap(),
|
|
||||||
<Ristretto as Ciphersuite>::generator() * shares[usize::from(i)],
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
core::array::from_fn(|i| {
|
|
||||||
ThresholdKeys::new(
|
|
||||||
ThresholdParams::new(
|
|
||||||
PARTICIPANTS,
|
|
||||||
PARTICIPANTS,
|
|
||||||
Participant::new(u16::try_from(i + 1).unwrap()).unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
Interpolation::Constant(vec![<Ristretto as Ciphersuite>::F::ONE; PARTICIPANTS as usize]),
|
|
||||||
Zeroizing::new(shares[i]),
|
|
||||||
verification_shares.clone(),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// Perform the promotion
|
|
||||||
let mut promotions = HashMap::new();
|
|
||||||
let mut proofs = HashMap::new();
|
|
||||||
for keys in &keys {
|
|
||||||
let i = keys.params().i();
|
|
||||||
let (promotion, proof) =
|
|
||||||
GeneratorPromotion::<_, AltGenerator<Ristretto>>::promote(&mut OsRng, keys.clone());
|
|
||||||
promotions.insert(i, promotion);
|
|
||||||
proofs.insert(
|
|
||||||
i,
|
|
||||||
GeneratorProof::<Ristretto>::read::<&[u8]>(&mut proof.serialize().as_ref()).unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete the promotion, and verify it worked
|
|
||||||
let new_group_key = AltGenerator::<Ristretto>::generator() * *recover_key(&keys).unwrap();
|
|
||||||
for (i, promoting) in promotions.drain() {
|
|
||||||
let promoted = promoting.complete(&clone_without(&proofs, &i)).unwrap();
|
|
||||||
assert_eq!(keys[usize::from(u16::from(i) - 1)].params(), promoted.params());
|
|
||||||
assert_eq!(
|
|
||||||
keys[usize::from(u16::from(i) - 1)].original_secret_share(),
|
|
||||||
promoted.original_secret_share()
|
|
||||||
);
|
|
||||||
assert_eq!(new_group_key, promoted.group_key());
|
|
||||||
for l in 0 .. PARTICIPANTS {
|
|
||||||
let verification_share =
|
|
||||||
promoted.original_verification_share(Participant::new(l + 1).unwrap());
|
|
||||||
assert_eq!(
|
|
||||||
AltGenerator::<Ristretto>::generator() * **keys[usize::from(l)].original_secret_share(),
|
|
||||||
verification_share
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "dleq"
|
|
||||||
version = "0.4.1"
|
|
||||||
description = "Implementation of single and cross-curve Discrete Log Equality proofs"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dleq"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.81"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rustversion = "1"
|
|
||||||
|
|
||||||
thiserror = { version = "2", default-features = false, optional = true }
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
|
|
||||||
digest = { version = "0.10", default-features = false }
|
|
||||||
transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false }
|
|
||||||
|
|
||||||
ff = { version = "0.13", default-features = false }
|
|
||||||
group = { version = "0.13", default-features = false }
|
|
||||||
|
|
||||||
multiexp = { path = "../multiexp", version = "0.4", default-features = false, features = ["batch"], optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
hex-literal = "0.4"
|
|
||||||
|
|
||||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
|
||||||
|
|
||||||
blake2 = "0.10"
|
|
||||||
|
|
||||||
k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic", "bits"] }
|
|
||||||
dalek-ff-group = { path = "../dalek-ff-group" }
|
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../transcript", features = ["recommended"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = ["thiserror?/std", "rand_core/std", "zeroize/std", "digest/std", "transcript/std", "ff/std", "multiexp?/std"]
|
|
||||||
serialize = ["std"]
|
|
||||||
|
|
||||||
# Needed for cross-group DLEqs
|
|
||||||
secure_capacity_difference = []
|
|
||||||
experimental = ["std", "thiserror", "multiexp"]
|
|
||||||
|
|
||||||
default = [
|
|
||||||
"std",
|
|
||||||
# Only applies to experimental, yet is default to ensure security
|
|
||||||
# experimental doesn't mandate it itself in case two curves with extreme
|
|
||||||
# capacity differences are desired to be used together, in which case the user
|
|
||||||
# must specify experimental without default features
|
|
||||||
"secure_capacity_difference"
|
|
||||||
]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2020-2023 Luke Parker, Lee Bousfield
|
|
||||||
|
|
||||||
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.
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Discrete Log Equality
|
|
||||||
|
|
||||||
Implementation of discrete log equality proofs for curves implementing
|
|
||||||
`ff`/`group`.
|
|
||||||
|
|
||||||
There is also a highly experimental cross-group DLEq proof, under
|
|
||||||
the `experimental` feature, which has no formal proofs available yet is
|
|
||||||
available here regardless.
|
|
||||||
|
|
||||||
This library, except for the `experimental` feature, was
|
|
||||||
[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf),
|
|
||||||
culminating in commit
|
|
||||||
[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06).
|
|
||||||
Any subsequent changes have not undergone auditing.
|
|
||||||
|
|
||||||
### Cross-Group DLEq
|
|
||||||
|
|
||||||
The present cross-group DLEq is based off
|
|
||||||
[MRL-0010](https://web.getmonero.org/resources/research-lab/pubs/MRL-0010.pdf),
|
|
||||||
which isn't computationally correct as while it proves both keys have the same
|
|
||||||
discrete logarithm for their `G'`/`H'` component, it doesn't prove a lack of a
|
|
||||||
`G`/`H` component. Accordingly, it was augmented with a pair of Schnorr Proof of
|
|
||||||
Knowledges, proving a known `G'`/`H'` component, guaranteeing a lack of a
|
|
||||||
`G`/`H` component (assuming an unknown relation between `G`/`H` and `G'`/`H'`).
|
|
||||||
|
|
||||||
The challenges for the ring signatures were also merged, removing one-element
|
|
||||||
from each bit's proof with only a slight reduction to challenge security (as
|
|
||||||
instead of being uniform over each scalar field, they're uniform over the
|
|
||||||
mutual bit capacity of each scalar field). This reduction is identical to the
|
|
||||||
one applied to the proved-for scalar, and accordingly should not reduce overall
|
|
||||||
security. It does create a lack of domain separation, yet that shouldn't be an
|
|
||||||
issue.
|
|
||||||
|
|
||||||
The following variants are available:
|
|
||||||
|
|
||||||
- `ClassicLinear`. This is only for reference purposes, being the above
|
|
||||||
described proof, with no further optimizations.
|
|
||||||
|
|
||||||
- `ConciseLinear`. This proves for 2 bits at a time, not increasing the
|
|
||||||
signature size for both bits yet decreasing the amount of
|
|
||||||
commitments/challenges in total.
|
|
||||||
|
|
||||||
- `EfficientLinear`. This provides ring signatures in the form
|
|
||||||
`((R_G, R_H), s)`, instead of `(e, s)`, and accordingly enables a batch
|
|
||||||
verification of their final step. It is the most performant, and also the
|
|
||||||
largest, option.
|
|
||||||
|
|
||||||
- `CompromiseLinear`. This provides signatures in the form `((R_G, R_H), s)` AND
|
|
||||||
proves for 2-bits at a time. While this increases the amount of steps in
|
|
||||||
verifying the ring signatures, which aren't batch verified, and decreases the
|
|
||||||
amount of items batched (an operation which grows in efficiency with
|
|
||||||
quantity), it strikes a balance between speed and size.
|
|
||||||
|
|
||||||
The following numbers are from benchmarks performed with k256/curve25519_dalek
|
|
||||||
on a Intel i7-118567:
|
|
||||||
|
|
||||||
| Algorithm | Size | Verification Time |
|
|
||||||
|--------------------|-------------------------|-------------------|
|
|
||||||
| `ClassicLinear` | 56829 bytes (+27%) | 157ms (0%) |
|
|
||||||
| `ConciseLinear` | 44607 bytes (Reference) | 156ms (Reference) |
|
|
||||||
| `EfficientLinear` | 65145 bytes (+46%) | 122ms (-22%) |
|
|
||||||
| `CompromiseLinear` | 48765 bytes (+9%) | 137ms (-12%) |
|
|
||||||
|
|
||||||
`CompromiseLinear` is the best choice by only being marginally sub-optimal
|
|
||||||
regarding size, yet still achieving most of the desired performance
|
|
||||||
improvements. That said, neither the original postulation (which had flaws) nor
|
|
||||||
any construction here has been proven nor audited. Accordingly, they are solely
|
|
||||||
experimental, and none are recommended.
|
|
||||||
|
|
||||||
All proofs are suffixed "Linear" in the hope a logarithmic proof makes itself
|
|
||||||
available, which would likely immediately become the most efficient option.
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use transcript::Transcript;
|
|
||||||
|
|
||||||
use group::{
|
|
||||||
ff::{Field, PrimeFieldBits},
|
|
||||||
prime::PrimeGroup,
|
|
||||||
};
|
|
||||||
|
|
||||||
use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
use crate::cross_group::{
|
|
||||||
Generators, DLEqError,
|
|
||||||
scalar::{scalar_convert, mutual_scalar_from_bytes},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use ff::PrimeField;
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use crate::{read_scalar, cross_group::read_point};
|
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub(crate) enum Re<G0: PrimeGroup, G1: PrimeGroup> {
|
|
||||||
R(G0, G1),
|
|
||||||
// Merged challenges have a slight security reduction, yet one already applied to the scalar
|
|
||||||
// being proven for, and this saves ~8kb. Alternatively, challenges could be redefined as a seed,
|
|
||||||
// present here, which is then hashed for each of the two challenges, remaining unbiased/unique
|
|
||||||
// while maintaining the bandwidth savings, yet also while adding 252 hashes for
|
|
||||||
// Secp256k1/Ed25519
|
|
||||||
e(G0::Scalar),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<G0: PrimeGroup, G1: PrimeGroup> Re<G0, G1> {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub(crate) fn R_default() -> Re<G0, G1> {
|
|
||||||
Re::R(G0::identity(), G1::identity())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn e_default() -> Re<G0, G1> {
|
|
||||||
Re::e(G0::Scalar::ZERO)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub(crate) struct Aos<G0: PrimeGroup + Zeroize, G1: PrimeGroup + Zeroize, const RING_LEN: usize> {
|
|
||||||
Re_0: Re<G0, G1>,
|
|
||||||
s: [(G0::Scalar, G1::Scalar); RING_LEN],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<
|
|
||||||
G0: PrimeGroup<Scalar: PrimeFieldBits + Zeroize> + Zeroize,
|
|
||||||
G1: PrimeGroup<Scalar: PrimeFieldBits + Zeroize> + Zeroize,
|
|
||||||
const RING_LEN: usize,
|
|
||||||
> Aos<G0, G1, RING_LEN>
|
|
||||||
{
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
fn nonces<T: Transcript>(mut transcript: T, nonces: (G0, G1)) -> (G0::Scalar, G1::Scalar) {
|
|
||||||
transcript.domain_separate(b"aos_membership_proof");
|
|
||||||
transcript.append_message(b"ring_len", u8::try_from(RING_LEN).unwrap().to_le_bytes());
|
|
||||||
transcript.append_message(b"nonce_0", nonces.0.to_bytes());
|
|
||||||
transcript.append_message(b"nonce_1", nonces.1.to_bytes());
|
|
||||||
mutual_scalar_from_bytes(transcript.challenge(b"challenge").as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
fn R(
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
s: (G0::Scalar, G1::Scalar),
|
|
||||||
A: (G0, G1),
|
|
||||||
e: (G0::Scalar, G1::Scalar),
|
|
||||||
) -> (G0, G1) {
|
|
||||||
(((generators.0.alt * s.0) - (A.0 * e.0)), ((generators.1.alt * s.1) - (A.1 * e.1)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case, clippy::type_complexity)]
|
|
||||||
fn R_batch(
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
s: (G0::Scalar, G1::Scalar),
|
|
||||||
A: (G0, G1),
|
|
||||||
e: (G0::Scalar, G1::Scalar),
|
|
||||||
) -> (Vec<(G0::Scalar, G0)>, Vec<(G1::Scalar, G1)>) {
|
|
||||||
(vec![(-s.0, generators.0.alt), (e.0, A.0)], vec![(-s.1, generators.1.alt), (e.1, A.1)])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
fn R_nonces<T: Transcript>(
|
|
||||||
transcript: T,
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
s: (G0::Scalar, G1::Scalar),
|
|
||||||
A: (G0, G1),
|
|
||||||
e: (G0::Scalar, G1::Scalar),
|
|
||||||
) -> (G0::Scalar, G1::Scalar) {
|
|
||||||
Self::nonces(transcript, Self::R(generators, s, A, e))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub(crate) fn prove<R: RngCore + CryptoRng, T: Clone + Transcript>(
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &T,
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
ring: &[(G0, G1)],
|
|
||||||
mut actual: usize,
|
|
||||||
blinding_key: &mut (G0::Scalar, G1::Scalar),
|
|
||||||
mut Re_0: Re<G0, G1>,
|
|
||||||
) -> Self {
|
|
||||||
// While it is possible to use larger values, it's not efficient to do so
|
|
||||||
// 2 + 2 == 2^2, yet 2 + 2 + 2 < 2^3
|
|
||||||
debug_assert!((RING_LEN == 2) || (RING_LEN == 4));
|
|
||||||
debug_assert_eq!(RING_LEN, ring.len());
|
|
||||||
|
|
||||||
let mut s = [(G0::Scalar::ZERO, G1::Scalar::ZERO); RING_LEN];
|
|
||||||
|
|
||||||
let mut r = (G0::Scalar::random(&mut *rng), G1::Scalar::random(&mut *rng));
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let original_R = (generators.0.alt * r.0, generators.1.alt * r.1);
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut R = original_R;
|
|
||||||
|
|
||||||
for i in ((actual + 1) ..= (actual + RING_LEN)).map(|i| i % RING_LEN) {
|
|
||||||
let e = Self::nonces(transcript.clone(), R);
|
|
||||||
if i == 0 {
|
|
||||||
match Re_0 {
|
|
||||||
Re::R(ref mut R0_0, ref mut R1_0) => {
|
|
||||||
*R0_0 = R.0;
|
|
||||||
*R1_0 = R.1
|
|
||||||
}
|
|
||||||
Re::e(ref mut e_0) => *e_0 = e.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Solve for the real index
|
|
||||||
if i == actual {
|
|
||||||
s[i] = (r.0 + (e.0 * blinding_key.0), r.1 + (e.1 * blinding_key.1));
|
|
||||||
debug_assert_eq!(Self::R(generators, s[i], ring[actual], e), original_R);
|
|
||||||
actual.zeroize();
|
|
||||||
blinding_key.0.zeroize();
|
|
||||||
blinding_key.1.zeroize();
|
|
||||||
r.0.zeroize();
|
|
||||||
r.1.zeroize();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a decoy response
|
|
||||||
s[i] = (G0::Scalar::random(&mut *rng), G1::Scalar::random(&mut *rng));
|
|
||||||
R = Self::R(generators, s[i], ring[i], e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Aos { Re_0, s }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumes the ring has already been transcripted in some form. Critically insecure if it hasn't
|
|
||||||
pub(crate) fn verify<R: RngCore + CryptoRng, T: Clone + Transcript>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &T,
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
batch: &mut (BatchVerifier<(), G0>, BatchVerifier<(), G1>),
|
|
||||||
ring: &[(G0, G1)],
|
|
||||||
) -> Result<(), DLEqError> {
|
|
||||||
debug_assert!((RING_LEN == 2) || (RING_LEN == 4));
|
|
||||||
debug_assert_eq!(RING_LEN, ring.len());
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
match self.Re_0 {
|
|
||||||
Re::R(R0_0, R1_0) => {
|
|
||||||
let mut e = Self::nonces(transcript.clone(), (R0_0, R1_0));
|
|
||||||
#[allow(clippy::needless_range_loop)]
|
|
||||||
for i in 0 .. (RING_LEN - 1) {
|
|
||||||
e = Self::R_nonces(transcript.clone(), generators, self.s[i], ring[i], e);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut statements =
|
|
||||||
Self::R_batch(generators, *self.s.last().unwrap(), *ring.last().unwrap(), e);
|
|
||||||
statements.0.push((G0::Scalar::ONE, R0_0));
|
|
||||||
statements.1.push((G1::Scalar::ONE, R1_0));
|
|
||||||
batch.0.queue(&mut *rng, (), statements.0);
|
|
||||||
batch.1.queue(&mut *rng, (), statements.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Re::e(e_0) => {
|
|
||||||
let e_0 = (e_0, scalar_convert(e_0).ok_or(DLEqError::InvalidChallenge)?);
|
|
||||||
let mut e = None;
|
|
||||||
#[allow(clippy::needless_range_loop)]
|
|
||||||
for i in 0 .. RING_LEN {
|
|
||||||
e = Some(Self::R_nonces(
|
|
||||||
transcript.clone(),
|
|
||||||
generators,
|
|
||||||
self.s[i],
|
|
||||||
ring[i],
|
|
||||||
e.unwrap_or(e_0),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Will panic if the above loop is never run somehow
|
|
||||||
// If e wasn't an Option, and instead initially set to e_0, it'd always pass
|
|
||||||
if e_0 != e.unwrap() {
|
|
||||||
Err(DLEqError::InvalidProof)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub(crate) fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
match self.Re_0 {
|
|
||||||
Re::R(R0, R1) => {
|
|
||||||
w.write_all(R0.to_bytes().as_ref())?;
|
|
||||||
w.write_all(R1.to_bytes().as_ref())?;
|
|
||||||
}
|
|
||||||
Re::e(e) => w.write_all(e.to_repr().as_ref())?,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0 .. RING_LEN {
|
|
||||||
w.write_all(self.s[i].0.to_repr().as_ref())?;
|
|
||||||
w.write_all(self.s[i].1.to_repr().as_ref())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub(crate) fn read<R: Read>(r: &mut R, mut Re_0: Re<G0, G1>) -> std::io::Result<Self> {
|
|
||||||
match Re_0 {
|
|
||||||
Re::R(ref mut R0, ref mut R1) => {
|
|
||||||
*R0 = read_point(r)?;
|
|
||||||
*R1 = read_point(r)?
|
|
||||||
}
|
|
||||||
Re::e(ref mut e) => *e = read_scalar(r)?,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut s = [(G0::Scalar::ZERO, G1::Scalar::ZERO); RING_LEN];
|
|
||||||
for s in &mut s {
|
|
||||||
*s = (read_scalar(r)?, read_scalar(r)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Aos { Re_0, s })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use transcript::Transcript;
|
|
||||||
|
|
||||||
use group::{ff::PrimeFieldBits, prime::PrimeGroup};
|
|
||||||
use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
use crate::cross_group::{
|
|
||||||
Generators, DLEqError,
|
|
||||||
aos::{Re, Aos},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use crate::cross_group::read_point;
|
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
pub(crate) enum BitSignature {
|
|
||||||
ClassicLinear,
|
|
||||||
ConciseLinear,
|
|
||||||
EfficientLinear,
|
|
||||||
CompromiseLinear,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BitSignature {
|
|
||||||
pub(crate) const fn to_u8(&self) -> u8 {
|
|
||||||
match self {
|
|
||||||
BitSignature::ClassicLinear => 0,
|
|
||||||
BitSignature::ConciseLinear => 1,
|
|
||||||
BitSignature::EfficientLinear => 2,
|
|
||||||
BitSignature::CompromiseLinear => 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn from(algorithm: u8) -> BitSignature {
|
|
||||||
match algorithm {
|
|
||||||
0 => BitSignature::ClassicLinear,
|
|
||||||
1 => BitSignature::ConciseLinear,
|
|
||||||
2 => BitSignature::EfficientLinear,
|
|
||||||
3 => BitSignature::CompromiseLinear,
|
|
||||||
_ => panic!("Unknown algorithm"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn bits(&self) -> u8 {
|
|
||||||
match self {
|
|
||||||
BitSignature::ClassicLinear | BitSignature::EfficientLinear => 1,
|
|
||||||
BitSignature::ConciseLinear | BitSignature::CompromiseLinear => 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn ring_len(&self) -> usize {
|
|
||||||
2_usize.pow(self.bits() as u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn aos_form<G0: PrimeGroup, G1: PrimeGroup>(&self) -> Re<G0, G1> {
|
|
||||||
match self {
|
|
||||||
BitSignature::ClassicLinear | BitSignature::ConciseLinear => Re::e_default(),
|
|
||||||
BitSignature::EfficientLinear | BitSignature::CompromiseLinear => Re::R_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub(crate) struct Bits<
|
|
||||||
G0: PrimeGroup + Zeroize,
|
|
||||||
G1: PrimeGroup + Zeroize,
|
|
||||||
const SIGNATURE: u8,
|
|
||||||
const RING_LEN: usize,
|
|
||||||
> {
|
|
||||||
pub(crate) commitments: (G0, G1),
|
|
||||||
signature: Aos<G0, G1, RING_LEN>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<
|
|
||||||
G0: PrimeGroup<Scalar: PrimeFieldBits + Zeroize> + Zeroize,
|
|
||||||
G1: PrimeGroup<Scalar: PrimeFieldBits + Zeroize> + Zeroize,
|
|
||||||
const SIGNATURE: u8,
|
|
||||||
const RING_LEN: usize,
|
|
||||||
> Bits<G0, G1, SIGNATURE, RING_LEN>
|
|
||||||
{
|
|
||||||
fn transcript<T: Transcript>(transcript: &mut T, i: usize, commitments: (G0, G1)) {
|
|
||||||
transcript.domain_separate(b"bits");
|
|
||||||
transcript.append_message(b"group", u16::try_from(i).unwrap().to_le_bytes());
|
|
||||||
transcript.append_message(b"commitment_0", commitments.0.to_bytes());
|
|
||||||
transcript.append_message(b"commitment_1", commitments.1.to_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ring(pow_2: (G0, G1), commitments: (G0, G1)) -> Vec<(G0, G1)> {
|
|
||||||
let mut res = vec![commitments; RING_LEN];
|
|
||||||
for i in 1 .. RING_LEN {
|
|
||||||
res[i] = (res[i - 1].0 - pow_2.0, res[i - 1].1 - pow_2.1);
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shift(pow_2: &mut (G0, G1)) {
|
|
||||||
for _ in 0 .. BitSignature::from(SIGNATURE).bits() {
|
|
||||||
pow_2.0 = pow_2.0.double();
|
|
||||||
pow_2.1 = pow_2.1.double();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn prove<R: RngCore + CryptoRng, T: Clone + Transcript>(
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
i: usize,
|
|
||||||
pow_2: &mut (G0, G1),
|
|
||||||
mut bits: u8,
|
|
||||||
blinding_key: &mut (G0::Scalar, G1::Scalar),
|
|
||||||
) -> Self {
|
|
||||||
let mut commitments =
|
|
||||||
((generators.0.alt * blinding_key.0), (generators.1.alt * blinding_key.1));
|
|
||||||
commitments.0 += pow_2.0 * G0::Scalar::from(bits.into());
|
|
||||||
commitments.1 += pow_2.1 * G1::Scalar::from(bits.into());
|
|
||||||
|
|
||||||
Self::transcript(transcript, i, commitments);
|
|
||||||
|
|
||||||
let signature = Aos::prove(
|
|
||||||
rng,
|
|
||||||
transcript,
|
|
||||||
generators,
|
|
||||||
&Self::ring(*pow_2, commitments),
|
|
||||||
usize::from(bits),
|
|
||||||
blinding_key,
|
|
||||||
BitSignature::from(SIGNATURE).aos_form(),
|
|
||||||
);
|
|
||||||
bits.zeroize();
|
|
||||||
|
|
||||||
Self::shift(pow_2);
|
|
||||||
Bits { commitments, signature }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn verify<R: RngCore + CryptoRng, T: Clone + Transcript>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
batch: &mut (BatchVerifier<(), G0>, BatchVerifier<(), G1>),
|
|
||||||
i: usize,
|
|
||||||
pow_2: &mut (G0, G1),
|
|
||||||
) -> Result<(), DLEqError> {
|
|
||||||
Self::transcript(transcript, i, self.commitments);
|
|
||||||
|
|
||||||
self.signature.verify(
|
|
||||||
rng,
|
|
||||||
transcript,
|
|
||||||
generators,
|
|
||||||
batch,
|
|
||||||
&Self::ring(*pow_2, self.commitments),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Self::shift(pow_2);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub(crate) fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
|
||||||
w.write_all(self.commitments.0.to_bytes().as_ref())?;
|
|
||||||
w.write_all(self.commitments.1.to_bytes().as_ref())?;
|
|
||||||
self.signature.write(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub(crate) fn read<R: Read>(r: &mut R) -> std::io::Result<Self> {
|
|
||||||
Ok(Bits {
|
|
||||||
commitments: (read_point(r)?, read_point(r)?),
|
|
||||||
signature: Aos::read(r, BitSignature::from(SIGNATURE).aos_form())?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
use core::ops::{Deref, DerefMut};
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use std::io::{self, Read, Write};
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
|
||||||
|
|
||||||
use digest::{Digest, HashMarker};
|
|
||||||
|
|
||||||
use transcript::Transcript;
|
|
||||||
|
|
||||||
use group::{
|
|
||||||
ff::{Field, PrimeField, PrimeFieldBits},
|
|
||||||
prime::PrimeGroup,
|
|
||||||
};
|
|
||||||
use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
/// Scalar utilities.
|
|
||||||
pub mod scalar;
|
|
||||||
use scalar::{scalar_convert, mutual_scalar_from_bytes};
|
|
||||||
|
|
||||||
pub(crate) mod schnorr;
|
|
||||||
use self::schnorr::SchnorrPoK;
|
|
||||||
|
|
||||||
pub(crate) mod aos;
|
|
||||||
|
|
||||||
mod bits;
|
|
||||||
use bits::{BitSignature, Bits};
|
|
||||||
|
|
||||||
// Use black_box when possible
|
|
||||||
#[rustversion::since(1.66)]
|
|
||||||
use core::hint::black_box;
|
|
||||||
#[rustversion::before(1.66)]
|
|
||||||
fn black_box<T>(val: T) -> T {
|
|
||||||
val
|
|
||||||
}
|
|
||||||
|
|
||||||
fn u8_from_bool(bit_ref: &mut bool) -> u8 {
|
|
||||||
let bit_ref = black_box(bit_ref);
|
|
||||||
|
|
||||||
let mut bit = black_box(*bit_ref);
|
|
||||||
#[allow(clippy::cast_lossless)]
|
|
||||||
let res = black_box(bit as u8);
|
|
||||||
bit.zeroize();
|
|
||||||
debug_assert!((res | 1) == 1);
|
|
||||||
|
|
||||||
bit_ref.zeroize();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub(crate) fn read_point<R: Read, G: PrimeGroup>(r: &mut R) -> io::Result<G> {
|
|
||||||
let mut repr = G::Repr::default();
|
|
||||||
r.read_exact(repr.as_mut())?;
|
|
||||||
let point = G::from_bytes(&repr);
|
|
||||||
let Some(point) = Option::<G>::from(point) else { Err(io::Error::other("invalid point"))? };
|
|
||||||
if point.to_bytes().as_ref() != repr.as_ref() {
|
|
||||||
Err(io::Error::other("non-canonical point"))?;
|
|
||||||
}
|
|
||||||
Ok(point)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A pair of generators, one committing to values (primary), one blinding (alt), for an elliptic
|
|
||||||
/// curve.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct Generators<G: PrimeGroup> {
|
|
||||||
/// The generator used to commit to values.
|
|
||||||
///
|
|
||||||
/// This should likely be the curve's traditional 'basepoint'.
|
|
||||||
pub primary: G,
|
|
||||||
/// The generator used to blind values. This must be distinct from the primary generator.
|
|
||||||
pub alt: G,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<G: PrimeGroup> Generators<G> {
|
|
||||||
/// Create a new set of generators.
|
|
||||||
pub fn new(primary: G, alt: G) -> Option<Generators<G>> {
|
|
||||||
if primary == alt {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
Some(Generators { primary, alt })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript<T: Transcript>(&self, transcript: &mut T) {
|
|
||||||
transcript.domain_separate(b"generators");
|
|
||||||
transcript.append_message(b"primary", self.primary.to_bytes());
|
|
||||||
transcript.append_message(b"alternate", self.alt.to_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error for cross-group DLEq proofs.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
|
|
||||||
pub enum DLEqError {
|
|
||||||
/// Invalid proof length.
|
|
||||||
#[error("invalid proof length")]
|
|
||||||
InvalidProofLength,
|
|
||||||
/// Invalid challenge.
|
|
||||||
#[error("invalid challenge")]
|
|
||||||
InvalidChallenge,
|
|
||||||
/// Invalid proof.
|
|
||||||
#[error("invalid proof")]
|
|
||||||
InvalidProof,
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should never be directly instantiated and uses a u8 to represent internal values
|
|
||||||
// Any external usage is likely invalid
|
|
||||||
#[doc(hidden)]
|
|
||||||
// Debug would be such a dump of data this likely isn't helpful, but at least it's available to
|
|
||||||
// anyone who wants it
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct __DLEqProof<
|
|
||||||
G0: PrimeGroup<Scalar: PrimeFieldBits> + Zeroize,
|
|
||||||
G1: PrimeGroup<Scalar: PrimeFieldBits> + Zeroize,
|
|
||||||
const SIGNATURE: u8,
|
|
||||||
const RING_LEN: usize,
|
|
||||||
const REMAINDER_RING_LEN: usize,
|
|
||||||
> {
|
|
||||||
bits: Vec<Bits<G0, G1, SIGNATURE, RING_LEN>>,
|
|
||||||
remainder: Option<Bits<G0, G1, SIGNATURE, REMAINDER_RING_LEN>>,
|
|
||||||
poks: (SchnorrPoK<G0>, SchnorrPoK<G1>),
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! dleq {
|
|
||||||
($doc_str: expr, $name: ident, $signature: expr, $remainder: literal,) => {
|
|
||||||
#[doc = $doc_str]
|
|
||||||
pub type $name<G0, G1> = __DLEqProof<
|
|
||||||
G0,
|
|
||||||
G1,
|
|
||||||
{ $signature.to_u8() },
|
|
||||||
{ $signature.ring_len() },
|
|
||||||
// There may not be a remainder, yet if there is one, it'll be just one bit
|
|
||||||
// A ring for one bit has a RING_LEN of 2
|
|
||||||
{
|
|
||||||
if $remainder {
|
|
||||||
2
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proves for 1-bit at a time with the signature form (e, s), as originally described in MRL-0010.
|
|
||||||
// Uses a merged challenge, unlike MRL-0010, for the ring signature, saving an element from each
|
|
||||||
// bit and removing a hash while slightly reducing challenge security. This security reduction is
|
|
||||||
// already applied to the scalar being proven for, a result of the requirement it's mutually valid
|
|
||||||
// over both scalar fields, hence its application here as well. This is mainly here as a point of
|
|
||||||
// reference for the following DLEq proofs, all which use merged challenges, and isn't performant
|
|
||||||
// in comparison to the others
|
|
||||||
dleq!(
|
|
||||||
"The DLEq proof described in MRL-0010.",
|
|
||||||
ClassicLinearDLEq,
|
|
||||||
BitSignature::ClassicLinear,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Proves for 2-bits at a time to save 3/7 elements of every other bit
|
|
||||||
// <9% smaller than CompromiseLinear, yet ~12% slower
|
|
||||||
dleq!(
|
|
||||||
"A DLEq proof modified from MRL-0010, proving for two bits at a time to save on space.",
|
|
||||||
ConciseLinearDLEq,
|
|
||||||
BitSignature::ConciseLinear,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Uses AOS signatures of the form R, s, to enable the final step of the ring signature to be
|
|
||||||
// batch verified, at the cost of adding an additional element per bit
|
|
||||||
dleq!(
|
|
||||||
"
|
|
||||||
A DLEq proof modified from MRL-0010, using R, s forms instead of c, s forms to enable batch
|
|
||||||
verification at the cost of space usage.
|
|
||||||
",
|
|
||||||
EfficientLinearDLEq,
|
|
||||||
BitSignature::EfficientLinear,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Proves for 2-bits at a time while using the R, s form. This saves 3/7 elements of every other
|
|
||||||
// bit, while adding 1 element to every bit, and is more efficient than ConciseLinear yet less
|
|
||||||
// efficient than EfficientLinear due to having more ring signature steps which aren't batched
|
|
||||||
// >25% smaller than EfficientLinear and just 11% slower, making it the recommended option
|
|
||||||
dleq!(
|
|
||||||
"
|
|
||||||
A DLEq proof modified from MRL-0010, using R, s forms instead of c, s forms, while proving for
|
|
||||||
two bits at a time, to enable batch verification and take advantage of space savings.
|
|
||||||
|
|
||||||
This isn't quite as efficient as EfficientLinearDLEq, and isn't as compact as
|
|
||||||
ConciseLinearDLEq, yet strikes a strong balance of performance and conciseness.
|
|
||||||
",
|
|
||||||
CompromiseLinearDLEq,
|
|
||||||
BitSignature::CompromiseLinear,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
impl<
|
|
||||||
G0: PrimeGroup<Scalar: PrimeFieldBits + Zeroize> + Zeroize,
|
|
||||||
G1: PrimeGroup<Scalar: PrimeFieldBits + Zeroize> + Zeroize,
|
|
||||||
const SIGNATURE: u8,
|
|
||||||
const RING_LEN: usize,
|
|
||||||
const REMAINDER_RING_LEN: usize,
|
|
||||||
> __DLEqProof<G0, G1, SIGNATURE, RING_LEN, REMAINDER_RING_LEN>
|
|
||||||
{
|
|
||||||
pub(crate) fn transcript<T: Transcript>(
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
keys: (G0, G1),
|
|
||||||
) {
|
|
||||||
transcript.domain_separate(b"cross_group_dleq");
|
|
||||||
generators.0.transcript(transcript);
|
|
||||||
generators.1.transcript(transcript);
|
|
||||||
transcript.domain_separate(b"points");
|
|
||||||
transcript.append_message(b"point_0", keys.0.to_bytes());
|
|
||||||
transcript.append_message(b"point_1", keys.1.to_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn blinding_key<R: RngCore + CryptoRng, F: PrimeField>(
|
|
||||||
rng: &mut R,
|
|
||||||
total: &mut F,
|
|
||||||
last: bool,
|
|
||||||
) -> F {
|
|
||||||
let blinding_key = if last { -*total } else { F::random(&mut *rng) };
|
|
||||||
*total += blinding_key;
|
|
||||||
blinding_key
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reconstruct_keys(&self) -> (G0, G1) {
|
|
||||||
let mut res = (
|
|
||||||
self.bits.iter().map(|bit| bit.commitments.0).sum::<G0>(),
|
|
||||||
self.bits.iter().map(|bit| bit.commitments.1).sum::<G1>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(bit) = &self.remainder {
|
|
||||||
res.0 += bit.commitments.0;
|
|
||||||
res.1 += bit.commitments.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
fn prove_internal<R: RngCore + CryptoRng, T: Clone + Transcript>(
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
f: (Zeroizing<G0::Scalar>, Zeroizing<G1::Scalar>),
|
|
||||||
) -> (Self, (Zeroizing<G0::Scalar>, Zeroizing<G1::Scalar>)) {
|
|
||||||
Self::transcript(
|
|
||||||
transcript,
|
|
||||||
generators,
|
|
||||||
((generators.0.primary * f.0.deref()), (generators.1.primary * f.1.deref())),
|
|
||||||
);
|
|
||||||
|
|
||||||
let poks = (
|
|
||||||
SchnorrPoK::<G0>::prove(rng, transcript, generators.0.primary, &f.0),
|
|
||||||
SchnorrPoK::<G1>::prove(rng, transcript, generators.1.primary, &f.1),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut blinding_key_total = (G0::Scalar::ZERO, G1::Scalar::ZERO);
|
|
||||||
let mut blinding_key = |rng: &mut R, last| {
|
|
||||||
let blinding_key = (
|
|
||||||
Self::blinding_key(&mut *rng, &mut blinding_key_total.0, last),
|
|
||||||
Self::blinding_key(&mut *rng, &mut blinding_key_total.1, last),
|
|
||||||
);
|
|
||||||
if last {
|
|
||||||
debug_assert_eq!(blinding_key_total.0, G0::Scalar::ZERO);
|
|
||||||
debug_assert_eq!(blinding_key_total.1, G1::Scalar::ZERO);
|
|
||||||
}
|
|
||||||
blinding_key
|
|
||||||
};
|
|
||||||
|
|
||||||
let capacity = usize::try_from(G0::Scalar::CAPACITY.min(G1::Scalar::CAPACITY)).unwrap();
|
|
||||||
let bits_per_group = usize::from(BitSignature::from(SIGNATURE).bits());
|
|
||||||
|
|
||||||
let mut pow_2 = (generators.0.primary, generators.1.primary);
|
|
||||||
|
|
||||||
let mut raw_bits = f.0.to_le_bits();
|
|
||||||
let mut bits = Vec::with_capacity(capacity);
|
|
||||||
let mut these_bits: u8 = 0;
|
|
||||||
// Needed to zero out the bits
|
|
||||||
#[allow(unused_assignments)]
|
|
||||||
for (i, mut bit) in raw_bits.iter_mut().enumerate() {
|
|
||||||
if i == capacity {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accumulate this bit
|
|
||||||
let mut bit = u8_from_bool(bit.deref_mut());
|
|
||||||
these_bits |= bit << (i % bits_per_group);
|
|
||||||
bit.zeroize();
|
|
||||||
|
|
||||||
if (i % bits_per_group) == (bits_per_group - 1) {
|
|
||||||
let last = i == (capacity - 1);
|
|
||||||
let mut blinding_key = blinding_key(&mut *rng, last);
|
|
||||||
bits.push(Bits::prove(
|
|
||||||
&mut *rng,
|
|
||||||
transcript,
|
|
||||||
generators,
|
|
||||||
i / bits_per_group,
|
|
||||||
&mut pow_2,
|
|
||||||
these_bits,
|
|
||||||
&mut blinding_key,
|
|
||||||
));
|
|
||||||
these_bits.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug_assert_eq!(bits.len(), capacity / bits_per_group);
|
|
||||||
|
|
||||||
let mut remainder = None;
|
|
||||||
if capacity != ((capacity / bits_per_group) * bits_per_group) {
|
|
||||||
let mut blinding_key = blinding_key(&mut *rng, true);
|
|
||||||
remainder = Some(Bits::prove(
|
|
||||||
&mut *rng,
|
|
||||||
transcript,
|
|
||||||
generators,
|
|
||||||
capacity / bits_per_group,
|
|
||||||
&mut pow_2,
|
|
||||||
these_bits,
|
|
||||||
&mut blinding_key,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
these_bits.zeroize();
|
|
||||||
|
|
||||||
let proof = __DLEqProof { bits, remainder, poks };
|
|
||||||
debug_assert_eq!(
|
|
||||||
proof.reconstruct_keys(),
|
|
||||||
(generators.0.primary * f.0.deref(), generators.1.primary * f.1.deref())
|
|
||||||
);
|
|
||||||
(proof, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prove the Cross-Group Discrete Log Equality for the points derived from the scalar created as
|
|
||||||
/// the output of the passed in Digest.
|
|
||||||
///
|
|
||||||
/// Given the non-standard requirements to achieve uniformity, needing to be < 2^x instead of
|
|
||||||
/// less than a prime moduli, this is the simplest way to safely and securely generate a Scalar,
|
|
||||||
/// without risk of failure nor bias.
|
|
||||||
///
|
|
||||||
/// It also ensures a lack of determinable relation between keys, guaranteeing security in the
|
|
||||||
/// currently expected use case for this, atomic swaps, where each swap leaks the key. Knowing
|
|
||||||
/// the relationship between keys would allow breaking all swaps after just one.
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
pub fn prove<R: RngCore + CryptoRng, T: Clone + Transcript, D: Digest + HashMarker>(
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
digest: D,
|
|
||||||
) -> (Self, (Zeroizing<G0::Scalar>, Zeroizing<G1::Scalar>)) {
|
|
||||||
// This pattern theoretically prevents the compiler from moving it, so our protection against
|
|
||||||
// a copy remaining un-zeroized is actually what's causing a copy. There's still a feeling of
|
|
||||||
// safety granted by it, even if there's a loss in performance.
|
|
||||||
let (mut f0, mut f1) =
|
|
||||||
mutual_scalar_from_bytes::<G0::Scalar, G1::Scalar>(digest.finalize().as_ref());
|
|
||||||
let f = (Zeroizing::new(f0), Zeroizing::new(f1));
|
|
||||||
f0.zeroize();
|
|
||||||
f1.zeroize();
|
|
||||||
|
|
||||||
Self::prove_internal(rng, transcript, generators, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prove the Cross-Group Discrete Log Equality for the points derived from the scalar passed in,
|
|
||||||
/// failing if it's not mutually valid.
|
|
||||||
///
|
|
||||||
/// This allows for rejection sampling externally derived scalars until they're safely usable,
|
|
||||||
/// as needed.
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
pub fn prove_without_bias<R: RngCore + CryptoRng, T: Clone + Transcript>(
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
f0: Zeroizing<G0::Scalar>,
|
|
||||||
) -> Option<(Self, (Zeroizing<G0::Scalar>, Zeroizing<G1::Scalar>))> {
|
|
||||||
scalar_convert(*f0.deref()) // scalar_convert will zeroize it, though this is unfortunate
|
|
||||||
.map(|f1| Self::prove_internal(rng, transcript, generators, (f0, Zeroizing::new(f1))))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify a Cross-Group Discrete Log Equality proof, returning the points proven for.
|
|
||||||
pub fn verify<R: RngCore + CryptoRng, T: Clone + Transcript>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: (Generators<G0>, Generators<G1>),
|
|
||||||
) -> Result<(G0, G1), DLEqError> {
|
|
||||||
let capacity = usize::try_from(G0::Scalar::CAPACITY.min(G1::Scalar::CAPACITY)).unwrap();
|
|
||||||
let bits_per_group = usize::from(BitSignature::from(SIGNATURE).bits());
|
|
||||||
let has_remainder = (capacity % bits_per_group) != 0;
|
|
||||||
|
|
||||||
// These shouldn't be possible, as locally created and deserialized proofs should be properly
|
|
||||||
// formed in these regards, yet it doesn't hurt to check and would be problematic if true
|
|
||||||
if (self.bits.len() != (capacity / bits_per_group)) ||
|
|
||||||
((self.remainder.is_none() && has_remainder) ||
|
|
||||||
(self.remainder.is_some() && !has_remainder))
|
|
||||||
{
|
|
||||||
return Err(DLEqError::InvalidProofLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
let keys = self.reconstruct_keys();
|
|
||||||
Self::transcript(transcript, generators, keys);
|
|
||||||
|
|
||||||
let batch_capacity = match BitSignature::from(SIGNATURE) {
|
|
||||||
BitSignature::ClassicLinear | BitSignature::ConciseLinear => 3,
|
|
||||||
BitSignature::EfficientLinear | BitSignature::CompromiseLinear => (self.bits.len() + 1) * 3,
|
|
||||||
};
|
|
||||||
let mut batch = (BatchVerifier::new(batch_capacity), BatchVerifier::new(batch_capacity));
|
|
||||||
|
|
||||||
self.poks.0.verify(&mut *rng, transcript, generators.0.primary, keys.0, &mut batch.0);
|
|
||||||
self.poks.1.verify(&mut *rng, transcript, generators.1.primary, keys.1, &mut batch.1);
|
|
||||||
|
|
||||||
let mut pow_2 = (generators.0.primary, generators.1.primary);
|
|
||||||
for (i, bits) in self.bits.iter().enumerate() {
|
|
||||||
bits.verify(&mut *rng, transcript, generators, &mut batch, i, &mut pow_2)?;
|
|
||||||
}
|
|
||||||
if let Some(bit) = &self.remainder {
|
|
||||||
bit.verify(&mut *rng, transcript, generators, &mut batch, self.bits.len(), &mut pow_2)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!batch.0.verify_vartime()) || (!batch.1.verify_vartime()) {
|
|
||||||
Err(DLEqError::InvalidProof)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a Cross-Group Discrete Log Equality proof to a type satisfying std::io::Write.
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
for bit in &self.bits {
|
|
||||||
bit.write(w)?;
|
|
||||||
}
|
|
||||||
if let Some(bit) = &self.remainder {
|
|
||||||
bit.write(w)?;
|
|
||||||
}
|
|
||||||
self.poks.0.write(w)?;
|
|
||||||
self.poks.1.write(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a Cross-Group Discrete Log Equality proof from a type satisfying std::io::Read.
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
|
|
||||||
let capacity = usize::try_from(G0::Scalar::CAPACITY.min(G1::Scalar::CAPACITY)).unwrap();
|
|
||||||
let bits_per_group = usize::from(BitSignature::from(SIGNATURE).bits());
|
|
||||||
|
|
||||||
let mut bits = Vec::with_capacity(capacity / bits_per_group);
|
|
||||||
for _ in 0 .. (capacity / bits_per_group) {
|
|
||||||
bits.push(Bits::read(r)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut remainder = None;
|
|
||||||
if (capacity % bits_per_group) != 0 {
|
|
||||||
remainder = Some(Bits::read(r)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(__DLEqProof { bits, remainder, poks: (SchnorrPoK::read(r)?, SchnorrPoK::read(r)?) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
use core::ops::DerefMut;
|
|
||||||
|
|
||||||
use ff::PrimeFieldBits;
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use crate::cross_group::u8_from_bool;
|
|
||||||
|
|
||||||
/// Convert a uniform scalar into one usable on both fields, clearing the top bits as needed.
|
|
||||||
pub fn scalar_normalize<F0: PrimeFieldBits + Zeroize, F1: PrimeFieldBits>(
|
|
||||||
mut scalar: F0,
|
|
||||||
) -> (F0, F1) {
|
|
||||||
let mutual_capacity = F0::CAPACITY.min(F1::CAPACITY);
|
|
||||||
|
|
||||||
// A mutual key is only as secure as its weakest group
|
|
||||||
// Accordingly, this bans a capacity difference of more than 4 bits to prevent a curve generally
|
|
||||||
// offering n-bits of security from being forced into a situation with much fewer bits
|
|
||||||
#[cfg(feature = "secure_capacity_difference")]
|
|
||||||
assert!((F0::CAPACITY.max(F1::CAPACITY) - mutual_capacity) <= 4);
|
|
||||||
|
|
||||||
let mut res1 = F0::ZERO;
|
|
||||||
let mut res2 = F1::ZERO;
|
|
||||||
// Uses the bits API to ensure a consistent endianness
|
|
||||||
let mut bits = scalar.to_le_bits();
|
|
||||||
scalar.zeroize();
|
|
||||||
// Convert it to big endian
|
|
||||||
bits.reverse();
|
|
||||||
|
|
||||||
let mut skip = bits.len() - usize::try_from(mutual_capacity).unwrap();
|
|
||||||
// Needed to zero out the bits
|
|
||||||
#[allow(unused_assignments)]
|
|
||||||
for mut bit in &mut bits {
|
|
||||||
if skip > 0 {
|
|
||||||
bit.deref_mut().zeroize();
|
|
||||||
skip -= 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
res1 = res1.double();
|
|
||||||
res2 = res2.double();
|
|
||||||
|
|
||||||
let mut bit = u8_from_bool(bit.deref_mut());
|
|
||||||
res1 += F0::from(bit.into());
|
|
||||||
res2 += F1::from(bit.into());
|
|
||||||
bit.zeroize();
|
|
||||||
}
|
|
||||||
|
|
||||||
(res1, res2)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper to convert a scalar between fields. Returns None if the scalar isn't mutually valid.
|
|
||||||
pub fn scalar_convert<F0: PrimeFieldBits + Zeroize, F1: PrimeFieldBits>(
|
|
||||||
mut scalar: F0,
|
|
||||||
) -> Option<F1> {
|
|
||||||
let (mut valid, converted) = scalar_normalize(scalar);
|
|
||||||
let res = Some(converted).filter(|_| scalar == valid);
|
|
||||||
scalar.zeroize();
|
|
||||||
valid.zeroize();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a mutually valid scalar from bytes via bit truncation to not introduce bias.
|
|
||||||
pub fn mutual_scalar_from_bytes<F0: PrimeFieldBits + Zeroize, F1: PrimeFieldBits>(
|
|
||||||
bytes: &[u8],
|
|
||||||
) -> (F0, F1) {
|
|
||||||
let capacity = usize::try_from(F0::CAPACITY.min(F1::CAPACITY)).unwrap();
|
|
||||||
debug_assert!((bytes.len() * 8) >= capacity);
|
|
||||||
|
|
||||||
let mut accum = F0::ZERO;
|
|
||||||
for b in 0 .. capacity {
|
|
||||||
accum = accum.double();
|
|
||||||
accum += F0::from(((bytes[b / 8] >> (b % 8)) & 1).into());
|
|
||||||
}
|
|
||||||
(accum, scalar_convert(accum).unwrap())
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
use core::ops::Deref;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
|
||||||
|
|
||||||
use transcript::Transcript;
|
|
||||||
|
|
||||||
use group::{
|
|
||||||
ff::{Field, PrimeFieldBits},
|
|
||||||
prime::PrimeGroup,
|
|
||||||
};
|
|
||||||
use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
use crate::challenge;
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use ff::PrimeField;
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use crate::{read_scalar, cross_group::read_point};
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub(crate) struct SchnorrPoK<G: PrimeGroup + Zeroize> {
|
|
||||||
R: G,
|
|
||||||
s: G::Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<G: PrimeGroup<Scalar: PrimeFieldBits + Zeroize> + Zeroize> SchnorrPoK<G> {
|
|
||||||
// Not HRAm due to the lack of m
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
fn hra<T: Transcript>(transcript: &mut T, generator: G, R: G, A: G) -> G::Scalar {
|
|
||||||
transcript.domain_separate(b"schnorr_proof_of_knowledge");
|
|
||||||
transcript.append_message(b"generator", generator.to_bytes());
|
|
||||||
transcript.append_message(b"nonce", R.to_bytes());
|
|
||||||
transcript.append_message(b"public_key", A.to_bytes());
|
|
||||||
challenge(transcript)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn prove<R: RngCore + CryptoRng, T: Transcript>(
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &mut T,
|
|
||||||
generator: G,
|
|
||||||
private_key: &Zeroizing<G::Scalar>,
|
|
||||||
) -> SchnorrPoK<G> {
|
|
||||||
let nonce = Zeroizing::new(G::Scalar::random(rng));
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let R = generator * nonce.deref();
|
|
||||||
SchnorrPoK {
|
|
||||||
R,
|
|
||||||
s: (SchnorrPoK::hra(transcript, generator, R, generator * private_key.deref()) *
|
|
||||||
private_key.deref()) +
|
|
||||||
nonce.deref(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn verify<R: RngCore + CryptoRng, T: Transcript>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &mut T,
|
|
||||||
generator: G,
|
|
||||||
public_key: G,
|
|
||||||
batch: &mut BatchVerifier<(), G>,
|
|
||||||
) {
|
|
||||||
batch.queue(
|
|
||||||
rng,
|
|
||||||
(),
|
|
||||||
[
|
|
||||||
(-self.s, generator),
|
|
||||||
(G::Scalar::ONE, self.R),
|
|
||||||
(Self::hra(transcript, generator, self.R, public_key), public_key),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
|
|
||||||
w.write_all(self.R.to_bytes().as_ref())?;
|
|
||||||
w.write_all(self.s.to_repr().as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> std::io::Result<SchnorrPoK<G>> {
|
|
||||||
Ok(SchnorrPoK { R: read_point(r)?, s: read_scalar(r)? })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
|
|
||||||
use core::ops::Deref;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
|
||||||
|
|
||||||
use transcript::Transcript;
|
|
||||||
|
|
||||||
use ff::{Field, PrimeField};
|
|
||||||
use group::prime::PrimeGroup;
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
use std::io::{self, Error, Read, Write};
|
|
||||||
|
|
||||||
/// A cross-group DLEq proof capable of proving that two public keys, across two different curves,
|
|
||||||
/// share a private key.
|
|
||||||
#[cfg(feature = "experimental")]
|
|
||||||
pub mod cross_group;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
// Produce a non-biased challenge from the transcript in the specified field
|
|
||||||
pub(crate) fn challenge<T: Transcript, F: PrimeField>(transcript: &mut T) -> F {
|
|
||||||
// From here, there are three ways to get a scalar under the ff/group API
|
|
||||||
// 1: Scalar::random(ChaCha20Rng::from_seed(self.transcript.rng_seed(b"challenge")))
|
|
||||||
// 2: Grabbing a UInt library to perform reduction by the modulus, then determining endianness
|
|
||||||
// and loading it in
|
|
||||||
// 3: Iterating over each byte and manually doubling/adding. This is simplest
|
|
||||||
|
|
||||||
let mut challenge = F::ZERO;
|
|
||||||
|
|
||||||
// Get a wide amount of bytes to safely reduce without bias
|
|
||||||
// In most cases, <=1.5x bytes is enough. 2x is still standard and there's some theoretical
|
|
||||||
// groups which may technically require more than 1.5x bytes for this to work as intended
|
|
||||||
let target_bytes = usize::try_from(F::NUM_BITS).unwrap().div_ceil(8) * 2;
|
|
||||||
let mut challenge_bytes = transcript.challenge(b"challenge");
|
|
||||||
let challenge_bytes_len = challenge_bytes.as_ref().len();
|
|
||||||
// If the challenge is 32 bytes, and we need 64, we need two challenges
|
|
||||||
let needed_challenges = target_bytes.div_ceil(challenge_bytes_len);
|
|
||||||
|
|
||||||
// The following algorithm should be equivalent to a wide reduction of the challenges,
|
|
||||||
// interpreted as concatenated, big-endian byte string
|
|
||||||
let mut handled_bytes = 0;
|
|
||||||
'outer: for _ in 0 ..= needed_challenges {
|
|
||||||
// Cursor of which byte of the challenge to use next
|
|
||||||
let mut b = 0;
|
|
||||||
while b < challenge_bytes_len {
|
|
||||||
// Get the next amount of bytes to attempt
|
|
||||||
// Only grabs the needed amount of bytes, up to 8 at a time (u64), so long as they're
|
|
||||||
// available in the challenge
|
|
||||||
let chunk_bytes = (target_bytes - handled_bytes).min(8).min(challenge_bytes_len - b);
|
|
||||||
|
|
||||||
let mut chunk = 0;
|
|
||||||
for _ in 0 .. chunk_bytes {
|
|
||||||
chunk <<= 8;
|
|
||||||
chunk |= u64::from(challenge_bytes.as_ref()[b]);
|
|
||||||
b += 1;
|
|
||||||
}
|
|
||||||
// Add this chunk
|
|
||||||
challenge += F::from(chunk);
|
|
||||||
|
|
||||||
handled_bytes += chunk_bytes;
|
|
||||||
// If we've reached the target amount of bytes, break
|
|
||||||
if handled_bytes == target_bytes {
|
|
||||||
break 'outer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shift over by however many bits will be in the next chunk
|
|
||||||
let next_chunk_bytes = (target_bytes - handled_bytes).min(8).min(challenge_bytes_len);
|
|
||||||
for _ in 0 .. (next_chunk_bytes * 8) {
|
|
||||||
challenge = challenge.double();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Secure thanks to the Transcript trait having a bound of updating on challenge
|
|
||||||
challenge_bytes = transcript.challenge(b"challenge_extension");
|
|
||||||
}
|
|
||||||
|
|
||||||
challenge
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to read a scalar
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
fn read_scalar<R: Read, F: PrimeField>(r: &mut R) -> io::Result<F> {
|
|
||||||
let mut repr = F::Repr::default();
|
|
||||||
r.read_exact(repr.as_mut())?;
|
|
||||||
let scalar = F::from_repr(repr);
|
|
||||||
if scalar.is_none().into() {
|
|
||||||
Err(Error::other("invalid scalar"))?;
|
|
||||||
}
|
|
||||||
Ok(scalar.unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error for DLEq proofs.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
pub enum DLEqError {
|
|
||||||
/// The proof was invalid.
|
|
||||||
InvalidProof,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A proof that points have the same discrete logarithm across generators.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct DLEqProof<G: PrimeGroup<Scalar: Zeroize>> {
|
|
||||||
c: G::Scalar,
|
|
||||||
s: G::Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
impl<G: PrimeGroup<Scalar: Zeroize>> DLEqProof<G> {
|
|
||||||
fn transcript<T: Transcript>(transcript: &mut T, generator: G, nonce: G, point: G) {
|
|
||||||
transcript.append_message(b"generator", generator.to_bytes());
|
|
||||||
transcript.append_message(b"nonce", nonce.to_bytes());
|
|
||||||
transcript.append_message(b"point", point.to_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prove that the points created by `scalar * G`, for each specified generator, share a discrete
|
|
||||||
/// logarithm.
|
|
||||||
pub fn prove<R: RngCore + CryptoRng, T: Transcript>(
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: &[G],
|
|
||||||
scalar: &Zeroizing<G::Scalar>,
|
|
||||||
) -> DLEqProof<G> {
|
|
||||||
let r = Zeroizing::new(G::Scalar::random(rng));
|
|
||||||
|
|
||||||
transcript.domain_separate(b"dleq");
|
|
||||||
for generator in generators {
|
|
||||||
// R, A
|
|
||||||
Self::transcript(transcript, *generator, *generator * r.deref(), *generator * scalar.deref());
|
|
||||||
}
|
|
||||||
|
|
||||||
let c = challenge(transcript);
|
|
||||||
// r + ca
|
|
||||||
let s = (c * scalar.deref()) + r.deref();
|
|
||||||
|
|
||||||
DLEqProof { c, s }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transcript a specific generator/nonce/point (G/R/A), as used when verifying a proof.
|
|
||||||
// This takes in the generator/point, and then the challenge and solution to calculate the nonce.
|
|
||||||
fn verify_statement<T: Transcript>(
|
|
||||||
transcript: &mut T,
|
|
||||||
generator: G,
|
|
||||||
point: G,
|
|
||||||
c: G::Scalar,
|
|
||||||
s: G::Scalar,
|
|
||||||
) {
|
|
||||||
// s = r + ca
|
|
||||||
// sG - cA = R
|
|
||||||
// R, A
|
|
||||||
Self::transcript(transcript, generator, (generator * s) - (point * c), point);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify the specified points share a discrete logarithm across the specified generators.
|
|
||||||
pub fn verify<T: Transcript>(
|
|
||||||
&self,
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: &[G],
|
|
||||||
points: &[G],
|
|
||||||
) -> Result<(), DLEqError> {
|
|
||||||
if generators.len() != points.len() {
|
|
||||||
Err(DLEqError::InvalidProof)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
transcript.domain_separate(b"dleq");
|
|
||||||
for (generator, point) in generators.iter().zip(points) {
|
|
||||||
Self::verify_statement(transcript, *generator, *point, self.c, self.s);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.c != challenge(transcript) {
|
|
||||||
Err(DLEqError::InvalidProof)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a DLEq proof to something implementing Write.
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(self.c.to_repr().as_ref())?;
|
|
||||||
w.write_all(self.s.to_repr().as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a DLEq proof from something implementing Read.
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<DLEqProof<G>> {
|
|
||||||
Ok(DLEqProof { c: read_scalar(r)?, s: read_scalar(r)? })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize a DLEq proof to a `Vec<u8>`.
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = vec![];
|
|
||||||
self.write(&mut res).unwrap();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A proof that multiple series of points each have a single discrete logarithm across generators.
|
|
||||||
///
|
|
||||||
/// This is effectively n distinct DLEq proofs, one for each discrete logarithm and its points
|
|
||||||
/// across some generators, yet with a smaller overall proof size.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct MultiDLEqProof<G: PrimeGroup<Scalar: Zeroize>> {
|
|
||||||
c: G::Scalar,
|
|
||||||
s: Vec<G::Scalar>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
impl<G: PrimeGroup<Scalar: Zeroize>> MultiDLEqProof<G> {
|
|
||||||
/// Prove for each scalar that the series of points created by multiplying it against its
|
|
||||||
/// matching generators share a discrete logarithm.
|
|
||||||
/// This function panics if `generators.len() != scalars.len()`.
|
|
||||||
pub fn prove<R: RngCore + CryptoRng, T: Transcript>(
|
|
||||||
rng: &mut R,
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: &[Vec<G>],
|
|
||||||
scalars: &[Zeroizing<G::Scalar>],
|
|
||||||
) -> MultiDLEqProof<G> {
|
|
||||||
assert_eq!(
|
|
||||||
generators.len(),
|
|
||||||
scalars.len(),
|
|
||||||
"amount of series of generators doesn't match the amount of scalars"
|
|
||||||
);
|
|
||||||
|
|
||||||
transcript.domain_separate(b"multi_dleq");
|
|
||||||
|
|
||||||
let mut nonces = vec![];
|
|
||||||
for (i, (scalar, generators)) in scalars.iter().zip(generators).enumerate() {
|
|
||||||
// Delineate between discrete logarithms
|
|
||||||
transcript.append_message(b"discrete_logarithm", i.to_le_bytes());
|
|
||||||
|
|
||||||
let nonce = Zeroizing::new(G::Scalar::random(&mut *rng));
|
|
||||||
for generator in generators {
|
|
||||||
DLEqProof::transcript(
|
|
||||||
transcript,
|
|
||||||
*generator,
|
|
||||||
*generator * nonce.deref(),
|
|
||||||
*generator * scalar.deref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
nonces.push(nonce);
|
|
||||||
}
|
|
||||||
|
|
||||||
let c = challenge(transcript);
|
|
||||||
|
|
||||||
let mut s = vec![];
|
|
||||||
for (scalar, nonce) in scalars.iter().zip(nonces) {
|
|
||||||
s.push((c * scalar.deref()) + nonce.deref());
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiDLEqProof { c, s }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify each series of points share a discrete logarithm against their matching series of
|
|
||||||
/// generators.
|
|
||||||
pub fn verify<T: Transcript>(
|
|
||||||
&self,
|
|
||||||
transcript: &mut T,
|
|
||||||
generators: &[Vec<G>],
|
|
||||||
points: &[Vec<G>],
|
|
||||||
) -> Result<(), DLEqError> {
|
|
||||||
if points.len() != generators.len() {
|
|
||||||
Err(DLEqError::InvalidProof)?;
|
|
||||||
}
|
|
||||||
if self.s.len() != generators.len() {
|
|
||||||
Err(DLEqError::InvalidProof)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
transcript.domain_separate(b"multi_dleq");
|
|
||||||
for (i, (generators, points)) in generators.iter().zip(points).enumerate() {
|
|
||||||
if points.len() != generators.len() {
|
|
||||||
Err(DLEqError::InvalidProof)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
transcript.append_message(b"discrete_logarithm", i.to_le_bytes());
|
|
||||||
for (generator, point) in generators.iter().zip(points) {
|
|
||||||
DLEqProof::verify_statement(transcript, *generator, *point, self.c, self.s[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.c != challenge(transcript) {
|
|
||||||
Err(DLEqError::InvalidProof)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a multi-DLEq proof to something implementing Write.
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(self.c.to_repr().as_ref())?;
|
|
||||||
for s in &self.s {
|
|
||||||
w.write_all(s.to_repr().as_ref())?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a multi-DLEq proof from something implementing Read.
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub fn read<R: Read>(r: &mut R, discrete_logs: usize) -> io::Result<MultiDLEqProof<G>> {
|
|
||||||
let c = read_scalar(r)?;
|
|
||||||
let mut s = vec![];
|
|
||||||
for _ in 0 .. discrete_logs {
|
|
||||||
s.push(read_scalar(r)?);
|
|
||||||
}
|
|
||||||
Ok(MultiDLEqProof { c, s })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize a multi-DLEq proof to a `Vec<u8>`.
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = vec![];
|
|
||||||
self.write(&mut res).unwrap();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use group::{ff::Field, Group};
|
|
||||||
|
|
||||||
use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
cross_group::aos::{Re, Aos},
|
|
||||||
tests::cross_group::{G0, G1, transcript, generators},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
fn test_aos_serialization<const RING_LEN: usize>(proof: &Aos<G0, G1, RING_LEN>, Re_0: Re<G0, G1>) {
|
|
||||||
let mut buf = vec![];
|
|
||||||
proof.write(&mut buf).unwrap();
|
|
||||||
let deserialized = Aos::read::<&[u8]>(&mut buf.as_ref(), Re_0).unwrap();
|
|
||||||
assert_eq!(proof, &deserialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_aos<const RING_LEN: usize>(default: &Re<G0, G1>) {
|
|
||||||
let generators = generators();
|
|
||||||
|
|
||||||
let mut ring_keys = [(<G0 as Group>::Scalar::ZERO, <G1 as Group>::Scalar::ZERO); RING_LEN];
|
|
||||||
// Side-effect of G0 being a type-alias with identity() deprecated
|
|
||||||
#[allow(deprecated)]
|
|
||||||
let mut ring = [(G0::identity(), G1::identity()); RING_LEN];
|
|
||||||
for i in 0 .. RING_LEN {
|
|
||||||
ring_keys[i] =
|
|
||||||
(<G0 as Group>::Scalar::random(&mut OsRng), <G1 as Group>::Scalar::random(&mut OsRng));
|
|
||||||
ring[i] = (generators.0.alt * ring_keys[i].0, generators.1.alt * ring_keys[i].1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (actual, key) in ring_keys.iter_mut().enumerate() {
|
|
||||||
let proof = Aos::<_, _, RING_LEN>::prove(
|
|
||||||
&mut OsRng,
|
|
||||||
&transcript(),
|
|
||||||
generators,
|
|
||||||
&ring,
|
|
||||||
actual,
|
|
||||||
key,
|
|
||||||
default.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut batch = (BatchVerifier::new(0), BatchVerifier::new(0));
|
|
||||||
proof.verify(&mut OsRng, &transcript(), generators, &mut batch, &ring).unwrap();
|
|
||||||
// For e, these should have nothing. For R, these should have 6 elements each which sum to 0
|
|
||||||
assert!(batch.0.verify_vartime());
|
|
||||||
assert!(batch.1.verify_vartime());
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
test_aos_serialization(&proof, default.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_aos_e() {
|
|
||||||
test_aos::<2>(&Re::e_default());
|
|
||||||
test_aos::<4>(&Re::e_default());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[test]
|
|
||||||
fn test_aos_R() {
|
|
||||||
// Batch verification appreciates the longer vectors, which means not batching bits
|
|
||||||
test_aos::<2>(&Re::R_default());
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
use core::ops::Deref;
|
|
||||||
|
|
||||||
use hex_literal::hex;
|
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::{RngCore, OsRng};
|
|
||||||
|
|
||||||
use ff::{Field, PrimeField};
|
|
||||||
use group::{Group, GroupEncoding};
|
|
||||||
|
|
||||||
use blake2::{Digest, Blake2b512};
|
|
||||||
|
|
||||||
use k256::{Scalar, ProjectivePoint};
|
|
||||||
use dalek_ff_group::{self as dfg, EdwardsPoint};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
cross_group::{
|
|
||||||
scalar::mutual_scalar_from_bytes, Generators, ClassicLinearDLEq, EfficientLinearDLEq,
|
|
||||||
ConciseLinearDLEq, CompromiseLinearDLEq,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mod scalar;
|
|
||||||
mod aos;
|
|
||||||
|
|
||||||
type G0 = ProjectivePoint;
|
|
||||||
type G1 = EdwardsPoint;
|
|
||||||
|
|
||||||
pub(crate) fn transcript() -> RecommendedTranscript {
|
|
||||||
RecommendedTranscript::new(b"Cross-Group DLEq Proof Test")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn generators() -> (Generators<G0>, Generators<G1>) {
|
|
||||||
(
|
|
||||||
Generators::new(
|
|
||||||
ProjectivePoint::GENERATOR,
|
|
||||||
ProjectivePoint::from_bytes(
|
|
||||||
&(hex!("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0").into()),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
Generators::new(
|
|
||||||
EdwardsPoint::generator(),
|
|
||||||
EdwardsPoint::from_bytes(&hex!(
|
|
||||||
"8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94"
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! verify_and_deserialize {
|
|
||||||
($type: ty, $proof: ident, $generators: ident, $keys: ident) => {
|
|
||||||
let public_keys = $proof.verify(&mut OsRng, &mut transcript(), $generators).unwrap();
|
|
||||||
assert_eq!($generators.0.primary * $keys.0.deref(), public_keys.0);
|
|
||||||
assert_eq!($generators.1.primary * $keys.1.deref(), public_keys.1);
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
{
|
|
||||||
let mut buf = vec![];
|
|
||||||
$proof.write(&mut buf).unwrap();
|
|
||||||
let deserialized = <$type>::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
|
||||||
assert_eq!($proof, deserialized);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! test_dleq {
|
|
||||||
($str: literal, $benchmark: ident, $name: ident, $type: ident) => {
|
|
||||||
#[ignore]
|
|
||||||
#[test]
|
|
||||||
fn $benchmark() {
|
|
||||||
println!("Benchmarking with Secp256k1/Ed25519");
|
|
||||||
let generators = generators();
|
|
||||||
|
|
||||||
let mut seed = [0; 32];
|
|
||||||
OsRng.fill_bytes(&mut seed);
|
|
||||||
let key = Blake2b512::new().chain_update(seed);
|
|
||||||
|
|
||||||
let runs = 200;
|
|
||||||
let mut proofs = Vec::with_capacity(usize::try_from(runs).unwrap());
|
|
||||||
let time = std::time::Instant::now();
|
|
||||||
for _ in 0 .. runs {
|
|
||||||
proofs.push($type::prove(&mut OsRng, &mut transcript(), generators, key.clone()).0);
|
|
||||||
}
|
|
||||||
println!("{} had a average prove time of {}ms", $str, time.elapsed().as_millis() / runs);
|
|
||||||
|
|
||||||
let time = std::time::Instant::now();
|
|
||||||
for proof in &proofs {
|
|
||||||
proof.verify(&mut OsRng, &mut transcript(), generators).unwrap();
|
|
||||||
}
|
|
||||||
println!("{} had a average verify time of {}ms", $str, time.elapsed().as_millis() / runs);
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
{
|
|
||||||
let mut buf = vec![];
|
|
||||||
proofs[0].write(&mut buf).unwrap();
|
|
||||||
println!("{} had a proof size of {} bytes", $str, buf.len());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn $name() {
|
|
||||||
let generators = generators();
|
|
||||||
|
|
||||||
for i in 0 .. 1 {
|
|
||||||
let (proof, keys) = if i == 0 {
|
|
||||||
let mut seed = [0; 32];
|
|
||||||
OsRng.fill_bytes(&mut seed);
|
|
||||||
|
|
||||||
$type::prove(
|
|
||||||
&mut OsRng,
|
|
||||||
&mut transcript(),
|
|
||||||
generators,
|
|
||||||
Blake2b512::new().chain_update(seed),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let mut key;
|
|
||||||
let mut res;
|
|
||||||
while {
|
|
||||||
key = Zeroizing::new(Scalar::random(&mut OsRng));
|
|
||||||
res = $type::prove_without_bias(&mut OsRng, &mut transcript(), generators, key.clone());
|
|
||||||
res.is_none()
|
|
||||||
} {}
|
|
||||||
let res = res.unwrap();
|
|
||||||
assert_eq!(key, res.1 .0);
|
|
||||||
res
|
|
||||||
};
|
|
||||||
|
|
||||||
verify_and_deserialize!($type::<G0, G1>, proof, generators, keys);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test_dleq!("ClassicLinear", benchmark_classic_linear, test_classic_linear, ClassicLinearDLEq);
|
|
||||||
test_dleq!("ConciseLinear", benchmark_concise_linear, test_concise_linear, ConciseLinearDLEq);
|
|
||||||
test_dleq!(
|
|
||||||
"EfficientLinear",
|
|
||||||
benchmark_efficient_linear,
|
|
||||||
test_efficient_linear,
|
|
||||||
EfficientLinearDLEq
|
|
||||||
);
|
|
||||||
test_dleq!(
|
|
||||||
"CompromiseLinear",
|
|
||||||
benchmark_compromise_linear,
|
|
||||||
test_compromise_linear,
|
|
||||||
CompromiseLinearDLEq
|
|
||||||
);
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rejection_sampling() {
|
|
||||||
let mut pow_2 = Scalar::ONE;
|
|
||||||
for _ in 0 .. dfg::Scalar::CAPACITY {
|
|
||||||
pow_2 = pow_2.double();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
// Either would work
|
|
||||||
EfficientLinearDLEq::prove_without_bias(
|
|
||||||
&mut OsRng,
|
|
||||||
&mut transcript(),
|
|
||||||
generators(),
|
|
||||||
Zeroizing::new(pow_2)
|
|
||||||
)
|
|
||||||
.is_none()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remainder() {
|
|
||||||
// Uses Secp256k1 for both to achieve an odd capacity of 255
|
|
||||||
assert_eq!(Scalar::CAPACITY, 255);
|
|
||||||
let generators = (generators().0, generators().0);
|
|
||||||
// This will ignore any unused bits, ensuring every remaining one is set
|
|
||||||
let keys = mutual_scalar_from_bytes::<Scalar, Scalar>(&[0xFF; 32]);
|
|
||||||
let keys = (Zeroizing::new(keys.0), Zeroizing::new(keys.1));
|
|
||||||
assert_eq!(Scalar::ONE + keys.0.deref(), Scalar::from(2u64).pow_vartime([255]));
|
|
||||||
assert_eq!(keys.0, keys.1);
|
|
||||||
|
|
||||||
let (proof, res) = ConciseLinearDLEq::prove_without_bias(
|
|
||||||
&mut OsRng,
|
|
||||||
&mut transcript(),
|
|
||||||
generators,
|
|
||||||
keys.0.clone(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(keys, res);
|
|
||||||
|
|
||||||
verify_and_deserialize!(
|
|
||||||
ConciseLinearDLEq::<ProjectivePoint, ProjectivePoint>,
|
|
||||||
proof,
|
|
||||||
generators,
|
|
||||||
keys
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use ff::{Field, PrimeField};
|
|
||||||
|
|
||||||
use k256::Scalar as K256Scalar;
|
|
||||||
use dalek_ff_group::Scalar as DalekScalar;
|
|
||||||
|
|
||||||
use crate::cross_group::scalar::{scalar_normalize, scalar_convert};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_scalar() {
|
|
||||||
assert_eq!(
|
|
||||||
scalar_normalize::<_, DalekScalar>(K256Scalar::ZERO),
|
|
||||||
(K256Scalar::ZERO, DalekScalar::ZERO)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
scalar_normalize::<_, DalekScalar>(K256Scalar::ONE),
|
|
||||||
(K256Scalar::ONE, DalekScalar::ONE)
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut initial;
|
|
||||||
while {
|
|
||||||
initial = K256Scalar::random(&mut OsRng);
|
|
||||||
let (k, ed) = scalar_normalize::<_, DalekScalar>(initial);
|
|
||||||
|
|
||||||
// The initial scalar should equal the new scalar with Ed25519's capacity
|
|
||||||
let mut initial_bytes = initial.to_repr().to_vec();
|
|
||||||
// Drop the first 4 bits to hit 252
|
|
||||||
initial_bytes[0] &= 0b00001111;
|
|
||||||
let k_bytes = k.to_repr().to_vec();
|
|
||||||
assert_eq!(initial_bytes, k_bytes);
|
|
||||||
|
|
||||||
let mut ed_bytes = ed.to_repr().as_ref().to_vec();
|
|
||||||
// Reverse to big endian
|
|
||||||
ed_bytes.reverse();
|
|
||||||
assert_eq!(k_bytes, ed_bytes);
|
|
||||||
|
|
||||||
// Verify conversion works as expected
|
|
||||||
assert_eq!(scalar_convert::<_, DalekScalar>(k), Some(ed));
|
|
||||||
|
|
||||||
// Run this test again if this secp256k1 scalar didn't have any bits cleared
|
|
||||||
initial == k
|
|
||||||
} {}
|
|
||||||
// Verify conversion returns None when the scalar isn't mutually valid
|
|
||||||
assert!(scalar_convert::<_, DalekScalar>(initial).is_none());
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
use core::ops::Deref;
|
|
||||||
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use group::{
|
|
||||||
ff::{Field, PrimeFieldBits},
|
|
||||||
prime::PrimeGroup,
|
|
||||||
};
|
|
||||||
use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
use crate::cross_group::schnorr::SchnorrPoK;
|
|
||||||
|
|
||||||
fn test_schnorr<G: PrimeGroup<Scalar: PrimeFieldBits + Zeroize> + Zeroize>() {
|
|
||||||
let transcript = RecommendedTranscript::new(b"Schnorr Test");
|
|
||||||
|
|
||||||
let mut batch = BatchVerifier::new(10);
|
|
||||||
for _ in 0 .. 10 {
|
|
||||||
let private = Zeroizing::new(G::Scalar::random(&mut OsRng));
|
|
||||||
SchnorrPoK::prove(&mut OsRng, &mut transcript.clone(), G::generator(), &private).verify(
|
|
||||||
&mut OsRng,
|
|
||||||
&mut transcript.clone(),
|
|
||||||
G::generator(),
|
|
||||||
G::generator() * private.deref(),
|
|
||||||
&mut batch,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(batch.verify_vartime());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_secp256k1() {
|
|
||||||
test_schnorr::<k256::ProjectivePoint>();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ed25519() {
|
|
||||||
test_schnorr::<dalek_ff_group::EdwardsPoint>();
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
use core::ops::Deref;
|
|
||||||
|
|
||||||
use hex_literal::hex;
|
|
||||||
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
use ff::Field;
|
|
||||||
use group::GroupEncoding;
|
|
||||||
|
|
||||||
use k256::{Scalar, ProjectivePoint};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
use crate::{DLEqProof, MultiDLEqProof};
|
|
||||||
|
|
||||||
#[cfg(feature = "experimental")]
|
|
||||||
mod cross_group;
|
|
||||||
|
|
||||||
fn generators() -> [k256::ProjectivePoint; 5] {
|
|
||||||
[
|
|
||||||
ProjectivePoint::GENERATOR,
|
|
||||||
ProjectivePoint::from_bytes(
|
|
||||||
&(hex!("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0").into()),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
// Just an increment of the last byte from the previous, where the previous two are valid
|
|
||||||
ProjectivePoint::from_bytes(
|
|
||||||
&(hex!("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4").into()),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
ProjectivePoint::from_bytes(
|
|
||||||
&(hex!("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803aca").into()),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
ProjectivePoint::from_bytes(
|
|
||||||
&(hex!("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803acb").into()),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_dleq() {
|
|
||||||
let generators = generators();
|
|
||||||
let transcript = || RecommendedTranscript::new(b"DLEq Proof Test");
|
|
||||||
|
|
||||||
for i in 0 .. 5 {
|
|
||||||
let key = Zeroizing::new(Scalar::random(&mut OsRng));
|
|
||||||
let proof = DLEqProof::prove(&mut OsRng, &mut transcript(), &generators[.. i], &key);
|
|
||||||
|
|
||||||
let mut keys = [ProjectivePoint::GENERATOR; 5];
|
|
||||||
for k in 0 .. 5 {
|
|
||||||
keys[k] = generators[k] * key.deref();
|
|
||||||
}
|
|
||||||
proof.verify(&mut transcript(), &generators[.. i], &keys[.. i]).unwrap();
|
|
||||||
// Different challenge
|
|
||||||
assert!(proof
|
|
||||||
.verify(
|
|
||||||
&mut RecommendedTranscript::new(b"different challenge"),
|
|
||||||
&generators[.. i],
|
|
||||||
&keys[.. i]
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
|
|
||||||
// All of these following tests should effectively be a different challenge and accordingly
|
|
||||||
// pointless. They're still nice to have though
|
|
||||||
|
|
||||||
// We could edit these tests to always test with at least two generators
|
|
||||||
// Then we don't test proofs with zero/one generator(s)
|
|
||||||
// While those are stupid, and pointless, and potentially point to a failure in the caller,
|
|
||||||
// it could also be part of a dynamic system which deals with variable amounts of generators
|
|
||||||
// Not panicking in such use cases, even if they're inefficient, provides seamless behavior
|
|
||||||
if i >= 2 {
|
|
||||||
// Different generators
|
|
||||||
assert!(proof
|
|
||||||
.verify(
|
|
||||||
&mut transcript(),
|
|
||||||
generators[.. i].iter().copied().rev().collect::<Vec<_>>().as_ref(),
|
|
||||||
&keys[.. i]
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
// Different keys
|
|
||||||
assert!(proof
|
|
||||||
.verify(
|
|
||||||
&mut transcript(),
|
|
||||||
&generators[.. i],
|
|
||||||
keys[.. i].iter().copied().rev().collect::<Vec<_>>().as_ref()
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
{
|
|
||||||
let mut buf = vec![];
|
|
||||||
proof.write(&mut buf).unwrap();
|
|
||||||
let deserialized = DLEqProof::<ProjectivePoint>::read::<&[u8]>(&mut buf.as_ref()).unwrap();
|
|
||||||
assert_eq!(proof, deserialized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multi_dleq() {
|
|
||||||
let generators = generators();
|
|
||||||
let transcript = || RecommendedTranscript::new(b"MultiDLEq Proof Test");
|
|
||||||
|
|
||||||
// Test up to 3 keys
|
|
||||||
for k in 0 ..= 3 {
|
|
||||||
let mut keys = vec![];
|
|
||||||
let mut these_generators = vec![];
|
|
||||||
let mut pub_keys = vec![];
|
|
||||||
for i in 0 .. k {
|
|
||||||
let key = Zeroizing::new(Scalar::random(&mut OsRng));
|
|
||||||
// For each key, test a variable set of generators
|
|
||||||
// 0: 0
|
|
||||||
// 1: 1, 2
|
|
||||||
// 2: 2, 3, 4
|
|
||||||
let key_generators = generators[i ..= (i + i)].to_vec();
|
|
||||||
let mut these_pub_keys = vec![];
|
|
||||||
for generator in &key_generators {
|
|
||||||
these_pub_keys.push(generator * key.deref());
|
|
||||||
}
|
|
||||||
keys.push(key);
|
|
||||||
these_generators.push(key_generators);
|
|
||||||
pub_keys.push(these_pub_keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
let proof = MultiDLEqProof::prove(&mut OsRng, &mut transcript(), &these_generators, &keys);
|
|
||||||
|
|
||||||
proof.verify(&mut transcript(), &these_generators, &pub_keys).unwrap();
|
|
||||||
// Different challenge
|
|
||||||
assert!(proof
|
|
||||||
.verify(&mut RecommendedTranscript::new(b"different challenge"), &these_generators, &pub_keys)
|
|
||||||
.is_err());
|
|
||||||
|
|
||||||
// Test verifying for a different amount of keys fail
|
|
||||||
if k > 0 {
|
|
||||||
assert!(proof.verify(&mut transcript(), &these_generators, &pub_keys[.. k - 1]).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
|
||||||
{
|
|
||||||
let mut buf = vec![];
|
|
||||||
proof.write(&mut buf).unwrap();
|
|
||||||
let deserialized =
|
|
||||||
MultiDLEqProof::<ProjectivePoint>::read::<&[u8]>(&mut buf.as_ref(), k).unwrap();
|
|
||||||
assert_eq!(proof, deserialized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,6 @@ ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, fea
|
|||||||
|
|
||||||
multiexp = { path = "../../crypto/multiexp", default-features = false, features = ["batch"] }
|
multiexp = { path = "../../crypto/multiexp", default-features = false, features = ["batch"] }
|
||||||
|
|
||||||
dleq = { path = "../../crypto/dleq", default-features = false }
|
|
||||||
schnorr-signatures = { path = "../../crypto/schnorr", default-features = false }
|
schnorr-signatures = { path = "../../crypto/schnorr", default-features = false }
|
||||||
|
|
||||||
secq256k1 = { path = "../../crypto/evrf/secq256k1", default-features = false }
|
secq256k1 = { path = "../../crypto/evrf/secq256k1", default-features = false }
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ pub use ciphersuite;
|
|||||||
|
|
||||||
pub use multiexp;
|
pub use multiexp;
|
||||||
|
|
||||||
pub use dleq;
|
|
||||||
pub use schnorr_signatures;
|
pub use schnorr_signatures;
|
||||||
|
|
||||||
pub use secq256k1;
|
pub use secq256k1;
|
||||||
|
|||||||
Reference in New Issue
Block a user