Crypto part of the Groth's NIDKG (#1182)

* Work in progress NIDKG

* Encryption of multiple shares

* Extracted baby-step giant-step lookup table as a separate entity

* Proof of discrete log

* Adjusted discrete log domainn

* Producing proof of log during keygen

* Zeroize for epoch

* Proof of secret sharing

* empty main for compiler appeasement

* Construction of proof of chunking

* Initial untested verification of proof of chunking

* Converted chunk responses from Scalar to u64

* Additional tests for proof of chunking

* Minor cleanup and reorganisation

* Fixed enc/dec to use f0

* Deriving node coverage of required tree nodes

* Finally seemingnly working encryption under nonzero epoch

* Branch park

* Decryption key updates to specified epochs

* Ciphertext integrity checks

* Progress in integration tests

* Fixed ciphertext combining and integration test

* Dealing type and simplification of the integration test

* Benchmark for creation of baby-step-giant-step lookup table

* Initial import cleanup + broken 2nd integration test

* Using correct assertions in the integration test (and correctly combining shares)

* Removed unused modules

* Changed proof of sharing to allow for node indices being different from [1,2,...n]

* Reorganised bte module

* Benchmark for g2 precomputation

* Created more strongly typed Epoch type

which is essentially a Tau such that it is a leaf node

* Extending tau with a temporary oracle output

* Using random oracle for tau extension

* More benchmarks!

* encryption-related benchmarks

* Serialization of PublicKeyWithProof

* Typos

* Removed any changes made in validator-api or smart contracts

* Made the integration test slightly more concise

* Further purge of unused modules

* Fixed combining share to use lagrangian interpolation

* Recovery of verification keys from the dealings

* Verification key verification + extended integration tests

* Fixed Tau not being included in digest for producing Tau_h

* Tau serialization

* Serialization of a BTE Node

* Serialization of DecryptionKey

* Serialization of PublicCoefficients

* Utility method for setting constant coefficient of a polynomial

* Serialization of Ciphertexts

* Serialization of Proof of Secret Sharing

* Serialization of Proof of Chunking

* Serialization of Dealing

* Adjusted capacity of responses_r in proof of chunking

* Made notation more consistent with the paper equivalents

* Optional arguments for creating/verifying resharing dealings
This commit is contained in:
Jędrzej Stuczyński
2022-04-12 11:59:26 +01:00
committed by GitHub
parent d3372bfc85
commit 37de4bf2f7
19 changed files with 6367 additions and 14 deletions
Generated
+92 -14
View File
@@ -286,10 +286,22 @@ version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848"
dependencies = [
"funty",
"radium",
"funty 1.1.0",
"radium 0.6.2",
"tap",
"wyz",
"wyz 0.2.0",
]
[[package]]
name = "bitvec"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1489fcb93a5bb47da0462ca93ad252ad6af2145cce58d10d46a83931ba9f016b"
dependencies = [
"funty 2.0.0",
"radium 0.7.0",
"tap",
"wyz 0.5.0",
]
[[package]]
@@ -373,11 +385,25 @@ dependencies = [
"digest 0.9.0",
"ff 0.10.1",
"group 0.10.0",
"pairing",
"pairing 0.20.0",
"rand_core 0.6.3",
"subtle 2.4.1",
]
[[package]]
name = "bls12_381"
version = "0.6.0"
source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278"
dependencies = [
"digest 0.9.0",
"ff 0.11.0",
"group 0.11.0",
"pairing 0.21.0",
"rand_core 0.6.3",
"subtle 2.4.1",
"zeroize",
]
[[package]]
name = "bs58"
version = "0.4.0"
@@ -895,7 +921,7 @@ dependencies = [
name = "credentials"
version = "0.1.0"
dependencies = [
"bls12_381",
"bls12_381 0.5.0",
"coconut-interface",
"cosmrs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"crypto",
@@ -1131,9 +1157,9 @@ dependencies = [
[[package]]
name = "curve25519-dalek"
version = "3.2.1"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0"
checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
dependencies = [
"byteorder",
"digest 0.9.0",
@@ -1317,6 +1343,27 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "dkg"
version = "0.1.0"
dependencies = [
"bitvec 1.0.0",
"bls12_381 0.6.0",
"bs58",
"criterion",
"ff 0.11.0",
"group 0.11.0",
"lazy_static",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_core 0.6.3",
"serde",
"serde_derive",
"sha2",
"thiserror",
"zeroize",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
@@ -1753,6 +1800,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.21"
@@ -2042,6 +2095,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89"
dependencies = [
"byteorder",
"ff 0.11.0",
"rand_core 0.6.3",
"subtle 2.4.1",
@@ -3199,7 +3253,7 @@ name = "nymcoconut"
version = "0.5.0"
dependencies = [
"bincode",
"bls12_381",
"bls12_381 0.5.0",
"bs58",
"criterion",
"digest 0.9.0",
@@ -3427,6 +3481,15 @@ dependencies = [
"group 0.10.0",
]
[[package]]
name = "pairing"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42"
dependencies = [
"group 0.11.0",
]
[[package]]
name = "parity-scale-codec"
version = "2.3.1"
@@ -3434,7 +3497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909"
dependencies = [
"arrayvec 0.7.2",
"bitvec",
"bitvec 0.20.4",
"byte-slice-cast",
"impl-trait-for-tuples",
"parity-scale-codec-derive",
@@ -3953,6 +4016,12 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb"
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.6.5"
@@ -6508,10 +6577,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]]
name = "x25519-dalek"
version = "1.2.0"
name = "wyz"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077"
checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e"
dependencies = [
"tap",
]
[[package]]
name = "x25519-dalek"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f"
dependencies = [
"curve25519-dalek",
"rand_core 0.5.1",
@@ -6535,9 +6613,9 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zeroize"
version = "1.3.0"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"
checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619"
dependencies = [
"zeroize_derive",
]
+1
View File
@@ -26,6 +26,7 @@ members = [
"common/config",
"common/credentials",
"common/crypto",
"common/crypto/dkg",
"common/bandwidth-claim-contract",
"common/cosmwasm-smart-contracts/coconut-bandwidth-contract",
"common/cosmwasm-smart-contracts/contracts-common",
+41
View File
@@ -0,0 +1,41 @@
[package]
name = "dkg"
version = "0.1.0"
edition = "2021"
resolver = "2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bitvec = "1.0.0"
# unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork
# as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="gt-serialisation", default-features = false, features = ["alloc", "pairings", "experimental", "zeroize"] }
bs58 = "0.4"
lazy_static = "1.4.0"
rand = { version = "0.8.5", default-features = false}
rand_chacha = "0.3"
rand_core = "0.6.3"
sha2 = "0.9"
serde = "1.0"
serde_derive = "1.0"
thiserror = "1.0"
zeroize = { version = "1.4", features = ["zeroize_derive"] }
[dependencies.group]
version = "0.11"
default-features = false
[dependencies.ff]
version = "0.11"
default-features = false
[dev-dependencies]
criterion = "0.3"
[[bench]]
name = "benchmarks"
harness = false
+541
View File
@@ -0,0 +1,541 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::{G1Projective, G2Affine, G2Prepared, Scalar};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use dkg::bte::encryption::BabyStepGiantStepLookup;
use dkg::bte::proof_chunking::ProofOfChunking;
use dkg::bte::proof_discrete_log::ProofOfDiscreteLog;
use dkg::bte::proof_sharing::ProofOfSecretSharing;
use dkg::bte::{
decrypt_share, encrypt_shares, keygen, proof_chunking, proof_sharing, setup, DecryptionKey,
Epoch, PublicKey,
};
use dkg::interpolation::polynomial::Polynomial;
use dkg::{Dealing, NodeIndex, Share};
use ff::Field;
use rand_core::{RngCore, SeedableRng};
use std::collections::BTreeMap;
pub fn precompute_default_bsgs_table(c: &mut Criterion) {
c.bench_function("bsgs default table", |b| {
b.iter(|| black_box(BabyStepGiantStepLookup::default()))
});
}
pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) {
let g2 = G2Affine::generator();
c.bench_function("precomputing G2Prepared", |b| {
b.iter(|| black_box(G2Prepared::from(g2)))
});
}
fn prepare_keys(
mut rng: impl RngCore,
nodes: usize,
) -> (BTreeMap<NodeIndex, PublicKey>, Vec<DecryptionKey>) {
let params = setup();
let mut node_indices = (0..nodes).map(|_| rng.next_u64()).collect::<Vec<_>>();
node_indices.sort_unstable();
let mut receivers = BTreeMap::new();
let mut dks = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
dks.push(dk)
}
(receivers, dks)
}
pub fn creating_dealing_for_3_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 2;
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 3);
c.bench_function("creating single dealing for 3 parties (threshold 2)", |b| {
b.iter(|| {
black_box({
Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
)
})
})
});
}
pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 2;
let epoch = Epoch::new(2);
let (receivers, mut dks) = prepare_keys(&mut rng, 3);
let (dealing, _) = Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
);
let first_key = dks.get_mut(0).unwrap();
first_key.try_update_to(epoch, &params, &mut rng).unwrap();
c.bench_function(
"verifying single dealing made for 3 parties (threshold 2) and recovering share",
|b| {
b.iter(|| {
assert!(dealing
.verify(&params, epoch, threshold, &receivers)
.is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
})
},
);
}
pub fn creating_dealing_for_20_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 14;
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 20);
c.bench_function(
"creating single dealing for 20 parties (threshold 14)",
|b| {
b.iter(|| {
black_box({
Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
)
})
})
},
);
}
pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 14;
let epoch = Epoch::new(2);
let (receivers, mut dks) = prepare_keys(&mut rng, 20);
let (dealing, _) = Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
);
let first_key = dks.get_mut(0).unwrap();
first_key.try_update_to(epoch, &params, &mut rng).unwrap();
c.bench_function(
"verifying single dealing made for 20 parties (threshold 14) and recovering share",
|b| {
b.iter(|| {
assert!(dealing
.verify(&params, epoch, threshold, &receivers)
.is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
})
},
);
}
pub fn creating_dealing_for_100_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 67;
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
c.bench_function(
"creating single dealing for 100 parties (threshold 67)",
|b| {
b.iter(|| {
black_box({
Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
)
})
})
},
);
}
pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 67;
let epoch = Epoch::new(2);
let (receivers, mut dks) = prepare_keys(&mut rng, 100);
let (dealing, _) = Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
);
let first_key = dks.get_mut(0).unwrap();
first_key.try_update_to(epoch, &params, &mut rng).unwrap();
c.bench_function(
"verifying single dealing made for 100 parties (threshold 67) and recovering share",
|b| {
b.iter(|| {
assert!(dealing
.verify(&params, epoch, threshold, &receivers)
.is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
})
},
);
}
pub fn creating_proof_of_key_possession(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let g1 = G1Projective::generator();
let x = Scalar::random(&mut rng);
let y = g1 * x;
c.bench_function("creating proof of key possession", |b| {
b.iter(|| black_box(ProofOfDiscreteLog::construct(&mut rng, &y, &x)))
});
}
pub fn verifying_proof_of_key_possession(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let g1 = G1Projective::generator();
let x = Scalar::random(&mut rng);
let y = g1 * x;
let zk_proof = ProofOfDiscreteLog::construct(&mut rng, &y, &x);
c.bench_function("verifying proof of key possession", |b| {
b.iter(|| black_box(zk_proof.verify(&y)))
});
}
pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
let polynomial = Polynomial::new_random(&mut rng, 67);
let shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, &params, &mut rng);
c.bench_function("creating proof of chunking for 100 parties", |b| {
b.iter(|| {
let chunking_instance =
proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts);
black_box(
ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares)
.expect("failed to construct proof of chunking"),
)
})
});
}
pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
let polynomial = Polynomial::new_random(&mut rng, 67);
let shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, &params, &mut rng);
let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts);
let proof_of_chunking =
ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares)
.expect("failed to construct proof of chunking");
c.bench_function("verifying proof of chunking for 100 parties", |b| {
b.iter(|| {
let chunking_instance =
proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts);
black_box(proof_of_chunking.verify(chunking_instance))
})
});
}
pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
let polynomial = Polynomial::new_random(&mut rng, 67);
let shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, &params, &mut rng);
let combined_ciphertexts = ciphertexts.combine_ciphertexts();
let combined_r = hazmat.combine_rs();
let combined_rr = ciphertexts.combine_rs();
let public_coefficients = polynomial.public_coefficients();
c.bench_function("creating proof of secret sharing for 100 parties", |b| {
b.iter(|| {
let sharing_instance = proof_sharing::Instance::new(
&receivers,
&public_coefficients,
&combined_rr,
&combined_ciphertexts,
);
black_box(
ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares)
.expect("failed to construct proof of secret sharing"),
)
})
});
}
pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
let polynomial = Polynomial::new_random(&mut rng, 67);
let shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, &params, &mut rng);
let combined_ciphertexts = ciphertexts.combine_ciphertexts();
let combined_r = hazmat.combine_rs();
let combined_rr = ciphertexts.combine_rs();
let public_coefficients = polynomial.public_coefficients();
let sharing_instance = proof_sharing::Instance::new(
&receivers,
&public_coefficients,
&combined_rr,
&combined_ciphertexts,
);
let proof_of_secret_sharing =
ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares)
.expect("failed to construct proof of secret sharing");
c.bench_function("verifying proof of secret sharing for 100 parties", |b| {
b.iter(|| {
let sharing_instance = proof_sharing::Instance::new(
&receivers,
&public_coefficients,
&combined_rr,
&combined_ciphertexts,
);
black_box(proof_of_secret_sharing.verify(sharing_instance))
})
});
}
pub fn single_share_encryption(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (_, pk) = keygen(&params, &mut rng);
let polynomial = Polynomial::new_random(&mut rng, 3);
let share: Share = polynomial.evaluate_at(&Scalar::from(42)).into();
c.bench_function("single share encryption", |b| {
b.iter(|| {
black_box(encrypt_shares(
&[(&share, pk.public_key())],
epoch,
&params,
&mut rng,
))
})
});
}
pub fn share_encryption_100(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
let polynomial = Polynomial::new_random(&mut rng, 3);
let shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
c.bench_function("100 shares encryption", |b| {
b.iter(|| {
black_box(encrypt_shares(
&remote_share_key_pairs,
epoch,
&params,
&mut rng,
))
})
});
}
pub fn share_decryption(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (mut dk, pk) = keygen(&params, &mut rng);
let polynomial = Polynomial::new_random(&mut rng, 3);
let share: Share = polynomial.evaluate_at(&Scalar::from(42)).into();
let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], epoch, &params, &mut rng);
dk.try_update_to(epoch, &params, &mut rng).unwrap();
c.bench_function("single share decryption", |b| {
b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, epoch, None)))
});
}
criterion_group!(
utils,
precompute_default_bsgs_table,
precomputing_g2_generator_for_miller_loop,
);
criterion_group!(
dealings_creation,
creating_dealing_for_3_parties,
creating_dealing_for_20_parties,
creating_dealing_for_100_parties,
);
// note: in our setting each party will have to create at least 4 dealings (one per attribute in credential)
// and verify 99 * 4 of them (4 from each other dealer)
criterion_group!(
dealings_verification,
verifying_dealing_made_for_3_parties_and_recovering_share,
verifying_dealing_made_for_20_parties_and_recovering_share,
verifying_dealing_made_for_100_parties_and_recovering_share,
);
criterion_group!(
proofs_of_knowledge,
creating_proof_of_key_possession,
verifying_proof_of_key_possession,
creating_proof_of_chunking_for_100_parties,
verifying_proof_of_chunking_for_100_parties,
creating_proof_of_secret_sharing_for_100_parties,
verifying_proof_of_secret_sharing_for_100_parties
);
criterion_group!(
encryption,
single_share_encryption,
share_encryption_100,
share_decryption,
);
criterion_main!(
utils,
dealings_creation,
dealings_verification,
proofs_of_knowledge,
encryption
);
// TODO: benchmark using affine vs projective representation throughout the crate
// (when conversion / serialization / computation is involved)
+772
View File
@@ -0,0 +1,772 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::keys::{DecryptionKey, PublicKey};
use crate::bte::{Epoch, Params, CHUNK_SIZE, G2_GENERATOR_PREPARED, NUM_CHUNKS, PAIRING_BASE};
use crate::error::DkgError;
use crate::utils::{combine_g1_chunks, combine_scalar_chunks, deserialize_g1, deserialize_g2};
use crate::{Chunk, ChunkedShare, Share};
use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar};
use ff::Field;
use group::{Curve, Group, GroupEncoding};
use rand_core::RngCore;
use std::collections::HashMap;
use std::ops::Neg;
use zeroize::Zeroize;
#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub struct Ciphertexts {
pub rr: [G1Projective; NUM_CHUNKS],
pub ss: [G1Projective; NUM_CHUNKS],
pub zz: [G2Projective; NUM_CHUNKS],
pub ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>,
}
impl Ciphertexts {
pub fn verify_integrity(&self, params: &Params, epoch: Epoch) -> bool {
// if this checks fails it means the ciphertext is undefined as values
// in `r`, `s` and `z` are meaningless since technically this ciphertext
// has been created for 0 parties
if self.ciphertext_chunks.is_empty() {
return false;
}
let g1_neg = G1Affine::generator().neg();
let f = epoch
.as_extended_tau(&self.rr, &self.ss, &self.ciphertext_chunks)
.evaluate_f(params);
// we have to use `f` in up to `NUM_CHUNKS` pairings (if everything is valid),
// so perform some precomputation on it
let f_prepared = G2Prepared::from(f.to_affine());
// for each triple (R_i, S_i, Z_i) check whether e(g1, Z_i) == e(R_j, f) • e(S_i, h),
// which is equivalent to checking whether e(R_j, f) • e(S_i, h) • e(g1, Z_i)^-1 == id
// and due to bilinear property whether e(R_j, f) • e(S_i, h) • e(g1^-1, Z_i) == id
for i in 0..self.rr.len() {
let miller = bls12_381::multi_miller_loop(&[
(&self.rr[i].to_affine(), &f_prepared),
(&self.ss[i].to_affine(), &params._h_prepared),
(&g1_neg, &G2Prepared::from(self.zz[i].to_affine())),
]);
let res = miller.final_exponentiation();
if !bool::from(res.is_identity()) {
return false;
}
}
true
}
pub fn combine_rs(&self) -> G1Projective {
combine_g1_chunks(&self.rr)
}
// required for the purposes of the proof of secret sharing
pub fn combine_ciphertexts(&self) -> Vec<G1Projective> {
self.ciphertext_chunks
.iter()
.map(|share_ciphertext| combine_g1_chunks(share_ciphertext))
.collect()
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let num_receivers = self.ciphertext_chunks.len();
let mut bytes = Vec::with_capacity(NUM_CHUNKS * ((num_receivers + 2) * 48 + 96) + 4);
for r_i in &self.rr {
bytes.extend_from_slice(r_i.to_bytes().as_ref())
}
for s_i in &self.ss {
bytes.extend_from_slice(s_i.to_bytes().as_ref())
}
for z_i in &self.zz {
bytes.extend_from_slice(z_i.to_bytes().as_ref())
}
bytes.extend_from_slice(&(num_receivers as u32).to_be_bytes());
for c_i in &self.ciphertext_chunks {
for c_ij in c_i {
bytes.extend_from_slice(c_ij.to_bytes().as_ref())
}
}
bytes
}
pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
// at the very minimum we must have enough bytes for a single receiver
if bytes.len() < NUM_CHUNKS * (3 * 48 + 96) + 4 {
return Err(DkgError::new_deserialization_failure(
"Ciphertexts",
"insufficient number of bytes provided",
));
}
let mut rr = Vec::with_capacity(NUM_CHUNKS);
let mut ss = Vec::with_capacity(NUM_CHUNKS);
let mut zz = Vec::with_capacity(NUM_CHUNKS);
let mut i = 0;
for _ in 0..NUM_CHUNKS {
rr.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure("Ciphertexts.r", "invalid curve point")
})?);
i += 48;
}
for _ in 0..NUM_CHUNKS {
ss.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure("Ciphertexts.s", "invalid curve point")
})?);
i += 48;
}
for _ in 0..NUM_CHUNKS {
zz.push(deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure("Ciphertexts.z", "invalid curve point")
})?);
i += 96;
}
let num_receivers = u32::from_be_bytes(bytes[i..i + 4].try_into().unwrap()) as usize;
i += 4;
if bytes[i..].len() != num_receivers * NUM_CHUNKS * 48 {
return Err(DkgError::new_deserialization_failure(
"Ciphertexts",
"invalid number of bytes provided",
));
}
let mut ciphertext_chunks = Vec::with_capacity(num_receivers);
for _ in 0..num_receivers {
let mut ci = Vec::with_capacity(NUM_CHUNKS);
for _ in 0..NUM_CHUNKS {
ci.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure(
"Ciphertexts.ciphertext_chunks",
"invalid curve point",
)
})?);
i += 48;
}
// this unwrap is fine as we have exactly NUM_CHUNKS elements in each vector
ciphertext_chunks.push(ci.try_into().unwrap())
}
// and the same is true here, the unwraps are fine as we have exactly NUM_CHUNKS elements in each as required
Ok(Ciphertexts {
rr: rr.try_into().unwrap(),
ss: ss.try_into().unwrap(),
zz: zz.try_into().unwrap(),
ciphertext_chunks,
})
}
}
#[derive(Zeroize)]
#[zeroize(drop)]
/// Randomness generated during ciphertext generation that is required for proofs of knowledge.
/// It must be handled with extreme care as its misuse might help malicious parties to recover
/// the underlying plaintext.
pub struct HazmatRandomness {
r: [Scalar; NUM_CHUNKS],
s: [Scalar; NUM_CHUNKS],
}
impl HazmatRandomness {
pub fn r(&self) -> &[Scalar; NUM_CHUNKS] {
&self.r
}
pub fn s(&self) -> &[Scalar; NUM_CHUNKS] {
&self.s
}
pub fn combine_rs(&self) -> Scalar {
combine_scalar_chunks(&self.r)
}
}
pub fn encrypt_shares(
shares: &[(&Share, &PublicKey)],
epoch: Epoch,
params: &Params,
mut rng: impl RngCore,
) -> (Ciphertexts, HazmatRandomness) {
let g1 = G1Projective::generator();
let mut rand_rs = Vec::with_capacity(NUM_CHUNKS);
let mut rand_ss = Vec::with_capacity(NUM_CHUNKS);
let mut rr = Vec::with_capacity(NUM_CHUNKS);
let mut ss = Vec::with_capacity(NUM_CHUNKS);
// generate relevant re-usable pseudorandom data
for _ in 0..NUM_CHUNKS {
let rand_r = Scalar::random(&mut rng);
let rand_s = Scalar::random(&mut rng);
// g1^r
let rr_i = g1 * rand_r;
// g1^s
let ss_i = g1 * rand_s;
rand_rs.push(rand_r);
rand_ss.push(rand_s);
rr.push(rr_i);
ss.push(ss_i);
}
// produce per-chunk ciphertexts
let mut cc = Vec::with_capacity(shares.len());
for (share, pk) in shares {
let m = share.to_chunks();
let mut ci = Vec::with_capacity(NUM_CHUNKS);
for (j, chunk) in m.chunks.iter().enumerate() {
// can't really have a more efficient implementation until https://github.com/zkcrypto/bls12_381/pull/70 is merged...
let c = pk.0 * rand_rs[j] + g1 * Scalar::from(*chunk as u64);
ci.push(c)
}
// the conversion must succeed since we must have EXACTLY `NUM_CHUNKS` elements
cc.push(ci.try_into().unwrap())
}
// convert into arrays, note that the unwraps are fine as we have exactly `NUM_CHUNKS` elements in each vector
let rr = rr.try_into().unwrap();
let ss = ss.try_into().unwrap();
let f = epoch.as_extended_tau(&rr, &ss, &cc).evaluate_f(params);
let mut zz = Vec::with_capacity(NUM_CHUNKS);
for i in 0..NUM_CHUNKS {
zz.push(f * rand_rs[i] + params.h * rand_ss[i]);
}
// the conversions here must also succeed since the other vecs also have `NUM_CHUNKS` elements
(
Ciphertexts {
rr,
ss,
zz: zz.try_into().unwrap(),
ciphertext_chunks: cc,
},
HazmatRandomness {
r: rand_rs.try_into().unwrap(),
s: rand_ss.try_into().unwrap(),
},
)
}
pub fn decrypt_share(
dk: &DecryptionKey,
// in the case of multiple receivers, specifies which index of ciphertext chunks should be used
i: usize,
ciphertext: &Ciphertexts,
epoch: Epoch,
lookup_table: Option<&BabyStepGiantStepLookup>,
) -> Result<Share, DkgError> {
let mut plaintext = ChunkedShare::default();
let decryption_node = dk.try_get_compatible_node(epoch)?;
let extended_tau = epoch.as_extended_tau(
&ciphertext.rr,
&ciphertext.ss,
&ciphertext.ciphertext_chunks,
);
if i >= ciphertext.ciphertext_chunks.len() {
return Err(DkgError::UnavailableCiphertext(i));
}
let height = decryption_node.tau.height();
let b_neg = decryption_node
.ds
.iter()
.chain(decryption_node.dh.iter())
.zip(extended_tau.0.iter().by_vals().skip(height))
.filter(|(_, i)| *i)
.map(|(d_i, _)| d_i)
.fold(decryption_node.b, |acc, d_i| acc + d_i)
.neg()
.to_affine();
let e_neg = decryption_node.e.neg().to_affine();
for j in 0..NUM_CHUNKS {
let rr_j = &ciphertext.rr[j];
let ss_j = &ciphertext.ss[j];
let zz_j = ciphertext.zz[j].to_affine();
let cc_ij = &ciphertext.ciphertext_chunks[i][j];
let miller = bls12_381::multi_miller_loop(&[
(&cc_ij.to_affine(), &G2_GENERATOR_PREPARED),
(&rr_j.to_affine(), &G2Prepared::from(b_neg)),
(&decryption_node.a.to_affine(), &G2Prepared::from(zz_j)),
(&ss_j.to_affine(), &G2Prepared::from(e_neg)),
]);
let m = miller.final_exponentiation();
plaintext.chunks[j] = baby_step_giant_step(&m, &PAIRING_BASE, lookup_table)?;
}
plaintext.try_into()
}
pub struct BabyStepGiantStepLookup {
base: Gt,
m: Chunk,
lookup: HashMap<[u8; 576], Chunk>,
}
impl BabyStepGiantStepLookup {
pub fn precompute(base: &Gt) -> Self {
let mut lookup = HashMap::new();
let mut g = Gt::identity();
// 1. m ← Ceiling(√n)
let m = (CHUNK_SIZE as f32).sqrt().ceil() as Chunk;
// 2. For all j where 0 ≤ j < m:
for j in 0..m {
// Compute α^j and store the pair (j, α^j) in a table.
lookup.insert(g.to_uncompressed(), j);
g += base;
}
BabyStepGiantStepLookup {
base: *base,
m,
lookup,
}
}
pub fn try_solve(&self, target: &Gt) -> Result<Chunk, DkgError> {
// 3. Compute α^{m}
let m_neg = Scalar::from(self.m as u64).neg();
let alpha_m = self.base * m_neg;
// 4. γ ← β. (set γ = β)
let mut gamma = *target;
// 5. For all i where 0 ≤ i < m:
for i in 0..self.m {
// 1. Check to see if γ is the second component (αj) of any pair in the table.
if let Some(j) = self.lookup.get(&gamma.to_uncompressed()) {
// 2. If so, return im + j.
return Ok(i * self.m + j);
} else {
// 3. If not, γγα^{m}.
gamma += alpha_m;
}
}
Err(DkgError::UnsolvableDiscreteLog)
}
}
impl Default for BabyStepGiantStepLookup {
fn default() -> Self {
BabyStepGiantStepLookup::precompute(&PAIRING_BASE)
}
}
/// Attempts to solve the discrete log problem g^m, where g is in the Gt group and
/// m should be within the [0, CHUNK_MAX] range.
///
/// The implementation follows the following algorithm: https://en.wikipedia.org/wiki/Baby-step_giant-step#The_algorithm
///
/// # Arguments
///
/// * `target`: the result of the exponentiation, M in M = g^m,
/// * `base`: the base used for exponentiation, g in M = g^m
/// * `lookup_table`: precomputed table containing (j, α^j) pairs
pub fn baby_step_giant_step(
target: &Gt,
base: &Gt,
lookup_table: Option<&BabyStepGiantStepLookup>,
) -> Result<Chunk, DkgError> {
if let Some(lookup_table) = lookup_table {
// compute expected m to make sure the provided lookup is valid
let m = (CHUNK_SIZE as f32).sqrt().ceil() as Chunk;
if &lookup_table.base != base || lookup_table.lookup.len() != m as usize {
return Err(DkgError::MismatchedLookupTable);
}
lookup_table.try_solve(target)
} else {
BabyStepGiantStepLookup::precompute(base).try_solve(target)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bte::{keygen, setup, DEFAULT_BSGS_TABLE};
use rand_core::SeedableRng;
fn verify_hazmat_rand(ciphertext: &Ciphertexts, randomness: &HazmatRandomness) {
let g1 = G1Projective::generator();
for i in 0..ciphertext.rr.len() {
assert_eq!(ciphertext.rr[i], g1 * randomness.r[i]);
assert_eq!(ciphertext.ss[i], g1 * randomness.s[i]);
}
}
#[test]
fn baby_giant_100_without_table() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
for i in 0u64..100 {
let base = Gt::random(&mut rng);
let x = (rng.next_u64() + i) % CHUNK_SIZE as u64;
let target = base * Scalar::from(x);
assert_eq!(
baby_step_giant_step(&target, &base, None).unwrap(),
x as Chunk
);
}
}
#[test]
fn baby_giant_100_with_table() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let base = Gt::random(&mut rng);
let lookup_table = BabyStepGiantStepLookup::precompute(&base);
let table = Some(&lookup_table);
for i in 0u64..100 {
let x = (rng.next_u64() + i) % CHUNK_SIZE as u64;
let target = base * Scalar::from(x);
assert_eq!(
baby_step_giant_step(&target, &base, table).unwrap(),
x as Chunk
);
}
}
#[test]
fn share_decryption_20() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let (decryption_key1, public_key1) = keygen(&params, &mut rng);
let (decryption_key2, public_key2) = keygen(&params, &mut rng);
let epoch = Epoch::new(0);
let lookup_table = &DEFAULT_BSGS_TABLE;
for _ in 0..10 {
let m1 = Share::random(&mut rng);
let m2 = Share::random(&mut rng);
let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)];
let (ciphertext, hazmat) = encrypt_shares(shares, epoch, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered1 =
decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap();
let recovered2 =
decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap();
assert_eq!(m1, recovered1);
assert_eq!(m2, recovered2);
}
}
#[test]
fn share_encryption_under_nonzero_epoch() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let (mut decryption_key1, public_key1) = keygen(&params, &mut rng);
let (mut decryption_key2, public_key2) = keygen(&params, &mut rng);
let epoch = Epoch::new(12345);
decryption_key1
.try_update_to(epoch, &params, &mut rng)
.unwrap();
decryption_key2
.try_update_to(epoch, &params, &mut rng)
.unwrap();
let lookup_table = &DEFAULT_BSGS_TABLE;
for _ in 0..10 {
let m1 = Share::random(&mut rng);
let m2 = Share::random(&mut rng);
let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)];
let (ciphertext, hazmat) = encrypt_shares(shares, epoch, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered1 =
decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap();
let recovered2 =
decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap();
assert_eq!(m1, recovered1);
assert_eq!(m2, recovered2);
}
}
#[test]
fn decryption_with_root_key() {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let (root_key, public_key) = keygen(&params, &mut rng);
let share = Share::random(&mut rng);
let epoch0 = Epoch::new(0);
let epoch42 = Epoch::new(42);
let epoch_big = Epoch::new(3292547435);
let (ciphertext1, hazmat1) =
encrypt_shares(&[(&share, &public_key.key)], epoch0, &params, &mut rng);
verify_hazmat_rand(&ciphertext1, &hazmat1);
let (ciphertext2, hazmat2) =
encrypt_shares(&[(&share, &public_key.key)], epoch42, &params, &mut rng);
verify_hazmat_rand(&ciphertext2, &hazmat2);
let (ciphertext3, hazmat3) =
encrypt_shares(&[(&share, &public_key.key)], epoch_big, &params, &mut rng);
verify_hazmat_rand(&ciphertext3, &hazmat3);
let recovered1 = decrypt_share(&root_key, 0, &ciphertext1, epoch0, None).unwrap();
let recovered2 = decrypt_share(&root_key, 0, &ciphertext2, epoch42, None).unwrap();
let recovered3 = decrypt_share(&root_key, 0, &ciphertext3, epoch_big, None).unwrap();
assert_eq!(share, recovered1);
assert_eq!(share, recovered2);
assert_eq!(share, recovered3);
}
#[test]
fn update_and_decrypt_10() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let (mut decryption_key, public_key) = keygen(&params, &mut rng);
for epoch_value in 0..10 {
let epoch = Epoch::new(epoch_value);
let share = Share::random(&mut rng);
decryption_key
.try_update_to(epoch, &params, &mut rng)
.unwrap();
let (ciphertext, hazmat) =
encrypt_shares(&[(&share, &public_key.key)], epoch, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap();
assert_eq!(share, recovered);
}
}
#[test]
fn reblinding_node_doesnt_affect_decryption() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let (mut decryption_key, public_key) = keygen(&params, &mut rng);
let epoch = Epoch::new(12345);
decryption_key
.try_update_to(epoch, &params, &mut rng)
.unwrap();
for node in decryption_key.nodes.iter_mut() {
node.reblind(&params, &mut rng);
}
let share = Share::random(&mut rng);
let (ciphertext, hazmat) =
encrypt_shares(&[(&share, &public_key.key)], epoch, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap();
assert_eq!(share, recovered);
// attempt to update the key again so we have to derive fresh nodes using previous reblinded results
let epoch2 = Epoch::new(67890);
decryption_key
.try_update_to(epoch2, &params, &mut rng)
.unwrap();
for node in decryption_key.nodes.iter_mut() {
node.reblind(&params, &mut rng);
}
let share2 = Share::random(&mut rng);
let (ciphertext, hazmat) =
encrypt_shares(&[(&share2, &public_key.key)], epoch2, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch2, None).unwrap();
assert_eq!(share2, recovered);
}
#[test]
fn ciphertext_integrity_check_passes_for_valid_data() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, public_key) = keygen(&params, &mut rng);
let epoch = Epoch::new(1);
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let share = Share::random(&mut rng);
let (ciphertext, _) =
encrypt_shares(&[(&share, &public_key.key)], epoch, &params, &mut rng);
assert!(ciphertext.verify_integrity(&params, epoch))
}
#[test]
fn ciphertext_integrity_check_passes_fails_for_malformed_data() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, public_key) = keygen(&params, &mut rng);
let epoch = Epoch::new(1);
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let share = Share::random(&mut rng);
let (ciphertext, _) =
encrypt_shares(&[(&share, &public_key.key)], epoch, &params, &mut rng);
let mut bad_cipher1 = ciphertext.clone();
bad_cipher1.rr[4] = G1Projective::generator();
assert!(!bad_cipher1.verify_integrity(&params, epoch));
let mut bad_cipher2 = ciphertext.clone();
bad_cipher2.ss[4] = G1Projective::generator();
assert!(!bad_cipher2.verify_integrity(&params, epoch));
let mut bad_cipher3 = ciphertext;
bad_cipher3.zz[4] = G2Projective::generator();
assert!(!bad_cipher3.verify_integrity(&params, epoch));
}
#[test]
fn ciphertext_integrity_check_passes_fails_for_wrong_epoch() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, public_key) = keygen(&params, &mut rng);
let epoch = Epoch::new(1);
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let share = Share::random(&mut rng);
let (ciphertext, _) =
encrypt_shares(&[(&share, &public_key.key)], epoch, &params, &mut rng);
let another_epoch = Epoch::new(2);
assert!(!ciphertext.verify_integrity(&params, another_epoch))
}
#[test]
fn ciphertext_combining() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let nodes = 3;
let mut shares = Vec::new();
let mut public_keys = Vec::new();
for _ in 0..nodes {
shares.push(Share::random(&mut rng));
let (_, pk) = keygen(&params, &mut rng);
public_keys.push(*pk.public_key());
}
let refs = shares.iter().zip(public_keys.iter()).collect::<Vec<_>>();
let (ciphertext, hazmat) = encrypt_shares(&refs, Epoch::new(42), &params, &mut rng);
let combined_r = combine_scalar_chunks(hazmat.r());
let combined_rr = ciphertext.combine_rs();
let combined_ciphertexts = ciphertext.combine_ciphertexts();
let g1 = G1Projective::generator();
for i in 0..nodes {
let expected = public_keys[i].0 * combined_r + g1 * shares[i].0;
assert_eq!(expected, combined_ciphertexts[i]);
assert_eq!(combined_rr, g1 * combined_r);
}
}
#[test]
fn ciphertexts_roundtrip() {
fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts {
Ciphertexts {
rr: (0..NUM_CHUNKS)
.map(|_| G1Projective::random(&mut rng))
.collect::<Vec<_>>()
.try_into()
.unwrap(),
ss: (0..NUM_CHUNKS)
.map(|_| G1Projective::random(&mut rng))
.collect::<Vec<_>>()
.try_into()
.unwrap(),
zz: (0..NUM_CHUNKS)
.map(|_| G2Projective::random(&mut rng))
.collect::<Vec<_>>()
.try_into()
.unwrap(),
ciphertext_chunks: (0..num_receivers)
.map(|_| {
(0..NUM_CHUNKS)
.map(|_| G1Projective::random(&mut rng))
.collect::<Vec<_>>()
.try_into()
.unwrap()
})
.collect(),
}
}
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let good_ciphertexts = vec![
random_ciphertexts(&mut rng, 1),
random_ciphertexts(&mut rng, 2),
random_ciphertexts(&mut rng, 10),
];
for ciphertexts in &good_ciphertexts {
let bytes = ciphertexts.to_bytes();
let recovered = Ciphertexts::try_from_bytes(&bytes).unwrap();
assert_eq!(ciphertexts, &recovered);
}
// ciphertext for 0 receivers is invalid by default
let ciphertexts = random_ciphertexts(&mut rng, 0);
let bytes = ciphertexts.to_bytes();
assert!(Ciphertexts::try_from_bytes(&bytes).is_err());
}
}
+875
View File
@@ -0,0 +1,875 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::proof_discrete_log::ProofOfDiscreteLog;
use crate::bte::{Epoch, Params, Tau};
use crate::error::DkgError;
use crate::utils::{deserialize_g1, deserialize_g2, deserialize_scalar};
use bls12_381::{G1Projective, G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand_core::RngCore;
use zeroize::Zeroize;
#[derive(Debug, Zeroize)]
#[zeroize(drop)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub(crate) struct Node {
pub(crate) tau: Tau,
// g1^rho
pub(crate) a: G1Projective,
// g2^x
pub(crate) b: G2Projective,
// f_i^rho, up to lambda_t elements
pub(crate) ds: Vec<G2Projective>,
// fh_i^rho, always lambda_h elements
pub(crate) dh: Vec<G2Projective>,
// h^rho
pub(crate) e: G2Projective,
}
impl Node {
fn new_root(
a: G1Projective,
b: G2Projective,
ds: Vec<G2Projective>,
dh: Vec<G2Projective>,
e: G2Projective,
) -> Self {
Node {
tau: Tau::new_root(),
a,
b,
ds,
dh,
e,
}
}
fn is_root(&self) -> bool {
self.tau.0.is_empty()
}
pub(crate) fn reblind(&mut self, params: &Params, mut rng: impl RngCore) {
let delta = Scalar::random(&mut rng);
self.a += G1Projective::generator() * delta;
// TODO: or do we have to do full tau evaluation here?
self.b += self.tau.evaluate_partial_f(params) * delta;
self.ds
.iter_mut()
.zip(params.fs.iter().skip(self.tau.height()))
.for_each(|(d_i, f_i)| *d_i += f_i * delta);
self.dh
.iter_mut()
.zip(params.fh.iter())
.for_each(|(d_i, f_i)| *d_i += f_i * delta);
self.e += params.h * delta;
}
// note: it's unsafe to use this method outside `try_update_to` as
// we have guaranteed there that `self` is parent of the target
// and that `self.tau != target_tau`
/// Given `self` with `Tau1` and `target_tau` with `Tau2`, such that `Tau1` prefixes `Tau2`,
/// i.e. `Tau2 == Tau1 || SUFFIX`, and `Tau2` is a leaf node, derive all required crypto material
/// for its construction.
fn derive_target_child_with_partials(
&self,
params: &Params,
target_tau: Tau,
partial_b: &G2Projective,
partial_f: &G2Projective,
mut rng: impl RngCore,
) -> Self {
debug_assert!(self.tau.is_parent_of(&target_tau));
debug_assert_ne!(self.tau, target_tau);
let delta = Scalar::random(&mut rng);
let a = self.a + G1Projective::generator() * delta;
let b = partial_b + partial_f * delta;
let ds = self
.ds
.iter()
.zip(params.fs.iter())
.skip(target_tau.height())
.map(|(d_i, f_i)| d_i + f_i * delta)
.collect();
let dh = self
.dh
.iter()
.zip(params.fh.iter())
.map(|(dh_i, fh_i)| dh_i + fh_i * delta)
.collect();
let e = self.e + params.h * delta;
Node {
tau: target_tau,
a,
b,
ds,
dh,
e,
}
}
// note: it's unsafe to use this method outside `try_update_to` as
// we have guaranteed there that `self` is parent of the target
// and that `self.tau != target_tau`
/// Given `self` with `Tau1` and `most_direct_parent` with `Tau2`, such that `Tau1` prefixes `Tau2`,
/// i.e. `Tau2 == Tau1 || SUFFIX`, derive node with `Tau3 = Tau2 || 1`
fn derive_right_nonfinal_child_of_with_partials(
&self,
params: &Params,
most_direct_parent: Tau,
partial_b: &G2Projective,
partial_f: &G2Projective,
mut rng: impl RngCore,
) -> Self {
let right_branch = most_direct_parent.right_child();
debug_assert!(self.tau.is_parent_of(&most_direct_parent));
debug_assert!(self.tau.is_parent_of(&right_branch));
debug_assert_ne!(self.tau, right_branch);
// n is height difference between self and the child
let n = right_branch.height() - self.tau.height();
// i is the index of the last bit we just added
let i = right_branch.height() - 1;
let delta = Scalar::random(&mut rng);
let a = self.a + G1Projective::generator() * delta;
let d0 = self.ds[n - 1];
let b = partial_b + d0 + (partial_f + params.fs[i]) * delta;
let ds = self
.ds
.iter()
.skip(n)
.zip(params.fs.iter().skip(right_branch.height()))
.map(|(d_i, f_i)| d_i + f_i * delta)
.collect();
let dh = self
.dh
.iter()
.zip(params.fh.iter())
.map(|(dh_i, fh_i)| dh_i + fh_i * delta)
.collect();
let e = self.e + params.h * delta;
Node {
tau: right_branch,
a,
b,
ds,
dh,
e,
}
}
// tau_bytes_len || tau || a || b || len_ds || ds || len_dh || dh || e
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let g1_elements = 1;
let g2_elements = self.ds.len() + self.dh.len() + 2;
let tau_bytes = self.tau.to_bytes();
// the extra 12 comes from the triple u32 we use for encoding lengths of tau, ds and dh
let mut bytes =
Vec::with_capacity(tau_bytes.len() + g1_elements * 48 + g2_elements * 96 + 12);
bytes.extend_from_slice(&((tau_bytes.len() as u32).to_be_bytes()));
bytes.extend_from_slice(&tau_bytes);
bytes.extend_from_slice(self.a.to_bytes().as_ref());
bytes.extend_from_slice(self.b.to_bytes().as_ref());
bytes.extend_from_slice(&((self.ds.len() as u32).to_be_bytes()));
for d_i in &self.ds {
bytes.extend_from_slice(d_i.to_bytes().as_ref());
}
bytes.extend_from_slice(&((self.dh.len() as u32).to_be_bytes()));
for dh_i in &self.dh {
bytes.extend_from_slice(dh_i.to_bytes().as_ref());
}
bytes.extend_from_slice(self.e.to_bytes().as_ref());
bytes
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
// at the very least we require bytes for:
// - tau_len ( 4 )
// - tau ( could be 0 for root node )
// - a ( 48 )
// - b ( 96 )
// - length indication of ds ( 4 )
// - length indication of dh ( 4 )
// - e ( 96 )
if bytes.len() < 4 + 48 + 96 + 4 + 4 + 96 {
return Err(DkgError::new_deserialization_failure(
"Node",
"insufficient number of bytes provided",
));
}
let tau_len = u32::from_be_bytes((&bytes[..4]).try_into().unwrap()) as usize;
let mut i = 4;
let tau = Tau::try_from_bytes(&bytes[i..i + tau_len])?;
i += tau_len;
// perform another length check to account for bytes consumed by tau
if bytes[i..].len() < 48 + 96 + 4 + 4 + 96 {
return Err(DkgError::new_deserialization_failure(
"Node",
"insufficient number of bytes provided",
));
}
let a = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure("Node.a", "invalid curve point")
})?;
i += 48;
let b = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure("Node.b", "invalid curve point")
})?;
i += 96;
let ds_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
if bytes[i..].len() < ds_len * 96 + 4 {
return Err(DkgError::new_deserialization_failure(
"Node",
"insufficient number of bytes provided (ds)",
));
}
let mut ds = Vec::with_capacity(ds_len);
for j in 0..ds_len {
let d_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure(
format!("Node.ds_{}", j),
"invalid curve point",
)
})?;
ds.push(d_i);
i += 96;
}
let dh_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
if bytes[i..].len() != (dh_len + 1) * 96 {
return Err(DkgError::new_deserialization_failure(
"Node",
"insufficient number of bytes provided (dh)",
));
}
let mut dh = Vec::with_capacity(dh_len);
for j in 0..dh_len {
let dh_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure(
format!("Node.dh_{}", j),
"invalid curve point",
)
})?;
dh.push(dh_i);
i += 96;
}
let e = deserialize_g2(&bytes[i..]).ok_or_else(|| {
DkgError::new_deserialization_failure("Node.h", "invalid curve point")
})?;
Ok(Node {
tau,
a,
b,
ds,
dh,
e,
})
}
}
// produces public key and a decryption key for the root of the tree
pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) {
let g1 = G1Projective::generator();
let g2 = G2Projective::generator();
let mut x = Scalar::random(&mut rng);
let y = g1 * x;
let proof = ProofOfDiscreteLog::construct(&mut rng, &y, &x);
let mut rho = Scalar::random(&mut rng);
let a = g1 * rho;
let b = g2 * x + params.f0 * rho;
let ds = params.fs.iter().map(|f_i| f_i * rho).collect();
let dh = params.fh.iter().map(|fh_i| fh_i * rho).collect();
let e = params.h * rho;
let dk = DecryptionKey::new_root(Node::new_root(a, b, ds, dh, e));
let public_key = PublicKey(y);
let key_with_proof = PublicKeyWithProof {
key: public_key,
proof,
};
x.zeroize();
rho.zeroize();
(dk, key_with_proof)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PublicKey(pub(crate) G1Projective);
impl PublicKey {
pub fn verify(&self, proof: &ProofOfDiscreteLog) -> bool {
proof.verify(&self.0)
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct PublicKeyWithProof {
pub(crate) key: PublicKey,
pub(crate) proof: ProofOfDiscreteLog,
}
impl PublicKeyWithProof {
pub fn verify(&self) -> bool {
self.key.verify(&self.proof)
}
pub fn public_key(&self) -> &PublicKey {
&self.key
}
pub fn to_bytes(&self) -> Vec<u8> {
// we have 2 G1 elements and 1 Scalar
let mut bytes = Vec::with_capacity(2 * 48 + 32);
bytes.extend_from_slice(self.key.0.to_bytes().as_ref());
bytes.extend_from_slice(self.proof.rand_commitment.to_bytes().as_ref());
bytes.extend_from_slice(self.proof.response.to_bytes().as_ref());
bytes
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
if bytes.len() != 2 * 48 + 32 {
return Err(DkgError::new_deserialization_failure(
"PublicKeyWithProof",
"provided bytes had invalid length",
));
}
let y_bytes = &bytes[..48];
let commitment_bytes = &bytes[48..96];
let response_bytes = &bytes[96..];
let y = deserialize_g1(y_bytes).ok_or_else(|| {
DkgError::new_deserialization_failure("PublicKeyWithProof.key.0", "invalid curve point")
})?;
let rand_commitment = deserialize_g1(commitment_bytes).ok_or_else(|| {
DkgError::new_deserialization_failure(
"PublicKeyWithProof.proof.rand_commitment",
"invalid curve point",
)
})?;
let response = deserialize_scalar(response_bytes).ok_or_else(|| {
DkgError::new_deserialization_failure(
"PublicKeyWithProof.proof.response",
"invalid scalar",
)
})?;
Ok(PublicKeyWithProof {
key: PublicKey(y),
proof: ProofOfDiscreteLog {
rand_commitment,
response,
},
})
}
}
#[derive(Debug, Zeroize)]
#[zeroize(drop)]
#[cfg_attr(test, derive(PartialEq))]
pub struct DecryptionKey {
// note that the nodes are ordered from "right" to "left"
pub(crate) nodes: Vec<Node>,
}
impl DecryptionKey {
fn new_root(root_node: Node) -> Self {
DecryptionKey {
nodes: vec![root_node],
}
}
fn current(&self) -> Result<&Node, DkgError> {
// we must have at least a single node, otherwise we have a malformed key
self.nodes.last().ok_or(DkgError::MalformedDecryptionKey)
}
pub fn current_epoch(&self, params: &Params) -> Result<Option<Epoch>, DkgError> {
let current_node = self.current()?;
if current_node.is_root() {
Ok(None)
} else {
Epoch::try_from_tau(&current_node.tau, params).map(Option::Some)
}
}
pub(crate) fn try_get_compatible_node(&self, epoch: Epoch) -> Result<&Node, DkgError> {
let tau = epoch.as_tau();
self.nodes
.iter()
.rev()
.find(|node| node.tau.is_parent_of(&tau))
.ok_or(DkgError::ExpiredKey)
}
pub fn try_update_to_next_epoch(
&mut self,
params: &Params,
mut rng: impl RngCore,
) -> Result<(), DkgError> {
if self.nodes.is_empty() {
return Err(DkgError::MalformedDecryptionKey);
}
let mut target_epoch = Epoch::new(0);
if self.nodes.len() == 1 && self.nodes[0].is_root() {
return self.try_update_to(target_epoch, params, &mut rng);
}
// unwrap is fine as we have asserted self.nodes is not empty
self.nodes.pop().unwrap();
if let Some(tail) = self.nodes.last() {
target_epoch = tail.tau.lowest_valid_epoch_child(params)?;
} else {
// essentially our key consisted of only a single node and it wasn't a root,
// so either it was malformed or we somehow reached the final epoch and wanted to update
// beyond that. Either way, update to l + 1 is impossible
return Err(DkgError::MalformedDecryptionKey);
}
self.try_update_to(target_epoch, params, &mut rng)
}
/// Attempts to update `self` to the provided `epoch`. If the update is not possible,
/// because the target was in the past or the key is malformed, an error is returned.
///
/// Note that this method mutates the key in place and if the original key was malformed,
/// there are no guarantees about its internal state post-call.
pub fn try_update_to(
&mut self,
target_epoch: Epoch,
params: &Params,
mut rng: impl RngCore,
) -> Result<(), DkgError> {
if self.nodes.is_empty() {
// somehow we have an empty decryption key
return Err(DkgError::MalformedDecryptionKey);
}
// makes it easier to work with since we will be generating non-leaf nodes
let target_tau = target_epoch.as_tau();
let current_tau = &self.current()?.tau;
if current_tau == &target_tau {
// our key is already updated to the target
return Ok(());
}
if current_tau > &target_tau {
// we cannot derive keys for past epochs
return Err(DkgError::TargetEpochUpdateInThePast);
}
// drop the nodes that are no longer required and get the most direct parent for the target epoch available
let mut parent = loop {
// if pop() fails the key is malformed since we checked that the target_epoch > current_epoch,
// hence the update should have been possible
let tail = self.nodes.pop().ok_or(DkgError::MalformedDecryptionKey)?;
if tail.tau.is_parent_of(&target_tau) {
break tail;
}
};
// essentially the case of updating epoch n to n + 1, where n is even;
// in that case the last two nodes are [..., epoch_{n+1}, epoch_n]
// so we just have to reblind the n+1 node and we're done
if parent.tau == target_tau {
parent.reblind(params, &mut rng);
self.nodes.push(parent);
return Ok(());
}
// accumulators, note that the previous elements have already been included by the parent,
// i.e. for example for parent at height l <= n, b = g2^x * f0^rho * d1^{tau_1} * ... * dl^{tau_l}
// new_b_accumulator = b * d1^{tau_1} * d2^{tau_2} * ... * dn^{tau_n}
// new_f_accumulator = f0 * f1^{tau_1} * f2^{tau_2} * ... * fn^{tau_n} (up to lambda_t)
let mut new_b_accumulator = parent.b;
let mut new_f_accumulator = parent.tau.evaluate_partial_f(params);
let parent_height = parent.tau.height();
// path from the parent to the child
for (n, bit) in target_tau
.0
.iter()
.by_vals()
.skip(parent.tau.height())
.enumerate()
{
// ith bit of the [child] epoch
// note that n represents height difference between parent and the current bit
let i = n + parent_height;
// if the bit is NOT set, push the right '1' subtree (for future keys)
// so for example if given parent with some `PREFIX` tau and target_epoch being `PREFIX || 010`,
// in the first loop iteration we're going to look at bit `0` and
// derive child node `PREFIX || 1` so that in the future we could derive keys for all other epochs starting with `PREFIX || 1`
// in the next loop iteration we're going to look at bit `1` and simply update the accumulators,
// as we don't need to generate any "left" nodes as all of them would have constructed epochs that are already in the past
// finally, in the last iteration, we look at the bit `0` and derive node `PREFIX || 011`,
// i.e. the one that FOLLOWS the target node.
if !bit {
let direct_parent = target_tau.try_get_parent_at_height(i)?;
self.nodes
.push(parent.derive_right_nonfinal_child_of_with_partials(
params,
direct_parent,
&new_b_accumulator,
&new_f_accumulator,
&mut rng,
));
} else {
// only update the accumulators when the bit is set, as d^0 == identity, so there's
// no point in doing anything else;
// note that we don't have to generate any new nodes when going into the right branch
// of the tree as everything on the left would have been in the past, so we don't care about them
new_b_accumulator += parent.ds[n]; // add d0
new_f_accumulator += params.fs[i]; // f_i
}
}
self.nodes.push(parent.derive_target_child_with_partials(
params,
target_epoch.as_tau(),
&new_b_accumulator,
&new_f_accumulator,
&mut rng,
));
Ok(())
}
pub fn to_bytes(&self) -> Vec<u8> {
let num_nodes = self.nodes.len() as u32;
// unfortunately we're not going to know the expected capacity
let mut bytes = Vec::new();
bytes.extend_from_slice(&num_nodes.to_be_bytes());
for node in &self.nodes {
let mut node_bytes = node.to_bytes();
bytes.extend_from_slice(&((node_bytes.len() as u32).to_be_bytes()));
bytes.append(&mut node_bytes)
}
bytes
}
pub fn try_from_bytes(b: &[u8]) -> Result<Self, DkgError> {
// we have to be able to read the length of nodes
if b.len() < 4 {
return Err(DkgError::new_deserialization_failure(
"DecryptionKey",
"insufficient number of bytes provided",
));
}
let nodes_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize;
let mut nodes = Vec::with_capacity(nodes_len);
let mut i = 4;
for _ in 0..nodes_len {
// check if we can actually read the length...
if b[i..].len() < 4 {
return Err(DkgError::new_deserialization_failure(
"DecryptionKey.Node",
"insufficient number of bytes provided for BTE Node recovery",
));
}
let node_bytes = u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]) as usize;
if b[i + 4..].len() < node_bytes {
return Err(DkgError::new_deserialization_failure(
"DecryptionKey.Node",
"insufficient number of bytes provided for BTE Node recovery",
));
}
i += 4;
let node = Node::try_from_bytes(&b[i..i + node_bytes])?;
nodes.push(node);
i += node_bytes;
}
Ok(DecryptionKey { nodes })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bte::setup;
use bitvec::bitvec;
use bitvec::order::Msb0;
use rand_core::SeedableRng;
#[test]
fn basic_coverage_nodes() {
// it's some basic test I've been performing when writing the update function, but figured
// might as well put it into a unit test. note that it doesn't check the entire structure,
// but just the few last nodes of low height
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, _) = keygen(&params, &mut rng);
let root_node_copy = dk.nodes.clone();
// this is a root node
assert_eq!(dk.nodes.len(), 1);
assert!(dk.nodes[0].is_root());
// we have to have a node for right branch on each height (1, 01, 001, ... etc)
// plus an additional one for the two left-most leaves (epochs "0" and "1")
dk.try_update_to(Epoch::new(0), &params, &mut rng).unwrap();
assert_eq!(dk.nodes.len(), 33);
let expected_last = Tau::new(0);
// (and yes, I had to look up those names in a thesaurus)
let expected_penultimate = Tau::new(1);
// note that this value is 31bit long
let expected_antepenultimate = Tau(bitvec![u32, Msb0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1
]);
let mut nodes_iter = dk.nodes.iter().rev();
assert_eq!(expected_last, nodes_iter.next().unwrap().tau);
assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau);
assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau);
let mut epoch_zero_nodes = dk.nodes.clone();
// nodes for epoch1 should be identical for those for epoch0 minus the 00..00 leaf
dk.try_update_to(Epoch::new(1), &params, &mut rng).unwrap();
assert_eq!(dk.nodes.len(), 32);
epoch_zero_nodes.pop().unwrap();
assert_eq!(
epoch_zero_nodes
.iter()
.map(|node| node.tau.clone())
.collect::<Vec<_>>(),
dk.nodes
.iter()
.map(|node| node.tau.clone())
.collect::<Vec<_>>()
);
dk.try_update_to(Epoch::new(2), &params, &mut rng).unwrap();
dk.try_update_to(Epoch::new(3), &params, &mut rng).unwrap();
dk.try_update_to(Epoch::new(4), &params, &mut rng).unwrap();
let expected_last = Tau::new(4);
let expected_penultimate = Tau::new(5);
let expected_antepenultimate = Tau(bitvec![u32, Msb0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
]);
let expected_preantepenultimate = Tau(bitvec![u32, Msb0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
]);
assert_eq!(dk.nodes.len(), 32);
let mut nodes_iter = dk.nodes.iter().rev();
assert_eq!(expected_last, nodes_iter.next().unwrap().tau);
assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau);
assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau);
assert_eq!(expected_preantepenultimate, nodes_iter.next().unwrap().tau);
// the result should be the same of regardless if we update incrementally or go to the target immediately
let mut new_root = DecryptionKey {
nodes: root_node_copy,
};
new_root
.try_update_to(Epoch::new(4), &params, &mut rng)
.unwrap();
assert_eq!(
dk.nodes
.iter()
.map(|node| node.tau.clone())
.collect::<Vec<_>>(),
new_root
.nodes
.iter()
.map(|node| node.tau.clone())
.collect::<Vec<_>>()
);
// getting expected nodes for those epochs is non-trivial for test purposes, but the last node
// should ALWAYS be equal to the target epoch
dk.try_update_to(Epoch::new(42), &params, &mut rng).unwrap();
assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(42));
dk.try_update_to(Epoch::new(123456), &params, &mut rng)
.unwrap();
assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(123456));
dk.try_update_to(Epoch::new(3292547435), &params, &mut rng)
.unwrap();
assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(3292547435));
// trying to go to past epochs fails
assert!(dk
.try_update_to(Epoch::new(531), &params, &mut rng)
.is_err())
}
#[test]
fn updating_to_next_epoch() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, _) = keygen(&params, &mut rng);
// for root node current epoch is `None`
assert_eq!(None, dk.current_epoch(&params).unwrap());
// for root node it should result in epoch 0
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(Some(Epoch::new(0)), dk.current_epoch(&params).unwrap());
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(Some(Epoch::new(1)), dk.current_epoch(&params).unwrap());
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(Some(Epoch::new(2)), dk.current_epoch(&params).unwrap());
// if we start from some non-root epoch, it should result in l + 1
dk.try_update_to(Epoch::new(42), &params, &mut rng).unwrap();
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(Some(Epoch::new(43)), dk.current_epoch(&params).unwrap());
dk.try_update_to(Epoch::new(12345), &params, &mut rng)
.unwrap();
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(Some(Epoch::new(12346)), dk.current_epoch(&params).unwrap());
dk.try_update_to(Epoch::new(3292547435), &params, &mut rng)
.unwrap();
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(
Some(Epoch::new(3292547436)),
dk.current_epoch(&params).unwrap()
);
}
#[test]
fn public_key_with_proof_roundtrip() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (_, pk) = keygen(&params, &mut rng);
let bytes = pk.to_bytes();
let recovered = PublicKeyWithProof::try_from_bytes(&bytes).unwrap();
assert_eq!(pk, recovered)
}
#[test]
fn bte_node_roundtrip() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, _) = keygen(&params, &mut rng);
let root_node = dk.nodes[0].clone();
let bytes = root_node.to_bytes();
let recovered = Node::try_from_bytes(&bytes).unwrap();
assert_eq!(root_node, recovered);
dk.try_update_to(Epoch::new(3292547435), &params, &mut rng)
.unwrap();
for node in &dk.nodes {
let bytes = node.to_bytes();
let recovered = Node::try_from_bytes(&bytes).unwrap();
assert_eq!(node, &recovered);
}
}
#[test]
fn decryption_key_node_roundtrip() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, _) = keygen(&params, &mut rng);
let bytes = dk.to_bytes();
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
assert_eq!(dk, recovered);
dk.try_update_to(Epoch::new(0), &params, &mut rng).unwrap();
let bytes = dk.to_bytes();
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
assert_eq!(dk, recovered);
dk.try_update_to(Epoch::new(1), &params, &mut rng).unwrap();
let bytes = dk.to_bytes();
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
assert_eq!(dk, recovered);
dk.try_update_to(Epoch::new(42), &params, &mut rng).unwrap();
let bytes = dk.to_bytes();
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
assert_eq!(dk, recovered);
dk.try_update_to(Epoch::new(3292547435), &params, &mut rng)
.unwrap();
let bytes = dk.to_bytes();
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
assert_eq!(dk, recovered);
}
}
+463
View File
@@ -0,0 +1,463 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::DkgError;
use crate::utils::{hash_g2, RandomOracleBuilder};
use crate::{Chunk, Share};
use bitvec::field::BitField;
use bitvec::order::Msb0;
use bitvec::vec::BitVec;
use bitvec::view::BitView;
use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt};
use group::Curve;
use lazy_static::lazy_static;
use zeroize::Zeroize;
pub mod encryption;
pub mod keys;
pub mod proof_chunking;
pub mod proof_discrete_log;
pub mod proof_sharing;
pub use encryption::{decrypt_share, encrypt_shares, Ciphertexts};
pub use keys::{keygen, DecryptionKey, PublicKey, PublicKeyWithProof};
lazy_static! {
pub(crate) static ref PAIRING_BASE: Gt =
bls12_381::pairing(&G1Affine::generator(), &G2Affine::generator());
pub(crate) static ref G2_GENERATOR_PREPARED: G2Prepared =
G2Prepared::from(G2Affine::generator());
pub(crate) static ref DEFAULT_BSGS_TABLE: encryption::BabyStepGiantStepLookup =
encryption::BabyStepGiantStepLookup::default();
}
// Domain tries to follow guidelines specified by:
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
const SETUP_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381G2_XMD:SHA-256_SSWU_RO_SETUP";
// this particular domain is not for curve hashing, but might as well also follow the same naming pattern
const TREE_TAU_EXTENSION_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_SHA-256_TREE_EXTENSION";
const MAX_EPOCHS_EXP: usize = 32;
const HASH_SECURITY_PARAM: usize = 256;
// note: CHUNK_BYTES * NUM_CHUNKS must equal to SCALAR_SIZE
pub const CHUNK_BYTES: usize = 2;
pub const NUM_CHUNKS: usize = 16;
pub const SCALAR_SIZE: usize = 32;
/// In paper B; number of distinct chunks
pub const CHUNK_SIZE: usize = 1 << (CHUNK_BYTES << 3);
pub(crate) type EpochStore = u32;
#[derive(Clone, Debug, PartialEq, PartialOrd)]
// None empty bitvec implies this is a root node
pub(crate) struct Tau(BitVec<EpochStore, Msb0>);
impl Tau {
pub fn new_root() -> Self {
Tau(BitVec::new())
}
// TODO: perhaps this should be explicitly moved to some test module
#[cfg(test)]
pub(crate) fn new(epoch: EpochStore) -> Self {
Tau(epoch.view_bits().to_bitvec())
}
#[allow(unused)]
pub fn left_child(&self) -> Self {
let mut child = self.0.clone();
child.push(false);
Tau(child)
}
pub fn right_child(&self) -> Self {
let mut child = self.0.clone();
child.push(true);
Tau(child)
}
pub fn is_leaf(&self, params: &Params) -> bool {
self.height() == params.lambda_t
}
pub fn try_get_parent_at_height(&self, height: usize) -> Result<Self, DkgError> {
if height > self.0.len() {
return Err(DkgError::NotAValidParent);
}
Ok(Tau(self.0[..height].to_bitvec()))
}
// essentially is this tau prefixing the other
pub fn is_parent_of(&self, other: &Tau) -> bool {
if self.0.len() > other.0.len() {
return false;
}
for (i, b) in self.0.iter().enumerate() {
if b != other.0[i] {
return false;
}
}
true
}
pub fn lowest_valid_epoch_child(&self, params: &Params) -> Result<Epoch, DkgError> {
if self.0.len() > params.lambda_t {
// this node is already BELOW a valid leaf-epoch node. it can only happen
// if either some invariant was broken or additional data was pushed to `tau`
// in order compute some intermediate results, but in that case this method should have
// never been called anyway. tl;dr: if this is called, the underlying key is malformed
return Err(DkgError::NotAValidParent);
}
let mut child = self.0.clone();
for _ in 0..(params.lambda_t - self.0.len()) {
child.push(false)
}
// the unwrap here is fine as we ensure we have exactly `params.tree_height` bits here
// (we could just propagate the error instead of unwraping and putting it behind an `Ok` anyway
// but I'd prefer to just blow up since this would be a serious error
Ok(Epoch::try_from_tau(&Tau(child), params).unwrap())
}
pub fn height(&self) -> usize {
self.0.len()
}
fn extend(
&self,
rr: &[G1Projective; NUM_CHUNKS],
ss: &[G1Projective; NUM_CHUNKS],
cc: &[[G1Projective; NUM_CHUNKS]],
) -> Self {
let mut random_oracle_builder = RandomOracleBuilder::new(TREE_TAU_EXTENSION_DOMAIN);
random_oracle_builder.update_with_g1_elements(rr.iter());
random_oracle_builder.update_with_g1_elements(ss.iter());
for ciphertext_chunks in cc {
random_oracle_builder.update_with_g1_elements(ciphertext_chunks.iter());
}
let tau_mem = self.0.as_raw_slice();
assert_eq!(tau_mem.len(), 1, "tau length invariant was broken");
random_oracle_builder.update(&tau_mem[0].to_be_bytes());
let oracle_output = random_oracle_builder.finalize();
debug_assert_eq!(oracle_output.len() * 8, HASH_SECURITY_PARAM);
let mut extended_tau = self.clone();
for byte in oracle_output {
extended_tau
.0
.extend_from_bitslice(byte.view_bits::<Msb0>())
}
extended_tau
}
// considers all lambda_t + lambda_h bits
fn evaluate_f(&self, params: &Params) -> G2Projective {
self.0
.iter()
.by_vals()
.zip(params.fs.iter().chain(params.fh.iter()))
.filter(|(i, _)| *i)
.map(|(_, f_i)| f_i)
.fold(params.f0, |acc, f_i| acc + f_i)
}
// only considers up to lambda_t bits
fn evaluate_partial_f(&self, params: &Params) -> G2Projective {
self.0
.iter()
.by_vals()
.zip(params.fs.iter())
.filter(|(i, _)| *i)
.map(|(_, f_i)| f_i)
.fold(params.f0, |acc, f_i| acc + f_i)
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let len_bytes = (self.0.len() as u32).to_be_bytes();
len_bytes
.into_iter()
.chain(self.0.chunks(8).map(BitField::load_be))
.collect()
}
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<Self, DkgError> {
if b.len() < 4 {
return Err(DkgError::new_deserialization_failure(
"Tau",
"insufficient number of bytes provided",
));
}
let tau_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize;
// maximum theoretical length
if tau_len > MAX_EPOCHS_EXP + HASH_SECURITY_PARAM {
return Err(DkgError::new_deserialization_failure(
"Tau",
format!(
"malformed length {} is greater than maximum {}",
tau_len,
MAX_EPOCHS_EXP + HASH_SECURITY_PARAM
),
));
}
if tau_len == 0 {
if b.len() != 4 {
Err(DkgError::new_deserialization_failure(
"Tau",
"malformed bytes",
))
} else {
Ok(Tau::new_root())
}
} else if b.len() == 4 {
Err(DkgError::new_deserialization_failure(
"Tau",
"insufficient number of bytes provided",
))
} else {
let mut inner = BitVec::repeat(false, tau_len);
for (slot, &byte) in inner.chunks_mut(8).zip(b[4..].iter()) {
slot.store_be(byte);
}
Ok(Tau(inner))
}
}
}
impl Zeroize for Tau {
fn zeroize(&mut self) {
for v in self.0.as_raw_mut_slice() {
v.zeroize()
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Epoch(EpochStore);
impl Epoch {
pub fn new(value: EpochStore) -> Self {
Epoch(value)
}
pub(crate) fn as_tau(&self) -> Tau {
(*self).into()
}
pub(crate) fn as_extended_tau(
&self,
rr: &[G1Projective; NUM_CHUNKS],
ss: &[G1Projective; NUM_CHUNKS],
cc: &[[G1Projective; NUM_CHUNKS]],
) -> Tau {
self.as_tau().extend(rr, ss, cc)
}
pub(crate) fn try_from_tau(tau: &Tau, params: &Params) -> Result<Self, DkgError> {
if !tau.is_leaf(params) {
Err(DkgError::MalformedEpoch)
} else {
Ok(Epoch(tau.0.load_be()))
}
}
}
impl From<Epoch> for Tau {
fn from(epoch: Epoch) -> Self {
Tau(epoch.0.view_bits().to_bitvec())
}
}
impl From<EpochStore> for Epoch {
fn from(epoch: EpochStore) -> Self {
Epoch(epoch)
}
}
pub struct Params {
/// Maximum size of an epoch, in bits.
pub lambda_t: usize,
/// Security parameter of our $H_{\Lamda_H}$ hash function
pub lambda_h: usize,
// keeping f0 separate from the rest of the curve points makes it easier to work with tau
f0: G2Projective,
fs: Vec<G2Projective>, // f_1, f_2, .... f_{lambda_t} in the paper
fh: Vec<G2Projective>, // f_{lambda_t+1}, f_{lambda_t+1}, .... f_{lambda_t+lambda_h} in the paper
h: G2Projective,
/// Precomputed `h` used for the miller loop
_h_prepared: G2Prepared,
}
pub fn setup() -> Params {
let f0 = hash_g2(b"f0", SETUP_DOMAIN);
let fs = (1..=MAX_EPOCHS_EXP)
.map(|i| hash_g2(format!("f{}", i), SETUP_DOMAIN))
.collect();
let fh = (0..HASH_SECURITY_PARAM)
.map(|i| hash_g2(format!("fh{}", i), SETUP_DOMAIN))
.collect();
let h = hash_g2(b"h", SETUP_DOMAIN);
Params {
lambda_t: MAX_EPOCHS_EXP,
lambda_h: HASH_SECURITY_PARAM,
f0,
fs,
fh,
h,
_h_prepared: G2Prepared::from(h.to_affine()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use bitvec::bitvec;
use bitvec::order::Msb0;
#[test]
fn creating_tau_from_epoch() {
assert!(Tau::new_root().0.is_empty());
let zero = Tau::new(0);
assert!(zero.0.iter().by_vals().all(|b| !b));
let one = Tau::new(1);
let mut iter = one.0.iter().by_vals();
// first 31 bits are 0, the last one is 1
for _ in 0..31 {
assert!(!iter.next().unwrap())
}
assert!(iter.next().unwrap());
// 101010 in binary
let forty_two = Tau::new(42);
// first 26 bits are not set
let mut iter = forty_two.0.iter().by_vals();
for _ in 0..26 {
assert!(!iter.next().unwrap())
}
assert!(iter.next().unwrap());
assert!(!iter.next().unwrap());
assert!(iter.next().unwrap());
assert!(!iter.next().unwrap());
assert!(iter.next().unwrap());
assert!(!iter.next().unwrap());
// value that requires an actual u32 (i.e. takes 4 bytes to represent)
// 11000100_01000000_01001001_01101011 in binary
let big_val = Tau::new(3292547435);
let expected = bitvec![u32, Msb0;
1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
0, 1, 1
];
assert_eq!(expected, big_val.0)
}
#[test]
fn getting_parent_at_height() {
let tau = Tau(bitvec![u32, Msb0; 1,0,1,1,0,0,1]);
let expected_0 = Tau(BitVec::new());
let expected_1 = Tau(bitvec![u32, Msb0; 1]);
let expected_5 = Tau(bitvec![u32, Msb0; 1,0,1,1,0]);
assert_eq!(expected_0, tau.try_get_parent_at_height(0).unwrap());
assert_eq!(expected_1, tau.try_get_parent_at_height(1).unwrap());
assert_eq!(expected_5, tau.try_get_parent_at_height(5).unwrap());
assert_eq!(tau, tau.try_get_parent_at_height(7).unwrap());
assert!(tau.try_get_parent_at_height(8).is_err())
}
#[test]
fn converting_tau_to_epoch() {
let params = setup();
let tau0: Tau = Epoch::new(0).into();
let tau1: Tau = Epoch::new(1).into();
let tau42: Tau = Epoch::new(42).into();
let tau_big: Tau = Epoch::new(3292547435).into();
assert_eq!(Epoch::new(0), Epoch::try_from_tau(&tau0, &params).unwrap());
assert_eq!(Epoch::new(1), Epoch::try_from_tau(&tau1, &params).unwrap());
assert_eq!(
Epoch::new(42),
Epoch::try_from_tau(&tau42, &params).unwrap()
);
assert_eq!(
Epoch::new(3292547435),
Epoch::try_from_tau(&tau_big, &params).unwrap()
);
assert!(Epoch::try_from_tau(&Tau(BitVec::new()), &params).is_err());
assert!(Epoch::try_from_tau(&Tau(bitvec![u32, Msb0; 1,0,1,1,0]), &params).is_err());
let _31bit_tau = Tau(bitvec![u32, Msb0;
1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
0, 1
]);
assert!(Epoch::try_from_tau(&_31bit_tau, &params).is_err());
let _33bit_tau = Tau(bitvec![u32, Msb0;
1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
0, 1, 1, 0
]);
assert!(Epoch::try_from_tau(&_33bit_tau, &params).is_err());
}
#[test]
fn tau_roundtrip() {
let good_taus = vec![
Tau::new_root(),
Tau::new(0),
Tau::new(1),
Tau::new(2),
Tau::new(42),
Tau::new(123456),
Tau::new(3292547435),
Tau::new(u32::MAX),
];
for tau in good_taus {
let bytes = tau.to_bytes();
let recovered = Tau::try_from_bytes(&bytes).unwrap();
assert_eq!(tau, recovered);
}
// more valid variants
let mut another_tau = Tau::new(u32::MAX);
another_tau.0.push(true);
another_tau.0.push(false);
another_tau.0.push(true);
let bytes = another_tau.to_bytes();
let recovered = Tau::try_from_bytes(&bytes).unwrap();
assert_eq!(another_tau, recovered);
// ensure there are no panics
let big_length_bytes = [255, 255, 255, 255, 42];
assert!(Tau::try_from_bytes(&big_length_bytes).is_err());
assert!(Tau::try_from_bytes(&[]).is_err());
assert!(Tau::try_from_bytes(&[1, 1, 1, 1]).is_err());
assert!(Tau::try_from_bytes(&[0, 0, 0, 1]).is_err());
assert!(Tau::try_from_bytes(&[1, 0, 0, 0]).is_err());
assert!(Tau::try_from_bytes(&[1, 0, 0]).is_err());
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,94 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::utils::hash_to_scalar;
use bls12_381::{G1Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand_core::RngCore;
use zeroize::Zeroize;
// Domain tries to follow guidelines specified by:
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
const DISCRETE_LOG_DOMAIN: &[u8] =
b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_DISCRETE_LOG";
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ProofOfDiscreteLog {
pub(crate) rand_commitment: G1Projective,
pub(crate) response: Scalar,
}
impl ProofOfDiscreteLog {
pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self {
let mut rand_x = Scalar::random(&mut rng);
let rand_commitment = G1Projective::generator() * rand_x;
let challenge = Self::compute_challenge(public, &rand_commitment);
let response = rand_x + challenge * witness;
rand_x.zeroize();
ProofOfDiscreteLog {
rand_commitment,
response,
}
}
// note: we don't have to explicitly check whether points are on correct curves / fields
// as if they weren't, they'd fail to get deserialized
pub fn verify(&self, public: &G1Projective) -> bool {
let challenge = Self::compute_challenge(public, &self.rand_commitment);
// y^c • a == g1^rand_x
public * challenge + self.rand_commitment == G1Projective::generator() * self.response
}
pub(crate) fn compute_challenge(public: &G1Projective, rand_commit: &G1Projective) -> Scalar {
let public_bytes = public.to_bytes();
let rand_commit_bytes = rand_commit.to_bytes();
let mut bytes = Vec::with_capacity(96);
bytes.extend_from_slice(public_bytes.as_ref());
bytes.extend_from_slice(rand_commit_bytes.as_ref());
hash_to_scalar(bytes, DISCRETE_LOG_DOMAIN)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand_core::SeedableRng;
#[test]
fn should_verify_a_valid_proof() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let witness = Scalar::random(&mut rng);
let public = G1Projective::generator() * witness;
let proof = ProofOfDiscreteLog::construct(&mut rng, &public, &witness);
assert!(proof.verify(&public))
}
#[test]
fn should_fail_on_invalid_proof() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let witness = Scalar::random(&mut rng);
let public = G1Projective::generator() * witness;
let other_witness = Scalar::random(&mut rng);
let other_public = G1Projective::generator() * other_witness;
let proof = ProofOfDiscreteLog::construct(&mut rng, &public, &witness);
let other_proof = ProofOfDiscreteLog::construct(&mut rng, &other_public, &other_witness);
assert!(!proof.verify(&other_public));
assert!(!other_proof.verify(&public));
}
}
+615
View File
@@ -0,0 +1,615 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::PublicKey;
use crate::error::DkgError;
use crate::interpolation::polynomial::PublicCoefficients;
use crate::utils::{deserialize_g1, deserialize_g2, deserialize_scalar, hash_to_scalar};
use crate::{NodeIndex, Share};
use bls12_381::{G1Projective, G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand_core::RngCore;
use std::collections::BTreeMap;
// Domain tries to follow guidelines specified by:
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
const INSTANCE_DOMAIN: &[u8] =
b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_SECRET_SHARING_INSTANCE";
const CHALLENGE_DOMAIN: &[u8] =
b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_SECRET_SHARING_CHALLENGE";
#[cfg_attr(test, derive(Clone))]
pub struct Instance<'a> {
public_keys: &'a BTreeMap<NodeIndex, PublicKey>,
public_coefficients: &'a PublicCoefficients,
combined_randomizer: &'a G1Projective,
combined_ciphertexts: &'a [G1Projective],
}
impl<'a> Instance<'a> {
pub fn new(
public_keys: &'a BTreeMap<NodeIndex, PublicKey>,
public_coefficients: &'a PublicCoefficients,
combined_randomizer: &'a G1Projective,
combined_ciphertexts: &'a [G1Projective],
) -> Instance<'a> {
Instance {
public_keys,
public_coefficients,
combined_randomizer,
combined_ciphertexts,
}
}
fn hash_to_scalar(&self) -> Scalar {
let g1s = self.public_keys.len() + 1 + self.combined_ciphertexts.len();
let g2s = self.public_coefficients.size();
let mut bytes = Vec::with_capacity(g1s * 48 + g2s * 96);
for pk in self.public_keys.values() {
bytes.extend_from_slice(pk.0.to_bytes().as_ref())
}
for coeff in self.public_coefficients.inner() {
bytes.extend_from_slice(coeff.to_bytes().as_ref())
}
bytes.extend_from_slice(self.combined_randomizer.to_bytes().as_ref());
for ciphertext in self.combined_ciphertexts {
bytes.extend_from_slice(ciphertext.to_bytes().as_ref())
}
hash_to_scalar(&bytes, INSTANCE_DOMAIN)
}
fn validate(&self) -> bool {
if self.public_keys.is_empty() || self.public_coefficients.is_empty() {
return false;
}
if self.public_keys.len() != self.combined_ciphertexts.len() {
return false;
}
true
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub struct ProofOfSecretSharing {
ff: G1Projective,
aa: G2Projective,
yy: G1Projective,
response_r: Scalar,
response_alpha: Scalar,
}
impl ProofOfSecretSharing {
pub fn construct(
mut rng: impl RngCore,
instance: Instance,
witness_r: &Scalar,
witnesses_s: &[Share],
) -> Result<Self, DkgError> {
if !instance.validate() {
return Err(DkgError::MalformedProofOfSharingInstance);
}
let g1 = G1Projective::generator();
let g2 = G2Projective::generator();
let x = instance.hash_to_scalar();
// alpha, rho ← random_scalars
let alpha = Scalar::random(&mut rng);
let rho = Scalar::random(&mut rng);
// F = g1^rho
let ff = g1 * rho;
// A = g2^alpha
let aa = g2 * alpha;
// Y = (y_1^{x^1} • ... y_n^{x^n})^rho • g1^alpha
// produce intermediate product (y_1^{x^1} • ... y_n^{x^n})
let product =
instance
.public_keys
.values()
.rev()
.fold(G1Projective::identity(), |mut acc, pk| {
acc += pk.0;
acc *= x;
acc
});
let yy = product * rho + g1 * alpha;
let challenge = Self::compute_challenge(&x, &ff, &aa, &yy);
// response_r = r • challenge + rho
let response_r = witness_r * challenge + rho;
// response_alpha = (share_1 • x^1 + ... share_n • x^n) • challenge + alpha
// produce intermediate sum (share_1 • x^1 + ... share_n • x^n)
let sum = witnesses_s
.iter()
.rev()
.fold(Scalar::zero(), |mut acc, witness| {
acc += witness.inner();
acc *= x;
acc
});
let response_alpha = sum * challenge + alpha;
Ok(ProofOfSecretSharing {
ff,
aa,
yy,
response_r,
response_alpha,
})
}
pub fn verify(&self, instance: Instance) -> bool {
if !instance.validate() {
return false;
}
let g1 = G1Projective::generator();
let g2 = G2Projective::generator();
let x = instance.hash_to_scalar();
let challenge = Self::compute_challenge(&x, &self.ff, &self.aa, &self.yy);
// check if R^challenge * F == g1^response_r
if instance.combined_randomizer * challenge + self.ff != g1 * self.response_r {
return false;
}
// check if
// (A_0 ^ (id1^0 • x^1 + ... idn^0 • x^n) • ... A_{t-1} ^ (id1^{t-1} • x^{t-1} + ... idn^{t-1} • x^n))^challenge * A
// ==
// g2^response_alpha
let product = instance
.public_coefficients
.inner()
.iter()
.enumerate()
.fold(G2Projective::identity(), |mut acc, (k, coeff)| {
// intermediate (id1^k • x^1 + ... + idn^k • x^n) sum
let sum: Scalar = instance
.public_keys
.keys()
.enumerate()
.map(|(i, node_id)| {
let id_scalar = Scalar::from(*node_id);
id_scalar.pow(&[k as u64, 0, 0, 0]) * x.pow(&[(i + 1) as u64, 0, 0, 0])
})
.sum();
acc += coeff * sum;
acc
});
if product * challenge + self.aa != g2 * self.response_alpha {
return false;
}
// check if
// (ciphertext_1 ^ (x^1) • ... ciphertext_n ^ (x^n)) ^ challenge • Y
// ==
// (pk_1 ^ (x^1) • ... pk_n ^ (x^n)) ^ response_r • g1^response_alpha
let product_1 = instance.combined_ciphertexts.iter().rev().fold(
G1Projective::identity(),
|mut acc, ciphertext| {
acc += ciphertext;
acc *= x;
acc
},
);
let product_2 =
instance
.public_keys
.values()
.rev()
.fold(G1Projective::identity(), |mut acc, pk| {
acc += pk.0;
acc *= x;
acc
});
if product_1 * challenge + self.yy != product_2 * self.response_r + g1 * self.response_alpha
{
return false;
}
true
}
pub(crate) fn compute_challenge(
commitment: &Scalar,
blinder_g1: &G1Projective,
blinder_g2: &G2Projective,
blinded_instance: &G1Projective,
) -> Scalar {
let mut bytes = Vec::with_capacity(224);
bytes.extend_from_slice(commitment.to_bytes().as_ref());
bytes.extend_from_slice(blinder_g1.to_bytes().as_ref());
bytes.extend_from_slice(blinder_g2.to_bytes().as_ref());
bytes.extend_from_slice(blinded_instance.to_bytes().as_ref());
hash_to_scalar(&bytes, CHALLENGE_DOMAIN)
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
// we have 2 G1 elements, single G2 element and 2 scalars
let mut bytes = Vec::with_capacity(2 * 48 + 96 + 2 * 32);
bytes.extend_from_slice(self.ff.to_bytes().as_ref());
bytes.extend_from_slice(self.aa.to_bytes().as_ref());
bytes.extend_from_slice(self.yy.to_bytes().as_ref());
bytes.extend_from_slice(self.response_r.to_bytes().as_ref());
bytes.extend_from_slice(self.response_alpha.to_bytes().as_ref());
bytes
}
pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
if bytes.len() != 2 * 48 + 96 + 2 * 32 {
return Err(DkgError::new_deserialization_failure(
"ProofOfSecretSharing",
"invalid number of bytes provided",
));
}
let mut i = 0;
let f = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure("ProofOfSecretSharing.f", "invalid curve point")
})?;
i += 48;
let a = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure("ProofOfSecretSharing.a", "invalid curve point")
})?;
i += 96;
let y = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure("ProofOfSecretSharing.y", "invalid curve point")
})?;
i += 48;
let response_r = deserialize_scalar(&bytes[i..i + 32]).ok_or_else(|| {
DkgError::new_deserialization_failure(
"ProofOfSecretSharing.response_r",
"invalid scalar",
)
})?;
i += 32;
let response_alpha = deserialize_scalar(&bytes[i..]).ok_or_else(|| {
DkgError::new_deserialization_failure(
"ProofOfSecretSharing.response_alpha",
"invalid scalar",
)
})?;
Ok(ProofOfSecretSharing {
ff: f,
aa: a,
yy: y,
response_r,
response_alpha,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interpolation::polynomial::Polynomial;
use group::Group;
use rand_core::SeedableRng;
const NODES: u64 = 50;
const THRESHOLD: u64 = 40;
fn setup(
mut rng: impl RngCore,
) -> (
BTreeMap<NodeIndex, PublicKey>,
PublicCoefficients,
G1Projective,
Vec<G1Projective>,
Scalar,
Vec<Share>,
) {
let g1 = G1Projective::generator();
let mut pks = BTreeMap::new();
let polynomial = Polynomial::new_random(&mut rng, THRESHOLD - 1);
let public_coefficients = polynomial.public_coefficients();
let mut shares: Vec<Share> = Vec::new();
let mut node_indices = (0..NODES).map(|_| rng.next_u64()).collect::<Vec<_>>();
node_indices.sort_unstable();
for node_index in node_indices {
let share = polynomial.evaluate_at(&Scalar::from(node_index));
shares.push(share.into());
pks.insert(node_index, PublicKey(g1 * Scalar::random(&mut rng)));
}
let r = Scalar::random(&mut rng);
let rr = g1 * r;
let ciphertexts = pks
.values()
.zip(&shares)
.map(|(pk, share)| pk.0 * r + g1 * share.inner())
.collect();
(pks, public_coefficients, rr, ciphertexts, r, shares)
}
#[test]
fn should_fail_to_create_proof_with_invalid_instance() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let g1 = G1Projective::generator();
let mut pks = BTreeMap::new();
let polynomial = Polynomial::new_random(&mut rng, THRESHOLD - 1);
let public_coefficients = polynomial.public_coefficients();
let mut shares: Vec<Share> = Vec::new();
for _ in 0..NODES {
let node_index = rng.next_u64();
let share = polynomial.evaluate_at(&Scalar::from(node_index));
shares.push(share.into());
pks.insert(node_index, PublicKey(g1 * Scalar::random(&mut rng)));
}
let r = Scalar::random(&mut rng);
let rr = g1 * r;
let mut shares = Vec::new();
for node_id in 1..NODES + 1 {
let share = polynomial.evaluate_at(&Scalar::from(node_id));
shares.push(share);
}
let ciphertexts = pks
.values()
.zip(&shares)
.map(|(pk, share)| pk.0 * r + g1 * share)
.collect::<Vec<_>>();
// no public keys
let bad_instance1 = Instance {
public_keys: &BTreeMap::new(),
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(!bad_instance1.validate());
// no public coefficients
let bad_instance2 = Instance {
public_keys: &pks,
public_coefficients: &PublicCoefficients {
coefficients: Vec::new(),
},
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(!bad_instance2.validate());
// no ciphertexts
let bad_instance3 = Instance {
public_keys: &pks,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &[],
};
assert!(!bad_instance3.validate());
// public_keys.len() != combined_ciphertexts.len()
let bad_ciphertexts = ciphertexts.iter().skip(1).cloned().collect::<Vec<_>>();
let bad_instance4 = Instance {
public_keys: &pks,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &bad_ciphertexts,
};
assert!(!bad_instance4.validate());
// changed index of one of the keys
let mut bad_pks = pks.clone();
let first_id = bad_pks.keys().copied().take(1).collect::<Vec<_>>();
let first_val = bad_pks.remove(&first_id[0]).unwrap();
bad_pks.insert(rng.next_u64(), first_val);
let bad_instance5 = Instance {
public_keys: &bad_pks,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &bad_ciphertexts,
};
assert!(!bad_instance5.validate());
let good_instance = Instance {
public_keys: &pks,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(good_instance.validate())
}
#[test]
fn should_verify_a_valid_proof() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng);
let instance = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
let sharing_proof =
ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap();
assert!(sharing_proof.verify(instance))
}
#[test]
fn should_fail_to_verify_proof_with_invalid_instance() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng);
let instance = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
let sharing_proof =
ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap();
// no public keys
let bad_instance1 = Instance {
public_keys: &BTreeMap::new(),
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(!sharing_proof.verify(bad_instance1));
// no public coefficients
let bad_instance2 = Instance {
public_keys: &public_keys,
public_coefficients: &PublicCoefficients {
coefficients: Vec::new(),
},
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(!sharing_proof.verify(bad_instance2));
// no ciphertexts
let bad_instance3 = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &[],
};
assert!(!sharing_proof.verify(bad_instance3));
// public_keys.len() != combined_ciphertexts.len()
let bad_ciphertexts = ciphertexts.iter().skip(1).cloned().collect::<Vec<_>>();
let bad_instance4 = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &bad_ciphertexts,
};
assert!(!sharing_proof.verify(bad_instance4));
}
#[test]
fn should_fail_to_verify_proof_with_wrong_instance() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng);
let instance = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
let sharing_proof =
ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap();
let (public_keys, public_coefficients, rr, ciphertexts, _, _) = setup(&mut rng);
let bad_instance = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(!sharing_proof.verify(bad_instance));
}
#[test]
fn should_fail_to_verify_invalid_proof() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng);
let instance = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
let good_proof =
ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap();
let mut bad_proof = good_proof.clone();
bad_proof.ff = G1Projective::generator();
assert!(!bad_proof.verify(instance.clone()));
let mut bad_proof = good_proof.clone();
bad_proof.aa = G2Projective::generator();
assert!(!bad_proof.verify(instance.clone()));
let mut bad_proof = good_proof.clone();
bad_proof.yy = G1Projective::generator();
assert!(!bad_proof.verify(instance.clone()));
let mut bad_proof = good_proof.clone();
bad_proof.response_r = Scalar::from(42);
assert!(!bad_proof.verify(instance.clone()));
let mut bad_proof = good_proof;
bad_proof.response_alpha = Scalar::from(42);
assert!(!bad_proof.verify(instance));
}
#[test]
fn proof_of_secret_sharing_roundtrip() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let proof_fixture = ProofOfSecretSharing {
ff: G1Projective::random(&mut rng),
aa: G2Projective::random(&mut rng),
yy: G1Projective::random(&mut rng),
response_r: Scalar::random(&mut rng),
response_alpha: Scalar::random(&mut rng),
};
let bytes = proof_fixture.to_bytes();
let recovered = ProofOfSecretSharing::try_from_bytes(&bytes).unwrap();
assert_eq!(proof_fixture, recovered);
assert!(ProofOfSecretSharing::try_from_bytes(&bytes[1..]).is_err())
}
}
+500
View File
@@ -0,0 +1,500 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::proof_chunking::ProofOfChunking;
use crate::bte::proof_sharing::ProofOfSecretSharing;
use crate::bte::{
encrypt_shares, proof_chunking, proof_sharing, Ciphertexts, Epoch, Params, PublicKey,
};
use crate::error::DkgError;
use crate::interpolation::polynomial::{Polynomial, PublicCoefficients};
use crate::interpolation::{
perform_lagrangian_interpolation_at_origin, perform_lagrangian_interpolation_at_x,
};
use crate::{NodeIndex, Share, Threshold};
use bls12_381::{G2Projective, Scalar};
use rand_core::RngCore;
use std::collections::BTreeMap;
use zeroize::Zeroize;
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Dealing {
pub public_coefficients: PublicCoefficients,
pub ciphertexts: Ciphertexts,
pub proof_of_chunking: ProofOfChunking,
pub proof_of_sharing: ProofOfSecretSharing,
}
impl Dealing {
// I'm not a big fan of this function signature, but I'm not clear on how to improve it while
// allowing the dealer to skip decryption of its own share if it was also one of the receivers
pub fn create(
mut rng: impl RngCore,
params: &Params,
dealer_index: NodeIndex,
threshold: Threshold,
epoch: Epoch,
// BTreeMap ensures the keys are sorted by their indices
receivers: &BTreeMap<NodeIndex, PublicKey>,
prior_resharing_secret: Option<Scalar>,
) -> (Self, Option<Share>) {
assert!(threshold > 0);
let mut polynomial = Polynomial::new_random(&mut rng, threshold - 1);
if let Some(prior_secret) = prior_resharing_secret {
polynomial.set_constant_coefficient(prior_secret)
}
let mut shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
let (ciphertexts, hazmat) =
encrypt_shares(&remote_share_key_pairs, epoch, params, &mut rng);
// create proofs of knowledge
let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts);
let proof_of_chunking =
ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares)
.expect("failed to construct proof of chunking");
let combined_ciphertexts = ciphertexts.combine_ciphertexts();
let mut combined_r = hazmat.combine_rs();
let combined_rr = ciphertexts.combine_rs();
let public_coefficients = polynomial.public_coefficients();
let sharing_instance = proof_sharing::Instance::new(
receivers,
&public_coefficients,
&combined_rr,
&combined_ciphertexts,
);
let proof_of_sharing =
ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares)
.expect("failed to construct proof of secret sharing");
combined_r.zeroize();
let dealing = Dealing {
public_coefficients,
ciphertexts,
proof_of_chunking,
proof_of_sharing,
};
let dealers_key_index = receivers
.keys()
.position(|node_index| node_index == &dealer_index);
if let Some(dealer_key_index) = dealers_key_index {
let dealers_share = shares.remove(dealer_key_index);
shares.zeroize();
(dealing, Some(dealers_share))
} else {
(dealing, None)
}
}
// rather than returning a bool for whether the dealing is valid or not, a Result is returned
// instead so that we would have more information regarding a possible failure cause
pub fn verify(
&self,
params: &Params,
epoch: Epoch,
threshold: Threshold,
receivers: &BTreeMap<NodeIndex, PublicKey>,
prior_resharing_public: Option<G2Projective>,
) -> Result<(), DkgError> {
if threshold == 0 || threshold as usize > receivers.len() {
return Err(DkgError::InvalidThreshold {
actual: threshold as usize,
participating: receivers.len(),
});
}
if self.ciphertexts.ciphertext_chunks.len() != receivers.len() {
return Err(DkgError::WrongCiphertextSize {
actual: self.ciphertexts.ciphertext_chunks.len(),
expected: receivers.len(),
});
}
if self.public_coefficients.size() != threshold as usize {
return Err(DkgError::WrongPublicCoefficientsSize {
actual: self.public_coefficients.size(),
expected: threshold as usize,
});
}
if !self.ciphertexts.verify_integrity(params, epoch) {
return Err(DkgError::FailedCiphertextIntegrityCheck);
}
// TODO: perhaps change the underlying arguments in proofs of knowledge to avoid this allocation?
let sorted_receivers = receivers.values().copied().collect::<Vec<_>>();
let chunking_instance = proof_chunking::Instance::new(&sorted_receivers, &self.ciphertexts);
if !self.proof_of_chunking.verify(chunking_instance) {
return Err(DkgError::InvalidProofOfChunking);
}
let combined_randomizer = &self.ciphertexts.combine_rs();
let combined_ciphertexts = &self.ciphertexts.combine_ciphertexts();
let sharing_instance = proof_sharing::Instance::new(
receivers,
&self.public_coefficients,
combined_randomizer,
combined_ciphertexts,
);
if !self.proof_of_sharing.verify(sharing_instance) {
return Err(DkgError::InvalidProofOfSharing);
}
if let Some(prior_public) = prior_resharing_public {
let dealt_public = &self.public_coefficients[0];
if dealt_public != &prior_public {
return Err(DkgError::InvalidResharing);
}
}
Ok(())
}
// coeff_len || coeff || cc_len || cc || pi_c_len || pi_c || pi_s_len || pi_s
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
let mut coefficients_bytes = self.public_coefficients.to_bytes();
bytes.extend_from_slice(&(coefficients_bytes.len() as u32).to_be_bytes());
bytes.append(&mut coefficients_bytes);
let mut ciphertexts_bytes = self.ciphertexts.to_bytes();
bytes.extend_from_slice(&(ciphertexts_bytes.len() as u32).to_be_bytes());
bytes.append(&mut ciphertexts_bytes);
let mut proof_sharing_bytes = self.proof_of_sharing.to_bytes();
bytes.extend_from_slice(&(proof_sharing_bytes.len() as u32).to_be_bytes());
bytes.append(&mut proof_sharing_bytes);
let mut proof_chunking_bytes = self.proof_of_chunking.to_bytes();
bytes.extend_from_slice(&(proof_chunking_bytes.len() as u32).to_be_bytes());
bytes.append(&mut proof_chunking_bytes);
bytes
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
// can we read the length of serialized public coefficients?
if bytes.len() < 4 {
return Err(DkgError::new_deserialization_failure(
"Dealing",
"insufficient number of bytes provided",
));
}
let mut i = 0;
let coefficients_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
let public_coefficients =
PublicCoefficients::try_from_bytes(&bytes[i..i + coefficients_bytes_len])?;
i += coefficients_bytes_len;
let ciphertexts_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
let ciphertexts = Ciphertexts::try_from_bytes(&bytes[i..i + ciphertexts_bytes_len])?;
i += ciphertexts_bytes_len;
let proof_of_sharing_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
let proof_of_sharing =
ProofOfSecretSharing::try_from_bytes(&bytes[i..i + proof_of_sharing_bytes_len])?;
i += proof_of_sharing_bytes_len;
let proof_of_chunking_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
if bytes[i..].len() != proof_of_chunking_bytes_len {
return Err(DkgError::new_deserialization_failure(
"Dealing",
"invalid number of bytes provided",
));
}
let proof_of_chunking = ProofOfChunking::try_from_bytes(&bytes[i..])?;
Ok(Dealing {
public_coefficients,
ciphertexts,
proof_of_chunking,
proof_of_sharing,
})
}
}
// this assumes all dealings have been verified
pub fn try_recover_verification_keys(
dealings: &[Dealing],
threshold: Threshold,
receivers: &BTreeMap<NodeIndex, PublicKey>,
) -> Result<(G2Projective, Vec<G2Projective>), DkgError> {
if dealings.is_empty() {
return Err(DkgError::NoDealingsAvailable);
}
let threshold_usize = threshold as usize;
if !dealings
.iter()
.all(|dealing| dealing.public_coefficients.size() == threshold_usize)
{
return Err(DkgError::MismatchedDealings);
}
// currently we expect every dealer to also be a receiver. This restriction might be relaxed in the future
if dealings.len() != receivers.len() {
return Err(DkgError::MismatchedDealings);
}
let indices = receivers.keys().collect::<Vec<_>>();
// Compute A0, ..., A_{t-1}
let mut interpolated_coefficients = Vec::with_capacity(threshold_usize);
for k in 0..threshold_usize {
let mut samples = Vec::with_capacity(indices.len());
for (j, dealing) in dealings.iter().enumerate() {
samples.push((
Scalar::from(*indices[j]),
*dealing.public_coefficients.nth(k),
))
}
let interpolated = perform_lagrangian_interpolation_at_origin(&samples)?;
interpolated_coefficients.push(interpolated);
}
let master_verification_key = interpolated_coefficients[0];
let interpolated_coefficients = PublicCoefficients {
coefficients: interpolated_coefficients,
};
// shvk_j = A0^{j^0} * A1^{j^1} * ... * A_{t-1}^{j^{t-1}}
let verification_key_shares = receivers
.keys()
.map(|index| interpolated_coefficients.evaluate_at(&Scalar::from(*index)))
.collect();
Ok((master_verification_key, verification_key_shares))
}
pub fn verify_verification_keys(
master_key: &G2Projective,
shares: &[G2Projective],
receivers: &BTreeMap<NodeIndex, PublicKey>,
threshold: Threshold,
) -> Result<(), DkgError> {
if shares.len() != receivers.len() {
return Err(DkgError::NotEnoughReceiversProvided);
}
if threshold as usize > receivers.len() {
return Err(DkgError::InvalidThreshold {
actual: threshold as usize,
participating: receivers.len(),
});
}
let indices = receivers.keys().copied().collect::<Vec<_>>();
let indices_with_origin = std::iter::once(&0)
.chain(receivers.keys())
.collect::<Vec<_>>();
let all_shares = std::iter::once(master_key)
.chain(shares.iter())
.collect::<Vec<_>>();
for (i, share) in shares.iter().enumerate() {
let samples = indices_with_origin
.iter()
.zip(all_shares.iter())
.map(|(&node_index, &share)| (Scalar::from(*node_index), *share))
.take(threshold as usize)
.collect::<Vec<_>>();
let interpolated =
perform_lagrangian_interpolation_at_x(&Scalar::from(indices[i]), &samples)?;
if share != &interpolated {
return Err(DkgError::MismatchedVerificationKey);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bte::{decrypt_share, keygen, setup};
use crate::combine_shares;
use rand_core::SeedableRng;
#[test]
fn recovering_partial_verification_keys() {
// START OF SETUP
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 2;
let node_indices = vec![1, 4, 7];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
let dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
epoch,
&receivers,
None,
)
.0
})
.collect::<Vec<_>>();
let mut derived_secrets = Vec::new();
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let shares = dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap())
.collect();
derived_secrets.push(
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap(),
)
}
let master_secret = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[2]), derived_secrets[2]),
(Scalar::from(node_indices[1]), derived_secrets[1]),
])
.unwrap();
// END OF SETUP
let (recovered_master, recovered_partials) =
try_recover_verification_keys(&dealings, threshold, &receivers).unwrap();
let g2 = G2Projective::generator();
assert_eq!(g2 * master_secret, recovered_master);
assert_eq!(g2 * derived_secrets[0], recovered_partials[0]);
assert_eq!(g2 * derived_secrets[1], recovered_partials[1]);
assert_eq!(g2 * derived_secrets[2], recovered_partials[2]);
}
#[test]
fn verifying_partial_verification_keys() {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 2;
let node_indices = vec![1, 4, 7];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
let dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
epoch,
&receivers,
None,
)
.0
})
.collect::<Vec<_>>();
let (recovered_master, recovered_partials) =
try_recover_verification_keys(&dealings, threshold, &receivers).unwrap();
assert!(verify_verification_keys(
&recovered_master,
&recovered_partials,
&receivers,
threshold
)
.is_ok())
}
#[test]
fn dealing_roundtrip() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let parties = 5;
let threshold = ((parties as f32 * 2.) / 3. + 1.) as Threshold;
let node_indices = (1..=parties).collect::<Vec<_>>();
let epoch = Epoch::new(2);
let mut receivers = BTreeMap::new();
for index in &node_indices {
let (_, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
}
let (dealing, _) = Dealing::create(
&mut rng,
&params,
node_indices[0],
threshold,
epoch,
&receivers,
None,
);
let bytes = dealing.to_bytes();
let recovered = Dealing::try_from_bytes(&bytes).unwrap();
assert_eq!(dealing, recovered);
}
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DkgError {
#[error("Provided set of values contained duplicate coordinate")]
DuplicateCoordinate,
#[error("The public key is malformed")]
MalformedPublicKey,
#[error("The decryption key is malformed")]
MalformedDecryptionKey,
#[error("Could not solve the discrete log")]
UnsolvableDiscreteLog,
#[error("Received share is malformed")]
MalformedShare,
#[error("The share encrypted under index {0} doesn't exist")]
UnavailableCiphertext(usize),
#[error("The provided lookup table is mismatched")]
MismatchedLookupTable,
#[error("Failed to verify proof of discrete logarithm")]
InvalidProofOfDiscreteLog,
#[error("Tried to construct proof of sharing with an invalid instance")]
MalformedProofOfSharingInstance,
#[error("Tried to construct proof of chunking with an invalid instance")]
MalformedProofOfChunkingInstance,
#[error("Aborted construction of proof of chunking - could not complete it within specified number of attempts")]
AbortedProofOfChunking,
#[error("Tried to update the decryption key to an epoch in the past")]
TargetEpochUpdateInThePast,
#[error("Provided epoch is malformed")]
MalformedEpoch,
#[error("Provided node is not a valid parent")]
NotAValidParent,
#[error("Provided decryption key has expired")]
ExpiredKey,
#[error("Provided threshold value ({actual}) is either 0 or larger than the total number of the participating parties ({participating})")]
InvalidThreshold { actual: usize, participating: usize },
#[error(
"Provided ciphertext has been generated for a different number of participating parties (expected: {expected}, actual: {actual})"
)]
WrongCiphertextSize { actual: usize, expected: usize },
#[error(
"Provided public coefficients have been generated for a different number of participating parties (expected: {expected}, actual: {actual})"
)]
WrongPublicCoefficientsSize { actual: usize, expected: usize },
#[error("The provided ciphertexts failed integrity check")]
FailedCiphertextIntegrityCheck,
#[error("The provided proof of secret sharing was invalid")]
InvalidProofOfSharing,
#[error("The provided proof of chunking was invalid")]
InvalidProofOfChunking,
#[error("Failed to deserialize {name} - {reason}")]
DeserializationFailure { name: String, reason: String },
#[error("No dealings were provided")]
NoDealingsAvailable,
#[error("Provided dealings were created under different parameters")]
MismatchedDealings,
#[error(
"Not enough dealings are available. We have {available} while require at least {required}"
)]
NotEnoughDealingsAvailable { available: usize, required: usize },
#[error("Received different number of x and y coordinates for lagrangian interpolation (xs: {x}, ys: {y})")]
MismatchedLagrangianSamplesLengths { x: usize, y: usize },
#[error("Derived partial verification key is mismatched")]
MismatchedVerificationKey,
#[error("Insufficient number of receivers was provided")]
NotEnoughReceiversProvided,
#[error(
"The reshared dealing has different public constant coefficient than its prior variant"
)]
InvalidResharing,
}
impl DkgError {
pub fn new_deserialization_failure<S: Into<String>, T: Into<String>>(
name: S,
reason: T,
) -> DkgError {
DkgError::DeserializationFailure {
name: name.into(),
reason: reason.into(),
}
}
}
+157
View File
@@ -0,0 +1,157 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::DkgError;
use bls12_381::Scalar;
use core::iter::Sum;
use core::ops::Mul;
use std::collections::HashSet;
pub mod polynomial;
fn contains_duplicates(vals: &[Scalar]) -> bool {
let mut set = HashSet::new();
for x in vals {
if !set.insert(x.to_bytes()) {
return true;
}
}
false
}
#[inline]
fn generate_lagrangian_coefficients_at_x(
x: &Scalar,
points: &[Scalar],
) -> Result<Vec<Scalar>, DkgError> {
let num_points = points.len();
if num_points == 0 {
return Ok(Vec::new());
} else if num_points == 1 {
return Ok(vec![Scalar::one()]);
}
if contains_duplicates(points) {
return Err(DkgError::DuplicateCoordinate);
}
let mut res = Vec::with_capacity(points.len());
for (i, xi) in points.iter().enumerate() {
let mut numerator = Scalar::one();
let mut denominator = Scalar::one();
for (j, xj) in points.iter().enumerate() {
if j != i {
// numerator = (x - xs[0]) * ... * (x - xs[j]), j != i
numerator *= x - xj;
// denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i
denominator *= xi - xj;
}
}
// 1 / denominator
let inv: Scalar =
Option::from(denominator.invert()).ok_or(DkgError::DuplicateCoordinate)?;
// numerator / denominator
res.push(numerator * inv)
}
Ok(res)
}
/// Performs a Lagrange interpolation at specified x for a polynomial defined by set of coordinates
/// (x, f(x)), where x is a `Scalar` and f(x) is a generic type that can be obtained by evaluating `f` at `x`.
/// It can be used for Scalars, G1 and G2 points.
pub fn perform_lagrangian_interpolation_at_x<T>(
x: &Scalar,
points: &[(Scalar, T)],
) -> Result<T, DkgError>
where
T: Sum,
for<'a> &'a T: Mul<Scalar, Output = T>,
{
let xs = points.iter().map(|p| p.0).collect::<Vec<_>>();
let coefficients = generate_lagrangian_coefficients_at_x(x, &xs)?;
Ok(coefficients
.into_iter()
.zip(points.iter().map(|p| &p.1))
.map(|(coeff, y)| y * coeff)
.sum())
}
/// Performs a Lagrange interpolation at the origin for a polynomial defined by set of coordinates
/// (x, f(x)), where x is a `Scalar` and f(x) is a generic type that can be obtained by evaluating `f` at `x`.
/// It can be used for Scalars, G1 and G2 points.
pub fn perform_lagrangian_interpolation_at_origin<T>(points: &[(Scalar, T)]) -> Result<T, DkgError>
where
T: Sum,
for<'a> &'a T: Mul<Scalar, Output = T>,
{
perform_lagrangian_interpolation_at_x(&Scalar::zero(), points)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn performing_lagrangian_scalar_interpolation_at_origin() {
// x^2 + 3
// x, f(x):
// 1, 4,
// 2, 7,
// 3, 12,
let points = vec![
(Scalar::from(1), Scalar::from(4)),
(Scalar::from(2), Scalar::from(7)),
(Scalar::from(3), Scalar::from(12)),
];
assert_eq!(
Scalar::from(3),
perform_lagrangian_interpolation_at_origin(&points).unwrap()
);
// x^3 + 3x^2 - 5x + 11
// x, f(x):
// 1, 10
// 2, 21
// 3, 50
// 4, 103
let points = vec![
(Scalar::from(1), Scalar::from(10)),
(Scalar::from(2), Scalar::from(21)),
(Scalar::from(3), Scalar::from(50)),
(Scalar::from(4), Scalar::from(103)),
];
assert_eq!(
Scalar::from(11),
perform_lagrangian_interpolation_at_origin(&points).unwrap()
);
// more points than it is required
// x^2 + x + 10
// x, f(x)
// 1, 12
// 2, 16
// 3, 22
// 4, 30
// 5, 40
let points = vec![
(Scalar::from(1), Scalar::from(12)),
(Scalar::from(2), Scalar::from(16)),
(Scalar::from(3), Scalar::from(22)),
(Scalar::from(4), Scalar::from(30)),
(Scalar::from(5), Scalar::from(40)),
];
assert_eq!(
Scalar::from(10),
perform_lagrangian_interpolation_at_origin(&points).unwrap()
);
}
}
@@ -0,0 +1,395 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::DkgError;
use crate::utils::deserialize_g2;
use bls12_381::{G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand_core::RngCore;
use std::ops::{Add, Index, IndexMut};
use zeroize::Zeroize;
#[derive(Clone, Debug, PartialEq)]
pub struct PublicCoefficients {
pub(crate) coefficients: Vec<G2Projective>,
}
impl PublicCoefficients {
pub(crate) fn size(&self) -> usize {
self.coefficients.len()
}
pub(crate) fn nth(&self, n: usize) -> &G2Projective {
&self.coefficients[n]
}
pub(crate) fn is_empty(&self) -> bool {
self.coefficients.is_empty()
}
pub(crate) fn inner(&self) -> &[G2Projective] {
&self.coefficients
}
pub(crate) fn evaluate_at(&self, x: &Scalar) -> G2Projective {
if self.coefficients.is_empty() {
G2Projective::identity()
// if x is zero then we can ignore most of the expensive computation and
// just return the last term of the polynomial
} else if x.is_zero().into() {
// we checked that coefficients are not empty so unwrap here is fine
*self.coefficients.first().unwrap()
} else {
self.coefficients
.iter()
.enumerate()
// coefficient[n] * x ^ n
.map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0]))
.sum()
}
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let coeffs = self.coefficients.len();
let mut bytes = Vec::with_capacity(4 + 96 * coeffs);
bytes.extend_from_slice(&((coeffs as u32).to_be_bytes()));
for coeff in &self.coefficients {
bytes.extend_from_slice(coeff.to_bytes().as_ref())
}
bytes
}
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<Self, DkgError> {
if b.len() < 4 {
return Err(DkgError::new_deserialization_failure(
"PublicCoefficients",
"insufficient number of bytes provided",
));
}
let coeffs = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize;
let mut coefficients = Vec::with_capacity(coeffs);
if b.len() != 4 + coeffs * 96 {
return Err(DkgError::new_deserialization_failure(
"PublicCoefficients",
"insufficient number of bytes provided",
));
}
let mut i = 4;
for _ in 0..coeffs {
let coefficient = deserialize_g2(&b[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure(
"PublicCoefficients.coefficient",
"invalid curve point",
)
})?;
coefficients.push(coefficient);
i += 96;
}
Ok(PublicCoefficients { coefficients })
}
}
impl Index<usize> for PublicCoefficients {
type Output = G2Projective;
fn index(&self, index: usize) -> &Self::Output {
self.coefficients.index(index)
}
}
impl IndexMut<usize> for PublicCoefficients {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
self.coefficients.index_mut(index)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Zeroize)]
#[zeroize(drop)]
pub struct Polynomial {
coefficients: Vec<Scalar>,
}
impl Polynomial {
// for polynomial of degree n, we generate n+1 values
// (for example for degree 1, like y = x + 2, we need [2,1])
/// Creates new pseudorandom polynomial of specified degree.
pub fn new_random(mut rng: impl RngCore, degree: u64) -> Self {
Polynomial {
coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(),
}
}
/// Creates new polynomial with provided coefficients.
pub fn new(coefficients: Vec<Scalar>) -> Self {
Polynomial { coefficients }
}
pub fn set_constant_coefficient(&mut self, value: Scalar) {
if self.coefficients.is_empty() {
self.coefficients = vec![value]
} else {
self.coefficients[0] = value
}
}
/// Creates a zero-polynomial, i.e. p(x) = 0
pub const fn zero() -> Self {
Polynomial {
coefficients: Vec::new(),
}
}
/// Returns public coefficients associated with this polynomial.
pub fn public_coefficients(&self) -> PublicCoefficients {
let g2 = G2Projective::generator();
let coefficients = self.coefficients.iter().map(|a_i| g2 * a_i).collect();
PublicCoefficients { coefficients }
}
/// Evaluates the polynomial at point x.
pub fn evaluate_at(&self, x: &Scalar) -> Scalar {
if self.coefficients.is_empty() {
Scalar::zero()
// if x is zero then we can ignore most of the expensive computation and
// just return the last term of the polynomial
} else if x.is_zero().into() {
// we checked that coefficients are not empty so unwrap here is fine
*self.coefficients.first().unwrap()
} else {
self.coefficients
.iter()
.enumerate()
// coefficient[n] * x ^ n
.map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0]))
.sum()
}
}
}
impl Index<usize> for Polynomial {
type Output = Scalar;
fn index(&self, index: usize) -> &Self::Output {
self.coefficients.index(index)
}
}
impl IndexMut<usize> for Polynomial {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
self.coefficients.index_mut(index)
}
}
impl<'b> Add<&'b Polynomial> for Polynomial {
type Output = Polynomial;
fn add(self, rhs: &'b Polynomial) -> Polynomial {
&self + rhs
}
}
impl<'a> Add<Polynomial> for &'a Polynomial {
type Output = Polynomial;
fn add(self, rhs: Polynomial) -> Polynomial {
self + &rhs
}
}
impl Add<Polynomial> for Polynomial {
type Output = Polynomial;
fn add(self, rhs: Polynomial) -> Polynomial {
&self + &rhs
}
}
impl<'a, 'b> Add<&'b Polynomial> for &'a Polynomial {
type Output = Polynomial;
fn add(self, rhs: &'b Polynomial) -> Self::Output {
let len = self.coefficients.len();
let rhs_len = rhs.coefficients.len();
// to have easier bound checks
if rhs_len > len {
return rhs + self;
}
// we know len >= rhs_len and hence the output will also be of size len
let mut res = Vec::with_capacity(len);
for i in 0..len {
if let Some(rhs_coeff) = rhs.coefficients.get(i) {
res.push(self[i] + rhs_coeff)
} else {
res.push(self[i])
}
}
Polynomial { coefficients: res }
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand_core::SeedableRng;
#[test]
fn polynomial_evaluation() {
// y = 42 (it should be 42 regardless of x)
let poly = Polynomial {
coefficients: vec![Scalar::from(42)],
};
assert_eq!(Scalar::from(42), poly.evaluate_at(&Scalar::from(1)));
assert_eq!(Scalar::from(42), poly.evaluate_at(&Scalar::from(0)));
assert_eq!(Scalar::from(42), poly.evaluate_at(&Scalar::from(10)));
// y = x + 10, at x = 2 (exp: 12)
let poly = Polynomial {
coefficients: vec![Scalar::from(10), Scalar::from(1)],
};
assert_eq!(Scalar::from(12), poly.evaluate_at(&Scalar::from(2)));
// y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39)
let poly = Polynomial {
coefficients: vec![
(-Scalar::from(3)),
Scalar::from(2),
(-Scalar::from(5)),
Scalar::zero(),
Scalar::from(1),
],
};
assert_eq!(Scalar::from(39), poly.evaluate_at(&Scalar::from(3)));
// empty polynomial
let poly = Polynomial::zero();
// should always be 0
assert_eq!(Scalar::from(0), poly.evaluate_at(&Scalar::from(1)));
assert_eq!(Scalar::from(0), poly.evaluate_at(&Scalar::from(0)));
assert_eq!(Scalar::from(0), poly.evaluate_at(&Scalar::from(10)));
}
#[test]
fn polynomial_addition() {
let empty = Polynomial::zero();
let p1 = Polynomial {
coefficients: vec![Scalar::from(1), Scalar::from(2), Scalar::from(3)],
};
let p2 = Polynomial {
coefficients: vec![Scalar::from(4), Scalar::from(5)],
};
let expected_sum = Polynomial {
coefficients: vec![Scalar::from(5), Scalar::from(7), Scalar::from(3)],
};
assert_eq!(p1, &p1 + &empty);
assert_eq!(p1, empty + &p1);
assert_eq!(expected_sum, &p1 + &p2);
assert_eq!(expected_sum, &p2 + &p1);
}
#[test]
fn public_coefficients_evaluation() {
// we use the same values as in polynomial evaluation test
let g2 = G2Projective::generator();
// y = 42 (it should be 42 regardless of x)
let coeffs = PublicCoefficients {
coefficients: vec![g2 * Scalar::from(42)],
};
assert_eq!(g2 * Scalar::from(42), coeffs.evaluate_at(&Scalar::from(1)));
assert_eq!(g2 * Scalar::from(42), coeffs.evaluate_at(&Scalar::from(0)));
assert_eq!(g2 * Scalar::from(42), coeffs.evaluate_at(&Scalar::from(10)));
// y = x + 10, at x = 2 (exp: 12)
let poly = PublicCoefficients {
coefficients: vec![g2 * Scalar::from(10), g2 * Scalar::from(1)],
};
assert_eq!(g2 * Scalar::from(12), poly.evaluate_at(&Scalar::from(2)));
// y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39)
let coeffs = PublicCoefficients {
coefficients: vec![
(-g2 * Scalar::from(3)),
g2 * Scalar::from(2),
(-g2 * Scalar::from(5)),
G2Projective::identity(),
g2 * Scalar::from(1),
],
};
assert_eq!(g2 * Scalar::from(39), coeffs.evaluate_at(&Scalar::from(3)));
// empty coefficients
let coeffs = PublicCoefficients {
coefficients: Vec::new(),
};
// should always be 0
assert_eq!(
G2Projective::identity(),
coeffs.evaluate_at(&Scalar::from(1))
);
assert_eq!(
G2Projective::identity(),
coeffs.evaluate_at(&Scalar::from(0))
);
assert_eq!(
G2Projective::identity(),
coeffs.evaluate_at(&Scalar::from(10))
);
}
#[test]
fn public_coefficients_roundtrip() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let good = vec![
Polynomial::zero().public_coefficients(),
Polynomial::new_random(&mut rng, 0).public_coefficients(),
Polynomial::new_random(&mut rng, 1).public_coefficients(),
Polynomial::new_random(&mut rng, 4).public_coefficients(),
Polynomial::new_random(&mut rng, 15).public_coefficients(),
];
for coefficient in good {
let bytes = coefficient.to_bytes();
let recovered = PublicCoefficients::try_from_bytes(&bytes).unwrap();
assert_eq!(coefficient, recovered);
}
assert!(PublicCoefficients::try_from_bytes(&[]).is_err());
assert!(PublicCoefficients::try_from_bytes(&[1]).is_err());
assert!(PublicCoefficients::try_from_bytes(&[1, 2, 3, 4]).is_err());
let g2 = G2Projective::generator().to_bytes();
let mut bad_length = Vec::new();
bad_length.extend_from_slice(&2u32.to_be_bytes());
bad_length.extend_from_slice(g2.as_ref());
assert!(PublicCoefficients::try_from_bytes(&bad_length).is_err());
let mut incomplete = Vec::new();
incomplete.extend_from_slice(&1u32.to_be_bytes());
incomplete.extend_from_slice(&g2.as_ref()[..95]);
assert!(PublicCoefficients::try_from_bytes(&incomplete).is_err());
}
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// forward-secure public key encryption scheme
pub mod bte;
pub mod error;
pub mod interpolation;
// this entire module is a big placeholder for whatever scheme we decide to use for the
// secure channel encryption scheme, but I would assume that the top-level API would
// remain more or less the same
pub mod dealing;
pub(crate) mod share;
pub(crate) mod utils;
pub use dealing::*;
pub use share::*;
// TODO: presumably this should live in a some different, common, crate?
pub type Threshold = u64;
pub type NodeIndex = u64;
#[cfg(test)]
mod tests {
use crate::interpolation::perform_lagrangian_interpolation_at_origin;
use crate::interpolation::polynomial::Polynomial;
use bls12_381::Scalar;
use rand_chacha::rand_core::SeedableRng;
#[test]
fn basic_dummy_secret_sharing() {
let degree = 2;
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let p1 = Polynomial::new_random(&mut rng, degree);
let p2 = Polynomial::new_random(&mut rng, degree);
let p3 = Polynomial::new_random(&mut rng, degree);
let p4 = Polynomial::new_random(&mut rng, degree);
let zero = Scalar::zero();
let one = Scalar::one();
let two = Scalar::from(2);
let three = Scalar::from(3);
let four = Scalar::from(4);
// i.e. given:
// p1 = a1 + x * b1 + ...
// p2 = a2 + x * b2 + ...
// ...
// expected = (a1 + a2 + ...) + x * (b1 + b2 + ...) + ...
// note: master polynomial is NEVER explicitly computed
let expected_master = &p1 + &p2 + &p3 + &p4;
let v1_secret = p1.evaluate_at(&one)
+ p2.evaluate_at(&one)
+ p3.evaluate_at(&one)
+ p4.evaluate_at(&one);
let v2_secret = p1.evaluate_at(&two)
+ p2.evaluate_at(&two)
+ p3.evaluate_at(&two)
+ p4.evaluate_at(&two);
let v3_secret = p1.evaluate_at(&three)
+ p2.evaluate_at(&three)
+ p3.evaluate_at(&three)
+ p4.evaluate_at(&three);
let v4_secret = p1.evaluate_at(&four)
+ p2.evaluate_at(&four)
+ p3.evaluate_at(&four)
+ p4.evaluate_at(&four);
// note that the following would have never happened in actual dkg setting, but it's
// used here mostly for a sanity check on the maths used
let samples = vec![
(one, v1_secret),
(two, v2_secret),
(three, v3_secret),
(four, v4_secret),
];
let master_secret = perform_lagrangian_interpolation_at_origin(&samples).unwrap();
assert_eq!(expected_master.evaluate_at(&zero), master_secret);
assert_eq!(expected_master.evaluate_at(&one), v1_secret);
assert_eq!(expected_master.evaluate_at(&two), v2_secret);
assert_eq!(expected_master.evaluate_at(&three), v3_secret);
assert_eq!(expected_master.evaluate_at(&four), v4_secret);
// since we have 4 parties, but polynomials used are of degree 2, we only need at least 3
// issuers to contribute
let samples2 = vec![(one, v1_secret), (three, v3_secret), (four, v4_secret)];
let master_secret2 = perform_lagrangian_interpolation_at_origin(&samples2).unwrap();
assert_eq!(master_secret, master_secret2)
}
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::{CHUNK_BYTES, NUM_CHUNKS, SCALAR_SIZE};
use crate::error::DkgError;
use crate::interpolation::perform_lagrangian_interpolation_at_origin;
use crate::NodeIndex;
use bls12_381::Scalar;
use zeroize::Zeroize;
// if this type is changed, one must ensure all values can fit in it
pub type Chunk = u16;
#[derive(PartialEq, Eq, Debug, Zeroize)]
#[cfg_attr(test, derive(Clone))]
#[zeroize(drop)]
pub struct Share(pub(crate) Scalar);
pub fn combine_shares(shares: Vec<Share>, node_indices: &[NodeIndex]) -> Result<Scalar, DkgError> {
if shares.len() != node_indices.len() {
return Err(DkgError::MismatchedLagrangianSamplesLengths {
x: node_indices.len(),
y: shares.len(),
});
}
let samples = shares
.into_iter()
.zip(node_indices.iter())
.map(|(share, index)| (Scalar::from(*index), share.0))
.collect::<Vec<_>>();
perform_lagrangian_interpolation_at_origin(&samples)
}
impl Share {
// not really used outside tests
#[cfg(test)]
pub(crate) fn random(mut rng: impl rand_core::RngCore) -> Self {
use ff::Field;
Share(Scalar::random(&mut rng))
}
pub(crate) fn to_chunks(&self) -> ChunkedShare {
let mut chunks = [0; NUM_CHUNKS];
let mut bytes = self.0.to_bytes();
for (chunk, chunk_bytes) in chunks.iter_mut().zip(bytes[..].chunks_exact(CHUNK_BYTES)) {
let mut tmp = [0u8; CHUNK_BYTES];
tmp.copy_from_slice(chunk_bytes);
*chunk = Chunk::from_le_bytes(tmp)
}
bytes.zeroize();
ChunkedShare { chunks }
}
pub(crate) fn inner(&self) -> &Scalar {
&self.0
}
}
impl From<Scalar> for Share {
fn from(s: Scalar) -> Self {
Share(s)
}
}
#[derive(Default, Zeroize)]
#[cfg_attr(test, derive(Clone))]
#[zeroize(drop)]
pub(crate) struct ChunkedShare {
pub(crate) chunks: [Chunk; NUM_CHUNKS],
}
impl From<Share> for ChunkedShare {
fn from(share: Share) -> ChunkedShare {
share.to_chunks()
}
}
impl TryFrom<ChunkedShare> for Share {
type Error = DkgError;
fn try_from(chunked: ChunkedShare) -> Result<Share, Self::Error> {
let mut bytes = [0u8; SCALAR_SIZE];
for (chunk, chunk_bytes) in chunked
.chunks
.iter()
.zip(bytes[..].chunks_exact_mut(CHUNK_BYTES))
{
let tmp = chunk.to_le_bytes();
chunk_bytes.copy_from_slice(&tmp[..]);
}
let recovered = Option::from(Scalar::from_bytes(&bytes))
.map(Share)
.ok_or(DkgError::MalformedShare)?;
bytes.zeroize();
Ok(recovered)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::combine_scalar_chunks;
use rand_core::SeedableRng;
#[test]
fn chunking_share() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let share = Share::random(&mut rng);
let chunks: ChunkedShare = share.clone().into();
let scalar_chunks = chunks
.chunks
.iter()
.map(|c| Scalar::from(*c as u64))
.collect::<Vec<_>>();
let expected = combine_scalar_chunks(&scalar_chunks);
assert_eq!(expected, share.0);
let recombined: Share = chunks.try_into().unwrap();
assert_eq!(expected, recombined.0);
}
}
+4
View File
@@ -0,0 +1,4 @@
Just a todo file to keep track of what should be changed
- Deriving challenges and oracles should be more streamlined; perhaps some trait for hashing
- perhaps there could be additional intermediate types in proofs of knowledge (for moves / responses / etc)
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::CHUNK_SIZE;
use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField};
use bls12_381::G1Projective;
use bls12_381::{G2Projective, Scalar};
use group::GroupEncoding;
use sha2::{Digest, Sha256};
#[macro_export]
macro_rules! ensure_len {
($a:expr, $b:expr) => {
if $a.len() != $b {
return false;
}
};
}
pub(crate) struct RandomOracleBuilder {
inner_state: Sha256,
}
impl RandomOracleBuilder {
pub(crate) fn new(domain: &[u8]) -> Self {
let mut inner_state = Sha256::new();
inner_state.update(domain);
RandomOracleBuilder { inner_state }
}
pub(crate) fn update(&mut self, data: impl AsRef<[u8]>) {
self.inner_state.update(data)
}
pub(crate) fn update_with_g1_elements<'a, I>(&mut self, items: I)
where
I: Iterator<Item = &'a G1Projective>,
{
items.for_each(|item| self.update(item.to_bytes()))
}
pub(crate) fn finalize(self) -> [u8; 32] {
self.inner_state.finalize().into()
}
}
// those will most likely need to somehow get re-combined with coconut (or maybe extracted to a completely different module)
pub(crate) fn hash_to_scalar<M: AsRef<[u8]>>(msg: M, domain: &[u8]) -> Scalar {
// the unwrap here is fine as the result vector will have 1 element (as specified) and will not be empty
hash_to_scalars(msg, domain, 1).pop().unwrap()
}
pub(crate) fn hash_to_scalars<M: AsRef<[u8]>>(msg: M, domain: &[u8], n: usize) -> Vec<Scalar> {
let mut output = vec![Scalar::zero(); n];
Scalar::hash_to_field::<ExpandMsgXmd<Sha256>>(msg.as_ref(), domain, &mut output);
output
}
pub(crate) fn hash_g2<M: AsRef<[u8]>>(msg: M, domain: &[u8]) -> G2Projective {
<G2Projective as HashToCurve<ExpandMsgXmd<Sha256>>>::hash_to_curve(msg, domain)
}
pub(crate) fn combine_scalar_chunks(chunks: &[Scalar]) -> Scalar {
let chunk_size_scalar = Scalar::from(CHUNK_SIZE as u64);
chunks.iter().rev().fold(Scalar::zero(), |mut acc, chunk| {
acc *= chunk_size_scalar;
acc += chunk;
acc
})
}
pub(crate) fn combine_g1_chunks(chunks: &[G1Projective]) -> G1Projective {
let chunk_size_scalar = Scalar::from(CHUNK_SIZE as u64);
chunks
.iter()
.rev()
.fold(G1Projective::identity(), |mut acc, chunk| {
acc *= chunk_size_scalar;
acc += chunk;
acc
})
}
pub(crate) fn deserialize_scalar(b: &[u8]) -> Option<Scalar> {
if b.len() != 32 {
None
} else {
let mut repr: [u8; 32] = Default::default();
repr.as_mut().copy_from_slice(b);
Scalar::from_bytes(&repr).into()
}
}
pub(crate) fn deserialize_g1(b: &[u8]) -> Option<G1Projective> {
if b.len() != 48 {
None
} else {
let mut encoding = <G1Projective as GroupEncoding>::Repr::default();
encoding.as_mut().copy_from_slice(b);
G1Projective::from_bytes(&encoding).into()
}
}
pub(crate) fn deserialize_g2(b: &[u8]) -> Option<G2Projective> {
if b.len() != 96 {
None
} else {
let mut encoding = <G2Projective as GroupEncoding>::Repr::default();
encoding.as_mut().copy_from_slice(b);
G2Projective::from_bytes(&encoding).into()
}
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::{G2Projective, Scalar};
use dkg::bte::{decrypt_share, keygen, setup, Epoch};
use dkg::interpolation::perform_lagrangian_interpolation_at_origin;
use dkg::{combine_shares, try_recover_verification_keys, Dealing};
use rand_core::SeedableRng;
use std::collections::BTreeMap;
#[test]
fn single_sender() {
// makes it easier to understand than `full_threshold_secret_sharing`
// and is a good stepping stone, because its everything each node will have to perform (from one point of view)
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
// the simplest possible case
let threshold = 2;
// the indices are going to get assigned externally, so for test sake, use non-consecutive ones
let node_indices = vec![15u64, 248, 33521];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
// TODO: HERE BE SERIALIZATION / DESERIALIZATION THAT'S NOT IMPLEMENTED YET
// verify remote proofs of key possession
for key in full_keys.iter() {
assert!(key.1.verify());
}
let (dealing, dealer_share) = Dealing::create(
&mut rng,
&params,
node_indices[0],
threshold,
epoch,
&receivers,
None,
);
dealing
.verify(&params, epoch, threshold, &receivers, None)
.unwrap();
// make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check)
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap();
}
// and for good measure, check that the dealer's share matches decryption result
let recovered_dealer =
decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, epoch, None).unwrap();
assert_eq!(recovered_dealer, dealer_share.unwrap());
}
#[test]
fn full_threshold_secret_sharing() {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
// the simplest possible case
let threshold = 2;
// the indices are going to get assigned externally, so for test sake, use non-consecutive ones
let node_indices = vec![15u64, 248, 33521];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
// TODO: HERE BE SERIALIZATION / DESERIALIZATION THAT'S NOT IMPLEMENTED YET
// verify remote proofs of key possession
for key in full_keys.iter() {
assert!(key.1.verify());
}
let dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
epoch,
&receivers,
None,
)
.0
})
.collect::<Vec<_>>();
for dealing in dealings.iter() {
dealing
.verify(&params, epoch, threshold, &receivers, None)
.unwrap();
}
// recover verification keys
let (recovered_master, recovered_partials) =
try_recover_verification_keys(&dealings, threshold, &receivers).unwrap();
let g2 = G2Projective::generator();
let mut derived_secrets = Vec::new();
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let shares = dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap())
.collect();
// we know dealer_share matches, but it would be inconvenient to try to put them in here,
// so for ease of use (IN A TEST SETTING), just decrypt one's own share
let recovered_secret =
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap();
// make sure it matches the associated vk
assert_eq!(recovered_partials[i], g2 * recovered_secret);
derived_secrets.push(recovered_secret)
}
// sanity check that the shares were combined correctly and if we take threshold number of them,
// we end up with the same master secret, note: those are NEVER explicitly recovered in actual system
// (remember threshold was 2)
let master1 = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[0]), derived_secrets[0]),
(Scalar::from(node_indices[1]), derived_secrets[1]),
])
.unwrap();
let master2 = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[1]), derived_secrets[1]),
(Scalar::from(node_indices[2]), derived_secrets[2]),
])
.unwrap();
assert_eq!(master1, master2);
assert_eq!(recovered_master, g2 * master1);
}
#[test]
fn full_threshold_secret_resharing() {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
// the simplest possible case
let threshold = 2;
// the indices are going to get assigned externally, so for test sake, use non-consecutive ones
let node_indices = vec![15u64, 248, 33521];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
let first_dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
epoch,
&receivers,
None,
)
.0
})
.collect::<Vec<_>>();
// recover verification keys
let (public_original_master, recovered_partials) =
try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap();
let mut derived_secrets = Vec::new();
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let shares = first_dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap())
.collect();
let recovered_secret =
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap();
derived_secrets.push(recovered_secret)
}
let original_master = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[0]), derived_secrets[0]),
(Scalar::from(node_indices[1]), derived_secrets[1]),
])
.unwrap();
let next_epoch = Epoch::new(3);
// attempt to create resharing dealings!
let resharing_dealings = node_indices
.iter()
.zip(derived_secrets.iter())
.map(|(&dealer_index, prior_secret)| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
next_epoch,
&receivers,
Some(*prior_secret),
)
.0
})
.collect::<Vec<_>>();
for (reshared_dealing, prior_vk) in resharing_dealings.iter().zip(recovered_partials.iter()) {
reshared_dealing
.verify(&params, next_epoch, threshold, &receivers, Some(*prior_vk))
.unwrap();
}
// recover verification keys
let (public_reshared_master, reshared_partials) =
try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap();
let mut reshared_secrets = Vec::new();
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(next_epoch, &params, &mut rng).unwrap();
let shares = resharing_dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, next_epoch, None).unwrap())
.collect();
let recovered_secret =
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap();
reshared_secrets.push(recovered_secret)
}
let reshared_master = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[0]), reshared_secrets[0]),
(Scalar::from(node_indices[1]), reshared_secrets[1]),
])
.unwrap();
// the master secret and public values didn't change
assert_eq!(original_master, reshared_master);
assert_eq!(public_original_master, public_reshared_master);
// but partials did
assert_ne!(derived_secrets, reshared_secrets);
assert_ne!(recovered_partials, reshared_partials);
}