diff --git a/Cargo.lock b/Cargo.lock index 21c68b00d17..87cc9b44b9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7360,6 +7360,7 @@ dependencies = [ "ic-test-utilities-time", "ic-types", "ic-types-test-utils", + "ic-vetkd-utils", "k256 0.13.4", "maplit", "mockall", diff --git a/rs/crypto/BUILD.bazel b/rs/crypto/BUILD.bazel index 3cdaa0b0151..72317f6bb02 100644 --- a/rs/crypto/BUILD.bazel +++ b/rs/crypto/BUILD.bazel @@ -59,6 +59,7 @@ MACRO_DEPENDENCIES = [ DEV_DEPENDENCIES = [ # Keep sorted. + "//packages/ic-vetkd-utils", "//rs/certification/test-utils", "//rs/crypto/ecdsa_secp256r1", "//rs/crypto/for_verification_only", diff --git a/rs/crypto/Cargo.toml b/rs/crypto/Cargo.toml index 46b7c841deb..ae2d4889ba2 100644 --- a/rs/crypto/Cargo.toml +++ b/rs/crypto/Cargo.toml @@ -41,6 +41,7 @@ ic-protobuf = { path = "../protobuf" } ic-registry-client-helpers = { path = "../registry/helpers" } ic-registry-keys = { path = "../registry/keys" } ic-types = { path = "../types/types" } +ic-vetkd-utils = { path = "../../packages/ic-vetkd-utils" } parking_lot = { workspace = true } rustls = { workspace = true } serde = { workspace = true } diff --git a/rs/crypto/test_utils/ni-dkg/src/lib.rs b/rs/crypto/test_utils/ni-dkg/src/lib.rs index 4cb89c34c68..3b3d82255a6 100644 --- a/rs/crypto/test_utils/ni-dkg/src/lib.rs +++ b/rs/crypto/test_utils/ni-dkg/src/lib.rs @@ -878,7 +878,11 @@ impl NiDkgTestEnvironment { let temp_crypto_builder = TempCryptoComponent::builder() .with_registry(Arc::clone(&self.registry) as Arc<_>) .with_node_id(node_id) - .with_keys(NodeKeysToGenerate::only_dkg_dealing_encryption_key()) + .with_keys(NodeKeysToGenerate { + generate_node_signing_keys: true, + generate_dkg_dealing_encryption_keys: true, + ..NodeKeysToGenerate::none() + }) .with_rng(ChaCha20Rng::from_seed(rng.gen())); let temp_crypto_builder = if use_remote_vault { temp_crypto_builder.with_remote_vault() @@ -891,6 +895,11 @@ impl NiDkgTestEnvironment { .expect("Failed to retrieve node public keys") .dkg_dealing_encryption_public_key .expect("missing dkg_dealing_encryption_pk"); + let node_signing_pubkey = temp_crypto + .current_node_public_keys() + .expect("Failed to retrieve node public keys") + .node_signing_public_key + .expect("missing dkg_dealing_encryption_pk"); self.crypto_components.insert(node_id, temp_crypto); // Insert DKG dealing encryption public key into registry @@ -901,6 +910,14 @@ impl NiDkgTestEnvironment { Some(dkg_dealing_encryption_pubkey), ) .expect("failed to add DKG dealing encryption key to registry"); + // Insert node signing public key into registry + self.registry_data + .add( + &make_crypto_node_key(node_id, KeyPurpose::NodeSigning), + ni_dkg_config.registry_version(), + Some(node_signing_pubkey), + ) + .expect("failed to add node signing public key to registry"); } /// Cleans up nodes whose IDs are no longer in use diff --git a/rs/crypto/tests/vetkd.rs b/rs/crypto/tests/vetkd.rs new file mode 100644 index 00000000000..600b397b49b --- /dev/null +++ b/rs/crypto/tests/vetkd.rs @@ -0,0 +1,218 @@ +use ic_crypto_temp_crypto::CryptoComponentRng; +use ic_crypto_temp_crypto::TempCryptoComponentGeneric; +use ic_crypto_test_utils::crypto_for; +use ic_crypto_test_utils_ni_dkg::{ + run_ni_dkg_and_create_single_transcript, NiDkgTestEnvironment, RandomNiDkgConfig, +}; +use ic_crypto_test_utils_reproducible_rng::reproducible_rng; +use ic_interfaces::crypto::VetKdProtocol; +use ic_interfaces::crypto::{LoadTranscriptResult, NiDkgAlgorithm}; +use ic_types::crypto::canister_threshold_sig::MasterPublicKey; +use ic_types::crypto::threshold_sig::ni_dkg::config::NiDkgConfig; +use ic_types::crypto::threshold_sig::ni_dkg::{NiDkgId, NiDkgTranscript}; +use ic_types::crypto::threshold_sig::ThresholdSigPublicKey; +use ic_types::crypto::vetkd::VetKdArgs; +use ic_types::crypto::vetkd::VetKdEncryptedKey; +use ic_types::crypto::vetkd::VetKdEncryptedKeyShare; +use ic_types::crypto::AlgorithmId; +use ic_types::crypto::ExtendedDerivationPath; +use ic_types::{NodeId, NumberOfNodes}; +use ic_types_test_utils::ids::canister_test_id; +use rand::prelude::*; +use rand_chacha::ChaCha20Rng; +use std::collections::{BTreeMap, BTreeSet}; +use std::convert::TryFrom; + +#[test] +fn should_consistently_derive_the_same_vetkey_given_sufficient_shares() { + let rng = &mut reproducible_rng(); + let subnet_size = rng.gen_range(1..7); + let (config, dkg_id, crypto_components) = setup_with_random_ni_dkg_config(subnet_size, rng); + + let transcript = run_ni_dkg_and_load_transcript_for_receivers(&config, &crypto_components); + + let derivation_path = ExtendedDerivationPath { + caller: canister_test_id(234).get(), + derivation_path: vec![b"some".to_vec(), b"derivation".to_vec(), b"path".to_vec()], + }; + let derived_public_key = ic_crypto_utils_canister_threshold_sig::derive_vetkd_public_key( + &MasterPublicKey { + algorithm_id: AlgorithmId::ThresBls12_381, + public_key: ThresholdSigPublicKey::try_from(&transcript) + .expect("invalid transcript") + .into_bytes() + .to_vec(), + }, + &derivation_path, + ) + .expect("failed to compute derived public key"); + let transport_secret_key = + ic_vetkd_utils::TransportSecretKey::from_seed(rng.gen::<[u8; 32]>().to_vec()) + .expect("failed to create transport secret key"); + let vetkd_args = VetKdArgs { + ni_dkg_id: dkg_id, + derivation_path, + derivation_id: b"some-derivation-id".to_vec(), + encryption_public_key: transport_secret_key.public_key(), + }; + + let mut expected_decrypted_key: Option> = None; + for _ in 1..=3 { + let encrypted_key = create_key_shares_and_verify_and_combine( + KeyShareCreatorsAndCombiner { + creators: n_random_nodes_in( + config.receivers().get(), + config.threshold().get(), + rng, + ), + combiner: random_node_in(config.receivers().get(), rng), + }, + &vetkd_args, + &crypto_components, + ); + + let random_verifier = random_node_in(config.receivers().get(), rng); + assert_eq!( + crypto_for(random_verifier, &crypto_components) + .verify_encrypted_key(&encrypted_key, &vetkd_args), + Ok(()) + ); + + let decrypted_key = transport_secret_key + .decrypt( + &encrypted_key.encrypted_key, + &derived_public_key, + &vetkd_args.derivation_id, + ) + .expect("failed to decrypt vetKey"); + + if let Some(expected_decrypted_key) = &expected_decrypted_key { + assert_eq!(&decrypted_key, expected_decrypted_key); + } else { + expected_decrypted_key = Some(decrypted_key); + } + } +} + +fn setup_with_random_ni_dkg_config( + subnet_size: usize, + rng: &mut R, +) -> ( + NiDkgConfig, + NiDkgId, + BTreeMap>, +) { + let config = RandomNiDkgConfig::builder() + .subnet_size(subnet_size) + .build(rng) + .into_config(); + let dkg_id = config.dkg_id().clone(); + let crypto_components = + NiDkgTestEnvironment::new_for_config_with_remote_vault(&config, rng).crypto_components; + (config, dkg_id, crypto_components) +} + +fn create_key_shares_and_verify_and_combine( + creators_and_combiner: KeyShareCreatorsAndCombiner, + vetkd_args: &VetKdArgs, + crypto_components: &BTreeMap>, +) -> VetKdEncryptedKey { + let key_shares = create_and_verify_key_shares_for_each( + &creators_and_combiner.creators, + vetkd_args, + crypto_components, + ); + crypto_for(creators_and_combiner.combiner, crypto_components) + .combine_encrypted_key_shares(&key_shares, vetkd_args) + .expect("failed to combine signature shares") +} + +fn create_and_verify_key_shares_for_each( + key_share_creators: &[NodeId], + vetkd_args: &VetKdArgs, + crypto_components: &BTreeMap>, +) -> BTreeMap { + key_share_creators + .iter() + .map(|creator| { + let crypto = crypto_for(*creator, crypto_components); + let key_share = crypto + .create_encrypted_key_share(vetkd_args.clone()) + .unwrap_or_else(|e| { + panic!( + "vetKD encrypted key share creation by node {:?} failed: {}", + creator, e + ) + }); + assert_eq!( + crypto.verify_encrypted_key_share(*creator, &key_share, vetkd_args), + Ok(()) + ); + (*creator, key_share) + }) + .collect() +} + +#[derive(Clone, Debug)] +struct KeyShareCreatorsAndCombiner { + creators: Vec, + combiner: NodeId, +} + +///////////////////////////////////////////////////////////////////////////////// +// The following helper functions where copied from threshold_sigs_with_ni_dkg.rs +///////////////////////////////////////////////////////////////////////////////// + +fn run_ni_dkg_and_load_transcript_for_receivers( + config: &NiDkgConfig, + crypto_components: &BTreeMap>, +) -> NiDkgTranscript { + let transcript = run_ni_dkg_and_create_single_transcript(config, crypto_components); + load_transcript_for_receivers_expecting_status( + config, + &transcript, + crypto_components, + Some(LoadTranscriptResult::SigningKeyAvailable), + ); + transcript +} + +fn load_transcript_for_receivers_expecting_status( + config: &NiDkgConfig, + transcript: &NiDkgTranscript, + crypto_components: &BTreeMap>, + expected_status: Option, +) { + for node_id in config.receivers().get() { + let result = crypto_for(*node_id, crypto_components).load_transcript(transcript); + + if result.is_err() { + panic!( + "failed to load transcript {} for node {}: {}", + transcript, + *node_id, + result.unwrap_err() + ); + } + + if let Some(expected_status) = expected_status { + let result = result.unwrap(); + assert_eq!(result, expected_status); + } + } +} + +fn random_node_in(nodes: &BTreeSet, rng: &mut R) -> NodeId { + *nodes.iter().choose(rng).expect("nodes empty") +} + +fn n_random_nodes_in( + nodes: &BTreeSet, + n: NumberOfNodes, + rng: &mut R, +) -> Vec { + let n_usize = usize::try_from(n.get()).expect("conversion to usize failed"); + let chosen = nodes.iter().copied().choose_multiple(rng, n_usize); + assert_eq!(chosen.len(), n_usize); + chosen +}