diff --git a/Cargo.lock b/Cargo.lock index 9215ed4213..b46e5422a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3958,6 +3958,7 @@ dependencies = [ "itertools", "ivalue-utils", "kademlia", + "key-manager", "libp2p", "log", "multihash", @@ -4097,6 +4098,7 @@ dependencies = [ "humantime-serde", "itertools", "json-utils", + "key-manager", "libp2p", "local-vm", "log", diff --git a/crates/key-manager/src/error.rs b/crates/key-manager/src/error.rs index ca3551e252..7b4424ad09 100644 --- a/crates/key-manager/src/error.rs +++ b/crates/key-manager/src/error.rs @@ -14,6 +14,7 @@ * limitations under the License. */ +use libp2p::PeerId; use std::path::PathBuf; use thiserror::Error; @@ -51,3 +52,9 @@ pub enum PersistedKeypairError { err: std::io::Error, }, } + +#[derive(Debug, Error)] +pub enum KeyManagerError { + #[error("Keypair for peer_id {0} not found")] + KeypairNotFound(PeerId), +} diff --git a/crates/key-manager/src/key_manager.rs b/crates/key-manager/src/key_manager.rs index f4989d890a..09bf50ca82 100644 --- a/crates/key-manager/src/key_manager.rs +++ b/crates/key-manager/src/key_manager.rs @@ -17,14 +17,17 @@ use fluence_keypair::{KeyFormat, KeyPair}; use libp2p::PeerId; use std::collections::HashMap; +use std::ops::Range; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; -use crate::error::PersistedKeypairError; +use crate::error::{KeyManagerError, PersistedKeypairError}; use crate::persistence::{load_persisted_keypairs, persist_keypair, PersistedKeypair}; use parking_lot::RwLock; +pub const INSECURE_KEYPAIR_SEED: Range = 0..32; + #[derive(Clone)] pub struct KeyManager { /// scope_peer_id -> scope_keypair @@ -33,6 +36,8 @@ pub struct KeyManager { scope_peer_ids: Arc>>, keypairs_dir: PathBuf, host_peer_id: PeerId, + // temporary public, will refactor + pub insecure_keypair: KeyPair, } impl KeyManager { @@ -42,6 +47,11 @@ impl KeyManager { scope_peer_ids: Arc::new(Default::default()), keypairs_dir, host_peer_id, + insecure_keypair: KeyPair::from_secret_key( + INSECURE_KEYPAIR_SEED.collect(), + KeyFormat::Ed25519, + ) + .expect("error creating insecure keypair"), }; this.load_persisted_keypairs(); @@ -105,12 +115,12 @@ impl KeyManager { } } - pub fn get_scope_keypair(&self, scope_peer_id: PeerId) -> eyre::Result { + pub fn get_scope_keypair(&self, scope_peer_id: PeerId) -> Result { self.scope_keypairs .read() .get(&scope_peer_id) .cloned() - .ok_or_else(|| eyre::eyre!("Keypair for peer id {} not found", scope_peer_id)) + .ok_or(KeyManagerError::KeypairNotFound(scope_peer_id)) } pub fn generate_keypair(&self) -> KeyPair { diff --git a/crates/key-manager/src/lib.rs b/crates/key-manager/src/lib.rs index 36676351e7..c260d9d448 100644 --- a/crates/key-manager/src/lib.rs +++ b/crates/key-manager/src/lib.rs @@ -4,4 +4,6 @@ mod error; mod key_manager; mod persistence; +pub use error::KeyManagerError; pub use key_manager::KeyManager; +pub use key_manager::INSECURE_KEYPAIR_SEED; diff --git a/crates/particle-node-tests/Cargo.toml b/crates/particle-node-tests/Cargo.toml index 6ae54b845e..1617a04293 100644 --- a/crates/particle-node-tests/Cargo.toml +++ b/crates/particle-node-tests/Cargo.toml @@ -31,6 +31,7 @@ script-storage = { workspace = true } aquamarine = { workspace = true } sorcerer = { workspace = true } fluence-keypair = { workspace = true } +key-manager = { workspace = true } fluence-libp2p = { workspace = true } humantime-serde = "1.1.1" diff --git a/crates/particle-node-tests/tests/builtin.rs b/crates/particle-node-tests/tests/builtin.rs index ee69f20b40..ae2af39bba 100644 --- a/crates/particle-node-tests/tests/builtin.rs +++ b/crates/particle-node-tests/tests/builtin.rs @@ -39,6 +39,7 @@ use created_swarm::{ use fluence_libp2p::RandomPeerId; use fluence_libp2p::Transport; use json_utils::into_array; +use key_manager::INSECURE_KEYPAIR_SEED; use now_millis::now_ms; use particle_protocol::Particle; use service_modules::load_module; @@ -1436,8 +1437,8 @@ fn sign_invalid_tetraplets() { client.receive_args().unwrap().as_slice() { assert!(host_error.contains(&format!("data is expected to be produced by service 'registry' on peer '{relay}', was from peer '{wrong_peer}'"))); - assert!(srv_error.contains("data is expected to result from a call to 'registry.get_record_bytes', was from 'op.identity'")); - assert!(func_error.contains("data is expected to result from a call to 'registry.get_record_bytes', was from 'registry.get_key_bytes'")); + assert!(srv_error.contains("data is expected to result from a call to 'registry.get_record_bytes' or 'registry.get_record_metadata_bytes', was from 'op.identity'")); + assert!(func_error.contains("data is expected to result from a call to 'registry.get_record_bytes' or 'registry.get_record_metadata_bytes', was from 'registry.get_key_bytes'")); } else { panic!("incorrect args: expected three arguments") } @@ -1631,7 +1632,7 @@ fn json_builtins() { #[test] fn insecure_sign_verify() { - let kp = KeyPair::from_secret_key((0..32).collect(), KeyFormat::Ed25519).unwrap(); + let kp = KeyPair::from_secret_key(INSECURE_KEYPAIR_SEED.collect(), KeyFormat::Ed25519).unwrap(); let swarms = make_swarms_with_builtins( 1, "tests/builtins/services".as_ref(), diff --git a/crates/particle-node-tests/tests/spells.rs b/crates/particle-node-tests/tests/spells.rs index 3574dcaf2c..b9b6cf6c2e 100644 --- a/crates/particle-node-tests/tests/spells.rs +++ b/crates/particle-node-tests/tests/spells.rs @@ -23,7 +23,7 @@ use maplit::hashmap; use serde_json::{json, Value as JValue}; use connected_client::ConnectedClient; -use created_swarm::make_swarms; +use created_swarm::{make_swarms, make_swarms_with_builtins}; use fluence_spell_dtos::trigger_config::TriggerConfig; use log_utils::enable_logs; use service_modules::load_module; @@ -1247,6 +1247,53 @@ fn resolve_global_alias() { } } +#[test] +fn worker_sig_test() { + let swarms = make_swarms_with_builtins(1, "tests/builtins/services".as_ref(), None, None); + + let mut client = ConnectedClient::connect_to(swarms[0].multiaddr.clone()) + .wrap_err("connect client") + .unwrap(); + + let script = format!( + r#" + (seq + (seq + (seq + (call %init_peer_id% ("registry" "get_record_bytes") ["key_id" "" [] [] 1 []] data) + (call %init_peer_id% ("sig" "get_peer_id") [] peer_id) + ) + (seq + (call %init_peer_id% ("sig" "sign") [data] sig_result) + (call %init_peer_id% ("sig" "verify") [sig_result.$.signature.[0]! data] result) + ) + ) + (call "{0}" ("op" "return") [sig_result result peer_id]) + ) + "#, + client.peer_id + ); + + let mut config = TriggerConfig::default(); + config.clock.period_sec = 2; + config.clock.start_sec = 1; + let (_, worker_id) = create_spell(&mut client, &script, config, json!({})); + + use serde_json::Value::Bool; + use serde_json::Value::Object; + use serde_json::Value::String; + + if let [Object(sig_result), Bool(result), String(peer_id)] = + client.receive_args().unwrap().as_slice() + { + assert!(sig_result["success"].as_bool().unwrap()); + assert!(result); + assert_eq!(*peer_id, worker_id); + } else { + panic!("incorrect args: expected two arguments") + } +} + #[test] fn spell_relay_id_test() { let swarms = make_swarms(1); diff --git a/particle-builtins/Cargo.toml b/particle-builtins/Cargo.toml index 43d2a017d2..a5883d39af 100644 --- a/particle-builtins/Cargo.toml +++ b/particle-builtins/Cargo.toml @@ -20,6 +20,7 @@ now-millis = { workspace = true } toml-utils = { workspace = true } peer-metrics = { workspace = true } uuid-utils = { workspace = true } +key-manager = { workspace = true } libp2p = { workspace = true } avm-server = { workspace = true } diff --git a/particle-builtins/src/builtins.rs b/particle-builtins/src/builtins.rs index aed8d0f0a9..184e362e75 100644 --- a/particle-builtins/src/builtins.rs +++ b/particle-builtins/src/builtins.rs @@ -23,7 +23,7 @@ use std::str::FromStr; use std::time::{Duration, Instant}; use derivative::Derivative; -use fluence_keypair::{KeyFormat, KeyPair, Signature}; +use fluence_keypair::{KeyPair, Signature}; use futures::stream::FuturesUnordered; use futures::StreamExt; use humantime_serde::re::humantime::format_duration as pretty; @@ -36,6 +36,7 @@ use JValue::Array; use connection_pool::{ConnectionPoolApi, ConnectionPoolT}; use kademlia::{KademliaApi, KademliaApiT}; +use key_manager::KeyManager; use now_millis::{now_ms, now_sec}; use particle_args::{from_base58, Args, ArgsError, JError}; use particle_execution::{FunctionOutcome, ParticleParams, ServiceFunction}; @@ -69,6 +70,7 @@ pub struct Builtins { pub connectivity: C, pub script_storage: ScriptStorageApi, + // TODO: move all peer ids and keypairs to key manager pub management_peer_id: PeerId, pub builtins_management_peer_id: PeerId, pub local_peer_id: PeerId, @@ -85,7 +87,7 @@ pub struct Builtins { particles_vault_dir: path::PathBuf, #[derivative(Debug = "ignore")] - insecure_keypair: KeyPair, + key_manager: KeyManager, } impl Builtins @@ -99,6 +101,7 @@ where config: ServicesConfig, services_metrics: ServicesMetrics, root_keypair: KeyPair, + key_manager: KeyManager, ) -> Self { let modules_dir = &config.modules_dir; let blueprint_dir = &config.blueprint_dir; @@ -128,8 +131,7 @@ where node_info, particles_vault_dir, custom_services: <_>::default(), - insecure_keypair: KeyPair::from_secret_key((0..32).collect(), KeyFormat::Ed25519) - .expect("error creating insecure keypair"), + key_manager, } } @@ -251,9 +253,9 @@ where ("array", "slice") => wrap(self.array_slice(args.function_args)), ("array", "length") => wrap(self.array_length(args.function_args)), - ("sig", "sign") => wrap(self.sign(args)), - ("sig", "verify") => wrap(self.verify(args)), - ("sig", "get_peer_id") => wrap(self.get_peer_id()), + ("sig", "sign") => wrap(self.sign(args, particle)), + ("sig", "verify") => wrap(self.verify(args, particle)), + ("sig", "get_peer_id") => wrap(self.get_peer_id(particle)), ("insecure_sig", "sign") => wrap(self.insecure_sign(args)), ("insecure_sig", "verify") => wrap(self.insecure_verify(args)), @@ -269,7 +271,7 @@ where ("run-console", "print") => wrap_unit(Ok(log::debug!(target: "run-console", "{}", json!(args.function_args)))), - _ => FunctionOutcome::NotDefined { args, params: particle }, + _ => FunctionOutcome::NotDefined { args, params: particle }, } } @@ -659,7 +661,7 @@ where if start > end || end > array.len() { return Err(JError::new(format!( - "slice indexes out of bounds. start index: {:?}, end index: {:?}, array length: {:?}", + "slice indexes out of bounds. start index: {:?}, end index: {:?}, array length: {:?}", start, end, array.len()) )); } @@ -886,7 +888,7 @@ where } } - fn sign(&self, args: Args) -> Result { + fn sign(&self, args: Args, params: ParticleParams) -> Result { let tetraplets = args.tetraplets; let mut args = args.function_args.into_iter(); let result: Result = try { @@ -894,18 +896,24 @@ where let tetraplet = tetraplets.get(0).map(|v| v.as_slice()); if let Some([t]) = tetraplet { - if t.peer_pk != self.local_peer_id.to_base58() { + if t.peer_pk != self.local_peer_id.to_base58() + && !self + .key_manager + .is_scope_peer_id(PeerId::from_str(&t.peer_pk)?) + { return Err(JError::new(format!( "data is expected to be produced by service 'registry' on peer '{}', was from peer '{}'", self.local_peer_id, t.peer_pk ))); } - if (t.service_id.as_str(), t.function_name.as_str()) - != ("registry", "get_record_bytes") - { + let duplet = (t.service_id.as_str(), t.function_name.as_str()); + let metadata_bytes = ("registry", "get_record_metadata_bytes"); + let record_bytes = ("registry", "get_record_bytes"); + + if duplet != record_bytes && duplet != metadata_bytes { return Err(JError::new(format!( - "data is expected to result from a call to 'registry.get_record_bytes', was from '{}.{}'", + "data is expected to result from a call to 'registry.get_record_bytes' or 'registry.get_record_metadata_bytes', was from '{}.{}'", t.service_id, t.function_name ))); } @@ -919,7 +927,13 @@ where return Err(JError::new(format!("expected tetraplet for a scalar argument, got tetraplet for an array: {tetraplet:?}, tetraplets"))); } - json!(self.root_keypair.sign(&data)?.to_vec()) + if params.host_id == self.local_peer_id { + json!(self.root_keypair.sign(&data)?.to_vec()) + } else { + // if this call is initiated by the worker on this worker as host_id and init_peer_id + let keypair = self.key_manager.get_scope_keypair(params.init_peer_id)?; + json!(keypair.sign(&data)?.to_vec()) + } }; match result { @@ -937,27 +951,46 @@ where } } - fn verify(&self, args: Args) -> Result { + fn verify(&self, args: Args, params: ParticleParams) -> Result { let mut args = args.function_args.into_iter(); let signature: Vec = Args::next("signature", &mut args)?; let data: Vec = Args::next("data", &mut args)?; let signature = Signature::from_bytes(self.root_keypair.public().get_key_format(), signature); - Ok(JValue::Bool( - self.root_keypair.public().verify(&data, &signature).is_ok(), - )) + // TODO: move root_keypair to key_manager and unify verification + if params.host_id == self.local_peer_id { + Ok(JValue::Bool( + self.root_keypair.public().verify(&data, &signature).is_ok(), + )) + } else { + Ok(JValue::Bool( + self.key_manager + .get_scope_keypair(params.host_id)? + .public() + .verify(&data, &signature) + .is_ok(), + )) + } } - fn get_peer_id(&self) -> Result { - Ok(JValue::String(self.root_keypair.get_peer_id().to_base58())) + fn get_peer_id(&self, params: ParticleParams) -> Result { + if params.host_id == self.local_peer_id { + Ok(JValue::String(self.root_keypair.get_peer_id().to_base58())) + } else { + Ok(JValue::String( + self.key_manager + .get_scope_peer_id(params.init_peer_id)? + .to_base58(), + )) + } } fn insecure_sign(&self, args: Args) -> Result { let mut args = args.function_args.into_iter(); let result: Result = try { let data: Vec = Args::next("data", &mut args)?; - json!(self.insecure_keypair.sign(&data)?.to_vec()) + json!(self.key_manager.insecure_keypair.sign(&data)?.to_vec()) }; match result { @@ -979,11 +1012,14 @@ where let mut args = args.function_args.into_iter(); let signature: Vec = Args::next("signature", &mut args)?; let data: Vec = Args::next("data", &mut args)?; - let signature = - Signature::from_bytes(self.insecure_keypair.public().get_key_format(), signature); + let signature = Signature::from_bytes( + self.key_manager.insecure_keypair.public().get_key_format(), + signature, + ); Ok(JValue::Bool( - self.insecure_keypair + self.key_manager + .insecure_keypair .public() .verify(&data, &signature) .is_ok(), @@ -992,7 +1028,7 @@ where fn insecure_get_peer_id(&self) -> Result { Ok(JValue::String( - self.insecure_keypair.get_peer_id().to_base58(), + self.key_manager.insecure_keypair.get_peer_id().to_base58(), )) } } diff --git a/particle-node/src/node.rs b/particle-node/src/node.rs index 2ee913f204..f344db5db9 100644 --- a/particle-node/src/node.rs +++ b/particle-node/src/node.rs @@ -202,6 +202,7 @@ impl Node { script_storage_api, services_metrics, config.node_config.root_key_pair.clone(), + key_manager.clone(), )); let (effects_out, effects_in) = unbounded(); @@ -317,6 +318,7 @@ impl Node { script_storage_api: ScriptStorageApi, services_metrics: ServicesMetrics, root_keypair: KeyPair, + key_manager: KeyManager, ) -> Builtins { let node_info = NodeInfo { external_addresses, @@ -331,6 +333,7 @@ impl Node { services_config, services_metrics, root_keypair, + key_manager, ) } } diff --git a/particle-services/src/app_services.rs b/particle-services/src/app_services.rs index 101f85cd75..a483198da5 100644 --- a/particle-services/src/app_services.rs +++ b/particle-services/src/app_services.rs @@ -101,6 +101,7 @@ pub struct ParticleAppServices { services: Arc>, modules: ModuleRepository, aliases: Arc>, + // TODO: move these peer ids to key manager management_peer_id: PeerId, builtins_management_peer_id: PeerId, pub metrics: Option, diff --git a/sorcerer/src/error.rs b/sorcerer/src/error.rs index 5d8d8f4311..19e1a96bcb 100644 --- a/sorcerer/src/error.rs +++ b/sorcerer/src/error.rs @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +use key_manager::KeyManagerError; use particle_protocol::ParticleError; use thiserror::Error; @@ -27,7 +28,7 @@ pub enum SorcererError { #[error("Keypair for spell {spell_id} is missing: {err}")] ScopeKeypairMissing { #[source] - err: eyre::Report, + err: KeyManagerError, spell_id: String, }, }