diff --git a/.github/workflows/backend-rust.yml b/.github/workflows/backend-rust.yml index cf449d13..27c2707d 100644 --- a/.github/workflows/backend-rust.yml +++ b/.github/workflows/backend-rust.yml @@ -25,7 +25,7 @@ jobs: run: | set -eExuo pipefail export CARGO_TERM_COLOR=always # ensure output has colors - cargo build --release --target wasm32-unknown-unknown -p ic-vetkeys-manager-canister -p ic-vetkeys-encrypted-maps-canister + cargo build --release --target wasm32-unknown-unknown -p ic-vetkeys-manager-canister -p ic-vetkeys-encrypted-maps-canister -p ic-vetkeys-canisters-tests cargo test cargo test --doc cargo-test-backend-darwin: @@ -39,6 +39,6 @@ jobs: run: | set -eExuo pipefail export CARGO_TERM_COLOR=always # ensure output has colors - cargo build --release --target wasm32-unknown-unknown -p ic-vetkeys-manager-canister -p ic-vetkeys-encrypted-maps-canister + cargo build --release --target wasm32-unknown-unknown -p ic-vetkeys-manager-canister -p ic-vetkeys-encrypted-maps-canister -p ic-vetkeys-canisters-tests cargo test cargo test --doc diff --git a/Cargo.lock b/Cargo.lock index bdb70b1b..9c9507c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1312,6 +1312,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ic-vetkeys-canisters-tests" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk", + "ic-cdk-macros", + "ic-dummy-getrandom-for-wasm", + "ic-vetkeys", + "ic-vetkeys-test-utils", + "pocket-ic", + "rand 0.8.5", + "serde", +] + [[package]] name = "ic-vetkeys-encrypted-maps-canister" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 790f2463..38b1e9be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "backend/rs/ic_vetkeys_test_utils", "backend/rs/canisters/ic_vetkeys_encrypted_maps_canister", "backend/rs/canisters/ic_vetkeys_manager_canister", + "backend/rs/canisters/tests", "examples/basic_ibe/backend", "examples/basic_timelock_ibe/backend", "examples/password_manager_with_metadata/backend" diff --git a/backend/rs/canisters/tests/Cargo.toml b/backend/rs/canisters/tests/Cargo.toml new file mode 100644 index 00000000..b7fa2e6b --- /dev/null +++ b/backend/rs/canisters/tests/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ic-vetkeys-canisters-tests" +authors.workspace = true +description.workspace = true +documentation.workspace = true +edition.workspace = true +version.workspace = true +license.workspace = true + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +candid = { workspace = true } +ic-cdk = { workspace = true } +ic-cdk-macros = { workspace = true } +ic-dummy-getrandom-for-wasm = { workspace = true } +ic-vetkeys = { path = "../../ic_vetkeys" } +serde = { workspace = true } + +[dev-dependencies] +ic-vetkeys-test-utils = { path = "../../ic_vetkeys_test_utils" } +pocket-ic = { workspace = true } +rand = { workspace = true } \ No newline at end of file diff --git a/backend/rs/canisters/tests/Makefile b/backend/rs/canisters/tests/Makefile new file mode 100644 index 00000000..dbd9c3fb --- /dev/null +++ b/backend/rs/canisters/tests/Makefile @@ -0,0 +1,9 @@ +.PHONY: build +.SILENT: build +build: + cargo build --release --target wasm32-unknown-unknown + +.PHONY: test +.SILENT: test +test: build + cargo test diff --git a/backend/rs/canisters/tests/README.md b/backend/rs/canisters/tests/README.md new file mode 100644 index 00000000..c1603d89 --- /dev/null +++ b/backend/rs/canisters/tests/README.md @@ -0,0 +1,5 @@ +# Canister tests + +Currently, we test: +* `ic_vetkeys::management_canister::sign_with_bls` +* `ic_vetkeys::management_canister::bls_public_key` \ No newline at end of file diff --git a/backend/rs/canisters/tests/src/lib.rs b/backend/rs/canisters/tests/src/lib.rs new file mode 100644 index 00000000..35952877 --- /dev/null +++ b/backend/rs/canisters/tests/src/lib.rs @@ -0,0 +1,64 @@ +use ic_cdk::update; +use ic_vetkeys::vetkd_api_types::{ + VetKDDeriveKeyReply, VetKDDeriveKeyRequest, VetKDKeyId, VetKDPublicKeyReply, + VetKDPublicKeyRequest, +}; + +#[update] +async fn sign_with_bls(input: Vec, context: Vec, key_id: VetKDKeyId) -> Vec { + ic_vetkeys::management_canister::sign_with_bls(input, context, key_id) + .await + .expect("sign_with_bls call failed") +} + +#[update] +async fn bls_public_key(context: Vec, key_id: VetKDKeyId) -> Vec { + ic_vetkeys::management_canister::bls_public_key(None, context, key_id) + .await + .expect("bls_public_key call failed") +} + +#[update] +async fn vetkd_derive_key( + input: Vec, + context: Vec, + key_id: VetKDKeyId, + transport_public_key: Vec, +) -> Vec { + let request = VetKDDeriveKeyRequest { + input, + context, + key_id, + transport_public_key, + }; + + let reply: (VetKDDeriveKeyReply,) = ic_cdk::api::call::call_with_payment128( + candid::Principal::management_canister(), + "vetkd_derive_key", + (request,), + 26_153_846_153, + ) + .await + .expect("vetkd_derive_key call failed"); + + reply.0.encrypted_key +} + +#[update] +async fn vetkd_public_key(context: Vec, key_id: VetKDKeyId) -> Vec { + let request = VetKDPublicKeyRequest { + canister_id: None, + context, + key_id, + }; + + let reply: (VetKDPublicKeyReply,) = ic_cdk::api::call::call( + candid::Principal::management_canister(), + "vetkd_public_key", + (request,), + ) + .await + .expect("vetkd_public_key call failed"); + + reply.0.public_key +} diff --git a/backend/rs/canisters/tests/tests/sign_with_bls.rs b/backend/rs/canisters/tests/tests/sign_with_bls.rs new file mode 100644 index 00000000..571a22ad --- /dev/null +++ b/backend/rs/canisters/tests/tests/sign_with_bls.rs @@ -0,0 +1,145 @@ +use candid::{decode_one, encode_args, CandidType, Principal}; +use ic_vetkeys::vetkd_api_types::{VetKDCurve, VetKDKeyId}; +use ic_vetkeys::{verify_bls_signature, DerivedPublicKey, EncryptedVetKey, TransportSecretKey}; +use ic_vetkeys_test_utils::{git_root_dir, reproducible_rng}; +use pocket_ic::{PocketIc, PocketIcBuilder}; +use rand::{CryptoRng, Rng}; +use std::path::Path; + +#[test] +fn bls_signature_should_be_valid_and_equal_to_decrypted_vetkey() { + let rng = &mut reproducible_rng(); + let env = TestEnvironment::new(); + let input = random_bytes(rng, 10); + let context = random_bytes(rng, 10); + let key_id = VetKDKeyId { + curve: VetKDCurve::Bls12_381_G2, + name: "dfx_test_key".to_string(), + }; + let transport_secret_key = random_transport_key(rng); + let transport_public_key = transport_secret_key.public_key(); + + let bls_signature: Vec = env.update( + Principal::anonymous(), + "sign_with_bls", + encode_args((input.clone(), context.clone(), key_id.clone())).unwrap(), + ); + + let verification_key: Vec = env.update( + Principal::anonymous(), + "vetkd_public_key", + encode_args((context.clone(), key_id.clone())).unwrap(), + ); + let encrypted_vetkey_bytes: Vec = env.update( + Principal::anonymous(), + "vetkd_derive_key", + encode_args((input.clone(), context, key_id, transport_public_key)).unwrap(), + ); + let encrypted_vetkey = EncryptedVetKey::deserialize(encrypted_vetkey_bytes.as_ref()).unwrap(); + let derived_public_key = DerivedPublicKey::deserialize(verification_key.as_ref()).unwrap(); + let decrypted_vetkey = encrypted_vetkey + .decrypt_and_verify(&transport_secret_key, &derived_public_key, &input) + .unwrap(); + + assert_eq!(bls_signature, decrypted_vetkey.signature_bytes().to_vec()); + assert!(verify_bls_signature( + &derived_public_key, + &input, + &bls_signature + )); +} + +#[test] +fn bls_public_key_should_be_equal_to_verification_key() { + let rng = &mut reproducible_rng(); + let env = TestEnvironment::new(); + let context = random_bytes(rng, 10); + let key_id = VetKDKeyId { + curve: VetKDCurve::Bls12_381_G2, + name: "dfx_test_key".to_string(), + }; + let bls_public_key: Vec = env.update( + Principal::anonymous(), + "bls_public_key", + encode_args((context.clone(), key_id.clone())).unwrap(), + ); + let verification_key: Vec = env.update( + Principal::anonymous(), + "vetkd_public_key", + encode_args((context.clone(), key_id.clone())).unwrap(), + ); + assert_eq!(bls_public_key, verification_key); +} +struct TestEnvironment { + pic: PocketIc, + canister_id: Principal, +} + +impl TestEnvironment { + fn new() -> Self { + let pic = PocketIcBuilder::new() + .with_application_subnet() + .with_ii_subnet() + .with_fiduciary_subnet() + .with_nonmainnet_features(true) + .build(); + + let canister_id = pic.create_canister(); + pic.add_cycles(canister_id, 2_000_000_000_000); + + let wasm_bytes = load_canister_wasm(); + pic.install_canister(canister_id, wasm_bytes, vec![], None); + + // Make sure the canister is properly initialized + fast_forward(&pic, 5); + + Self { pic, canister_id } + } + + fn update candid::Deserialize<'de>>( + &self, + caller: Principal, + method_name: &str, + args: Vec, + ) -> T { + let reply = self + .pic + .update_call(self.canister_id, caller, method_name, args); + match reply { + Ok(data) => decode_one(&data).expect("failed to decode reply"), + Err(user_error) => panic!("canister returned a user error: {user_error}"), + } + } +} + +fn load_canister_wasm() -> Vec { + let wasm_path_string = match std::env::var("CUSTOM_WASM_PATH") { + Ok(path) if !path.is_empty() => path, + _ => format!( + "{}/target/wasm32-unknown-unknown/release/ic_vetkeys_canisters_tests.wasm", + git_root_dir() + ), + }; + let wasm_path = Path::new(&wasm_path_string); + std::fs::read(wasm_path) + .expect("wasm does not exist - run `cargo build --release --target wasm32-unknown-unknown`") +} + +fn random_transport_key(rng: &mut R) -> TransportSecretKey { + let mut seed = vec![0u8; 32]; + rng.fill_bytes(&mut seed); + TransportSecretKey::from_seed(seed).unwrap() +} + +fn random_bytes(rng: &mut R, max_length: usize) -> Vec { + let length = rng.gen_range(0..max_length); + let mut bytes = vec![0u8; length]; + rng.fill_bytes(&mut bytes); + bytes +} + +fn fast_forward(ic: &PocketIc, ticks: u64) { + for _ in 0..ticks - 1 { + ic.tick(); + } +} diff --git a/backend/rs/ic_vetkeys/src/types.rs b/backend/rs/ic_vetkeys/src/types.rs index 36604278..c3bacebc 100644 --- a/backend/rs/ic_vetkeys/src/types.rs +++ b/backend/rs/ic_vetkeys/src/types.rs @@ -7,6 +7,8 @@ use ic_stable_structures::{ }; use serde::{Deserialize, Serialize}; +pub type CanisterId = candid::Principal; + pub type KeyName = Blob<32>; pub type MapName = KeyName; pub type MapId = KeyId; diff --git a/backend/rs/ic_vetkeys/src/utils/mod.rs b/backend/rs/ic_vetkeys/src/utils/mod.rs index 59157aa0..93c0813c 100644 --- a/backend/rs/ic_vetkeys/src/utils/mod.rs +++ b/backend/rs/ic_vetkeys/src/utils/mod.rs @@ -10,12 +10,15 @@ use ic_bls12_381::{ hash_to_curve::{ExpandMsgXmd, HashToCurve}, G1Affine, G1Projective, G2Affine, G2Prepared, Gt, Scalar, }; +use ic_cdk::api::call::RejectionCode; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use std::array::TryFromSliceError; use std::ops::Neg; use zeroize::{Zeroize, ZeroizeOnDrop}; +use crate::vetkd_api_types::{VetKDCurve, VetKDDeriveKeyReply, VetKDDeriveKeyRequest, VetKDKeyId}; + lazy_static::lazy_static! { static ref G2PREPARED_NEG_G : G2Prepared = G2Affine::generator().neg().into(); } @@ -328,6 +331,9 @@ impl EncryptedVetKey { /// The length of the serialized encoding of this type const BYTES: usize = 2 * G1AFFINE_BYTES + G2AFFINE_BYTES; + const C2_OFFSET: usize = G1AFFINE_BYTES; + const C3_OFFSET: usize = G1AFFINE_BYTES + G2AFFINE_BYTES; + /// Decrypts and verifies the VetKey pub fn decrypt_and_verify( &self, @@ -373,16 +379,13 @@ impl EncryptedVetKey { pub fn deserialize_array( val: &[u8; Self::BYTES], ) -> Result { - let c2_start = G1AFFINE_BYTES; - let c3_start = G1AFFINE_BYTES + G2AFFINE_BYTES; - - let c1_bytes: &[u8; G1AFFINE_BYTES] = &val[..c2_start] + let c1_bytes: &[u8; G1AFFINE_BYTES] = &val[..Self::C2_OFFSET] .try_into() .map_err(|_e| EncryptedVetKeyDeserializationError::InvalidEncryptedVetKey)?; - let c2_bytes: &[u8; G2AFFINE_BYTES] = &val[c2_start..c3_start] + let c2_bytes: &[u8; G2AFFINE_BYTES] = &val[Self::C2_OFFSET..Self::C3_OFFSET] .try_into() .map_err(|_e| EncryptedVetKeyDeserializationError::InvalidEncryptedVetKey)?; - let c3_bytes: &[u8; G1AFFINE_BYTES] = &val[c3_start..] + let c3_bytes: &[u8; G1AFFINE_BYTES] = &val[Self::C3_OFFSET..] .try_into() .map_err(|_e| EncryptedVetKeyDeserializationError::InvalidEncryptedVetKey)?; @@ -749,3 +752,137 @@ fn deserialize_g2(bytes: &[u8]) -> Result { Err("Invalid G2 elliptic curve point".to_string()) } } + +/// This module contains functions for calling the ICP management canister's `vetkd_derive_key` endpoint from within a canister. +pub mod management_canister { + use crate::{ + types::CanisterId, + vetkd_api_types::{VetKDPublicKeyReply, VetKDPublicKeyRequest}, + }; + + use super::*; + + /// Derives a vetKey that is public to the canister and ICP nodes. + /// This function is useful if vetKeys are supposed to be decrypted by the canister itself, e.g., when vetKeys are used as BLS signatures, for timelock encryption, or for producing verifiable randomness. + /// + /// **Warning**: A vetKey produced by this function is *insecure* to use as a private key by a user. + /// + /// A public vetKey is derived by calling the ICP management canister's `vetkd_derive_key` endpoint with a **fixed public transport key** that produces an **unencrypted vetKey**. + /// Therefore, this function is more efficient than actually retrieving the encrypted vetKey and calling [`EncryptedVetKey::decrypt_and_verify`]. + /// + /// # Arguments + /// * `input` - corresponds to `input` in `vetkd_derive_key` + /// * `context` - corresponds to `context` in `vetkd_derive_key` + /// * `key_id` - corresponds to `key_id` in `vetkd_derive_key` + /// + /// # Returns + /// * `Ok(VetKey)` - The derived vetKey on success + /// * `Err(DeriveUnencryptedVetkeyError)` - If derivation fails due to unsupported curve or canister call error + async fn derive_public_vetkey( + input: Vec, + context: Vec, + key_id: VetKDKeyId, + ) -> Result, VetKDDeriveKeyCallError> { + if key_id.curve != VetKDCurve::Bls12_381_G2 { + return Err(VetKDDeriveKeyCallError::UnsupportedCurve); + } + + let request = VetKDDeriveKeyRequest { + input, + context, + key_id, + // Encryption with the G1 identity element produces unencrypted vetKeys + transport_public_key: G1Affine::identity().to_compressed().to_vec(), + }; + + let reply: (VetKDDeriveKeyReply,) = + ic_cdk::api::call::call_with_payment128::<_, (VetKDDeriveKeyReply,)>( + candid::Principal::management_canister(), + "vetkd_derive_key", + (request,), + 26_153_846_153, + ) + .await + .map_err(VetKDDeriveKeyCallError::CallFailed)?; + + if reply.0.encrypted_key.len() != EncryptedVetKey::BYTES { + return Err(VetKDDeriveKeyCallError::InvalidReply); + } + + Ok(reply.0.encrypted_key + [EncryptedVetKey::C3_OFFSET..EncryptedVetKey::C3_OFFSET + G1AFFINE_BYTES] + .to_vec()) + } + + #[derive(Debug, Eq, PartialEq)] + /// Errors that can occur when deriving an unencrypted vetKey + pub enum VetKDDeriveKeyCallError { + /// The curve is currently not supported + UnsupportedCurve, + /// The canister call failed + CallFailed((RejectionCode, String)), + /// Invalid reply from the management canister + InvalidReply, + } + + /// Creates a threshold BLS12-381 signature for the given `message`. + /// + /// The `context` parameter defines signer's identity. + /// The returned signature can be verified with the public key retrieved via [`bls_public_key`] with the same `context` and `key_id`. + /// Having the public key, message, and signature, we now can verify that the signature is valid. + /// For that, we can call [`verify_bls_signature`] from this crate in Rust or `verifyBlsSignature` from the `@dfinity/vetkeys` package in TypeScript/JavaScript. + /// + /// This function internally calls the `vetkd_derive_key` method of the Internet Computer, which requires additional cycles to be attached in order to be successful. + /// The amount of the required cycles depends on the size of the subnet that holds the vetKD master key (defined by `key_id`). + /// Currently, this function attaches to the call `26_153_846_153` cycles, which is the expected maximum of what is needed. + /// The unused cycles are refunded after the call. + /// In the future, this function will call `ic0_cost_vetkd_derive_key` for a more precise cost calculation. + /// + /// # Arguments + /// * `message` - the message to be signed + /// * `context` - the identity of the signer + /// * `key_id` - the key ID of the threshold key deployed on the Internet Computer + /// + /// # Returns + /// * `Ok(Vec)` - The signature on success + /// * `Err(VetKDDeriveKeyCallError)` - If derivation fails due to unsupported curve or canister call error + pub async fn sign_with_bls( + message: Vec, + context: Vec, + key_id: VetKDKeyId, + ) -> Result, VetKDDeriveKeyCallError> { + derive_public_vetkey(message, context, key_id).await + } + + /// Returns the public key of a threshold BLS12-381 key. + /// Signatures produced with [`sign_with_bls`] are verifiable under a public key returned by this method iff the public key is for the correct `canister_id` and the same `context` and `key_id` was used. + /// + /// # Arguments + /// * `canister_id` - the canister ID that the public key is computed for. If `canister_id` is `None`, it will default to the canister id of the caller. + /// * `context` - the identity of the signer + /// * `key_id` - the key ID of the threshold key deployed on the Internet Computer + /// + /// # Returns + /// * `Ok(Vec)` - The public key on success + /// * `Err((RejectionCode, String))` - If the canister call fails + pub async fn bls_public_key( + canister_id: Option, + context: Vec, + key_id: VetKDKeyId, + ) -> Result, (RejectionCode, String)> { + let request = VetKDPublicKeyRequest { + canister_id, + context, + key_id, + }; + + let reply: (VetKDPublicKeyReply,) = ic_cdk::api::call::call( + candid::Principal::management_canister(), + "vetkd_public_key", + (request,), + ) + .await?; + + Ok(reply.0.public_key) + } +} diff --git a/backend/rs/ic_vetkeys/src/vetkd_api_types.rs b/backend/rs/ic_vetkeys/src/vetkd_api_types.rs index 59e236eb..a50f600a 100644 --- a/backend/rs/ic_vetkeys/src/vetkd_api_types.rs +++ b/backend/rs/ic_vetkeys/src/vetkd_api_types.rs @@ -2,7 +2,7 @@ use candid::CandidType; use ic_cdk::api::management_canister::main::CanisterId; use serde::{Deserialize, Serialize}; -#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum VetKDCurve { #[serde(rename = "bls12_381_g2")] #[allow(non_camel_case_types)]