From a98145fc9039bbfd3ea402366bbbafff93133908 Mon Sep 17 00:00:00 2001 From: Davide Baldo Date: Tue, 22 Oct 2024 18:45:15 +0200 Subject: [PATCH 1/7] feat(rust): implemented key rotation and rekeying for the kafka use case --- .../src/kafka/key_exchange/controller.rs | 399 ++++++++++++++++-- .../ockam_api/src/kafka/key_exchange/mod.rs | 13 +- .../src/kafka/key_exchange/secure_channels.rs | 310 ++++++++------ .../src/kafka/protocol_aware/inlet/request.rs | 26 +- .../kafka/protocol_aware/inlet/response.rs | 2 + .../src/kafka/protocol_aware/inlet/tests.rs | 5 +- .../ockam_api/src/kafka/protocol_aware/mod.rs | 4 +- .../src/kafka/protocol_aware/tests.rs | 9 +- .../src/kafka/tests/integration_test.rs | 12 +- .../src/kafka/tests/interceptor_test.rs | 37 +- .../src/nodes/service/secure_channel.rs | 2 +- .../ockam/ockam_api/src/test_utils/mod.rs | 148 +++++-- .../rust/ockam/ockam_api/tests/latency.rs | 27 +- .../rust/ockam/ockam_api/tests/portals.rs | 53 +-- .../rust/ockam/ockam_core/src/compat.rs | 61 +++ .../ockam_identity/src/secure_channel/api.rs | 9 +- .../src/secure_channel/decryptor.rs | 118 +++++- .../src/secure_channel/encryptor.rs | 35 +- .../src/secure_channel/encryptor_worker.rs | 58 ++- .../src/secure_channel/key_tracker.rs | 4 + .../ockam/ockam_identity/tests/channel.rs | 14 +- .../ockam/ockam_identity/tests/persistence.rs | 32 +- .../rust/ockam/ockam_vault/src/error.rs | 3 + .../vault_for_secure_channels.rs | 32 ++ .../src/traits/vault_for_secure_channels.rs | 8 + 25 files changed, 1036 insertions(+), 385 deletions(-) diff --git a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs index 5fcd4c46229..65c1323f394 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs @@ -1,17 +1,20 @@ -use crate::kafka::key_exchange::{KafkaKeyExchangeController, TopicPartition}; +use crate::kafka::key_exchange::KafkaKeyExchangeController; use crate::kafka::protocol_aware::KafkaEncryptedContent; use crate::kafka::{ConsumerPublishing, ConsumerResolution}; use crate::nodes::models::relay::ReturnTiming; use crate::nodes::NodeManager; use ockam::identity::{ - DecryptionRequest, DecryptionResponse, EncryptionRequest, EncryptionResponse, Identifier, - SecureChannels, + utils, DecryptionRequest, DecryptionResponse, EncryptionRequest, EncryptionResponse, + SecureChannels, TimestampInSeconds, }; use ockam_abac::PolicyAccessControl; +use ockam_core::compat::clock::{Clock, ProductionClock}; use ockam_core::compat::collections::{HashMap, HashSet}; -use ockam_core::{async_trait, route, Address}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{async_trait, route, Address, Error}; use ockam_node::Context; -use std::sync::Arc; +use std::sync::{Arc, Weak}; +use time::Duration; use tokio::sync::Mutex; #[derive(Clone)] @@ -25,35 +28,28 @@ impl KafkaKeyExchangeController for KafkaKeyExchangeControllerImpl { &self, context: &mut Context, topic_name: &str, - partition_index: i32, content: Vec, ) -> ockam_core::Result { - let secure_channel_entry = self - .get_or_create_secure_channel(context, topic_name, partition_index) - .await?; - - let consumer_decryptor_address = secure_channel_entry.their_decryptor_address(); - - trace!("encrypting content with {consumer_decryptor_address}"); + let topic_key_handler = self.get_or_exchange_key(context, topic_name).await?; let encryption_response: EncryptionResponse = context .send_and_receive( - route![secure_channel_entry.encryptor_api_address().clone()], - EncryptionRequest(content), + route![topic_key_handler.encryptor_api_address.clone()], + EncryptionRequest::Encrypt(content), ) .await?; let encrypted_content = match encryption_response { EncryptionResponse::Ok(p) => p, EncryptionResponse::Err(cause) => { - warn!("cannot encrypt kafka message"); + warn!("Cannot encrypt kafka message"); return Err(cause); } }; - trace!("encrypted content with {consumer_decryptor_address}"); Ok(KafkaEncryptedContent { content: encrypted_content, - consumer_decryptor_address, + consumer_decryptor_address: topic_key_handler.consumer_decryptor_address, + rekey_counter: topic_key_handler.rekey_counter, }) } @@ -61,6 +57,7 @@ impl KafkaKeyExchangeController for KafkaKeyExchangeControllerImpl { &self, context: &mut Context, consumer_decryptor_address: &Address, + rekey_counter: u16, encrypted_content: Vec, ) -> ockam_core::Result> { let secure_channel_decryptor_api_address = self @@ -73,7 +70,7 @@ impl KafkaKeyExchangeController for KafkaKeyExchangeControllerImpl { let decrypt_response = context .send_and_receive( route![secure_channel_decryptor_api_address], - DecryptionRequest(encrypted_content), + DecryptionRequest(encrypted_content, Some(rekey_counter)), ) .await?; @@ -92,21 +89,19 @@ impl KafkaKeyExchangeController for KafkaKeyExchangeControllerImpl { &self, context: &mut Context, topic_name: &str, - partitions: Vec, ) -> ockam_core::Result<()> { let mut inner = self.inner.lock().await; match inner.consumer_publishing.clone() { ConsumerPublishing::None => {} ConsumerPublishing::Relay(where_to_publish) => { - for partition in partitions { - let topic_key: TopicPartition = (topic_name.to_string(), partition); - if inner.topic_relay_set.contains(&topic_key) { - continue; - } - let alias = format!("consumer_{topic_name}_{partition}"); - let relay_info = inner - .node_manager + if inner.topic_relay_set.contains(topic_name) { + return Ok(()); + } + let alias = format!("consumer_{topic_name}"); + + if let Some(node_manager) = inner.node_manager.upgrade() { + let relay_info = node_manager .create_relay( context, &where_to_publish.clone(), @@ -116,9 +111,8 @@ impl KafkaKeyExchangeController for KafkaKeyExchangeControllerImpl { ReturnTiming::AfterConnection, ) .await?; - trace!("remote relay created: {relay_info:?}"); - inner.topic_relay_set.insert(topic_key); + inner.topic_relay_set.insert(topic_name.to_string()); } } } @@ -127,20 +121,119 @@ impl KafkaKeyExchangeController for KafkaKeyExchangeControllerImpl { } } +#[derive(Debug, PartialEq)] +pub(crate) struct TopicEncryptionKey { + pub(crate) rekey_counter: u16, + pub(crate) encryptor_api_address: Address, + pub(crate) consumer_decryptor_address: Address, +} + +const ROTATION_RETRY_DELAY: Duration = Duration::minutes(5); +pub(crate) struct TopicEncryptionKeyState { + pub(crate) producer_encryptor_address: Address, + pub(crate) valid_until: TimestampInSeconds, + pub(crate) rotate_after: TimestampInSeconds, + pub(crate) last_rekey: TimestampInSeconds, + pub(crate) rekey_counter: u16, + pub(crate) rekey_period: Duration, + pub(crate) last_rotation_attempt: TimestampInSeconds, +} + +pub(crate) enum RequiredOperation { + Rekey, + ShouldRotate, + MustRotate, + None, +} + +impl TopicEncryptionKeyState { + /// Return the operation that should be performed on the key before using it + pub(crate) fn operation( + &self, + now: TimestampInSeconds, + ) -> ockam_core::Result { + if now >= self.valid_until { + return Ok(RequiredOperation::MustRotate); + } + + if now >= self.rotate_after + && now >= self.last_rotation_attempt + ROTATION_RETRY_DELAY.whole_seconds() as u64 + { + return Ok(RequiredOperation::ShouldRotate); + } + + if now >= self.last_rekey + self.rekey_period.whole_seconds() as u64 { + return Ok(RequiredOperation::Rekey); + } + + Ok(RequiredOperation::None) + } + + pub(crate) fn mark_rotation_attempt(&mut self) { + self.last_rotation_attempt = utils::now().unwrap(); + } + + pub(crate) async fn rekey( + &mut self, + context: &mut Context, + secure_channel: &SecureChannels, + now: TimestampInSeconds, + ) -> ockam_core::Result<()> { + if self.rekey_counter == u16::MAX { + return Err(Error::new( + Origin::Channel, + Kind::Unknown, + "Rekey counter overflow", + )); + } + + let encryptor_address = &self.producer_encryptor_address; + + let secure_channel_entry = secure_channel.secure_channel_registry().get_channel_by_encryptor_address( + encryptor_address, + ).ok_or_else(|| { + Error::new( + Origin::Channel, + Kind::Unknown, + format!("Cannot find secure channel address `{encryptor_address}` in local registry"), + ) + })?; + + let rekey_response: EncryptionResponse = context + .send_and_receive( + route![secure_channel_entry.encryptor_api_address().clone()], + EncryptionRequest::Rekey, + ) + .await?; + + match rekey_response { + EncryptionResponse::Ok(_) => {} + EncryptionResponse::Err(cause) => { + error!("Cannot rekey secure channel: {cause}"); + return Err(cause); + } + } + + self.last_rekey = now; + self.rekey_counter += 1; + + Ok(()) + } +} + +pub(crate) type TopicName = String; + pub struct InnerSecureChannelController { + pub(crate) clock: Box, // we identify the secure channel instance by using the decryptor address of the consumer // which is known to both parties - pub(crate) topic_encryptor_map: HashMap, - // since topic/partition is using a key exchange only secure channel, - // we need another secure channel for each consumer identifier - // to make sure the relative credential is properly updated - pub(crate) identity_encryptor_map: HashMap, - pub(crate) node_manager: Arc, + pub(crate) producer_topic_encryptor_map: HashMap, + pub(crate) node_manager: Weak, // describes how to reach the consumer node pub(crate) consumer_resolution: ConsumerResolution, // describes if/how to publish the consumer pub(crate) consumer_publishing: ConsumerPublishing, - pub(crate) topic_relay_set: HashSet, + pub(crate) topic_relay_set: HashSet, pub(crate) secure_channels: Arc, pub(crate) consumer_policy_access_control: PolicyAccessControl, pub(crate) producer_policy_access_control: PolicyAccessControl, @@ -154,13 +247,33 @@ impl KafkaKeyExchangeControllerImpl { consumer_publishing: ConsumerPublishing, consumer_policy_access_control: PolicyAccessControl, producer_policy_access_control: PolicyAccessControl, + ) -> KafkaKeyExchangeControllerImpl { + Self::new_extended( + ProductionClock, + node_manager, + secure_channels, + consumer_resolution, + consumer_publishing, + consumer_policy_access_control, + producer_policy_access_control, + ) + } + + pub(crate) fn new_extended( + clock: impl Clock, + node_manager: Arc, + secure_channels: Arc, + consumer_resolution: ConsumerResolution, + consumer_publishing: ConsumerPublishing, + consumer_policy_access_control: PolicyAccessControl, + producer_policy_access_control: PolicyAccessControl, ) -> KafkaKeyExchangeControllerImpl { Self { inner: Arc::new(Mutex::new(InnerSecureChannelController { - topic_encryptor_map: Default::default(), - identity_encryptor_map: Default::default(), + clock: Box::new(clock), + producer_topic_encryptor_map: Default::default(), topic_relay_set: Default::default(), - node_manager, + node_manager: Arc::downgrade(&node_manager), secure_channels, consumer_resolution, consumer_publishing, @@ -170,3 +283,209 @@ impl KafkaKeyExchangeControllerImpl { } } } + +#[cfg(test)] +mod test { + use crate::kafka::key_exchange::controller::KafkaKeyExchangeControllerImpl; + use crate::kafka::{ConsumerPublishing, ConsumerResolution}; + use crate::test_utils::{AuthorityConfiguration, TestNode}; + use ockam::identity::Identifier; + use ockam_abac::{Action, Env, Resource, ResourceType}; + use ockam_core::compat::clock::test::TestClock; + use ockam_multiaddr::MultiAddr; + use std::sync::Arc; + use std::time::Duration; + use tokio::runtime::Runtime; + use tokio::time::timeout; + + #[test] + pub fn rekey_rotation() -> ockam_core::Result<()> { + let runtime = Arc::new(Runtime::new().unwrap()); + let runtime_cloned = runtime.clone(); + std::env::set_var("OCKAM_LOGGING", "false"); + + runtime_cloned.block_on(async move { + let test_body = async move { + TestNode::clean().await?; + let authority = TestNode::create_extended( + runtime.clone(), + None, + AuthorityConfiguration::SelfReferencing, + ) + .await; + + let mut consumer_node = TestNode::create_extended( + runtime.clone(), + None, + AuthorityConfiguration::Node(&authority), + ) + .await; + let mut producer_node = TestNode::create_extended( + runtime.clone(), + None, + AuthorityConfiguration::Node(&authority), + ) + .await; + + consumer_node + .node_manager + .start_key_exchanger_service( + &consumer_node.context, + crate::DefaultAddress::KEY_EXCHANGER_LISTENER.into(), + ) + .await?; + + let test_clock = TestClock::new(0); + + let destination = consumer_node.listen_address().await.multi_addr().unwrap(); + let producer_secure_channel_controller = create_secure_channel_controller( + test_clock.clone(), + &mut producer_node, + destination.clone(), + authority.node_manager.identifier(), + ) + .await; + + let _consumer_secure_channel_controller = create_secure_channel_controller( + test_clock.clone(), + &mut consumer_node, + destination.clone(), + authority.node_manager.identifier(), + ) + .await; + + let first_key = producer_secure_channel_controller + .get_or_exchange_key(&mut producer_node.context, "topic_name") + .await?; + + assert_eq!(first_key.rekey_counter, 0); + + // 00:10 - nothing should change + test_clock.add_seconds(10); + + let second_key = producer_secure_channel_controller + .get_or_exchange_key(&mut producer_node.context, "topic_name") + .await?; + + assert_eq!(first_key, second_key); + + // 01:00 - the default rekeying period is 1 minute + test_clock.add_seconds(50); + + let third_key = producer_secure_channel_controller + .get_or_exchange_key(&mut producer_node.context, "topic_name") + .await?; + + assert_eq!(third_key.rekey_counter, 1); + assert_eq!( + first_key.consumer_decryptor_address, + third_key.consumer_decryptor_address + ); + + // 04:00 - yet another rekey should happen, but no rotation + test_clock.add_seconds(60 * 3); + + let fourth_key = producer_secure_channel_controller + .get_or_exchange_key(&mut producer_node.context, "topic_name") + .await?; + + assert_eq!(fourth_key.rekey_counter, 2); + assert_eq!( + first_key.consumer_decryptor_address, + fourth_key.consumer_decryptor_address + ); + + // 05:00 - the default duration of the key is 10 minutes, + // but the rotation should happen after 5 minutes + test_clock.add_seconds(60); + + let fifth_key = producer_secure_channel_controller + .get_or_exchange_key(&mut producer_node.context, "topic_name") + .await?; + + assert_ne!( + third_key.consumer_decryptor_address, + fifth_key.consumer_decryptor_address + ); + assert_eq!(fifth_key.rekey_counter, 0); + + // Now let's simulate a failure to rekey by shutting down the consumer + consumer_node.context.stop().await?; + drop(consumer_node); + + // 06:00 - The producer should still be able to rekey + test_clock.add_seconds(60); + let sixth_key = producer_secure_channel_controller + .get_or_exchange_key(&mut producer_node.context, "topic_name") + .await?; + + assert_eq!(sixth_key.rekey_counter, 1); + assert_eq!( + fifth_key.consumer_decryptor_address, + sixth_key.consumer_decryptor_address + ); + + // 10:00 - Rotation fails, but the existing key is still valid + // and needs to be rekeyed + // (since we exchanged key at 05:00, it should be valid until 15:00) + test_clock.add_seconds(60 * 4); + let seventh_key = producer_secure_channel_controller + .get_or_exchange_key(&mut producer_node.context, "topic_name") + .await?; + + assert_eq!(seventh_key.rekey_counter, 2); + assert_eq!( + fifth_key.consumer_decryptor_address, + seventh_key.consumer_decryptor_address + ); + + // 15:00 - Rotation fails, and the existing key is no longer valid + test_clock.add_seconds(60 * 5); + let result = producer_secure_channel_controller + .get_or_exchange_key(&mut producer_node.context, "topic_name") + .await; + + assert!(result.is_err()); + + Ok(()) + }; + + timeout(Duration::from_secs(10), test_body).await.unwrap() + }) + } + + async fn create_secure_channel_controller( + test_clock: TestClock, + node: &mut TestNode, + destination: MultiAddr, + authority: Identifier, + ) -> KafkaKeyExchangeControllerImpl { + let consumer_policy_access_control = + node.node_manager.policies().make_policy_access_control( + node.secure_channels.identities().identities_attributes(), + Resource::new("arbitrary-resource-name", ResourceType::KafkaConsumer), + Action::HandleMessage, + Env::new(), + Some(authority.clone()), + ); + + let producer_policy_access_control = + node.node_manager.policies().make_policy_access_control( + node.secure_channels.identities().identities_attributes(), + Resource::new("arbitrary-resource-name", ResourceType::KafkaProducer), + Action::HandleMessage, + Env::new(), + Some(authority), + ); + + KafkaKeyExchangeControllerImpl::new_extended( + test_clock, + (*node.node_manager).clone(), + node.node_manager.secure_channels(), + ConsumerResolution::SingleNode(destination), + ConsumerPublishing::None, + consumer_policy_access_control, + producer_policy_access_control, + ) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/mod.rs b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/mod.rs index 57747f6db7d..fb4dc81de38 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/mod.rs @@ -25,13 +25,8 @@ pub enum ConsumerPublishing { #[n(2)] Relay(#[n(1)] MultiAddr), } -type TopicPartition = (String, i32); - /// Offer simple APIs to encrypt and decrypt kafka messages. -/// Underneath it creates secure channels for each topic/partition -/// and uses them to encrypt the content. -/// Multiple secure channels may be created for the same topic/partition -/// but each will be explicitly labeled. +/// Underneath it creates secure channels for each topic uses them to encrypt the content. #[async_trait] pub(crate) trait KafkaKeyExchangeController: Send + Sync + 'static { /// Encrypts the content specifically for the consumer waiting for that topic name and @@ -43,7 +38,6 @@ pub(crate) trait KafkaKeyExchangeController: Send + Sync + 'static { &self, context: &mut Context, topic_name: &str, - partition_index: i32, content: Vec, ) -> ockam_core::Result; @@ -53,16 +47,15 @@ pub(crate) trait KafkaKeyExchangeController: Send + Sync + 'static { &self, context: &mut Context, consumer_decryptor_address: &Address, + rekey_counter: u16, encrypted_content: Vec, ) -> ockam_core::Result>; - /// Starts relays in the orchestrator for each {topic_name}_{partition} combination - /// should be used only by the consumer. + /// Starts relays in the orchestrator for each topic name, should be used only by the consumer. /// does nothing if they were already created, but fails it they already exist. async fn publish_consumer( &self, context: &mut Context, topic_name: &str, - partitions: Vec, ) -> ockam_core::Result<()>; } diff --git a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs index a8317e06330..4ac9fce7d68 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs @@ -1,40 +1,20 @@ use crate::kafka::key_exchange::controller::{ - InnerSecureChannelController, KafkaKeyExchangeControllerImpl, + InnerSecureChannelController, KafkaKeyExchangeControllerImpl, RequiredOperation, + TopicEncryptionKey, TopicEncryptionKeyState, }; use crate::kafka::ConsumerResolution; use crate::nodes::service::SecureChannelType; use crate::DefaultAddress; -use ockam::identity::SecureChannelRegistryEntry; +use ockam::identity::{SecureChannelRegistryEntry, TimestampInSeconds}; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{Address, Error, Result}; use ockam_multiaddr::proto::{Secure, Service}; use ockam_multiaddr::MultiAddr; use ockam_node::Context; +use time::Duration; use tokio::sync::MutexGuard; impl KafkaKeyExchangeControllerImpl { - /// Creates a secure channel for the given destination. - async fn create_secure_channel( - inner: &MutexGuard<'_, InnerSecureChannelController>, - context: &Context, - mut destination: MultiAddr, - ) -> Result
{ - destination.push_back(Service::new(DefaultAddress::SECURE_CHANNEL_LISTENER))?; - let secure_channel = inner - .node_manager - .create_secure_channel( - context, - destination, - None, - None, - None, - None, - SecureChannelType::KeyExchangeAndMessages, - ) - .await?; - Ok(secure_channel.encryptor_address().clone()) - } - /// Creates a secure channel for the given destination, for key exchange only. async fn create_key_exchange_only_secure_channel( inner: &MutexGuard<'_, InnerSecureChannelController>, @@ -42,132 +22,100 @@ impl KafkaKeyExchangeControllerImpl { mut destination: MultiAddr, ) -> Result
{ destination.push_back(Service::new(DefaultAddress::KEY_EXCHANGER_LISTENER))?; - let secure_channel = inner - .node_manager - .create_secure_channel( - context, - destination, - None, - None, - None, - None, - SecureChannelType::KeyExchangeOnly, - ) - .await?; - Ok(secure_channel.encryptor_address().clone()) + if let Some(node_manager) = inner.node_manager.upgrade() { + let secure_channel = node_manager + .create_secure_channel( + context, + destination, + None, + None, + None, + None, + SecureChannelType::KeyExchangeOnly, + ) + .await?; + + // TODO: temporary workaround until the secure channel persistence is changed + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + Ok(secure_channel.encryptor_address().clone()) + } else { + Err(Error::new( + Origin::Transport, + Kind::Internal, + "Node Manager is not available", + )) + } } /// Creates a secure channel from the producer to the consumer needed to encrypt messages. - /// Returns the relative secure channel entry. - pub(crate) async fn get_or_create_secure_channel( + /// Returns the relative encryption key information. + pub(crate) async fn get_or_exchange_key( &self, context: &mut Context, topic_name: &str, - partition: i32, - ) -> Result { + ) -> Result { let mut inner = self.inner.lock().await; - // TODO: it may be better to exchange a new key for each partition - // when we have only one consumer, we use the same secure channel for all topics - let topic_partition_key = match &inner.consumer_resolution { - ConsumerResolution::SingleNode(_) | ConsumerResolution::None => ("".to_string(), 0i32), - ConsumerResolution::ViaRelay(_) => (topic_name.to_string(), partition), - }; + let rekey_counter; + let encryptor_address; - let encryptor_address = { - if let Some(encryptor_address) = inner.topic_encryptor_map.get(&topic_partition_key) { - encryptor_address.clone() - } else { - // destination is without the final service - let destination = match inner.consumer_resolution.clone() { - ConsumerResolution::SingleNode(mut destination) => { - debug!("creating new direct secure channel to consumer: {destination}"); - // remove /secure/api service from the destination if present - if let Some(service) = destination.last() { - let service: Option = service.cast(); - if let Some(service) = service { - if service.as_bytes() - == DefaultAddress::SECURE_CHANNEL_LISTENER.as_bytes() - { - destination.pop_back(); - } - } + let now = TimestampInSeconds(inner.clock.now()?); + let secure_channels = inner.secure_channels.clone(); + if let Some(encryption_key) = inner.producer_topic_encryptor_map.get_mut(topic_name) { + // before using it, check if it's still valid + match encryption_key.operation(now)? { + RequiredOperation::None => { + // the key is still valid + rekey_counter = encryption_key.rekey_counter; + encryptor_address = encryption_key.producer_encryptor_address.clone(); + } + RequiredOperation::Rekey => { + encryption_key.rekey(context, &secure_channels, now).await?; + rekey_counter = encryption_key.rekey_counter; + encryptor_address = encryption_key.producer_encryptor_address.clone(); + } + RequiredOperation::ShouldRotate => { + encryption_key.mark_rotation_attempt(); + // the key is still valid, but it's time to rotate it + let result = self.exchange_key(context, topic_name, &mut inner).await; + match result { + Ok(producer_encryptor_address) => { + rekey_counter = 0; + encryptor_address = producer_encryptor_address; } - destination - } - ConsumerResolution::ViaRelay(mut destination) => { - // consumer_ is the arbitrary chosen prefix by both parties - let topic_partition_address = - format!("forward_to_consumer_{topic_name}_{partition}"); - debug!( - "creating new secure channel via relay to {topic_partition_address}" - ); - destination.push_back(Service::new(topic_partition_address))?; - destination - } - ConsumerResolution::None => { - return Err(Error::new( - Origin::Transport, - Kind::Invalid, - "cannot encrypt messages with consumer key when consumer route resolution is not set", - )); - } - }; + Err(error) => { + warn!( + "Failed to rotate encryption key for topic `{topic_name}`: {error}. The current key will be used instead." + ); - let producer_encryptor_address = Self::create_key_exchange_only_secure_channel( - &inner, - context, - destination.clone(), - ) - .await?; + // borrow it again to satisfy the borrow checker + let encryption_key = inner + .producer_topic_encryptor_map + .get_mut(topic_name) + .expect("key should be present"); - if let Some(entry) = inner - .secure_channels - .secure_channel_registry() - .get_channel_by_encryptor_address(&producer_encryptor_address) - { - if let Err(error) = Self::validate_consumer_credentials(&inner, &entry).await { - inner - .node_manager - .delete_secure_channel(context, &producer_encryptor_address) - .await?; - return Err(error); - }; + // we might still need to rekey + if let RequiredOperation::Rekey = encryption_key.operation(now)? { + encryption_key.rekey(context, &secure_channels, now).await?; + } - // creates a dedicated secure channel to the consumer to keep the - // credentials up to date - if !inner.identity_encryptor_map.contains_key(entry.their_id()) { - if let Err(err) = - Self::create_secure_channel(&inner, context, destination).await - { - inner - .node_manager - .delete_secure_channel(context, &producer_encryptor_address) - .await?; - return Err(err); + encryptor_address = encryption_key.producer_encryptor_address.clone(); + rekey_counter = encryption_key.rekey_counter; } } - } else { - return Err(Error::new( - Origin::Transport, - Kind::Internal, - format!( - "cannot find secure channel address `{producer_encryptor_address}` in local registry" - ), - )); } - - inner - .topic_encryptor_map - .insert(topic_partition_key, producer_encryptor_address.clone()); - - debug!("created secure channel"); - producer_encryptor_address - } + RequiredOperation::MustRotate => { + // the key is no longer valid, must not be reused + rekey_counter = 0; + encryptor_address = self.exchange_key(context, topic_name, &mut inner).await?; + } + }; + } else { + rekey_counter = 0; + encryptor_address = self.exchange_key(context, topic_name, &mut inner).await?; }; - inner - .secure_channels + let entry = secure_channels .secure_channel_registry() .get_channel_by_encryptor_address(&encryptor_address) .ok_or_else(|| { @@ -176,7 +124,103 @@ impl KafkaKeyExchangeControllerImpl { Kind::Unknown, format!("cannot find secure channel address `{encryptor_address}` in local registry"), ) - }) + })?; + + Ok(TopicEncryptionKey { + rekey_counter, + encryptor_api_address: entry.encryptor_api_address().clone(), + consumer_decryptor_address: entry.their_decryptor_address().clone(), + }) + } + + async fn exchange_key( + &self, + context: &mut Context, + topic_name: &str, + inner: &mut MutexGuard<'_, InnerSecureChannelController>, + ) -> Result
{ + // destination is without the final service + let destination = match inner.consumer_resolution.clone() { + ConsumerResolution::SingleNode(mut destination) => { + debug!("creating new direct secure channel to consumer: {destination}"); + // remove /secure/api service from the destination if present + if let Some(service) = destination.last() { + let service: Option = service.cast(); + if let Some(service) = service { + if service.as_bytes() == DefaultAddress::SECURE_CHANNEL_LISTENER.as_bytes() + { + destination.pop_back(); + } + } + } + destination + } + ConsumerResolution::ViaRelay(mut destination) => { + // consumer_ is the arbitrary chosen prefix by both parties + let topic_address = format!("forward_to_consumer_{topic_name}"); + debug!("creating new secure channel via relay to {topic_address}"); + destination.push_back(Service::new(topic_address))?; + destination + } + ConsumerResolution::None => { + return Err(Error::new( + Origin::Transport, + Kind::Invalid, + "cannot encrypt messages with consumer key when consumer route resolution is not set", + )); + } + }; + + let producer_encryptor_address = + Self::create_key_exchange_only_secure_channel(inner, context, destination.clone()) + .await?; + + if let Some(entry) = inner + .secure_channels + .secure_channel_registry() + .get_channel_by_encryptor_address(&producer_encryptor_address) + { + if let Err(error) = Self::validate_consumer_credentials(inner, &entry).await { + if let Some(node_manager) = inner.node_manager.upgrade() { + node_manager + .delete_secure_channel(context, &producer_encryptor_address) + .await?; + } + return Err(error); + }; + } else { + return Err(Error::new( + Origin::Transport, + Kind::Internal, + format!( + "cannot find secure channel address `{producer_encryptor_address}` in local registry" + ), + )); + } + + let now = TimestampInSeconds(inner.clock.now()?); + + // TODO: retrieve these values from the other party + let valid_until = now + TimestampInSeconds(10 * 60); // 10 minutes + let rotate_after = now + TimestampInSeconds(5 * 60); // 5 minutes + let rekey_period = Duration::minutes(1); + + let encryption_key = TopicEncryptionKeyState { + producer_encryptor_address: producer_encryptor_address.clone(), + valid_until, + rotate_after, + rekey_period, + last_rekey: now, + last_rotation_attempt: TimestampInSeconds(0), + rekey_counter: 0, + }; + + inner + .producer_topic_encryptor_map + .insert(topic_name.to_string(), encryption_key); + + info!("Successfully exchanged new key with {destination} for topic {topic_name}"); + Ok(producer_encryptor_address) } async fn validate_consumer_credentials( diff --git a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/request.rs b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/request.rs index e12667732a9..8459cc41f80 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/request.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/request.rs @@ -4,7 +4,7 @@ use crate::kafka::protocol_aware::RequestInfo; use crate::kafka::protocol_aware::{InterceptError, KafkaMessageRequestInterceptor}; use bytes::{Bytes, BytesMut}; use kafka_protocol::messages::fetch_request::FetchRequest; -use kafka_protocol::messages::produce_request::{PartitionProduceData, ProduceRequest}; +use kafka_protocol::messages::produce_request::ProduceRequest; use kafka_protocol::messages::request_header::RequestHeader; use kafka_protocol::messages::{ApiKey, ApiVersionsRequest, TopicName}; use kafka_protocol::protocol::buf::ByteBuf; @@ -173,14 +173,8 @@ impl InletInterceptorImpl { })? }; - let partitions: Vec = topic - .partitions - .iter() - .map(|partition| partition.partition) - .collect(); - self.key_exchange_controller - .publish_consumer(context, &topic_id, partitions) + .publish_consumer(context, &topic_id) .await .map_err(InterceptError::Ockam)? } @@ -221,15 +215,10 @@ impl InletInterceptorImpl { let buffer = if !self.encrypted_fields.is_empty() { // if we encrypt only specific fields, we assume the record must be // valid JSON map - self.encrypt_specific_fields( - context, - &topic.name, - data, - &record_value, - ) - .await? + self.encrypt_specific_fields(context, &topic.name, &record_value) + .await? } else { - self.encrypt_whole_record(context, &topic.name, data, record_value) + self.encrypt_whole_record(context, &topic.name, record_value) .await? }; record.value = Some(buffer.into()); @@ -265,12 +254,11 @@ impl InletInterceptorImpl { &self, context: &mut Context, topic_name: &TopicName, - data: &mut PartitionProduceData, record_value: Bytes, ) -> Result, InterceptError> { let encrypted_content = self .key_exchange_controller - .encrypt_content(context, topic_name, data.index, record_value.to_vec()) + .encrypt_content(context, topic_name, record_value.to_vec()) .await .map_err(InterceptError::Ockam)?; @@ -287,7 +275,6 @@ impl InletInterceptorImpl { &self, context: &mut Context, topic_name: &TopicName, - data: &mut PartitionProduceData, record_value: &Bytes, ) -> Result, InterceptError> { let mut record_value = serde_json::from_slice::(record_value)?; @@ -300,7 +287,6 @@ impl InletInterceptorImpl { .encrypt_content( context, topic_name, - data.index, serde_json::to_vec(value).map_err(|_| InterceptError::InvalidData)?, ) .await diff --git a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/response.rs b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/response.rs index c0b221cf503..3f1fc6b4a0c 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/response.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/response.rs @@ -345,6 +345,7 @@ impl InletInterceptorImpl { .decrypt_content( context, &message_wrapper.consumer_decryptor_address, + message_wrapper.rekey_counter, message_wrapper.content, ) .await @@ -378,6 +379,7 @@ impl InletInterceptorImpl { .decrypt_content( context, &message_wrapper.consumer_decryptor_address, + message_wrapper.rekey_counter, message_wrapper.content, ) .await diff --git a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/tests.rs b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/tests.rs index b7db0eb0119..c4714bdb0f9 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/tests.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/tests.rs @@ -31,7 +31,6 @@ impl KafkaKeyExchangeController for MockKafkaKeyExchangeController { &self, _context: &mut Context, _topic_name: &str, - _partition_index: i32, content: Vec, ) -> ockam_core::Result { let mut new_content = ENCRYPTED_PREFIX.to_vec(); @@ -39,6 +38,7 @@ impl KafkaKeyExchangeController for MockKafkaKeyExchangeController { Ok(KafkaEncryptedContent { consumer_decryptor_address: Address::from_string("mock"), content: new_content, + rekey_counter: u16::MAX, }) } @@ -46,6 +46,7 @@ impl KafkaKeyExchangeController for MockKafkaKeyExchangeController { &self, _context: &mut Context, _consumer_decryptor_address: &Address, + _rekey_counter: u16, encrypted_content: Vec, ) -> ockam_core::Result> { Ok(encrypted_content[PREFIX_LEN..].to_vec()) @@ -55,7 +56,6 @@ impl KafkaKeyExchangeController for MockKafkaKeyExchangeController { &self, _context: &mut Context, _topic_name: &str, - _partitions: Vec, ) -> ockam_core::Result<()> { Ok(()) } @@ -198,6 +198,7 @@ pub fn encode_field_value(value: serde_json::Value) -> String { .encode(KafkaEncryptedContent { consumer_decryptor_address: Address::from_string("mock"), content: encrypted_content, + rekey_counter: u16::MAX, }) .unwrap(); diff --git a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/mod.rs b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/mod.rs index 2e1644dba45..0d002fcb7d3 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/mod.rs @@ -150,7 +150,9 @@ pub(crate) struct KafkaEncryptedContent { /// The secure channel identifier used to encrypt the content #[n(0)] pub consumer_decryptor_address: Address, /// The encrypted content - #[n(1)] pub content: Vec + #[n(1)] pub content: Vec, + /// Number of times rekey was performed before encrypting the content + #[n(2)] pub rekey_counter: u16, } /// By default, kafka supports up to 1MB messages. 16MB is the maximum suggested diff --git a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/tests.rs b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/tests.rs index afdd9b25173..c8a4e24b015 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/tests.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/tests.rs @@ -9,7 +9,7 @@ mod test { }; use crate::kafka::{ConsumerPublishing, ConsumerResolution}; use crate::port_range::PortRange; - use crate::test_utils::TestNode; + use crate::test_utils::{AuthorityConfiguration, TestNode}; use kafka_protocol::messages::ApiKey; use kafka_protocol::messages::BrokerId; use kafka_protocol::messages::{ApiVersionsRequest, MetadataRequest, MetadataResponse}; @@ -27,7 +27,12 @@ mod test { context: &mut Context, ) -> ockam::Result<()> { TestNode::clean().await?; - let handle = crate::test_utils::start_manager_for_tests(context, None, None).await?; + let handle = crate::test_utils::start_manager_for_tests( + context, + None, + AuthorityConfiguration::SelfReferencing, + ) + .await?; let inlet_map = KafkaInletController::new( (*handle.node_manager).clone(), diff --git a/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs b/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs index 016fc7f062d..1761bc0b20f 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs @@ -27,7 +27,7 @@ use crate::kafka::key_exchange::controller::KafkaKeyExchangeControllerImpl; use crate::kafka::protocol_aware::inlet::KafkaInletInterceptorFactory; use crate::kafka::protocol_aware::utils::{encode_request, encode_response}; use crate::kafka::{ConsumerPublishing, ConsumerResolution, KafkaInletController}; -use crate::test_utils::{NodeManagerHandle, TestNode}; +use crate::test_utils::{AuthorityConfiguration, NodeManagerHandle, TestNode}; use ockam::compat::tokio::io::DuplexStream; use ockam::tcp::{TcpInletOptions, TcpOutletOptions}; use ockam::Context; @@ -131,7 +131,12 @@ async fn producer__flow_with_mock_kafka__content_encryption_and_decryption( context: &mut Context, ) -> ockam::Result<()> { TestNode::clean().await?; - let handle = crate::test_utils::start_manager_for_tests(context, None, None).await?; + let handle = crate::test_utils::start_manager_for_tests( + context, + None, + AuthorityConfiguration::SelfReferencing, + ) + .await?; let consumer_bootstrap_port = create_kafka_service( context, @@ -150,8 +155,7 @@ async fn producer__flow_with_mock_kafka__content_encryption_and_decryption( .await?; // for the consumer to become available to the producer, the consumer has to issue a Fetch - // request first, so the sidecar can react by creating the relay for partition - // 1 of 'my-topic' + // request first, so the sidecar can react by creating the relay for topic 'my-topic' { let mut consumer_mock_kafka = TcpServerSimulator::start("127.0.0.1:0").await; handle diff --git a/implementations/rust/ockam/ockam_api/src/kafka/tests/interceptor_test.rs b/implementations/rust/ockam/ockam_api/src/kafka/tests/interceptor_test.rs index bea6e701729..6df8cf4d8bf 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/tests/interceptor_test.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/tests/interceptor_test.rs @@ -31,7 +31,7 @@ use crate::kafka::protocol_aware::KafkaMessageInterceptorWrapper; use crate::kafka::protocol_aware::MAX_KAFKA_MESSAGE_SIZE; use crate::kafka::{ConsumerPublishing, ConsumerResolution}; use crate::port_range::PortRange; -use crate::test_utils::{NodeManagerHandle, TestNode}; +use crate::test_utils::{AuthorityConfiguration, NodeManagerHandle, TestNode}; const TEST_MAX_KAFKA_MESSAGE_SIZE: u32 = 128 * 1024; const TEST_KAFKA_API_VERSION: i16 = 13; @@ -66,7 +66,12 @@ async fn kafka_portal_worker__pieces_of_kafka_message__message_assembled( context: &mut Context, ) -> ockam::Result<()> { TestNode::clean().await?; - let handle = crate::test_utils::start_manager_for_tests(context, None, None).await?; + let handle = crate::test_utils::start_manager_for_tests( + context, + None, + AuthorityConfiguration::SelfReferencing, + ) + .await?; let portal_inlet_address = setup_only_worker(context, &handle).await; let mut request_buffer = BytesMut::new(); @@ -109,7 +114,12 @@ async fn kafka_portal_worker__double_kafka_message__message_assembled( context: &mut Context, ) -> ockam::Result<()> { TestNode::clean().await?; - let handle = crate::test_utils::start_manager_for_tests(context, None, None).await?; + let handle = crate::test_utils::start_manager_for_tests( + context, + None, + AuthorityConfiguration::SelfReferencing, + ) + .await?; let portal_inlet_address = setup_only_worker(context, &handle).await; let mut request_buffer = BytesMut::new(); @@ -147,7 +157,12 @@ async fn kafka_portal_worker__bigger_than_limit_kafka_message__error( context: &mut Context, ) -> ockam::Result<()> { TestNode::clean().await?; - let handle = crate::test_utils::start_manager_for_tests(context, None, None).await?; + let handle = crate::test_utils::start_manager_for_tests( + context, + None, + AuthorityConfiguration::SelfReferencing, + ) + .await?; let portal_inlet_address = setup_only_worker(context, &handle).await; // with the message container it goes well over the max allowed message kafka size @@ -194,7 +209,12 @@ async fn kafka_portal_worker__almost_over_limit_than_limit_kafka_message__two_ka context: &mut Context, ) -> ockam::Result<()> { TestNode::clean().await?; - let handle = crate::test_utils::start_manager_for_tests(context, None, None).await?; + let handle = crate::test_utils::start_manager_for_tests( + context, + None, + AuthorityConfiguration::SelfReferencing, + ) + .await?; let portal_inlet_address = setup_only_worker(context, &handle).await; // let's build the message to 90% of max. size @@ -363,7 +383,12 @@ async fn kafka_portal_worker__metadata_exchange__response_changed( context: &mut Context, ) -> ockam::Result<()> { TestNode::clean().await?; - let handle = crate::test_utils::start_manager_for_tests(context, None, None).await?; + let handle = crate::test_utils::start_manager_for_tests( + context, + None, + AuthorityConfiguration::SelfReferencing, + ) + .await?; let project_authority = handle .node_manager .node_manager diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs index 916fc710ce6..8501636d484 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs @@ -303,7 +303,7 @@ impl NodeManager { /// SECURE CHANNEL LISTENERS impl NodeManager { - pub(super) async fn start_key_exchanger_service( + pub(crate) async fn start_key_exchanger_service( &self, context: &Context, address: Address, diff --git a/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs b/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs index eb866a05fe0..e9ad1436994 100644 --- a/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs @@ -2,6 +2,13 @@ use crate::config::lookup::InternetAddress; use crate::nodes::service::{NodeManagerCredentialRetrieverOptions, NodeManagerTrustOptions}; +use ockam::identity::utils::AttributesBuilder; +use ockam::identity::SecureChannels; +use ockam::tcp::{TcpListenerOptions, TcpTransport}; +use ockam::transport::HostnamePort; +use ockam::Result; +use ockam_core::AsyncTryClone; +use ockam_node::database::{DatabaseConfiguration, SqlxDatabase}; use ockam_node::{Context, NodeBuilder}; use sqlx::__rt::timeout; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -15,14 +22,6 @@ use tokio::net::{TcpListener, TcpSocket, TcpStream}; use tokio::runtime::Runtime; use tracing::{error, info}; -use ockam::identity::utils::AttributesBuilder; -use ockam::identity::SecureChannels; -use ockam::tcp::{TcpListenerOptions, TcpTransport}; -use ockam::transport::HostnamePort; -use ockam::Result; -use ockam_core::AsyncTryClone; -use ockam_node::database::{DatabaseConfiguration, SqlxDatabase}; - use crate::authenticator::credential_issuer::{DEFAULT_CREDENTIAL_VALIDITY, PROJECT_MEMBER_SCHEMA}; use crate::cli_state::{random_name, CliState}; use crate::nodes::service::{NodeManagerGeneralOptions, NodeManagerTransportOptions}; @@ -49,14 +48,21 @@ impl Drop for NodeManagerHandle { } } +/// Authority configuration for the node manager. +pub enum AuthorityConfiguration<'a> { + None, + Node(&'a TestNode), + SelfReferencing, +} + /// Starts a local node manager and returns a handle to it. /// -/// Be careful: if you drop the returned handle before the end of the test +/// Be careful: if you drop the returned handle before the end of the test, /// things *will* break. pub async fn start_manager_for_tests( context: &mut Context, bind_addr: Option<&str>, - trust_options: Option, + authority_configuration: AuthorityConfiguration<'_>, ) -> Result { let tcp = TcpTransport::create(context).await?; let tcp_listener = tcp @@ -80,18 +86,83 @@ pub async fn start_manager_for_tests( let vault = cli_state.make_vault(named_vault).await?; let identities = cli_state.make_identities(vault).await?; - let attributes = AttributesBuilder::with_schema(PROJECT_MEMBER_SCHEMA).build(); - let credential = identities - .credentials() - .credentials_creation() - .issue_credential( - &identifier, - &identifier, - attributes, - DEFAULT_CREDENTIAL_VALIDITY, - ) - .await - .unwrap(); + let trust_options = match authority_configuration { + AuthorityConfiguration::None => NodeManagerTrustOptions::new( + NodeManagerCredentialRetrieverOptions::None, + NodeManagerCredentialRetrieverOptions::None, + None, + NodeManagerCredentialRetrieverOptions::None, + ), + AuthorityConfiguration::Node(authority) => { + // if we have a third-party authority, we need to manually exchange identities + // since no actual secure channel is established to exchange credentials + authority + .secure_channels + .identities() + .identities_verification() + .import_from_change_history( + Some(&identifier), + identities + .identities_verification() + .get_change_history(&identifier) + .await?, + ) + .await?; + + let authority_identifier = authority.node_manager_handle.node_manager.identifier(); + identities + .identities_verification() + .import_from_change_history( + Some(&authority_identifier), + authority + .secure_channels + .identities() + .identities_verification() + .get_change_history(&authority_identifier) + .await?, + ) + .await?; + + let credential = authority + .secure_channels + .identities() + .credentials() + .credentials_creation() + .issue_credential( + &authority_identifier, + &identifier, + AttributesBuilder::with_schema(PROJECT_MEMBER_SCHEMA).build(), + DEFAULT_CREDENTIAL_VALIDITY, + ) + .await?; + + NodeManagerTrustOptions::new( + NodeManagerCredentialRetrieverOptions::InMemory(credential), + NodeManagerCredentialRetrieverOptions::None, + Some(authority_identifier), + NodeManagerCredentialRetrieverOptions::None, + ) + } + AuthorityConfiguration::SelfReferencing => { + let credential = identities + .credentials() + .credentials_creation() + .issue_credential( + &identifier, + &identifier, + AttributesBuilder::with_schema(PROJECT_MEMBER_SCHEMA).build(), + DEFAULT_CREDENTIAL_VALIDITY, + ) + .await?; + + NodeManagerTrustOptions::new( + NodeManagerCredentialRetrieverOptions::InMemory(credential), + NodeManagerCredentialRetrieverOptions::None, + Some(identifier), + NodeManagerCredentialRetrieverOptions::None, + ) + } + }; let node_manager = InMemoryNode::new( context, @@ -101,14 +172,7 @@ pub async fn start_manager_for_tests( tcp.async_try_clone().await?, None, ), - trust_options.unwrap_or_else(|| { - NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::InMemory(credential), - NodeManagerCredentialRetrieverOptions::None, - Some(identifier), - NodeManagerCredentialRetrieverOptions::None, - ) - }), + trust_options, ) .await?; @@ -218,22 +282,22 @@ impl TestNode { } pub async fn create(runtime: Arc, listen_addr: Option<&str>) -> Self { + Self::create_extended(runtime, listen_addr, AuthorityConfiguration::None).await + } + + pub async fn create_extended( + runtime: Arc, + listen_addr: Option<&str>, + authority_configuration: AuthorityConfiguration<'_>, + ) -> Self { let (mut context, mut executor) = NodeBuilder::new().with_runtime(runtime.clone()).build(); runtime.spawn(async move { executor.start_router().await.expect("cannot start router"); }); - let node_manager_handle = start_manager_for_tests( - &mut context, - listen_addr, - Some(NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::None, - NodeManagerCredentialRetrieverOptions::None, - None, - NodeManagerCredentialRetrieverOptions::None, - )), - ) - .await - .expect("cannot start node manager"); + let node_manager_handle = + start_manager_for_tests(&mut context, listen_addr, authority_configuration) + .await + .expect("cannot start node manager"); Self { context, diff --git a/implementations/rust/ockam/ockam_api/tests/latency.rs b/implementations/rust/ockam/ockam_api/tests/latency.rs index 4eb7bf974ee..7ed6b3d5a24 100644 --- a/implementations/rust/ockam/ockam_api/tests/latency.rs +++ b/implementations/rust/ockam/ockam_api/tests/latency.rs @@ -1,3 +1,5 @@ +#![recursion_limit = "256"] + use ockam_api::nodes::service::SecureChannelType; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -10,8 +12,7 @@ use tokio::time::timeout; use ockam_api::nodes::models::portal::OutletAccessControl; use ockam_api::test_utils::{start_tcp_echo_server, TestNode}; use ockam_core::env::FromString; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{route, Address, AllowAll, Error, NeutralMessage}; +use ockam_core::{route, Address, AllowAll, NeutralMessage}; use ockam_multiaddr::MultiAddr; use ockam_transport_core::HostnamePort; @@ -21,12 +22,12 @@ use ockam_transport_core::HostnamePort; /// `cargo test --test latency --release -- --ignored --show-output` #[ignore] #[test] -pub fn measure_message_latency_two_nodes() { +pub fn measure_message_latency_two_nodes() -> ockam_core::Result<()> { let runtime = Arc::new(Runtime::new().unwrap()); let runtime_cloned = runtime.clone(); std::env::remove_var("OCKAM_LOG_LEVEL"); - let result: ockam::Result<()> = runtime_cloned.block_on(async move { + runtime_cloned.block_on(async move { let test_body = async move { TestNode::clean().await?; let mut first_node = TestNode::create(runtime.clone(), None).await; @@ -109,20 +110,17 @@ pub fn measure_message_latency_two_nodes() { }; timeout(Duration::from_secs(30), test_body).await.unwrap() - }); - - result.unwrap(); - drop(runtime_cloned); + }) } #[ignore] #[test] -pub fn measure_buffer_latency_two_nodes_portal() { +pub fn measure_buffer_latency_two_nodes_portal() -> ockam_core::Result<()> { let runtime = Arc::new(Runtime::new().unwrap()); let runtime_cloned = runtime.clone(); std::env::remove_var("OCKAM_LOG_LEVEL"); - let result: ockam::Result<()> = runtime_cloned.block_on(async move { + runtime_cloned.block_on(async move { let test_body = async move { let echo_server_handle = start_tcp_echo_server().await; @@ -200,11 +198,6 @@ pub fn measure_buffer_latency_two_nodes_portal() { Ok(()) }; - timeout(Duration::from_secs(30), test_body) - .await - .unwrap_or_else(|_| Err(Error::new(Origin::Node, Kind::Timeout, "Test timed out"))) - }); - - result.unwrap(); - drop(runtime_cloned); + timeout(Duration::from_secs(30), test_body).await.unwrap() + }) } diff --git a/implementations/rust/ockam/ockam_api/tests/portals.rs b/implementations/rust/ockam/ockam_api/tests/portals.rs index b86a303bfc8..e6cf766e1e7 100644 --- a/implementations/rust/ockam/ockam_api/tests/portals.rs +++ b/implementations/rust/ockam/ockam_api/tests/portals.rs @@ -1,7 +1,8 @@ use ockam_api::config::lookup::InternetAddress; use ockam_api::nodes::models::portal::OutletAccessControl; use ockam_api::test_utils::{ - start_manager_for_tests, start_passthrough_server, start_tcp_echo_server, Disruption, TestNode, + start_manager_for_tests, start_passthrough_server, start_tcp_echo_server, + AuthorityConfiguration, Disruption, TestNode, }; use ockam_api::ConnectionStatus; use ockam_core::compat::rand::RngCore; @@ -24,7 +25,8 @@ use tracing::info; async fn inlet_outlet_local_successful(context: &mut Context) -> ockam::Result<()> { TestNode::clean().await?; let echo_server_handle = start_tcp_echo_server().await; - let node_manager_handle = start_manager_for_tests(context, None, None).await?; + let node_manager_handle = + start_manager_for_tests(context, None, AuthorityConfiguration::SelfReferencing).await?; let outlet_status = node_manager_handle .node_manager @@ -221,7 +223,7 @@ fn portal_node_goes_down_reconnect() { } #[test] -fn portal_low_bandwidth_connection_keep_working_for_60s() { +fn portal_low_bandwidth_connection_keep_working_for_60s() -> ockam_core::Result<()> { // in this test we use two nodes, connected through a passthrough server // which limits the bandwidth to 170kb per second // @@ -240,7 +242,7 @@ fn portal_low_bandwidth_connection_keep_working_for_60s() { let runtime_cloned = runtime.clone(); std::env::remove_var("OCKAM_LOG_LEVEL"); - let result: ockam::Result<()> = handle.block_on(async move { + handle.block_on(async move { let test_body = async move { let echo_server_handle = start_tcp_echo_server().await; @@ -346,22 +348,18 @@ fn portal_low_bandwidth_connection_keep_working_for_60s() { Ok(()) }; - timeout(Duration::from_secs(90), test_body) - .await - .unwrap_or_else(|_| Err(Error::new(Origin::Node, Kind::Timeout, "Test timed out"))) - }); - - result.unwrap(); + timeout(Duration::from_secs(90), test_body).await.unwrap() + }) } #[test] -fn portal_heavy_load_exchanged() { +fn portal_heavy_load_exchanged() -> ockam_core::Result<()> { let runtime = Arc::new(Runtime::new().unwrap()); let handle = runtime.handle(); let runtime_cloned = runtime.clone(); std::env::remove_var("OCKAM_LOG_LEVEL"); - let result: ockam::Result<()> = handle.block_on(async move { + handle.block_on(async move { let test_body = async move { let echo_server_handle = start_tcp_echo_server().await; @@ -457,17 +455,13 @@ fn portal_heavy_load_exchanged() { Ok(()) }; - timeout(Duration::from_secs(90), test_body) - .await - .unwrap_or_else(|_| Err(Error::new(Origin::Node, Kind::Timeout, "Test timed out"))) - }); - - result.unwrap(); + timeout(Duration::from_secs(90), test_body).await.unwrap() + }) } #[ignore] #[test] -fn portal_connection_drop_packets() { +fn portal_connection_drop_packets() -> ockam_core::Result<()> { // Drop even packets after 32 packets (to allow for the initial // handshake to complete). // This test checks that: @@ -475,22 +469,25 @@ fn portal_connection_drop_packets() { // - the portion of the received data matches with the sent data. // - test_portal_payload_transfer(Disruption::DropPacketsAfter(32), Disruption::None); + test_portal_payload_transfer(Disruption::DropPacketsAfter(32), Disruption::None) } #[ignore] #[test] -fn portal_connection_change_packet_order() { +fn portal_connection_change_packet_order() -> ockam_core::Result<()> { // Change packet order after 32 packets (to allow for the initial // handshake to complete). // This test checks that: // - connection is interrupted when a failure is detected // - the portion of the received data matches with the sent data. - test_portal_payload_transfer(Disruption::PacketsOutOfOrderAfter(32), Disruption::None); + test_portal_payload_transfer(Disruption::PacketsOutOfOrderAfter(32), Disruption::None) } -fn test_portal_payload_transfer(outgoing_disruption: Disruption, incoming_disruption: Disruption) { +fn test_portal_payload_transfer( + outgoing_disruption: Disruption, + incoming_disruption: Disruption, +) -> ockam_core::Result<()> { // we use two nodes, connected through a passthrough server // ┌────────┐ ┌───────────┐ ┌────────┐ // │ Node └─────► TCP └────────► Node │ @@ -507,7 +504,7 @@ fn test_portal_payload_transfer(outgoing_disruption: Disruption, incoming_disrup let runtime_cloned = runtime.clone(); std::env::remove_var("OCKAM_LOG_LEVEL"); - let result: ockam::Result<_> = handle.block_on(async move { + handle.block_on(async move { let test_body = async move { let echo_server_handle = start_tcp_echo_server().await; @@ -609,10 +606,6 @@ fn test_portal_payload_transfer(outgoing_disruption: Disruption, incoming_disrup Ok(()) }; - timeout(Duration::from_secs(60), test_body) - .await - .unwrap_or_else(|_| Err(Error::new(Origin::Node, Kind::Timeout, "Test timed out"))) - }); - - result.unwrap(); + timeout(Duration::from_secs(60), test_body).await.unwrap() + }) } diff --git a/implementations/rust/ockam/ockam_core/src/compat.rs b/implementations/rust/ockam/ockam_core/src/compat.rs index 68136336506..06616baade7 100644 --- a/implementations/rust/ockam/ockam_core/src/compat.rs +++ b/implementations/rust/ockam/ockam_core/src/compat.rs @@ -317,6 +317,67 @@ pub mod vec { pub type Vec = heapless::Vec; } +/// Provides [`Clock`] and [`test::TestClock`]. +/// These are useful for testing time-dependent code. +pub mod clock { + /// A trait for providing the current time. + pub trait Clock: Send + Sync + 'static { + /// Returns the current time in seconds since the Unix epoch. + fn now(&self) -> crate::Result; + } + + /// A production implementation of the [`Clock`] trait. + /// Unless you are writing testing code, this is the implementation you should use. + #[derive(Clone, Default, Debug)] + pub struct ProductionClock; + + impl Clock for ProductionClock { + fn now(&self) -> crate::Result { + super::time::now() + } + } + + #[cfg(feature = "std")] + #[allow(dead_code, missing_docs)] + pub mod test { + use crate::compat::clock::Clock; + use core::fmt::Debug; + use std::sync::atomic::AtomicU64; + use std::sync::Arc; + + #[derive(Clone)] + pub struct TestClock { + pub time: Arc, + } + + impl Clock for TestClock { + fn now(&self) -> crate::Result { + Ok(self.time.load(std::sync::atomic::Ordering::Relaxed)) + } + } + + impl Debug for TestClock { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:?}", self.time) + } + } + + impl TestClock { + pub fn new(time: u64) -> Self { + Self { + time: Arc::new(AtomicU64::new(time)), + } + } + + pub fn add_seconds(&self, seconds: u64) { + let time = self.time.load(std::sync::atomic::Ordering::Relaxed); + self.time + .store(time + seconds, std::sync::atomic::Ordering::Relaxed); + } + } + } +} + /// Provides `std::time` for `std` targets. #[cfg(feature = "std")] pub mod time { diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs index a0daab91b6f..936aa03bde3 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs @@ -5,7 +5,12 @@ use serde::{Deserialize, Serialize}; /// Request type for `EncryptorWorker` API Address #[derive(Serialize, Deserialize, Message)] -pub struct EncryptionRequest(pub Vec); +pub enum EncryptionRequest { + /// Encrypt data + Encrypt(Vec), + /// Trigger a manual rekey + Rekey, +} /// Response type for `EncryptorWorker` API Address #[derive(Serialize, Deserialize, Message)] @@ -18,7 +23,7 @@ pub enum EncryptionResponse { /// Request type for `Decryptor` API Address (the `Decryptor` is accessible through the `HandshakeWorker`) #[derive(Serialize, Deserialize, Message)] -pub struct DecryptionRequest(pub Vec); +pub struct DecryptionRequest(pub Vec, pub Option); /// Response type for `Decryptor` API Address (the `Decryptor` is accessible through the `HandshakeWorker`) #[derive(Serialize, Deserialize, Message)] diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs index d37ce85fbac..42027b53f72 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs @@ -5,7 +5,7 @@ use ockam_core::{Decodable, LocalMessage}; use ockam_node::Context; use crate::models::Identifier; -use crate::secure_channel::encryptor::{Encryptor, KEY_RENEWAL_INTERVAL}; +use crate::secure_channel::encryptor::KEY_RENEWAL_INTERVAL; use crate::secure_channel::handshake::handshake_state_machine::CommonStateMachine; use crate::secure_channel::key_tracker::KeyTracker; use crate::secure_channel::nonce_tracker::NonceTracker; @@ -17,6 +17,7 @@ use crate::{ }; use crate::secure_channel::encryptor_worker::SecureChannelSharedState; +use ockam_core::errcode::{Kind, Origin}; use ockam_vault::{AeadSecretKeyHandle, VaultForSecureChannels}; use tracing::{debug, info, trace, warn}; use tracing_attributes::instrument; @@ -80,9 +81,13 @@ impl DecryptorHandler { // Decode raw payload binary let mut request = DecryptionRequest::decode(&msg.payload)?; - - // Decrypt the binary - let decrypted_payload = self.decryptor.decrypt(request.0.as_mut_slice()).await; + let decrypted_payload = if let Some(rekey_counter) = request.1 { + self.decryptor + .decrypt_with_rekey_counter(&mut request.0, rekey_counter) + .await + } else { + self.decryptor.decrypt(request.0.as_mut_slice()).await + }; let response = match decrypted_payload { Ok((payload, _nonce)) => DecryptionResponse::Ok(payload.to_vec()), @@ -231,6 +236,7 @@ pub(crate) struct Decryptor { vault: Arc, key_tracker: KeyTracker, nonce_tracker: Option, + rekey_cache: Option<(u16, AeadSecretKeyHandle)>, } impl Decryptor { @@ -239,6 +245,7 @@ impl Decryptor { vault, key_tracker: KeyTracker::new(key, KEY_RENEWAL_INTERVAL), nonce_tracker: Some(NonceTracker::new()), + rekey_cache: None, } } @@ -248,6 +255,7 @@ impl Decryptor { vault, key_tracker: KeyTracker::new(key, KEY_RENEWAL_INTERVAL), nonce_tracker: None, + rekey_cache: None, } } @@ -273,7 +281,7 @@ impl Decryptor { if let Some(key) = self.key_tracker.get_key(nonce)? { key } else { - rekey_key = Encryptor::rekey(&self.vault, &self.key_tracker.current_key).await?; + rekey_key = self.vault.rekey(&self.key_tracker.current_key, 1).await?; &rekey_key } } else { @@ -305,15 +313,111 @@ impl Decryptor { } } + #[instrument(skip_all)] + pub async fn decrypt_with_rekey_counter<'a>( + &mut self, + payload: &'a mut [u8], + rekey_counter: u16, + ) -> Result<(&'a [u8], Nonce)> { + if payload.len() < 8 { + return Err(IdentityError::InvalidNonce)?; + } + + let nonce = Nonce::try_from(&payload[..8])?; + let nonce_tracker = if let Some(nonce_tracker) = &self.nonce_tracker { + Some(nonce_tracker.mark(nonce)?) + } else { + None + }; + + let key_handle = + if let Some((cached_rekey_counter, cached_key_handle)) = self.rekey_cache.clone() { + if cached_rekey_counter == rekey_counter { + Some(cached_key_handle) + } else { + self.rekey_cache = None; + self.vault + .delete_aead_secret_key(cached_key_handle.clone()) + .await?; + None + } + } else { + None + }; + + let key_handle = match key_handle { + Some(key) => key, + None => { + let current_number_of_rekeys = self.key_tracker.number_of_rekeys(); + if current_number_of_rekeys > rekey_counter as u64 { + return Err(ockam_core::Error::new( + Origin::Channel, + Kind::Invalid, + "cannot rekey backwards", + )); + } else if current_number_of_rekeys > u16::MAX as u64 { + return Err(ockam_core::Error::new( + Origin::Channel, + Kind::Invalid, + "rekey counter overflow", + )); + } else { + let n_rekying = rekey_counter - current_number_of_rekeys as u16; + if n_rekying > 0 { + let key_handle = self + .vault + .rekey(&self.key_tracker.current_key, n_rekying) + .await?; + self.rekey_cache = Some((rekey_counter, key_handle.clone())); + key_handle + } else { + self.key_tracker.current_key.clone() + } + } + } + }; + + // to improve protection against connection disruption attacks, we want to validate the + // message with a decryption _before_ committing to the new state + let result = self + .vault + .aead_decrypt( + &key_handle, + &mut payload[NOISE_NONCE_LEN..], + &nonce.to_aes_gcm_nonce(), + &[], + ) + .await; + + if result.is_ok() { + self.nonce_tracker = nonce_tracker; + if let Some(key_to_delete) = self.key_tracker.update_key(&key_handle)? { + self.vault.delete_aead_secret_key(key_to_delete).await?; + } + } + + result.map(|payload| (&*payload, nonce)) + } + /// Remove the channel keys on shutdown #[instrument(skip_all)] pub(crate) async fn shutdown(&self) -> Result<()> { self.vault .delete_aead_secret_key(self.key_tracker.current_key.clone()) .await?; - if let Some(previous_key) = self.key_tracker.previous_key.clone() { - self.vault.delete_aead_secret_key(previous_key).await?; + + if let Some(previous_key) = &self.key_tracker.previous_key { + self.vault + .delete_aead_secret_key(previous_key.clone()) + .await?; }; + + if let Some((_, key_handle)) = &self.rekey_cache { + self.vault + .delete_aead_secret_key(key_handle.clone()) + .await?; + } + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs index b7e7db73b28..7a6fa90cec7 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs @@ -4,8 +4,7 @@ use ockam_core::{Error, Result}; use ockam_vault::{AeadSecretKeyHandle, VaultForSecureChannels}; use tracing_attributes::instrument; -use crate::secure_channel::handshake::handshake::AES_GCM_TAGSIZE; -use crate::{Nonce, MAX_NONCE, NOISE_NONCE_LEN}; +use crate::{Nonce, NOISE_NONCE_LEN}; pub(crate) struct Encryptor { key: AeadSecretKeyHandle, @@ -20,28 +19,6 @@ pub(crate) struct Encryptor { pub(crate) const KEY_RENEWAL_INTERVAL: u64 = 32; impl Encryptor { - #[instrument(skip_all)] - pub async fn rekey( - vault: &Arc, - key: &AeadSecretKeyHandle, - ) -> Result { - let mut new_key_buffer = vec![0u8; 32 + AES_GCM_TAGSIZE]; - vault - .aead_encrypt( - key, - new_key_buffer.as_mut_slice(), - &MAX_NONCE.to_aes_gcm_nonce(), - &[], - ) - .await?; - - let buffer = vault - .import_secret_buffer(new_key_buffer[0..32].to_vec()) - .await?; - - vault.convert_secret_buffer_to_aead_key(buffer).await - } - #[instrument(skip_all)] pub async fn encrypt(&mut self, payload: &mut [u8]) -> Result<()> { let current_nonce = self.nonce; @@ -52,7 +29,7 @@ impl Encryptor { && current_nonce.value() > 0 && current_nonce.value() % KEY_RENEWAL_INTERVAL == 0 { - let new_key = Self::rekey(&self.vault, &self.key).await?; + let new_key = self.vault.rekey(&self.key, 1).await?; let old_key = core::mem::replace(&mut self.key, new_key); self.vault.delete_aead_secret_key(old_key).await?; } @@ -71,6 +48,14 @@ impl Encryptor { Ok(()) } + #[instrument(skip_all)] + pub async fn manual_rekey(&mut self) -> Result<()> { + let new_key = self.vault.rekey(&self.key, 1).await?; + let old_key = core::mem::replace(&mut self.key, new_key); + self.vault.delete_aead_secret_key(old_key).await?; + Ok(()) + } + pub fn new( key: AeadSecretKeyHandle, nonce: Nonce, diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs index b958fdac970..c0587a6aa5f 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs @@ -133,28 +133,46 @@ impl EncryptorWorker { // Decode raw payload binary let request = EncryptionRequest::decode(&msg.payload)?; + // If encryption fails, that means we have some internal error, + // and we may be in an invalid state, it's better to stop the Worker let mut should_stop = false; - let len = NOISE_NONCE_LEN + request.0.len() + AES_GCM_TAGSIZE; - let mut encrypted_payload = vec![0u8; len]; - encrypted_payload[NOISE_NONCE_LEN..len - AES_GCM_TAGSIZE].copy_from_slice(&request.0); - - // Encrypt the message - let response = match self - .encryptor - .encrypt(encrypted_payload.as_mut_slice()) - .await - { - Ok(()) => EncryptionResponse::Ok(encrypted_payload), - // If encryption failed, that means we have some internal error, - // and we may be in an invalid state, it's better to stop the Worker - Err(err) => { - should_stop = true; - error!( - "Error while encrypting: {err} at: {}", - self.addresses.encryptor - ); - EncryptionResponse::Err(err) + let response = match request { + EncryptionRequest::Encrypt(plaintext) => { + let len = NOISE_NONCE_LEN + plaintext.len() + AES_GCM_TAGSIZE; + let mut encrypted_payload = vec![0u8; len]; + encrypted_payload[NOISE_NONCE_LEN..len - AES_GCM_TAGSIZE] + .copy_from_slice(&plaintext); + + // Encrypt the message + match self + .encryptor + .encrypt(encrypted_payload.as_mut_slice()) + .await + { + Ok(()) => EncryptionResponse::Ok(encrypted_payload), + // If encryption failed, that means we have some internal error, + // and we may be in an invalid state, it's better to stop the Worker + Err(err) => { + should_stop = true; + error!( + "Error while encrypting: {err} at: {}", + self.addresses.encryptor + ); + EncryptionResponse::Err(err) + } + } } + EncryptionRequest::Rekey => match self.encryptor.manual_rekey().await { + Ok(()) => EncryptionResponse::Ok(Vec::new()), + Err(err) => { + should_stop = true; + error!( + "Error while rekeying: {err} at: {}", + self.addresses.encryptor + ); + EncryptionResponse::Err(err) + } + }, }; // Send the reply to the caller diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/key_tracker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/key_tracker.rs index d0c48339e65..d600a4c7837 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/key_tracker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/key_tracker.rs @@ -23,6 +23,10 @@ impl KeyTracker { renewal_interval, } } + + pub(crate) fn number_of_rekeys(&self) -> u64 { + self.number_of_rekeys + } } impl KeyTracker { diff --git a/implementations/rust/ockam/ockam_identity/tests/channel.rs b/implementations/rust/ockam/ockam_identity/tests/channel.rs index bcf55d3112d..2086b1e5986 100644 --- a/implementations/rust/ockam/ockam_identity/tests/channel.rs +++ b/implementations/rust/ockam/ockam_identity/tests/channel.rs @@ -10,9 +10,9 @@ use ockam_identity::models::{CredentialSchemaIdentifier, Identifier}; use ockam_identity::secure_channels::secure_channels; use ockam_identity::utils::AttributesBuilder; use ockam_identity::{ - DecryptionResponse, EncryptionRequest, EncryptionResponse, IdentityAccessControlBuilder, - SecureChannelListenerOptions, SecureChannelOptions, SecureChannels, TrustEveryonePolicy, - TrustIdentifierPolicy, Vault, + DecryptionRequest, DecryptionResponse, EncryptionRequest, EncryptionResponse, + IdentityAccessControlBuilder, SecureChannelListenerOptions, SecureChannelOptions, + SecureChannels, TrustEveryonePolicy, TrustIdentifierPolicy, Vault, }; use ockam_node::{Context, MessageReceiveOptions, WorkerBuilder}; use ockam_vault::{ @@ -464,7 +464,7 @@ async fn test_channel_api(ctx: &mut Context) -> Result<()> { let encrypted_alice: EncryptionResponse = ctx .send_and_receive( route![alice_channel_data.encryptor_api_address().clone()], - EncryptionRequest(b"Ping".to_vec()), + EncryptionRequest::Encrypt(b"Ping".to_vec()), ) .await?; let encrypted_alice = match encrypted_alice { @@ -475,7 +475,7 @@ async fn test_channel_api(ctx: &mut Context) -> Result<()> { let encrypted_bob: EncryptionResponse = ctx .send_and_receive( route![bob_channel_data.encryptor_api_address().clone()], - EncryptionRequest(b"Pong".to_vec()), + EncryptionRequest::Encrypt(b"Pong".to_vec()), ) .await?; let encrypted_bob = match encrypted_bob { @@ -486,7 +486,7 @@ async fn test_channel_api(ctx: &mut Context) -> Result<()> { let decrypted_alice: DecryptionResponse = ctx .send_and_receive( route![alice_channel_data.decryptor_api_address().clone()], - encrypted_bob, + DecryptionRequest(encrypted_bob, None), ) .await?; let decrypted_alice = match decrypted_alice { @@ -497,7 +497,7 @@ async fn test_channel_api(ctx: &mut Context) -> Result<()> { let decrypted_bob: DecryptionResponse = ctx .send_and_receive( route![bob_channel_data.decryptor_api_address().clone()], - encrypted_alice, + DecryptionRequest(encrypted_alice, None), ) .await?; let decrypted_bob = match decrypted_bob { diff --git a/implementations/rust/ockam/ockam_identity/tests/persistence.rs b/implementations/rust/ockam/ockam_identity/tests/persistence.rs index 3e859728d64..b9b22860c99 100644 --- a/implementations/rust/ockam/ockam_identity/tests/persistence.rs +++ b/implementations/rust/ockam/ockam_identity/tests/persistence.rs @@ -53,7 +53,7 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let EncryptionResponse::Ok(encrypted_msg1_alice) = ctx .send_and_receive( route![alice_channel.encryptor_api_address().clone()], - EncryptionRequest(msg1_alice.clone()), + EncryptionRequest::Encrypt(msg1_alice.clone()), ) .await? else { @@ -62,7 +62,7 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let EncryptionResponse::Ok(encrypted_msg2_alice) = ctx .send_and_receive( route![alice_channel.encryptor_api_address().clone()], - EncryptionRequest(msg2_alice.clone()), + EncryptionRequest::Encrypt(msg2_alice.clone()), ) .await? else { @@ -71,7 +71,7 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let EncryptionResponse::Ok(encrypted_msg1_bob) = ctx .send_and_receive( route![bob_channel.encryptor_api_address().clone()], - EncryptionRequest(msg1_bob.clone()), + EncryptionRequest::Encrypt(msg1_bob.clone()), ) .await? else { @@ -80,7 +80,7 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let EncryptionResponse::Ok(encrypted_msg2_bob) = ctx .send_and_receive( route![bob_channel.encryptor_api_address().clone()], - EncryptionRequest(msg2_bob.clone()), + EncryptionRequest::Encrypt(msg2_bob.clone()), ) .await? else { @@ -90,7 +90,7 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg1_alice) = ctx .send_and_receive( route![bob_channel.decryptor_api_address().clone()], - DecryptionRequest(encrypted_msg1_alice), + DecryptionRequest(encrypted_msg1_alice, None), ) .await? else { @@ -100,7 +100,7 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg2_alice) = ctx .send_and_receive( route![bob_channel.decryptor_api_address().clone()], - DecryptionRequest(encrypted_msg2_alice), + DecryptionRequest(encrypted_msg2_alice, None), ) .await? else { @@ -110,7 +110,7 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg1_bob) = ctx .send_and_receive( route![alice_channel.decryptor_api_address().clone()], - DecryptionRequest(encrypted_msg1_bob), + DecryptionRequest(encrypted_msg1_bob, None), ) .await? else { @@ -120,7 +120,7 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg2_bob) = ctx .send_and_receive( route![alice_channel.decryptor_api_address().clone()], - DecryptionRequest(encrypted_msg2_bob), + DecryptionRequest(encrypted_msg2_bob, None), ) .await? else { @@ -222,7 +222,7 @@ fn test_persistence() -> ockam_core::Result<()> { let EncryptionResponse::Ok(encrypted_msg1_alice) = ctx1 .send_and_receive( route![alice_channel.encryptor_api_address().clone()], - EncryptionRequest(msg1_alice.clone()), + EncryptionRequest::Encrypt(msg1_alice.clone()), ) .await? else { @@ -231,7 +231,7 @@ fn test_persistence() -> ockam_core::Result<()> { let EncryptionResponse::Ok(encrypted_msg2_alice) = ctx1 .send_and_receive( route![alice_channel.encryptor_api_address().clone()], - EncryptionRequest(msg2_alice.clone()), + EncryptionRequest::Encrypt(msg2_alice.clone()), ) .await? else { @@ -240,7 +240,7 @@ fn test_persistence() -> ockam_core::Result<()> { let EncryptionResponse::Ok(encrypted_msg1_bob) = ctx1 .send_and_receive( route![bob_channel.encryptor_api_address().clone()], - EncryptionRequest(msg1_bob.clone()), + EncryptionRequest::Encrypt(msg1_bob.clone()), ) .await? else { @@ -249,7 +249,7 @@ fn test_persistence() -> ockam_core::Result<()> { let EncryptionResponse::Ok(encrypted_msg2_bob) = ctx1 .send_and_receive( route![bob_channel.encryptor_api_address().clone()], - EncryptionRequest(msg2_bob.clone()), + EncryptionRequest::Encrypt(msg2_bob.clone()), ) .await? else { @@ -326,7 +326,7 @@ fn test_persistence() -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg1_alice) = ctx2 .send_and_receive( route![data.decryptor_api_address_bob.clone()], - DecryptionRequest(data.encrypted_msg1_alice), + DecryptionRequest(data.encrypted_msg1_alice, None), ) .await? else { @@ -336,7 +336,7 @@ fn test_persistence() -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg2_alice) = ctx2 .send_and_receive( route![data.decryptor_api_address_bob.clone()], - DecryptionRequest(data.encrypted_msg2_alice), + DecryptionRequest(data.encrypted_msg2_alice, None), ) .await? else { @@ -346,7 +346,7 @@ fn test_persistence() -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg1_bob) = ctx2 .send_and_receive( route![data.decryptor_api_address_alice.clone()], - DecryptionRequest(data.encrypted_msg1_bob), + DecryptionRequest(data.encrypted_msg1_bob, None), ) .await? else { @@ -356,7 +356,7 @@ fn test_persistence() -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg2_bob) = ctx2 .send_and_receive( route![data.decryptor_api_address_alice.clone()], - DecryptionRequest(data.encrypted_msg2_bob), + DecryptionRequest(data.encrypted_msg2_bob, None), ) .await? else { diff --git a/implementations/rust/ockam/ockam_vault/src/error.rs b/implementations/rust/ockam/ockam_vault/src/error.rs index 687431f691a..aea6087e635 100644 --- a/implementations/rust/ockam/ockam_vault/src/error.rs +++ b/implementations/rust/ockam/ockam_vault/src/error.rs @@ -35,6 +35,8 @@ pub enum VaultError { InvalidSignatureSize, /// Aead secret was not found in the storage AeadSecretNotFound, + /// Invalid rekey count + InvalidRekeyCount, /// Buffer is too short during encryption InsufficientEncryptBuffer, /// Buffer is too short during decryption @@ -61,6 +63,7 @@ impl core::fmt::Display for VaultError { Self::InvalidSha256Len => write!(f, "invalid sha256 len"), Self::InvalidSignatureSize => write!(f, "invalid signature len"), Self::AeadSecretNotFound => write!(f, "aead secret was not found in the storage"), + Self::InvalidRekeyCount => write!(f, "invalid rekey count"), Self::InsufficientEncryptBuffer => write!(f, "insufficient encrypt buffer"), Self::InsufficientDecryptBuffer => write!(f, "insufficient decrypt buffer"), } diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs index 0cce4e24ef5..96340d96f59 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs @@ -22,6 +22,8 @@ use crate::{ use super::make_aes; +const AES_GCM_TAGSIZE: usize = 16; + /// [`SecureChannelVault`] implementation using software pub struct SoftwareVaultForSecureChannels { ephemeral_buffer_secrets: Arc>>, @@ -307,6 +309,36 @@ impl VaultForSecureChannels for SoftwareVaultForSecureChannels { Ok(plaintext) } + #[instrument(skip_all)] + async fn rekey( + &self, + secret_key_handle: &AeadSecretKeyHandle, + n: u16, + ) -> Result { + if n == 0 { + return Err(VaultError::InvalidRekeyCount)?; + } + + const MAX_NONCE: [u8; 12] = [ + 0x00, 0x00, 0x00, 0x00, // we only use 8 bytes of nonce + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // u64::MAX + ]; + + let mut new_key_buffer = vec![0u8; 32 + AES_GCM_TAGSIZE]; + let mut counter = n; + + while counter > 0 { + let secret = self.get_aead_secret(secret_key_handle).await?; + let aes = make_aes(&secret); + aes.encrypt_message(&mut new_key_buffer, &MAX_NONCE, &[])?; + + counter -= 1; + } + + let buffer = self.import_secret_buffer(new_key_buffer).await?; + self.convert_secret_buffer_to_aead_key(buffer).await + } + #[instrument(skip_all)] async fn persist_aead_key(&self, secret_key_handle: &AeadSecretKeyHandle) -> Result<()> { let secret = self.get_aead_secret(secret_key_handle).await?; diff --git a/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs b/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs index 4edec6998e1..8c3a310e76d 100644 --- a/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs +++ b/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs @@ -58,6 +58,14 @@ pub trait VaultForSecureChannels: Send + Sync + 'static { aad: &[u8], ) -> Result<&'a mut [u8]>; + /// Perform rekey `n`. times. `n` must be greater than 0. + /// [1]: http://www.noiseprotocol.org/noise.html#cipher-functions + async fn rekey( + &self, + secret_key_handle: &AeadSecretKeyHandle, + n: u16, + ) -> Result; + /// Persist an existing AEAD key. async fn persist_aead_key(&self, secret_key_handle: &AeadSecretKeyHandle) -> Result<()>; From 8f77e1106fa8f69d6992ae02d8984b2164e94395 Mon Sep 17 00:00:00 2001 From: Davide Baldo Date: Mon, 4 Nov 2024 11:31:20 +0100 Subject: [PATCH 2/7] chore(rust): removed `NodeManager` dependency when instantiating a `MultiAddr` --- .../ockam_api/src/nodes/connection/mod.rs | 47 +++++++++-- .../src/nodes/connection/plain_tcp.rs | 19 ++--- .../ockam_api/src/nodes/connection/project.rs | 79 +++++++++++++------ .../ockam_api/src/nodes/connection/secure.rs | 64 ++++++++++----- .../ockam_api/src/nodes/service/manager.rs | 54 +++++-------- 5 files changed, 166 insertions(+), 97 deletions(-) diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs index 3d97e591ea5..aaddc827ce6 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs @@ -19,6 +19,7 @@ pub(crate) use plain_tcp::PlainTcpInstantiator; pub(crate) use project::ProjectInstantiator; pub(crate) use secure::SecureChannelInstantiator; use std::fmt::{Debug, Formatter}; +use std::sync::Arc; #[derive(Clone)] pub struct Connection { @@ -181,13 +182,49 @@ pub trait Instantiator: Send + Sync + 'static { /// The returned [`Changes`] will be used to update the builder state. async fn instantiate( &self, - ctx: &Context, - node_manager: &NodeManager, + context: &Context, transport_route: Route, extracted: (MultiAddr, MultiAddr, MultiAddr), ) -> Result; } +/// Aggregates multiple [`Instantiator`]s, having a single object containing +/// all runtime dependencies. +pub struct ConnectionInstantiator { + instantiator: Vec>, +} + +impl ConnectionInstantiator { + pub fn new() -> Self { + ConnectionInstantiator { + instantiator: vec![], + } + } + + pub fn add(mut self, instantiator: impl Instantiator) -> Self { + self.instantiator.push(Arc::new(instantiator)); + self + } + + /// Resolve project ID (if any), create secure channel (if needed) and create a tcp connection + /// Returns [`Connection`] + pub async fn connect(&self, ctx: &Context, addr: &MultiAddr) -> Result { + debug!("connecting to {}", &addr); + + let mut connection_builder = ConnectionBuilder::new(addr.clone()); + for instantiator in self.instantiator.clone() { + connection_builder = connection_builder + .instantiate(ctx, instantiator.as_ref()) + .await?; + } + let connection = connection_builder.build(); + connection.add_default_consumers(ctx); + + debug!("connected to {connection:?}"); + Ok(connection) + } +} + impl ConnectionBuilder { pub fn new(multi_addr: MultiAddr) -> Self { ConnectionBuilder { @@ -214,11 +251,10 @@ impl ConnectionBuilder { /// Used to instantiate a connection from a [`MultiAddr`] /// when called multiple times the instantiator order matters and it's up to the /// user make sure higher protocol abstraction are called before lower level ones - pub async fn instantiate( + pub async fn instantiate( mut self, ctx: &Context, - node_manager: &NodeManager, - instantiator: impl Instantiator, + instantiator: &T, ) -> Result { //executing a regex-like search, shifting the starting point one by one //not efficient by any mean, but it shouldn't be an issue @@ -240,7 +276,6 @@ impl ConnectionBuilder { let mut changes = instantiator .instantiate( ctx, - node_manager, self.transport_route.clone(), self.extract(start, instantiator.matches().len()), ) diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/plain_tcp.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/plain_tcp.rs index faaa3eb4199..baf94ea597d 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/plain_tcp.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/plain_tcp.rs @@ -2,18 +2,20 @@ use crate::error::ApiError; use crate::nodes::connection::{Changes, ConnectionBuilder, Instantiator}; use crate::{multiaddr_to_route, route_to_multiaddr}; -use crate::nodes::NodeManager; -use ockam_core::{async_trait, Error, Route}; +use ockam_core::{async_trait, Route}; use ockam_multiaddr::proto::{DnsAddr, Ip4, Ip6, Tcp}; use ockam_multiaddr::{Match, MultiAddr, Protocol}; use ockam_node::Context; +use ockam_transport_tcp::TcpTransport; /// Creates the tcp connection. -pub(crate) struct PlainTcpInstantiator {} +pub(crate) struct PlainTcpInstantiator { + tcp_transport: TcpTransport, +} impl PlainTcpInstantiator { - pub(crate) fn new() -> Self { - Self {} + pub(crate) fn new(tcp_transport: TcpTransport) -> Self { + Self { tcp_transport } } } @@ -29,14 +31,13 @@ impl Instantiator for PlainTcpInstantiator { async fn instantiate( &self, - _ctx: &Context, - node_manager: &NodeManager, + _context: &Context, _transport_route: Route, extracted: (MultiAddr, MultiAddr, MultiAddr), - ) -> Result { + ) -> Result { let (before, tcp_piece, after) = extracted; - let mut tcp = multiaddr_to_route(&tcp_piece, &node_manager.tcp_transport) + let mut tcp = multiaddr_to_route(&tcp_piece, &self.tcp_transport) .await .ok_or_else(|| { ApiError::core(format!( diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs index 154d6cba092..644447f2ebc 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs @@ -1,28 +1,40 @@ use crate::error::ApiError; use crate::nodes::connection::{Changes, Instantiator}; -use crate::nodes::NodeManager; -use crate::{multiaddr_to_route, try_address_to_multiaddr}; +use crate::{multiaddr_to_route, try_address_to_multiaddr, CliState}; +use std::sync::Arc; -use ockam_core::{async_trait, Error, Route}; +use ockam_core::{async_trait, Route}; use ockam_multiaddr::proto::Project; use ockam_multiaddr::{Match, MultiAddr, Protocol}; use ockam_node::Context; -use crate::nodes::service::SecureChannelType; -use ockam::identity::Identifier; +use ockam::identity::{Identifier, SecureChannelOptions, SecureChannels}; +use ockam_transport_tcp::TcpTransport; use std::time::Duration; /// Creates a secure connection to the project using provided credential pub(crate) struct ProjectInstantiator { identifier: Identifier, timeout: Option, + cli_state: CliState, + secure_channels: Arc, + tcp_transport: TcpTransport, } impl ProjectInstantiator { - pub fn new(identifier: Identifier, timeout: Option) -> Self { + pub fn new( + identifier: Identifier, + timeout: Option, + cli_state: CliState, + secure_channels: Arc, + tcp_transport: TcpTransport, + ) -> Self { Self { identifier, timeout, + cli_state, + secure_channels, + tcp_transport, } } } @@ -35,11 +47,10 @@ impl Instantiator for ProjectInstantiator { async fn instantiate( &self, - ctx: &Context, - node_manager: &NodeManager, + context: &Context, _transport_route: Route, extracted: (MultiAddr, MultiAddr, MultiAddr), - ) -> Result { + ) -> Result { let (_before, project_piece, after) = extracted; let project_protocol_value = project_piece @@ -50,11 +61,25 @@ impl Instantiator for ProjectInstantiator { .cast::() .ok_or_else(|| ApiError::core("invalid project protocol in multiaddr"))?; - let (project_multiaddr, project_identifier) = - node_manager.resolve_project(&project).await?; + let (project_multiaddr, project_identifier) = self + .cli_state + .projects() + .get_project_by_name(&project) + .await + .map(|project| { + ( + project.project_multiaddr().cloned(), + project + .project_identifier() + .ok_or_else(|| ApiError::core("project identifier is missing")), + ) + })?; + + let project_identifier = project_identifier?; + let project_multiaddr = project_multiaddr?; debug!(addr = %project_multiaddr, "creating secure channel"); - let tcp = multiaddr_to_route(&project_multiaddr, &node_manager.tcp_transport) + let tcp = multiaddr_to_route(&project_multiaddr, &self.tcp_transport) .await .ok_or_else(|| { ApiError::core(format!( @@ -63,27 +88,29 @@ impl Instantiator for ProjectInstantiator { })?; debug!("create a secure channel to the project {project_identifier}"); - let sc = node_manager - .create_secure_channel_internal( - ctx, - tcp.route, - &self.identifier.clone(), - Some(vec![project_identifier]), - None, - self.timeout, - SecureChannelType::KeyExchangeAndMessages, - ) + + let options = SecureChannelOptions::new().with_authority(project_identifier); + let options = if let Some(timeout) = self.timeout { + options.with_timeout(timeout) + } else { + options + }; + + let secure_channel = self + .secure_channels + .create_secure_channel(context, &self.identifier.clone(), tcp.route, options) .await?; - // when creating a secure channel we want the route to pass through that + // when creating a secure channel, we want the route to pass through that // ignoring previous steps, since they will be implicit - let mut current_multiaddr = try_address_to_multiaddr(sc.encryptor_address()).unwrap(); + let mut current_multiaddr = + try_address_to_multiaddr(secure_channel.encryptor_address()).unwrap(); current_multiaddr.try_extend(after.iter())?; Ok(Changes { - flow_control_id: Some(sc.flow_control_id().clone()), + flow_control_id: Some(secure_channel.flow_control_id().clone()), current_multiaddr, - secure_channel_encryptors: vec![sc.encryptor_address().clone()], + secure_channel_encryptors: vec![secure_channel.encryptor_address().clone()], tcp_connection: tcp.tcp_connection, }) } diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs index 7903c177764..9592d702a05 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs @@ -1,12 +1,14 @@ +use std::sync::Arc; use std::time::Duration; use crate::nodes::connection::{Changes, Instantiator}; -use crate::nodes::NodeManager; use crate::{local_multiaddr_to_route, try_address_to_multiaddr}; -use crate::nodes::service::SecureChannelType; -use ockam::identity::Identifier; -use ockam_core::{async_trait, route, AsyncTryClone, Error, Route}; +use ockam::identity::{ + Identifier, SecureChannelOptions, SecureChannels, TrustEveryonePolicy, + TrustMultiIdentifiersPolicy, +}; +use ockam_core::{async_trait, route, Route}; use ockam_multiaddr::proto::Secure; use ockam_multiaddr::{Match, MultiAddr, Protocol}; use ockam_node::Context; @@ -16,6 +18,8 @@ pub(crate) struct SecureChannelInstantiator { identifier: Identifier, authorized_identities: Option>, timeout: Option, + secure_channels: Arc, + authority: Option, } impl SecureChannelInstantiator { @@ -23,11 +27,15 @@ impl SecureChannelInstantiator { identifier: &Identifier, timeout: Option, authorized_identities: Option>, + authority: Option, + secure_channels: Arc, ) -> Self { Self { identifier: identifier.clone(), authorized_identities, + authority, timeout, + secure_channels, } } } @@ -40,39 +48,53 @@ impl Instantiator for SecureChannelInstantiator { async fn instantiate( &self, - ctx: &Context, - node_manager: &NodeManager, + context: &Context, transport_route: Route, extracted: (MultiAddr, MultiAddr, MultiAddr), - ) -> Result { + ) -> Result { let (_before, secure_piece, after) = extracted; debug!(%secure_piece, %transport_route, "creating secure channel"); let route = local_multiaddr_to_route(&secure_piece)?; - let sc_ctx = ctx.async_try_clone().await?; - let sc = node_manager - .create_secure_channel_internal( - &sc_ctx, - //the transport route is needed to reach the secure channel listener - //since it can be in another node - route![transport_route, route], + let options = SecureChannelOptions::new(); + + let options = match self.authorized_identities.clone() { + Some(ids) => options.with_trust_policy(TrustMultiIdentifiersPolicy::new(ids)), + None => options.with_trust_policy(TrustEveryonePolicy), + }; + + let options = if let Some(authority) = self.authority.clone() { + options.with_authority(authority) + } else { + options + }; + + let options = if let Some(timeout) = self.timeout { + options.with_timeout(timeout) + } else { + options + }; + + let secure_channel = self + .secure_channels + .create_secure_channel( + context, &self.identifier, - self.authorized_identities.clone(), - None, - self.timeout, - SecureChannelType::KeyExchangeAndMessages, + route![transport_route, route], + options, ) .await?; // when creating a secure channel we want the route to pass through that // ignoring previous steps, since they will be implicit - let mut current_multiaddr = try_address_to_multiaddr(sc.encryptor_address()).unwrap(); + let mut current_multiaddr = + try_address_to_multiaddr(secure_channel.encryptor_address()).unwrap(); current_multiaddr.try_extend(after.iter())?; Ok(Changes { current_multiaddr, - flow_control_id: Some(sc.flow_control_id().clone()), - secure_channel_encryptors: vec![sc.encryptor_address().clone()], + flow_control_id: Some(secure_channel.flow_control_id().clone()), + secure_channel_encryptors: vec![secure_channel.encryptor_address().clone()], tcp_connection: None, }) } diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs index fbe0d18bbe4..7ab884e24e0 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs @@ -1,7 +1,7 @@ use crate::cloud::project::Project; use crate::cloud::{AuthorityNodeClient, ControllerClient, CredentialsEnabled, ProjectNodeClient}; use crate::nodes::connection::{ - Connection, ConnectionBuilder, PlainTcpInstantiator, ProjectInstantiator, + Connection, ConnectionInstantiator, PlainTcpInstantiator, ProjectInstantiator, SecureChannelInstantiator, }; use crate::nodes::models::portal::OutletStatus; @@ -292,41 +292,25 @@ impl NodeManager { timeout: Option, ) -> ockam_core::Result { let authorized = authorized.map(|authorized| vec![authorized]); - self.connect(ctx, addr, identifier, authorized, timeout) - .await - } - - /// Resolve project ID (if any), create secure channel (if needed) and create a tcp connection - /// Returns [`Connection`] - async fn connect( - &self, - ctx: &Context, - addr: &MultiAddr, - identifier: Identifier, - authorized: Option>, - timeout: Option, - ) -> ockam_core::Result { - debug!(?timeout, "connecting to {}", &addr); - let connection = ConnectionBuilder::new(addr.clone()) - .instantiate( - ctx, - self, - ProjectInstantiator::new(identifier.clone(), timeout), - ) - .await? - .instantiate(ctx, self, PlainTcpInstantiator::new()) - .await? - .instantiate( - ctx, - self, - SecureChannelInstantiator::new(&identifier, timeout, authorized), - ) - .await? - .build(); - connection.add_default_consumers(ctx); - debug!("connected to {connection:?}"); - Ok(connection) + let connection_instantiator = ConnectionInstantiator::new() + .add(ProjectInstantiator::new( + identifier.clone(), + timeout, + self.cli_state.clone(), + self.secure_channels.clone(), + self.tcp_transport.clone(), + )) + .add(PlainTcpInstantiator::new(self.tcp_transport.clone())) + .add(SecureChannelInstantiator::new( + &identifier, + timeout, + authorized, + self.project_authority(), + self.secure_channels.clone(), + )); + + connection_instantiator.connect(ctx, addr).await } pub(crate) async fn resolve_project( From c76619d3722134d3169daad46d33a20a99baa3dd Mon Sep 17 00:00:00 2001 From: Davide Baldo Date: Wed, 30 Oct 2024 17:30:40 +0100 Subject: [PATCH 3/7] feat(rust): implemented ockam proxy vault --- Cargo.lock | 2 - .../rust/ockam/ockam_api/Cargo.toml | 1 - .../ockam_api/src/cli_state/cli_state.rs | 11 +- .../ockam_api/src/cli_state/identities.rs | 66 +- .../ockam/ockam_api/src/cli_state/nodes.rs | 46 +- .../src/cli_state/secure_channels.rs | 12 +- .../storage/vaults_repository_sql.rs | 120 +- .../ockam/ockam_api/src/cli_state/trust.rs | 124 +- .../ockam/ockam_api/src/cli_state/vaults.rs | 267 +++- .../rust/ockam/ockam_api/src/lib.rs | 3 + .../ockam_api/src/nodes/connection/mod.rs | 6 + .../ockam_api/src/nodes/connection/project.rs | 39 +- .../ockam_api/src/nodes/connection/secure.rs | 20 +- .../ockam_api/src/nodes/models/services.rs | 18 + .../ockam/ockam_api/src/nodes/registry.rs | 4 + .../src/nodes/service/default_address.rs | 2 + .../src/nodes/service/in_memory_node.rs | 11 +- .../ockam_api/src/nodes/service/manager.rs | 48 +- .../src/nodes/service/node_services.rs | 85 +- .../src/nodes/service/secure_channel.rs | 9 +- .../ockam_api/src/nodes/service/trust.rs | 66 +- .../ockam_api/src/nodes/service/worker.rs | 3 + .../ockam/ockam_api/src/proxy_vault/mod.rs | 4 + .../ockam_api/src/proxy_vault/protocol.rs | 1333 +++++++++++++++++ .../ockam/ockam_api/src/test_utils/mod.rs | 30 +- .../ockam/ockam_api/tests/common/common.rs | 2 +- .../ockam/ockam_api/tests/common/session.rs | 2 +- .../src/shared_service/relay/create.rs | 2 +- .../tcp_outlet/invitation_access_control.rs | 2 +- .../rust/ockam/ockam_app_lib/src/state/mod.rs | 2 +- .../ockam_command/src/authority/create.rs | 24 +- .../ockam_command/src/credential/issue.rs | 2 +- .../ockam/ockam_command/src/enroll/command.rs | 4 +- .../ockam_command/src/identity/create.rs | 23 +- .../ockam_command/src/identity/default.rs | 5 +- .../ockam/ockam_command/src/identity/show.rs | 3 +- .../ockam/ockam_command/src/message/send.rs | 5 +- .../ockam/ockam_command/src/node/create.rs | 9 +- .../src/node/create/foreground.rs | 1 + .../ockam/ockam_command/src/project/enroll.rs | 18 +- .../ockam/ockam_command/src/project/ticket.rs | 2 +- .../src/project_member/delete.rs | 2 +- .../ockam_command/src/project_member/mod.rs | 2 +- .../src/secure_channel/create.rs | 7 +- .../ockam/ockam_command/src/service/start.rs | 26 + .../ockam_command/src/space_admin/delete.rs | 2 +- .../rust/ockam/ockam_command/src/util/api.rs | 12 +- .../rust/ockam/ockam_command/src/util/mod.rs | 2 +- .../ockam/ockam_command/src/vault/create.rs | 68 +- .../ockam/ockam_command/src/vault/util.rs | 41 +- .../tests/bats/kafka/docker.bats | 133 +- .../tests/bats/orchestrator/remote_vault.bats | 86 ++ .../20241030100000_add_remote_ockam_vault.sql | 11 + .../20241030100000_add_remote_ockam_vault.sql | 11 + .../rust/ockam/ockam_transport_tcp/Cargo.toml | 1 - .../src/portal/inlet_listener.rs | 2 +- .../src/portal/tls_certificate.rs | 2 +- .../src/privileged_portal/ebpf_support.rs | 3 +- .../privileged_portal/privileged_portals.rs | 2 +- .../raw_socket/async_fd_packet_reader.rs | 2 +- .../raw_socket/async_fd_packet_writer.rs | 2 +- .../src/privileged_portal/transport.rs | 2 +- .../workers/internal_processor.rs | 2 +- .../workers/raw_socket_processor.rs | 2 +- .../workers/remote_worker.rs | 3 +- .../ockam_transport_tcp/tests/ebpf_portal.rs | 2 +- .../src/traits/vault_for_secure_channels.rs | 7 +- .../ockam/ockam_vault/src/types/hashes.rs | 24 +- .../rust/ockam/ockam_vault/src/types/mod.rs | 9 + .../ockam_vault/src/types/public_keys.rs | 21 +- .../ockam/ockam_vault/src/types/secrets.rs | 35 +- .../ockam/ockam_vault/src/types/signatures.rs | 2 +- tools/stress-test/src/main.rs | 2 +- 73 files changed, 2675 insertions(+), 289 deletions(-) create mode 100644 implementations/rust/ockam/ockam_api/src/proxy_vault/mod.rs create mode 100644 implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs create mode 100644 implementations/rust/ockam/ockam_command/tests/bats/orchestrator/remote_vault.bats create mode 100644 implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20241030100000_add_remote_ockam_vault.sql create mode 100644 implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/sqlite/20241030100000_add_remote_ockam_vault.sql diff --git a/Cargo.lock b/Cargo.lock index 20230e6fb2e..16f6000116f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4727,7 +4727,6 @@ dependencies = [ "jaq-parse", "jaq-std", "kafka-protocol", - "log", "miette", "minicbor", "mockall", @@ -5090,7 +5089,6 @@ dependencies = [ "caps", "cfg-if", "cfg_aliases 0.2.1", - "log", "minicbor", "nix 0.29.0", "ockam_core", diff --git a/implementations/rust/ockam/ockam_api/Cargo.toml b/implementations/rust/ockam/ockam_api/Cargo.toml index bf34684ce27..f078ac14a9c 100644 --- a/implementations/rust/ockam/ockam_api/Cargo.toml +++ b/implementations/rust/ockam/ockam_api/Cargo.toml @@ -74,7 +74,6 @@ jaq-interpret = "1" jaq-parse = "1" jaq-std = "1" kafka-protocol = "0.13" -log = "0.4" miette = { version = "7.2.0", features = ["fancy-no-backtrace"] } minicbor = { version = "0.25.1", default-features = false, features = ["alloc", "derive"] } nix = { version = "0.29", features = ["signal"] } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs b/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs index ee606c7eeb8..82edd9a974c 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs @@ -1,11 +1,10 @@ -use rand::random; -use std::path::{Path, PathBuf}; -use tokio::sync::broadcast::{channel, Receiver, Sender}; - use ockam::SqlxDatabase; use ockam_core::env::get_env_with_default; use ockam_node::database::DatabaseConfiguration; use ockam_node::Executor; +use rand::random; +use std::path::{Path, PathBuf}; +use tokio::sync::broadcast::{channel, Receiver, Sender}; use crate::cli_state::error::Result; use crate::cli_state::CliStateError; @@ -343,10 +342,10 @@ mod tests { // create 2 identities let identity1 = cli - .create_identity_with_name_and_vault("identity1", "vault1") + .create_identity_with_name_and_vault(None, "identity1", "vault1") .await?; let identity2 = cli - .create_identity_with_name_and_vault("identity2", "vault2") + .create_identity_with_name_and_vault(None, "identity2", "vault2") .await?; // create 2 nodes diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs b/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs index ec81a1a9526..4e4152a85fe 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs @@ -3,6 +3,7 @@ use ockam::identity::models::ChangeHistory; use ockam::identity::{Identifier, Identity}; use ockam_core::errcode::{Kind, Origin}; use ockam_core::Error; +use ockam_node::Context; use ockam_vault::{HandleToSecret, SigningSecretKeyHandle}; use crate::cli_state::{random_name, CliState, Result}; @@ -31,6 +32,7 @@ impl CliState { #[instrument(skip_all, fields(name = %name, vault_name = %vault_name))] pub async fn create_identity_with_name_and_vault( &self, + context: Option<&Context>, name: &str, vault_name: &str, ) -> Result { @@ -39,7 +41,9 @@ impl CliState { }; let vault = self.get_named_vault(vault_name).await?; - let identities = self.make_identities(self.make_vault(vault).await?).await?; + let vault = self.make_vault(context, vault).await?; + + let identities = self.make_identities(vault).await?; let identity = identities.identities_creation().create_identity().await?; let named_identity = self .store_named_identity(&identity, name, vault_name) @@ -65,9 +69,13 @@ impl CliState { /// Create an identity associated with a name, using the default vault /// If there is already an identity with that name, return its identifier #[instrument(skip_all, fields(name = %name))] - pub async fn create_identity_with_name(&self, name: &str) -> Result { + pub async fn create_identity_with_name( + &self, + context: Option<&Context>, + name: &str, + ) -> Result { let vault = self.get_or_create_default_named_vault().await?; - self.create_identity_with_name_and_vault(name, &vault.name()) + self.create_identity_with_name_and_vault(context, name, &vault.name()) .await } @@ -77,6 +85,7 @@ impl CliState { #[instrument(skip_all, fields(name = %name, vault_name = %vault_name, key_id = %key_id))] pub async fn create_identity_with_key_id( &self, + context: Option<&Context>, name: &str, vault_name: &str, key_id: &str, @@ -96,8 +105,10 @@ impl CliState { key_id.as_bytes().to_vec(), )); + let vault = self.make_vault(context, vault).await?; + // create the identity - let identities = self.make_identities(self.make_vault(vault).await?).await?; + let identities = self.make_identities(vault).await?; let identifier = identities .identities_creation() .identity_builder() @@ -154,13 +165,14 @@ impl CliState { #[instrument(skip_all, fields(name = name.clone()))] pub async fn get_named_identity_or_default( &self, + context: Option<&Context>, name: &Option, ) -> Result { match name { // Identity specified. Some(name) => self.get_named_identity(name).await, // No identity specified. - None => self.get_or_create_default_named_identity().await, + None => self.get_or_create_default_named_identity(context).await, } } @@ -191,7 +203,11 @@ impl CliState { /// Return a full identity from its name /// Use the default identity if no name is given #[instrument(skip_all, fields(name = name.clone()))] - pub async fn get_identity_by_optional_name(&self, name: &Option) -> Result { + pub async fn get_identity_by_optional_name( + &self, + context: Option<&Context>, + name: &Option, + ) -> Result { let named_identity = match name { Some(name) => { self.identities_repository() @@ -209,7 +225,7 @@ impl CliState { Some(identity) => { let change_history = self.get_change_history(&identity.identifier()).await?; let named_vault = self.get_named_vault(&identity.vault_name).await?; - let identity_vault = self.make_vault(named_vault).await?; + let identity_vault = self.make_vault(context, named_vault).await?; Ok(Identity::import_from_change_history( Some(&identity.identifier()), change_history, @@ -243,14 +259,23 @@ impl CliState { /// Return the name of the default identity. /// This function creates the default identity if it does not exist! #[instrument(skip_all)] - pub async fn get_default_identity_name(&self) -> Result { - Ok(self.get_or_create_default_named_identity().await?.name()) + pub async fn get_or_create_default_identity_name( + &self, + context: Option<&Context>, + ) -> Result { + Ok(self + .get_or_create_default_named_identity(context) + .await? + .name()) } /// Return the default named identity /// This function creates the default identity if it does not exist! #[instrument(skip_all)] - pub async fn get_or_create_default_named_identity(&self) -> Result { + pub async fn get_or_create_default_named_identity( + &self, + context: Option<&Context>, + ) -> Result { match self .identities_repository() .get_default_named_identity() @@ -263,7 +288,8 @@ impl CliState { self.notify_message(fmt_log!( "There is no default Identity on this machine, generating one...\n" )); - self.create_identity_with_name(&random_name()).await + self.create_identity_with_name(context, &random_name()) + .await } } } @@ -272,10 +298,14 @@ impl CliState { /// - the given name if defined /// - or the name of the default identity (which is created if it does not already exist!) #[instrument(skip_all, fields(name = name.clone()))] - pub async fn get_identity_name_or_default(&self, name: &Option) -> Result { + pub async fn get_or_create_identity_name_or_default( + &self, + context: Option<&Context>, + name: &Option, + ) -> Result { match name { Some(name) => Ok(name.clone()), - None => self.get_default_identity_name().await, + None => self.get_or_create_default_identity_name(context).await, } } @@ -472,14 +502,14 @@ mod tests { // then create an identity let identity_name = "identity-name"; let identity = cli - .create_identity_with_name_and_vault(identity_name, vault_name) + .create_identity_with_name_and_vault(None, identity_name, vault_name) .await?; let expected = cli.get_named_identity(identity_name).await?; assert_eq!(identity, expected); // don't recreate the identity if it already exists with that name let _ = cli - .create_identity_with_name_and_vault(identity_name, vault_name) + .create_identity_with_name_and_vault(None, identity_name, vault_name) .await?; let identities = cli.get_named_identities().await?; assert_eq!(identities.len(), 1); @@ -493,7 +523,7 @@ mod tests { // create an identity using the default vault let identity_name = "identity-name"; - let identity = cli.create_identity_with_name(identity_name).await?; + let identity = cli.create_identity_with_name(None, identity_name).await?; let expected = cli.get_named_identity(identity_name).await?; assert_eq!(identity, expected); @@ -509,7 +539,7 @@ mod tests { let cli = CliState::test().await?; // when we retrieve the default identity, we create it if it doesn't exist - let identity = cli.get_or_create_default_named_identity().await?; + let identity = cli.get_or_create_default_named_identity(None).await?; // when the identity is created there is a change history + a named identity let result = cli.get_change_history(&identity.identifier()).await; @@ -528,7 +558,7 @@ mod tests { #[tokio::test] async fn test_delete_identity() -> Result<()> { let cli = CliState::test().await?; - let identity = cli.create_identity_with_name("name").await?; + let identity = cli.create_identity_with_name(None, "name").await?; // when the identity is created there is a change history + a named identity let result = cli.get_change_history(&identity.identifier()).await; diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs b/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs index e2e90bc8c44..925daeb4244 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs @@ -1,3 +1,10 @@ +use crate::cli_state::{random_name, NamedVault, Result}; +use crate::cli_state::{CliState, CliStateError}; +use crate::cloud::project::Project; +use crate::colors::color_primary; +use crate::config::lookup::InternetAddress; +use crate::{fmt_warn, ConnectionStatus}; + use colorful::Colorful; use minicbor::{CborLen, Decode, Encode}; use nix::errno::Errno; @@ -9,6 +16,7 @@ use ockam_core::errcode::{Kind, Origin}; use ockam_core::Error; use ockam_multiaddr::proto::{DnsAddr, Node, Tcp}; use ockam_multiaddr::MultiAddr; +use ockam_node::Context; use serde::Serialize; use std::fmt::{Display, Formatter}; use std::path::PathBuf; @@ -16,14 +24,6 @@ use std::process; use std::time::Duration; use sysinfo::{Pid, ProcessStatus, ProcessesToUpdate, System}; -use crate::cli_state::{random_name, NamedVault, Result}; -use crate::cli_state::{CliState, CliStateError}; -use crate::cloud::project::Project; -use crate::colors::color_primary; -use crate::config::lookup::InternetAddress; - -use crate::{fmt_warn, ConnectionStatus}; - /// The methods below support the creation and update of local nodes impl CliState { /// Create a node, with some optional associated values, and start it @@ -31,13 +31,14 @@ impl CliState { ))] pub async fn start_node_with_optional_values( &self, + context: Option<&Context>, node_name: &str, identity_name: &Option, project_name: &Option, tcp_listener: Option<&TcpListener>, ) -> Result { let mut node = self - .create_node_with_optional_values(node_name, identity_name, project_name) + .create_node_with_optional_values(context, node_name, identity_name, project_name) .await?; if node.pid.is_none() { let pid = process::id(); @@ -61,13 +62,14 @@ impl CliState { ))] pub async fn create_node_with_optional_values( &self, + context: Option<&Context>, node_name: &str, identity_name: &Option, project_name: &Option, ) -> Result { let identity = match identity_name { Some(name) => self.get_named_identity(name).await?, - None => self.get_or_create_default_named_identity().await?, + None => self.get_or_create_default_named_identity(context).await?, }; let node = self .create_node_with_identifier(node_name, &identity.identifier()) @@ -78,9 +80,8 @@ impl CliState { /// This method creates a node with an associated identity /// The vault used to create the identity is the default vault - #[instrument(skip_all, fields(node_name = node_name))] - pub async fn create_node(&self, node_name: &str) -> Result { - let identity = self.create_identity_with_name(&random_name()).await?; + pub async fn create_test_node(&self, node_name: &str) -> Result { + let identity = self.create_identity_with_name(None, &random_name()).await?; self.create_node_with_identifier(node_name, &identity.identifier()) .await } @@ -712,7 +713,7 @@ mod tests { // a node can be created with just a name let node_name = "node-1"; - let result = cli.create_node(node_name).await?; + let result = cli.create_test_node(node_name).await?; assert_eq!(result.name(), node_name.to_string()); // the first node is the default one @@ -723,7 +724,7 @@ mod tests { let result = cli.get_or_create_default_named_vault().await.ok(); assert!(result.is_some()); - let result = cli.get_or_create_default_named_identity().await.ok(); + let result = cli.get_or_create_default_named_identity(None).await.ok(); assert!(result.is_some()); // that identity is associated to the node @@ -739,7 +740,7 @@ mod tests { // create a node let node_name = "node-1"; - let _ = cli.create_node(node_name).await?; + let _ = cli.create_test_node(node_name).await?; cli.set_tcp_listener_address( node_name, &SocketAddr::from_str("127.0.0.1:0").unwrap().into(), @@ -747,7 +748,7 @@ mod tests { .await?; // recreate the node with the same name - let _ = cli.create_node(node_name).await?; + let _ = cli.create_test_node(node_name).await?; // the node must still be the default node let result = cli.get_default_node().await?; @@ -768,7 +769,7 @@ mod tests { // a node can be created with just a name let node1 = "node-1"; - let node_info1 = cli.create_node(node1).await?; + let node_info1 = cli.create_test_node(node1).await?; // the created node is set as the default node let result = cli.get_default_node().await?; @@ -777,7 +778,7 @@ mod tests { // a node can also be removed // first let's create a second node let node2 = "node-2"; - let node_info2 = cli.create_node(node2).await?; + let node_info2 = cli.create_test_node(node2).await?; // and remove node 1 cli.remove_node(node1).await?; @@ -804,15 +805,15 @@ mod tests { // a node can be created with just a name let node = cli - .create_node_with_optional_values("node-1", &None, &None) + .create_node_with_optional_values(None, "node-1", &None, &None) .await?; let result = cli.get_node(&node.name()).await?; assert_eq!(result.name(), node.name()); // a node can be created with a name and an existing identity - let identity = cli.create_identity_with_name("name").await?; + let identity = cli.create_identity_with_name(None, "name").await?; let node = cli - .create_node_with_optional_values("node-2", &Some(identity.name()), &None) + .create_node_with_optional_values(None, "node-2", &Some(identity.name()), &None) .await?; let result = cli.get_node(&node.name()).await?; assert_eq!(result.identifier(), identity.identifier()); @@ -842,6 +843,7 @@ mod tests { let node = cli .create_node_with_optional_values( + None, "node-4", &Some(identity.name()), &Some(project.name.clone()), diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs b/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs index a9c4e362127..9abc218fae9 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs @@ -1,15 +1,19 @@ use std::sync::Arc; -use ockam::identity::{Identities, SecureChannelSqlxDatabase, SecureChannels}; - use crate::cli_state::CliState; use crate::cli_state::Result; +use ockam::identity::{Identities, SecureChannelSqlxDatabase, SecureChannels}; +use ockam_node::Context; impl CliState { - pub async fn secure_channels(&self, node_name: &str) -> Result> { + pub async fn secure_channels( + &self, + context: &Context, + node_name: &str, + ) -> Result> { debug!("create the secure channels service"); let named_vault = self.get_node_vault(node_name).await?; - let vault = self.make_vault(named_vault).await?; + let vault = self.make_vault(Some(context), named_vault).await?; let identities = Identities::create_with_node(self.database(), node_name) .with_vault(vault) .build(); diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs index 8b085c70f5e..651f80e0403 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs @@ -4,6 +4,7 @@ use sqlx::*; use ockam::{FromSqlxError, SqlxDatabase, ToVoid}; use ockam_core::async_trait; +use ockam_core::errcode::{Kind, Origin}; use ockam_core::Result; use ockam_node::database::{Boolean, Nullable}; @@ -39,15 +40,22 @@ impl VaultsRepository for VaultsSqlxDatabase { let query = query( r#" INSERT INTO - vault (name, path, is_default, is_kms) - VALUES ($1, $2, $3, $4) + vault (name, path, is_default, is_kms, vault_multiaddr, local_identifier, authority_identifier, authority_multiaddr, credential_scope) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (name) - DO UPDATE SET path = $2, is_default = $3, is_kms = $4"#, + DO UPDATE SET + path = $2, is_default = $3, is_kms = $4, + vault_multiaddr = $5, local_identifier = $6, authority_identifier = $7, authority_multiaddr = $8, credential_scope = $9"#, ) .bind(name) .bind(vault_type.path().map(|p| p.to_string_lossy().to_string())) .bind(!default_exists) - .bind(vault_type.use_aws_kms()); + .bind(vault_type.use_aws_kms()) + .bind(vault_type.vault_multiaddr().map(|r| r.to_string())) + .bind(vault_type.local_identifier().map(|i| i.to_string())) + .bind(vault_type.authority_identifier().map(|i| i.to_string())) + .bind(vault_type.authority_multiaddr().map(|r| r.to_string())) + .bind(vault_type.credential_scope()); query.execute(&mut *transaction).await.void()?; transaction.commit().await.void()?; @@ -55,9 +63,16 @@ impl VaultsRepository for VaultsSqlxDatabase { } async fn update_vault(&self, name: &str, vault_type: VaultType) -> Result<()> { - let query = query("UPDATE vault SET path = $1, is_kms = $2 WHERE name = $3") + let query = query("UPDATE vault SET \ + path = $1, is_kms = $2, \ + vault_multiaddr = $3, local_identifier = $4, authority_identifier = $5, authority_multiaddr = $6, credential_scope = $7 WHERE name = $8") .bind(vault_type.path().map(|p| p.to_string_lossy().to_string())) .bind(vault_type.use_aws_kms()) + .bind(vault_type.vault_multiaddr().map(|r| r.to_string())) + .bind(vault_type.local_identifier().map(|i| i.to_string())) + .bind(vault_type.authority_identifier().map(|i| i.to_string())) + .bind(vault_type.authority_multiaddr().map(|r| r.to_string())) + .bind(vault_type.credential_scope()) .bind(name); query.execute(&*self.database.pool).await.void() } @@ -68,7 +83,9 @@ impl VaultsRepository for VaultsSqlxDatabase { } async fn get_database_vault(&self) -> Result> { - let query = query_as("SELECT name, path, is_default, is_kms FROM vault WHERE path is NULL"); + let query = query_as( + "SELECT name, path, is_default, is_kms, vault_multiaddr, local_identifier, authority_identifier, authority_multiaddr, credential_scope \ + FROM vault WHERE path is NULL"); let row: Option = query .fetch_optional(&*self.database.pool) .await @@ -78,7 +95,8 @@ impl VaultsRepository for VaultsSqlxDatabase { async fn get_named_vault(&self, name: &str) -> Result> { let query = - query_as("SELECT name, path, is_default, is_kms FROM vault WHERE name = $1").bind(name); + query_as("SELECT name, path, is_default, is_kms, vault_multiaddr, local_identifier, authority_identifier, authority_multiaddr, credential_scope \ + FROM vault WHERE name = $1").bind(name); let row: Option = query .fetch_optional(&*self.database.pool) .await @@ -87,7 +105,9 @@ impl VaultsRepository for VaultsSqlxDatabase { } async fn get_named_vaults(&self) -> Result> { - let query = query_as("SELECT name, path, is_default, is_kms FROM vault"); + let query = + query_as("SELECT name, path, is_default, is_kms, vault_multiaddr, local_identifier, authority_identifier, authority_multiaddr, credential_scope \ + FROM vault"); let rows: Vec = query.fetch_all(&*self.database.pool).await.into_core()?; rows.iter().map(|r| r.named_vault()).collect() } @@ -101,25 +121,69 @@ pub(crate) struct VaultRow { path: Nullable, is_default: Boolean, is_kms: Boolean, + local_identifier: Nullable, + vault_multiaddr: Nullable, + authority_identifier: Nullable, + authority_multiaddr: Nullable, + credential_scope: Nullable, } impl VaultRow { pub(crate) fn named_vault(&self) -> Result { Ok(NamedVault::new( &self.name, - self.vault_type(), + self.vault_type()?, self.is_default(), )) } - pub(crate) fn vault_type(&self) -> VaultType { - match self.path.to_option() { - None => VaultType::database(UseAwsKms::from(self.is_kms.to_bool())), + pub(crate) fn vault_type(&self) -> Result { + let vault_type = match self.path.to_option() { + None => match self.vault_multiaddr.to_option() { + // if the `vault_multiaddr` is set, it is a remote vault + Some(vault_multiaddr) => { + let missing_field_error_lamba = || { + ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + format!("missing field for remote vault '{}'", self.name), + ) + }; + + let local_identifier = self + .local_identifier + .to_option() + .ok_or_else(missing_field_error_lamba)?; + let authority_multiaddr = self + .authority_multiaddr + .to_option() + .ok_or_else(missing_field_error_lamba)?; + let authority_identifier = self + .authority_identifier + .to_option() + .ok_or_else(missing_field_error_lamba)?; + let credential_scope = self + .credential_scope + .to_option() + .ok_or_else(missing_field_error_lamba)?; + + VaultType::remote( + vault_multiaddr.parse()?, + local_identifier.try_into()?, + authority_multiaddr.parse()?, + authority_identifier.try_into()?, + credential_scope, + ) + } + None => VaultType::database(UseAwsKms::from(self.is_kms.to_bool())), + }, Some(p) => VaultType::local_file( PathBuf::from(p).as_path(), UseAwsKms::from(self.is_kms.to_bool()), ), - } + }; + + Ok(vault_type) } pub(crate) fn is_default(&self) -> bool { @@ -130,6 +194,7 @@ impl VaultRow { #[cfg(test)] mod test { use super::*; + use crate::nodes::service::CredentialScope; use ockam_node::database::with_dbs; use std::sync::Arc; @@ -187,4 +252,33 @@ mod test { }) .await } + + #[tokio::test] + async fn test_store_remote_vault() -> Result<()> { + with_dbs(|db| async move { + let repository: Arc = Arc::new(VaultsSqlxDatabase::new(db)); + + // It is possible to create a remote vault + let vault_type = VaultType::remote( + "/secure/api/service/remote_vault".parse().unwrap(), + "Iabababababababababababababababababababababababababababababababab" + .try_into() + .unwrap(), + "/service/authority".parse().unwrap(), + "Iabababababababababababababababababababababababababababababababab" + .try_into() + .unwrap(), + CredentialScope::ProjectMember { + project_id: "project_id".to_string(), + } + .to_string(), + ); + // TODO: complete this test + let remote = repository.store_vault("remote", vault_type.clone()).await?; + let expected = NamedVault::new("remote", vault_type, true); + assert_eq!(remote, expected); + Ok(()) + }) + .await + } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/trust.rs b/implementations/rust/ockam/ockam_api/src/cli_state/trust.rs index 7227a68d737..b03bb2e9ef7 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/trust.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/trust.rs @@ -1,6 +1,6 @@ use crate::cloud::project::Project; use crate::nodes::service::{ - CredentialScope, NodeManagerCredentialRetrieverOptions, NodeManagerTrustOptions, + AuthorityOptions, CredentialRetrieverOptions, CredentialScope, NodeManagerTrustOptions, }; use crate::nodes::NodeManager; use crate::{multiaddr_to_transport_route, ApiError, CliState}; @@ -60,10 +60,10 @@ impl CliState { ); let trust_options = NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::Remote { info, scope }, - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::Remote { info, scope }, + CredentialRetrieverOptions::None, Some(authority_identifier.clone()), - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, ); debug!( @@ -76,13 +76,13 @@ impl CliState { if let Some(credential_scope) = credential_scope { let trust_options = NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::CacheOnly { + CredentialRetrieverOptions::CacheOnly { issuer: authority_identifier.clone(), scope: credential_scope.clone(), }, - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, Some(authority_identifier.clone()), - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, ); debug!( @@ -94,10 +94,10 @@ impl CliState { } let trust_options = NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::None, - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, Some(authority_identifier.clone()), - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, ); debug!( @@ -126,7 +126,7 @@ impl CliState { })?; let project_id = project.project_id().to_string(); - let project_member_retriever = NodeManagerCredentialRetrieverOptions::Remote { + let project_member_retriever = CredentialRetrieverOptions::Remote { info: RemoteCredentialRetrieverInfo::create_for_project_member( authority_identifier.clone(), authority_route, @@ -140,7 +140,7 @@ impl CliState { let controller_identifier = NodeManager::load_controller_identifier()?; let controller_transport_route = NodeManager::controller_route().await?; - let project_admin_retriever = NodeManagerCredentialRetrieverOptions::Remote { + let project_admin_retriever = CredentialRetrieverOptions::Remote { info: RemoteCredentialRetrieverInfo::create_for_project_admin( controller_identifier.clone(), controller_transport_route.clone(), @@ -152,7 +152,7 @@ impl CliState { .to_string(), }; - let account_admin_retriever = NodeManagerCredentialRetrieverOptions::Remote { + let account_admin_retriever = CredentialRetrieverOptions::Remote { info: RemoteCredentialRetrieverInfo::create_for_account_admin( controller_identifier.clone(), controller_transport_route, @@ -228,10 +228,10 @@ impl CliState { None => { debug!("TrustOptions configured: No Authority. No Credentials"); return Ok(NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::None, - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, None, - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, )); } }; @@ -239,13 +239,99 @@ impl CliState { if project.authority_identifier().is_none() { debug!("TrustOptions configured: No Authority. No Credentials"); return Ok(NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::None, - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, None, - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, )); } self.retrieve_trust_options_with_project(project).await } + + /// Extract `[AuthorityOptions]` from the provided parameters. + /// It returns an error when the full authority information cannot be extracted + #[instrument(skip_all, fields(project_name = project_name.clone(), authority_identity = authority_identity.as_ref().map(|a| a.to_string()).unwrap_or("n/a".to_string()), authority_route = authority_route.clone().map_or("n/a".to_string(), |r| r.to_string())))] + pub async fn retrieve_authority_options( + &self, + project_name: &Option, + authority_identity: &Option, + authority_route: &Option, + credential_scope: &Option, + ) -> Result { + if project_name.is_some() && (authority_identity.is_some() || authority_route.is_some()) { + return Err(Error::new( + Origin::Api, + Kind::NotFound, + "Both project_name and authority info are provided", + )); + } + + if authority_route.is_some() && authority_identity.is_none() { + return Err(Error::new( + Origin::Api, + Kind::NotFound, + "Authority address was provided but authority identity is unknown", + )); + } + + if let Some(authority_identity) = authority_identity { + // We're using explicitly specified authority instead of a project + let authority_multiaddr = authority_route.clone().ok_or_else(|| { + Error::new( + Origin::Api, + Kind::NotFound, + "Authority identity was provided but authority address is unknown", + ) + })?; + + let identities_verification = IdentitiesVerification::new( + self.change_history_repository(), + SoftwareVaultForVerifyingSignatures::create(), + ); + + let authority_identity = authority_identity.export()?; + let authority_identifier = identities_verification + .import(None, &authority_identity) + .await?; + + let scope = credential_scope.as_ref().ok_or_else(|| { + Error::new( + Origin::Api, + Kind::NotFound, + "Authority address was provided but credential scope was not provided", + ) + })?; + + Ok(AuthorityOptions { + identifier: authority_identifier, + multiaddr: authority_multiaddr, + credential_scope: scope.clone(), + }) + } else { + // Either The project name was specified or we're using the default project + let project = match project_name { + Some(project_name) => self.projects().get_project_by_name(project_name).await?, + None => self.projects().get_default_project().await?, + }; + + let authority_identifier = project + .authority_identifier() + .ok_or_else(|| Error::new(Origin::Api, Kind::NotFound, "No authority found"))?; + + let credential_scope = match credential_scope { + Some(scope) => scope.clone(), + None => CredentialScope::ProjectMember { + project_id: project.project_id().to_string(), + } + .to_string(), + }; + + Ok(AuthorityOptions { + identifier: authority_identifier, + multiaddr: project.authority_multiaddr()?.clone(), + credential_scope, + }) + } + } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs b/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs index 56b764606bc..6f252fa6984 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs @@ -1,7 +1,14 @@ use colorful::Colorful; -use ockam::identity::{Identities, Vault}; +use ockam::identity::{ + Identifier, Identities, RemoteCredentialRetrieverInfo, SecureChannelRegistry, + SecureChannelSqlxDatabase, SecureChannels, Vault, +}; use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{AsyncTryClone, Error}; +use ockam_multiaddr::MultiAddr; use ockam_node::database::SqlxDatabase; +use ockam_node::Context; +use ockam_transport_tcp::TcpTransport; use ockam_vault_aws::AwsSigningVault; use std::fmt::Write; use std::fmt::{Debug, Display, Formatter}; @@ -11,8 +18,12 @@ use std::sync::Arc; use crate::cli_state::{random_name, CliState, CliStateError, Result}; use crate::colors::color_primary; +use crate::nodes::connection::{ + ConnectionInstantiator, PlainTcpInstantiator, ProjectInstantiator, SecureChannelInstantiator, +}; +use crate::nodes::service::CredentialRetrieverOptions; use crate::output::Output; -use crate::{fmt_log, fmt_ok, fmt_warn}; +use crate::{fmt_log, fmt_ok, fmt_warn, multiaddr_to_transport_route, proxy_vault}; static DEFAULT_VAULT_NAME: &str = "default"; @@ -23,19 +34,34 @@ static DEFAULT_VAULT_NAME: &str = "default"; /// - any additional vault stores its keys in a separate file /// impl CliState { - /// Create a vault with a given name - /// If the path is not specified then: - /// - if this is the first vault then secrets are persisted in the main database - /// - if this is a new vault then secrets are persisted in $OCKAM_HOME/vault_name - #[instrument(skip_all, fields(vault_name = vault_name.clone()))] - pub async fn create_named_vault( + /// Create a remote vault with a given name, route and identifier + pub async fn create_remote_vault( &self, vault_name: Option, - path: Option, - use_aws_kms: UseAwsKms, + vault_multiaddr: MultiAddr, + local_identifier: Identifier, + authority_identifier: Identifier, + authority_multiaddr: MultiAddr, + credential_scope: String, ) -> Result { + let vault_name = self.name_vault(vault_name).await?; let vaults_repository = self.vaults_repository(); + Ok(vaults_repository + .store_vault( + &vault_name, + VaultType::RemoteVault { + vault_multiaddr, + local_identifier, + authority_identifier, + authority_multiaddr, + credential_scope, + }, + ) + .await?) + } + + async fn name_vault(&self, vault_name: Option) -> Result { // determine the vault name to use if not given by the user let vault_name = match vault_name { Some(vault_name) => vault_name.clone(), @@ -43,7 +69,8 @@ impl CliState { }; // verify that a vault with that name does not exist - if vaults_repository + if self + .vaults_repository() .get_named_vault(&vault_name) .await? .is_some() @@ -54,10 +81,28 @@ impl CliState { }); } + Ok(vault_name) + } + + /// Create a vault with a given name + /// If the path is not specified then: + /// - if this is the first vault then secrets are persisted in the main database + /// - if this is a new vault then secrets are persisted in $OCKAM_HOME/vault_name + #[instrument(skip_all, fields(vault_name = vault_name.clone()))] + pub async fn create_named_vault( + &self, + vault_name: Option, + path: Option, + use_aws_kms: UseAwsKms, + ) -> Result { + let vault_name = self.name_vault(vault_name).await?; + + let vaults_repository = self.vaults_repository(); + // Determine if the vault needs to be created at a specific path // or if data can be stored in the main database directly match path { - None => match self.vaults_repository().get_database_vault().await? { + None => match vaults_repository.get_database_vault().await? { None => Ok(vaults_repository .store_vault(&vault_name, VaultType::database(use_aws_kms)) .await?), @@ -110,6 +155,9 @@ impl CliState { VaultType::LocalFileVault { path, .. } => { let _ = std::fs::remove_file(path); } + VaultType::RemoteVault { .. } => { + // nothing to do + } } } Ok(()) @@ -288,13 +336,120 @@ impl CliState { // remove the old file std::fs::remove_file(old_path)?; } + VaultType::RemoteVault { .. } => Err(ockam_core::Error::new( + Origin::Api, + Kind::Invalid, + format!( + "The vault {} cannot be moved to {path:?} because this is a remote vault", + vault.name() + ), + ))?, } Ok(()) } /// Make a concrete vault based on the NamedVault metadata #[instrument(skip_all, fields(vault_name = named_vault.name))] - pub async fn make_vault(&self, named_vault: NamedVault) -> Result { + pub async fn make_vault( + &self, + context: Option<&Context>, + named_vault: NamedVault, + ) -> Result { + match named_vault.vault_type { + VaultType::RemoteVault { + vault_multiaddr, + local_identifier, + authority_identifier, + authority_multiaddr, + credential_scope, + } => { + if let Some(context) = context { + let context = context.async_try_clone().await?; + let tcp_transport = TcpTransport::create(&context).await?; + + let authority_route = multiaddr_to_transport_route(&authority_multiaddr) + .ok_or(Error::new( + Origin::Api, + Kind::NotFound, + format!("Invalid authority route: {}", &authority_multiaddr), + ))?; + + let credential_retriever_options = CredentialRetrieverOptions::Remote { + info: RemoteCredentialRetrieverInfo::create_for_project_member( + authority_identifier.clone(), + authority_route, + ), + scope: credential_scope.to_string(), + }; + + // create the vault for the specified identity + let local_named_vault = self + .get_named_identity_by_identifier(&local_identifier) + .await? + .vault_name(); + let local_vault_name = self.get_named_vault(&local_named_vault).await?; + let local_vault = self.make_local_vault(local_vault_name).await?; + + let node_name = "NODE NAME TODO!!! change me!"; + let identities = Identities::create_with_node(self.database(), node_name) + .with_vault(local_vault) + .build(); + + let secure_channels = Arc::new(SecureChannels::new( + identities, + SecureChannelRegistry::default(), //TODO: inherit registry from the node + Arc::new(SecureChannelSqlxDatabase::new(self.database())), + )); + + let credential_retriever_creator = credential_retriever_options + .create(&context, tcp_transport.clone(), &secure_channels) + .await?; + + let connection_instantiator = ConnectionInstantiator::new() + .add(PlainTcpInstantiator::new(tcp_transport.clone())) + .add(ProjectInstantiator::new( + local_identifier.clone(), + None, + self.clone(), + tcp_transport, + secure_channels.clone(), + credential_retriever_creator.clone(), + )) + .add(SecureChannelInstantiator::new( + local_identifier.clone(), + None, + None, + Some(authority_identifier.clone()), + secure_channels, + credential_retriever_creator, + )); + + Ok( + proxy_vault::create_vault( + context, + vault_multiaddr, + connection_instantiator, + ) + .await, + ) + } else { + Err(Error::new( + Origin::Api, + Kind::Invalid, + format!( + "The vault {} is a remote vault and cannot be created in this context", + named_vault.name + ), + ))? + } + } + + _ => self.make_local_vault(named_vault).await, + } + } + + pub async fn make_local_vault(&self, named_vault: NamedVault) -> Result { + let use_aws_kms = named_vault.vault_type.use_aws_kms(); let db = match named_vault.vault_type { VaultType::DatabaseVault { .. } => self.database(), VaultType::LocalFileVault { ref path, .. } => @@ -302,9 +457,17 @@ impl CliState { { SqlxDatabase::create_sqlite(path.as_path()).await? } + VaultType::RemoteVault { .. } => Err(Error::new( + Origin::Api, + Kind::Invalid, + format!( + "The vault {} is a remote vault and cannot be created in this context", + named_vault.name + ), + ))?, }; - if named_vault.vault_type.use_aws_kms() { + if use_aws_kms { let mut vault = Vault::create_with_database(db); let aws_vault = Arc::new(AwsSigningVault::create().await?); vault.identity_vault = aws_vault.clone(); @@ -417,6 +580,13 @@ pub enum VaultType { path: PathBuf, use_aws_kms: UseAwsKms, }, + RemoteVault { + vault_multiaddr: MultiAddr, + local_identifier: Identifier, + authority_identifier: Identifier, + authority_multiaddr: MultiAddr, + credential_scope: String, + }, } impl Display for VaultType { @@ -427,6 +597,7 @@ impl Display for VaultType { match &self { VaultType::DatabaseVault { .. } => "INTERNAL", VaultType::LocalFileVault { .. } => "EXTERNAL", + VaultType::RemoteVault { .. } => "REMOTE", } )?; if self.use_aws_kms() { @@ -457,6 +628,22 @@ impl VaultType { VaultType::DatabaseVault { use_aws_kms } } + pub fn remote( + vault_multiaddr: MultiAddr, + local_identifier: Identifier, + authority_multiaddr: MultiAddr, + authority_identifier: Identifier, + credential_scope: String, + ) -> Self { + VaultType::RemoteVault { + vault_multiaddr, + local_identifier, + authority_identifier, + authority_multiaddr, + credential_scope, + } + } + pub fn local_file(path: impl Into, use_aws_kms: UseAwsKms) -> Self { VaultType::LocalFileVault { path: path.into(), @@ -468,6 +655,7 @@ impl VaultType { match self { VaultType::DatabaseVault { .. } => None, VaultType::LocalFileVault { path, .. } => Some(path.as_path()), + VaultType::RemoteVault { .. } => None, } } @@ -478,6 +666,54 @@ impl VaultType { path: _, use_aws_kms, } => use_aws_kms == &UseAwsKms::Yes, + VaultType::RemoteVault { .. } => false, + } + } + + pub fn vault_multiaddr(&self) -> Option<&MultiAddr> { + match self { + VaultType::RemoteVault { + vault_multiaddr, .. + } => Some(vault_multiaddr), + _ => None, + } + } + + pub fn local_identifier(&self) -> Option<&Identifier> { + match self { + VaultType::RemoteVault { + local_identifier, .. + } => Some(local_identifier), + _ => None, + } + } + + pub fn authority_identifier(&self) -> Option<&Identifier> { + match self { + VaultType::RemoteVault { + authority_identifier, + .. + } => Some(authority_identifier), + _ => None, + } + } + + pub fn authority_multiaddr(&self) -> Option<&MultiAddr> { + match self { + VaultType::RemoteVault { + authority_multiaddr, + .. + } => Some(authority_multiaddr), + _ => None, + } + } + + pub fn credential_scope(&self) -> Option<&str> { + match self { + VaultType::RemoteVault { + credential_scope, .. + } => Some(credential_scope), + _ => None, } } } @@ -543,6 +779,7 @@ impl Output for NamedVault { match &self.vault_type { VaultType::DatabaseVault { .. } => "INTERNAL", VaultType::LocalFileVault { .. } => "EXTERNAL", + VaultType::RemoteVault { .. } => "REMOTE", } )?; if self.vault_type.use_aws_kms() { @@ -770,7 +1007,7 @@ mod tests { let vault = cli.create_named_vault(None, None, UseAwsKms::No).await?; let purpose_keys_repository = cli.purpose_keys_repository(); - let identity = cli.create_identity_with_name("name").await?; + let identity = cli.create_identity_with_name(None, "name").await?; let purpose_key_attestation = PurposeKeyAttestation { data: vec![1, 2, 3], signature: PurposeKeyAttestationSignature::ECDSASHA256CurveP256( diff --git a/implementations/rust/ockam/ockam_api/src/lib.rs b/implementations/rust/ockam/ockam_api/src/lib.rs index eac52566c41..ba6fc1668cd 100644 --- a/implementations/rust/ockam/ockam_api/src/lib.rs +++ b/implementations/rust/ockam/ockam_api/src/lib.rs @@ -15,6 +15,7 @@ //! │ ├─ node2 //! │ └─ ... //! ``` +#![recursion_limit = "256"] #[macro_use] extern crate tracing; @@ -44,6 +45,8 @@ pub mod logs; mod schema; mod date; +/// Proxy vault +pub mod proxy_vault; mod rendezvous_healthcheck; pub mod test_utils; mod ui; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs index aaddc827ce6..0d31915cf2f 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs @@ -194,6 +194,12 @@ pub struct ConnectionInstantiator { instantiator: Vec>, } +impl Default for ConnectionInstantiator { + fn default() -> Self { + Self::new() + } +} + impl ConnectionInstantiator { pub fn new() -> Self { ConnectionInstantiator { diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs index 644447f2ebc..54b0f07f571 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs @@ -8,7 +8,9 @@ use ockam_multiaddr::proto::Project; use ockam_multiaddr::{Match, MultiAddr, Protocol}; use ockam_node::Context; -use ockam::identity::{Identifier, SecureChannelOptions, SecureChannels}; +use ockam::identity::{ + CredentialRetrieverCreator, Identifier, SecureChannelOptions, SecureChannels, +}; use ockam_transport_tcp::TcpTransport; use std::time::Duration; @@ -19,6 +21,7 @@ pub(crate) struct ProjectInstantiator { cli_state: CliState, secure_channels: Arc, tcp_transport: TcpTransport, + credential_retriever_creator: Option>, } impl ProjectInstantiator { @@ -26,8 +29,9 @@ impl ProjectInstantiator { identifier: Identifier, timeout: Option, cli_state: CliState, - secure_channels: Arc, tcp_transport: TcpTransport, + secure_channels: Arc, + credential_retriever_creator: Option>, ) -> Self { Self { identifier, @@ -35,6 +39,7 @@ impl ProjectInstantiator { cli_state, secure_channels, tcp_transport, + credential_retriever_creator, } } } @@ -52,6 +57,10 @@ impl Instantiator for ProjectInstantiator { extracted: (MultiAddr, MultiAddr, MultiAddr), ) -> Result { let (_before, project_piece, after) = extracted; + debug!( + identifier=%self.identifier, + "creating project connection", + ); let project_protocol_value = project_piece .first() @@ -61,22 +70,16 @@ impl Instantiator for ProjectInstantiator { .cast::() .ok_or_else(|| ApiError::core("invalid project protocol in multiaddr"))?; - let (project_multiaddr, project_identifier) = self + let project = self .cli_state .projects() .get_project_by_name(&project) - .await - .map(|project| { - ( - project.project_multiaddr().cloned(), - project - .project_identifier() - .ok_or_else(|| ApiError::core("project identifier is missing")), - ) - })?; + .await?; - let project_identifier = project_identifier?; - let project_multiaddr = project_multiaddr?; + let project_identifier = project + .project_identifier() + .ok_or_else(|| ApiError::core("Project identifier is missing"))?; + let project_multiaddr = project.project_multiaddr().cloned()?; debug!(addr = %project_multiaddr, "creating secure channel"); let tcp = multiaddr_to_route(&project_multiaddr, &self.tcp_transport) @@ -90,6 +93,14 @@ impl Instantiator for ProjectInstantiator { debug!("create a secure channel to the project {project_identifier}"); let options = SecureChannelOptions::new().with_authority(project_identifier); + + let options = + if let Some(credential_retriever_creator) = self.credential_retriever_creator.clone() { + options.with_credential_retriever_creator(credential_retriever_creator)? + } else { + options + }; + let options = if let Some(timeout) = self.timeout { options.with_timeout(timeout) } else { diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs index 9592d702a05..d5b2d193358 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs @@ -5,8 +5,8 @@ use crate::nodes::connection::{Changes, Instantiator}; use crate::{local_multiaddr_to_route, try_address_to_multiaddr}; use ockam::identity::{ - Identifier, SecureChannelOptions, SecureChannels, TrustEveryonePolicy, - TrustMultiIdentifiersPolicy, + CredentialRetrieverCreator, Identifier, SecureChannelOptions, SecureChannels, + TrustEveryonePolicy, TrustMultiIdentifiersPolicy, }; use ockam_core::{async_trait, route, Route}; use ockam_multiaddr::proto::Secure; @@ -20,22 +20,25 @@ pub(crate) struct SecureChannelInstantiator { timeout: Option, secure_channels: Arc, authority: Option, + credential_retriever_creator: Option>, } impl SecureChannelInstantiator { pub(crate) fn new( - identifier: &Identifier, + identifier: Identifier, timeout: Option, authorized_identities: Option>, authority: Option, secure_channels: Arc, + credential_retriever_creator: Option>, ) -> Self { Self { - identifier: identifier.clone(), + identifier, authorized_identities, authority, timeout, secure_channels, + credential_retriever_creator, } } } @@ -53,7 +56,7 @@ impl Instantiator for SecureChannelInstantiator { extracted: (MultiAddr, MultiAddr, MultiAddr), ) -> Result { let (_before, secure_piece, after) = extracted; - debug!(%secure_piece, %transport_route, "creating secure channel"); + debug!(%secure_piece, %transport_route, identifier=%self.identifier, "creating secure channel"); let route = local_multiaddr_to_route(&secure_piece)?; let options = SecureChannelOptions::new(); @@ -69,6 +72,13 @@ impl Instantiator for SecureChannelInstantiator { options }; + let options = + if let Some(credential_retriever_creator) = self.credential_retriever_creator.clone() { + options.with_credential_retriever_creator(credential_retriever_creator)? + } else { + options + }; + let options = if let Some(timeout) = self.timeout { options.with_timeout(timeout) } else { diff --git a/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs b/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs index 30a77253d8d..37e7623616b 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs @@ -211,6 +211,24 @@ impl StartHopServiceRequest { } } +/// Request body of remote proxy vault service +#[derive(Debug, Clone, Encode, Decode, CborLen)] +#[rustfmt::skip] +#[cbor(map)] +pub struct StartRemoteProxyVaultServiceRequest { + #[n(1)] pub addr: String, + #[n(2)] pub vault_name: String, +} + +impl StartRemoteProxyVaultServiceRequest { + pub fn new(addr: impl Into, vault_name: impl Into) -> Self { + Self { + addr: addr.into(), + vault_name: vault_name.into(), + } + } +} + #[derive(Debug, Clone, Serialize, Encode, Decode, CborLen)] #[rustfmt::skip] #[cbor(map)] diff --git a/implementations/rust/ockam/ockam_api/src/nodes/registry.rs b/implementations/rust/ockam/ockam_api/src/nodes/registry.rs index 099e180044c..221d4300261 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/registry.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/registry.rs @@ -92,6 +92,9 @@ pub(crate) struct EchoerServiceInfo {} #[derive(Default, Clone)] pub(crate) struct HopServiceInfo {} +#[derive(Clone)] +pub(crate) struct RemoteProxyVaultInfo {} + #[derive(Eq, PartialEq, Clone)] pub enum KafkaServiceKind { Inlet, @@ -182,6 +185,7 @@ pub(crate) struct Registry { pub(crate) echoer_services: RegistryOf, pub(crate) kafka_services: RegistryOf, pub(crate) hop_services: RegistryOf, + pub(crate) remote_proxy_vaults: RegistryOf, pub(crate) relays: RegistryOf, pub(crate) inlets: RegistryOf, pub(crate) outlets: RegistryOf, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs index 833dcce2302..93e49af7fea 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs @@ -10,6 +10,7 @@ impl DefaultAddress { pub const UPPERCASE_SERVICE: &'static str = "uppercase"; pub const ECHO_SERVICE: &'static str = "echo"; pub const HOP_SERVICE: &'static str = "hop"; + pub const REMOTE_PROXY_VAULT: &'static str = "remote_proxy_vault"; pub const SECURE_CHANNEL_LISTENER: &'static str = "api"; pub const KEY_EXCHANGER_LISTENER: &'static str = "key_exchanger"; pub const UDP_PUNCTURE_NEGOTIATION_LISTENER: &'static str = "udp"; @@ -35,6 +36,7 @@ impl DefaultAddress { | Self::UPPERCASE_SERVICE | Self::ECHO_SERVICE | Self::HOP_SERVICE + | Self::REMOTE_PROXY_VAULT | Self::SECURE_CHANNEL_LISTENER | Self::KEY_EXCHANGER_LISTENER | Self::DIRECT_AUTHENTICATOR diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs index a626603f779..d2c0d7b743c 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs @@ -85,7 +85,7 @@ impl InMemoryNode { project_name: Option, ) -> miette::Result { let default_identity_name = cli_state - .get_or_create_default_named_identity() + .get_or_create_default_named_identity(Some(ctx)) .await? .name(); Self::start_node( @@ -107,7 +107,9 @@ impl InMemoryNode { identity: Option, project_name: Option, ) -> miette::Result { - let identity = cli_state.get_identity_name_or_default(&identity).await?; + let identity = cli_state + .get_or_create_identity_name_or_default(Some(ctx), &identity) + .await?; Self::start_node(ctx, cli_state, &identity, None, project_name, None, None).await } @@ -117,7 +119,9 @@ impl InMemoryNode { cli_state: &CliState, identity: Option, ) -> miette::Result { - let identity = cli_state.get_identity_name_or_default(&identity).await?; + let identity = cli_state + .get_or_create_identity_name_or_default(Some(ctx), &identity) + .await?; Self::start_node(ctx, cli_state, &identity, None, None, None, None).await } @@ -145,6 +149,7 @@ impl InMemoryNode { let node = cli_state .start_node_with_optional_values( + Some(ctx), &defaults.node_name, &Some(identity_name.to_string()), &project_name, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs index 7ab884e24e0..74d55bf820e 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs @@ -9,7 +9,7 @@ use crate::nodes::models::transport::{Port, TransportMode, TransportType}; use crate::nodes::registry::Registry; use crate::nodes::service::http::HttpServer; use crate::nodes::service::{ - CredentialRetrieverCreators, NodeManagerCredentialRetrieverOptions, NodeManagerTrustOptions, + CredentialRetrieverCreators, CredentialRetrieverOptions, NodeManagerTrustOptions, SecureChannelType, }; @@ -84,45 +84,31 @@ impl NodeManager { .store_default_resource_type_policies() .await?; - let secure_channels = cli_state.secure_channels(&node_name).await?; + let secure_channels = cli_state.secure_channels(ctx, &node_name).await?; let project_member_credential_retriever_creator: Option< Arc, - > = match trust_options.project_member_credential_retriever_options { - NodeManagerCredentialRetrieverOptions::None => None, - NodeManagerCredentialRetrieverOptions::CacheOnly { issuer, scope } => { - Some(Arc::new(CachedCredentialRetrieverCreator::new( - issuer.clone(), - scope, - secure_channels.identities().cached_credentials_repository(), - ))) - } - NodeManagerCredentialRetrieverOptions::Remote { info, scope } => { - Some(Arc::new(RemoteCredentialRetrieverCreator::new( - ctx.async_try_clone().await?, - Arc::new(transport_options.tcp_transport.clone()), - secure_channels.clone(), - info.clone(), - scope, - ))) - } - NodeManagerCredentialRetrieverOptions::InMemory(credential) => { - Some(Arc::new(MemoryCredentialRetrieverCreator::new(credential))) - } - }; + > = trust_options + .project_member_credential_retriever_options + .create( + ctx, + transport_options.tcp_transport.clone(), + &secure_channels, + ) + .await?; let project_admin_credential_retriever_creator: Option< Arc, > = match trust_options.project_admin_credential_retriever_options { - NodeManagerCredentialRetrieverOptions::None => None, - NodeManagerCredentialRetrieverOptions::CacheOnly { issuer, scope } => { + CredentialRetrieverOptions::None => None, + CredentialRetrieverOptions::CacheOnly { issuer, scope } => { Some(Arc::new(CachedCredentialRetrieverCreator::new( issuer.clone(), scope, secure_channels.identities().cached_credentials_repository(), ))) } - NodeManagerCredentialRetrieverOptions::Remote { info, scope } => { + CredentialRetrieverOptions::Remote { info, scope } => { Some(Arc::new(RemoteCredentialRetrieverCreator::new( ctx.async_try_clone().await?, Arc::new(transport_options.tcp_transport.clone()), @@ -131,7 +117,7 @@ impl NodeManager { scope, ))) } - NodeManagerCredentialRetrieverOptions::InMemory(credential) => { + CredentialRetrieverOptions::InMemory(credential) => { Some(Arc::new(MemoryCredentialRetrieverCreator::new(credential))) } }; @@ -298,16 +284,18 @@ impl NodeManager { identifier.clone(), timeout, self.cli_state.clone(), - self.secure_channels.clone(), self.tcp_transport.clone(), + self.secure_channels.clone(), + self.credential_retriever_creators.project_member.clone(), )) .add(PlainTcpInstantiator::new(self.tcp_transport.clone())) .add(SecureChannelInstantiator::new( - &identifier, + identifier, timeout, authorized, self.project_authority(), self.secure_channels.clone(), + self.credential_retriever_creators.project_member.clone(), )); connection_instantiator.connect(ctx, addr).await diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs index 4c1d6fc81f1..6ce0ec0e653 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs @@ -1,21 +1,22 @@ use either::Either; -use ockam::{Address, Context, Result}; -use ockam_abac::{Action, Resource, ResourceType}; -use ockam_core::api::{Error, Response}; -use ockam_node::WorkerBuilder; - +use crate::cli_state::VaultType; use crate::echoer::Echoer; use crate::error::ApiError; use crate::hop::Hop; use crate::nodes::models::node::{NodeResources, NodeStatus}; use crate::nodes::models::services::{ - ServiceStatus, StartEchoerServiceRequest, StartHopServiceRequest, StartUppercaseServiceRequest, + ServiceStatus, StartEchoerServiceRequest, StartHopServiceRequest, + StartRemoteProxyVaultServiceRequest, StartUppercaseServiceRequest, }; -use crate::nodes::registry::KafkaServiceKind; +use crate::nodes::registry::{KafkaServiceKind, RemoteProxyVaultInfo}; use crate::nodes::service::default_address::DefaultAddress; use crate::nodes::NodeManager; use crate::uppercase::Uppercase; +use ockam::{Address, Context, Result}; +use ockam_abac::{Action, Resource, ResourceType}; +use ockam_core::api::{Error, Response}; +use ockam_node::WorkerBuilder; use super::NodeManagerWorker; @@ -65,6 +66,21 @@ impl NodeManagerWorker { } } + pub(super) async fn start_remote_proxy_vault( + &self, + context: &Context, + request: StartRemoteProxyVaultServiceRequest, + ) -> Result> { + match self + .node_manager + .start_remote_proxy_vault(context, request.addr.into(), request.vault_name) + .await + { + Ok(_) => Ok(Response::ok()), + Err(e) => Err(Response::internal_error_no_request(&e.to_string())), + } + } + pub(super) async fn list_services_of_type( &self, service_type: &str, @@ -155,6 +171,17 @@ impl NodeManager { DefaultAddress::HOP_SERVICE, )) }); + self.registry + .remote_proxy_vaults + .keys() + .await + .iter() + .for_each(|addr| { + list.push(ServiceStatus::new( + addr.address(), + DefaultAddress::REMOTE_PROXY_VAULT, + )) + }); self.registry .kafka_services .entries() @@ -255,6 +282,50 @@ impl NodeManager { Ok(()) } + pub(super) async fn start_remote_proxy_vault( + &self, + context: &Context, + addr: Address, + vault_name: String, + ) -> Result<()> { + let default_secure_channel_listener_flow_control_id = context + .flow_controls() + .get_flow_control_with_spawner(&DefaultAddress::SECURE_CHANNEL_LISTENER.into()) + .ok_or_else(|| { + ApiError::core("Unable to get flow control for secure channel listener") + })?; + + if self.registry.remote_proxy_vaults.contains_key(&addr).await { + return Err(ApiError::core(format!( + "remote proxy vault already exists at {addr}" + ))); + } + + let named_vault = self.cli_state.get_named_vault(&vault_name).await?; + if matches!(named_vault.vault_type(), VaultType::RemoteVault { .. }) { + return Err(ApiError::core(format!( + "vault {named_vault} is a remote vault" + ))); + } + + let vault = self.cli_state.make_local_vault(named_vault).await?; + crate::proxy_vault::start_server(context, addr.clone(), vault).await?; + + context.flow_controls().add_consumer( + addr.clone(), + &default_secure_channel_listener_flow_control_id, + ); + + info!("remote proxy vault was initialized at {addr}"); + + self.registry + .remote_proxy_vaults + .insert(addr, RemoteProxyVaultInfo {}) + .await; + + Ok(()) + } + pub async fn get_node_status(&self) -> Result { let node = self.cli_state.get_node(&self.node_name).await?; Ok(NodeStatus::from(&node)) diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs index 8501636d484..e2dc954c1f4 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs @@ -349,7 +349,14 @@ impl NodeManager { .cli_state .get_named_vault(&named_identity.vault_name()) .await?; - let vault = self.cli_state.make_vault(named_vault).await?; + + // no need to recreate the vault if the identity is the same + let vault = if self.node_identifier == identifier { + self.secure_channels.vault() + } else { + self.cli_state.make_vault(Some(ctx), named_vault).await? + }; + let secure_channels = self.build_secure_channels(vault).await?; let options = diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/trust.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/trust.rs index f389d03f58e..5111a736195 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/trust.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/trust.rs @@ -1,6 +1,14 @@ use ockam::identity::models::CredentialAndPurposeKey; -use ockam::identity::{CredentialRetrieverCreator, Identifier, RemoteCredentialRetrieverInfo}; +use ockam::identity::{ + CachedCredentialRetrieverCreator, CredentialRetrieverCreator, Identifier, + MemoryCredentialRetrieverCreator, RemoteCredentialRetrieverCreator, + RemoteCredentialRetrieverInfo, SecureChannels, +}; use ockam_core::errcode::{Kind, Origin}; +use ockam_core::AsyncTryClone; +use ockam_multiaddr::MultiAddr; +use ockam_node::Context; +use ockam_transport_tcp::TcpTransport; use std::fmt::Display; use std::str::FromStr; use std::sync::Arc; @@ -70,8 +78,15 @@ impl Display for CredentialScope { } } -#[derive(Debug)] -pub enum NodeManagerCredentialRetrieverOptions { +pub struct AuthorityOptions { + pub identifier: Identifier, + pub multiaddr: MultiAddr, + pub credential_scope: String, +} + +#[derive(Debug, Default)] +pub enum CredentialRetrieverOptions { + #[default] None, CacheOnly { issuer: Identifier, @@ -84,19 +99,52 @@ pub enum NodeManagerCredentialRetrieverOptions { InMemory(CredentialAndPurposeKey), } +impl CredentialRetrieverOptions { + pub async fn create( + &self, + ctx: &Context, + tcp_transport: TcpTransport, + secure_channels: &Arc, + ) -> ockam_core::Result>> { + Ok(match self { + CredentialRetrieverOptions::None => None, + CredentialRetrieverOptions::CacheOnly { issuer, scope } => { + Some(Arc::new(CachedCredentialRetrieverCreator::new( + issuer.clone(), + scope.clone(), + secure_channels.identities().cached_credentials_repository(), + ))) + } + CredentialRetrieverOptions::Remote { info, scope } => { + Some(Arc::new(RemoteCredentialRetrieverCreator::new( + ctx.async_try_clone().await?, + Arc::new(tcp_transport), + secure_channels.clone(), + info.clone(), + scope.clone(), + ))) + } + CredentialRetrieverOptions::InMemory(credential) => Some(Arc::new( + MemoryCredentialRetrieverCreator::new(credential.clone()), + )), + }) + } +} + +#[derive(Default)] pub struct NodeManagerTrustOptions { - pub(super) project_member_credential_retriever_options: NodeManagerCredentialRetrieverOptions, + pub(super) project_member_credential_retriever_options: CredentialRetrieverOptions, pub(super) project_authority: Option, - pub(super) project_admin_credential_retriever_options: NodeManagerCredentialRetrieverOptions, - pub(super) _account_admin_credential_retriever_options: NodeManagerCredentialRetrieverOptions, + pub(super) project_admin_credential_retriever_options: CredentialRetrieverOptions, + pub(super) _account_admin_credential_retriever_options: CredentialRetrieverOptions, } impl NodeManagerTrustOptions { pub fn new( - project_member_credential_retriever_options: NodeManagerCredentialRetrieverOptions, - project_admin_credential_retriever_options: NodeManagerCredentialRetrieverOptions, + project_member_credential_retriever_options: CredentialRetrieverOptions, + project_admin_credential_retriever_options: CredentialRetrieverOptions, project_authority: Option, - account_admin_credential_retriever_options: NodeManagerCredentialRetrieverOptions, + account_admin_credential_retriever_options: CredentialRetrieverOptions, ) -> Self { Self { project_member_credential_retriever_options, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs index 2580e6bcd63..70bac0c7fe6 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs @@ -124,6 +124,9 @@ impl NodeManagerWorker { (Post, ["node", "services", DefaultAddress::HOP_SERVICE]) => { encode_response(req, self.start_hop_service(ctx, dec.decode()?).await)? } + (Post, ["node", "services", DefaultAddress::REMOTE_PROXY_VAULT]) => { + encode_response(req, self.start_remote_proxy_vault(ctx, dec.decode()?).await)? + } (Post, ["node", "services", DefaultAddress::KAFKA_OUTLET]) => encode_response( req, self.start_kafka_outlet_service(ctx, dec.decode()?).await, diff --git a/implementations/rust/ockam/ockam_api/src/proxy_vault/mod.rs b/implementations/rust/ockam/ockam_api/src/proxy_vault/mod.rs new file mode 100644 index 00000000000..4f341bcc153 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/proxy_vault/mod.rs @@ -0,0 +1,4 @@ +mod protocol; + +pub use protocol::create_vault; +pub use protocol::start_server; diff --git a/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs b/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs new file mode 100644 index 00000000000..4b1aebfeeba --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs @@ -0,0 +1,1333 @@ +use crate::nodes::connection::{Connection, ConnectionInstantiator}; +use crate::DefaultAddress; +use minicbor::{CborLen, Decode, Encode}; +use ockam::identity::{utils, TimestampInSeconds, Vault}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{ + async_trait, cbor_encode_preallocate, route, Address, NeutralMessage, Route, Routed, Worker, +}; +use ockam_multiaddr::MultiAddr; +use ockam_node::Context; +use std::sync::Arc; +use tokio::sync::Mutex as AsyncMutex; + +#[derive(Encode, Decode, CborLen)] +#[rustfmt::skip] +enum ProxyError { + #[n(0)] Unknown, + #[n(1)] Invalid, + #[n(2)] Unsupported, + #[n(3)] NotFound, + #[n(4)] Misuse, + #[n(5)] Timeout, + #[n(6)] Protocol, + +} + +impl From for ockam_core::Error { + fn from(error: ProxyError) -> Self { + match error { + ProxyError::Unknown => { + ockam_core::Error::new(Origin::Vault, Kind::Unknown, "Unknown error") + } + ProxyError::Invalid => { + ockam_core::Error::new(Origin::Vault, Kind::Invalid, "Invalid request") + } + ProxyError::Unsupported => { + ockam_core::Error::new(Origin::Vault, Kind::Unsupported, "Unsupported request") + } + ProxyError::NotFound => { + ockam_core::Error::new(Origin::Vault, Kind::NotFound, "Not found") + } + ProxyError::Misuse => ockam_core::Error::new(Origin::Vault, Kind::Misuse, "Misuse"), + ProxyError::Timeout => ockam_core::Error::new(Origin::Vault, Kind::Timeout, "Timeout"), + ProxyError::Protocol => { + ockam_core::Error::new(Origin::Vault, Kind::Protocol, "Invalid response") + } + } + } +} + +impl From for ProxyError { + fn from(error: ockam_core::Error) -> Self { + match error.code().kind { + Kind::Invalid => ProxyError::Invalid, + Kind::Unsupported => ProxyError::Unsupported, + Kind::NotFound => ProxyError::NotFound, + Kind::Misuse => ProxyError::Misuse, + Kind::Timeout => ProxyError::Timeout, + Kind::Serialization | Kind::Parse | Kind::Io | Kind::Protocol => ProxyError::Protocol, + _ => ProxyError::Unknown, + } + } +} + +struct Server { + vault: Vault, +} + +impl Server { + async fn start_worker( + context: &Context, + address: Address, + vault: Vault, + ) -> ockam_core::Result<()> { + let worker = Self { vault }; + context.start_worker(address, worker).await + } +} + +#[async_trait] +impl Worker for Server { + type Message = NeutralMessage; + type Context = Context; + + async fn handle_message( + &mut self, + context: &mut Self::Context, + msg: Routed, + ) -> ockam_core::Result<()> { + let local_message = msg.into_local_message(); + let mut onward_route = local_message.onward_route().clone(); + onward_route.step()?; + let onward_address = onward_route.next()?.clone(); + let return_route = local_message.return_route().clone(); + let payload = local_message.into_payload(); + + // TODO: check ACLs + let response = match onward_address.address() { + "identity_vault" => { + vault_for_signing::handle_request(self.vault.identity_vault.as_ref(), payload) + .await? + } + "secure_channel_vault" => { + vault_for_secure_channels::handle_request( + self.vault.secure_channel_vault.as_ref(), + payload, + ) + .await? + } + "credential_vault" => { + vault_for_signing::handle_request( + self.vault.credential_vault.as_ref(), + minicbor::decode(&payload)?, + ) + .await? + } + "verifying_vault" => { + vault_for_verify_signatures::handle_request( + self.vault.verifying_vault.as_ref(), + payload, + ) + .await? + } + _ => { + warn!("Unknown address: {}, ignoring request", onward_address); + return Ok(()); + } + }; + + let response = NeutralMessage::from(response); + context.send(return_route.clone(), response).await?; + + Ok(()) + } +} + +struct ConnectionState { + connection: Option, + last_ping: TimestampInSeconds, +} + +struct Client { + route: MultiAddr, + connection: Arc>, + context: Context, + instantiator: ConnectionInstantiator, +} + +impl Client { + fn new(context: Context, route: MultiAddr, instantiator: ConnectionInstantiator) -> Self { + let connection = Arc::new(AsyncMutex::new(ConnectionState { + connection: None, + last_ping: utils::now().unwrap(), + })); + + Self { + route, + connection, + context, + instantiator, + } + } + + /// Verify that the connection is still open by pinging the other end + /// In case the connection times out, the connection is established again + async fn assert_connection(&self) -> ockam_core::Result { + let mut guard = self.connection.lock().await; + let now = utils::now()?; + if let Some(connection) = guard.connection.as_ref() { + if guard.last_ping < now + 10 { + connection.route() + } else { + trace!("pinging connection {}", connection.transport_route()); + let echo_route = route![connection.transport_route(), DefaultAddress::ECHO_SERVICE]; + let result: ockam_core::Result<()> = + self.context.send_and_receive(echo_route, ()).await; + + let route = connection.route()?; + match result { + Ok(_) => { + guard.last_ping = now; + Ok(route) + } + Err(_) => { + debug!( + "connection timed out, re-establishing connection to {}", + self.route + ); + guard.connection = None; + let connection = self + .instantiator + .connect(&self.context, &self.route) + .await?; + let route = connection.route()?; + guard.connection = Some(connection); + guard.last_ping = now; + Ok(route) + } + } + } + } else { + debug!("establishing connection to {}", self.route); + // we need to establish the connection + let connection = self + .instantiator + .connect(&self.context, &self.route) + .await?; + let route = connection.route()?; + guard.connection = Some(connection); + guard.last_ping = now; + Ok(route) + } + } + + fn create_for_destination(self: &Arc, destination: &str) -> Arc { + Arc::new(SpecificClient { + client: self.clone(), + destination: destination.into(), + }) + } +} + +// A client with a known address destination +struct SpecificClient { + client: Arc, + destination: Address, +} + +impl SpecificClient { + async fn send_and_receive(&self, request: RQ) -> ockam_core::Result + where + RQ: minicbor::Encode<()> + minicbor::CborLen<()>, + for<'a> RS: minicbor::Decode<'a, ()>, + { + let route = self.client.assert_connection().await?; + let encoded = cbor_encode_preallocate(request)?; + let response: NeutralMessage = self + .client + .context + .send_and_receive( + route![route, self.destination.clone()], + NeutralMessage::from(encoded), + ) + .await?; + + Ok(minicbor::decode::(&response.into_vec())?) + } +} + +mod vault_for_signing { + use crate::proxy_vault::protocol::{ProxyError, SpecificClient}; + use minicbor::{CborLen, Decode, Encode}; + use ockam_core::{async_trait, cbor_encode_preallocate}; + use ockam_vault::{ + Signature, SigningKeyType, SigningSecretKeyHandle, VaultForSigning, VerifyingPublicKey, + }; + + #[derive(Encode, Decode, CborLen)] + #[rustfmt::skip] + pub(super) enum Request { + #[n(0)] Sign { + #[n(0)] signing_secret_key_handle: SigningSecretKeyHandle, + #[n(1)] data: Vec, + }, + #[n(1)] GenerateSigningSecretKey { + #[n(0)] signing_key_type: SigningKeyType, + }, + #[n(2)] GetVerifyingPublicKey { + #[n(0)] signing_secret_key_handle: SigningSecretKeyHandle, + }, + #[n(3)] GetSecretKeyHandle { + #[n(0)] verifying_public_key: VerifyingPublicKey, + }, + #[n(4)] DeleteSigningSecretKey { + #[n(0)] signing_secret_key_handle: SigningSecretKeyHandle, + }, + } + + #[derive(Encode, Decode, CborLen)] + #[rustfmt::skip] + enum Response { + #[n(0)] Sign(#[n(0)] Result), + #[n(1)] SigningSecretKeyHandle(#[n(0)] Result), + #[n(2)] VerifyingPublicKey(#[n(0)] Result), + #[n(3)] SecretKeyHandle(#[n(0)] Result), + #[n(4)] DeletedSigningSecretKey(#[n(0)] Result), + } + + pub(super) async fn handle_request( + vault: &dyn VaultForSigning, + request: Vec, + ) -> ockam_core::Result> { + let request: Request = minicbor::decode(&request)?; + let response = match request { + Request::Sign { + signing_secret_key_handle, + data, + } => { + trace!("sign request for {:?}", signing_secret_key_handle); + let signature = vault + .sign(&signing_secret_key_handle, &data) + .await + .map_err(Into::into); + Response::Sign(signature) + } + Request::GenerateSigningSecretKey { signing_key_type } => { + trace!( + "generate_signing_secret_key request for {:?}", + signing_key_type + ); + let handle = vault + .generate_signing_secret_key(signing_key_type) + .await + .map_err(Into::into); + Response::SigningSecretKeyHandle(handle) + } + Request::GetVerifyingPublicKey { + signing_secret_key_handle, + } => { + trace!( + "get_verifying_public_key request for {:?}", + signing_secret_key_handle + ); + let key = vault + .get_verifying_public_key(&signing_secret_key_handle) + .await + .map_err(Into::into); + Response::VerifyingPublicKey(key) + } + Request::GetSecretKeyHandle { + verifying_public_key, + } => { + trace!( + "get_secret_key_handle request for {:?}", + verifying_public_key + ); + let handle = vault + .get_secret_key_handle(&verifying_public_key) + .await + .map_err(Into::into); + Response::SecretKeyHandle(handle) + } + Request::DeleteSigningSecretKey { + signing_secret_key_handle, + } => { + trace!( + "delete_signing_secret_key request for {:?}", + signing_secret_key_handle + ); + let result = vault + .delete_signing_secret_key(signing_secret_key_handle) + .await + .map_err(Into::into); + Response::DeletedSigningSecretKey(result) + } + }; + + cbor_encode_preallocate(response) + } + + #[async_trait] + impl VaultForSigning for SpecificClient { + async fn sign( + &self, + signing_secret_key_handle: &SigningSecretKeyHandle, + data: &[u8], + ) -> ockam_core::Result { + trace!("sending sign request for {:?}", signing_secret_key_handle); + let response: Response = self + .send_and_receive(Request::Sign { + signing_secret_key_handle: signing_secret_key_handle.clone(), + data: data.to_vec(), + }) + .await?; + + let result = match response { + Response::Sign(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn generate_signing_secret_key( + &self, + signing_key_type: SigningKeyType, + ) -> ockam_core::Result { + trace!( + "sending generate_signing_secret_key request for {:?}", + signing_key_type + ); + let response: Response = self + .send_and_receive(Request::GenerateSigningSecretKey { signing_key_type }) + .await?; + + let result = match response { + Response::SigningSecretKeyHandle(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn get_verifying_public_key( + &self, + signing_secret_key_handle: &SigningSecretKeyHandle, + ) -> ockam_core::Result { + trace!( + "sending get_verifying_public_key request for {:?}", + signing_secret_key_handle + ); + let response: Response = self + .send_and_receive(Request::GetVerifyingPublicKey { + signing_secret_key_handle: signing_secret_key_handle.clone(), + }) + .await?; + + let result = match response { + Response::VerifyingPublicKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn get_secret_key_handle( + &self, + verifying_public_key: &VerifyingPublicKey, + ) -> ockam_core::Result { + trace!( + "sending get_secret_key_handle request for {:?}", + verifying_public_key + ); + let response: Response = self + .send_and_receive(Request::GetSecretKeyHandle { + verifying_public_key: verifying_public_key.clone(), + }) + .await?; + + let result = match response { + Response::SecretKeyHandle(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn delete_signing_secret_key( + &self, + signing_secret_key_handle: SigningSecretKeyHandle, + ) -> ockam_core::Result { + trace!( + "sending delete_signing_secret_key request for {:?}", + signing_secret_key_handle + ); + let response: Response = self + .send_and_receive(Request::DeleteSigningSecretKey { + signing_secret_key_handle, + }) + .await?; + + let result = match response { + Response::DeletedSigningSecretKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + } +} + +pub mod vault_for_secure_channels { + use crate::proxy_vault::protocol::{ProxyError, SpecificClient}; + use minicbor::{CborLen, Decode, Encode}; + use ockam_core::{async_trait, cbor_encode_preallocate}; + use ockam_vault::{ + AeadSecretKeyHandle, HKDFNumberOfOutputs, HashOutput, HkdfOutput, SecretBufferHandle, + VaultForSecureChannels, X25519PublicKey, X25519SecretKeyHandle, + }; + + pub(super) async fn handle_request( + vault: &dyn VaultForSecureChannels, + request: Vec, + ) -> ockam_core::Result> { + let request: Request = minicbor::decode(&request)?; + let response = match request { + Request::X25519Ecdh { + secret_key_handle, + peer_public_key, + } => { + trace!("x25519_ecdh request for {secret_key_handle:?} and {peer_public_key:?}",); + let result = vault + .x25519_ecdh(&secret_key_handle, &peer_public_key) + .await; + Response::X25519Ecdh(result.map_err(Into::into)) + } + Request::Hash { data } => { + trace!("hash request"); + let result = vault.hash(&data).await; + Response::Hash(result.map_err(Into::into)) + } + Request::Hkdf { + salt, + input_key_material, + number_of_outputs, + } => { + trace!("hkdf request for {input_key_material:?}, {number_of_outputs:?}",); + let result = vault + .hkdf(&salt, input_key_material.as_ref(), number_of_outputs) + .await; + Response::Hkdf(result.map_err(Into::into)) + } + Request::AeadEncrypt { + secret_key_handle, + mut plain_text, + nonce, + aad, + } => { + trace!("aead_encrypt request for {secret_key_handle:?}"); + let result = vault + .aead_encrypt(&secret_key_handle, &mut plain_text, &nonce, &aad) + .await; + let result = result.map(|_| plain_text); + Response::AeadEncrypt(result.map_err(Into::into)) + } + Request::AeadDecrypt { + secret_key_handle, + mut cipher_text, + nonce, + aad, + } => { + trace!("aead_decrypt request for {secret_key_handle:?}"); + let result = vault + .aead_decrypt(&secret_key_handle, &mut cipher_text, &nonce, &aad) + .await; + Response::AeadDecrypt( + result + .map(|decrypted| decrypted.to_vec()) + .map_err(Into::into), + ) + } + Request::PersistAeadKey { secret_key_handle } => { + trace!("persist_aead_key request for {secret_key_handle:?}"); + let result = vault.persist_aead_key(&secret_key_handle).await; + Response::PersistAeadKey(result.map_err(Into::into)) + } + Request::LoadAeadKey { secret_key_handle } => { + trace!("load_aead_key request for {secret_key_handle:?}"); + let result = vault.load_aead_key(&secret_key_handle).await; + Response::LoadAeadKey(result.map_err(Into::into)) + } + Request::GenerateStaticX25519SecretKey => { + trace!("generate_static_x25519_secret_key request"); + let result = vault.generate_static_x25519_secret_key().await; + Response::GenerateStaticX25519SecretKey(result.map_err(Into::into)) + } + Request::DeleteStaticX25519SecretKey { secret_key_handle } => { + trace!("delete_static_x25519_secret_key request for {secret_key_handle:?}"); + let result = vault + .delete_static_x25519_secret_key(secret_key_handle) + .await; + Response::DeleteStaticX25519SecretKey(result.map_err(Into::into)) + } + Request::GenerateEphemeralX25519SecretKey => { + trace!("generate_ephemeral_x25519_secret_key request"); + let result = vault.generate_ephemeral_x25519_secret_key().await; + Response::GenerateEphemeralX25519SecretKey(result.map_err(Into::into)) + } + Request::DeleteEphemeralX25519SecretKey { secret_key_handle } => { + trace!("delete_ephemeral_x25519_secret_key request for {secret_key_handle:?}"); + let result = vault + .delete_ephemeral_x25519_secret_key(secret_key_handle) + .await; + Response::DeleteEphemeralX25519SecretKey(result.map_err(Into::into)) + } + Request::GetX25519PublicKey { secret_key_handle } => { + trace!("get_x25519_public_key request for {secret_key_handle:?}"); + let result = vault.get_x25519_public_key(&secret_key_handle).await; + Response::GetX25519PublicKey(result.map_err(Into::into)) + } + Request::GetX25519SecretKeyHandle { public_key } => { + trace!("get_x25519_secret_key_handle request for {public_key:?}"); + let result = vault.get_x25519_secret_key_handle(&public_key).await; + Response::GetX25519SecretKeyHandle(result.map_err(Into::into)) + } + Request::ImportSecretBuffer { buffer } => { + trace!("import_secret_buffer request"); + let result = vault.import_secret_buffer(buffer).await; + Response::ImportSecretBuffer(result.map_err(Into::into)) + } + Request::DeleteSecretBuffer { + secret_buffer_handle, + } => { + trace!("delete_secret_buffer request for {secret_buffer_handle:?}"); + let result = vault.delete_secret_buffer(secret_buffer_handle).await; + Response::DeleteSecretBuffer(result.map_err(Into::into)) + } + Request::ConvertSecretBufferToAeadKey { + secret_buffer_handle, + } => { + trace!("convert_secret_buffer_to_aead_key request for {secret_buffer_handle:?}"); + let result = vault + .convert_secret_buffer_to_aead_key(secret_buffer_handle) + .await; + Response::ConvertSecretBufferToAeadKey(result.map_err(Into::into)) + } + Request::DeleteAeadSecretKey { secret_key_handle } => { + trace!("delete_aead_secret_key request for {secret_key_handle:?}"); + let result = vault.delete_aead_secret_key(secret_key_handle).await; + Response::DeleteAeadSecretKey(result.map_err(Into::into)) + } + Request::Rekey { + secret_key_handle, + n, + } => { + trace!("rekey request for {secret_key_handle:?}"); + let result = vault.rekey(&secret_key_handle, n).await; + Response::Rekey(result.map_err(Into::into)) + } + }; + cbor_encode_preallocate(response) + } + + #[derive(Encode, Decode, CborLen)] + #[rustfmt::skip] + enum Request { + #[n(0)] X25519Ecdh { + #[n(0)] secret_key_handle: X25519SecretKeyHandle, + #[n(1)] peer_public_key: X25519PublicKey, + }, + #[n(1)] Hash { + #[n(0)] data: Vec, + }, + #[n(2)] Hkdf { + #[n(0)] salt: SecretBufferHandle, + #[n(1)] input_key_material: Option, + #[n(2)] number_of_outputs: HKDFNumberOfOutputs, + }, + #[n(3)] AeadEncrypt { + #[n(0)] secret_key_handle: AeadSecretKeyHandle, + #[n(1)] plain_text: Vec, + #[n(2)] nonce: Vec, + #[n(3)] aad: Vec, + }, + #[n(4)] AeadDecrypt { + #[n(0)] secret_key_handle: AeadSecretKeyHandle, + #[n(1)] cipher_text: Vec, + #[n(2)] nonce: Vec, + #[n(3)] aad: Vec, + }, + #[n(5)] PersistAeadKey { + #[n(0)] secret_key_handle: AeadSecretKeyHandle, + }, + #[n(6)] LoadAeadKey { + #[n(0)] secret_key_handle: AeadSecretKeyHandle, + }, + #[n(7)] GenerateStaticX25519SecretKey, + #[n(8)] DeleteStaticX25519SecretKey { + #[n(0)] secret_key_handle: X25519SecretKeyHandle, + }, + #[n(9)] GenerateEphemeralX25519SecretKey, + #[n(10)] DeleteEphemeralX25519SecretKey { + #[n(0)] secret_key_handle: X25519SecretKeyHandle, + }, + #[n(11)] GetX25519PublicKey { + #[n(0)] secret_key_handle: X25519SecretKeyHandle, + }, + #[n(12)] GetX25519SecretKeyHandle { + #[n(0)] public_key: X25519PublicKey, + }, + #[n(13)] ImportSecretBuffer { + #[n(0)] buffer: Vec, + }, + #[n(14)] DeleteSecretBuffer { + #[n(0)] secret_buffer_handle: SecretBufferHandle, + }, + #[n(15)] ConvertSecretBufferToAeadKey { + #[n(0)] secret_buffer_handle: SecretBufferHandle, + }, + #[n(16)] DeleteAeadSecretKey { + #[n(0)] secret_key_handle: AeadSecretKeyHandle, + }, + #[n(17)] Rekey { + #[n(0)] secret_key_handle: AeadSecretKeyHandle, + #[n(1)] n: u16, + }, + } + + #[derive(Encode, Decode, CborLen)] + #[rustfmt::skip] + enum Response { + #[n(0)] X25519Ecdh(#[n(0)] Result), + #[n(1)] Hash(#[n(0)] Result), + #[n(2)] Hkdf(#[n(0)] Result), + #[n(3)] AeadEncrypt(#[n(0)] Result, ProxyError>), + #[n(4)] AeadDecrypt(#[n(0)] Result, ProxyError>), + #[n(5)] PersistAeadKey(#[n(0)] Result<(), ProxyError>), + #[n(6)] LoadAeadKey(#[n(0)] Result<(), ProxyError>), + #[n(7)] GenerateStaticX25519SecretKey(#[n(0)] Result), + #[n(8)] DeleteStaticX25519SecretKey(#[n(0)] Result), + #[n(9)] GenerateEphemeralX25519SecretKey(#[n(0)] Result), + #[n(10)] DeleteEphemeralX25519SecretKey(#[n(0)] Result), + #[n(11)] GetX25519PublicKey(#[n(0)] Result), + #[n(12)] GetX25519SecretKeyHandle(#[n(0)] Result), + #[n(13)] ImportSecretBuffer(#[n(0)] Result), + #[n(14)] DeleteSecretBuffer(#[n(0)] Result), + #[n(15)] ConvertSecretBufferToAeadKey(#[n(0)] Result), + #[n(16)] DeleteAeadSecretKey(#[n(0)] Result), + #[n(17)] Rekey(#[n(0)] Result), + } + + #[async_trait] + impl VaultForSecureChannels for SpecificClient { + async fn x25519_ecdh( + &self, + secret_key_handle: &X25519SecretKeyHandle, + peer_public_key: &X25519PublicKey, + ) -> ockam_core::Result { + trace!("sending x25519_ecdh request for {secret_key_handle:?} and {peer_public_key:?}"); + let response: Response = self + .send_and_receive(Request::X25519Ecdh { + secret_key_handle: secret_key_handle.clone(), + peer_public_key: peer_public_key.clone(), + }) + .await?; + + let result = match response { + Response::X25519Ecdh(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn hash(&self, data: &[u8]) -> ockam_core::Result { + trace!("sending hash request"); + let response: Response = self + .send_and_receive(Request::Hash { + data: data.to_vec(), + }) + .await?; + + let result = match response { + Response::Hash(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn hkdf( + &self, + salt: &SecretBufferHandle, + input_key_material: Option<&SecretBufferHandle>, + number_of_outputs: HKDFNumberOfOutputs, + ) -> ockam_core::Result { + trace!("sending hkdf request for {input_key_material:?}, {number_of_outputs:?}"); + let response: Response = self + .send_and_receive(Request::Hkdf { + salt: salt.clone(), + input_key_material: input_key_material.cloned(), + number_of_outputs, + }) + .await?; + + let result = match response { + Response::Hkdf(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn aead_encrypt( + &self, + secret_key_handle: &AeadSecretKeyHandle, + plain_text: &mut [u8], + nonce: &[u8], + aad: &[u8], + ) -> ockam_core::Result<()> { + trace!("sending aead_encrypt request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::AeadEncrypt { + secret_key_handle: secret_key_handle.clone(), + plain_text: plain_text.to_vec(), + nonce: nonce.to_vec(), + aad: aad.to_vec(), + }) + .await?; + + match response { + Response::AeadEncrypt(result) => { + let result = result?; + if result.len() != plain_text.len() { + return Err(ProxyError::Protocol)?; + } + plain_text.copy_from_slice(result.as_slice()); + Ok(()) + } + _ => Err(ProxyError::Protocol)?, + } + } + + async fn aead_decrypt<'a>( + &self, + secret_key_handle: &AeadSecretKeyHandle, + cipher_text: &'a mut [u8], + nonce: &[u8], + aad: &[u8], + ) -> ockam_core::Result<&'a mut [u8]> { + trace!("sending aead_decrypt request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::AeadDecrypt { + secret_key_handle: secret_key_handle.clone(), + cipher_text: cipher_text.to_vec(), + nonce: nonce.to_vec(), + aad: aad.to_vec(), + }) + .await?; + + let result = match response { + Response::AeadDecrypt(result) => { + let result = result?; + if cipher_text.len() < result.len() { + return Err(ProxyError::Protocol)?; + } + let clear_text = cipher_text[..result.len()].as_mut(); + clear_text.copy_from_slice(result.as_slice()); + clear_text + } + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn rekey( + &self, + secret_key_handle: &AeadSecretKeyHandle, + n: u16, + ) -> ockam_core::Result { + trace!("sending rekey request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::Rekey { + secret_key_handle: secret_key_handle.clone(), + n, + }) + .await?; + + let result = match response { + Response::Rekey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn persist_aead_key( + &self, + secret_key_handle: &AeadSecretKeyHandle, + ) -> ockam_core::Result<()> { + trace!("sending persist_aead_key request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::PersistAeadKey { + secret_key_handle: secret_key_handle.clone(), + }) + .await?; + + match response { + Response::PersistAeadKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + } + + Ok(()) + } + + async fn load_aead_key( + &self, + secret_key_handle: &AeadSecretKeyHandle, + ) -> ockam_core::Result<()> { + trace!("sending load_aead_key request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::LoadAeadKey { + secret_key_handle: secret_key_handle.clone(), + }) + .await?; + + match response { + Response::LoadAeadKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + } + + Ok(()) + } + + async fn generate_static_x25519_secret_key( + &self, + ) -> ockam_core::Result { + trace!("sending generate_static_x25519_secret_key request"); + let response: Response = self + .send_and_receive(Request::GenerateStaticX25519SecretKey) + .await?; + + let result = match response { + Response::GenerateStaticX25519SecretKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn delete_static_x25519_secret_key( + &self, + secret_key_handle: X25519SecretKeyHandle, + ) -> ockam_core::Result { + trace!("sending delete_static_x25519_secret_key request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::DeleteStaticX25519SecretKey { secret_key_handle }) + .await?; + + let result = match response { + Response::DeleteStaticX25519SecretKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn generate_ephemeral_x25519_secret_key( + &self, + ) -> ockam_core::Result { + trace!("sending generate_ephemeral_x25519_secret_key request"); + let response: Response = self + .send_and_receive(Request::GenerateEphemeralX25519SecretKey) + .await?; + + let result = match response { + Response::GenerateEphemeralX25519SecretKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn delete_ephemeral_x25519_secret_key( + &self, + secret_key_handle: X25519SecretKeyHandle, + ) -> ockam_core::Result { + trace!("sending delete_ephemeral_x25519_secret_key request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::DeleteEphemeralX25519SecretKey { secret_key_handle }) + .await?; + + let result = match response { + Response::DeleteEphemeralX25519SecretKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn get_x25519_public_key( + &self, + secret_key_handle: &X25519SecretKeyHandle, + ) -> ockam_core::Result { + trace!("sending get_x25519_public_key request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::GetX25519PublicKey { + secret_key_handle: secret_key_handle.clone(), + }) + .await?; + + let result = match response { + Response::GetX25519PublicKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn get_x25519_secret_key_handle( + &self, + public_key: &X25519PublicKey, + ) -> ockam_core::Result { + trace!("sending get_x25519_secret_key_handle request for {public_key:?}"); + let response: Response = self + .send_and_receive(Request::GetX25519SecretKeyHandle { + public_key: public_key.clone(), + }) + .await?; + + let result = match response { + Response::GetX25519SecretKeyHandle(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn import_secret_buffer( + &self, + buffer: Vec, + ) -> ockam_core::Result { + trace!("sending import_secret_buffer request"); + let response: Response = self + .send_and_receive(Request::ImportSecretBuffer { buffer }) + .await?; + + let result = match response { + Response::ImportSecretBuffer(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn delete_secret_buffer( + &self, + secret_buffer_handle: SecretBufferHandle, + ) -> ockam_core::Result { + trace!("sending delete_secret_buffer request for {secret_buffer_handle:?}"); + let response: Response = self + .send_and_receive(Request::DeleteSecretBuffer { + secret_buffer_handle, + }) + .await?; + + let result = match response { + Response::DeleteSecretBuffer(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn convert_secret_buffer_to_aead_key( + &self, + secret_buffer_handle: SecretBufferHandle, + ) -> ockam_core::Result { + trace!( + "sending convert_secret_buffer_to_aead_key request for {secret_buffer_handle:?}" + ); + let response: Response = self + .send_and_receive(Request::ConvertSecretBufferToAeadKey { + secret_buffer_handle, + }) + .await?; + + let result = match response { + Response::ConvertSecretBufferToAeadKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn delete_aead_secret_key( + &self, + secret_key_handle: AeadSecretKeyHandle, + ) -> ockam_core::Result { + trace!("sending delete_aead_secret_key request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::DeleteAeadSecretKey { secret_key_handle }) + .await?; + + let result = match response { + Response::DeleteAeadSecretKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + } +} + +pub mod vault_for_verify_signatures { + use crate::proxy_vault::protocol::{ProxyError, SpecificClient}; + use minicbor::{CborLen, Decode, Encode}; + use ockam_core::{async_trait, cbor_encode_preallocate}; + use ockam_vault::{Sha256Output, Signature, VaultForVerifyingSignatures, VerifyingPublicKey}; + + pub(super) async fn handle_request( + vault: &dyn VaultForVerifyingSignatures, + request: Vec, + ) -> ockam_core::Result> { + let request: Request = minicbor::decode(&request)?; + let response = match request { + Request::Sha256 { data } => { + trace!("sha256 request"); + let result = vault.sha256(&data).await; + Response::Sha256(result.map_err(Into::into)) + } + Request::VerifySignature { + verifying_public_key, + data, + signature, + } => { + trace!("verify_signature request for {verifying_public_key:?}"); + let result = vault + .verify_signature(&verifying_public_key, &data, &signature) + .await; + Response::VerifySignature(result.map_err(Into::into)) + } + }; + cbor_encode_preallocate(response) + } + + #[derive(Encode, Decode, CborLen)] + #[rustfmt::skip] + enum Request { + #[n(0)] Sha256 { + #[n(0)] data: Vec, + }, + #[n(1)] VerifySignature { + #[n(0)] verifying_public_key: VerifyingPublicKey, + #[n(1)] data: Vec, + #[n(2)] signature: Signature, + }, + } + + #[derive(Encode, Decode, CborLen)] + #[rustfmt::skip] + enum Response { + #[n(0)] Sha256(#[n(0)] Result), + #[n(1)] VerifySignature(#[n(0)] Result), + } + + #[async_trait] + impl VaultForVerifyingSignatures for SpecificClient { + async fn sha256(&self, data: &[u8]) -> ockam_core::Result { + trace!("sending sha256 request"); + let response: Response = self + .send_and_receive(Request::Sha256 { + data: data.to_vec(), + }) + .await?; + + let result = match response { + Response::Sha256(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn verify_signature( + &self, + verifying_public_key: &VerifyingPublicKey, + data: &[u8], + signature: &Signature, + ) -> ockam_core::Result { + trace!("sending verify_signature request for {verifying_public_key:?}"); + let response: Response = self + .send_and_receive(Request::VerifySignature { + verifying_public_key: verifying_public_key.clone(), + data: data.to_vec(), + signature: signature.clone(), + }) + .await?; + + let result = match response { + Response::VerifySignature(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + } +} + +pub async fn create_vault( + context: Context, + route: MultiAddr, + instantiator: ConnectionInstantiator, +) -> Vault { + let client = Arc::new(Client::new(context, route, instantiator)); + let result = client.assert_connection().await; + if let Err(e) = result { + warn!("Failed to establish remote vault connection during boostrap: {e}"); + } + + Vault::new( + client.create_for_destination("identity_vault"), + client.create_for_destination("secure_channel_vault"), + client.create_for_destination("credential_vault"), + client.create_for_destination("verifying_vault"), + ) +} + +pub async fn start_server( + context: &Context, + address: Address, + vault: Vault, +) -> ockam_core::Result<()> { + Server::start_worker(context, address, vault).await +} + +#[cfg(test)] +mod test { + use crate::nodes::connection::ConnectionInstantiator; + use crate::proxy_vault::{create_vault, start_server}; + use ockam::identity::{Vault, MAX_NONCE}; + use ockam_core::AsyncTryClone; + use ockam_node::Context; + use ockam_vault::SigningKeyType; + + #[ockam::test] + async fn test_basic_operations(context: &mut Context) -> ockam_core::Result<()> { + let in_memory_software_vault = Vault::create().await?; + start_server( + context, + "proxy_vault".into(), + in_memory_software_vault.clone(), + ) + .await?; + + let remote_vault = create_vault( + context.async_try_clone().await?, + "/service/proxy_vault".parse()?, + ConnectionInstantiator::new(), + ) + .await; + + // generate_signing_secret_key + let secret_key = remote_vault + .identity_vault + .generate_signing_secret_key(SigningKeyType::EdDSACurve25519) + .await?; + + // get_verifying_public_key + let public_key = remote_vault + .identity_vault + .get_verifying_public_key(&secret_key) + .await?; + let direct_public_key = in_memory_software_vault + .identity_vault + .get_verifying_public_key(&secret_key) + .await?; + assert_eq!(public_key, direct_public_key); + + // sha256 + let data = b"hello"; + let hash = remote_vault.verifying_vault.sha256(data).await?; + let direct_hash = in_memory_software_vault + .verifying_vault + .sha256(data) + .await?; + assert_eq!(hash.0, direct_hash.0); + + // sign + let signature = remote_vault + .identity_vault + .sign(&secret_key, &hash.0) + .await?; + let direct_signature = in_memory_software_vault + .identity_vault + .sign(&secret_key, &hash.0) + .await?; + assert_eq!(signature, direct_signature); + + // verify_signature + let result = remote_vault + .verifying_vault + .verify_signature(&public_key, &hash.0, &signature) + .await?; + let direct_result = in_memory_software_vault + .verifying_vault + .verify_signature(&public_key, &hash.0, &signature) + .await?; + assert!(result); + assert_eq!(result, direct_result); + + // generate_static_x25519_secret_key + let secret_key = remote_vault + .secure_channel_vault + .generate_static_x25519_secret_key() + .await?; + let public_key = remote_vault + .secure_channel_vault + .get_x25519_public_key(&secret_key) + .await?; + let direct_public_key = in_memory_software_vault + .secure_channel_vault + .get_x25519_public_key(&secret_key) + .await?; + assert_eq!(public_key, direct_public_key); + + // convert_secret_buffer_to_aead_key + let secret_buffer = remote_vault + .secure_channel_vault + .import_secret_buffer(vec![0; 32]) + .await + .unwrap(); + + let aead_key = remote_vault + .secure_channel_vault + .convert_secret_buffer_to_aead_key(secret_buffer) + .await?; + + // ahead_encrypt + let nonce = MAX_NONCE.to_aes_gcm_nonce(); + let aad = b""; + + let mut buffer = b"very long message".to_vec(); + remote_vault + .secure_channel_vault + .aead_encrypt(&aead_key, &mut buffer, &nonce, aad) + .await?; + + let mut direct_buffer = b"very long message".to_vec(); + in_memory_software_vault + .secure_channel_vault + .aead_encrypt(&aead_key, &mut direct_buffer, &nonce, aad) + .await?; + assert_eq!(buffer, direct_buffer); + + // ahead_decrypt + let mut data = buffer.clone(); + remote_vault + .secure_channel_vault + .aead_decrypt(&aead_key, &mut data, &nonce, aad) + .await?; + + let mut direct_data = direct_buffer.clone(); + in_memory_software_vault + .secure_channel_vault + .aead_decrypt(&aead_key, &mut direct_data, &nonce, aad) + .await?; + assert_eq!(data, direct_data); + + Ok(()) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs b/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs index e9ad1436994..6d373083142 100644 --- a/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use crate::config::lookup::InternetAddress; -use crate::nodes::service::{NodeManagerCredentialRetrieverOptions, NodeManagerTrustOptions}; +use crate::nodes::service::{CredentialRetrieverOptions, NodeManagerTrustOptions}; use ockam::identity::utils::AttributesBuilder; use ockam::identity::SecureChannels; use ockam::tcp::{TcpListenerOptions, TcpTransport}; @@ -76,22 +76,28 @@ pub async fn start_manager_for_tests( let node_name = random_name(); cli_state - .start_node_with_optional_values(&node_name, &None, &None, Some(&tcp_listener)) + .start_node_with_optional_values( + Some(context), + &node_name, + &None, + &None, + Some(&tcp_listener), + ) .await .unwrap(); // Premise: we need an identity and a credential before the node manager starts. let identifier = cli_state.get_node(&node_name).await?.identifier(); let named_vault = cli_state.get_or_create_default_named_vault().await?; - let vault = cli_state.make_vault(named_vault).await?; + let vault = cli_state.make_vault(Some(context), named_vault).await?; let identities = cli_state.make_identities(vault).await?; let trust_options = match authority_configuration { AuthorityConfiguration::None => NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::None, - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, None, - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, ), AuthorityConfiguration::Node(authority) => { // if we have a third-party authority, we need to manually exchange identities @@ -137,10 +143,10 @@ pub async fn start_manager_for_tests( .await?; NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::InMemory(credential), - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::InMemory(credential), + CredentialRetrieverOptions::None, Some(authority_identifier), - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, ) } AuthorityConfiguration::SelfReferencing => { @@ -156,10 +162,10 @@ pub async fn start_manager_for_tests( .await?; NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::InMemory(credential), - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::InMemory(credential), + CredentialRetrieverOptions::None, Some(identifier), - NodeManagerCredentialRetrieverOptions::None, + CredentialRetrieverOptions::None, ) } }; diff --git a/implementations/rust/ockam/ockam_api/tests/common/common.rs b/implementations/rust/ockam/ockam_api/tests/common/common.rs index b1a1a381488..999ecde7bac 100644 --- a/implementations/rust/ockam/ockam_api/tests/common/common.rs +++ b/implementations/rust/ockam/ockam_api/tests/common/common.rs @@ -1,6 +1,5 @@ use core::time::Duration; -use log::debug; use ockam::identity::models::CredentialSchemaIdentifier; use ockam::identity::utils::AttributesBuilder; use ockam::identity::{ @@ -20,6 +19,7 @@ use ockam_transport_tcp::TcpTransport; use rand::{thread_rng, Rng}; use std::sync::Arc; use tempfile::NamedTempFile; +use tracing::debug; // Default Configuration with fake TrustedIdentifier (which can be changed after the call), // with freshly created Authority Identifier and temporary files for storage and vault diff --git a/implementations/rust/ockam/ockam_api/tests/common/session.rs b/implementations/rust/ockam/ockam_api/tests/common/session.rs index 86e2723d5be..f33debc0df9 100644 --- a/implementations/rust/ockam/ockam_api/tests/common/session.rs +++ b/implementations/rust/ockam/ockam_api/tests/common/session.rs @@ -1,5 +1,4 @@ use core::sync::atomic::{AtomicBool, Ordering}; -use log::info; use ockam::{route, Address, Context}; use ockam_api::session::replacer::{ AdditionalSessionReplacer, CurrentInletStatus, ReplacerOutcome, ReplacerOutputKind, @@ -10,6 +9,7 @@ use ockam_core::errcode::{Kind, Origin}; use ockam_core::{async_trait, Any, Error, NeutralMessage, Result, Route, Routed, Worker}; use std::sync::atomic::AtomicU8; use std::time::Duration; +use tracing::info; pub struct MockEchoer { pub responsive: Arc, diff --git a/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs b/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs index 3627e5a8df8..7cff81681d5 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs @@ -132,7 +132,7 @@ async fn relay_remote_address(cli_state: &CliState) -> ockam::Result { async fn relay_alias(cli_state: &CliState) -> ockam::Result { Ok(cli_state - .get_or_create_default_named_identity() + .get_or_create_default_named_identity(None) .await? .identifier() .to_string()) diff --git a/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/invitation_access_control.rs b/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/invitation_access_control.rs index d33d81c5c31..f4e2eef0d3f 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/invitation_access_control.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/invitation_access_control.rs @@ -64,7 +64,7 @@ impl AppState { let local_identity = self .state() .await - .get_or_create_default_named_identity() + .get_or_create_default_named_identity(None) .await? .identifier(); diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs b/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs index 90c7246e21f..c56e54f75e9 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs @@ -707,7 +707,7 @@ pub(crate) async fn make_node_manager( .into_diagnostic()?; let _ = cli_state - .start_node_with_optional_values(NODE_NAME, &None, &None, Some(&listener)) + .start_node_with_optional_values(Some(&ctx), NODE_NAME, &None, &None, Some(&listener)) .await?; let trust_options = cli_state diff --git a/implementations/rust/ockam/ockam_command/src/authority/create.rs b/implementations/rust/ockam/ockam_command/src/authority/create.rs index bc729cab171..ecb9a565354 100644 --- a/implementations/rust/ockam/ockam_command/src/authority/create.rs +++ b/implementations/rust/ockam/ockam_command/src/authority/create.rs @@ -148,11 +148,13 @@ impl CreateCommand { // If no name is specified on the command line, use "authority" let identity_name = self.identity.clone().unwrap_or("authority".to_string()); if opts.state.get_named_identity(&identity_name).await.is_err() { - opts.state.create_identity_with_name(&identity_name).await?; + opts.state + .create_identity_with_name(None, &identity_name) + .await?; }; opts.state - .create_node_with_optional_values(&self.node_name, &self.identity, &None) + .create_node_with_optional_values(None, &self.node_name, &self.identity, &None) .await?; // Construct the arguments list and re-execute the ockam @@ -304,11 +306,19 @@ impl CreateCommand { // If no name is specified on the command line, use "authority" let identity_name = self.identity.clone().unwrap_or("authority".to_string()); if opts.state.get_named_identity(&identity_name).await.is_err() { - opts.state.create_identity_with_name(&identity_name).await?; + opts.state + .create_identity_with_name(Some(ctx), &identity_name) + .await?; }; let node = state - .start_node_with_optional_values(&self.node_name, &Some(identity_name), &None, None) + .start_node_with_optional_values( + Some(ctx), + &self.node_name, + &Some(identity_name), + &None, + None, + ) .await?; state .set_tcp_listener_address(&node.name(), &self.tcp_listener_address) @@ -351,7 +361,11 @@ impl CreateCommand { let exporter = "ockam-opentelemetry-exporter"; let exporter_identity = match opts.state.get_named_identity(exporter).await { Ok(exporter) => exporter, - Err(_) => opts.state.create_identity_with_name(exporter).await?, + Err(_) => { + opts.state + .create_identity_with_name(Some(ctx), exporter) + .await? + } }; // Create a default project in the database. That project information is used by the diff --git a/implementations/rust/ockam/ockam_command/src/credential/issue.rs b/implementations/rust/ockam/ockam_command/src/credential/issue.rs index feb6b6f2e9d..cdbb6ac4810 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/issue.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/issue.rs @@ -69,7 +69,7 @@ impl IssueCommand { .await?; let named_vault = opts.state.get_named_vault_or_default(&self.vault).await?; - let vault = opts.state.make_vault(named_vault).await?; + let vault = opts.state.make_vault(None, named_vault).await?; let identities = opts.state.make_identities(vault).await?; let mut attributes_builder = AttributesBuilder::with_schema(PROJECT_MEMBER_SCHEMA); diff --git a/implementations/rust/ockam/ockam_command/src/enroll/command.rs b/implementations/rust/ockam/ockam_command/src/enroll/command.rs index a49bab67610..f62cdc8b357 100644 --- a/implementations/rust/ockam/ockam_command/src/enroll/command.rs +++ b/implementations/rust/ockam/ockam_command/src/enroll/command.rs @@ -121,7 +121,7 @@ impl EnrollCommand { let _notification_handler = NotificationHandler::start(&opts.state, opts.terminal.clone()); opts.state - .get_named_identity_or_default(&self.identity) + .get_named_identity_or_default(Some(ctx), &self.identity) .await? }; @@ -214,7 +214,7 @@ impl EnrollCommand { // Use default identity. None => { if let Ok(named_identity) = - cli_state.get_or_create_default_named_identity().await + cli_state.get_or_create_default_named_identity(None).await { let name = named_identity.name(); let identifier = named_identity.identifier(); diff --git a/implementations/rust/ockam/ockam_command/src/identity/create.rs b/implementations/rust/ockam/ockam_command/src/identity/create.rs index 3026fffe5eb..0e03e6a23e6 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/create.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/create.rs @@ -1,3 +1,4 @@ +use crate::{docs, Command, CommandGlobalOpts}; use async_trait::async_trait; use clap::Args; use colorful::Colorful; @@ -13,8 +14,6 @@ use ockam_node::Context; use ockam_vault::SoftwareVaultForVerifyingSignatures; use std::collections::HashMap; -use crate::{docs, Command, CommandGlobalOpts}; - const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); @@ -45,7 +44,7 @@ pub struct CreateCommand { impl Command for CreateCommand { const NAME: &'static str = "identity create"; - async fn async_run(self, _ctx: &Context, opts: CommandGlobalOpts) -> crate::Result<()> { + async fn async_run(self, context: &Context, opts: CommandGlobalOpts) -> crate::Result<()> { let _notification_handler = NotificationHandler::start(&opts.state, opts.terminal.clone()); let vault = match &self.vault { Some(vault_name) => opts.state.get_or_create_named_vault(vault_name).await?, @@ -54,23 +53,33 @@ impl Command for CreateCommand { if let Some(identity) = self.identity.clone() { self.import(opts, vault, identity).await?; } else { - self.create(opts, vault).await?; + self.create(context, opts, vault).await?; }; Ok(()) } } impl CreateCommand { - async fn create(self, opts: CommandGlobalOpts, vault: NamedVault) -> miette::Result<()> { + async fn create( + self, + context: &Context, + opts: CommandGlobalOpts, + vault: NamedVault, + ) -> miette::Result<()> { let identity = match &self.key_id { Some(key_id) => { opts.state - .create_identity_with_key_id(&self.name, &vault.name(), key_id.as_ref()) + .create_identity_with_key_id( + Some(context), + &self.name, + &vault.name(), + key_id.as_ref(), + ) .await? } None => { opts.state - .create_identity_with_name_and_vault(&self.name, &vault.name()) + .create_identity_with_name_and_vault(Some(context), &self.name, &vault.name()) .await? } }; diff --git a/implementations/rust/ockam/ockam_command/src/identity/default.rs b/implementations/rust/ockam/ockam_command/src/identity/default.rs index 5c807c78c46..e59a2d91277 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/default.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/default.rs @@ -49,7 +49,10 @@ impl DefaultCommand { } } None => { - let identity = opts.state.get_or_create_default_named_identity().await?; + let identity = opts + .state + .get_or_create_default_named_identity(None) + .await?; opts.terminal .stdout() .plain(fmt_ok!( diff --git a/implementations/rust/ockam/ockam_command/src/identity/show.rs b/implementations/rust/ockam/ockam_command/src/identity/show.rs index dadb668b5a6..0ac1892515f 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/show.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/show.rs @@ -111,7 +111,8 @@ impl ShowCommand { full: bool, encoding: Option, ) -> miette::Result<()> { - let identity = opts.state.get_identity_by_optional_name(name).await?; + // TODO: start an embedded node + let identity = opts.state.get_identity_by_optional_name(None, name).await?; let (plain, json) = if full { if Some(EncodeFormat::Hex) == encoding { diff --git a/implementations/rust/ockam/ockam_command/src/message/send.rs b/implementations/rust/ockam/ockam_command/src/message/send.rs index 4cb8e6addc0..e0f68d28328 100644 --- a/implementations/rust/ockam/ockam_command/src/message/send.rs +++ b/implementations/rust/ockam/ockam_command/src/message/send.rs @@ -90,7 +90,10 @@ impl Command for SendCommand { } else { let identity_name = opts .state - .get_identity_name_or_default(&self.identity_opts.identity_name) + .get_or_create_identity_name_or_default( + Some(ctx), + &self.identity_opts.identity_name, + ) .await?; info!("starting an in memory node to send a message"); diff --git a/implementations/rust/ockam/ockam_command/src/node/create.rs b/implementations/rust/ockam/ockam_command/src/node/create.rs index 0a3d8ba1eea..06d774d7cea 100644 --- a/implementations/rust/ockam/ockam_command/src/node/create.rs +++ b/implementations/rust/ockam/ockam_command/src/node/create.rs @@ -346,12 +346,15 @@ impl CreateCommand { if let Ok(identity) = opts.state.get_named_identity(name).await { identity.name() } else { - opts.state.create_identity_with_name(name).await?.name() + opts.state + .create_identity_with_name(None, name) + .await? + .name() } } None => opts .state - .get_or_create_default_named_identity() + .get_or_create_default_named_identity(None) .await? .name(), }) @@ -547,7 +550,7 @@ mod tests { async fn get_default_node_name_with_previous_state() { let state = CliState::test().await.unwrap(); let default_node_name = "n1"; - state.create_node(default_node_name).await.unwrap(); + state.create_test_node(default_node_name).await.unwrap(); let cmd = CreateCommand::default(); let name = cmd.get_default_node_name(&state).await; diff --git a/implementations/rust/ockam/ockam_command/src/node/create/foreground.rs b/implementations/rust/ockam/ockam_command/src/node/create/foreground.rs index a02b1919caa..188c0d1100d 100644 --- a/implementations/rust/ockam/ockam_command/src/node/create/foreground.rs +++ b/implementations/rust/ockam/ockam_command/src/node/create/foreground.rs @@ -72,6 +72,7 @@ impl CreateCommand { let node_info = opts .state .start_node_with_optional_values( + Some(ctx), &node_name, &self.identity, &self.trust_opts.project_name, diff --git a/implementations/rust/ockam/ockam_command/src/project/enroll.rs b/implementations/rust/ockam/ockam_command/src/project/enroll.rs index 9358c57b792..d35ca5798c6 100644 --- a/implementations/rust/ockam/ockam_command/src/project/enroll.rs +++ b/implementations/rust/ockam/ockam_command/src/project/enroll.rs @@ -2,19 +2,17 @@ use std::fmt::{Debug, Formatter, Write}; use std::sync::Arc; use std::time::Duration; -use async_trait::async_trait; -use clap::Args; -use colorful::Colorful; -use miette::Context as _; -use miette::{miette, IntoDiagnostic}; -use serde::Serialize; - use crate::credential::CredentialOutput; use crate::enroll::OidcServiceExt; use crate::shared_args::{IdentityOpts, RetryOpts, TrustOpts}; use crate::util::parsers::duration_parser; use crate::value_parsers::parse_enrollment_ticket; use crate::{docs, Command, CommandGlobalOpts, Error, Result}; +use async_trait::async_trait; +use clap::Args; +use colorful::Colorful; +use miette::Context as _; +use miette::{miette, IntoDiagnostic}; use ockam::Context; use ockam_api::cli_state::{EnrollmentTicket, NamedIdentity}; use ockam_api::cloud::project::models::OktaAuth0; @@ -28,6 +26,7 @@ use ockam_api::nodes::InMemoryNode; use ockam_api::output::{human_readable_time, Output}; use ockam_api::terminal::fmt; use ockam_api::{fmt_log, fmt_ok}; +use serde::Serialize; const LONG_ABOUT: &str = include_str!("./static/enroll/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/enroll/after_long_help.txt"); @@ -110,11 +109,12 @@ impl Command for EnrollCommand { // Create authority client let identity = opts .state - .get_named_identity_or_default(&self.identity_opts.identity_name) + .get_named_identity_or_default(Some(ctx), &self.identity_opts.identity_name) .await?; - let node = InMemoryNode::start_with_project_name( + let node = InMemoryNode::start_with_identity_and_project_name( ctx, &opts.state, + Some(identity.name().to_string()), Some(project.name().to_string()), ) .await? diff --git a/implementations/rust/ockam/ockam_command/src/project/ticket.rs b/implementations/rust/ockam/ockam_command/src/project/ticket.rs index 619a5e8248b..589271c5c57 100644 --- a/implementations/rust/ockam/ockam_command/src/project/ticket.rs +++ b/implementations/rust/ockam/ockam_command/src/project/ticket.rs @@ -95,7 +95,7 @@ impl Command for TicketCommand { let cmd = self.parse_args(&opts).await?; let identity = opts .state - .get_identity_name_or_default(&cmd.identity_opts.identity_name) + .get_or_create_identity_name_or_default(Some(ctx), &cmd.identity_opts.identity_name) .await?; let node = InMemoryNode::start_with_project_name( diff --git a/implementations/rust/ockam/ockam_command/src/project_member/delete.rs b/implementations/rust/ockam/ockam_command/src/project_member/delete.rs index 9c9b5de3745..ed80afb015a 100644 --- a/implementations/rust/ockam/ockam_command/src/project_member/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/project_member/delete.rs @@ -60,7 +60,7 @@ impl Command for DeleteCommand { let identity = opts .state - .get_named_identity_or_default(&self.identity_opts.identity_name) + .get_named_identity_or_default(Some(ctx), &self.identity_opts.identity_name) .await?; let mut output = DeleteMemberOutput { diff --git a/implementations/rust/ockam/ockam_command/src/project_member/mod.rs b/implementations/rust/ockam/ockam_command/src/project_member/mod.rs index aede345a19a..39960cc27e6 100644 --- a/implementations/rust/ockam/ockam_command/src/project_member/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/project_member/mod.rs @@ -111,7 +111,7 @@ pub(super) async fn create_authority_client( project: &Project, ) -> crate::Result { let identity = cli_state - .get_identity_name_or_default(&identity_opts.identity_name) + .get_or_create_identity_name_or_default(Some(ctx), &identity_opts.identity_name) .await?; node.create_authority_client_with_project(ctx, project, Some(identity)) diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs index a90259c7ba7..1eee42a4c7b 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs @@ -84,7 +84,7 @@ impl CreateCommand { .wrap_err(format!("Could not convert {} into route", &self.to))?; let identity_name = opts .state - .get_identity_name_or_default(&self.identity_opts.identity_name) + .get_or_create_identity_name_or_default(Some(ctx), &self.identity_opts.identity_name) .await?; let projects_sc = get_projects_secure_channels_from_config_lookup( @@ -124,7 +124,10 @@ impl CreateCommand { let create_secure_channel = async { let identity_name = opts .state - .get_identity_name_or_default(&self.identity_opts.identity_name) + .get_or_create_identity_name_or_default( + Some(ctx), + &self.identity_opts.identity_name, + ) .await?; let payload = CreateSecureChannelRequest::new( &to, diff --git a/implementations/rust/ockam/ockam_command/src/service/start.rs b/implementations/rust/ockam/ockam_command/src/service/start.rs index 99d32c2bb6e..cb5127daeb0 100644 --- a/implementations/rust/ockam/ockam_command/src/service/start.rs +++ b/implementations/rust/ockam/ockam_command/src/service/start.rs @@ -29,12 +29,23 @@ pub enum StartSubCommand { #[arg(long, default_value_t = hop_default_addr())] addr: String, }, + RemoteProxyVault { + #[arg(long, default_value_t = remote_proxy_vault_default_addr())] + addr: String, + /// Name of the vault to expose + #[arg(long)] + vault_name: String, + }, } fn hop_default_addr() -> String { DefaultAddress::HOP_SERVICE.to_string() } +fn remote_proxy_vault_default_addr() -> String { + DefaultAddress::REMOTE_PROXY_VAULT.to_string() +} + impl StartCommand { pub fn run(self, opts: CommandGlobalOpts) -> miette::Result<()> { async_cmd(&self.name(), opts.clone(), |ctx| async move { @@ -56,6 +67,10 @@ impl StartCommand { ))?; addr } + StartSubCommand::RemoteProxyVault { addr, vault_name } => { + start_remote_proxy_vault_service(ctx, &node, addr, vault_name).await?; + addr + } }; opts.terminal.write_line(fmt_ok!( @@ -91,3 +106,14 @@ pub async fn start_hop_service( let req = api::start_hop_service(service_addr); start_service_impl(ctx, node, "Hop", req).await } + +/// Public so `ockam_command::node::create` can use it. +pub async fn start_remote_proxy_vault_service( + ctx: &Context, + node: &BackgroundNodeClient, + service_addr: &str, + vault_name: &str, +) -> Result<()> { + let req = api::start_remote_proxy_vault_service(service_addr, vault_name); + start_service_impl(ctx, node, "Remote Proxy Vault", req).await +} diff --git a/implementations/rust/ockam/ockam_command/src/space_admin/delete.rs b/implementations/rust/ockam/ockam_command/src/space_admin/delete.rs index a69a7e29af4..781c812bb64 100644 --- a/implementations/rust/ockam/ockam_command/src/space_admin/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/space_admin/delete.rs @@ -72,7 +72,7 @@ impl DeleteTui { .await?; let identity_name = opts .state - .get_identity_name_or_default(&cmd.identity_opts.identity_name) + .get_or_create_identity_name_or_default(Some(ctx), &cmd.identity_opts.identity_name) .await?; let identity_enrollment = opts .state diff --git a/implementations/rust/ockam/ockam_command/src/util/api.rs b/implementations/rust/ockam/ockam_command/src/util/api.rs index 52f47ec9bc6..d6944eb1d7f 100644 --- a/implementations/rust/ockam/ockam_command/src/util/api.rs +++ b/implementations/rust/ockam/ockam_command/src/util/api.rs @@ -1,7 +1,9 @@ use miette::IntoDiagnostic; use ockam::identity::Identifier; use ockam_api::nodes::models::flow_controls::AddConsumer; -use ockam_api::nodes::models::services::StartHopServiceRequest; +use ockam_api::nodes::models::services::{ + StartHopServiceRequest, StartRemoteProxyVaultServiceRequest, +}; use ockam_api::nodes::service::default_address::DefaultAddress; use ockam_api::nodes::*; use ockam_core::api::Request; @@ -100,6 +102,14 @@ pub(crate) fn start_hop_service(addr: &str) -> Request { Request::post(node_service(DefaultAddress::HOP_SERVICE)).body(payload) } +pub(crate) fn start_remote_proxy_vault_service( + addr: &str, + vault_name: &str, +) -> Request { + let payload = StartRemoteProxyVaultServiceRequest::new(addr, vault_name); + Request::post(node_service(DefaultAddress::REMOTE_PROXY_VAULT)).body(payload) +} + pub(crate) fn add_consumer(id: FlowControlId, address: MultiAddr) -> Request { let payload = AddConsumer::new(id, address); Request::post("/node/flow_controls/add_consumer").body(payload) diff --git a/implementations/rust/ockam/ockam_command/src/util/mod.rs b/implementations/rust/ockam/ockam_command/src/util/mod.rs index 195615a72c2..141161256f3 100644 --- a/implementations/rust/ockam/ockam_command/src/util/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/util/mod.rs @@ -231,7 +231,7 @@ mod tests { async fn test_process_multi_addr(_ctx: &mut Context) -> ockam::Result<()> { let cli_state = CliState::test().await?; - cli_state.create_node("n1").await?; + cli_state.create_test_node("n1").await?; cli_state .set_tcp_listener_address( diff --git a/implementations/rust/ockam/ockam_command/src/vault/create.rs b/implementations/rust/ockam/ockam_command/src/vault/create.rs index 241c242592c..d420ccc4183 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/create.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/create.rs @@ -3,11 +3,13 @@ use std::path::PathBuf; use async_trait::async_trait; use clap::Args; use colorful::Colorful; +use miette::{miette, IntoDiagnostic, WrapErr}; use ockam_api::cli_state::UseAwsKms; use ockam_api::{fmt_info, fmt_ok}; - +use ockam_multiaddr::MultiAddr; use ockam_node::Context; +use crate::shared_args::TrustOpts; use crate::{docs, Command, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); @@ -20,14 +22,31 @@ long_about = docs::about(LONG_ABOUT), after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct CreateCommand { + /// Name of the vault to create. If omitted, a random name will be generated. #[arg()] pub name: Option, + /// Path where the vault will be created. + /// When omitted, the vault will be created in the Ockam home directory, + /// using the name as filename. #[arg(long)] pub path: Option, + /// The route to a remote vault. + /// The destination vault must be running the `remote_ockam_vault` service. + #[arg(long, value_name = "ROUTE", requires = "identity")] + pub route: Option, + + /// The identity to access the remote vault. Must be used in conjunction with `--route`. + #[arg(long, value_name = "IDENTITY_NAME", requires = "route")] + pub identity: Option, + + /// Use AWS KMS #[arg(long, default_value = "false")] pub aws_kms: bool, + + #[command(flatten)] + pub trust_opts: TrustOpts, } #[async_trait] @@ -35,16 +54,51 @@ impl Command for CreateCommand { const NAME: &'static str = "vault create"; async fn async_run(self, _ctx: &Context, opts: CommandGlobalOpts) -> crate::Result<()> { + if self.route.is_some() && opts.state.get_named_vaults().await?.is_empty() { + Err(miette!( + "Cannot create a remote vault without a local vault. Create a local vault first." + ))?; + } + if opts.state.get_named_vaults().await?.is_empty() { opts.terminal.write_line(fmt_info!( - "This is the first vault to be created in this environment. It will be set as the default vault" - ))?; + "This is the first vault to be created in this environment. It will be set as the default vault" + ))?; } - let vault = opts - .state - .create_named_vault(self.name, self.path, UseAwsKms::from(self.aws_kms)) - .await?; + let vault = if let Some(route) = self.route { + let identity = self + .identity + .ok_or_else(|| miette!("Identity must be provided when using --route"))?; + let identity = opts.state.get_identifier_by_name(&identity).await?; + + let authority_options = opts + .state + .retrieve_authority_options( + &self.trust_opts.project_name, + &self.trust_opts.authority_identity, + &self.trust_opts.authority_route, + &self.trust_opts.credential_scope, + ) + .await + .into_diagnostic() + .wrap_err("Failed to retrieve authority options, either manually specify one or enroll to a project.")?; + + opts.state + .create_remote_vault( + self.name, + route, + identity, + authority_options.identifier, + authority_options.multiaddr, + authority_options.credential_scope, + ) + .await? + } else { + opts.state + .create_named_vault(self.name, self.path, UseAwsKms::from(self.aws_kms)) + .await? + }; opts.terminal .stdout() diff --git a/implementations/rust/ockam/ockam_command/src/vault/util.rs b/implementations/rust/ockam/ockam_command/src/vault/util.rs index 7a23f0d53a1..5733f2cbc62 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/util.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/util.rs @@ -41,10 +41,10 @@ impl Output for VaultOutput { .to_string() .color(OckamColor::PrimaryResource.color()); - let vault_type = if self.vault.path().is_some() { - "External" - } else { - "Internal" + let vault_type = match self.vault.vault_type() { + VaultType::DatabaseVault { .. } => "Internal", + VaultType::LocalFileVault { .. } => "External", + VaultType::RemoteVault { .. } => "Remote", } .to_string() .color(OckamColor::PrimaryResource.color()); @@ -83,7 +83,6 @@ impl Output for VaultOutput { Type: {vault_type} Path: {vault_path}"#, name = name, - vault_type = vault_type, vault_path = path .to_string_lossy() .to_string() @@ -94,7 +93,7 @@ impl Output for VaultOutput { use_aws_kms: UseAwsKms::Yes, } => formatdoc!( r#"Name: {name} - Type: External + Type: {vault_type} Path: {vault_path} Uses AWS KMS: {uses_aws_kms}"#, name = name, @@ -104,6 +103,36 @@ impl Output for VaultOutput { .color(OckamColor::PrimaryResource.color()), uses_aws_kms = uses_aws_kms, ), + VaultType::RemoteVault { + vault_multiaddr, + local_identifier, + authority_identifier, + authority_multiaddr, + credential_scope, + } => formatdoc!( + r#"Name: {name} + Type: {vault_type} + Route: {route} + Local Identity: {local_identifier} + Authority Identifier: {authority_identifier} + Authority Route: {authority_multiaddr} + Credential Scope: {credential_scope}"#, + route = vault_multiaddr + .to_string() + .color(OckamColor::PrimaryResource.color()), + local_identifier = local_identifier + .to_string() + .color(OckamColor::PrimaryResource.color()), + authority_identifier = authority_identifier + .to_string() + .color(OckamColor::PrimaryResource.color()), + authority_multiaddr = authority_multiaddr + .to_string() + .color(OckamColor::PrimaryResource.color()), + credential_scope = credential_scope + .to_string() + .color(OckamColor::PrimaryResource.color()), + ), }) } } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/kafka/docker.bats b/implementations/rust/ockam/ockam_command/tests/bats/kafka/docker.bats index 80aeaa6d7be..1906d811868 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/kafka/docker.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/kafka/docker.bats @@ -30,7 +30,7 @@ kafka_docker_end_to_end_encrypted_explicit_consumer() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay kafka_consumer) producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=1 + export OCKAM_LOGGING=true export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -98,7 +98,7 @@ kafka_docker_end_to_end_encrypted_project_relay_consumer() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay '*') producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=1 + export OCKAM_LOGGING=true export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -165,7 +165,7 @@ kafka_docker_end_to_end_encrypted_rust_relay_consumer() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay '*') producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=1 + export OCKAM_LOGGING=true export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -231,8 +231,8 @@ kafka_docker_end_to_end_encrypted_direct_connection() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay kafka_consumer) producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=1 - export OCKAM_LOG_LEVEL=info + export OCKAM_LOGGING=true + export OCKAM_LOG_LEVEL=trace export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" export DIRECT_CONSUMER_OUTPUT="$ADMIN_HOME/direct_consumer.log" @@ -285,11 +285,124 @@ kafka_docker_end_to_end_encrypted_direct_connection() { kafka_docker_end_to_end_encrypted_direct_connection } +kafka_docker_end_to_end_encrypted_multiple_consumers_direct_connection() { + inlet_port="$(random_port)" + + # Admin + export ADMIN_HOME="$OCKAM_HOME" + + export OCKAM_LOGGING=0 + vault_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay vault) + consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay kafka_consumer) + producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) + + export OCKAM_LOGGING=true + export OCKAM_LOG_LEVEL=debug + export RUST_BACKTRACE=full + + export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" + export DIRECT_CONSUMER_OUTPUT="$ADMIN_HOME/direct_consumer.log" + + # Vault Node + setup_home_dir + run_success "$OCKAM" node create vault --tcp-listener-address 127.0.0.1:${inlet_port} + run_success "$OCKAM" service start remote-proxy-vault --vault-name default + + # Consumer 1 + setup_home_dir + # create a remote identity, but we need an enrolled local identity first + run_success "$OCKAM" identity create local-identity + run_success "$OCKAM" project enroll --identity local-identity "${consumer_ticket}" + run_success "$OCKAM" message send --to /ip4/127.0.0.1/tcp/${inlet_port}/secure/api/service/echo hello + assert_output "hello" + run_success "$OCKAM" vault create \ + --route /ip4/127.0.0.1/tcp/${inlet_port}/secure/api/service/remote_proxy_vault \ + --identity local-identity remote-vault + run_success "$OCKAM" identity create remote-identity --vault remote-vault + run_success "$OCKAM" project enroll --identity remote-identity "${consumer_ticket}" + run_success "$OCKAM" node create consumer --identity remote-identity + + run_success "$OCKAM" kafka-outlet create --bootstrap-server 127.0.0.1:19092 + run_success "$OCKAM" kafka-inlet create --from 29092 \ + --avoid-publishing \ + --to self + run_success "$OCKAM" relay create kafka_consumer + + # Consumer 2 + setup_home_dir + # create a remote identity, but we need an enrolled local identity first + run_success "$OCKAM" identity create local-identity + run_success "$OCKAM" project enroll --identity local-identity "${consumer_ticket}" + run_success "$OCKAM" message send --to /ip4/127.0.0.1/tcp/${inlet_port}/secure/api/service/echo hello + assert_output "hello" + run_success "$OCKAM" vault create \ + --route /ip4/127.0.0.1/tcp/${inlet_port}/secure/api/service/remote_proxy_vault \ + --identity local-identity remote-vault + run_success "$OCKAM" identity create remote-identity --vault remote-vault + run_success "$OCKAM" project enroll --identity remote-identity "${consumer_ticket}" + run_success "$OCKAM" node create consumer --identity remote-identity + + run_success "$OCKAM" kafka-outlet create --bootstrap-server 127.0.0.1:19092 + run_success "$OCKAM" kafka-inlet create --from 39092 \ + --avoid-publishing \ + --to self + run_success "$OCKAM" relay create kafka_consumer + + run kafka-topics --bootstrap-server localhost:29092 --delete --topic demo || true + sleep 5 + run_success kafka-topics --bootstrap-server localhost:29092 --create --topic demo --partitions 1 --replication-factor 1 + + # Read messages, but just 2 from each consumer + kafka-console-consumer --topic demo --bootstrap-server localhost:29092 --group consumer-group-name --max-messages 2 --timeout-ms 60000 >>"${CONSUMER_OUTPUT}" & + kafka-console-consumer --topic demo --bootstrap-server localhost:39092 --group consumer-group-name --max-messages 2 --timeout-ms 60000 >>"${CONSUMER_OUTPUT}" & + + # direct consumer + kafka-console-consumer --topic demo --bootstrap-server localhost:19092 --max-messages 1 --timeout-ms 60000 >"$DIRECT_CONSUMER_OUTPUT" & + + # Producer + setup_home_dir + run_success "$OCKAM" node create producer + run_success "$OCKAM" project enroll "${producer_ticket}" + run_success "$OCKAM" kafka-outlet create --bootstrap-server 127.0.0.1:19092 + run_success "$OCKAM" kafka-inlet create --from 49092 \ + --to self \ + --consumer /project/default/service/forward_to_kafka_consumer/secure/api + + sleep 5 + run bash -c "echo 'Hello from producer - 1' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" + run bash -c "echo 'Hello from producer - 2' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" + run bash -c "echo 'Hello from producer - 3' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" + run bash -c "echo 'Hello from producer - 4' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" + sleep 20 + + run cat "$CONSUMER_OUTPUT" + assert_output --partial "Hello from producer - 1" + assert_output --partial "Hello from producer - 2" + assert_output --partial "Hello from producer - 3" + assert_output --partial "Hello from producer - 4" + + # direct connection to the kafka broker + run cat "$DIRECT_CONSUMER_OUTPUT" + refute_output --partial "Hello" +} + +@test "kafka - docker - end-to-end-encrypted - multiple consumers - direct connection - redpanda" { + export KAFKA_COMPOSE_FILE="redpanda-docker-compose.yaml" + start_kafka + kafka_docker_end_to_end_encrypted_multiple_consumers_direct_connection +} + +@test "kafka - docker - end-to-end-encrypted - multiple consumers - direct connection - apache" { + export KAFKA_COMPOSE_FILE="apache-docker-compose.yaml" + start_kafka + kafka_docker_end_to_end_encrypted_multiple_consumers_direct_connection +} + kafka_docker_end_to_end_encrypted_single_gateway() { # Admin export ADMIN_HOME="$OCKAM_HOME" - export OCKAM_LOGGING=1 - export OCKAM_LOG_LEVEL=info + export OCKAM_LOGGING=true + export OCKAM_LOG_LEVEL=debug export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" export DIRECT_CONSUMER_OUTPUT="$ADMIN_HOME/direct_consumer.log" @@ -340,7 +453,7 @@ kafka_docker_cleartext() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=1 + export OCKAM_LOGGING=true export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -403,7 +516,7 @@ kafka_docker_cleartext() { kafka_docker_end_to_end_encrypted_offset_decryption() { # Admin export ADMIN_HOME="$OCKAM_HOME" - export OCKAM_LOGGING=1 + export OCKAM_LOGGING=true export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -466,7 +579,7 @@ kafka_docker_end_to_end_encrypted_offset_decryption() { kafka_docker_encrypt_only_two_fields() { # Admin export ADMIN_HOME="$OCKAM_HOME" - export OCKAM_LOGGING=1 + export OCKAM_LOGGING=true export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" diff --git a/implementations/rust/ockam/ockam_command/tests/bats/orchestrator/remote_vault.bats b/implementations/rust/ockam/ockam_command/tests/bats/orchestrator/remote_vault.bats new file mode 100644 index 00000000000..c9b0f990c1a --- /dev/null +++ b/implementations/rust/ockam/ockam_command/tests/bats/orchestrator/remote_vault.bats @@ -0,0 +1,86 @@ +setup() { + load ../load/base.bash + load ../load/orchestrator.bash + load_bats_ext + setup_home_dir + skip_if_orchestrator_tests_not_enabled + copy_enrolled_home_dir +} + +teardown() { + teardown_home_dir +} + +@test "remote vault - remote identity" { + inlet_port="$(random_port)" + + ticket=$(${OCKAM} project ticket --usage-count 2) + + # start remote vault node + run_success ${OCKAM} node create vault-node --tcp-listener-address 127.0.0.1:${inlet_port} + run_success ${OCKAM} service start remote-proxy-vault --vault-name default + + # create a local identity, which will be used to access the remote vault, and enroll to the project + setup_home_dir + run_success ${OCKAM} identity create local-identity + run_success ${OCKAM} project enroll --identity local-identity "${ticket}" + run_success ${OCKAM} vault create \ + --identity local-identity \ + --route /ip4/127.0.0.1/tcp/${inlet_port}/secure/api/service/remote_proxy_vault \ + remote-vault + run_success ${OCKAM} vault show remote-vault + run_success ${OCKAM} identity create remote-identity --vault remote-vault + + # enroll remote identity + run_success ${OCKAM} project enroll --identity remote-identity "${ticket}" + + # create a node using the remote vault and send a message to the echo service + run_success ${OCKAM} node create using-remote-vault-node --identity remote-identity + run_success ${OCKAM} message send --timeout 60 \ + --identity local-identity \ + --to /node/using-remote-vault-node/secure/api/service/echo \ + hello + assert_output "hello" + run_success ${OCKAM} message send --timeout 60 \ + --identity remote-identity \ + --to /node/using-remote-vault-node/secure/api/service/echo \ + hello + assert_output "hello" +} + +@test "remote vault - remote identity through a relay" { + ticket=$(${OCKAM} project ticket --usage-count 2) + + # start remote vault node + run_success ${OCKAM} node create vault-node + run_success ${OCKAM} service start remote-proxy-vault --vault-name default + run_success ${OCKAM} relay create vault + + # create a local identity, which will be used to access the remote vault, and enroll to the project + setup_home_dir + run_success ${OCKAM} identity create local-identity + run_success ${OCKAM} project enroll --identity local-identity "${ticket}" + run_success ${OCKAM} vault create \ + --identity local-identity \ + --route /project/default/service/forward_to_vault/secure/api/service/remote_proxy_vault \ + remote-vault + + run_success ${OCKAM} vault show remote-vault + run_success ${OCKAM} identity create remote-identity --vault remote-vault + + # enroll remote identity + run_success ${OCKAM} project enroll --identity remote-identity "${ticket}" + + # create a node using the remote vault and send a message to the echo service + run_success ${OCKAM} node create using-remote-vault-node --identity remote-identity + run_success ${OCKAM} message send --timeout 60 \ + --identity local-identity \ + --to /node/using-remote-vault-node/secure/api/service/echo \ + hello + assert_output "hello" + run_success ${OCKAM} message send --timeout 60 \ + --identity remote-identity \ + --to /node/using-remote-vault-node/secure/api/service/echo \ + hello + assert_output "hello" +} diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20241030100000_add_remote_ockam_vault.sql b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20241030100000_add_remote_ockam_vault.sql new file mode 100644 index 00000000000..9584432def4 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20241030100000_add_remote_ockam_vault.sql @@ -0,0 +1,11 @@ +-- Add support for remote ockam vaults +ALTER TABLE vault + ADD COLUMN vault_multiaddr TEXT; +ALTER TABLE vault + ADD COLUMN local_identifier TEXT; +ALTER TABLE vault + ADD COLUMN authority_identifier TEXT; +ALTER TABLE vault + ADD COLUMN authority_multiaddr TEXT; +ALTER TABLE vault + ADD COLUMN credential_scope TEXT; diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/sqlite/20241030100000_add_remote_ockam_vault.sql b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/sqlite/20241030100000_add_remote_ockam_vault.sql new file mode 100644 index 00000000000..9584432def4 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/sqlite/20241030100000_add_remote_ockam_vault.sql @@ -0,0 +1,11 @@ +-- Add support for remote ockam vaults +ALTER TABLE vault + ADD COLUMN vault_multiaddr TEXT; +ALTER TABLE vault + ADD COLUMN local_identifier TEXT; +ALTER TABLE vault + ADD COLUMN authority_identifier TEXT; +ALTER TABLE vault + ADD COLUMN authority_multiaddr TEXT; +ALTER TABLE vault + ADD COLUMN credential_scope TEXT; diff --git a/implementations/rust/ockam/ockam_transport_tcp/Cargo.toml b/implementations/rust/ockam/ockam_transport_tcp/Cargo.toml index 9d6108dde41..63be28e23bd 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/Cargo.toml +++ b/implementations/rust/ockam/ockam_transport_tcp/Cargo.toml @@ -37,7 +37,6 @@ cfg_aliases = "0.2.1" [dependencies] async-trait = "0.1.82" cfg-if = "1.0.0" -log = "0.4.21" minicbor = { version = "0.25.1", default-features = false, features = ["derive"] } ockam_core = { path = "../ockam_core", version = "^0.122.0" } ockam_ebpf = { version = "0.6.0", optional = true } diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/portal/inlet_listener.rs b/implementations/rust/ockam/ockam_transport_tcp/src/portal/inlet_listener.rs index 07e2941b9e6..9ad04aaa4db 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/portal/inlet_listener.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/portal/inlet_listener.rs @@ -2,7 +2,6 @@ use crate::portal::addresses::{Addresses, PortalType}; use crate::portal::tls_certificate::TlsCertificateProvider; use crate::portal::{InletSharedState, ReadHalfMaybeTls, WriteHalfMaybeTls}; use crate::{portal::TcpPortalWorker, TcpInlet, TcpInletOptions, TcpRegistry}; -use log::warn; use ockam_core::compat::net::SocketAddr; use ockam_core::compat::sync::Arc; use ockam_core::errcode::{Kind, Origin}; @@ -17,6 +16,7 @@ use std::time::Duration; use tokio::net::TcpListener; use tokio::time::Instant; use tokio_rustls::{TlsAcceptor, TlsStream}; +use tracing::log::warn; use tracing::{debug, error, instrument}; /// A TCP Portal Inlet listen processor diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/portal/tls_certificate.rs b/implementations/rust/ockam/ockam_transport_tcp/src/portal/tls_certificate.rs index 28f2eed9921..3c0297f5b3e 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/portal/tls_certificate.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/portal/tls_certificate.rs @@ -1,5 +1,4 @@ use core::fmt::{Debug, Display, Formatter}; -use log::warn; use minicbor::{Decode, Encode}; use ockam_core::async_trait; use ockam_node::Context; @@ -9,6 +8,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; use tokio::time::Instant; +use tracing::log::warn; /// Refresh the certificate every day. pub const DEFAULT_CACHE_RETENTION: Duration = Duration::from_secs(60 * 60 * 24); diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/ebpf_support.rs b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/ebpf_support.rs index 266cba1c202..a33eb936622 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/ebpf_support.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/ebpf_support.rs @@ -9,7 +9,6 @@ use aya::programs::{tc, Link, ProgramError, SchedClassifier, TcAttachType}; use aya::{Ebpf, EbpfError}; use aya_log::EbpfLogger; use core::fmt::{Debug, Formatter}; -use log::error; use ockam_core::compat::collections::HashMap; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{Address, Error, Result}; @@ -20,7 +19,7 @@ use rand::random; use std::collections::HashSet; use std::sync::{Arc, Mutex}; use std::time::Duration; -use tracing::{debug, info, warn}; +use tracing::{debug, error, info, warn}; /// Interval at which we will get all addresses and attach eBPF to newly added interfaces pub const INTERFACE_LIST_UPDATE_INTERVAL: Duration = Duration::from_secs(30); diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/privileged_portals.rs b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/privileged_portals.rs index 93b73a65326..bceb9f02b3e 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/privileged_portals.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/privileged_portals.rs @@ -4,7 +4,6 @@ use crate::{TcpInlet, TcpInletOptions, TcpOutletOptions, TcpTransport}; use caps::Capability::{CAP_BPF, CAP_NET_ADMIN, CAP_NET_RAW, CAP_SYS_ADMIN}; use caps::{CapSet, Capability}; use core::fmt::Debug; -use log::{debug, error}; use nix::unistd::Uid; use ockam_core::{Address, DenyAll, Result, Route}; use ockam_node::compat::asynchronous::{resolve_peer, RwLock}; @@ -15,6 +14,7 @@ use std::sync::Arc; use tokio::net::TcpListener; use tokio::sync::mpsc::channel; use tracing::instrument; +use tracing::log::{debug, error}; impl TcpTransport { /// Check if privileged portals can be run with current permissions diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/raw_socket/async_fd_packet_reader.rs b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/raw_socket/async_fd_packet_reader.rs index b9614abd1e4..488ec1cecf5 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/raw_socket/async_fd_packet_reader.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/raw_socket/async_fd_packet_reader.rs @@ -4,7 +4,6 @@ use crate::privileged_portal::packet::{ use crate::privileged_portal::packet_binary::{ipv4_header, tcp_header}; use crate::privileged_portal::TcpPacketReader; use async_trait::async_trait; -use log::{error, trace}; use nix::sys::socket::MsgFlags; use ockam_core::Result; use ockam_transport_core::TransportError; @@ -12,6 +11,7 @@ use std::os::fd::{AsRawFd, OwnedFd}; use std::sync::Arc; use tokio::io::unix::AsyncFd; use tokio::io::Interest; +use tracing::log::{error, trace}; /// RawSocket packet reader implemented via tokio's AsyncFd pub struct AsyncFdPacketReader { diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/raw_socket/async_fd_packet_writer.rs b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/raw_socket/async_fd_packet_writer.rs index c2b7c5f5be5..9642e2b0a84 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/raw_socket/async_fd_packet_writer.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/raw_socket/async_fd_packet_writer.rs @@ -2,7 +2,6 @@ use crate::privileged_portal::packet::TcpStrippedHeaderAndPayload; use crate::privileged_portal::packet_binary::tcp_header_ports; use crate::privileged_portal::{tcp_set_checksum, Port, TcpPacketWriter}; use async_trait::async_trait; -use log::{debug, error}; use nix::sys::socket::{MsgFlags, SockaddrIn}; use ockam_core::Result; use ockam_transport_core::TransportError; @@ -11,6 +10,7 @@ use std::os::fd::{AsRawFd, OwnedFd}; use std::sync::Arc; use tokio::io::unix::AsyncFd; use tokio::io::Interest; +use tracing::log::{debug, error}; /// RawSocket packet writer implemented via tokio's AsyncFd pub struct AsyncFdPacketWriter { diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/transport.rs b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/transport.rs index 2e16c947705..aa23da7f69d 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/transport.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/transport.rs @@ -1,9 +1,9 @@ use crate::privileged_portal::{Iface, TcpPacketWriter}; use crate::TcpTransport; use aya::programs::tc::{qdisc_detach_program, TcAttachType}; -use log::{error, info, warn}; use ockam_core::Result; use std::collections::HashSet; +use tracing::log::{error, info, warn}; impl TcpTransport { /// Start [`RawSocketProcessor`]. Should be done once. diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/internal_processor.rs b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/internal_processor.rs index 70528406221..37d7ca4578e 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/internal_processor.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/internal_processor.rs @@ -1,6 +1,5 @@ use crate::privileged_portal::packet::RawSocketReadResult; use crate::privileged_portal::{Inlet, InletConnection, OckamPortalPacket, Outlet, PortalMode}; -use log::{debug, trace, warn}; use ockam_core::{ async_trait, cbor_encode_preallocate, route, LocalInfoIdentifier, LocalMessage, Processor, Result, @@ -11,6 +10,7 @@ use rand::random; use std::net::Ipv4Addr; use std::sync::Arc; use tokio::sync::mpsc::Receiver; +use tracing::log::{debug, trace, warn}; /// Processor handles all packets for the corresponding Inlet or Outlet. /// Packets are read by [`RawSocketProcessor`] and redirected here. diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/raw_socket_processor.rs b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/raw_socket_processor.rs index b8d56a4a7dc..6c0a3f3c953 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/raw_socket_processor.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/raw_socket_processor.rs @@ -3,10 +3,10 @@ use crate::privileged_portal::{ create_async_fd_raw_socket, Inlet, InletRegistry, Outlet, OutletRegistry, TcpPacketReader, TcpPacketWriter, }; -use log::trace; use ockam_core::{async_trait, Processor, Result}; use ockam_node::Context; use ockam_transport_core::TransportError; +use tracing::log::trace; /// Processor responsible for receiving all data with OCKAM_TCP_PORTAL_PROTOCOL on the machine /// and redirect it to individual portal workers. diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/remote_worker.rs b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/remote_worker.rs index 48ee5ad70d1..57ed8653f8d 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/remote_worker.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/privileged_portal/workers/remote_worker.rs @@ -3,7 +3,6 @@ use crate::privileged_portal::{ ConnectionIdentifier, Inlet, InletConnection, OckamPortalPacket, Outlet, OutletConnection, OutletConnectionReturnRoute, Port, TcpPacketWriter, TcpTransportEbpfSupport, }; -use log::{debug, trace}; use ockam_core::{ async_trait, Any, LocalInfoIdentifier, Result, Route, Routed, SecureChannelLocalInfo, Worker, }; @@ -12,7 +11,7 @@ use ockam_transport_core::TransportError; use std::net::Ipv4Addr; use std::sync::{Arc, RwLock}; use tokio::net::TcpListener; -use tracing::warn; +use tracing::log::{debug, trace, warn}; /// Portal mode of operation pub enum PortalMode { diff --git a/implementations/rust/ockam/ockam_transport_tcp/tests/ebpf_portal.rs b/implementations/rust/ockam/ockam_transport_tcp/tests/ebpf_portal.rs index 3a888c3408c..d651f4b43e0 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/tests/ebpf_portal.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/tests/ebpf_portal.rs @@ -1,9 +1,9 @@ #[cfg(privileged_portals_support)] mod tests { - use log::info; use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; + use tracing::log::info; use ockam_core::compat::rand::random; use ockam_core::{route, Result}; diff --git a/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs b/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs index 8c3a310e76d..76236478fc0 100644 --- a/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs +++ b/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs @@ -2,16 +2,19 @@ use crate::{ AeadSecretKeyHandle, HashOutput, HkdfOutput, SecretBufferHandle, X25519PublicKey, X25519SecretKeyHandle, }; +use minicbor::{CborLen, Decode, Encode}; use ockam_core::compat::vec::Vec; use ockam_core::{async_trait, compat::boxed::Box, Result}; /// Possible number of outputs of HKDF. +#[derive(Debug, Encode, Decode, CborLen)] +#[rustfmt::skip] pub enum HKDFNumberOfOutputs { /// Derive 2 secrets. - Two, + #[n(0)] Two, /// Derive 3 secrets. - Three, + #[n(1)] Three, } /// Vault for running a Secure Channel diff --git a/implementations/rust/ockam/ockam_vault/src/types/hashes.rs b/implementations/rust/ockam/ockam_vault/src/types/hashes.rs index 398b4cda6d5..b78701c1927 100644 --- a/implementations/rust/ockam/ockam_vault/src/types/hashes.rs +++ b/implementations/rust/ockam/ockam_vault/src/types/hashes.rs @@ -1,17 +1,18 @@ -use cfg_if::cfg_if; - use crate::{HandleToSecret, SecretBufferHandle}; +use cfg_if::cfg_if; +use minicbor::{CborLen, Decode, Encode}; use ockam_core::compat::vec::Vec; /// SHA256 digest length pub const SHA256_LENGTH: usize = 32; /// SHA-256 Output. -pub struct Sha256Output(pub [u8; SHA256_LENGTH]); +#[derive(Encode, Decode, CborLen)] +pub struct Sha256Output(#[cbor(n(0), with = "minicbor::bytes")] pub [u8; SHA256_LENGTH]); /// Handle to an AES-256 Secret Key. -#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] -pub struct AeadSecretKeyHandle(pub AeadSecretKeyHandleType); +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Encode, Decode, CborLen)] +pub struct AeadSecretKeyHandle(#[n(0)] pub AeadSecretKeyHandleType); impl AeadSecretKeyHandle { /// Constructor @@ -23,17 +24,20 @@ impl AeadSecretKeyHandle { cfg_if! { if #[cfg(any(not(feature = "disable_default_noise_protocol"), feature = "OCKAM_XX_25519_AES256_GCM_SHA256"))] { /// Hash used for Noise handshake. - pub struct HashOutput(pub Sha256Output); + #[derive(Encode, Decode, CborLen)] + pub struct HashOutput(#[n(0)] pub Sha256Output); /// SHA-256 HKDF Output. - pub struct Sha256HkdfOutput(pub Vec); + #[derive(Encode, Decode, CborLen)] + pub struct Sha256HkdfOutput(#[n(0)] pub Vec); /// HKDF Output. - pub struct HkdfOutput(pub Sha256HkdfOutput); + #[derive(Encode, Decode, CborLen)] + pub struct HkdfOutput(#[n(0)] pub Sha256HkdfOutput); /// Handle to an AES-256 Secret Key. - #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] - pub struct Aes256GcmSecretKeyHandle(pub HandleToSecret); + #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Encode, Decode, CborLen)] + pub struct Aes256GcmSecretKeyHandle(#[n(0)] pub HandleToSecret); impl Aes256GcmSecretKeyHandle { /// Constructor diff --git a/implementations/rust/ockam/ockam_vault/src/types/mod.rs b/implementations/rust/ockam/ockam_vault/src/types/mod.rs index 85c3da7f1e1..c0dead544d8 100644 --- a/implementations/rust/ockam/ockam_vault/src/types/mod.rs +++ b/implementations/rust/ockam/ockam_vault/src/types/mod.rs @@ -7,3 +7,12 @@ pub use hashes::*; pub use public_keys::*; pub use secrets::*; pub use signatures::*; + +use alloc::format; +use alloc::string::String; + +/// Returns a hex of 16bit representing the data, used for debugging. +fn debug_hash(data: &[u8]) -> String { + let sum: u16 = data.iter().fold(0, |acc, x| acc.wrapping_add(*x as u16)); + format!("{:04x}", sum) +} diff --git a/implementations/rust/ockam/ockam_vault/src/types/public_keys.rs b/implementations/rust/ockam/ockam_vault/src/types/public_keys.rs index 6c8b689ea8f..7ba64651ec9 100644 --- a/implementations/rust/ockam/ockam_vault/src/types/public_keys.rs +++ b/implementations/rust/ockam/ockam_vault/src/types/public_keys.rs @@ -1,3 +1,4 @@ +use core::fmt::Debug; use minicbor::{CborLen, Decode, Encode}; /// X25519 public key length. @@ -27,12 +28,22 @@ pub enum VerifyingPublicKey { /// [1]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf /// [2]: https://ed25519.cr.yp.to/papers.html /// [2]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf -#[derive(Encode, Decode, CborLen, Clone, Debug, PartialEq, Eq)] +#[derive(Encode, Decode, CborLen, Clone, PartialEq, Eq)] #[cbor(transparent)] pub struct EdDSACurve25519PublicKey( #[cbor(n(0), with = "minicbor::bytes")] pub [u8; EDDSA_CURVE25519_PUBLIC_KEY_LENGTH], ); +impl Debug for EdDSACurve25519PublicKey { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!( + f, + "EdDSACurve25519PublicKey({})", + crate::types::debug_hash(&self.0) + ) + } +} + /// A Curve P-256 Public Key that is only used for ECDSA SHA256 signatures. /// /// This type only supports the uncompressed form which is 65 bytes and has @@ -60,8 +71,14 @@ pub struct ECDSASHA256CurveP256PublicKey( /// /// [1]: https://datatracker.ietf.org/doc/html/rfc7748 /// [2]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf -#[derive(Encode, Decode, CborLen, Clone, Debug, PartialEq, Eq)] +#[derive(Encode, Decode, CborLen, Clone, PartialEq, Eq)] #[cbor(transparent)] pub struct X25519PublicKey( #[cbor(n(0), with = "minicbor::bytes")] pub [u8; X25519_PUBLIC_KEY_LENGTH], ); + +impl Debug for X25519PublicKey { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "X25519PublicKey({})", crate::types::debug_hash(&self.0)) + } +} diff --git a/implementations/rust/ockam/ockam_vault/src/types/secrets.rs b/implementations/rust/ockam/ockam_vault/src/types/secrets.rs index 6d29c21f261..492dd42eef7 100644 --- a/implementations/rust/ockam/ockam_vault/src/types/secrets.rs +++ b/implementations/rust/ockam/ockam_vault/src/types/secrets.rs @@ -1,9 +1,18 @@ +use core::fmt::Debug; +use minicbor::{CborLen, Decode, Encode}; use ockam_core::compat::vec::Vec; /// Implementation-specific arbitrary vector of bytes that allows a concrete Vault implementation /// to address a specific secret that it stores. -#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] -pub struct HandleToSecret(Vec); +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Encode, Decode, CborLen)] +#[rustfmt::skip] +pub struct HandleToSecret(#[cbor(n(0), with = "minicbor::bytes")] Vec); + +impl Debug for HandleToSecret { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "HandleToSecret({})", crate::types::debug_hash(&self.0)) + } +} impl HandleToSecret { /// Constructor. @@ -23,12 +32,13 @@ impl HandleToSecret { } /// A handle to signing secret key inside a vault. -#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Encode, Decode, CborLen)] +#[rustfmt::skip] pub enum SigningSecretKeyHandle { /// Curve25519 key that is only used for EdDSA signatures. - EdDSACurve25519(HandleToSecret), + #[n(0)] EdDSACurve25519(#[n(0)] HandleToSecret), /// Curve P-256 key that is only used for ECDSA SHA256 signatures. - ECDSASHA256CurveP256(HandleToSecret), + #[n(1)] ECDSASHA256CurveP256(#[n(0)] HandleToSecret), } impl SigningSecretKeyHandle { @@ -42,18 +52,19 @@ impl SigningSecretKeyHandle { } /// Key type for Signing. See [`super::signatures::Signature`]. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Encode, Decode, CborLen)] +#[rustfmt::skip] pub enum SigningKeyType { /// See [`super::signatures::EdDSACurve25519Signature`] - EdDSACurve25519, + #[n(0)] EdDSACurve25519, /// See [`super::signatures::ECDSASHA256CurveP256Signature`] - ECDSASHA256CurveP256, + #[n(1)] ECDSASHA256CurveP256, } /// A handle to a X25519 Secret Key. -#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] -pub struct X25519SecretKeyHandle(pub HandleToSecret); +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Encode, Decode, CborLen)] +pub struct X25519SecretKeyHandle(#[n(0)] pub HandleToSecret); /// A handle to a secret Buffer (like an HKDF output). -#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] -pub struct SecretBufferHandle(pub HandleToSecret); +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Encode, Decode, CborLen)] +pub struct SecretBufferHandle(#[n(0)] pub HandleToSecret); diff --git a/implementations/rust/ockam/ockam_vault/src/types/signatures.rs b/implementations/rust/ockam/ockam_vault/src/types/signatures.rs index 65ec943e38c..5ea73028618 100644 --- a/implementations/rust/ockam/ockam_vault/src/types/signatures.rs +++ b/implementations/rust/ockam/ockam_vault/src/types/signatures.rs @@ -6,7 +6,7 @@ pub const EDDSA_CURVE25519_SIGNATURE_LENGTH: usize = 64; pub const ECDSA_SHA256_CURVEP256_SIGNATURE_LENGTH: usize = 64; /// A cryptographic signature. -#[derive(Encode, Decode, CborLen)] +#[derive(Clone, Encode, Decode, CborLen, PartialEq, Eq, Debug)] #[rustfmt::skip] pub enum Signature { /// An EdDSA signature using Curve 25519. diff --git a/tools/stress-test/src/main.rs b/tools/stress-test/src/main.rs index 135c98febf8..d24a5280e12 100644 --- a/tools/stress-test/src/main.rs +++ b/tools/stress-test/src/main.rs @@ -173,7 +173,7 @@ impl State { let listener = tcp.listen(&"127.0.0.1:0", options).await?; let _ = cli_state - .start_node_with_optional_values(NODE_NAME, &None, &None, Some(&listener)) + .start_node_with_optional_values(Some(&ctx), NODE_NAME, &None, &None, Some(&listener)) .await?; let trust_options = cli_state From 59ff67aa7c279412f53f9820af3ebe768880efb1 Mon Sep 17 00:00:00 2001 From: Davide Baldo Date: Thu, 5 Dec 2024 17:35:43 +0100 Subject: [PATCH 4/7] feat(rust): implementing kafka custodian --- Cargo.lock | 1 + .../rust/ockam/ockam_api/Cargo.toml | 1 + .../src/kafka/key_exchange/controller.rs | 217 ++++++-------- .../src/kafka/key_exchange/listener.rs | 130 +++++++++ .../ockam_api/src/kafka/key_exchange/mod.rs | 12 +- .../src/kafka/key_exchange/secure_channels.rs | 269 +++++++----------- .../kafka/protocol_aware/inlet/response.rs | 35 +-- .../src/kafka/protocol_aware/inlet/tests.rs | 21 +- .../ockam_api/src/kafka/protocol_aware/mod.rs | 26 +- .../src/kafka/protocol_aware/tests.rs | 11 +- .../src/kafka/tests/integration_test.rs | 13 +- .../src/kafka/tests/interceptor_test.rs | 24 +- .../ockam_api/src/nodes/connection/mod.rs | 4 +- .../ockam_api/src/nodes/models/services.rs | 89 +++--- .../ockam/ockam_api/src/nodes/registry.rs | 2 + .../src/nodes/service/default_address.rs | 1 + .../src/nodes/service/kafka_services.rs | 212 ++++++++++---- .../src/nodes/service/node_services.rs | 1 + .../src/nodes/service/secure_channel.rs | 2 + .../ockam_api/src/nodes/service/worker.rs | 3 + .../ockam_api/src/proxy_vault/protocol.rs | 190 +++++++++++++ .../src/kafka/consumer/create.rs | 1 + .../src/kafka/custodian/create.rs | 133 +++++++++ .../src/kafka/custodian/delete.rs | 131 +++++++++ .../ockam_command/src/kafka/custodian/list.rs | 51 ++++ .../ockam_command/src/kafka/custodian/mod.rs | 48 ++++ .../ockam_command/src/kafka/custodian/show.rs | 115 ++++++++ .../static/delete/after_long_help.txt | 7 + .../custodian/static/list/after_long_help.txt | 7 + .../ockam_command/src/kafka/inlet/create.rs | 6 + .../rust/ockam/ockam_command/src/kafka/mod.rs | 5 + .../src/kafka/producer/create.rs | 1 + .../ockam/ockam_command/src/subcommand.rs | 6 +- .../ockam/ockam_command/src/terminal/tui.rs | 3 + .../tests/bats/kafka/docker.bats | 151 ++++++++-- .../ockam_identity/src/identity/identity.rs | 3 + .../ockam_identity/src/secure_channel/api.rs | 14 +- .../src/secure_channel/decryptor.rs | 32 ++- .../src/secure_channel/encryptor_worker.rs | 3 + .../rust/ockam/ockam_identity/src/vault.rs | 18 +- .../ockam/ockam_identity/tests/channel.rs | 15 +- .../ockam/ockam_identity/tests/persistence.rs | 40 ++- .../ockam_node/src/context/send_message.rs | 49 +++- .../ockam/ockam_rust_elixir_nifs/src/lib.rs | 15 +- .../vault_for_secure_channels/common.rs | 23 ++ .../software/vault_for_secure_channels/mod.rs | 3 + .../vault_for_secure_channels/types.rs | 3 + .../vault_for_encryption_at_rest.rs | 171 +++++++++++ .../vault_for_secure_channels.rs | 37 +-- .../rust/ockam/ockam_vault/src/traits/mod.rs | 2 + .../traits/vault_for_at_rest_encryption.rs | 38 +++ .../ockam/ockam_vault/src/types/hashes.rs | 5 + 52 files changed, 1797 insertions(+), 603 deletions(-) create mode 100644 implementations/rust/ockam/ockam_api/src/kafka/key_exchange/listener.rs create mode 100644 implementations/rust/ockam/ockam_command/src/kafka/custodian/create.rs create mode 100644 implementations/rust/ockam/ockam_command/src/kafka/custodian/delete.rs create mode 100644 implementations/rust/ockam/ockam_command/src/kafka/custodian/list.rs create mode 100644 implementations/rust/ockam/ockam_command/src/kafka/custodian/mod.rs create mode 100644 implementations/rust/ockam/ockam_command/src/kafka/custodian/show.rs create mode 100644 implementations/rust/ockam/ockam_command/src/kafka/custodian/static/delete/after_long_help.txt create mode 100644 implementations/rust/ockam/ockam_command/src/kafka/custodian/static/list/after_long_help.txt create mode 100644 implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/common.rs create mode 100644 implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_encryption_at_rest.rs create mode 100644 implementations/rust/ockam/ockam_vault/src/traits/vault_for_at_rest_encryption.rs diff --git a/Cargo.lock b/Cargo.lock index 16f6000116f..19e798dcad1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4786,6 +4786,7 @@ dependencies = [ "treeline", "url", "uuid", + "zeroize", ] [[package]] diff --git a/implementations/rust/ockam/ockam_api/Cargo.toml b/implementations/rust/ockam/ockam_api/Cargo.toml index f078ac14a9c..efa7871963f 100644 --- a/implementations/rust/ockam/ockam_api/Cargo.toml +++ b/implementations/rust/ockam/ockam_api/Cargo.toml @@ -112,6 +112,7 @@ tracing-error = "0.2.0" tracing-opentelemetry = "0.27.0" tracing-subscriber = { version = "0.3", features = ["json"] } url = "2.5.2" +zeroize = { version = "1.8.1", features = ["zeroize_derive"] } ockam_multiaddr = { path = "../ockam_multiaddr", version = "0.66.0", features = ["cbor", "serde"] } ockam_transport_core = { path = "../ockam_transport_core", version = "^0.99.0" } diff --git a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs index 65c1323f394..7de12088288 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs @@ -3,23 +3,25 @@ use crate::kafka::protocol_aware::KafkaEncryptedContent; use crate::kafka::{ConsumerPublishing, ConsumerResolution}; use crate::nodes::models::relay::ReturnTiming; use crate::nodes::NodeManager; -use ockam::identity::{ - utils, DecryptionRequest, DecryptionResponse, EncryptionRequest, EncryptionResponse, - SecureChannels, TimestampInSeconds, -}; +use ockam::identity::{utils, TimestampInSeconds, AES_GCM_NONCE_LEN}; use ockam_abac::PolicyAccessControl; use ockam_core::compat::clock::{Clock, ProductionClock}; use ockam_core::compat::collections::{HashMap, HashSet}; use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{async_trait, route, Address, Error}; +use ockam_core::{async_trait, Error}; use ockam_node::Context; +use ockam_vault::{AeadSecretKeyHandle, HandleToSecret, VaultForEncryptionAtRest}; use std::sync::{Arc, Weak}; use time::Duration; use tokio::sync::Mutex; +const AES_GCM_TAGSIZE: usize = 16; + #[derive(Clone)] pub(crate) struct KafkaKeyExchangeControllerImpl { pub(crate) inner: Arc>, + // outside, so we don't need to lock the inner when performing encryption/decryption + pub(crate) encryption_at_rest: Arc, } #[async_trait] @@ -31,58 +33,44 @@ impl KafkaKeyExchangeController for KafkaKeyExchangeControllerImpl { content: Vec, ) -> ockam_core::Result { let topic_key_handler = self.get_or_exchange_key(context, topic_name).await?; - let encryption_response: EncryptionResponse = context - .send_and_receive( - route![topic_key_handler.encryptor_api_address.clone()], - EncryptionRequest::Encrypt(content), - ) - .await?; - let encrypted_content = match encryption_response { - EncryptionResponse::Ok(p) => p, - EncryptionResponse::Err(cause) => { - warn!("Cannot encrypt kafka message"); - return Err(cause); - } - }; + let content_str = String::from_utf8_lossy(&content); + debug!("Encrypting content for topic: {topic_name}, content: {content_str}"); + + let mut destination = vec![0; AES_GCM_NONCE_LEN + content.len() + AES_GCM_TAGSIZE]; + destination[AES_GCM_NONCE_LEN..content.len() + AES_GCM_NONCE_LEN].copy_from_slice(&content); + + let content_str = String::from_utf8_lossy(&destination); + debug!("Destination: {destination:? }, content: {content_str}"); + + self.encryption_at_rest + .aead_encrypt(&topic_key_handler.secret_key_handle, &mut destination, &[]) + .await?; Ok(KafkaEncryptedContent { - content: encrypted_content, - consumer_decryptor_address: topic_key_handler.consumer_decryptor_address, + content: destination, + secret_key_handler: topic_key_handler.key_identifier_for_consumer, rekey_counter: topic_key_handler.rekey_counter, }) } - async fn decrypt_content( + async fn decrypt_content<'a>( &self, - context: &mut Context, - consumer_decryptor_address: &Address, - rekey_counter: u16, - encrypted_content: Vec, - ) -> ockam_core::Result> { - let secure_channel_decryptor_api_address = self - .get_or_load_secure_channel_decryptor_api_address_for( - context, - consumer_decryptor_address, - ) - .await?; - - let decrypt_response = context - .send_and_receive( - route![secure_channel_decryptor_api_address], - DecryptionRequest(encrypted_content, Some(rekey_counter)), + kafka_encrypted_content: &'a mut KafkaEncryptedContent, + ) -> ockam_core::Result<&'a [u8]> { + let secret_key_handle = AeadSecretKeyHandle::new(HandleToSecret::new( + kafka_encrypted_content.secret_key_handler.clone(), + )); + + Ok(self + .encryption_at_rest + .aead_decrypt( + &secret_key_handle, + kafka_encrypted_content.rekey_counter, + &mut kafka_encrypted_content.content, + &[], ) - .await?; - - let decrypted_content = match decrypt_response { - DecryptionResponse::Ok(p) => p, - DecryptionResponse::Err(cause) => { - error!("cannot decrypt kafka message: closing connection"); - return Err(cause); - } - }; - - Ok(decrypted_content) + .await?) } async fn publish_consumer( @@ -124,18 +112,21 @@ impl KafkaKeyExchangeController for KafkaKeyExchangeControllerImpl { #[derive(Debug, PartialEq)] pub(crate) struct TopicEncryptionKey { pub(crate) rekey_counter: u16, - pub(crate) encryptor_api_address: Address, - pub(crate) consumer_decryptor_address: Address, + pub(crate) secret_key_handle: AeadSecretKeyHandle, + pub(crate) key_identifier_for_consumer: Vec, } const ROTATION_RETRY_DELAY: Duration = Duration::minutes(5); + +#[derive(Debug)] pub(crate) struct TopicEncryptionKeyState { - pub(crate) producer_encryptor_address: Address, + pub(crate) secret_key_handle: AeadSecretKeyHandle, + pub(crate) key_identifier_for_consumer: Vec, pub(crate) valid_until: TimestampInSeconds, pub(crate) rotate_after: TimestampInSeconds, pub(crate) last_rekey: TimestampInSeconds, pub(crate) rekey_counter: u16, - pub(crate) rekey_period: Duration, + pub(crate) rekey_period: std::time::Duration, pub(crate) last_rotation_attempt: TimestampInSeconds, } @@ -162,7 +153,7 @@ impl TopicEncryptionKeyState { return Ok(RequiredOperation::ShouldRotate); } - if now >= self.last_rekey + self.rekey_period.whole_seconds() as u64 { + if now >= self.last_rekey + self.rekey_period.as_secs() { return Ok(RequiredOperation::Rekey); } @@ -175,8 +166,7 @@ impl TopicEncryptionKeyState { pub(crate) async fn rekey( &mut self, - context: &mut Context, - secure_channel: &SecureChannels, + encryption_at_rest_vault: &Arc, now: TimestampInSeconds, ) -> ockam_core::Result<()> { if self.rekey_counter == u16::MAX { @@ -187,33 +177,10 @@ impl TopicEncryptionKeyState { )); } - let encryptor_address = &self.producer_encryptor_address; - - let secure_channel_entry = secure_channel.secure_channel_registry().get_channel_by_encryptor_address( - encryptor_address, - ).ok_or_else(|| { - Error::new( - Origin::Channel, - Kind::Unknown, - format!("Cannot find secure channel address `{encryptor_address}` in local registry"), - ) - })?; - - let rekey_response: EncryptionResponse = context - .send_and_receive( - route![secure_channel_entry.encryptor_api_address().clone()], - EncryptionRequest::Rekey, - ) + self.secret_key_handle = encryption_at_rest_vault + .rekey_and_delete(&self.secret_key_handle) .await?; - match rekey_response { - EncryptionResponse::Ok(_) => {} - EncryptionResponse::Err(cause) => { - error!("Cannot rekey secure channel: {cause}"); - return Err(cause); - } - } - self.last_rekey = now; self.rekey_counter += 1; @@ -224,61 +191,55 @@ impl TopicEncryptionKeyState { pub(crate) type TopicName = String; pub struct InnerSecureChannelController { - pub(crate) clock: Box, // we identify the secure channel instance by using the decryptor address of the consumer // which is known to both parties pub(crate) producer_topic_encryptor_map: HashMap, - pub(crate) node_manager: Weak, // describes how to reach the consumer node pub(crate) consumer_resolution: ConsumerResolution, // describes if/how to publish the consumer pub(crate) consumer_publishing: ConsumerPublishing, pub(crate) topic_relay_set: HashSet, - pub(crate) secure_channels: Arc, pub(crate) consumer_policy_access_control: PolicyAccessControl, - pub(crate) producer_policy_access_control: PolicyAccessControl, + pub(crate) clock: Box, + pub(crate) node_manager: Weak, } impl KafkaKeyExchangeControllerImpl { pub(crate) fn new( node_manager: Arc, - secure_channels: Arc, + encryption_at_rest: Arc, consumer_resolution: ConsumerResolution, consumer_publishing: ConsumerPublishing, consumer_policy_access_control: PolicyAccessControl, - producer_policy_access_control: PolicyAccessControl, ) -> KafkaKeyExchangeControllerImpl { Self::new_extended( ProductionClock, node_manager, - secure_channels, + encryption_at_rest, consumer_resolution, consumer_publishing, consumer_policy_access_control, - producer_policy_access_control, ) } pub(crate) fn new_extended( clock: impl Clock, node_manager: Arc, - secure_channels: Arc, + encryption_at_rest: Arc, consumer_resolution: ConsumerResolution, consumer_publishing: ConsumerPublishing, consumer_policy_access_control: PolicyAccessControl, - producer_policy_access_control: PolicyAccessControl, ) -> KafkaKeyExchangeControllerImpl { Self { + encryption_at_rest, inner: Arc::new(Mutex::new(InnerSecureChannelController { clock: Box::new(clock), producer_topic_encryptor_map: Default::default(), topic_relay_set: Default::default(), node_manager: Arc::downgrade(&node_manager), - secure_channels, consumer_resolution, consumer_publishing, consumer_policy_access_control, - producer_policy_access_control, })), } } @@ -287,11 +248,14 @@ impl KafkaKeyExchangeControllerImpl { #[cfg(test)] mod test { use crate::kafka::key_exchange::controller::KafkaKeyExchangeControllerImpl; + use crate::kafka::key_exchange::listener::KafkaKeyExchangeListener; use crate::kafka::{ConsumerPublishing, ConsumerResolution}; use crate::test_utils::{AuthorityConfiguration, TestNode}; + use crate::DefaultAddress; use ockam::identity::Identifier; use ockam_abac::{Action, Env, Resource, ResourceType}; use ockam_core::compat::clock::test::TestClock; + use ockam_core::AllowAll; use ockam_multiaddr::MultiAddr; use std::sync::Arc; use std::time::Duration; @@ -327,13 +291,27 @@ mod test { ) .await; - consumer_node - .node_manager - .start_key_exchanger_service( - &consumer_node.context, - crate::DefaultAddress::KEY_EXCHANGER_LISTENER.into(), - ) - .await?; + let consumer_secure_channel_listener_flow_control_id = consumer_node + .context + .flow_controls() + .get_flow_control_with_spawner(&DefaultAddress::SECURE_CHANNEL_LISTENER.into()) + .unwrap(); + + KafkaKeyExchangeListener::create( + &consumer_node.context, + consumer_node + .node_manager + .secure_channels + .vault() + .encryption_at_rest_vault, + Duration::from_secs(60), + Duration::from_secs(60), + Duration::from_secs(60), + &consumer_secure_channel_listener_flow_control_id, + AllowAll, + AllowAll, + ) + .await?; let test_clock = TestClock::new(0); @@ -377,10 +355,7 @@ mod test { .await?; assert_eq!(third_key.rekey_counter, 1); - assert_eq!( - first_key.consumer_decryptor_address, - third_key.consumer_decryptor_address - ); + assert_eq!(first_key.secret_key_handle, third_key.secret_key_handle); // 04:00 - yet another rekey should happen, but no rotation test_clock.add_seconds(60 * 3); @@ -390,10 +365,7 @@ mod test { .await?; assert_eq!(fourth_key.rekey_counter, 2); - assert_eq!( - first_key.consumer_decryptor_address, - fourth_key.consumer_decryptor_address - ); + assert_eq!(first_key.secret_key_handle, fourth_key.secret_key_handle); // 05:00 - the default duration of the key is 10 minutes, // but the rotation should happen after 5 minutes @@ -403,10 +375,7 @@ mod test { .get_or_exchange_key(&mut producer_node.context, "topic_name") .await?; - assert_ne!( - third_key.consumer_decryptor_address, - fifth_key.consumer_decryptor_address - ); + assert_ne!(third_key.secret_key_handle, fifth_key.secret_key_handle); assert_eq!(fifth_key.rekey_counter, 0); // Now let's simulate a failure to rekey by shutting down the consumer @@ -420,10 +389,7 @@ mod test { .await?; assert_eq!(sixth_key.rekey_counter, 1); - assert_eq!( - fifth_key.consumer_decryptor_address, - sixth_key.consumer_decryptor_address - ); + assert_eq!(fifth_key.secret_key_handle, sixth_key.secret_key_handle); // 10:00 - Rotation fails, but the existing key is still valid // and needs to be rekeyed @@ -434,10 +400,7 @@ mod test { .await?; assert_eq!(seventh_key.rekey_counter, 2); - assert_eq!( - fifth_key.consumer_decryptor_address, - seventh_key.consumer_decryptor_address - ); + assert_eq!(fifth_key.secret_key_handle, seventh_key.secret_key_handle); // 15:00 - Rotation fails, and the existing key is no longer valid test_clock.add_seconds(60 * 5); @@ -469,23 +432,17 @@ mod test { Some(authority.clone()), ); - let producer_policy_access_control = - node.node_manager.policies().make_policy_access_control( - node.secure_channels.identities().identities_attributes(), - Resource::new("arbitrary-resource-name", ResourceType::KafkaProducer), - Action::HandleMessage, - Env::new(), - Some(authority), - ); - KafkaKeyExchangeControllerImpl::new_extended( test_clock, (*node.node_manager).clone(), - node.node_manager.secure_channels(), + node.node_manager + .secure_channels() + .vault() + .encryption_at_rest_vault + .clone(), ConsumerResolution::SingleNode(destination), ConsumerPublishing::None, consumer_policy_access_control, - producer_policy_access_control, ) } } diff --git a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/listener.rs b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/listener.rs new file mode 100644 index 00000000000..5fefb017d78 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/listener.rs @@ -0,0 +1,130 @@ +use crate::DefaultAddress; +use minicbor::{CborLen, Decode, Encode}; +use ockam::identity::TimestampInSeconds; +use ockam_core::flow_control::FlowControlId; +use ockam_core::{ + async_trait, Address, Decodable, Encodable, Encoded, IncomingAccessControl, Message, + OutgoingAccessControl, Routed, Worker, +}; +use ockam_node::{Context, WorkerBuilder}; +use ockam_vault::VaultForEncryptionAtRest; +use rand::Rng; +use std::sync::Arc; +use std::time::Duration; + +pub(crate) struct KafkaKeyExchangeListener { + encryption_at_rest: Arc, + rekey_period: Duration, + key_validity: Duration, + key_rotation: Duration, +} + +#[derive(Debug, CborLen, Encode, Decode)] +#[rustfmt::skip] +pub(crate) struct KeyExchangeRequest { +} + +#[derive(Debug, CborLen, Encode, Decode)] +#[rustfmt::skip] +pub(crate) struct KeyExchangeResponse { + #[n(0)] pub key_identifier_for_consumer: Vec, + #[n(1)] pub secret_key: [u8; 32], + #[n(2)] pub valid_until: TimestampInSeconds, + #[n(3)] pub rotate_after: TimestampInSeconds, + #[n(4)] pub rekey_period: Duration, +} + +impl Encodable for KeyExchangeRequest { + fn encode(self) -> ockam_core::Result { + ockam_core::cbor_encode_preallocate(self) + } +} + +impl Decodable for KeyExchangeRequest { + fn decode(data: &[u8]) -> ockam_core::Result { + Ok(minicbor::decode(data)?) + } +} +impl Message for KeyExchangeRequest {} +impl Encodable for KeyExchangeResponse { + fn encode(self) -> ockam_core::Result { + ockam_core::cbor_encode_preallocate(self) + } +} + +impl Decodable for KeyExchangeResponse { + fn decode(data: &[u8]) -> ockam_core::Result { + Ok(minicbor::decode(data)?) + } +} + +impl Message for KeyExchangeResponse {} + +#[async_trait] +impl Worker for KafkaKeyExchangeListener { + type Context = Context; + type Message = KeyExchangeRequest; + + async fn handle_message( + &mut self, + context: &mut Self::Context, + message: Routed, + ) -> ockam_core::Result<()> { + let mut secret_key = [0u8; 32]; + rand::thread_rng().fill(&mut secret_key[..]); + let handle = self + .encryption_at_rest + .import_aead_key(secret_key.to_vec()) + .await?; + + let now = TimestampInSeconds(ockam_core::compat::time::now()?); + let valid_until = now + self.key_validity; + let rotate_after = now + self.key_rotation; + + context + .send( + message.return_route().clone(), + KeyExchangeResponse { + key_identifier_for_consumer: handle.into_vec(), + secret_key, + valid_until, + rotate_after, + rekey_period: self.rekey_period, + }, + ) + .await?; + + Ok(()) + } +} + +impl KafkaKeyExchangeListener { + #[allow(clippy::too_many_arguments)] + pub async fn create( + context: &Context, + encryption_at_rest: Arc, + key_rotation: Duration, + key_validity: Duration, + rekey_period: Duration, + secure_channel_flow_control: &FlowControlId, + incoming_access_control: impl IncomingAccessControl, + outgoing_access_control: impl OutgoingAccessControl, + ) -> ockam_core::Result<()> { + let address = Address::from_string(DefaultAddress::KAFKA_CUSTODIAN); + context + .flow_controls() + .add_consumer(address.clone(), secure_channel_flow_control); + + WorkerBuilder::new(KafkaKeyExchangeListener { + encryption_at_rest, + key_rotation, + key_validity, + rekey_period, + }) + .with_address(address) + .with_incoming_access_control(incoming_access_control) + .with_outgoing_access_control(outgoing_access_control) + .start(context) + .await + } +} diff --git a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/mod.rs b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/mod.rs index fb4dc81de38..212c1b68bdd 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/mod.rs @@ -1,10 +1,11 @@ use crate::kafka::protocol_aware::KafkaEncryptedContent; use minicbor::{CborLen, Decode, Encode}; -use ockam_core::{async_trait, Address}; +use ockam_core::async_trait; use ockam_multiaddr::MultiAddr; use ockam_node::Context; pub(crate) mod controller; +pub(crate) mod listener; mod secure_channels; /// Describe how to reach the consumer node: either directly or through a relay @@ -43,13 +44,10 @@ pub(crate) trait KafkaKeyExchangeController: Send + Sync + 'static { /// Decrypts the content based on the consumer decryptor address /// the secure channel is expected to be already initialized. - async fn decrypt_content( + async fn decrypt_content<'a>( &self, - context: &mut Context, - consumer_decryptor_address: &Address, - rekey_counter: u16, - encrypted_content: Vec, - ) -> ockam_core::Result>; + kafka_encrypted_content: &'a mut KafkaEncryptedContent, + ) -> ockam_core::Result<&'a [u8]>; /// Starts relays in the orchestrator for each topic name, should be used only by the consumer. /// does nothing if they were already created, but fails it they already exist. diff --git a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs index 4ac9fce7d68..f970bbfe641 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs @@ -2,42 +2,72 @@ use crate::kafka::key_exchange::controller::{ InnerSecureChannelController, KafkaKeyExchangeControllerImpl, RequiredOperation, TopicEncryptionKey, TopicEncryptionKeyState, }; +use crate::kafka::key_exchange::listener::{KeyExchangeRequest, KeyExchangeResponse}; use crate::kafka::ConsumerResolution; -use crate::nodes::service::SecureChannelType; use crate::DefaultAddress; -use ockam::identity::{SecureChannelRegistryEntry, TimestampInSeconds}; +use ockam::identity::TimestampInSeconds; use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{Address, Error, Result}; +use ockam_core::{Error, Result}; use ockam_multiaddr::proto::{Secure, Service}; use ockam_multiaddr::MultiAddr; -use ockam_node::Context; -use time::Duration; +use ockam_node::{Context, MessageSendReceiveOptions}; +use ockam_vault::AeadSecretKeyHandle; +use std::sync::Arc; +use std::time::Duration; use tokio::sync::MutexGuard; +pub(crate) struct ExchangedKey { + pub secret_key_handler: AeadSecretKeyHandle, + pub key_identifier_for_consumer: Vec, + pub valid_until: TimestampInSeconds, + pub rotate_after: TimestampInSeconds, + pub rekey_period: Duration, +} + impl KafkaKeyExchangeControllerImpl { /// Creates a secure channel for the given destination, for key exchange only. - async fn create_key_exchange_only_secure_channel( + async fn ask_for_key_over_secure_channel( + &self, inner: &MutexGuard<'_, InnerSecureChannelController>, context: &Context, mut destination: MultiAddr, - ) -> Result
{ - destination.push_back(Service::new(DefaultAddress::KEY_EXCHANGER_LISTENER))?; + ) -> Result { + destination.push_back(Secure::new(DefaultAddress::SECURE_CHANNEL_LISTENER))?; + destination.push_back(Service::new(DefaultAddress::KAFKA_CUSTODIAN))?; if let Some(node_manager) = inner.node_manager.upgrade() { - let secure_channel = node_manager - .create_secure_channel( - context, - destination, - None, - None, - None, - None, - SecureChannelType::KeyExchangeOnly, - ) + let connection = node_manager + .make_connection(context, &destination, node_manager.identifier(), None, None) + .await?; + + let send_and_receive_options = MessageSendReceiveOptions::new() + .with_incoming_access_control(Arc::new( + inner.consumer_policy_access_control.create_incoming(), + )) + .with_outgoing_access_control(Arc::new( + inner + .consumer_policy_access_control + .create_outgoing(context) + .await?, + )); + + let route = connection.route()?; + let response: KeyExchangeResponse = context + .send_and_receive_extended(route, KeyExchangeRequest {}, send_and_receive_options) + .await? + .into_body()?; + + let aead_secret_key_handle = self + .encryption_at_rest + .import_aead_key(response.secret_key.to_vec()) .await?; - // TODO: temporary workaround until the secure channel persistence is changed - tokio::time::sleep(std::time::Duration::from_millis(500)).await; - Ok(secure_channel.encryptor_address().clone()) + Ok(ExchangedKey { + secret_key_handler: aead_secret_key_handle, + key_identifier_for_consumer: response.key_identifier_for_consumer, + valid_until: response.valid_until, + rotate_after: response.rotate_after, + rekey_period: response.rekey_period, + }) } else { Err(Error::new( Origin::Transport, @@ -57,31 +87,37 @@ impl KafkaKeyExchangeControllerImpl { let mut inner = self.inner.lock().await; let rekey_counter; - let encryptor_address; + let secret_key_handle; + let key_identifier_for_consumer; let now = TimestampInSeconds(inner.clock.now()?); - let secure_channels = inner.secure_channels.clone(); if let Some(encryption_key) = inner.producer_topic_encryptor_map.get_mut(topic_name) { // before using it, check if it's still valid match encryption_key.operation(now)? { RequiredOperation::None => { // the key is still valid rekey_counter = encryption_key.rekey_counter; - encryptor_address = encryption_key.producer_encryptor_address.clone(); + secret_key_handle = encryption_key.secret_key_handle.clone(); + key_identifier_for_consumer = + encryption_key.key_identifier_for_consumer.clone(); } RequiredOperation::Rekey => { - encryption_key.rekey(context, &secure_channels, now).await?; + encryption_key.rekey(&self.encryption_at_rest, now).await?; rekey_counter = encryption_key.rekey_counter; - encryptor_address = encryption_key.producer_encryptor_address.clone(); + secret_key_handle = encryption_key.secret_key_handle.clone(); + key_identifier_for_consumer = + encryption_key.key_identifier_for_consumer.clone(); } RequiredOperation::ShouldRotate => { encryption_key.mark_rotation_attempt(); // the key is still valid, but it's time to rotate it let result = self.exchange_key(context, topic_name, &mut inner).await; match result { - Ok(producer_encryptor_address) => { - rekey_counter = 0; - encryptor_address = producer_encryptor_address; + Ok(new_state) => { + rekey_counter = new_state.rekey_counter; + secret_key_handle = new_state.secret_key_handle.clone(); + key_identifier_for_consumer = + new_state.key_identifier_for_consumer.clone(); } Err(error) => { warn!( @@ -96,49 +132,44 @@ impl KafkaKeyExchangeControllerImpl { // we might still need to rekey if let RequiredOperation::Rekey = encryption_key.operation(now)? { - encryption_key.rekey(context, &secure_channels, now).await?; + encryption_key.rekey(&self.encryption_at_rest, now).await?; } - encryptor_address = encryption_key.producer_encryptor_address.clone(); + secret_key_handle = encryption_key.secret_key_handle.clone(); rekey_counter = encryption_key.rekey_counter; + key_identifier_for_consumer = + encryption_key.key_identifier_for_consumer.clone(); } } } RequiredOperation::MustRotate => { // the key is no longer valid, must not be reused - rekey_counter = 0; - encryptor_address = self.exchange_key(context, topic_name, &mut inner).await?; + let new_key_state = self.exchange_key(context, topic_name, &mut inner).await?; + secret_key_handle = new_key_state.secret_key_handle.clone(); + key_identifier_for_consumer = new_key_state.key_identifier_for_consumer.clone(); + rekey_counter = new_key_state.rekey_counter; } }; } else { - rekey_counter = 0; - encryptor_address = self.exchange_key(context, topic_name, &mut inner).await?; + let new_key_state = self.exchange_key(context, topic_name, &mut inner).await?; + secret_key_handle = new_key_state.secret_key_handle.clone(); + key_identifier_for_consumer = new_key_state.key_identifier_for_consumer.clone(); + rekey_counter = new_key_state.rekey_counter; }; - let entry = secure_channels - .secure_channel_registry() - .get_channel_by_encryptor_address(&encryptor_address) - .ok_or_else(|| { - Error::new( - Origin::Channel, - Kind::Unknown, - format!("cannot find secure channel address `{encryptor_address}` in local registry"), - ) - })?; - Ok(TopicEncryptionKey { rekey_counter, - encryptor_api_address: entry.encryptor_api_address().clone(), - consumer_decryptor_address: entry.their_decryptor_address().clone(), + secret_key_handle, + key_identifier_for_consumer, }) } - async fn exchange_key( - &self, + async fn exchange_key<'a>( + &'a self, context: &mut Context, topic_name: &str, - inner: &mut MutexGuard<'_, InnerSecureChannelController>, - ) -> Result
{ + inner: &'a mut MutexGuard<'_, InnerSecureChannelController>, + ) -> Result<&'a TopicEncryptionKeyState> { // destination is without the final service let destination = match inner.consumer_resolution.clone() { ConsumerResolution::SingleNode(mut destination) => { @@ -171,45 +202,17 @@ impl KafkaKeyExchangeControllerImpl { } }; - let producer_encryptor_address = - Self::create_key_exchange_only_secure_channel(inner, context, destination.clone()) - .await?; - - if let Some(entry) = inner - .secure_channels - .secure_channel_registry() - .get_channel_by_encryptor_address(&producer_encryptor_address) - { - if let Err(error) = Self::validate_consumer_credentials(inner, &entry).await { - if let Some(node_manager) = inner.node_manager.upgrade() { - node_manager - .delete_secure_channel(context, &producer_encryptor_address) - .await?; - } - return Err(error); - }; - } else { - return Err(Error::new( - Origin::Transport, - Kind::Internal, - format!( - "cannot find secure channel address `{producer_encryptor_address}` in local registry" - ), - )); - } + let exchanged_key = self + .ask_for_key_over_secure_channel(inner, context, destination.clone()) + .await?; let now = TimestampInSeconds(inner.clock.now()?); - - // TODO: retrieve these values from the other party - let valid_until = now + TimestampInSeconds(10 * 60); // 10 minutes - let rotate_after = now + TimestampInSeconds(5 * 60); // 5 minutes - let rekey_period = Duration::minutes(1); - let encryption_key = TopicEncryptionKeyState { - producer_encryptor_address: producer_encryptor_address.clone(), - valid_until, - rotate_after, - rekey_period, + secret_key_handle: exchanged_key.secret_key_handler, + key_identifier_for_consumer: exchanged_key.key_identifier_for_consumer, + valid_until: exchanged_key.valid_until, + rotate_after: exchanged_key.rotate_after, + rekey_period: exchanged_key.rekey_period, last_rekey: now, last_rotation_attempt: TimestampInSeconds(0), rekey_counter: 0, @@ -219,89 +222,13 @@ impl KafkaKeyExchangeControllerImpl { .producer_topic_encryptor_map .insert(topic_name.to_string(), encryption_key); - info!("Successfully exchanged new key with {destination} for topic {topic_name}"); - Ok(producer_encryptor_address) - } - - async fn validate_consumer_credentials( - inner: &MutexGuard<'_, InnerSecureChannelController>, - entry: &SecureChannelRegistryEntry, - ) -> Result<()> { - let authorized = inner - .consumer_policy_access_control - .is_identity_authorized(entry.their_id()) - .await?; - if authorized { - Ok(()) - } else { - Err(Error::new( - Origin::Transport, - Kind::Invalid, - format!( - "unauthorized secure channel for consumer with identifier {}", - entry.their_id() - ), - )) - } - } - - /// Returns the secure channel entry for the consumer decryptor address and validate it - /// against the producer manual policy. - pub(crate) async fn get_or_load_secure_channel_decryptor_api_address_for( - &self, - ctx: &Context, - decryptor_remote_address: &Address, - ) -> Result
{ - let inner = self.inner.lock().await; - let (decryptor_api_address, their_identifier) = match inner - .secure_channels - .secure_channel_registry() - .get_channel_by_decryptor_address(decryptor_remote_address) - { - Some(entry) => ( - entry.decryptor_api_address().clone(), - entry.their_id().clone(), - ), - None => { - match inner - .secure_channels - .start_persisted_secure_channel_decryptor(ctx, decryptor_remote_address) - .await - { - Ok(sc) => ( - sc.decryptor_api_address().clone(), - sc.their_identifier().clone(), - ), - Err(e) => { - return Err(Error::new( - Origin::Channel, - Kind::Unknown, - format!( - "secure channel decryptor {} can not be retrieved: {e:?}", - decryptor_remote_address.address() - ), - )); - } - } - } - }; - - let authorized = inner - .producer_policy_access_control - .is_identity_authorized(&their_identifier) - .await?; + let encryption_key = inner + .producer_topic_encryptor_map + .get(topic_name) + .expect("key should be present"); - if authorized { - Ok(decryptor_api_address) - } else { - Err(Error::new( - Origin::Transport, - Kind::Invalid, - format!( - "unauthorized secure channel for producer with identifier {}", - their_identifier - ), - )) - } + info!("Successfully exchanged new key with {destination} for topic {topic_name}"); + debug!("Exchanged key: {encryption_key:?}"); + Ok(encryption_key) } } diff --git a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/response.rs b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/response.rs index 3f1fc6b4a0c..94879dfcf77 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/response.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/response.rs @@ -67,7 +67,7 @@ impl KafkaMessageResponseInterceptor for InletInterceptorImpl { ApiKey::FetchKey => { if self.encrypt_content { return self - .handle_fetch_response(context, &mut buffer, &request_info, &header) + .handle_fetch_response(&mut buffer, &request_info, &header) .await; } } @@ -278,7 +278,6 @@ impl InletInterceptorImpl { async fn handle_fetch_response( &self, - context: &mut Context, buffer: &mut Bytes, request_info: &RequestInfo, header: &ResponseHeader, @@ -301,9 +300,9 @@ impl InletInterceptorImpl { for record in records.iter_mut() { if let Some(record_value) = record.value.take() { let decrypted_content = if self.encrypted_fields.is_empty() { - self.decrypt_whole_record(context, record_value).await? + self.decrypt_whole_record(record_value).await? } else { - self.decrypt_specific_fields(context, record_value).await? + self.decrypt_specific_fields(record_value).await? }; record.value = Some(decrypted_content.into()); } @@ -333,28 +332,19 @@ impl InletInterceptorImpl { ) } - async fn decrypt_whole_record( - &self, - context: &mut Context, - record_value: Bytes, - ) -> Result, InterceptError> { - let message_wrapper: KafkaEncryptedContent = + async fn decrypt_whole_record(&self, record_value: Bytes) -> Result, InterceptError> { + let mut message_wrapper: KafkaEncryptedContent = Decoder::new(record_value.as_ref()).decode()?; self.key_exchange_controller - .decrypt_content( - context, - &message_wrapper.consumer_decryptor_address, - message_wrapper.rekey_counter, - message_wrapper.content, - ) + .decrypt_content(&mut message_wrapper) .await + .map(|decrypted_content| decrypted_content.to_vec()) .map_err(InterceptError::Ockam) } async fn decrypt_specific_fields( &self, - context: &mut Context, record_value: Bytes, ) -> Result, InterceptError> { let mut record_value = serde_json::from_slice::(&record_value)?; @@ -371,21 +361,16 @@ impl InletInterceptorImpl { return Err("The encrypted field is not a hex-encoded string".into()); }; - let message_wrapper: KafkaEncryptedContent = + let mut message_wrapper: KafkaEncryptedContent = Decoder::new(&encrypted_content).decode()?; let decrypted_content = self .key_exchange_controller - .decrypt_content( - context, - &message_wrapper.consumer_decryptor_address, - message_wrapper.rekey_counter, - message_wrapper.content, - ) + .decrypt_content(&mut message_wrapper) .await .map_err(InterceptError::Ockam)?; - *value = serde_json::from_slice(decrypted_content.as_slice())?; + *value = serde_json::from_slice(decrypted_content)?; } } serde_json::to_vec(&record_value).map_err(|error| { diff --git a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/tests.rs b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/tests.rs index c4714bdb0f9..0eec1531159 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/tests.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/inlet/tests.rs @@ -15,7 +15,7 @@ use kafka_protocol::records::{ Compression, Record, RecordBatchDecoder, RecordBatchEncoder, RecordEncodeOptions, TimestampType, }; use minicbor::{Decoder, Encoder}; -use ockam_core::{async_trait, Address}; +use ockam_core::async_trait; use ockam_node::Context; use serde_json::json; use std::sync::Arc; @@ -36,20 +36,17 @@ impl KafkaKeyExchangeController for MockKafkaKeyExchangeController { let mut new_content = ENCRYPTED_PREFIX.to_vec(); new_content.extend_from_slice(&content); Ok(KafkaEncryptedContent { - consumer_decryptor_address: Address::from_string("mock"), + secret_key_handler: "mock".as_bytes().to_vec(), content: new_content, rekey_counter: u16::MAX, }) } - async fn decrypt_content( + async fn decrypt_content<'a>( &self, - _context: &mut Context, - _consumer_decryptor_address: &Address, - _rekey_counter: u16, - encrypted_content: Vec, - ) -> ockam_core::Result> { - Ok(encrypted_content[PREFIX_LEN..].to_vec()) + kafka_encrypted_content: &'a mut KafkaEncryptedContent, + ) -> ockam_core::Result<&'a [u8]> { + Ok(&kafka_encrypted_content.content[PREFIX_LEN..]) } async fn publish_consumer( @@ -175,8 +172,8 @@ pub fn decode_field_value(value: String) -> serde_json::Value { let value = hex::decode(value).unwrap(); let encrypted_content: KafkaEncryptedContent = Decoder::new(value.as_ref()).decode().unwrap(); assert_eq!( - encrypted_content.consumer_decryptor_address, - Address::from_string("mock") + encrypted_content.secret_key_handler, + "mock".as_bytes().to_vec() ); let encrypted_tag = @@ -196,7 +193,7 @@ pub fn encode_field_value(value: serde_json::Value) -> String { let mut encoder = Encoder::new(&mut write_buffer); encoder .encode(KafkaEncryptedContent { - consumer_decryptor_address: Address::from_string("mock"), + secret_key_handler: "mock".as_bytes().to_vec(), content: encrypted_content, rekey_counter: u16::MAX, }) diff --git a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/mod.rs b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/mod.rs index 0d002fcb7d3..11c65c58963 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/mod.rs @@ -2,12 +2,12 @@ use bytes::BytesMut; use kafka_protocol::messages::ApiKey; use kafka_protocol::protocol::buf::NotEnoughBytesError; use minicbor::{CborLen, Decode, Encode}; +use ockam_core::async_trait; use ockam_core::compat::{ collections::HashMap, fmt::Debug, sync::{Arc, Mutex}, }; -use ockam_core::{async_trait, Address}; use ockam_node::Context; pub(crate) mod outlet; @@ -147,12 +147,26 @@ impl PortalInterceptor for KafkaMessageInterceptorWrapper { #[rustfmt::skip] /// Wraps the content within every record batch pub(crate) struct KafkaEncryptedContent { - /// The secure channel identifier used to encrypt the content - #[n(0)] pub consumer_decryptor_address: Address, - /// The encrypted content - #[n(1)] pub content: Vec, + /// The secret key handle used to encrypt the content + #[n(0)] pub secret_key_handler: Vec, /// Number of times rekey was performed before encrypting the content - #[n(2)] pub rekey_counter: u16, + #[n(1)] pub rekey_counter: u16, + /// The encrypted content + #[n(2)] pub content: Vec, + + // TODO: the custodian should sign off information about the key with a purpose key: + // - secret_key_handle + // - producer identifier + // - key creation timestamp + + // TODO: the producer should use AAD to sign off: + // - topic name + // - record creation timestamp + // + // the topic name will protect against replay attacks to other topics + // the timestamp may protect against replay attacks in the same topic1, + // but it isn't clear how to properly validate atm (there is no simple way to + // get an expected timestamp range) } /// By default, kafka supports up to 1MB messages. 16MB is the maximum suggested diff --git a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/tests.rs b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/tests.rs index c8a4e24b015..72282f567dd 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/tests.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/protocol_aware/tests.rs @@ -55,21 +55,12 @@ mod test { Some(handle.node_manager.identifier()), ); - let producer_policy_access_control = policies.make_policy_access_control( - secure_channels.identities().identities_attributes(), - Resource::new("arbitrary-resource-name", ResourceType::KafkaProducer), - Action::HandleMessage, - Env::new(), - Some(handle.node_manager.identifier()), - ); - let secure_channel_controller = KafkaKeyExchangeControllerImpl::new( (*handle.node_manager).clone(), - secure_channels, + secure_channels.vault().encryption_at_rest_vault, ConsumerResolution::None, ConsumerPublishing::None, consumer_policy_access_control, - producer_policy_access_control, ); let interceptor = InletInterceptorImpl::new( diff --git a/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs b/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs index 1761bc0b20f..79e12dae0a4 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs @@ -67,23 +67,12 @@ async fn create_kafka_service( ) .await?; - let producer_policy_access_control = handle - .node_manager - .policy_access_control( - Some(project_authority.clone()), - Resource::new(listener_address.address(), ResourceType::KafkaProducer), - Action::HandleMessage, - None, - ) - .await?; - let secure_channel_controller = KafkaKeyExchangeControllerImpl::new( (*handle.node_manager).clone(), - handle.secure_channels.clone(), + handle.secure_channels.vault().encryption_at_rest_vault, ConsumerResolution::ViaRelay(MultiAddr::try_from("/service/api")?), ConsumerPublishing::None, consumer_policy_access_control, - producer_policy_access_control, ); let mut interceptor_multiaddr = MultiAddr::default(); diff --git a/implementations/rust/ockam/ockam_api/src/kafka/tests/interceptor_test.rs b/implementations/rust/ockam/ockam_api/src/kafka/tests/interceptor_test.rs index 6df8cf4d8bf..3cd47f1428d 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/tests/interceptor_test.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/tests/interceptor_test.rs @@ -314,21 +314,12 @@ async fn setup_only_worker(context: &mut Context, handle: &NodeManagerHandle) -> Some(authority_identifier.clone()), ); - let producer_policy_access_control = policies.make_policy_access_control( - secure_channels.identities().identities_attributes(), - Resource::new("arbitrary-resource-name", ResourceType::KafkaProducer), - Action::HandleMessage, - Env::new(), - Some(authority_identifier.clone()), - ); - let secure_channel_controller = KafkaKeyExchangeControllerImpl::new( (*handle.node_manager).clone(), - secure_channels, + secure_channels.vault().encryption_at_rest_vault, ConsumerResolution::ViaRelay(MultiAddr::default()), ConsumerPublishing::None, consumer_policy_access_control, - producer_policy_access_control, ); PortalInterceptorWorker::create_inlet_interceptor( @@ -405,23 +396,12 @@ async fn kafka_portal_worker__metadata_exchange__response_changed( ) .await?; - let producer_policy_access_control = handle - .node_manager - .policy_access_control( - Some(project_authority.clone()), - Resource::new("arbitrary-resource-name", ResourceType::KafkaProducer), - Action::HandleMessage, - None, - ) - .await?; - let secure_channel_controller = KafkaKeyExchangeControllerImpl::new( (*handle.node_manager).clone(), - handle.secure_channels.clone(), + handle.secure_channels.vault().encryption_at_rest_vault, ConsumerResolution::ViaRelay(MultiAddr::default()), ConsumerPublishing::None, consumer_policy_access_control, - producer_policy_access_control, ); let inlet_map = KafkaInletController::new( diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs index 0d31915cf2f..8b18ae20199 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs @@ -305,8 +305,10 @@ impl ConnectionBuilder { if changes.flow_control_id.is_some() { self.flow_control_id = changes.flow_control_id; } + start = 0; + } else { + start += 1; } - start += 1; } } diff --git a/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs b/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs index 37e7623616b..f8d50e6cefd 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs @@ -8,29 +8,30 @@ use ockam_multiaddr::MultiAddr; use ockam_transport_core::HostnamePort; use serde::Serialize; use std::fmt::Display; +use std::time::Duration; #[derive(Debug, Clone, Encode, Decode, CborLen)] #[rustfmt::skip] #[cbor(map)] pub struct StartServiceRequest { - #[n(1)] addr: String, - #[n(2)] req: T, + #[n(1)] pub address: String, + #[n(2)] pub request: T, } impl StartServiceRequest { - pub fn new>(req: T, addr: S) -> Self { + pub fn new>(request: T, address: S) -> Self { Self { - addr: addr.into(), - req, + address: address.into(), + request, } } pub fn address(&self) -> &str { - &self.addr + &self.address } pub fn request(&self) -> &T { - &self.req + &self.request } } @@ -51,6 +52,17 @@ impl DeleteServiceRequest { } } +#[derive(Debug, Clone, Encode, Decode, CborLen)] +#[rustfmt::skip] +#[cbor(map)] +pub struct StartKafkaCustodianRequest { + #[n(0)] pub vault: Option, + #[n(1)] pub producer_policy_expression: Option, + #[n(2)] pub key_rotation: Duration, + #[n(3)] pub key_validity: Duration, + #[n(4)] pub rekey_period: Duration, +} + #[derive(Debug, Clone, Encode, Decode, CborLen)] #[rustfmt::skip] #[cbor(map)] @@ -90,16 +102,17 @@ impl StartKafkaOutletRequest { #[rustfmt::skip] #[cbor(map)] pub struct StartKafkaInletRequest { - #[n(1)] bind_address: HostnamePort, - #[n(2)] brokers_port_range: (u16, u16), - #[n(3)] kafka_outlet_route: MultiAddr, - #[n(4)] encrypt_content: bool, - #[n(5)] consumer_resolution: ConsumerResolution, - #[n(6)] consumer_publishing: ConsumerPublishing, - #[n(7)] inlet_policy_expression: Option, - #[n(8)] consumer_policy_expression: Option, - #[n(9)] producer_policy_expression: Option, - #[n(10)] encrypted_fields: Vec, + #[n(1)] pub bind_address: HostnamePort, + #[n(2)] pub brokers_port_range: (u16, u16), + #[n(3)] pub outlet_node_multiaddr: MultiAddr, + #[n(4)] pub encrypt_content: bool, + #[n(5)] pub consumer_resolution: ConsumerResolution, + #[n(6)] pub consumer_publishing: ConsumerPublishing, + #[n(7)] pub inlet_policy_expression: Option, + #[n(8)] pub consumer_policy_expression: Option, + #[n(9)] pub producer_policy_expression: Option, + #[n(10)] pub encrypted_fields: Vec, + #[n(11)] pub vault: Option, } impl StartKafkaInletRequest { @@ -115,11 +128,12 @@ impl StartKafkaInletRequest { inlet_policy_expression: Option, consumer_policy_expression: Option, producer_policy_expression: Option, + vault: Option, ) -> Self { Self { bind_address, brokers_port_range: brokers_port_range.into(), - kafka_outlet_route, + outlet_node_multiaddr: kafka_outlet_route, encrypt_content, consumer_resolution, consumer_publishing, @@ -127,46 +141,9 @@ impl StartKafkaInletRequest { consumer_policy_expression, producer_policy_expression, encrypted_fields, + vault, } } - - pub fn bind_address(&self) -> HostnamePort { - self.bind_address.clone() - } - pub fn brokers_port_range(&self) -> (u16, u16) { - self.brokers_port_range - } - pub fn project_route(&self) -> MultiAddr { - self.kafka_outlet_route.clone() - } - - pub fn encrypt_content(&self) -> bool { - self.encrypt_content - } - - pub fn encrypted_fields(&self) -> Vec { - self.encrypted_fields.clone() - } - - pub fn consumer_resolution(&self) -> ConsumerResolution { - self.consumer_resolution.clone() - } - - pub fn consumer_publishing(&self) -> ConsumerPublishing { - self.consumer_publishing.clone() - } - - pub fn inlet_policy_expression(&self) -> Option { - self.inlet_policy_expression.clone() - } - - pub fn consumer_policy_expression(&self) -> Option { - self.consumer_policy_expression.clone() - } - - pub fn producer_policy_expression(&self) -> Option { - self.producer_policy_expression.clone() - } } /// Request body when instructing a node to start an Uppercase service diff --git a/implementations/rust/ockam/ockam_api/src/nodes/registry.rs b/implementations/rust/ockam/ockam_api/src/nodes/registry.rs index 221d4300261..b6bfbcfd6d0 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/registry.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/registry.rs @@ -99,6 +99,7 @@ pub(crate) struct RemoteProxyVaultInfo {} pub enum KafkaServiceKind { Inlet, Outlet, + Custodian, } impl Display for KafkaServiceKind { @@ -106,6 +107,7 @@ impl Display for KafkaServiceKind { match self { KafkaServiceKind::Inlet => write!(f, "inlet"), KafkaServiceKind::Outlet => write!(f, "outlet"), + KafkaServiceKind::Custodian => write!(f, "custodian"), } } } diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs index 93e49af7fea..324bbbcd53e 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs @@ -22,6 +22,7 @@ impl DefaultAddress { pub const OKTA_IDENTITY_PROVIDER: &'static str = "okta"; pub const KAFKA_OUTLET: &'static str = "kafka_outlet"; pub const KAFKA_INLET: &'static str = "kafka_inlet"; + pub const KAFKA_CUSTODIAN: &'static str = "kafka_custodian"; pub const LEASE_MANAGER: &'static str = "lease_manager"; pub fn get_rendezvous_server_address() -> Address { diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs index 05ed74d73d0..837227b17c2 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs @@ -1,16 +1,18 @@ use super::NodeManagerWorker; use crate::error::ApiError; use crate::kafka::key_exchange::controller::KafkaKeyExchangeControllerImpl; +use crate::kafka::key_exchange::listener::KafkaKeyExchangeListener; use crate::kafka::protocol_aware::inlet::KafkaInletInterceptorFactory; use crate::kafka::protocol_aware::outlet::KafkaOutletInterceptorFactory; use crate::kafka::KafkaOutletController; use crate::kafka::{ - kafka_policy_expression, ConsumerPublishing, ConsumerResolution, KafkaInletController, - KAFKA_OUTLET_BOOTSTRAP_ADDRESS, KAFKA_OUTLET_INTERCEPTOR_ADDRESS, + kafka_policy_expression, KafkaInletController, KAFKA_OUTLET_BOOTSTRAP_ADDRESS, + KAFKA_OUTLET_INTERCEPTOR_ADDRESS, }; use crate::nodes::models::portal::OutletAccessControl; use crate::nodes::models::services::{ - DeleteServiceRequest, StartKafkaInletRequest, StartKafkaOutletRequest, StartServiceRequest, + DeleteServiceRequest, StartKafkaCustodianRequest, StartKafkaInletRequest, + StartKafkaOutletRequest, StartServiceRequest, }; use crate::nodes::registry::{KafkaServiceInfo, KafkaServiceKind}; use crate::nodes::service::default_address::DefaultAddress; @@ -25,7 +27,6 @@ use ockam_core::compat::rand::random_string; use ockam_core::flow_control::FlowControls; use ockam_core::route; use ockam_multiaddr::proto::Project; -use ockam_multiaddr::MultiAddr; use ockam_transport_tcp::{PortalInletInterceptor, PortalOutletInterceptor}; use std::sync::Arc; @@ -35,23 +36,10 @@ impl NodeManagerWorker { context: &Context, body: StartServiceRequest, ) -> Result, Response> { - let request = body.request(); + let request = body.request; match self .node_manager - .start_kafka_inlet_service( - context, - Address::from_string(body.address()), - request.bind_address(), - request.brokers_port_range(), - request.project_route(), - request.encrypt_content(), - request.encrypted_fields(), - request.consumer_resolution(), - request.consumer_publishing(), - request.inlet_policy_expression(), - request.consumer_policy_expression(), - request.producer_policy_expression(), - ) + .start_kafka_inlet_service(context, Address::from_string(body.address), request) .await { Ok(_) => Ok(Response::ok().body(())), @@ -81,6 +69,23 @@ impl NodeManagerWorker { } } + pub(crate) async fn start_custodian_service( + &self, + context: &Context, + body: StartServiceRequest, + ) -> Result, Response> { + let service_address = Address::from_string(body.address()); + let request = body.request; + match self + .node_manager + .start_custodian_service(context, service_address, request) + .await + { + Ok(_) => Ok(Response::ok().body(())), + Err(e) => Err(Response::internal_error_no_request(&e.to_string())), + } + } + pub(crate) async fn delete_kafka_service( &self, ctx: &Context, @@ -109,21 +114,11 @@ impl NodeManagerWorker { } impl InMemoryNode { - #[allow(clippy::too_many_arguments)] pub async fn start_kafka_inlet_service( &self, context: &Context, interceptor_address: Address, - bind_address: HostnamePort, - brokers_port_range: (u16, u16), - outlet_node_multiaddr: MultiAddr, - encrypt_content: bool, - encrypted_fields: Vec, - consumer_resolution: ConsumerResolution, - consumer_publishing: ConsumerPublishing, - inlet_policy_expression: Option, - consumer_policy_expression: Option, - producer_policy_expression: Option, + request: StartKafkaInletRequest, ) -> Result<()> { let consumer_policy_access_control = self .policy_access_control( @@ -133,7 +128,7 @@ impl InMemoryNode { ResourceType::KafkaConsumer, ), Action::HandleMessage, - consumer_policy_expression, + request.consumer_policy_expression, ) .await?; @@ -145,49 +140,76 @@ impl InMemoryNode { ResourceType::KafkaProducer, ), Action::HandleMessage, - producer_policy_expression, + request.producer_policy_expression, ) .await?; - let secure_channel_controller = KafkaKeyExchangeControllerImpl::new( + let vault = if let Some(vault) = request.vault { + let named_vault = self.cli_state.get_named_vault(&vault).await?; + self.cli_state + .make_vault(Some(context), named_vault) + .await? + } else { + self.node_manager.secure_channels.vault().clone() + }; + + let key_exchange_controller = KafkaKeyExchangeControllerImpl::new( self.node_manager.clone(), - self.secure_channels.clone(), - consumer_resolution, - consumer_publishing, + vault.encryption_at_rest_vault.clone(), + request.consumer_resolution, + request.consumer_publishing, consumer_policy_access_control, - producer_policy_access_control, ); - self.node_manager - .start_key_exchanger_service(context, DefaultAddress::KEY_EXCHANGER_LISTENER.into()) - .await?; - - let inlet_policy_expression = if let Some(inlet_policy_expression) = inlet_policy_expression - { - Some(inlet_policy_expression) - } else if let Some(project) = outlet_node_multiaddr - .first() - .and_then(|v| v.cast::().map(|p| p.to_string())) - { - let (_, project_identifier) = self.resolve_project(&project).await?; - Some(PolicyExpression::FullExpression(kafka_policy_expression( - &project_identifier, - ))) - } else { - None - }; + let inlet_policy_expression = + if let Some(inlet_policy_expression) = request.inlet_policy_expression { + Some(inlet_policy_expression) + } else if let Some(project) = request + .outlet_node_multiaddr + .first() + .and_then(|v| v.cast::().map(|p| p.to_string())) + { + let (_, project_identifier) = self.resolve_project(&project).await?; + Some(PolicyExpression::FullExpression(kafka_policy_expression( + &project_identifier, + ))) + } else { + None + }; let inlet_controller = KafkaInletController::new( self.node_manager.clone(), - outlet_node_multiaddr.clone(), + request.outlet_node_multiaddr.clone(), route![interceptor_address.clone()], route![KAFKA_OUTLET_INTERCEPTOR_ADDRESS], - bind_address.hostname(), - PortRange::try_from(brokers_port_range) + request.bind_address.hostname(), + PortRange::try_from(request.brokers_port_range) .map_err(|_| ApiError::core("invalid port range"))?, inlet_policy_expression.clone(), ); + let default_secure_channel_listener_flow_control_id = context + .flow_controls() + .get_flow_control_with_spawner(&DefaultAddress::SECURE_CHANNEL_LISTENER.into()) + .ok_or_else(|| { + ApiError::core("Unable to get flow control for secure channel listener") + })?; + + // TODO: remove key exchange from inlets + KafkaKeyExchangeListener::create( + context, + vault.encryption_at_rest_vault, + std::time::Duration::from_secs(60 * 60 * 24), + std::time::Duration::from_secs(60 * 60 * 30), + std::time::Duration::from_secs(60 * 60), + &default_secure_channel_listener_flow_control_id, + producer_policy_access_control.create_incoming(), + producer_policy_access_control + .create_outgoing(context) + .await?, + ) + .await?; + // tldr: the alias for the inlet must be unique, and we want to keep it readable. // This function will create an inlet for either a producer or a consumer. // Since the policy is hardcoded (see the expression above) and it's the same @@ -201,13 +223,13 @@ impl InMemoryNode { // create the kafka bootstrap inlet self.create_inlet( context, - bind_address, + request.bind_address, route![interceptor_address.clone()], route![ KAFKA_OUTLET_INTERCEPTOR_ADDRESS, KAFKA_OUTLET_BOOTSTRAP_ADDRESS ], - outlet_node_multiaddr, + request.outlet_node_multiaddr, inlet_alias, inlet_policy_expression.clone(), None, @@ -234,10 +256,10 @@ impl InMemoryNode { context, interceptor_address.clone(), Arc::new(KafkaInletInterceptorFactory::new( - secure_channel_controller, + key_exchange_controller, inlet_controller, - encrypt_content, - encrypted_fields, + request.encrypt_content, + request.encrypted_fields, )), Arc::new(policy_access_control.create_incoming()), Arc::new(policy_access_control.create_outgoing(context).await?), @@ -336,6 +358,67 @@ impl InMemoryNode { Ok(()) } + pub async fn start_custodian_service( + &self, + context: &Context, + service_address: Address, + request: StartKafkaCustodianRequest, + ) -> Result<()> { + //TODO: dedup code with start_kafka_inlet_service + + let default_secure_channel_listener_flow_control_id = context + .flow_controls() + .get_flow_control_with_spawner(&DefaultAddress::SECURE_CHANNEL_LISTENER.into()) + .ok_or_else(|| { + ApiError::core("Unable to get flow control for secure channel listener") + })?; + + let vault = if let Some(vault) = request.vault { + let named_vault = self.cli_state.get_named_vault(&vault).await?; + self.cli_state + .make_vault(Some(context), named_vault) + .await? + } else { + self.node_manager.secure_channels.vault().clone() + }; + + let producer_policy_access_control = self + .policy_access_control( + self.project_authority().clone(), + Resource::new( + format!("kafka-producer-{}", service_address.address()), + ResourceType::KafkaProducer, + ), + Action::HandleMessage, + request.producer_policy_expression, + ) + .await?; + + KafkaKeyExchangeListener::create( + context, + vault.encryption_at_rest_vault, + request.key_rotation, + request.key_validity, + request.rekey_period, + &default_secure_channel_listener_flow_control_id, + producer_policy_access_control.create_incoming(), + producer_policy_access_control + .create_outgoing(context) + .await?, + ) + .await?; + + self.registry + .kafka_services + .insert( + DefaultAddress::KAFKA_CUSTODIAN.into(), + KafkaServiceInfo::new(KafkaServiceKind::Custodian), + ) + .await; + + Ok(()) + } + /// Delete a Kafka service from the registry. /// The expected kind must match the actual kind pub async fn delete_kafka_service( @@ -357,6 +440,9 @@ impl InMemoryNode { ctx.stop_worker(KAFKA_OUTLET_INTERCEPTOR_ADDRESS).await?; ctx.stop_worker(KAFKA_OUTLET_BOOTSTRAP_ADDRESS).await?; } + KafkaServiceKind::Custodian => { + ctx.stop_worker(DefaultAddress::KAFKA_CUSTODIAN).await?; + } } self.registry.kafka_services.remove(&address).await; Ok(DeleteKafkaServiceResult::ServiceDeleted) diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs index 6ce0ec0e653..8c280dd2149 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs @@ -193,6 +193,7 @@ impl NodeManager { match info.kind() { KafkaServiceKind::Inlet => DefaultAddress::KAFKA_INLET, KafkaServiceKind::Outlet => DefaultAddress::KAFKA_OUTLET, + KafkaServiceKind::Custodian => DefaultAddress::KAFKA_CUSTODIAN, }, )) }); diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs index e2dc954c1f4..5826178aa9e 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs @@ -303,6 +303,8 @@ impl NodeManager { /// SECURE CHANNEL LISTENERS impl NodeManager { + //TODO: remove everything about key exchange service from secure channel + #[allow(dead_code)] pub(crate) async fn start_key_exchanger_service( &self, context: &Context, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs index 70bac0c7fe6..963f2758e44 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs @@ -131,6 +131,9 @@ impl NodeManagerWorker { req, self.start_kafka_outlet_service(ctx, dec.decode()?).await, )?, + (Post, ["node", "services", DefaultAddress::KAFKA_CUSTODIAN]) => { + encode_response(req, self.start_custodian_service(ctx, dec.decode()?).await)? + } (Delete, ["node", "services", DefaultAddress::KAFKA_OUTLET]) => encode_response( req, self.delete_kafka_service(ctx, dec.decode()?, KafkaServiceKind::Outlet) diff --git a/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs b/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs index 4b1aebfeeba..0d4ef12da09 100644 --- a/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs +++ b/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs @@ -121,6 +121,14 @@ impl Worker for Server { ) .await? } + + "encryption_at_rest_vault" => { + vault_for_encryption_at_rest::handle_request( + self.vault.encryption_at_rest_vault.as_ref(), + payload, + ) + .await? + } _ => { warn!("Unknown address: {}, ignoring request", onward_address); return Ok(()); @@ -1168,6 +1176,187 @@ pub mod vault_for_verify_signatures { } } +pub mod vault_for_encryption_at_rest { + use crate::proxy_vault::protocol::{ProxyError, SpecificClient}; + use minicbor::{CborLen, Decode, Encode}; + use ockam_core::{async_trait, cbor_encode_preallocate}; + use ockam_vault::{AeadSecretKeyHandle, VaultForEncryptionAtRest}; + + pub(super) async fn handle_request( + vault: &dyn VaultForEncryptionAtRest, + request: Vec, + ) -> ockam_core::Result> { + let request: Request = minicbor::decode(&request)?; + let response = match request { + Request::AeadEncrypt { + secret_key_handle, + mut plain_text, + aad, + } => { + trace!("aead_encrypt request for {secret_key_handle:?}"); + let result = vault + .aead_encrypt(&secret_key_handle, &mut plain_text, &aad) + .await; + Response::AeadEncrypt(result.map(|_| plain_text).map_err(Into::into)) + } + Request::AeadDecrypt { + secret_key_handle, + rekey_counter, + mut cipher_text, + aad, + } => { + trace!("aead_decrypt request for {secret_key_handle:?}"); + let result = vault + .aead_decrypt(&secret_key_handle, rekey_counter, &mut cipher_text, &aad) + .await; + Response::AeadDecrypt(result.map(|slice| slice.to_vec()).map_err(Into::into)) + } + Request::RekeyAndDelete { secret_key_handle } => { + trace!("rekey_and_delete request for {secret_key_handle:?}"); + let result = vault.rekey_and_delete(&secret_key_handle).await; + Response::RekeyAndDelete(result.map_err(Into::into)) + } + Request::ImportAeadKey { secret } => { + trace!("import_aead_key request"); + let result = vault.import_aead_key(secret).await; + Response::ImportAeadKey(result.map_err(Into::into)) + } + }; + cbor_encode_preallocate(response) + } + + #[derive(Encode, Decode, CborLen)] + #[rustfmt::skip] + enum Request { + #[n(0)] AeadEncrypt { + #[n(0)] secret_key_handle: AeadSecretKeyHandle, + #[n(1)] plain_text: Vec, + #[n(2)] aad: Vec, + }, + #[n(1)] AeadDecrypt { + #[n(0)] secret_key_handle: AeadSecretKeyHandle, + #[n(1)] rekey_counter: u16, + #[n(2)] cipher_text: Vec, + #[n(3)] aad: Vec, + }, + #[n(2)] RekeyAndDelete { + #[n(0)] secret_key_handle: AeadSecretKeyHandle, + }, + #[n(3)] ImportAeadKey { + #[n(0)] secret: Vec, + }, + } + + #[derive(Encode, Decode, CborLen)] + #[rustfmt::skip] + enum Response { + #[n(0)] AeadEncrypt(#[n(0)] Result, ProxyError>), + #[n(1)] AeadDecrypt(#[n(0)] Result, ProxyError>), + #[n(2)] RekeyAndDelete(#[n(0)] Result), + #[n(3)] ImportAeadKey(#[n(0)] Result), + } + + #[async_trait] + impl VaultForEncryptionAtRest for SpecificClient { + async fn aead_encrypt( + &self, + secret_key_handle: &AeadSecretKeyHandle, + plain_text: &mut [u8], + aad: &[u8], + ) -> ockam_core::Result<()> { + trace!("sending aead_encrypt request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::AeadEncrypt { + secret_key_handle: secret_key_handle.clone(), + plain_text: plain_text.to_vec(), + aad: aad.to_vec(), + }) + .await?; + + match response { + Response::AeadEncrypt(result) => { + let result = result?; + if result.len() != plain_text.len() { + return Err(ProxyError::Protocol)?; + } + plain_text.copy_from_slice(result.as_slice()); + Ok(()) + } + _ => Err(ProxyError::Protocol)?, + } + } + + async fn aead_decrypt<'a>( + &self, + secret_key_handle: &AeadSecretKeyHandle, + rekey_counter: u16, + cipher_text: &'a mut [u8], + aad: &[u8], + ) -> ockam_core::Result<&'a mut [u8]> { + trace!("sending aead_decrypt request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::AeadDecrypt { + secret_key_handle: secret_key_handle.clone(), + rekey_counter, + cipher_text: cipher_text.to_vec(), + aad: aad.to_vec(), + }) + .await?; + + let result = match response { + Response::AeadDecrypt(result) => { + let result = result?; + if cipher_text.len() < result.len() { + return Err(ProxyError::Protocol)?; + } + let clear_text = cipher_text[..result.len()].as_mut(); + clear_text.copy_from_slice(result.as_slice()); + clear_text + } + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn rekey_and_delete( + &self, + secret_key_handle: &AeadSecretKeyHandle, + ) -> ockam_core::Result { + trace!("sending rekey_and_delete request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::RekeyAndDelete { + secret_key_handle: secret_key_handle.clone(), + }) + .await?; + + let result = match response { + Response::RekeyAndDelete(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + + async fn import_aead_key( + &self, + secret: Vec, + ) -> ockam_core::Result { + trace!("sending import_aead_key request"); + let response: Response = self + .send_and_receive(Request::ImportAeadKey { secret }) + .await?; + + let result = match response { + Response::ImportAeadKey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + } +} + pub async fn create_vault( context: Context, route: MultiAddr, @@ -1184,6 +1373,7 @@ pub async fn create_vault( client.create_for_destination("secure_channel_vault"), client.create_for_destination("credential_vault"), client.create_for_destination("verifying_vault"), + client.create_for_destination("encryption_at_rest_vault"), ) } diff --git a/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs index 30e0fd9e71d..553aac37c7f 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs @@ -53,6 +53,7 @@ impl CreateCommand { inlet_policy_expression: None, consumer_policy_expression: None, producer_policy_expression: None, + vault: None, } .run(opts) } diff --git a/implementations/rust/ockam/ockam_command/src/kafka/custodian/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/custodian/create.rs new file mode 100644 index 00000000000..a41989be9d3 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/kafka/custodian/create.rs @@ -0,0 +1,133 @@ +use crate::node::util::initialize_default_node; +use crate::util::parsers::duration_parser; +use crate::{kafka::kafka_custodian_default_addr, node::NodeOpts, Command, CommandGlobalOpts}; +use async_trait::async_trait; +use clap::{command, Args}; +use colorful::Colorful; +use miette::miette; +use ockam_abac::PolicyExpression; +use ockam_api::colors::color_primary; +use ockam_api::nodes::models::services::{StartKafkaCustodianRequest, StartServiceRequest}; +use ockam_api::nodes::BackgroundNodeClient; +use ockam_api::output::Output; +use ockam_api::{fmt_ok, DefaultAddress}; +use ockam_core::api::Request; +use ockam_node::Context; +use serde::Serialize; +use std::fmt::Write; +use std::time::Duration; + +/// Create a new Kafka Custodian. +#[derive(Clone, Debug, Args)] +pub struct CreateCommand { + #[command(flatten)] + pub node_opts: NodeOpts, + + /// The local address of the service + #[arg(long, default_value_t = kafka_custodian_default_addr())] + pub addr: String, + + /// Policy expression that will be used for access control to the Kafka Producer. + /// If you don't provide it, the policy set for the "kafka-producer" resource type will be used. + /// + /// You can check the fallback policy with `ockam policy show --resource-type kafka-producer`. + #[arg(hide = true, long = "allow-producer", id = "PRODUCER-EXPRESSION")] + pub producer_policy_expression: Option, + + #[arg(long, default_value = "24h", value_name = "DURATION", value_parser = duration_parser)] + pub key_rotation: Duration, + + #[arg(long, default_value = "30h", value_name = "DURATION", value_parser = duration_parser)] + pub key_validity: Duration, + + #[arg(long, default_value = "1h", value_name = "DURATION", value_parser = duration_parser)] + pub rekey_period: Duration, + + /// The name of the Vault used to decrypt the encrypted data. + /// When not provided, the default Vault will be used. + #[arg(long, value_name = "VAULT_NAME")] + pub vault: Option, +} + +#[async_trait] +impl Command for CreateCommand { + const NAME: &'static str = "kafka-custodian create"; + + async fn async_run(self, ctx: &Context, opts: CommandGlobalOpts) -> crate::Result<()> { + if self.key_validity < self.key_rotation { + return Err(miette!("Key validity must be greater than key rotation")); + } + + if self.rekey_period.is_zero() { + return Err(miette!("Rekey period must be greater than zero")); + } + + if self.rekey_period >= self.key_rotation { + return Err(miette!("Rekey period must be less than key rotation")); + } + + initialize_default_node(ctx, &opts).await?; + + let at_node = self.node_opts.at_node.clone(); + let addr = self.addr.clone(); + + let output = { + let pb = opts.terminal.spinner(); + if let Some(pb) = pb.as_ref() { + pb.set_message("Creating Kafka Custodian...\n".to_string()); + } + + let node = BackgroundNodeClient::create(ctx, &opts.state, &at_node).await?; + + let payload = StartKafkaCustodianRequest { + vault: self.vault.clone(), + producer_policy_expression: self.producer_policy_expression, + key_rotation: self.key_rotation, + key_validity: self.key_validity, + rekey_period: self.rekey_period, + }; + let payload = StartServiceRequest::new(payload, &addr); + let req = Request::post(format!( + "/node/services/{}", + DefaultAddress::KAFKA_CUSTODIAN + )) + .body(payload); + node.tell(ctx, req) + .await + .map_err(|e| miette!("Failed to start Kafka Custodian: {e}"))?; + + KafkaCustodianOutput { + node_name: node.node_name(), + } + }; + + opts.terminal + .stdout() + .plain(output.item()?) + .json_obj(output)? + .write_line()?; + + Ok(()) + } +} + +#[derive(Serialize)] +struct KafkaCustodianOutput { + node_name: String, +} + +impl Output for KafkaCustodianOutput { + fn item(&self) -> ockam_api::Result { + let mut f = String::new(); + writeln!( + f, + "{}\n", + fmt_ok!( + "Created a new Kafka Custodian in the Node {}", + color_primary(&self.node_name), + ) + )?; + + Ok(f) + } +} diff --git a/implementations/rust/ockam/ockam_command/src/kafka/custodian/delete.rs b/implementations/rust/ockam/ockam_command/src/kafka/custodian/delete.rs new file mode 100644 index 00000000000..074dc2c1b4f --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/kafka/custodian/delete.rs @@ -0,0 +1,131 @@ +use async_trait::async_trait; +use clap::Args; +use colorful::Colorful; +use console::Term; +use ockam_api::colors::color_primary; +use ockam_api::{fmt_ok, DefaultAddress}; + +use ockam_api::nodes::models::services::{DeleteServiceRequest, ServiceStatus}; +use ockam_api::nodes::BackgroundNodeClient; +use ockam_api::terminal::{Terminal, TerminalStream}; +use ockam_core::api::Request; +use ockam_core::AsyncTryClone; +use ockam_node::Context; + +use crate::tui::{DeleteCommandTui, PluralTerm}; +use crate::{docs, node::NodeOpts, Command, CommandGlobalOpts}; + +const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); + +/// Delete a Kafka Custodian +#[derive(Clone, Debug, Args)] +#[command(after_long_help = docs::after_help(AFTER_LONG_HELP))] +pub struct DeleteCommand { + #[command(flatten)] + pub node_opts: NodeOpts, + + /// Kafka Custodian service address + pub address: Option, + + /// Confirm the deletion without prompting + #[arg(display_order = 901, long, short)] + pub(crate) yes: bool, + + /// Delete all the Kafka Custodians + #[arg(long)] + pub(crate) all: bool, +} + +#[async_trait] +impl Command for DeleteCommand { + const NAME: &'static str = "kafka-custodian delete"; + + async fn async_run(self, ctx: &Context, opts: CommandGlobalOpts) -> crate::Result<()> { + Ok(DeleteTui::run(ctx, opts, self).await?) + } +} + +#[derive(AsyncTryClone)] +struct DeleteTui { + ctx: Context, + opts: CommandGlobalOpts, + node: BackgroundNodeClient, + cmd: DeleteCommand, +} + +impl DeleteTui { + pub async fn run( + ctx: &Context, + opts: CommandGlobalOpts, + cmd: DeleteCommand, + ) -> miette::Result<()> { + let node = BackgroundNodeClient::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; + let tui = Self { + ctx: ctx.async_try_clone().await?, + opts, + node, + cmd, + }; + tui.delete().await + } +} + +#[async_trait] +impl DeleteCommandTui for DeleteTui { + const ITEM_NAME: PluralTerm = PluralTerm::KafkaCustodian; + + fn cmd_arg_item_name(&self) -> Option { + self.cmd.address.clone() + } + + fn cmd_arg_delete_all(&self) -> bool { + self.cmd.all + } + + fn cmd_arg_confirm_deletion(&self) -> bool { + self.cmd.yes + } + + fn terminal(&self) -> Terminal> { + self.opts.terminal.clone() + } + + async fn list_items_names(&self) -> miette::Result> { + let instances: Vec = self + .node + .ask( + &self.ctx, + Request::get(format!( + "/node/services/{}", + DefaultAddress::KAFKA_CUSTODIAN + )), + ) + .await?; + let addresses = instances.into_iter().map(|i| i.addr).collect(); + Ok(addresses) + } + + async fn delete_single(&self, item_name: &str) -> miette::Result<()> { + self.node + .tell( + &self.ctx, + Request::delete(format!( + "/node/services/{}", + DefaultAddress::KAFKA_CUSTODIAN + )) + .body(DeleteServiceRequest::new(item_name)), + ) + .await?; + let node_name = self.node.node_name(); + self.terminal() + .stdout() + .plain(fmt_ok!( + "Kafka Custodian with address {} on Node {} has been deleted", + color_primary(item_name), + color_primary(&node_name) + )) + .json(serde_json::json!({ "address": item_name, "node": node_name })) + .write_line()?; + Ok(()) + } +} diff --git a/implementations/rust/ockam/ockam_command/src/kafka/custodian/list.rs b/implementations/rust/ockam/ockam_command/src/kafka/custodian/list.rs new file mode 100644 index 00000000000..e3e7b2ed52a --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/kafka/custodian/list.rs @@ -0,0 +1,51 @@ +use async_trait::async_trait; +use clap::Args; + +use ockam_api::nodes::models::services::ServiceStatus; +use ockam_api::nodes::service::default_address::DefaultAddress; +use ockam_api::nodes::BackgroundNodeClient; +use ockam_core::api::Request; +use ockam_node::Context; + +use crate::node::NodeOpts; +use crate::{docs, Command, CommandGlobalOpts}; + +const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); + +/// List Kafka Custodians +#[derive(Args, Clone, Debug)] +#[command(after_long_help = docs::after_help(AFTER_LONG_HELP))] +pub struct ListCommand { + #[command(flatten)] + pub node_opts: NodeOpts, +} + +#[async_trait] +impl Command for ListCommand { + const NAME: &'static str = "kafka-custodian list"; + + async fn async_run(self, ctx: &Context, opts: CommandGlobalOpts) -> crate::Result<()> { + let node = BackgroundNodeClient::create(ctx, &opts.state, &self.node_opts.at_node).await?; + let services: Vec = node + .ask( + ctx, + Request::get(format!( + "/node/services/{}", + DefaultAddress::KAFKA_CUSTODIAN + )), + ) + .await?; + + let plain = opts.terminal.build_list( + &services, + &format!("No Kafka Custodians found on {}", node.node_name()), + )?; + opts.terminal + .stdout() + .plain(plain) + .json_obj(&services)? + .write_line()?; + + Ok(()) + } +} diff --git a/implementations/rust/ockam/ockam_command/src/kafka/custodian/mod.rs b/implementations/rust/ockam/ockam_command/src/kafka/custodian/mod.rs new file mode 100644 index 00000000000..235c8e8ac50 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/kafka/custodian/mod.rs @@ -0,0 +1,48 @@ +use clap::{command, Args, Subcommand}; + +use crate::kafka::custodian::create::CreateCommand; +use crate::kafka::custodian::delete::DeleteCommand; +use crate::kafka::custodian::list::ListCommand; +use crate::kafka::custodian::show::ShowCommand; +use crate::{Command, CommandGlobalOpts}; + +pub(crate) mod create; +pub(crate) mod delete; +pub(crate) mod list; +pub(crate) mod show; + +/// Manage Kafka Custodians +#[derive(Clone, Debug, Args)] +#[command(arg_required_else_help = true, subcommand_required = true)] +pub struct KafkaCustodianCommand { + #[command(subcommand)] + pub(crate) subcommand: KafkaCustodianSubcommand, +} + +#[derive(Clone, Debug, Subcommand)] +pub enum KafkaCustodianSubcommand { + Create(CreateCommand), + Show(ShowCommand), + Delete(DeleteCommand), + List(ListCommand), +} + +impl KafkaCustodianCommand { + pub fn run(self, opts: CommandGlobalOpts) -> miette::Result<()> { + match self.subcommand { + KafkaCustodianSubcommand::Create(c) => c.run(opts), + KafkaCustodianSubcommand::Show(c) => c.run(opts), + KafkaCustodianSubcommand::Delete(c) => c.run(opts), + KafkaCustodianSubcommand::List(c) => c.run(opts), + } + } + + pub fn name(&self) -> String { + match &self.subcommand { + KafkaCustodianSubcommand::Create(c) => c.name(), + KafkaCustodianSubcommand::Show(c) => c.name(), + KafkaCustodianSubcommand::Delete(c) => c.name(), + KafkaCustodianSubcommand::List(c) => c.name(), + } + } +} diff --git a/implementations/rust/ockam/ockam_command/src/kafka/custodian/show.rs b/implementations/rust/ockam/ockam_command/src/kafka/custodian/show.rs new file mode 100644 index 00000000000..70ea8562622 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/kafka/custodian/show.rs @@ -0,0 +1,115 @@ +use async_trait::async_trait; +use clap::Args; +use console::Term; +use miette::miette; +use ockam_api::DefaultAddress; + +use ockam_api::nodes::models::services::ServiceStatus; +use ockam_api::nodes::BackgroundNodeClient; +use ockam_api::output::Output; +use ockam_api::terminal::{Terminal, TerminalStream}; +use ockam_core::api::Request; +use ockam_node::Context; + +use crate::tui::{PluralTerm, ShowCommandTui}; +use crate::{node::NodeOpts, Command, CommandGlobalOpts}; + +/// Show a Kafka Custodian +#[derive(Clone, Debug, Args)] +pub struct ShowCommand { + #[command(flatten)] + pub node_opts: NodeOpts, + + /// Kafka Custodian service address + pub address: Option, +} + +#[async_trait] +impl Command for ShowCommand { + const NAME: &'static str = "kafka-custodian show"; + + async fn async_run(self, ctx: &Context, opts: CommandGlobalOpts) -> crate::Result<()> { + Ok(ShowTui::run(ctx, opts, &self).await?) + } +} + +struct ShowTui<'a> { + ctx: &'a Context, + opts: CommandGlobalOpts, + node: BackgroundNodeClient, + cmd: &'a ShowCommand, +} + +impl<'a> ShowTui<'a> { + pub async fn run( + ctx: &'a Context, + opts: CommandGlobalOpts, + cmd: &'a ShowCommand, + ) -> miette::Result<()> { + let node = BackgroundNodeClient::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; + let tui = Self { + ctx, + opts, + node, + cmd, + }; + tui.show().await + } +} + +#[async_trait] +impl<'a> ShowCommandTui for ShowTui<'a> { + const ITEM_NAME: PluralTerm = PluralTerm::KafkaCustodian; + + fn cmd_arg_item_name(&self) -> Option { + self.cmd.address.clone() + } + + fn terminal(&self) -> Terminal> { + self.opts.terminal.clone() + } + + async fn get_arg_item_name_or_default(&self) -> miette::Result { + Ok(self + .cmd_arg_item_name() + .unwrap_or(DefaultAddress::KAFKA_CUSTODIAN.to_string())) + } + + async fn list_items_names(&self) -> miette::Result> { + let instances: Vec = self + .node + .ask( + self.ctx, + Request::get(format!( + "/node/services/{}", + DefaultAddress::KAFKA_CUSTODIAN + )), + ) + .await?; + let addresses = instances.into_iter().map(|i| i.addr).collect(); + Ok(addresses) + } + + async fn show_single(&self, item_name: &str) -> miette::Result<()> { + let instances: Vec = self + .node + .ask( + self.ctx, + Request::get(format!( + "/node/services/{}", + DefaultAddress::KAFKA_CUSTODIAN + )), + ) + .await?; + let status = instances + .into_iter() + .find(|i| i.addr == item_name) + .ok_or_else(|| miette!("Kafka Custodian not found"))?; + self.terminal() + .stdout() + .plain(status.item()?) + .json_obj(&status)? + .write_line()?; + Ok(()) + } +} diff --git a/implementations/rust/ockam/ockam_command/src/kafka/custodian/static/delete/after_long_help.txt b/implementations/rust/ockam/ockam_command/src/kafka/custodian/static/delete/after_long_help.txt new file mode 100644 index 00000000000..d52d60c968f --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/kafka/custodian/static/delete/after_long_help.txt @@ -0,0 +1,7 @@ +```sh +# To delete a kafka custodian on the default node +$ ockam kafka-custodian delete kcaddr + +# To delete a kafka custodian on a specific node +$ ockam kafka-custodian delete kcaddr --at n +``` diff --git a/implementations/rust/ockam/ockam_command/src/kafka/custodian/static/list/after_long_help.txt b/implementations/rust/ockam/ockam_command/src/kafka/custodian/static/list/after_long_help.txt new file mode 100644 index 00000000000..16f504c6f13 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/kafka/custodian/static/list/after_long_help.txt @@ -0,0 +1,7 @@ +```sh +# To list the kafka custodian on the default node +$ ockam kafka-custodian list + +# To list the kafka custodian on a specific node +$ ockam kafka-custodian list --at n +``` diff --git a/implementations/rust/ockam/ockam_command/src/kafka/inlet/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/inlet/create.rs index aafd142fbaf..64294dcb094 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/inlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/inlet/create.rs @@ -122,6 +122,11 @@ pub struct CreateCommand { /// You can check the fallback policy with `ockam policy show --resource-type kafka-producer`. #[arg(hide = true, long = "allow-producer", id = "PRODUCER-EXPRESSION")] pub producer_policy_expression: Option, + + /// The name of the Vault used to decrypt the encrypted data. + /// When not provided, the default Vault will be used. + #[arg(long, value_name = "VAULT_NAME")] + pub vault: Option, } #[async_trait] @@ -192,6 +197,7 @@ impl Command for CreateCommand { self.inlet_policy_expression, self.consumer_policy_expression, self.producer_policy_expression, + self.vault, ); let payload = StartServiceRequest::new(payload, &addr); let req = Request::post("/node/services/kafka_inlet").body(payload); diff --git a/implementations/rust/ockam/ockam_command/src/kafka/mod.rs b/implementations/rust/ockam/ockam_command/src/kafka/mod.rs index 0a006e89e6e..f944ac9dc05 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/mod.rs @@ -6,6 +6,7 @@ use std::cmp::min; use std::str::FromStr; pub(crate) mod consumer; +pub(crate) mod custodian; pub(crate) mod inlet; pub(crate) mod outlet; pub(crate) mod producer; @@ -29,6 +30,10 @@ fn kafka_inlet_default_addr() -> String { DefaultAddress::KAFKA_INLET.to_string() } +fn kafka_custodian_default_addr() -> String { + DefaultAddress::KAFKA_CUSTODIAN.to_string() +} + fn kafka_default_project_route() -> MultiAddr { MultiAddr::from_str(KAFKA_DEFAULT_PROJECT_ROUTE).expect("Failed to parse default project route") } diff --git a/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs index 1041705dcb0..daf900cea19 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs @@ -52,6 +52,7 @@ impl CreateCommand { inlet_policy_expression: None, consumer_policy_expression: None, producer_policy_expression: None, + vault: None, } .run(opts) } diff --git a/implementations/rust/ockam/ockam_command/src/subcommand.rs b/implementations/rust/ockam/ockam_command/src/subcommand.rs index 5e3e7380fb8..abe49afa4f4 100644 --- a/implementations/rust/ockam/ockam_command/src/subcommand.rs +++ b/implementations/rust/ockam/ockam_command/src/subcommand.rs @@ -27,6 +27,7 @@ use crate::identity::IdentityCommand; use crate::influxdb::inlet::InfluxDBInletCommand; use crate::influxdb::outlet::InfluxDBOutletCommand; use crate::kafka::consumer::KafkaConsumerCommand; +use crate::kafka::custodian::KafkaCustodianCommand; use crate::kafka::inlet::KafkaInletCommand; use crate::kafka::outlet::KafkaOutletCommand; use crate::kafka::producer::KafkaProducerCommand; @@ -104,6 +105,7 @@ pub enum OckamSubcommand { KafkaConsumer(KafkaConsumerCommand), KafkaProducer(KafkaProducerCommand), + KafkaCustodian(KafkaCustodianCommand), SecureChannelListener(SecureChannelListenerCommand), SecureChannel(SecureChannelCommand), @@ -149,7 +151,6 @@ impl OckamSubcommand { OckamSubcommand::Message(c) => c.run(opts), OckamSubcommand::Relay(c) => c.run(opts), - OckamSubcommand::KafkaOutlet(c) => c.run(opts), OckamSubcommand::TcpListener(c) => c.run(opts), OckamSubcommand::TcpConnection(c) => c.run(opts), OckamSubcommand::TcpOutlet(c) => c.run(opts), @@ -161,6 +162,8 @@ impl OckamSubcommand { OckamSubcommand::Rendezvous(c) => c.run(opts), OckamSubcommand::KafkaInlet(c) => c.run(opts), + OckamSubcommand::KafkaOutlet(c) => c.run(opts), + OckamSubcommand::KafkaCustodian(c) => c.run(opts), OckamSubcommand::KafkaConsumer(c) => c.run(opts), OckamSubcommand::KafkaProducer(c) => c.run(opts), @@ -327,6 +330,7 @@ impl OckamSubcommand { OckamSubcommand::Manpages(c) => c.name(), OckamSubcommand::Environment(c) => c.name(), OckamSubcommand::FlowControl(c) => c.name(), + OckamSubcommand::KafkaCustodian(c) => c.name(), } } } diff --git a/implementations/rust/ockam/ockam_command/src/terminal/tui.rs b/implementations/rust/ockam/ockam_command/src/terminal/tui.rs index 52af53cfd7b..b2293bef584 100644 --- a/implementations/rust/ockam/ockam_command/src/terminal/tui.rs +++ b/implementations/rust/ockam/ockam_command/src/terminal/tui.rs @@ -294,6 +294,7 @@ pub enum PluralTerm { TcpOutlet, KafkaInlet, KafkaOutlet, + KafkaCustodian, Policy, Member, } @@ -313,6 +314,7 @@ impl PluralTerm { PluralTerm::TcpOutlet => "tcp outlet", PluralTerm::KafkaInlet => "kafka inlet", PluralTerm::KafkaOutlet => "kafka outlet", + PluralTerm::KafkaCustodian => "kafka custodian", PluralTerm::Policy => "policy", PluralTerm::Member => "member", } @@ -332,6 +334,7 @@ impl PluralTerm { PluralTerm::TcpOutlet => "tcp outlets", PluralTerm::KafkaInlet => "kafka inlets", PluralTerm::KafkaOutlet => "kafka outlets", + PluralTerm::KafkaCustodian => "kafka custodians", PluralTerm::Policy => "policies", PluralTerm::Member => "members", } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/kafka/docker.bats b/implementations/rust/ockam/ockam_command/tests/bats/kafka/docker.bats index 1906d811868..43eb44d7dac 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/kafka/docker.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/kafka/docker.bats @@ -30,7 +30,7 @@ kafka_docker_end_to_end_encrypted_explicit_consumer() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay kafka_consumer) producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=true + export OCKAM_LOGGING=0 export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -98,7 +98,7 @@ kafka_docker_end_to_end_encrypted_project_relay_consumer() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay '*') producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=true + export OCKAM_LOGGING=0 export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -165,7 +165,7 @@ kafka_docker_end_to_end_encrypted_rust_relay_consumer() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay '*') producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=true + export OCKAM_LOGGING=0 export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -231,7 +231,7 @@ kafka_docker_end_to_end_encrypted_direct_connection() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay kafka_consumer) producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=true + export OCKAM_LOGGING=0 export OCKAM_LOG_LEVEL=trace export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -296,15 +296,15 @@ kafka_docker_end_to_end_encrypted_multiple_consumers_direct_connection() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay kafka_consumer) producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=true - export OCKAM_LOG_LEVEL=debug - export RUST_BACKTRACE=full + export OCKAM_LOGGING=0 + export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" export DIRECT_CONSUMER_OUTPUT="$ADMIN_HOME/direct_consumer.log" # Vault Node setup_home_dir + run_success "$OCKAM" project enroll "${vault_ticket}" run_success "$OCKAM" node create vault --tcp-listener-address 127.0.0.1:${inlet_port} run_success "$OCKAM" service start remote-proxy-vault --vault-name default @@ -313,18 +313,15 @@ kafka_docker_end_to_end_encrypted_multiple_consumers_direct_connection() { # create a remote identity, but we need an enrolled local identity first run_success "$OCKAM" identity create local-identity run_success "$OCKAM" project enroll --identity local-identity "${consumer_ticket}" - run_success "$OCKAM" message send --to /ip4/127.0.0.1/tcp/${inlet_port}/secure/api/service/echo hello - assert_output "hello" run_success "$OCKAM" vault create \ --route /ip4/127.0.0.1/tcp/${inlet_port}/secure/api/service/remote_proxy_vault \ --identity local-identity remote-vault - run_success "$OCKAM" identity create remote-identity --vault remote-vault - run_success "$OCKAM" project enroll --identity remote-identity "${consumer_ticket}" - run_success "$OCKAM" node create consumer --identity remote-identity + run_success "$OCKAM" node create consumer run_success "$OCKAM" kafka-outlet create --bootstrap-server 127.0.0.1:19092 run_success "$OCKAM" kafka-inlet create --from 29092 \ --avoid-publishing \ + --vault remote-vault \ --to self run_success "$OCKAM" relay create kafka_consumer @@ -333,20 +330,16 @@ kafka_docker_end_to_end_encrypted_multiple_consumers_direct_connection() { # create a remote identity, but we need an enrolled local identity first run_success "$OCKAM" identity create local-identity run_success "$OCKAM" project enroll --identity local-identity "${consumer_ticket}" - run_success "$OCKAM" message send --to /ip4/127.0.0.1/tcp/${inlet_port}/secure/api/service/echo hello - assert_output "hello" run_success "$OCKAM" vault create \ --route /ip4/127.0.0.1/tcp/${inlet_port}/secure/api/service/remote_proxy_vault \ --identity local-identity remote-vault - run_success "$OCKAM" identity create remote-identity --vault remote-vault - run_success "$OCKAM" project enroll --identity remote-identity "${consumer_ticket}" - run_success "$OCKAM" node create consumer --identity remote-identity + run_success "$OCKAM" node create consumer run_success "$OCKAM" kafka-outlet create --bootstrap-server 127.0.0.1:19092 run_success "$OCKAM" kafka-inlet create --from 39092 \ --avoid-publishing \ + --vault remote-vault \ --to self - run_success "$OCKAM" relay create kafka_consumer run kafka-topics --bootstrap-server localhost:29092 --delete --topic demo || true sleep 5 @@ -361,8 +354,8 @@ kafka_docker_end_to_end_encrypted_multiple_consumers_direct_connection() { # Producer setup_home_dir - run_success "$OCKAM" node create producer run_success "$OCKAM" project enroll "${producer_ticket}" + run_success "$OCKAM" node create producer run_success "$OCKAM" kafka-outlet create --bootstrap-server 127.0.0.1:19092 run_success "$OCKAM" kafka-inlet create --from 49092 \ --to self \ @@ -373,7 +366,7 @@ kafka_docker_end_to_end_encrypted_multiple_consumers_direct_connection() { run bash -c "echo 'Hello from producer - 2' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" run bash -c "echo 'Hello from producer - 3' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" run bash -c "echo 'Hello from producer - 4' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" - sleep 20 + sleep 5 run cat "$CONSUMER_OUTPUT" assert_output --partial "Hello from producer - 1" @@ -398,11 +391,115 @@ kafka_docker_end_to_end_encrypted_multiple_consumers_direct_connection() { kafka_docker_end_to_end_encrypted_multiple_consumers_direct_connection } +kafka_docker_end_to_end_encrypted_with_custodian_direct_connection() { + inlet_port="$(random_port)" + + ## In this test consumers are isolated from the producer and have only access to kafka and + ## the shared vault. It's custodian responsibility to write keys inside the shared vault. + + # Admin + export ADMIN_HOME="$OCKAM_HOME" + + export OCKAM_LOGGING=0 + vault_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay vault) + consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) + producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) + custodian_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member --relay custodian) + + export OCKAM_LOGGING=0 + export OCKAM_LOG_LEVEL=info + + export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" + export DIRECT_CONSUMER_OUTPUT="$ADMIN_HOME/direct_consumer.log" + + # remove the topic if it exists + run kafka-topics --bootstrap-server localhost:19092 --delete --topic demo || true + + # Vault Node + setup_home_dir + run_success "$OCKAM" project enroll "${vault_ticket}" + run_success "$OCKAM" node create vault --tcp-listener-address 127.0.0.1:${inlet_port} + run_success "$OCKAM" service start remote-proxy-vault --vault-name default + run_success "$OCKAM" relay create vault + + # Custodian + setup_home_dir + # create a remote identity, but we need an enrolled local identity first + run_success "$OCKAM" identity create local-identity + run_success "$OCKAM" project enroll --identity local-identity "${custodian_ticket}" + run_success "$OCKAM" vault create \ + --route /ip4/127.0.0.1/tcp/${inlet_port}/secure/api/service/remote_proxy_vault \ + --identity local-identity remote-vault + run_success "$OCKAM" node create custodian + run_success "$OCKAM" kafka-custodian create --vault remote-vault + run_success "$OCKAM" relay create custodian + + # direct consumer + run_success kafka-topics --bootstrap-server localhost:19092 --create --topic demo --partitions 1 --replication-factor 1 + kafka-console-consumer --topic demo --bootstrap-server localhost:19092 --max-messages 1 --timeout-ms 60000 >"$DIRECT_CONSUMER_OUTPUT" & + + # Producer + setup_home_dir + run_success "$OCKAM" project enroll "${producer_ticket}" + run_success "$OCKAM" node create producer + run_success "$OCKAM" kafka-outlet create --bootstrap-server 127.0.0.1:19092 + run_success "$OCKAM" kafka-inlet create --from 49092 \ + --to self \ + --consumer /project/default/service/forward_to_custodian/secure/api + + sleep 5 + + # producer 4 messages without involving the consumers + run bash -c "echo 'Hello from producer - 1' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" + run bash -c "echo 'Hello from producer - 2' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" + run bash -c "echo 'Hello from producer - 3' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" + run bash -c "echo 'Hello from producer - 4' | kafka-console-producer --topic demo --bootstrap-server localhost:49092 --max-block-ms 30000" + + # Consumer + setup_home_dir + run_success "$OCKAM" identity create local-identity + run_success "$OCKAM" project enroll --identity local-identity "${consumer_ticket}" + run_success "$OCKAM" vault create \ + --route /project/default/service/forward_to_vault/secure/api/service/remote_proxy_vault \ + --identity local-identity remote-vault + run_success "$OCKAM" node create consumer + + run_success "$OCKAM" kafka-outlet create --bootstrap-server 127.0.0.1:19092 + run_success "$OCKAM" kafka-inlet create --from 29092 \ + --avoid-publishing \ + --vault remote-vault \ + --to self + + kafka-console-consumer --topic demo --bootstrap-server localhost:29092 --from-beginning --group consumer-group-name --max-messages 4 --timeout-ms 60000 >>"${CONSUMER_OUTPUT}" + + run cat "$CONSUMER_OUTPUT" + assert_output --partial "Hello from producer - 1" + assert_output --partial "Hello from producer - 2" + assert_output --partial "Hello from producer - 3" + assert_output --partial "Hello from producer - 4" + + # direct connection to the kafka broker + run cat "$DIRECT_CONSUMER_OUTPUT" + refute_output --partial "Hello" +} + +@test "kafka - docker - end-to-end-encrypted - custodian - direct connection - redpanda" { + export KAFKA_COMPOSE_FILE="redpanda-docker-compose.yaml" + start_kafka + kafka_docker_end_to_end_encrypted_with_custodian_direct_connection +} + +@test "kafka - docker - end-to-end-encrypted - custodian - direct connection - apache" { + export KAFKA_COMPOSE_FILE="apache-docker-compose.yaml" + start_kafka + kafka_docker_end_to_end_encrypted_with_custodian_direct_connection +} + kafka_docker_end_to_end_encrypted_single_gateway() { # Admin export ADMIN_HOME="$OCKAM_HOME" - export OCKAM_LOGGING=true - export OCKAM_LOG_LEVEL=debug + export OCKAM_LOGGING=0 + export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" export DIRECT_CONSUMER_OUTPUT="$ADMIN_HOME/direct_consumer.log" @@ -435,13 +532,13 @@ kafka_docker_end_to_end_encrypted_single_gateway() { @test "kafka - docker - end-to-end-encrypted - single gateway - redpanda" { export KAFKA_COMPOSE_FILE="redpanda-docker-compose.yaml" start_kafka - kafka_docker_end_to_end_encrypted_direct_connection + kafka_docker_end_to_end_encrypted_single_gateway } @test "kafka - docker - end-to-end-encrypted - single gateway - apache" { export KAFKA_COMPOSE_FILE="apache-docker-compose.yaml" start_kafka - kafka_docker_end_to_end_encrypted_direct_connection + kafka_docker_end_to_end_encrypted_single_gateway } kafka_docker_cleartext() { @@ -453,7 +550,7 @@ kafka_docker_cleartext() { consumer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) producer_ticket=$($OCKAM project ticket --usage-count 10 --attribute role=member) - export OCKAM_LOGGING=true + export OCKAM_LOGGING=0 export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -516,7 +613,7 @@ kafka_docker_cleartext() { kafka_docker_end_to_end_encrypted_offset_decryption() { # Admin export ADMIN_HOME="$OCKAM_HOME" - export OCKAM_LOGGING=true + export OCKAM_LOGGING=0 export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" @@ -579,7 +676,7 @@ kafka_docker_end_to_end_encrypted_offset_decryption() { kafka_docker_encrypt_only_two_fields() { # Admin export ADMIN_HOME="$OCKAM_HOME" - export OCKAM_LOGGING=true + export OCKAM_LOGGING=0 export OCKAM_LOG_LEVEL=info export CONSUMER_OUTPUT="$ADMIN_HOME/consumer.log" diff --git a/implementations/rust/ockam/ockam_identity/src/identity/identity.rs b/implementations/rust/ockam/ockam_identity/src/identity/identity.rs index 7cdc16f63f2..2bd0079faf9 100644 --- a/implementations/rust/ockam/ockam_identity/src/identity/identity.rs +++ b/implementations/rust/ockam/ockam_identity/src/identity/identity.rs @@ -283,6 +283,7 @@ Change history: 81825837830101583285f68200815820f405e06d988fa8039cce1cd0ae607e46 Vault::create_secure_channel_vault().await?, Vault::create_credential_vault().await?, Vault::create_verifying_vault(), + Vault::create_encryption_at_rest_vault().await?, )) .build(); @@ -302,6 +303,7 @@ Change history: 81825837830101583285f68200815820f405e06d988fa8039cce1cd0ae607e46 Vault::create_secure_channel_vault().await?, Vault::create_credential_vault().await?, Vault::create_verifying_vault(), + Vault::create_encryption_at_rest_vault().await?, )) .build(); let identities02 = Identities::builder() @@ -311,6 +313,7 @@ Change history: 81825837830101583285f68200815820f405e06d988fa8039cce1cd0ae607e46 Vault::create_secure_channel_vault().await?, Vault::create_credential_vault().await?, Vault::create_verifying_vault(), + Vault::create_encryption_at_rest_vault().await?, )) .build(); diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs index 936aa03bde3..bd3e3b94831 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs @@ -10,6 +10,8 @@ pub enum EncryptionRequest { Encrypt(Vec), /// Trigger a manual rekey Rekey, + /// Derive new key + DeriveNewKey, } /// Response type for `EncryptorWorker` API Address @@ -23,7 +25,17 @@ pub enum EncryptionResponse { /// Request type for `Decryptor` API Address (the `Decryptor` is accessible through the `HandshakeWorker`) #[derive(Serialize, Deserialize, Message)] -pub struct DecryptionRequest(pub Vec, pub Option); +pub enum DecryptionRequest { + /// Decrypt data + Decrypt { + /// Ciphertext to decrypt + ciphertext: Vec, + /// Rekey counter + rekey_counter: Option, + }, + /// Derive new key + DeriveNewKey, +} /// Response type for `Decryptor` API Address (the `Decryptor` is accessible through the `HandshakeWorker`) #[derive(Serialize, Deserialize, Message)] diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs index 42027b53f72..e50b1e91646 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs @@ -80,18 +80,28 @@ impl DecryptorHandler { let return_route = msg.return_route; // Decode raw payload binary - let mut request = DecryptionRequest::decode(&msg.payload)?; - let decrypted_payload = if let Some(rekey_counter) = request.1 { - self.decryptor - .decrypt_with_rekey_counter(&mut request.0, rekey_counter) - .await - } else { - self.decryptor.decrypt(request.0.as_mut_slice()).await - }; + let request = DecryptionRequest::decode(&msg.payload)?; + let response = match request { + DecryptionRequest::Decrypt { + mut ciphertext, + rekey_counter, + } => { + let decrypted_payload = if let Some(rekey_counter) = rekey_counter { + self.decryptor + .decrypt_with_rekey_counter(&mut ciphertext, rekey_counter) + .await + } else { + self.decryptor.decrypt(&mut ciphertext).await + }; - let response = match decrypted_payload { - Ok((payload, _nonce)) => DecryptionResponse::Ok(payload.to_vec()), - Err(err) => DecryptionResponse::Err(err), + match decrypted_payload { + Ok((payload, _nonce)) => DecryptionResponse::Ok(payload.to_vec()), + Err(err) => DecryptionResponse::Err(err), + } + } + DecryptionRequest::DeriveNewKey => { + todo!() + } }; // Send reply to the caller diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs index c0587a6aa5f..66ac4a425f6 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs @@ -173,6 +173,9 @@ impl EncryptorWorker { EncryptionResponse::Err(err) } }, + EncryptionRequest::DeriveNewKey => { + todo!() + } }; // Send the reply to the caller diff --git a/implementations/rust/ockam/ockam_identity/src/vault.rs b/implementations/rust/ockam/ockam_identity/src/vault.rs index d9a72288803..c534dd5fc96 100644 --- a/implementations/rust/ockam/ockam_identity/src/vault.rs +++ b/implementations/rust/ockam/ockam_identity/src/vault.rs @@ -7,8 +7,9 @@ use ockam_vault::storage::SecretsRepository; #[cfg(feature = "storage")] use ockam_vault::storage::SecretsSqlxDatabase; use ockam_vault::{ - SoftwareVaultForSecureChannels, SoftwareVaultForSigning, SoftwareVaultForVerifyingSignatures, - VaultForSecureChannels, VaultForSigning, VaultForVerifyingSignatures, + SoftwareVaultForAtRestEncryption, SoftwareVaultForSecureChannels, SoftwareVaultForSigning, + SoftwareVaultForVerifyingSignatures, VaultForEncryptionAtRest, VaultForSecureChannels, + VaultForSigning, VaultForVerifyingSignatures, }; /// Vault @@ -22,6 +23,8 @@ pub struct Vault { pub credential_vault: Arc, /// Vault used for verifying signature and sha256 pub verifying_vault: Arc, + /// Vault used for encrypting data at rest + pub encryption_at_rest_vault: Arc, } impl Vault { @@ -31,12 +34,14 @@ impl Vault { secure_channel_vault: Arc, credential_vault: Arc, verifying_vault: Arc, + encryption_at_rest_vault: Arc, ) -> Self { Self { identity_vault, secure_channel_vault, credential_vault, verifying_vault, + encryption_at_rest_vault, } } @@ -48,6 +53,7 @@ impl Vault { Self::create_secure_channel_vault().await?, Self::create_credential_vault().await?, Self::create_verifying_vault(), + Self::create_encryption_at_rest_vault().await?, )) } @@ -79,6 +85,13 @@ impl Vault { pub fn create_verifying_vault() -> Arc { Arc::new(SoftwareVaultForVerifyingSignatures {}) } + + /// Create [`SoftwareVaultForAtRestEncryption`] with an in-memory storage + pub async fn create_encryption_at_rest_vault() -> Result> { + Ok(Arc::new(SoftwareVaultForAtRestEncryption::new(Arc::new( + SecretsSqlxDatabase::create().await?, + )))) + } } impl Vault { @@ -95,6 +108,7 @@ impl Vault { Arc::new(SoftwareVaultForSecureChannels::new(repository.clone())), Arc::new(SoftwareVaultForSigning::new(repository.clone())), Arc::new(SoftwareVaultForVerifyingSignatures {}), + Arc::new(SoftwareVaultForAtRestEncryption::new(repository)), ) } } diff --git a/implementations/rust/ockam/ockam_identity/tests/channel.rs b/implementations/rust/ockam/ockam_identity/tests/channel.rs index 2086b1e5986..f9b4e5a555d 100644 --- a/implementations/rust/ockam/ockam_identity/tests/channel.rs +++ b/implementations/rust/ockam/ockam_identity/tests/channel.rs @@ -16,7 +16,8 @@ use ockam_identity::{ }; use ockam_node::{Context, MessageReceiveOptions, WorkerBuilder}; use ockam_vault::{ - SoftwareVaultForSecureChannels, SoftwareVaultForSigning, SoftwareVaultForVerifyingSignatures, + SoftwareVaultForAtRestEncryption, SoftwareVaultForSecureChannels, SoftwareVaultForSigning, + SoftwareVaultForVerifyingSignatures, }; #[ockam_macros::test] @@ -486,7 +487,10 @@ async fn test_channel_api(ctx: &mut Context) -> Result<()> { let decrypted_alice: DecryptionResponse = ctx .send_and_receive( route![alice_channel_data.decryptor_api_address().clone()], - DecryptionRequest(encrypted_bob, None), + DecryptionRequest::Decrypt { + ciphertext: encrypted_bob, + rekey_counter: None, + }, ) .await?; let decrypted_alice = match decrypted_alice { @@ -497,7 +501,10 @@ async fn test_channel_api(ctx: &mut Context) -> Result<()> { let decrypted_bob: DecryptionResponse = ctx .send_and_receive( route![bob_channel_data.decryptor_api_address().clone()], - DecryptionRequest(encrypted_alice, None), + DecryptionRequest::Decrypt { + ciphertext: encrypted_alice, + rekey_counter: None, + }, ) .await?; let decrypted_bob = match decrypted_bob { @@ -915,6 +922,7 @@ async fn test_channel_delete_ephemeral_keys(ctx: &mut Context) -> Result<()> { alice_sc_vault.clone(), SoftwareVaultForSigning::create().await?, SoftwareVaultForVerifyingSignatures::create(), + SoftwareVaultForAtRestEncryption::create().await?, ); let bob_identity_vault = SoftwareVaultForSigning::create().await?; @@ -924,6 +932,7 @@ async fn test_channel_delete_ephemeral_keys(ctx: &mut Context) -> Result<()> { bob_sc_vault.clone(), SoftwareVaultForSigning::create().await?, SoftwareVaultForVerifyingSignatures::create(), + SoftwareVaultForAtRestEncryption::create().await?, ); let secure_channels_alice = SecureChannels::builder() diff --git a/implementations/rust/ockam/ockam_identity/tests/persistence.rs b/implementations/rust/ockam/ockam_identity/tests/persistence.rs index b9b22860c99..4ea4ea63027 100644 --- a/implementations/rust/ockam/ockam_identity/tests/persistence.rs +++ b/implementations/rust/ockam/ockam_identity/tests/persistence.rs @@ -90,7 +90,10 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg1_alice) = ctx .send_and_receive( route![bob_channel.decryptor_api_address().clone()], - DecryptionRequest(encrypted_msg1_alice, None), + DecryptionRequest::Decrypt { + ciphertext: encrypted_msg1_alice, + rekey_counter: None, + }, ) .await? else { @@ -100,7 +103,10 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg2_alice) = ctx .send_and_receive( route![bob_channel.decryptor_api_address().clone()], - DecryptionRequest(encrypted_msg2_alice, None), + DecryptionRequest::Decrypt { + ciphertext: encrypted_msg2_alice, + rekey_counter: None, + }, ) .await? else { @@ -110,7 +116,10 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg1_bob) = ctx .send_and_receive( route![alice_channel.decryptor_api_address().clone()], - DecryptionRequest(encrypted_msg1_bob, None), + DecryptionRequest::Decrypt { + ciphertext: encrypted_msg1_bob, + rekey_counter: None, + }, ) .await? else { @@ -120,7 +129,10 @@ async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg2_bob) = ctx .send_and_receive( route![alice_channel.decryptor_api_address().clone()], - DecryptionRequest(encrypted_msg2_bob, None), + DecryptionRequest::Decrypt { + ciphertext: encrypted_msg2_bob, + rekey_counter: None, + }, ) .await? else { @@ -326,7 +338,10 @@ fn test_persistence() -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg1_alice) = ctx2 .send_and_receive( route![data.decryptor_api_address_bob.clone()], - DecryptionRequest(data.encrypted_msg1_alice, None), + DecryptionRequest::Decrypt { + ciphertext: data.encrypted_msg1_alice, + rekey_counter: None, + }, ) .await? else { @@ -336,7 +351,10 @@ fn test_persistence() -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg2_alice) = ctx2 .send_and_receive( route![data.decryptor_api_address_bob.clone()], - DecryptionRequest(data.encrypted_msg2_alice, None), + DecryptionRequest::Decrypt { + ciphertext: data.encrypted_msg2_alice, + rekey_counter: None, + }, ) .await? else { @@ -346,7 +364,10 @@ fn test_persistence() -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg1_bob) = ctx2 .send_and_receive( route![data.decryptor_api_address_alice.clone()], - DecryptionRequest(data.encrypted_msg1_bob, None), + DecryptionRequest::Decrypt { + ciphertext: data.encrypted_msg1_bob, + rekey_counter: None, + }, ) .await? else { @@ -356,7 +377,10 @@ fn test_persistence() -> ockam_core::Result<()> { let DecryptionResponse::Ok(decrypted_msg2_bob) = ctx2 .send_and_receive( route![data.decryptor_api_address_alice.clone()], - DecryptionRequest(data.encrypted_msg2_bob, None), + DecryptionRequest::Decrypt { + ciphertext: data.encrypted_msg2_bob, + rekey_counter: None, + }, ) .await? else { diff --git a/implementations/rust/ockam/ockam_node/src/context/send_message.rs b/implementations/rust/ockam/ockam_node/src/context/send_message.rs index b31e7ddacc3..5170f891c8b 100644 --- a/implementations/rust/ockam/ockam_node/src/context/send_message.rs +++ b/implementations/rust/ockam/ockam_node/src/context/send_message.rs @@ -7,14 +7,17 @@ use core::time::Duration; use ockam_core::compat::{sync::Arc, vec::Vec}; use ockam_core::{ errcode::{Kind, Origin}, - route, Address, AllowAll, AllowOnwardAddress, Error, LocalMessage, Mailboxes, Message, - RelayMessage, Result, Route, Routed, + route, Address, AllOutgoingAccessControl, AllowAll, AllowOnwardAddress, Error, + IncomingAccessControl, LocalMessage, Mailboxes, Message, OutgoingAccessControl, RelayMessage, + Result, Route, Routed, }; use ockam_core::{LocalInfo, Mailbox}; /// Full set of options to `send_and_receive_extended` function pub struct MessageSendReceiveOptions { message_wait: MessageWait, + incoming_access_control: Option>, + outgoing_access_control: Option>, } impl Default for MessageSendReceiveOptions { @@ -28,6 +31,8 @@ impl MessageSendReceiveOptions { pub fn new() -> Self { Self { message_wait: MessageWait::Timeout(DEFAULT_TIMEOUT), + incoming_access_control: None, + outgoing_access_control: None, } } @@ -42,6 +47,24 @@ impl MessageSendReceiveOptions { self.message_wait = MessageWait::Blocking; self } + + /// Set incoming access control + pub fn with_incoming_access_control( + mut self, + incoming_access_control: Arc, + ) -> Self { + self.incoming_access_control = Some(incoming_access_control); + self + } + + /// Set outgoing access control + pub fn with_outgoing_access_control( + mut self, + outgoing_access_control: Arc, + ) -> Self { + self.outgoing_access_control = Some(outgoing_access_control); + self + } } impl Context { @@ -86,11 +109,29 @@ impl Context { let next = route.next()?.clone(); let address = Address::random_tagged("Context.send_and_receive.detached"); + + let incoming_access_control = + if let Some(incoming_access_control) = options.incoming_access_control { + incoming_access_control + } else { + Arc::new(AllowAll) + }; + + let outgoing_access_control: Arc = + if let Some(outgoing_access_control) = options.outgoing_access_control { + Arc::new(AllOutgoingAccessControl::new(vec![ + outgoing_access_control, + Arc::new(AllowOnwardAddress(next.clone())), + ])) + } else { + Arc::new(AllowOnwardAddress(next.clone())) + }; + let mailboxes = Mailboxes::new( Mailbox::new( address.clone(), - Arc::new(AllowAll), - Arc::new(AllowOnwardAddress(next.clone())), + incoming_access_control, + outgoing_access_control, ), vec![], ); diff --git a/implementations/rust/ockam/ockam_rust_elixir_nifs/src/lib.rs b/implementations/rust/ockam/ockam_rust_elixir_nifs/src/lib.rs index a9d02cfa845..d1af29f6d9a 100644 --- a/implementations/rust/ockam/ockam_rust_elixir_nifs/src/lib.rs +++ b/implementations/rust/ockam/ockam_rust_elixir_nifs/src/lib.rs @@ -34,8 +34,8 @@ use ockam_identity::{ }; use ockam_vault::{ EdDSACurve25519SecretKey, HandleToSecret, SigningKeyType, SigningSecret, - SigningSecretKeyHandle, SoftwareVaultForSecureChannels, SoftwareVaultForSigning, - X25519PublicKey, X25519SecretKey, + SigningSecretKeyHandle, SoftwareVaultForAtRestEncryption, SoftwareVaultForSecureChannels, + SoftwareVaultForSigning, X25519PublicKey, X25519SecretKey, }; use ockam_vault_aws::{AwsKmsConfig, AwsSigningVault, InitialKeysDiscovery}; use rustler::{Atom, Binary, Env, Error, NewBinary, NifResult}; @@ -50,6 +50,8 @@ lazy_static! { RwLock::new(None); static ref SECURE_CHANNEL_MEMORY_VAULT: RwLock>> = RwLock::new(None); + static ref ENCRYPTION_AT_REST_VAULT: RwLock>> = + RwLock::new(None); } mod atoms { @@ -111,8 +113,10 @@ fn load_memory_vault() -> bool { block_future(async move { let identity_vault = SoftwareVaultForSigning::create().await.unwrap(); let secure_channel_vault = SoftwareVaultForSecureChannels::create().await.unwrap(); + let encryption_at_rest_vault = SoftwareVaultForAtRestEncryption::create().await.unwrap(); *IDENTITY_MEMORY_VAULT.write().unwrap() = Some(identity_vault.clone()); *SECURE_CHANNEL_MEMORY_VAULT.write().unwrap() = Some(secure_channel_vault.clone()); + *ENCRYPTION_AT_REST_VAULT.write().unwrap() = Some(encryption_at_rest_vault.clone()); let builder = ockam_identity::Identities::builder() .await .unwrap() @@ -121,6 +125,7 @@ fn load_memory_vault() -> bool { secure_channel_vault, Vault::create_credential_vault().await.unwrap(), Vault::create_verifying_vault(), + encryption_at_rest_vault, )); *IDENTITIES.write().unwrap() = Some(builder.build()); }); @@ -134,6 +139,11 @@ fn setup_aws_kms(key_ids: Vec) -> NifResult { None => return Err(Error::Term(Box::new(atoms::attestation_decode_error()))), }; + let encryption_at_rest_vault = match ENCRYPTION_AT_REST_VAULT.read().unwrap().clone() { + Some(encryption_at_rest_vault) => encryption_at_rest_vault, + None => return Err(Error::Term(Box::new(atoms::attestation_decode_error()))), + }; + let key_ids = key_ids .into_iter() .map(|x| { @@ -156,6 +166,7 @@ fn setup_aws_kms(key_ids: Vec) -> NifResult { secure_channel_vault, Vault::create_credential_vault().await.unwrap(), Vault::create_verifying_vault(), + encryption_at_rest_vault, )); *IDENTITIES.write().unwrap() = Some(builder.build()); Ok(true) diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/common.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/common.rs new file mode 100644 index 00000000000..4c585c53d01 --- /dev/null +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/common.rs @@ -0,0 +1,23 @@ +use crate::{AeadSecretKeyHandle, HandleToSecret, SecretBufferHandle}; +use ockam_core::compat::rand::{thread_rng, RngCore}; + +pub(super) fn generate_random_handle() -> HandleToSecret { + // NOTE: Buffer and Aes secrets in the system are ephemeral and it should be fine, + // that every time we import the same secret - it gets different Handle value. + // However, if we decide to have persistent Buffer or Aes secrets, that should be + // changed (probably to hash value of the secret) + let mut rng = thread_rng(); + let mut rand = vec![0u8; 8]; + rng.fill_bytes(&mut rand); + HandleToSecret::new(rand) +} + +pub(super) fn generate_buffer_handle() -> SecretBufferHandle { + SecretBufferHandle(generate_random_handle()) +} + +pub(super) fn generate_aead_handle() -> AeadSecretKeyHandle { + use crate::Aes256GcmSecretKeyHandle; + let handle = generate_random_handle(); + AeadSecretKeyHandle(Aes256GcmSecretKeyHandle(handle)) +} diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/mod.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/mod.rs index 4e96d929b6d..adc1e3f14c2 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/mod.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/mod.rs @@ -18,9 +18,12 @@ cfg_if! { } } +mod common; mod types; +mod vault_for_encryption_at_rest; #[allow(clippy::module_inception)] mod vault_for_secure_channels; pub use types::*; +pub use vault_for_encryption_at_rest::*; pub use vault_for_secure_channels::*; diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/types.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/types.rs index e32beca2590..942640fc688 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/types.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/types.rs @@ -71,3 +71,6 @@ cfg_if! { // TODO } } + +/// AES GCM tag size +pub const AES_GCM_TAGSIZE: usize = 16; diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_encryption_at_rest.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_encryption_at_rest.rs new file mode 100644 index 00000000000..48046ed0b47 --- /dev/null +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_encryption_at_rest.rs @@ -0,0 +1,171 @@ +use ockam_core::compat::collections::BTreeMap; +use ockam_core::compat::rand::{thread_rng, RngCore}; +use ockam_core::compat::sync::{Arc, RwLock}; +use ockam_core::compat::vec::Vec; +use ockam_core::{async_trait, Result}; + +use super::make_aes; +use crate::storage::{SecretsRepository, SecretsSqlxDatabase}; + +use crate::software::vault_for_secure_channels::common::generate_aead_handle; +use crate::{ + AeadSecret, AeadSecretKeyHandle, BufferSecret, VaultError, VaultForEncryptionAtRest, + AEAD_SECRET_LENGTH, AES_GCM_TAGSIZE, AES_NONCE_LENGTH, +}; + +/// [`VaultForEncryptionAtRest`] implementation using software +pub struct SoftwareVaultForAtRestEncryption { + ephemeral_aead_secrets: Arc>>, + secrets_repository: Arc, +} +#[async_trait] +impl VaultForEncryptionAtRest for SoftwareVaultForAtRestEncryption { + async fn aead_encrypt( + &self, + secret_key_handle: &AeadSecretKeyHandle, + plain_text: &mut [u8], + aad: &[u8], + ) -> Result<()> { + let secret = self.get_aead_secret(secret_key_handle).await?; + let nonce = self.generate_nonce(); + let aes = make_aes(&secret); + if plain_text.len() < AES_NONCE_LENGTH { + return Err(VaultError::InsufficientEncryptBuffer)?; + } + aes.encrypt_message(&mut plain_text[AES_NONCE_LENGTH..], &nonce, aad)?; + plain_text[..AES_NONCE_LENGTH].copy_from_slice(&nonce); + Ok(()) + } + + async fn aead_decrypt<'a>( + &self, + secret_key_handle: &AeadSecretKeyHandle, + rekey_counter: u16, + cipher_text: &'a mut [u8], + aad: &[u8], + ) -> Result<&'a mut [u8]> { + let secret = self.get_aead_secret(secret_key_handle).await?; + let secret = if rekey_counter > 0 { + self.rekey(secret, rekey_counter).await? + } else { + secret + }; + + let aes = make_aes(&secret); + // the first AES_NONCE_LENGTH bytes of the cipher text are the nonce + let (nonce, cipher_text) = cipher_text.split_at_mut(AES_NONCE_LENGTH); + aes.decrypt_message(cipher_text, nonce, aad) + } + + async fn rekey_and_delete( + &self, + secret_key_handle: &AeadSecretKeyHandle, + ) -> Result { + let secret = self.get_aead_secret(secret_key_handle).await?; + let new_key_secret = self.rekey(secret, 1).await?; + let new_key_handle = self.import_aead_key(new_key_secret.0.to_vec()).await?; + self.secrets_repository + .delete_aead_secret(secret_key_handle) + .await?; + self.ephemeral_aead_secrets + .write() + .unwrap() + .remove(secret_key_handle); + Ok(new_key_handle) + } + + async fn import_aead_key(&self, secret: Vec) -> Result { + let secret = BufferSecret::new(secret); + + if secret.data().len() != AEAD_SECRET_LENGTH { + return Err(VaultError::InvalidSecretLength)?; + } + + let secret = AeadSecret(<[u8; 32]>::try_from(secret.data()).unwrap()); + let handle = generate_aead_handle(); + + self.secrets_repository + .store_aead_secret(&handle, secret.clone()) + .await?; + self.ephemeral_aead_secrets + .write() + .unwrap() + .insert(handle.clone(), secret); + + Ok(handle) + } +} + +impl SoftwareVaultForAtRestEncryption { + /// Create a new instance of [`SoftwareVaultForAtRestEncryption`] + pub fn new(secrets_repository: Arc) -> Self { + Self { + ephemeral_aead_secrets: Arc::new(RwLock::new(BTreeMap::new())), + secrets_repository, + } + } + + /// Create Software implementation Vault with an in-memory implementation to store secrets + #[cfg(feature = "storage")] + pub async fn create() -> Result> { + Ok(Arc::new(Self::new(Arc::new( + SecretsSqlxDatabase::create().await?, + )))) + } + + async fn rekey(&self, secret: AeadSecret, n: u16) -> Result { + if n == 0 { + return Err(VaultError::InvalidRekeyCount)?; + } + + const MAX_NONCE: [u8; AES_NONCE_LENGTH] = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ]; + + let mut new_key_buffer = Vec::with_capacity(32 + AES_GCM_TAGSIZE); + let mut counter = n; + let mut secret = secret.clone(); + + while counter > 0 { + new_key_buffer.clear(); + new_key_buffer.resize(32 + AES_GCM_TAGSIZE, 0); + + let aes = make_aes(&secret); + aes.encrypt_message(&mut new_key_buffer, &MAX_NONCE, &[])?; + secret = AeadSecret(<[u8; 32]>::try_from(&new_key_buffer[..32]).unwrap()); + + counter -= 1; + } + + Ok(secret) + } + + async fn get_aead_secret(&self, handle: &AeadSecretKeyHandle) -> Result { + let secret = self + .ephemeral_aead_secrets + .read() + .unwrap() + .get(handle) + .cloned(); + + match secret { + Some(secret) => Ok(secret), + None => { + let secret = self.secrets_repository.get_aead_secret(handle).await?; + let secret = secret.ok_or(VaultError::KeyNotFound)?; + self.ephemeral_aead_secrets + .write() + .unwrap() + .insert(handle.clone(), secret.clone()); + Ok(secret) + } + } + } + + fn generate_nonce(&self) -> [u8; AES_NONCE_LENGTH] { + // TODO: query database for the last nonce used??? + let mut nonce = [0u8; AES_NONCE_LENGTH]; + thread_rng().fill_bytes(&mut nonce); + nonce + } +} diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs index 96340d96f59..aca08877eba 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs @@ -1,9 +1,8 @@ use sha2::{Digest, Sha256}; use tracing::instrument; -use ockam_core::compat::boxed::Box; use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::rand::{thread_rng, RngCore}; +use ockam_core::compat::rand::thread_rng; use ockam_core::compat::sync::{Arc, RwLock}; use ockam_core::compat::vec::{vec, Vec}; use ockam_core::{async_trait, Result}; @@ -13,6 +12,11 @@ use crate::storage::SecretsRepository; #[cfg(feature = "storage")] use crate::storage::SecretsSqlxDatabase; +use super::make_aes; +use crate::software::vault_for_secure_channels::common::{ + generate_aead_handle, generate_buffer_handle, +}; +use crate::software::vault_for_secure_channels::types::AES_GCM_TAGSIZE; use crate::{ AeadSecret, AeadSecretKeyHandle, BufferSecret, HKDFNumberOfOutputs, HandleToSecret, HashOutput, HkdfOutput, SecretBufferHandle, SoftwareVaultForVerifyingSignatures, VaultError, @@ -20,10 +24,6 @@ use crate::{ AEAD_SECRET_LENGTH, }; -use super::make_aes; - -const AES_GCM_TAGSIZE: usize = 16; - /// [`SecureChannelVault`] implementation using software pub struct SoftwareVaultForSecureChannels { ephemeral_buffer_secrets: Arc>>, @@ -136,27 +136,6 @@ impl SoftwareVaultForSecureChannels { x25519_dalek::PublicKey::from(public_key.0) } - fn generate_random_handle() -> HandleToSecret { - // NOTE: Buffer and Aes secrets in the system are ephemeral and it should be fine, - // that every time we import the same secret - it gets different Handle value. - // However, if we decide to have persistent Buffer or Aes secrets, that should be - // changed (probably to hash value of the secret) - let mut rng = thread_rng(); - let mut rand = vec![0u8; 8]; - rng.fill_bytes(&mut rand); - HandleToSecret::new(rand) - } - - fn generate_buffer_handle() -> SecretBufferHandle { - SecretBufferHandle(Self::generate_random_handle()) - } - - fn generate_aead_handle() -> AeadSecretKeyHandle { - use crate::Aes256GcmSecretKeyHandle; - let handle = Self::generate_random_handle(); - AeadSecretKeyHandle(Aes256GcmSecretKeyHandle(handle)) - } - fn ecdh_internal( secret: X25519SecretKey, peer_public_key: X25519PublicKey, @@ -174,7 +153,7 @@ impl SoftwareVaultForSecureChannels { } fn import_buffer_secret_impl(&self, secret: BufferSecret) -> SecretBufferHandle { - let handle = Self::generate_buffer_handle(); + let handle = generate_buffer_handle(); self.ephemeral_buffer_secrets .write() @@ -450,7 +429,7 @@ impl VaultForSecureChannels for SoftwareVaultForSecureChannels { .map_err(|_| VaultError::InvalidSecretLength)?; let secret = AeadSecret(secret); - let handle = Self::generate_aead_handle(); + let handle = generate_aead_handle(); self.ephemeral_aead_secrets .write() diff --git a/implementations/rust/ockam/ockam_vault/src/traits/mod.rs b/implementations/rust/ockam/ockam_vault/src/traits/mod.rs index 434c12df10c..0615fe38bc8 100644 --- a/implementations/rust/ockam/ockam_vault/src/traits/mod.rs +++ b/implementations/rust/ockam/ockam_vault/src/traits/mod.rs @@ -1,7 +1,9 @@ +mod vault_for_at_rest_encryption; mod vault_for_secure_channels; mod vault_for_signing; mod vault_for_verifying_signatures; +pub use vault_for_at_rest_encryption::*; pub use vault_for_secure_channels::*; pub use vault_for_signing::*; pub use vault_for_verifying_signatures::*; diff --git a/implementations/rust/ockam/ockam_vault/src/traits/vault_for_at_rest_encryption.rs b/implementations/rust/ockam/ockam_vault/src/traits/vault_for_at_rest_encryption.rs new file mode 100644 index 00000000000..3097dd2fbc6 --- /dev/null +++ b/implementations/rust/ockam/ockam_vault/src/traits/vault_for_at_rest_encryption.rs @@ -0,0 +1,38 @@ +use crate::AeadSecretKeyHandle; + +use ockam_core::{async_trait, Result}; + +/// Vault for verifying signatures and computing SHA-256. +#[async_trait] +pub trait VaultForEncryptionAtRest: Send + Sync + 'static { + /// Perform AEAD encryption. + /// Nonce is generated internally. + /// The plain_text[0..AES_NONCE_LENGTH] will be overwritten by the nonce, use + /// plain_text[AES_NONCE_LENGTH..] to pass the actual data to encrypt. + async fn aead_encrypt( + &self, + secret_key_handle: &AeadSecretKeyHandle, + plain_text: &mut [u8], + aad: &[u8], + ) -> Result<()>; + + /// Perform AEAD decryption. + /// The nonce is the first AES_NONCE_LENGTH bytes of the cipher_text. + /// The cipher_text will be decrypted in place. + async fn aead_decrypt<'a>( + &self, + secret_key_handle: &AeadSecretKeyHandle, + rekey_counter: u16, + cipher_text: &'a mut [u8], + aad: &[u8], + ) -> Result<&'a mut [u8]>; + + /// Rekey, delete the old one and return the new key handle + async fn rekey_and_delete( + &self, + secret_key_handle: &AeadSecretKeyHandle, + ) -> Result; + + /// Import an AES-GCM key and return a handle to it + async fn import_aead_key(&self, secret: Vec) -> Result; +} diff --git a/implementations/rust/ockam/ockam_vault/src/types/hashes.rs b/implementations/rust/ockam/ockam_vault/src/types/hashes.rs index b78701c1927..fa83ffb9178 100644 --- a/implementations/rust/ockam/ockam_vault/src/types/hashes.rs +++ b/implementations/rust/ockam/ockam_vault/src/types/hashes.rs @@ -19,6 +19,11 @@ impl AeadSecretKeyHandle { pub fn new(handle: HandleToSecret) -> Self { Self(AeadSecretKeyHandleType::new(handle)) } + + /// Convert into an opaque vector + pub fn into_vec(self) -> Vec { + self.0 .0.take_value() + } } cfg_if! { From 098958d0ad975c6c42b210a4a2b7ca8f319cf71a Mon Sep 17 00:00:00 2001 From: Davide Baldo Date: Mon, 23 Dec 2024 17:34:47 +0100 Subject: [PATCH 5/7] feat(rust): added zeroize on `LocalMessage` --- Cargo.lock | 1 + .../rust/ockam/ockam_api/src/echoer.rs | 7 +- .../ockam_api/src/proxy_vault/protocol.rs | 27 +-- .../ockam/ockam_api/tests/common/session.rs | 2 +- .../rust/ockam/ockam_core/Cargo.toml | 1 + .../ockam/ockam_core/src/cbor/cow_bytes.rs | 15 ++ .../rust/ockam/ockam_core/src/lib.rs | 2 + .../rust/ockam/ockam_core/src/message.rs | 5 +- .../src/routing/message/local_message.rs | 28 ++- .../rust/ockam/ockam_core/src/zeroize.rs | 176 ++++++++++++++++++ .../src/secure_channel/decryptor.rs | 8 +- .../src/secure_channel/encryptor_worker.rs | 31 ++- .../handshake/handshake_worker.rs | 2 +- .../src/secure_channel/message.rs | 6 +- .../ockam_node/src/context/send_message.rs | 41 +++- .../src/transport_message.rs | 12 +- .../ockam_transport_tcp/src/workers/sender.rs | 2 +- .../src/messages/routing_message.rs | 12 +- .../src/puncture/puncture/receiver.rs | 3 +- .../src/puncture/puncture/sender.rs | 2 +- .../ockam_transport_udp/src/workers/sender.rs | 2 +- 21 files changed, 322 insertions(+), 63 deletions(-) create mode 100644 implementations/rust/ockam/ockam_core/src/zeroize.rs diff --git a/Cargo.lock b/Cargo.lock index 19e798dcad1..4d2fa786c82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4904,6 +4904,7 @@ dependencies = [ "tracing-opentelemetry", "tracing-subscriber", "utcnow", + "zeroize", ] [[package]] diff --git a/implementations/rust/ockam/ockam_api/src/echoer.rs b/implementations/rust/ockam/ockam_api/src/echoer.rs index 1f33e3f1665..bd76bf2ac26 100644 --- a/implementations/rust/ockam/ockam_api/src/echoer.rs +++ b/implementations/rust/ockam/ockam_api/src/echoer.rs @@ -13,7 +13,10 @@ impl Worker for Echoer { async fn handle_message(&mut self, ctx: &mut Context, msg: Routed) -> Result<()> { log::debug!(src = %msg.src_addr(), from = %msg.sender()?, to = %msg.return_route().next()?, "echoing back"); let msg = msg.into_local_message(); - ctx.send(msg.return_route, NeutralMessage::from(msg.payload)) - .await + ctx.send( + msg.return_route, + NeutralMessage::from(msg.payload.discard_zeroize()), + ) + .await } } diff --git a/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs b/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs index 0d4ef12da09..09316951c21 100644 --- a/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs +++ b/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs @@ -4,10 +4,11 @@ use minicbor::{CborLen, Decode, Encode}; use ockam::identity::{utils, TimestampInSeconds, Vault}; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{ - async_trait, cbor_encode_preallocate, route, Address, NeutralMessage, Route, Routed, Worker, + async_trait, cbor_encode_preallocate, route, Address, NeutralMessage, OnDrop, Route, Routed, + Worker, }; use ockam_multiaddr::MultiAddr; -use ockam_node::Context; +use ockam_node::{Context, MessageSendReceiveOptions}; use std::sync::Arc; use tokio::sync::Mutex as AsyncMutex; @@ -245,11 +246,13 @@ impl SpecificClient { let response: NeutralMessage = self .client .context - .send_and_receive( + .send_and_receive_extended( route![route, self.destination.clone()], NeutralMessage::from(encoded), + MessageSendReceiveOptions::new().with_on_drop(OnDrop::Zeroize), ) - .await?; + .await? + .into_body()?; Ok(minicbor::decode::(&response.into_vec())?) } @@ -258,7 +261,7 @@ impl SpecificClient { mod vault_for_signing { use crate::proxy_vault::protocol::{ProxyError, SpecificClient}; use minicbor::{CborLen, Decode, Encode}; - use ockam_core::{async_trait, cbor_encode_preallocate}; + use ockam_core::{async_trait, cbor_encode_preallocate, MaybeZeroizeOnDrop}; use ockam_vault::{ Signature, SigningKeyType, SigningSecretKeyHandle, VaultForSigning, VerifyingPublicKey, }; @@ -296,7 +299,7 @@ mod vault_for_signing { pub(super) async fn handle_request( vault: &dyn VaultForSigning, - request: Vec, + request: MaybeZeroizeOnDrop>, ) -> ockam_core::Result> { let request: Request = minicbor::decode(&request)?; let response = match request { @@ -480,7 +483,7 @@ mod vault_for_signing { pub mod vault_for_secure_channels { use crate::proxy_vault::protocol::{ProxyError, SpecificClient}; use minicbor::{CborLen, Decode, Encode}; - use ockam_core::{async_trait, cbor_encode_preallocate}; + use ockam_core::{async_trait, cbor_encode_preallocate, MaybeZeroizeOnDrop}; use ockam_vault::{ AeadSecretKeyHandle, HKDFNumberOfOutputs, HashOutput, HkdfOutput, SecretBufferHandle, VaultForSecureChannels, X25519PublicKey, X25519SecretKeyHandle, @@ -488,7 +491,7 @@ pub mod vault_for_secure_channels { pub(super) async fn handle_request( vault: &dyn VaultForSecureChannels, - request: Vec, + request: MaybeZeroizeOnDrop>, ) -> ockam_core::Result> { let request: Request = minicbor::decode(&request)?; let response = match request { @@ -1084,12 +1087,12 @@ pub mod vault_for_secure_channels { pub mod vault_for_verify_signatures { use crate::proxy_vault::protocol::{ProxyError, SpecificClient}; use minicbor::{CborLen, Decode, Encode}; - use ockam_core::{async_trait, cbor_encode_preallocate}; + use ockam_core::{async_trait, cbor_encode_preallocate, MaybeZeroizeOnDrop}; use ockam_vault::{Sha256Output, Signature, VaultForVerifyingSignatures, VerifyingPublicKey}; pub(super) async fn handle_request( vault: &dyn VaultForVerifyingSignatures, - request: Vec, + request: MaybeZeroizeOnDrop>, ) -> ockam_core::Result> { let request: Request = minicbor::decode(&request)?; let response = match request { @@ -1179,12 +1182,12 @@ pub mod vault_for_verify_signatures { pub mod vault_for_encryption_at_rest { use crate::proxy_vault::protocol::{ProxyError, SpecificClient}; use minicbor::{CborLen, Decode, Encode}; - use ockam_core::{async_trait, cbor_encode_preallocate}; + use ockam_core::{async_trait, cbor_encode_preallocate, MaybeZeroizeOnDrop}; use ockam_vault::{AeadSecretKeyHandle, VaultForEncryptionAtRest}; pub(super) async fn handle_request( vault: &dyn VaultForEncryptionAtRest, - request: Vec, + request: MaybeZeroizeOnDrop>, ) -> ockam_core::Result> { let request: Request = minicbor::decode(&request)?; let response = match request { diff --git a/implementations/rust/ockam/ockam_api/tests/common/session.rs b/implementations/rust/ockam/ockam_api/tests/common/session.rs index f33debc0df9..c1a9e323092 100644 --- a/implementations/rust/ockam/ockam_api/tests/common/session.rs +++ b/implementations/rust/ockam/ockam_api/tests/common/session.rs @@ -53,7 +53,7 @@ impl Worker for MockEchoer { ctx.send( msg.return_route().clone(), - NeutralMessage::from(msg.into_payload()), + NeutralMessage::from(msg.into_payload().discard_zeroize()), ) .await?; info!("Echo message back"); diff --git a/implementations/rust/ockam/ockam_core/Cargo.toml b/implementations/rust/ockam/ockam_core/Cargo.toml index 17d11801368..6cede60e614 100644 --- a/implementations/rust/ockam/ockam_core/Cargo.toml +++ b/implementations/rust/ockam/ockam_core/Cargo.toml @@ -98,6 +98,7 @@ tracing-opentelemetry = { version = "0.27.0", optional = true } tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"], optional = true } # Wasn't tested on no_std utcnow = { version = "0.2.5", default-features = false, features = ["fallback"], optional = true } +zeroize = { version = "1.8", default-features = false } [dev-dependencies] cddl-cat = { version = "0.6.2" } diff --git a/implementations/rust/ockam/ockam_core/src/cbor/cow_bytes.rs b/implementations/rust/ockam/ockam_core/src/cbor/cow_bytes.rs index d2efb7ee8e8..52ec4913291 100644 --- a/implementations/rust/ockam/ockam_core/src/cbor/cow_bytes.rs +++ b/implementations/rust/ockam/ockam_core/src/cbor/cow_bytes.rs @@ -4,6 +4,7 @@ use crate::compat::vec::Vec; use core::ops::Deref; use minicbor::{CborLen, Decode, Encode}; use serde::{Deserialize, Serialize}; +use zeroize::Zeroize; /// A new type around `Cow<'_, [u8]>` that borrows from input. /// @@ -79,3 +80,17 @@ impl<'a> Deref for CowBytes<'a> { &self.0 } } + +impl Default for CowBytes<'_> { + fn default() -> Self { + CowBytes(Cow::Borrowed(&[])) + } +} + +impl Zeroize for CowBytes<'_> { + fn zeroize(&mut self) { + if !self.is_borrowed() { + self.0.to_mut().zeroize(); + } + } +} diff --git a/implementations/rust/ockam/ockam_core/src/lib.rs b/implementations/rust/ockam/ockam_core/src/lib.rs index a72fec734a6..c33954fa6c9 100644 --- a/implementations/rust/ockam/ockam_core/src/lib.rs +++ b/implementations/rust/ockam/ockam_core/src/lib.rs @@ -86,6 +86,7 @@ mod processor; mod routing; mod uint; mod worker; +mod zeroize; pub use access_control::*; pub use cbor::*; @@ -96,6 +97,7 @@ pub use processor::*; pub use routing::*; pub use uint::*; pub use worker::*; +pub use zeroize::*; #[cfg(all(not(feature = "std"), feature = "alloc"))] #[doc(hidden)] diff --git a/implementations/rust/ockam/ockam_core/src/message.rs b/implementations/rust/ockam/ockam_core/src/message.rs index ad08f0800e6..6334589b33c 100644 --- a/implementations/rust/ockam/ockam_core/src/message.rs +++ b/implementations/rust/ockam/ockam_core/src/message.rs @@ -1,3 +1,4 @@ +use crate::zeroize::MaybeZeroizeOnDrop; use crate::{ compat::{ string::{String, ToString}, @@ -244,7 +245,7 @@ impl Routed { /// Consume the message wrapper and return the original message. #[inline] pub fn into_body(self) -> Result { - M::decode(&self.into_payload()) + M::decode(self.payload()) } /// Consume the message wrapper and return the underlying local message. @@ -267,7 +268,7 @@ impl Routed { /// Consume the message wrapper and return the underlying transport message's binary payload. #[inline] - pub fn into_payload(self) -> Vec { + pub fn into_payload(self) -> MaybeZeroizeOnDrop> { self.local_msg.into_payload() } } diff --git a/implementations/rust/ockam/ockam_core/src/routing/message/local_message.rs b/implementations/rust/ockam/ockam_core/src/routing/message/local_message.rs index d387a74c589..55a89facdb3 100644 --- a/implementations/rust/ockam/ockam_core/src/routing/message/local_message.rs +++ b/implementations/rust/ockam/ockam_core/src/routing/message/local_message.rs @@ -1,7 +1,8 @@ #[cfg(feature = "std")] use crate::OpenTelemetryContext; -use crate::{compat::vec::Vec, route, Address, Message, Route, TransportMessage}; +use crate::{compat::vec::Vec, route, Address, Message, OnDrop, Route, TransportMessage}; +use crate::zeroize::MaybeZeroizeOnDrop; use crate::{LocalInfo, Result}; use cfg_if::cfg_if; use serde::{Deserialize, Serialize}; @@ -46,7 +47,7 @@ pub struct LocalMessage { /// Return message route. This field must be populated by routers handling this message along the way. pub return_route: Route, /// The message payload. - pub payload: Vec, + pub payload: MaybeZeroizeOnDrop>, /// Local information added by workers to give additional context to the message /// independently of its payload. For example this can be used to store the identifier that /// was used to encrypt the payload @@ -142,7 +143,7 @@ impl LocalMessage { } /// Return the message payload - pub fn into_payload(self) -> Vec { + pub fn into_payload(self) -> MaybeZeroizeOnDrop> { self.payload } @@ -153,12 +154,12 @@ impl LocalMessage { /// Return a mutable reference to the message payload pub fn payload_mut(&mut self) -> &mut Vec { - &mut self.payload + self.payload.as_mut() } /// Set the message payload pub fn set_payload(mut self, payload: Vec) -> Self { - self.payload = payload; + self.payload = MaybeZeroizeOnDrop::new(payload, OnDrop::NoZeroize); self } @@ -213,7 +214,7 @@ impl LocalMessage { 1, self.onward_route, self.return_route, - self.payload, + self.payload.discard_zeroize(), None, ); @@ -285,7 +286,7 @@ impl LocalMessage { LocalMessage { onward_route, return_route, - payload, + payload: MaybeZeroizeOnDrop::new(payload, OnDrop::NoZeroize), local_info, #[cfg(feature = "std")] tracing_context: OpenTelemetryContext::current(), @@ -316,7 +317,18 @@ impl LocalMessage { /// Specify the payload for the message pub fn with_payload(self, payload: Vec) -> Self { - Self { payload, ..self } + Self { + payload: MaybeZeroizeOnDrop::new(payload, OnDrop::NoZeroize), + ..self + } + } + + /// Specify the payload for the message with zeroization + pub fn with_payload_on_drop(self, payload: Vec, on_drop: OnDrop) -> Self { + Self { + payload: MaybeZeroizeOnDrop::new(payload, on_drop), + ..self + } } /// Specify the local information for the message diff --git a/implementations/rust/ockam/ockam_core/src/zeroize.rs b/implementations/rust/ockam/ockam_core/src/zeroize.rs new file mode 100644 index 00000000000..2ad7d844942 --- /dev/null +++ b/implementations/rust/ockam/ockam_core/src/zeroize.rs @@ -0,0 +1,176 @@ +use core::fmt::{Debug, Display, Formatter}; +use core::hash::{Hash, Hasher}; +use core::ops::Deref; +use minicbor::encode::{Error, Write}; +use minicbor::{CborLen, Decode, Encode, Encoder}; +use std::ops::DerefMut; +use zeroize::Zeroize; + +/// OnDrop is an enum to specify whether to zeroize the inner value when dropped. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode, CborLen)] +#[rustfmt::skip] +pub enum OnDrop { + /// Do not zeroize the inner value when dropped + #[n(0)] NoZeroize, + /// Zeroize the inner value when dropped + #[n(1)] Zeroize, +} + +/// MaybeZeroizeOnDrop will zeroize the inner value when dropped when zeroize_on_drop is true. +pub struct MaybeZeroizeOnDrop { + target: T, + on_drop: OnDrop, +} + +impl MaybeZeroizeOnDrop { + /// Create a new MaybeZeroizeOnDrop with the given target and zeroize_on_drop flag. + pub fn new(target: T, on_drop: OnDrop) -> Self { + Self { target, on_drop } + } + + /// Gets on_drop + pub fn on_drop(&self) -> OnDrop { + self.on_drop + } + + /// Sets on_drop + pub fn set_zeroize(&mut self, on_drop: OnDrop) { + self.on_drop = on_drop; + } +} + +impl MaybeZeroizeOnDrop { + /// Return the inner value regardless of the zeroize_on_drop flag. + /// The caller has the responsibility to ensure that the inner value is zeroized when necessary. + pub fn discard_zeroize(mut self) -> T { + std::mem::take(&mut self.target) + } +} + +impl Deref for MaybeZeroizeOnDrop { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.target + } +} + +impl DerefMut for MaybeZeroizeOnDrop { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.target + } +} + +impl Drop for MaybeZeroizeOnDrop { + fn drop(&mut self) { + if let OnDrop::Zeroize = self.on_drop { + self.target.zeroize(); + } + } +} + +impl Zeroize for MaybeZeroizeOnDrop { + fn zeroize(&mut self) { + self.target.zeroize(); + } +} + +impl Default for MaybeZeroizeOnDrop { + fn default() -> Self { + Self { + target: T::default(), + on_drop: OnDrop::NoZeroize, + } + } +} + +impl Debug for MaybeZeroizeOnDrop { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MaybeZeroizeOnDrop") + .field("target", &self.target) + .field("on_drop", &self.on_drop) + .finish() + } +} + +impl Display for MaybeZeroizeOnDrop { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.target.fmt(f) + } +} + +impl Clone for MaybeZeroizeOnDrop { + fn clone(&self) -> Self { + Self { + target: self.target.clone(), + on_drop: self.on_drop, + } + } +} + +impl PartialEq for MaybeZeroizeOnDrop { + fn eq(&self, other: &Self) -> bool { + self.target.eq(&other.target) + } +} + +impl Eq for MaybeZeroizeOnDrop {} + +impl PartialOrd for MaybeZeroizeOnDrop { + fn partial_cmp(&self, other: &Self) -> Option { + self.target.partial_cmp(&other.target) + } +} + +impl Ord for MaybeZeroizeOnDrop { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.target.cmp(&other.target) + } +} + +impl Hash for MaybeZeroizeOnDrop { + fn hash(&self, state: &mut H) { + self.target.hash(state) + } +} + +// serde serialize/deserialize impls +impl serde::Serialize for MaybeZeroizeOnDrop { + fn serialize(&self, serializer: S) -> Result { + self.target.serialize(serializer) + } +} + +impl<'de, T: Zeroize + serde::Deserialize<'de>> serde::Deserialize<'de> for MaybeZeroizeOnDrop { + fn deserialize>(deserializer: D) -> Result { + T::deserialize(deserializer).map(|target| Self { + target, + on_drop: OnDrop::NoZeroize, + }) + } +} + +impl> Encode for MaybeZeroizeOnDrop { + fn encode(&self, e: &mut Encoder, ctx: &mut C) -> Result<(), Error> { + self.target.encode(e, ctx) + } + + fn is_nil(&self) -> bool { + self.target.is_nil() + } +} + +impl<'b, C, T: Zeroize + Decode<'b, C>> Decode<'b, C> for MaybeZeroizeOnDrop { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + T::decode(d, ctx).map(|target| Self { + target, + on_drop: OnDrop::NoZeroize, + }) + } +} + +impl> CborLen for MaybeZeroizeOnDrop { + fn cbor_len(&self, ctx: &mut C) -> usize { + self.target.cbor_len(ctx) + } +} diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs index e50b1e91646..857a1208795 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs @@ -1,6 +1,6 @@ use core::sync::atomic::Ordering; use ockam_core::compat::sync::Arc; -use ockam_core::{route, Any, Result, Route, Routed, SecureChannelLocalInfo}; +use ockam_core::{route, Any, OnDrop, Result, Route, Routed, SecureChannelLocalInfo}; use ockam_core::{Decodable, LocalMessage}; use ockam_node::Context; @@ -21,6 +21,7 @@ use ockam_core::errcode::{Kind, Origin}; use ockam_vault::{AeadSecretKeyHandle, VaultForSecureChannels}; use tracing::{debug, info, trace, warn}; use tracing_attributes::instrument; +use zeroize::Zeroize; pub(crate) struct DecryptorHandler { //for debug purposes only @@ -144,7 +145,7 @@ impl DecryptorHandler { let msg = LocalMessage::new() .with_onward_route(msg.onward_route) .with_return_route(return_route) - .with_payload(msg.payload.to_vec()) + .with_payload_on_drop(msg.payload.to_vec(), msg.on_drop) .with_local_info(local_info); match ctx @@ -217,6 +218,8 @@ impl DecryptorHandler { // Decode raw payload binary let mut payload = msg.payload; + // it might contain sensitive data, so we zeroize it in *case of an error* + payload.set_zeroize(OnDrop::Zeroize); // Decrypt the binary let (decrypted_payload, nonce) = self.decryptor.decrypt(payload.as_mut_slice()).await?; @@ -224,6 +227,7 @@ impl DecryptorHandler { match decrypted_msg.message { SecureChannelMessage::Payload(decrypted_msg) => { + payload.set_zeroize(decrypted_msg.on_drop); self.handle_payload(ctx, decrypted_msg, nonce, encrypted_msg_return_route) .await? } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs index 66ac4a425f6..1123fffd86a 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs @@ -1,17 +1,17 @@ use core::sync::atomic::{AtomicBool, Ordering}; -use tracing::{debug, error, info, warn}; -use tracing_attributes::instrument; - use ockam_core::compat::boxed::Box; use ockam_core::compat::sync::{Arc, RwLock}; use ockam_core::compat::vec::Vec; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{ - async_trait, route, CowBytes, Decodable, Error, LocalMessage, NeutralMessage, Route, + async_trait, route, CowBytes, Decodable, Error, LocalMessage, MaybeZeroizeOnDrop, + NeutralMessage, OnDrop, Route, }; use ockam_core::{Any, Result, Routed, Worker}; use ockam_node::Context; +use tracing::{debug, error, info, warn}; +use tracing_attributes::instrument; use crate::models::CredentialAndPurposeKey; use crate::secure_channel::addresses::Addresses; @@ -98,13 +98,21 @@ impl EncryptorWorker { &mut self, ctx: &Context, msg: SecureChannelPaddedMessage<'static>, + on_drop: OnDrop, ) -> Result> { let expected_len = minicbor::len(&msg); - let mut destination = vec![0u8; NOISE_NONCE_LEN + expected_len + AES_GCM_TAGSIZE]; + let mut destination = MaybeZeroizeOnDrop::new( + vec![0u8; NOISE_NONCE_LEN + expected_len + AES_GCM_TAGSIZE], + on_drop, + ); minicbor::encode(&msg, &mut destination[NOISE_NONCE_LEN..])?; match self.encryptor.encrypt(&mut destination).await { - Ok(()) => Ok(destination), + Ok(()) => { + // the content of the destination is now encrypted, + // and we can safely return it as `Vec` + Ok(destination.discard_zeroize()) + } // If encryption failed, that means we have some internal error, // and we may be in an invalid state, it's better to stop the Worker Err(err) => { @@ -203,21 +211,24 @@ impl EncryptorWorker { let msg = msg.into_local_message(); let mut onward_route = msg.onward_route; let return_route = msg.return_route; + let on_drop = msg.payload.on_drop(); + let payload = + MaybeZeroizeOnDrop::new(CowBytes::from(msg.payload.discard_zeroize()), on_drop); // Remove our address let _ = onward_route.step(); - let payload = CowBytes::from(msg.payload); let msg = PlaintextPayloadMessage { onward_route, return_route, payload, + on_drop, }; let msg = SecureChannelMessage::Payload(msg); let msg = Self::add_padding(msg); - let payload = self.encrypt(ctx, msg).await?; + let payload = self.encrypt(ctx, msg, on_drop).await?; let remote_route = self.shared_state.remote_route.read().unwrap().route.clone(); // Decryptor doesn't need the return_route since it has `self.remote_route` as well @@ -288,7 +299,7 @@ impl EncryptorWorker { let msg = SecureChannelMessage::RefreshCredentials(msg); let msg = Self::add_padding(msg); - let msg = self.encrypt(ctx, msg).await?; + let msg = self.encrypt(ctx, msg, OnDrop::NoZeroize).await?; info!( "Sending credentials refresh for {}", @@ -314,7 +325,7 @@ impl EncryptorWorker { let msg = Self::add_padding(msg); // Encrypt the message - let msg = self.encrypt(ctx, msg).await?; + let msg = self.encrypt(ctx, msg, OnDrop::NoZeroize).await?; let remote_route = self.shared_state.remote_route.read().unwrap().route.clone(); // Send the message to the decryptor on the other side diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs index 40c8b3383a2..0a6865ff2c9 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs @@ -272,7 +272,7 @@ impl HandshakeWorker { .state_machine .as_mut() .ok_or(IdentityError::HandshakeInternalError)? - .on_event(ReceivedMessage(payload)) + .on_event(ReceivedMessage(payload.discard_zeroize())) .await? { // set the remote route by taking the most up to date message return route diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/message.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/message.rs index 9a58e0b08ad..6ede1285040 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/message.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/message.rs @@ -1,7 +1,7 @@ use crate::models::{ChangeHistory, CredentialAndPurposeKey}; use minicbor::{CborLen, Decode, Encode}; use ockam_core::compat::vec::Vec; -use ockam_core::{CowBytes, Route}; +use ockam_core::{CowBytes, MaybeZeroizeOnDrop, OnDrop, Route}; /// Secure Channel Message format. #[derive(Debug, Encode, Decode, CborLen, Clone)] @@ -34,7 +34,9 @@ pub struct PlaintextPayloadMessage<'a> { /// Return route of the message. #[n(1)] pub return_route: Route, /// Untyped binary payload. - #[b(2)] pub payload: CowBytes<'a>, + #[b(2)] pub payload: MaybeZeroizeOnDrop>, + /// Whether to Zeroize the payload on drop. + #[n(3)] pub on_drop: OnDrop, } /// Secure Channel Message format. diff --git a/implementations/rust/ockam/ockam_node/src/context/send_message.rs b/implementations/rust/ockam/ockam_node/src/context/send_message.rs index 5170f891c8b..f8f66733e5a 100644 --- a/implementations/rust/ockam/ockam_node/src/context/send_message.rs +++ b/implementations/rust/ockam/ockam_node/src/context/send_message.rs @@ -8,14 +8,15 @@ use ockam_core::compat::{sync::Arc, vec::Vec}; use ockam_core::{ errcode::{Kind, Origin}, route, Address, AllOutgoingAccessControl, AllowAll, AllowOnwardAddress, Error, - IncomingAccessControl, LocalMessage, Mailboxes, Message, OutgoingAccessControl, RelayMessage, - Result, Route, Routed, + IncomingAccessControl, LocalMessage, Mailboxes, Message, OnDrop, OutgoingAccessControl, + RelayMessage, Result, Route, Routed, }; use ockam_core::{LocalInfo, Mailbox}; /// Full set of options to `send_and_receive_extended` function pub struct MessageSendReceiveOptions { message_wait: MessageWait, + on_drop: OnDrop, incoming_access_control: Option>, outgoing_access_control: Option>, } @@ -31,6 +32,7 @@ impl MessageSendReceiveOptions { pub fn new() -> Self { Self { message_wait: MessageWait::Timeout(DEFAULT_TIMEOUT), + on_drop: OnDrop::NoZeroize, incoming_access_control: None, outgoing_access_control: None, } @@ -65,6 +67,12 @@ impl MessageSendReceiveOptions { self.outgoing_access_control = Some(outgoing_access_control); self } + + /// Set on drop behavior + pub fn with_on_drop(mut self, on_drop: OnDrop) -> Self { + self.on_drop = on_drop; + self + } } impl Context { @@ -150,7 +158,9 @@ impl Context { #[cfg(feature = "std")] child_ctx.set_tracing_context(self.tracing_context()); - child_ctx.send(route, msg).await?; + child_ctx + .send_from_address_impl(route, msg, self.address(), vec![], options.on_drop) + .await?; child_ctx .receive_extended::( MessageReceiveOptions::new().with_message_wait(options.message_wait), @@ -226,8 +236,14 @@ impl Context { R: Into, M: Message + Send + 'static, { - self.send_from_address_impl(route.into(), msg, self.address(), local_info) - .await + self.send_from_address_impl( + route.into(), + msg, + self.address(), + local_info, + OnDrop::NoZeroize, + ) + .await } /// Send a message to an address or via a fully-qualified route @@ -253,8 +269,14 @@ impl Context { R: Into, M: Message + Send + 'static, { - self.send_from_address_impl(route.into(), msg, sending_address, Vec::new()) - .await + self.send_from_address_impl( + route.into(), + msg, + sending_address, + Vec::new(), + OnDrop::NoZeroize, + ) + .await } async fn send_from_address_impl( @@ -263,6 +285,7 @@ impl Context { msg: M, sending_address: Address, local_info: Vec, + on_drop: OnDrop, ) -> Result<()> where M: Message + Send + 'static, @@ -305,13 +328,13 @@ impl Context { .with_tracing_context(self.tracing_context().update()) .with_onward_route(route) .with_return_route(route![sending_address.clone()]) - .with_payload(payload) + .with_payload_on_drop(payload, on_drop) .with_local_info(local_info); } else { let local_msg = LocalMessage::new() .with_onward_route(route) .with_return_route(route![sending_address.clone()]) - .with_payload(payload) + .with_payload_on_drop(payload, on_drop) .with_local_info(local_info); } } diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/transport_message.rs b/implementations/rust/ockam/ockam_transport_tcp/src/transport_message.rs index 10ab6f94963..2d86d61effe 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/transport_message.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/transport_message.rs @@ -64,12 +64,14 @@ impl From> for LocalMessage { } } -impl From for TcpTransportMessage<'_> { - fn from(value: LocalMessage) -> Self { +impl TryFrom for TcpTransportMessage<'_> { + type Error = ockam_core::Error; + + fn try_from(value: LocalMessage) -> Result { let transport_message = Self::new( value.onward_route, value.return_route, - CowBytes::from(value.payload), + CowBytes::from(value.payload.discard_zeroize()), None, ); @@ -77,9 +79,9 @@ impl From for TcpTransportMessage<'_> { if #[cfg(feature = "std")] { // make sure to pass the latest tracing context let new_tracing_context = LocalMessage::start_new_tracing_context(value.tracing_context.update(), "TcpTransportMessage"); - transport_message.with_tracing_context(new_tracing_context) + Ok(transport_message.with_tracing_context(new_tracing_context)) } else { - transport_message + Ok(transport_message) } } } diff --git a/implementations/rust/ockam/ockam_transport_tcp/src/workers/sender.rs b/implementations/rust/ockam/ockam_transport_tcp/src/workers/sender.rs index 9f5cbb0e9a1..62bbc4758f1 100644 --- a/implementations/rust/ockam/ockam_transport_tcp/src/workers/sender.rs +++ b/implementations/rust/ockam/ockam_transport_tcp/src/workers/sender.rs @@ -120,7 +120,7 @@ impl TcpSendWorker { fn serialize_message(&mut self, local_message: LocalMessage) -> Result<()> { // Create a message buffer with prepended length - let transport_message = TcpTransportMessage::from(local_message); + let transport_message = TcpTransportMessage::try_from(local_message)?; let expected_payload_len = minicbor::len(&transport_message); diff --git a/implementations/rust/ockam/ockam_transport_udp/src/messages/routing_message.rs b/implementations/rust/ockam/ockam_transport_udp/src/messages/routing_message.rs index e6f9edb16f5..f55e25418dc 100644 --- a/implementations/rust/ockam/ockam_transport_udp/src/messages/routing_message.rs +++ b/implementations/rust/ockam/ockam_transport_udp/src/messages/routing_message.rs @@ -75,12 +75,14 @@ impl From> for LocalMessage { } } -impl From for UdpRoutingMessage<'_> { - fn from(value: LocalMessage) -> Self { +impl TryFrom for UdpRoutingMessage<'_> { + type Error = ockam_core::Error; + + fn try_from(value: LocalMessage) -> Result { let routing_message = Self::new( value.onward_route, value.return_route, - CowBytes::from(value.payload), + CowBytes::from(value.payload.discard_zeroize()), None, ); @@ -88,9 +90,9 @@ impl From for UdpRoutingMessage<'_> { if #[cfg(feature = "std")] { // make sure to pass the latest tracing context let new_tracing_context = LocalMessage::start_new_tracing_context(value.tracing_context.update(), "UdpRoutingMessage"); - routing_message.with_tracing_context(new_tracing_context) + Ok(routing_message.with_tracing_context(new_tracing_context)) } else { - routing_message + Ok(routing_message) } } } diff --git a/implementations/rust/ockam/ockam_transport_udp/src/puncture/puncture/receiver.rs b/implementations/rust/ockam/ockam_transport_udp/src/puncture/puncture/receiver.rs index a49cd0bf8ac..baa87568194 100644 --- a/implementations/rust/ockam/ockam_transport_udp/src/puncture/puncture/receiver.rs +++ b/implementations/rust/ockam/ockam_transport_udp/src/puncture/puncture/receiver.rs @@ -294,7 +294,8 @@ impl Worker for UdpPunctureReceiverWorker { if &addr == self.addresses.remote_address() { let msg = msg.into_local_message(); let return_route = msg.return_route; - self.handle_peer(ctx, msg.payload, &return_route).await?; + self.handle_peer(ctx, msg.payload.discard_zeroize(), &return_route) + .await?; } else if &addr == self.addresses.heartbeat_address() { self.handle_heartbeat(ctx).await?; } else { diff --git a/implementations/rust/ockam/ockam_transport_udp/src/puncture/puncture/sender.rs b/implementations/rust/ockam/ockam_transport_udp/src/puncture/puncture/sender.rs index b17e88cb56f..ecc38761fef 100644 --- a/implementations/rust/ockam/ockam_transport_udp/src/puncture/puncture/sender.rs +++ b/implementations/rust/ockam/ockam_transport_udp/src/puncture/puncture/sender.rs @@ -38,7 +38,7 @@ impl UdpPunctureSenderWorker { let wrapped_payload = PunctureMessage::Payload { onward_route, return_route, - payload: msg.payload, + payload: msg.payload.discard_zeroize(), }; let msg = LocalMessage::new() diff --git a/implementations/rust/ockam/ockam_transport_udp/src/workers/sender.rs b/implementations/rust/ockam/ockam_transport_udp/src/workers/sender.rs index ad696e2e5ee..4e9c8a0f1ba 100644 --- a/implementations/rust/ockam/ockam_transport_udp/src/workers/sender.rs +++ b/implementations/rust/ockam/ockam_transport_udp/src/workers/sender.rs @@ -117,7 +117,7 @@ struct TransportMessagesIterator { impl TransportMessagesIterator { fn new(current_routing_number: RoutingNumber, local_message: LocalMessage) -> Result { - let routing_message = UdpRoutingMessage::from(local_message); + let routing_message = UdpRoutingMessage::try_from(local_message)?; let routing_message = ockam_core::cbor_encode_preallocate(routing_message)?; From c85eb64f83bad8d2886f7d6dd213c31851042e1c Mon Sep 17 00:00:00 2001 From: Davide Baldo Date: Mon, 23 Dec 2024 19:01:59 +0100 Subject: [PATCH 6/7] feat(rust): removed secure channel key exchange and persistency --- .../ockam_api/src/authority_node/authority.rs | 7 +- .../src/cli_state/secure_channels.rs | 7 +- .../ockam/ockam_api/src/cli_state/vaults.rs | 5 +- .../rust/ockam/ockam_api/src/nodes/service.rs | 1 - .../ockam_api/src/nodes/service/manager.rs | 2 - .../ockam_api/src/nodes/service/relay.rs | 2 - .../src/nodes/service/secure_channel.rs | 98 +---- .../service/tcp_inlets/session_replacer.rs | 2 - .../ockam_api/tests/credential_issuer.rs | 7 +- .../rust/ockam/ockam_api/tests/latency.rs | 2 - .../ockam_identity/src/secure_channel/api.rs | 55 +-- .../src/secure_channel/decryptor.rs | 174 +------- .../src/secure_channel/encryptor.rs | 21 +- .../src/secure_channel/encryptor_worker.rs | 70 +-- .../handshake/handshake_worker.rs | 127 +----- .../src/secure_channel/key_tracker.rs | 4 - .../src/secure_channel/listener.rs | 11 - .../ockam_identity/src/secure_channel/mod.rs | 3 +- .../src/secure_channel/options.rs | 46 -- .../src/secure_channels/common.rs | 22 +- .../src/secure_channels/secure_channels.rs | 151 +------ .../secure_channels_builder.rs | 6 +- .../ockam/ockam_identity/tests/channel.rs | 192 ++++----- .../ockam/ockam_identity/tests/persistence.rs | 408 ------------------ 24 files changed, 173 insertions(+), 1250 deletions(-) delete mode 100644 implementations/rust/ockam/ockam_identity/tests/persistence.rs diff --git a/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs b/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs index fe660ebf83d..d8792900e9a 100644 --- a/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs +++ b/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs @@ -12,8 +12,7 @@ use crate::authenticator::{ }; use ockam::identity::utils::now; use ockam::identity::{ - Identifier, Identities, SecureChannelListenerOptions, SecureChannelSqlxDatabase, - SecureChannels, TrustEveryonePolicy, + Identifier, Identities, SecureChannelListenerOptions, SecureChannels, TrustEveryonePolicy, }; use ockam::tcp::{TcpListenerOptions, TcpTransport}; use ockam_core::compat::sync::Arc; @@ -75,14 +74,12 @@ impl Authority { let members = Arc::new(AuthorityMembersSqlxDatabase::new(database.clone())); let tokens = Arc::new(AuthorityEnrollmentTokenSqlxDatabase::new(database.clone())); - let secure_channel_repository = Arc::new(SecureChannelSqlxDatabase::new(database.clone())); Self::bootstrap_repository(members.clone(), configuration).await?; let identities = Identities::create_with_node(database, node_name).build(); - let secure_channels = - SecureChannels::from_identities(identities.clone(), secure_channel_repository); + let secure_channels = SecureChannels::from_identities(identities.clone()); let identifier = configuration.identifier(); info!(identifier=%identifier, "retrieved the authority identifier"); diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs b/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs index 9abc218fae9..7ce2b06d5fa 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::cli_state::CliState; use crate::cli_state::Result; -use ockam::identity::{Identities, SecureChannelSqlxDatabase, SecureChannels}; +use ockam::identity::{Identities, SecureChannels}; use ockam_node::Context; impl CliState { @@ -17,9 +17,6 @@ impl CliState { let identities = Identities::create_with_node(self.database(), node_name) .with_vault(vault) .build(); - Ok(SecureChannels::from_identities( - identities, - Arc::new(SecureChannelSqlxDatabase::new(self.database())), - )) + Ok(SecureChannels::from_identities(identities)) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs b/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs index 6f252fa6984..2a3a0d73241 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs @@ -1,7 +1,7 @@ use colorful::Colorful; use ockam::identity::{ - Identifier, Identities, RemoteCredentialRetrieverInfo, SecureChannelRegistry, - SecureChannelSqlxDatabase, SecureChannels, Vault, + Identifier, Identities, RemoteCredentialRetrieverInfo, SecureChannelRegistry, SecureChannels, + Vault, }; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{AsyncTryClone, Error}; @@ -398,7 +398,6 @@ impl CliState { let secure_channels = Arc::new(SecureChannels::new( identities, SecureChannelRegistry::default(), //TODO: inherit registry from the node - Arc::new(SecureChannelSqlxDatabase::new(self.database())), )); let credential_retriever_creator = credential_retriever_options diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service.rs b/implementations/rust/ockam/ockam_api/src/nodes/service.rs index 03fed806739..5bb1929e5bd 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service.rs @@ -28,7 +28,6 @@ mod trust; mod worker; pub use manager::*; -pub use secure_channel::SecureChannelType; pub use trust::*; pub use worker::*; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs index 74d55bf820e..267e65d6923 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs @@ -10,7 +10,6 @@ use crate::nodes::registry::Registry; use crate::nodes::service::http::HttpServer; use crate::nodes::service::{ CredentialRetrieverCreators, CredentialRetrieverOptions, NodeManagerTrustOptions, - SecureChannelType, }; use crate::cli_state::journeys::{NODE_NAME, USER_EMAIL, USER_NAME}; @@ -205,7 +204,6 @@ impl NodeManager { None, // Not checking identifiers here in favor of credential check None, ctx, - SecureChannelType::KeyExchangeAndMessages, ) .await?; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs index af7b3429da1..6066e8fb49b 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs @@ -21,7 +21,6 @@ use crate::nodes::models::secure_channel::{ }; use crate::nodes::registry::RegistryRelayInfo; use crate::nodes::service::in_memory_node::InMemoryNode; -use crate::nodes::service::secure_channel::SecureChannelType; use crate::nodes::BackgroundNodeClient; use crate::session::replacer::{ReplacerOutcome, ReplacerOutputKind, SessionReplacer}; use crate::session::session::Session; @@ -440,7 +439,6 @@ impl SecureChannelsCreation for InMemoryNode { Some(vec![authorized]), credential, timeout, - SecureChannelType::KeyExchangeAndMessages, ) .await .into_diagnostic() diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs index 5826178aa9e..2d747064b86 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs @@ -1,13 +1,13 @@ use std::time::Duration; use ockam::identity::models::CredentialAndPurposeKey; +use ockam::identity::TrustEveryonePolicy; use ockam::identity::Vault; use ockam::identity::{ Identifier, Identities, SecureChannelListenerOptions, SecureChannelOptions, SecureChannels, TrustMultiIdentifiersPolicy, }; use ockam::identity::{SecureChannel, SecureChannelListener}; -use ockam::identity::{SecureChannelSqlxDatabase, TrustEveryonePolicy}; use ockam::{Address, Result, Route}; use ockam_core::api::{Error, Response}; use ockam_core::compat::sync::Arc; @@ -29,12 +29,6 @@ use crate::nodes::registry::SecureChannelInfo; use crate::nodes::service::default_address::DefaultAddress; use crate::nodes::{NodeManager, NodeManagerWorker}; -#[derive(PartialOrd, PartialEq, Debug)] -pub enum SecureChannelType { - KeyExchangeAndMessages, - KeyExchangeOnly, -} - /// SECURE CHANNELS impl NodeManagerWorker { pub async fn list_secure_channels(&self) -> Result>, Response> { @@ -64,7 +58,6 @@ impl NodeManagerWorker { authorized_identifiers, credential, timeout, - SecureChannelType::KeyExchangeAndMessages, ) .await .map(|secure_channel| { @@ -124,13 +117,7 @@ impl NodeManagerWorker { let response = self .node_manager - .create_secure_channel_listener( - addr, - authorized_identifiers, - identity_name, - ctx, - SecureChannelType::KeyExchangeAndMessages, - ) + .create_secure_channel_listener(addr, authorized_identifiers, identity_name, ctx) .await .map(|_| Response::ok())?; Ok(response) @@ -177,7 +164,6 @@ impl NodeManager { authorized_identifiers: Option>, credential: Option, timeout: Option, - secure_channel_type: SecureChannelType, ) -> Result { let identifier = self.get_identifier_by_name(identity_name.clone()).await?; @@ -192,7 +178,6 @@ impl NodeManager { authorized_identifiers, credential, timeout, - secure_channel_type, ) .await?; @@ -209,7 +194,6 @@ impl NodeManager { authorized_identifiers: Option>, credential: Option, timeout: Option, - secure_channel_type: SecureChannelType, ) -> Result { debug!(%sc_route, "Creating secure channel"); let options = SecureChannelOptions::new(); @@ -240,13 +224,6 @@ impl NodeManager { None => options.with_trust_policy(TrustEveryonePolicy), }; - let options = if secure_channel_type == SecureChannelType::KeyExchangeOnly { - // TODO: Should key exchange channels be persisted automatically? - options.key_exchange_only().persist()? - } else { - options - }; - let sc = self .secure_channels .create_secure_channel(ctx, identifier, sc_route.clone(), options) @@ -303,35 +280,12 @@ impl NodeManager { /// SECURE CHANNEL LISTENERS impl NodeManager { - //TODO: remove everything about key exchange service from secure channel - #[allow(dead_code)] - pub(crate) async fn start_key_exchanger_service( - &self, - context: &Context, - address: Address, - ) -> Result { - // skip creation if it already exists - if let Some(listener) = self.registry.secure_channel_listeners.get(&address).await { - return Ok(listener); - } - - self.create_secure_channel_listener( - address.clone(), - None, - None, - context, - SecureChannelType::KeyExchangeOnly, - ) - .await - } - pub async fn create_secure_channel_listener( &self, address: Address, authorized_identifiers: Option>, identity_name: Option, ctx: &Context, - secure_channel_type: SecureChannelType, ) -> Result { debug!( "Handling request to create a new secure channel listener: {}", @@ -381,13 +335,6 @@ impl NodeManager { } }; - let options = if secure_channel_type == SecureChannelType::KeyExchangeOnly { - // TODO: Should key exchange channels be persisted automatically? - options.key_exchange_only().persist()? - } else { - options - }; - let listener = secure_channels .create_secure_channel_listener(ctx, &identifier, address.clone(), options) .await?; @@ -399,27 +346,25 @@ impl NodeManager { .insert(address.clone(), listener.clone()) .await; - if secure_channel_type == SecureChannelType::KeyExchangeAndMessages { - // TODO: Clean - // Add Echoer as a consumer by default - ctx.flow_controls() - .add_consumer(DefaultAddress::ECHO_SERVICE, listener.flow_control_id()); - - // TODO: PUNCTURE Make optional? - ctx.flow_controls().add_consumer( - DefaultAddress::UDP_PUNCTURE_NEGOTIATION_LISTENER, - listener.flow_control_id(), - ); - - // Add ourselves to allow tunneling - ctx.flow_controls() - .add_consumer(address, listener.flow_control_id()); - - ctx.flow_controls().add_consumer( - DefaultAddress::UPPERCASE_SERVICE, - listener.flow_control_id(), - ); - } + // TODO: Clean + // Add Echoer as a consumer by default + ctx.flow_controls() + .add_consumer(DefaultAddress::ECHO_SERVICE, listener.flow_control_id()); + + // TODO: PUNCTURE Make optional? + ctx.flow_controls().add_consumer( + DefaultAddress::UDP_PUNCTURE_NEGOTIATION_LISTENER, + listener.flow_control_id(), + ); + + // Add ourselves to allow tunneling + ctx.flow_controls() + .add_consumer(address, listener.flow_control_id()); + + ctx.flow_controls().add_consumer( + DefaultAddress::UPPERCASE_SERVICE, + listener.flow_control_id(), + ); Ok(listener) } @@ -477,7 +422,6 @@ impl NodeManager { Ok(Arc::new(SecureChannels::new( identities, self.secure_channels.secure_channel_registry(), - Arc::new(SecureChannelSqlxDatabase::new(self.cli_state.database())), ))) } } diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/tcp_inlets/session_replacer.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/tcp_inlets/session_replacer.rs index 9fb91fe0052..ef7000b2945 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/tcp_inlets/session_replacer.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/tcp_inlets/session_replacer.rs @@ -20,7 +20,6 @@ use ockam_transport_tcp::TcpInlet; use crate::error::ApiError; use crate::nodes::connection::Connection; -use crate::nodes::service::SecureChannelType; use crate::nodes::NodeManager; use crate::session::replacer::{ AdditionalSessionReplacer, CurrentInletStatus, ReplacerOutcome, ReplacerOutputKind, @@ -356,7 +355,6 @@ impl AdditionalSessionReplacer for InletSessionReplacer { None, // TODO: Have a dedicated timeout Some(Duration::from_secs(10)), - SecureChannelType::KeyExchangeAndMessages, ) .await?; let additional_sc = self.additional_secure_channel.insert(additional_sc); diff --git a/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs b/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs index ec1bd9eef36..a771f302a40 100644 --- a/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs +++ b/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs @@ -1,7 +1,7 @@ use minicbor::bytes::ByteSlice; +use ockam::identity::identities; use ockam::identity::models::CredentialAndPurposeKey; use ockam::identity::utils::now; -use ockam::identity::{identities, SecureChannelSqlxDatabase}; use ockam::identity::{ Identities, SecureChannelListenerOptions, SecureChannelOptions, SecureChannels, }; @@ -55,10 +55,7 @@ async fn credential(ctx: &mut Context) -> Result<()> { .with_purpose_keys_repository(identities.purpose_keys_repository()) .with_cached_credential_repository(identities.cached_credentials_repository()) .build(); - let secure_channels = SecureChannels::from_identities( - identities.clone(), - Arc::new(SecureChannelSqlxDatabase::create().await?), - ); + let secure_channels = SecureChannels::from_identities(identities.clone()); let identities_verification = identities.identities_verification(); // Create the CredentialIssuer: diff --git a/implementations/rust/ockam/ockam_api/tests/latency.rs b/implementations/rust/ockam/ockam_api/tests/latency.rs index 7ed6b3d5a24..53e5128573d 100644 --- a/implementations/rust/ockam/ockam_api/tests/latency.rs +++ b/implementations/rust/ockam/ockam_api/tests/latency.rs @@ -1,6 +1,5 @@ #![recursion_limit = "256"] -use ockam_api::nodes::service::SecureChannelType; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -48,7 +47,6 @@ pub fn measure_message_latency_two_nodes() -> ockam_core::Result<()> { None, None, None, - SecureChannelType::KeyExchangeAndMessages, ) .await .unwrap(); diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs index bd3e3b94831..a0fc95b3125 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs @@ -1,47 +1,20 @@ -use ockam_core::compat::vec::Vec; -use ockam_core::Error; -use ockam_core::Message; -use serde::{Deserialize, Serialize}; +use minicbor::{CborLen, Decode, Encode}; +use ockam_vault::AeadSecretKeyHandle; -/// Request type for `EncryptorWorker` API Address -#[derive(Serialize, Deserialize, Message)] -pub enum EncryptionRequest { - /// Encrypt data - Encrypt(Vec), - /// Trigger a manual rekey - Rekey, - /// Derive new key - DeriveNewKey, +/// Request type for `SecureChannel` API Address +#[derive(Encode, Decode, CborLen)] +#[rustfmt::skip] +pub enum SecureChannelApiRequest { + /// Derive a new key from current key and shutdown the worker + #[n(0)] ExtractKey, } -/// Response type for `EncryptorWorker` API Address -#[derive(Serialize, Deserialize, Message)] -pub enum EncryptionResponse { +/// Response type for `SecureChannel` API Address +#[derive(Encode, Decode, CborLen)] +#[rustfmt::skip] +pub enum SecureChannelApiResponse { /// Success - Ok(Vec), + #[n(0)] Ok(#[n(0)] AeadSecretKeyHandle), /// Error - Err(Error), -} - -/// Request type for `Decryptor` API Address (the `Decryptor` is accessible through the `HandshakeWorker`) -#[derive(Serialize, Deserialize, Message)] -pub enum DecryptionRequest { - /// Decrypt data - Decrypt { - /// Ciphertext to decrypt - ciphertext: Vec, - /// Rekey counter - rekey_counter: Option, - }, - /// Derive new key - DeriveNewKey, -} - -/// Response type for `Decryptor` API Address (the `Decryptor` is accessible through the `HandshakeWorker`) -#[derive(Serialize, Deserialize, Message)] -pub enum DecryptionResponse { - /// Success - Ok(Vec), - /// Error - Err(Error), + #[n(1)] Err(#[n(0)] String), } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs index 857a1208795..9971eb9c5ad 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs @@ -1,7 +1,7 @@ use core::sync::atomic::Ordering; use ockam_core::compat::sync::Arc; +use ockam_core::{cbor_encode_preallocate, LocalMessage, NeutralMessage}; use ockam_core::{route, Any, OnDrop, Result, Route, Routed, SecureChannelLocalInfo}; -use ockam_core::{Decodable, LocalMessage}; use ockam_node::Context; use crate::models::Identifier; @@ -11,17 +11,15 @@ use crate::secure_channel::key_tracker::KeyTracker; use crate::secure_channel::nonce_tracker::NonceTracker; use crate::secure_channel::{Addresses, Role}; use crate::{ - DecryptionRequest, DecryptionResponse, Identities, IdentityError, Nonce, - PlaintextPayloadMessage, RefreshCredentialsMessage, SecureChannelMessage, + Identities, IdentityError, Nonce, PlaintextPayloadMessage, RefreshCredentialsMessage, + SecureChannelApiRequest, SecureChannelApiResponse, SecureChannelMessage, SecureChannelPaddedMessage, NOISE_NONCE_LEN, }; use crate::secure_channel::encryptor_worker::SecureChannelSharedState; -use ockam_core::errcode::{Kind, Origin}; use ockam_vault::{AeadSecretKeyHandle, VaultForSecureChannels}; use tracing::{debug, info, trace, warn}; use tracing_attributes::instrument; -use zeroize::Zeroize; pub(crate) struct DecryptorHandler { //for debug purposes only @@ -41,24 +39,17 @@ impl DecryptorHandler { identities: Arc, authority: Option, role: Role, - key_exchange_only: bool, addresses: Addresses, key: AeadSecretKeyHandle, vault: Arc, their_identity_id: Identifier, shared_state: SecureChannelSharedState, ) -> Self { - let decryptor = if key_exchange_only { - Decryptor::new_naive(key, vault) - } else { - Decryptor::new(key, vault) - }; - Self { role, addresses, their_identity_id, - decryptor, + decryptor: Decryptor::new(key, vault), identities, authority, shared_state, @@ -66,11 +57,7 @@ impl DecryptorHandler { } #[instrument(skip_all)] - pub(crate) async fn handle_decrypt_api( - &mut self, - ctx: &mut Context, - msg: Routed, - ) -> Result<()> { + pub(crate) async fn handle_api(&mut self, ctx: &mut Context, msg: Routed) -> Result<()> { trace!( "SecureChannel {} received Decrypt API {}", self.role, @@ -81,34 +68,22 @@ impl DecryptorHandler { let return_route = msg.return_route; // Decode raw payload binary - let request = DecryptionRequest::decode(&msg.payload)?; + let request = minicbor::decode(msg.payload.as_slice())?; let response = match request { - DecryptionRequest::Decrypt { - mut ciphertext, - rekey_counter, - } => { - let decrypted_payload = if let Some(rekey_counter) = rekey_counter { - self.decryptor - .decrypt_with_rekey_counter(&mut ciphertext, rekey_counter) - .await - } else { - self.decryptor.decrypt(&mut ciphertext).await - }; - - match decrypted_payload { - Ok((payload, _nonce)) => DecryptionResponse::Ok(payload.to_vec()), - Err(err) => DecryptionResponse::Err(err), - } - } - DecryptionRequest::DeriveNewKey => { - todo!() + SecureChannelApiRequest::ExtractKey => { + let handle = self.decryptor.derive_new_key().await?; + SecureChannelApiResponse::Ok(handle) } }; + let response = NeutralMessage::from(cbor_encode_preallocate(&response)?); // Send reply to the caller ctx.send_from_address(return_route, response, self.addresses.decryptor_api.clone()) .await?; + // Once we have extracted the key, we can't use it anymore + ctx.stop_worker(self.addresses.encryptor.clone()).await?; + Ok(()) } @@ -227,7 +202,7 @@ impl DecryptorHandler { match decrypted_msg.message { SecureChannelMessage::Payload(decrypted_msg) => { - payload.set_zeroize(decrypted_msg.on_drop); + //TODO: payload.set_zeroize(decrypted_msg.on_drop); self.handle_payload(ctx, decrypted_msg, nonce, encrypted_msg_return_route) .await? } @@ -249,8 +224,7 @@ impl DecryptorHandler { pub(crate) struct Decryptor { vault: Arc, key_tracker: KeyTracker, - nonce_tracker: Option, - rekey_cache: Option<(u16, AeadSecretKeyHandle)>, + nonce_tracker: NonceTracker, } impl Decryptor { @@ -258,18 +232,7 @@ impl Decryptor { Self { vault, key_tracker: KeyTracker::new(key, KEY_RENEWAL_INTERVAL), - nonce_tracker: Some(NonceTracker::new()), - rekey_cache: None, - } - } - - /// Creates a new Decryptor without rekeying and nonce tracking - pub fn new_naive(key: AeadSecretKeyHandle, vault: Arc) -> Self { - Self { - vault, - key_tracker: KeyTracker::new(key, KEY_RENEWAL_INTERVAL), - nonce_tracker: None, - rekey_cache: None, + nonce_tracker: NonceTracker::new(), } } @@ -280,16 +243,11 @@ impl Decryptor { } let nonce = Nonce::try_from(&payload[..NOISE_NONCE_LEN])?; - let nonce_tracker = if let Some(nonce_tracker) = &self.nonce_tracker { - Some(nonce_tracker.mark(nonce)?) - } else { - None - }; + let nonce_tracker = self.nonce_tracker.mark(nonce)?; let rekey_key; - let rekeying = self.nonce_tracker.is_some(); - let key = if rekeying { + let key = // get the key corresponding to the current nonce and // rekey if necessary if let Some(key) = self.key_tracker.get_key(nonce)? { @@ -297,10 +255,7 @@ impl Decryptor { } else { rekey_key = self.vault.rekey(&self.key_tracker.current_key, 1).await?; &rekey_key - } - } else { - &self.key_tracker.current_key - }; + }; // to improve protection against connection disruption attacks, we want to validate the // message with a decryption _before_ committing to the new state @@ -328,89 +283,8 @@ impl Decryptor { } #[instrument(skip_all)] - pub async fn decrypt_with_rekey_counter<'a>( - &mut self, - payload: &'a mut [u8], - rekey_counter: u16, - ) -> Result<(&'a [u8], Nonce)> { - if payload.len() < 8 { - return Err(IdentityError::InvalidNonce)?; - } - - let nonce = Nonce::try_from(&payload[..8])?; - let nonce_tracker = if let Some(nonce_tracker) = &self.nonce_tracker { - Some(nonce_tracker.mark(nonce)?) - } else { - None - }; - - let key_handle = - if let Some((cached_rekey_counter, cached_key_handle)) = self.rekey_cache.clone() { - if cached_rekey_counter == rekey_counter { - Some(cached_key_handle) - } else { - self.rekey_cache = None; - self.vault - .delete_aead_secret_key(cached_key_handle.clone()) - .await?; - None - } - } else { - None - }; - - let key_handle = match key_handle { - Some(key) => key, - None => { - let current_number_of_rekeys = self.key_tracker.number_of_rekeys(); - if current_number_of_rekeys > rekey_counter as u64 { - return Err(ockam_core::Error::new( - Origin::Channel, - Kind::Invalid, - "cannot rekey backwards", - )); - } else if current_number_of_rekeys > u16::MAX as u64 { - return Err(ockam_core::Error::new( - Origin::Channel, - Kind::Invalid, - "rekey counter overflow", - )); - } else { - let n_rekying = rekey_counter - current_number_of_rekeys as u16; - if n_rekying > 0 { - let key_handle = self - .vault - .rekey(&self.key_tracker.current_key, n_rekying) - .await?; - self.rekey_cache = Some((rekey_counter, key_handle.clone())); - key_handle - } else { - self.key_tracker.current_key.clone() - } - } - } - }; - - // to improve protection against connection disruption attacks, we want to validate the - // message with a decryption _before_ committing to the new state - let result = self - .vault - .aead_decrypt( - &key_handle, - &mut payload[NOISE_NONCE_LEN..], - &nonce.to_aes_gcm_nonce(), - &[], - ) - .await; - - if result.is_ok() { - self.nonce_tracker = nonce_tracker; - if let Some(key_to_delete) = self.key_tracker.update_key(&key_handle)? { - self.vault.delete_aead_secret_key(key_to_delete).await?; - } - } - - result.map(|payload| (&*payload, nonce)) + pub async fn derive_new_key(&mut self) -> Result { + self.vault.rekey(&self.key_tracker.current_key, 1).await } /// Remove the channel keys on shutdown @@ -426,12 +300,6 @@ impl Decryptor { .await?; }; - if let Some((_, key_handle)) = &self.rekey_cache { - self.vault - .delete_aead_secret_key(key_handle.clone()) - .await?; - } - Ok(()) } } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs index 7a6fa90cec7..8723cb896d3 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs @@ -10,7 +10,6 @@ pub(crate) struct Encryptor { key: AeadSecretKeyHandle, nonce: Nonce, vault: Arc, - rekeying: bool, } // To simplify the implementation, we use the same constant for the size of the message @@ -25,10 +24,7 @@ impl Encryptor { self.nonce.increment()?; - if self.rekeying - && current_nonce.value() > 0 - && current_nonce.value() % KEY_RENEWAL_INTERVAL == 0 - { + if current_nonce.value() > 0 && current_nonce.value() % KEY_RENEWAL_INTERVAL == 0 { let new_key = self.vault.rekey(&self.key, 1).await?; let old_key = core::mem::replace(&mut self.key, new_key); self.vault.delete_aead_secret_key(old_key).await?; @@ -49,25 +45,16 @@ impl Encryptor { } #[instrument(skip_all)] - pub async fn manual_rekey(&mut self) -> Result<()> { - let new_key = self.vault.rekey(&self.key, 1).await?; - let old_key = core::mem::replace(&mut self.key, new_key); - self.vault.delete_aead_secret_key(old_key).await?; - Ok(()) + pub async fn derive_new_key(&mut self) -> Result { + self.vault.rekey(&self.key, 1).await } pub fn new( key: AeadSecretKeyHandle, nonce: Nonce, vault: Arc, - rekeying: bool, ) -> Self { - Self { - key, - nonce, - vault, - rekeying, - } + Self { key, nonce, vault } } #[instrument(skip_all)] diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs index 1123fffd86a..e7e65659417 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs @@ -1,11 +1,10 @@ use core::sync::atomic::{AtomicBool, Ordering}; - use ockam_core::compat::boxed::Box; use ockam_core::compat::sync::{Arc, RwLock}; use ockam_core::compat::vec::Vec; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{ - async_trait, route, CowBytes, Decodable, Error, LocalMessage, MaybeZeroizeOnDrop, + async_trait, cbor_encode_preallocate, route, CowBytes, Error, LocalMessage, MaybeZeroizeOnDrop, NeutralMessage, OnDrop, Route, }; use ockam_core::{Any, Result, Routed, Worker}; @@ -15,7 +14,7 @@ use tracing_attributes::instrument; use crate::models::CredentialAndPurposeKey; use crate::secure_channel::addresses::Addresses; -use crate::secure_channel::api::{EncryptionRequest, EncryptionResponse}; +use crate::secure_channel::api::{SecureChannelApiRequest, SecureChannelApiResponse}; use crate::secure_channel::encryptor::Encryptor; use crate::secure_channel::handshake::handshake::AES_GCM_TAGSIZE; use crate::{ @@ -57,7 +56,6 @@ pub(crate) struct SecureChannelSharedState { pub(crate) struct EncryptorWorker { role: &'static str, // For debug purposes only - key_exchange_only: bool, addresses: Addresses, encryptor: Encryptor, my_identifier: Identifier, @@ -71,7 +69,6 @@ impl EncryptorWorker { #[allow(clippy::too_many_arguments)] pub fn new( role: &'static str, - key_exchange_only: bool, addresses: Addresses, encryptor: Encryptor, my_identifier: Identifier, @@ -82,7 +79,6 @@ impl EncryptorWorker { ) -> Self { Self { role, - key_exchange_only, addresses, encryptor, my_identifier, @@ -125,7 +121,7 @@ impl EncryptorWorker { } #[instrument(skip_all)] - async fn handle_encrypt_api( + async fn handle_api( &mut self, ctx: &mut ::Context, msg: Routed<::Message>, @@ -139,60 +135,24 @@ impl EncryptorWorker { let return_route = msg.return_route; // Decode raw payload binary - let request = EncryptionRequest::decode(&msg.payload)?; + let request = minicbor::decode(msg.payload.as_slice())?; // If encryption fails, that means we have some internal error, // and we may be in an invalid state, it's better to stop the Worker - let mut should_stop = false; let response = match request { - EncryptionRequest::Encrypt(plaintext) => { - let len = NOISE_NONCE_LEN + plaintext.len() + AES_GCM_TAGSIZE; - let mut encrypted_payload = vec![0u8; len]; - encrypted_payload[NOISE_NONCE_LEN..len - AES_GCM_TAGSIZE] - .copy_from_slice(&plaintext); - - // Encrypt the message - match self - .encryptor - .encrypt(encrypted_payload.as_mut_slice()) - .await - { - Ok(()) => EncryptionResponse::Ok(encrypted_payload), - // If encryption failed, that means we have some internal error, - // and we may be in an invalid state, it's better to stop the Worker - Err(err) => { - should_stop = true; - error!( - "Error while encrypting: {err} at: {}", - self.addresses.encryptor - ); - EncryptionResponse::Err(err) - } - } - } - EncryptionRequest::Rekey => match self.encryptor.manual_rekey().await { - Ok(()) => EncryptionResponse::Ok(Vec::new()), - Err(err) => { - should_stop = true; - error!( - "Error while rekeying: {err} at: {}", - self.addresses.encryptor - ); - EncryptionResponse::Err(err) - } - }, - EncryptionRequest::DeriveNewKey => { - todo!() + SecureChannelApiRequest::ExtractKey => { + let handle = self.encryptor.derive_new_key().await?; + SecureChannelApiResponse::Ok(handle) } }; + let response = NeutralMessage::from(cbor_encode_preallocate(&response)?); // Send the reply to the caller ctx.send_from_address(return_route, response, self.addresses.encryptor_api.clone()) .await?; - if should_stop { - ctx.stop_worker(self.addresses.encryptor.clone()).await?; - } + // Once we have extracted the key, we can't use it anymore + ctx.stop_worker(self.addresses.encryptor.clone()).await?; Ok(()) } @@ -374,16 +334,10 @@ impl Worker for EncryptorWorker { ) -> Result<()> { let msg_addr = msg.msg_addr(); - if self.key_exchange_only { - if msg_addr == self.addresses.encryptor_api { - self.handle_encrypt_api(ctx, msg).await?; - } else { - return Err(IdentityError::UnknownChannelMsgDestination)?; - } - } else if msg_addr == self.addresses.encryptor { + if msg_addr == self.addresses.encryptor { self.handle_encrypt(ctx, msg).await?; } else if msg_addr == self.addresses.encryptor_api { - self.handle_encrypt_api(ctx, msg).await?; + self.handle_api(ctx, msg).await?; } else if msg_addr == self.addresses.encryptor_internal { self.handle_refresh_credentials(ctx).await?; } else { diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs index 0a6865ff2c9..ba09fd1ef81 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs @@ -10,8 +10,7 @@ use ockam_core::{ use ockam_core::{Result, Worker}; use ockam_node::callback::CallbackSender; use ockam_node::{Context, WorkerBuilder}; -use ockam_vault::AeadSecretKeyHandle; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info}; use tracing_attributes::instrument; use crate::models::Identifier; @@ -31,9 +30,8 @@ use crate::secure_channel::handshake::initiator_state_machine::InitiatorStateMac use crate::secure_channel::handshake::responder_state_machine::ResponderStateMachine; use crate::secure_channel::{Addresses, Role}; use crate::{ - ChangeHistoryRepository, CredentialRetriever, IdentityError, PersistedSecureChannel, - SecureChannelPurposeKey, SecureChannelRegistryEntry, SecureChannelRepository, SecureChannels, - TrustPolicy, + ChangeHistoryRepository, CredentialRetriever, IdentityError, SecureChannelPurposeKey, + SecureChannelRegistryEntry, SecureChannels, TrustPolicy, }; /// This struct implements a Worker receiving and sending messages @@ -45,7 +43,6 @@ pub(crate) struct HandshakeWorker { my_identifier: Identifier, addresses: Addresses, role: Role, - key_exchange_only: bool, remote_route: Option, decryptor_handler: Option, @@ -54,8 +51,6 @@ pub(crate) struct HandshakeWorker { credential_retriever: Option>, - secure_channel_repository: Option>, - shared_state: SecureChannelSharedState, } @@ -145,8 +140,6 @@ impl HandshakeWorker { remote_route: Option, timeout: Option, role: Role, - key_exchange_only: bool, - secure_channel_repository: Option>, encryptor_remote_route: Arc>, ) -> Result> { let vault = secure_channels.identities.vault().secure_channel_vault; @@ -197,14 +190,12 @@ impl HandshakeWorker { state_machine: Some(state_machine), my_identifier: my_identifier.clone(), role, - key_exchange_only, remote_route: remote_route.clone(), addresses: addresses.clone(), decryptor_handler: None, credential_retriever, authority, change_history_repository: identities.change_history_repository(), - secure_channel_repository, shared_state, }; @@ -318,16 +309,10 @@ impl HandshakeWorker { let decryptor_handler = self.decryptor_handler.as_mut().unwrap(); let msg_addr = message.msg_addr(); - if self.key_exchange_only { - if msg_addr == self.addresses.decryptor_api { - decryptor_handler.handle_decrypt_api(context, message).await - } else { - Err(IdentityError::UnknownChannelMsgDestination)? - } - } else if msg_addr == self.addresses.decryptor_remote { + if msg_addr == self.addresses.decryptor_remote { decryptor_handler.handle_decrypt(context, message).await } else if msg_addr == self.addresses.decryptor_api { - decryptor_handler.handle_decrypt_api(context, message).await + decryptor_handler.handle_api(context, message).await } else { Err(IdentityError::UnknownChannelMsgDestination)? } @@ -387,7 +372,6 @@ impl HandshakeWorker { self.secure_channels.identities.clone(), self.authority.clone(), self.role, - self.key_exchange_only, self.addresses.clone(), handshake_results.handshake_keys.decryption_key.clone(), self.secure_channels.identities.vault().secure_channel_vault, @@ -397,27 +381,18 @@ impl HandshakeWorker { // create a separate encryptor worker which will be started independently { - let (rekeying, credential_retriever) = if self.key_exchange_only { - // only the initial exchange is needed for key exchange only - (false, None) - } else { - (true, self.credential_retriever.clone()) - }; - self.shared_state.remote_route.write().unwrap().route = self.remote_route()?; let encryptor = EncryptorWorker::new( self.role.str(), - self.key_exchange_only, self.addresses.clone(), Encryptor::new( handshake_results.handshake_keys.encryption_key, 0.into(), self.secure_channels.identities.vault().secure_channel_vault, - rekeying, ), self.my_identifier.clone(), self.change_history_repository.clone(), - credential_retriever, + self.credential_retriever.clone(), handshake_results.presented_credential, self.shared_state.clone(), ); @@ -453,12 +428,6 @@ impl HandshakeWorker { .await?; } - self.persist( - their_identifier, - &handshake_results.handshake_keys.decryption_key, - ) - .await; - info!( "Initialized SecureChannel {} at local: {}, remote: {}", self.role.str(), @@ -490,88 +459,4 @@ impl HandshakeWorker { Ok(decryptor) } - - async fn persist(&self, their_identifier: Identifier, decryption_key: &AeadSecretKeyHandle) { - let Some(repository) = &self.secure_channel_repository else { - info!( - "Skipping persistence. Local: {}, Remote: {}", - self.addresses.encryptor, &self.addresses.decryptor_remote - ); - return; - }; - - let sc = PersistedSecureChannel::new( - self.role, - self.my_identifier.clone(), - their_identifier, - self.addresses.decryptor_remote.clone(), - self.addresses.decryptor_api.clone(), - decryption_key.clone(), - ); - match repository.put(sc).await { - Ok(_) => { - info!( - "Successfully persisted secure channel. Local: {}, Remote: {}", - self.addresses.encryptor, &self.addresses.decryptor_remote, - ); - } - Err(err) => { - warn!( - "Error while persisting secure channel: {err}. Local: {}, Remote: {}", - self.addresses.encryptor, &self.addresses.decryptor_remote - ); - - return; - } - } - - if let Err(err) = self - .secure_channels - .identities - .vault() - .secure_channel_vault - .persist_aead_key(decryption_key) - .await - { - warn!( - "Error persisting secure channel key: {err}. Local: {}, Remote: {}", - self.addresses.encryptor, &self.addresses.decryptor_remote - ); - }; - } - - #[allow(clippy::too_many_arguments)] - pub(crate) fn new( - secure_channels: Arc, - callback_sender: Option>, - state_machine: Option>, - my_identifier: Identifier, - addresses: Addresses, - role: Role, - key_exchange_only: bool, - remote_route: Option, - decryptor_handler: Option, - authority: Option, - change_history_repository: Arc, - credential_retriever: Option>, - secure_channel_repository: Option>, - shared_state: SecureChannelSharedState, - ) -> Self { - Self { - secure_channels, - callback_sender, - state_machine, - my_identifier, - addresses, - role, - key_exchange_only, - remote_route, - decryptor_handler, - authority, - change_history_repository, - credential_retriever, - secure_channel_repository, - shared_state, - } - } } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/key_tracker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/key_tracker.rs index d600a4c7837..d0c48339e65 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/key_tracker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/key_tracker.rs @@ -23,10 +23,6 @@ impl KeyTracker { renewal_interval, } } - - pub(crate) fn number_of_rekeys(&self) -> u64 { - self.number_of_rekeys - } } impl KeyTracker { diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/listener.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/listener.rs index 31a9838360e..6c5f62c4b62 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/listener.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/listener.rs @@ -10,13 +10,11 @@ use crate::secure_channel::handshake_worker::HandshakeWorker; use crate::secure_channel::options::SecureChannelListenerOptions; use crate::secure_channel::role::Role; use crate::secure_channels::secure_channels::SecureChannels; -use crate::SecureChannelRepository; pub(crate) struct SecureChannelListenerWorker { secure_channels: Arc, identifier: Identifier, options: SecureChannelListenerOptions, - secure_channel_repository: Option>, } impl SecureChannelListenerWorker { @@ -25,17 +23,10 @@ impl SecureChannelListenerWorker { identifier: Identifier, options: SecureChannelListenerOptions, ) -> Self { - let secure_channel_repository = if options.is_persistent { - Some(secure_channels.secure_channel_repository()) - } else { - None - }; - Self { secure_channels, identifier, options, - secure_channel_repository, } } @@ -110,8 +101,6 @@ impl Worker for SecureChannelListenerWorker { None, None, Role::Responder, - self.options.key_exchange_only, - self.secure_channel_repository.clone(), RemoteRoute::create(), ) .await?; diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/mod.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/mod.rs index 438cf5396fa..92f90966911 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/mod.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/mod.rs @@ -21,7 +21,6 @@ pub mod trust_policy; pub use access_control::*; pub(crate) use addresses::*; pub use api::*; -pub(crate) use decryptor::*; pub(crate) use encryptor_worker::*; pub(crate) use handshake::*; pub(crate) use listener::*; @@ -194,7 +193,7 @@ mod tests { let key_on_v2 = vault2.convert_secret_buffer_to_aead_key(key_on_v2).await?; Ok(( - Encryptor::new(key_on_v1, 0.into(), vault1, true), + Encryptor::new(key_on_v1, 0.into(), vault1), Decryptor::new(key_on_v2, vault2), )) } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/options.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/options.rs index 9166ee4e11d..4489525beb8 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/options.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/options.rs @@ -34,9 +34,6 @@ pub struct SecureChannelOptions { // To obtain our credentials pub(crate) credential_retriever_creator: Option>, pub(crate) timeout: Duration, - pub(crate) key_exchange_only: bool, - // Secure Channel will be persisted (currently only supported for key_exchange_only = true) - pub(crate) is_persistent: bool, } impl fmt::Debug for SecureChannelOptions { @@ -55,8 +52,6 @@ impl SecureChannelOptions { authority: None, credential_retriever_creator: None, timeout: DEFAULT_TIMEOUT, - key_exchange_only: false, - is_persistent: false, } } @@ -101,24 +96,6 @@ impl SecureChannelOptions { pub fn producer_flow_control_id(&self) -> FlowControlId { self.flow_control_id.clone() } - - /// The secure channel will be used to exchange key only. - /// In this mode, the secure channel cannot be used to exchange messages, and key rotation - /// is disabled along with automatic credential refresh. - pub fn key_exchange_only(mut self) -> Self { - self.key_exchange_only = true; - self - } - - /// Secure Channel will be persisted after a successful handshake - /// NOTE: Currently only supported after setting key_exchange_only = true - pub fn persist(mut self) -> Result { - if !self.key_exchange_only { - return Err(IdentityError::PersistentSupportIsLimited.into()); - } - self.is_persistent = true; - Ok(self) - } } impl SecureChannelOptions { @@ -182,9 +159,6 @@ pub struct SecureChannelListenerOptions { pub(crate) authority: Option, // To obtain our credentials pub(crate) credential_retriever_creator: Option>, - pub(crate) key_exchange_only: bool, - // Secure Channel will be persisted (currently only supported for key_exchange_only = true) - pub(crate) is_persistent: bool, } impl fmt::Debug for SecureChannelListenerOptions { @@ -205,8 +179,6 @@ impl SecureChannelListenerOptions { trust_policy: Arc::new(TrustEveryonePolicy), authority: None, credential_retriever_creator: None, - key_exchange_only: false, - is_persistent: false, } } @@ -254,24 +226,6 @@ impl SecureChannelListenerOptions { pub fn spawner_flow_control_id(&self) -> FlowControlId { self.flow_control_id.clone() } - - /// The listener will be used to exchange key only. - /// In this mode, the secure channel cannot be used to exchange messages, and key rotation - /// is disabled along with automatic credential refresh. - pub fn key_exchange_only(mut self) -> Self { - self.key_exchange_only = true; - self - } - - /// Secure Channel will be persisted after a successful handshake - /// NOTE: Currently only supported after setting key_exchange_only = true - pub fn persist(mut self) -> Result { - if !self.key_exchange_only { - return Err(IdentityError::PersistentSupportIsLimited.into()); - } - self.is_persistent = true; - Ok(self) - } } impl SecureChannelListenerOptions { diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channels/common.rs b/implementations/rust/ockam/ockam_identity/src/secure_channels/common.rs index 5171ff0acc4..5c27af6d864 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channels/common.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channels/common.rs @@ -15,7 +15,6 @@ pub struct SecureChannel { their_identifier: Identifier, encryptor_remote_route: Arc>, addresses: Addresses, - is_key_exchange_only: bool, flow_control_id: FlowControlId, } @@ -42,7 +41,6 @@ impl SecureChannel { their_identifier: Identifier, encryptor_remote_route: Arc>, addresses: Addresses, - is_key_exchange_only: bool, flow_control_id: FlowControlId, ) -> Self { Self { @@ -50,7 +48,6 @@ impl SecureChannel { their_identifier, encryptor_remote_route, addresses, - is_key_exchange_only, flow_control_id, } } @@ -107,11 +104,7 @@ impl SecureChannel { Ok(()) } - /// This secure channel is used only for handshake, further encryption happens using - /// api address. Encryption part may be absent. - pub fn is_key_exchange_only(&self) -> bool { - self.is_key_exchange_only - } + /// The Identifier of the other side pub fn their_identifier(&self) -> &Identifier { &self.their_identifier @@ -125,7 +118,6 @@ impl SecureChannel { pub struct SecureChannelListener { #[n(1)] address: Address, #[n(2)] flow_control_id: FlowControlId, - #[n(3)] is_key_exchange_only: bool, } impl fmt::Display for SecureChannelListener { @@ -140,14 +132,9 @@ impl fmt::Display for SecureChannelListener { impl SecureChannelListener { /// Constructor. - pub fn new( - address: Address, - is_key_exchange_only: bool, - flow_control_id: FlowControlId, - ) -> Self { + pub fn new(address: Address, flow_control_id: FlowControlId) -> Self { Self { address, - is_key_exchange_only, flow_control_id, } } @@ -161,9 +148,4 @@ impl SecureChannelListener { pub fn flow_control_id(&self) -> &FlowControlId { &self.flow_control_id } - /// This secure channel listener is used only for handshake, further encryption happens using - /// api address. Encryption part may be absent. - pub fn is_key_exchange_only(&self) -> bool { - self.is_key_exchange_only - } } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs index 9546970d2b2..8297a343768 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs @@ -1,32 +1,24 @@ -use core::sync::atomic::AtomicBool; use ockam_core::compat::sync::Arc; -use ockam_core::flow_control::FlowControls; use ockam_core::Result; use ockam_core::{Address, Route}; -use ockam_node::{Context, WorkerBuilder}; -use tracing::info; +use ockam_node::Context; use crate::identities::Identities; use crate::models::Identifier; use crate::secure_channel::handshake_worker::HandshakeWorker; use crate::secure_channel::{ - Addresses, DecryptorHandler, RemoteRoute, Role, SecureChannelListenerOptions, - SecureChannelListenerWorker, SecureChannelOptions, SecureChannelRegistry, - SecureChannelSharedState, + Addresses, RemoteRoute, Role, SecureChannelListenerOptions, SecureChannelListenerWorker, + SecureChannelOptions, SecureChannelRegistry, }; #[cfg(feature = "storage")] use crate::SecureChannelsBuilder; -use crate::{ - IdentityError, SecureChannel, SecureChannelListener, SecureChannelRegistryEntry, - SecureChannelRepository, Vault, -}; +use crate::{IdentityError, SecureChannel, SecureChannelListener, Vault}; /// Identity implementation #[derive(Clone)] pub struct SecureChannels { pub(crate) identities: Arc, pub(crate) secure_channel_registry: SecureChannelRegistry, - pub(crate) secure_channel_repository: Arc, } impl SecureChannels { @@ -34,25 +26,16 @@ impl SecureChannels { pub fn new( identities: Arc, secure_channel_registry: SecureChannelRegistry, - secure_channel_repository: Arc, ) -> Self { Self { identities, secure_channel_registry, - secure_channel_repository, } } /// Constructor - pub fn from_identities( - identities: Arc, - secure_channel_repository: Arc, - ) -> Arc { - Arc::new(Self::new( - identities, - SecureChannelRegistry::default(), - secure_channel_repository, - )) + pub fn from_identities(identities: Arc) -> Arc { + Arc::new(Self::new(identities, SecureChannelRegistry::default())) } /// Return the identities services associated to this service @@ -70,11 +53,6 @@ impl SecureChannels { self.secure_channel_registry.clone() } - /// Return the secure channel repository - pub fn secure_channel_repository(&self) -> Arc { - self.secure_channel_repository.clone() - } - /// Create a builder for secure channels #[cfg(feature = "storage")] pub async fn builder() -> Result { @@ -97,7 +75,6 @@ impl SecureChannels { ) -> Result { let address = address.into(); let options = options.into(); - let key_exchange_only = options.key_exchange_only; let flow_control_id = options.flow_control_id.clone(); SecureChannelListenerWorker::create( @@ -109,11 +86,7 @@ impl SecureChannels { ) .await?; - Ok(SecureChannelListener::new( - address, - key_exchange_only, - flow_control_id, - )) + Ok(SecureChannelListener::new(address, flow_control_id)) } /// Initiate a SecureChannel using `Route` to the SecureChannel listener and [`SecureChannelOptions`] @@ -151,12 +124,6 @@ impl SecureChannels { None => None, }; - let secure_channel_repository = if options.is_persistent { - Some(self.secure_channel_repository()) - } else { - None - }; - let encryptor_remote_route = RemoteRoute::create(); let Some(their_identifier) = HandshakeWorker::create( ctx, @@ -171,8 +138,6 @@ impl SecureChannels { Some(route), Some(options.timeout), Role::Initiator, - options.key_exchange_only, - secure_channel_repository, encryptor_remote_route.clone(), ) .await? @@ -185,112 +150,10 @@ impl SecureChannels { their_identifier, encryptor_remote_route, addresses, - options.key_exchange_only, flow_control_id, )) } - /// Start a decryptor side for a previously existed and persisted secure channel - /// Only decryptor api part is started - pub async fn start_persisted_secure_channel_decryptor( - &self, - ctx: &Context, - decryptor_remote_address: &Address, - ) -> Result { - info!( - "Starting persisted secure channel: {}", - decryptor_remote_address - ); - - let Some(persisted_secure_channel) = self - .secure_channel_repository - .get(decryptor_remote_address) - .await? - else { - return Err(IdentityError::PersistentSecureChannelNotFound)?; - }; - - let decryption_key = persisted_secure_channel.decryption_key_handle().clone(); - - self.vault() - .secure_channel_vault - .load_aead_key(&decryption_key) - .await?; - - let my_identifier = persisted_secure_channel.my_identifier(); - let their_identifier = persisted_secure_channel.their_identifier(); - let role = persisted_secure_channel.role(); - let shared_state = SecureChannelSharedState { - remote_route: RemoteRoute::create(), // Unused - should_send_close: Arc::new(AtomicBool::new(false)), // Don't need to send anything - }; - - let mut addresses = Addresses::generate(role); - // FIXME: All other addresses except these two are random and incorrect, we don't use them - // for now though - addresses.decryptor_remote = persisted_secure_channel.decryptor_remote().clone(); - addresses.decryptor_api = persisted_secure_channel.decryptor_api().clone(); - - let decryptor_handler = DecryptorHandler::new( - self.identities(), - None, // We don't need authority, we won't verify any credentials - role, - true, - addresses.clone(), - decryption_key, - self.vault().secure_channel_vault.clone(), - their_identifier.clone(), - shared_state.clone(), - ); - - let decryptor_worker = HandshakeWorker::new( - Arc::new(self.clone()), - None, // No callback will happen - None, - my_identifier.clone(), - addresses.clone(), - role, - true, - None, // No remote interaction - Some(decryptor_handler), - None, // We don't need authority, we won't verify any credentials - self.identities.change_history_repository(), - None, // We don't need credential retriever, we won't present any credentials - // Key exchange only secure channel's state is unchanged after the initial creation, so no need to update it - None, - shared_state.clone(), - ); - - WorkerBuilder::new(decryptor_worker) - .with_address(addresses.decryptor_api.clone()) // We only need API address here - .start(ctx) - .await?; - - let sc = SecureChannel::new( - ctx.flow_controls().clone(), - their_identifier.clone(), - shared_state.remote_route, - addresses.clone(), - true, - FlowControls::generate_flow_control_id(), // This is random and doesn't matter - ); - - let info = SecureChannelRegistryEntry::new( - addresses.encryptor.clone(), - addresses.encryptor_api.clone(), - addresses.decryptor_remote.clone(), - addresses.decryptor_api.clone(), - role.is_initiator(), - my_identifier.clone(), - their_identifier.clone(), - Address::random_local(), // Random, unused for now - ); - - self.secure_channel_registry.register_channel(info)?; - - Ok(sc) - } - /// Stop a SecureChannel given an encryptor address pub async fn stop_secure_channel(&self, ctx: &Context, channel: &Address) -> Result<()> { ctx.stop_worker(channel.clone()).await diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs index 53523d2ab34..0d3f99bb6bf 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs @@ -103,10 +103,6 @@ impl SecureChannelsBuilder { /// Build secure channels pub fn build(self) -> Arc { let identities = self.identities_builder.build(); - Arc::new(SecureChannels::new( - identities, - self.registry, - self.secure_channel_repository, - )) + Arc::new(SecureChannels::new(identities, self.registry)) } } diff --git a/implementations/rust/ockam/ockam_identity/tests/channel.rs b/implementations/rust/ockam/ockam_identity/tests/channel.rs index f9b4e5a555d..a625737731d 100644 --- a/implementations/rust/ockam/ockam_identity/tests/channel.rs +++ b/implementations/rust/ockam/ockam_identity/tests/channel.rs @@ -10,7 +10,6 @@ use ockam_identity::models::{CredentialSchemaIdentifier, Identifier}; use ockam_identity::secure_channels::secure_channels; use ockam_identity::utils::AttributesBuilder; use ockam_identity::{ - DecryptionRequest, DecryptionResponse, EncryptionRequest, EncryptionResponse, IdentityAccessControlBuilder, SecureChannelListenerOptions, SecureChannelOptions, SecureChannels, TrustEveryonePolicy, TrustIdentifierPolicy, Vault, }; @@ -402,121 +401,82 @@ async fn test_channel_registry(ctx: &mut Context) -> Result<()> { Ok(()) } -#[ockam_macros::test] -async fn test_channel_api(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels().await?; - let identities_creation = secure_channels.identities().identities_creation(); - - let alice = identities_creation.create_identity().await?; - let bob = identities_creation.create_identity().await?; - - let bob_listener = secure_channels - .create_secure_channel_listener( - ctx, - &bob, - "bob_listener", - SecureChannelListenerOptions::new(), - ) - .await?; - - let alice_channel = secure_channels - .create_secure_channel( - ctx, - &alice, - route!["bob_listener"], - SecureChannelOptions::new(), - ) - .await?; - - let mut bob_ctx = ctx - .new_detached_with_mailboxes(Mailboxes::main( - "bob", - Arc::new(AllowAll), - Arc::new(AllowAll), - )) - .await?; - - ctx.flow_controls() - .add_consumer("bob", bob_listener.flow_control_id()); - - ctx.send( - route![alice_channel.clone(), "bob"], - "Hello, Alice!".to_string(), - ) - .await?; - - let msg = bob_ctx.receive::().await?; - let return_route = msg.return_route().clone(); - - assert_eq!("Hello, Alice!", msg.into_body()?); - - let bob_channel = return_route.next().unwrap().clone(); - - let alice_channel_data = secure_channels - .secure_channel_registry() - .get_channel_by_encryptor_address(alice_channel.encryptor_address()) - .unwrap(); - - let bob_channel_data = secure_channels - .secure_channel_registry() - .get_channel_by_encryptor_address(&bob_channel) - .unwrap(); - - let encrypted_alice: EncryptionResponse = ctx - .send_and_receive( - route![alice_channel_data.encryptor_api_address().clone()], - EncryptionRequest::Encrypt(b"Ping".to_vec()), - ) - .await?; - let encrypted_alice = match encrypted_alice { - EncryptionResponse::Ok(p) => p, - EncryptionResponse::Err(err) => return Err(err), - }; - - let encrypted_bob: EncryptionResponse = ctx - .send_and_receive( - route![bob_channel_data.encryptor_api_address().clone()], - EncryptionRequest::Encrypt(b"Pong".to_vec()), - ) - .await?; - let encrypted_bob = match encrypted_bob { - EncryptionResponse::Ok(p) => p, - EncryptionResponse::Err(err) => return Err(err), - }; - - let decrypted_alice: DecryptionResponse = ctx - .send_and_receive( - route![alice_channel_data.decryptor_api_address().clone()], - DecryptionRequest::Decrypt { - ciphertext: encrypted_bob, - rekey_counter: None, - }, - ) - .await?; - let decrypted_alice = match decrypted_alice { - DecryptionResponse::Ok(p) => p, - DecryptionResponse::Err(err) => return Err(err), - }; - - let decrypted_bob: DecryptionResponse = ctx - .send_and_receive( - route![bob_channel_data.decryptor_api_address().clone()], - DecryptionRequest::Decrypt { - ciphertext: encrypted_alice, - rekey_counter: None, - }, - ) - .await?; - let decrypted_bob = match decrypted_bob { - DecryptionResponse::Ok(p) => p, - DecryptionResponse::Err(err) => return Err(err), - }; - - assert_eq!(decrypted_alice, b"Pong"); - assert_eq!(decrypted_bob, b"Ping"); - - Ok(()) -} +// #[ockam_macros::test] +// async fn test_channel_api(ctx: &mut Context) -> Result<()> { +// let secure_channels = secure_channels().await?; +// let identities_creation = secure_channels.identities().identities_creation(); +// +// let alice = identities_creation.create_identity().await?; +// let bob = identities_creation.create_identity().await?; +// +// let bob_listener = secure_channels +// .create_secure_channel_listener( +// ctx, +// &bob, +// "bob_listener", +// SecureChannelListenerOptions::new(), +// ) +// .await?; +// +// let alice_channel = secure_channels +// .create_secure_channel( +// ctx, +// &alice, +// route!["bob_listener"], +// SecureChannelOptions::new(), +// ) +// .await?; +// +// let mut bob_ctx = ctx +// .new_detached_with_mailboxes(Mailboxes::main( +// "bob", +// Arc::new(AllowAll), +// Arc::new(AllowAll), +// )) +// .await?; +// +// ctx.flow_controls() +// .add_consumer("bob", bob_listener.flow_control_id()); +// +// ctx.send( +// route![alice_channel.clone(), "bob"], +// "Hello, Alice!".to_string(), +// ) +// .await?; +// +// let msg = bob_ctx.receive::().await?; +// let return_route = msg.return_route().clone(); +// +// assert_eq!("Hello, Alice!", msg.into_body()?); +// +// let bob_channel = return_route.next().unwrap().clone(); +// +// let alice_channel_data = secure_channels +// .secure_channel_registry() +// .get_channel_by_encryptor_address(alice_channel.encryptor_address()) +// .unwrap(); +// +// let bob_channel_data = secure_channels +// .secure_channel_registry() +// .get_channel_by_encryptor_address(&bob_channel) +// .unwrap(); +// +// let encrypted_alice: SecureChannelApiResponse = ctx +// .send_and_receive( +// route![alice_channel_data.encryptor_api_address().clone()], +// SecureChannelApiRequest::Encrypt(b"Ping".to_vec()), +// ) +// .await?; +// let encrypted_alice = match encrypted_alice { +// SecureChannelApiResponse::Ok(p) => p, +// SecureChannelApiResponse::Err(err) => return Err(err), +// }; +// +// assert_eq!(decrypted_alice, b"Pong"); +// assert_eq!(decrypted_bob, b"Ping"); +// +// Ok(()) +// } #[ockam_macros::test] async fn test_tunneled_secure_channel_works(ctx: &mut Context) -> Result<()> { diff --git a/implementations/rust/ockam/ockam_identity/tests/persistence.rs b/implementations/rust/ockam/ockam_identity/tests/persistence.rs deleted file mode 100644 index 4ea4ea63027..00000000000 --- a/implementations/rust/ockam/ockam_identity/tests/persistence.rs +++ /dev/null @@ -1,408 +0,0 @@ -use ockam_core::{route, Address}; -use ockam_identity::{ - secure_channels, DecryptionRequest, DecryptionResponse, EncryptionRequest, EncryptionResponse, - SecureChannelListenerOptions, SecureChannelOptions, SecureChannelSqlxDatabase, SecureChannels, -}; -use ockam_node::compat::futures::FutureExt; -use ockam_node::database::SqlxDatabase; -use ockam_node::{Context, NodeBuilder}; -use ockam_vault::storage::SecretsSqlxDatabase; -use std::sync::Arc; -use std::time::Duration; -use tempfile::NamedTempFile; - -#[ockam_macros::test] -async fn test_key_exchange_only(ctx: &mut Context) -> ockam_core::Result<()> { - let secure_channels_alice = secure_channels().await?; - let secure_channels_bob = secure_channels().await?; - - let alice = secure_channels_alice - .identities() - .identities_creation() - .create_identity() - .await?; - let bob = secure_channels_bob - .identities() - .identities_creation() - .create_identity() - .await?; - - let bob_options = SecureChannelListenerOptions::new().key_exchange_only(); - secure_channels_bob - .create_secure_channel_listener(ctx, &bob, "bob_listener", bob_options) - .await?; - - let alice_options = SecureChannelOptions::new().key_exchange_only(); - let alice_channel = secure_channels_alice - .create_secure_channel(ctx, &alice, route!["bob_listener"], alice_options) - .await?; - - ctx.sleep(Duration::from_millis(200)).await; - - let bob_channel = secure_channels_bob - .secure_channel_registry() - .get_channel_list()[0] - .clone(); - - let msg1_alice = vec![1u8; 32]; - let msg2_alice = vec![2u8; 32]; - - let msg1_bob = vec![1u8; 32]; - let msg2_bob = vec![2u8; 32]; - - let EncryptionResponse::Ok(encrypted_msg1_alice) = ctx - .send_and_receive( - route![alice_channel.encryptor_api_address().clone()], - EncryptionRequest::Encrypt(msg1_alice.clone()), - ) - .await? - else { - panic!() - }; - let EncryptionResponse::Ok(encrypted_msg2_alice) = ctx - .send_and_receive( - route![alice_channel.encryptor_api_address().clone()], - EncryptionRequest::Encrypt(msg2_alice.clone()), - ) - .await? - else { - panic!() - }; - let EncryptionResponse::Ok(encrypted_msg1_bob) = ctx - .send_and_receive( - route![bob_channel.encryptor_api_address().clone()], - EncryptionRequest::Encrypt(msg1_bob.clone()), - ) - .await? - else { - panic!() - }; - let EncryptionResponse::Ok(encrypted_msg2_bob) = ctx - .send_and_receive( - route![bob_channel.encryptor_api_address().clone()], - EncryptionRequest::Encrypt(msg2_bob.clone()), - ) - .await? - else { - panic!() - }; - - let DecryptionResponse::Ok(decrypted_msg1_alice) = ctx - .send_and_receive( - route![bob_channel.decryptor_api_address().clone()], - DecryptionRequest::Decrypt { - ciphertext: encrypted_msg1_alice, - rekey_counter: None, - }, - ) - .await? - else { - panic!() - }; - - let DecryptionResponse::Ok(decrypted_msg2_alice) = ctx - .send_and_receive( - route![bob_channel.decryptor_api_address().clone()], - DecryptionRequest::Decrypt { - ciphertext: encrypted_msg2_alice, - rekey_counter: None, - }, - ) - .await? - else { - panic!() - }; - - let DecryptionResponse::Ok(decrypted_msg1_bob) = ctx - .send_and_receive( - route![alice_channel.decryptor_api_address().clone()], - DecryptionRequest::Decrypt { - ciphertext: encrypted_msg1_bob, - rekey_counter: None, - }, - ) - .await? - else { - panic!() - }; - - let DecryptionResponse::Ok(decrypted_msg2_bob) = ctx - .send_and_receive( - route![alice_channel.decryptor_api_address().clone()], - DecryptionRequest::Decrypt { - ciphertext: encrypted_msg2_bob, - rekey_counter: None, - }, - ) - .await? - else { - panic!() - }; - - assert_eq!(msg1_alice, decrypted_msg1_alice); - assert_eq!(msg2_alice, decrypted_msg2_alice); - assert_eq!(msg1_bob, decrypted_msg1_bob); - assert_eq!(msg2_bob, decrypted_msg2_bob); - - Ok(()) -} - -#[test] -fn test_persistence() -> ockam_core::Result<()> { - let (_db_file_alice, db_file_alice_path) = NamedTempFile::new().unwrap().keep().unwrap(); - let db_file_alice_path_clone = db_file_alice_path.clone(); - - let (_db_file_bob, db_file_bob_path) = NamedTempFile::new().unwrap().keep().unwrap(); - let db_file_bob_path_clone = db_file_bob_path.clone(); - - struct PassBetweenEnv { - decryptor_api_address_alice: Address, - decryptor_remote_address_alice: Address, - decryptor_api_address_bob: Address, - decryptor_remote_address_bob: Address, - msg1_alice: Vec, - msg2_alice: Vec, - msg1_bob: Vec, - msg2_bob: Vec, - encrypted_msg1_alice: Vec, - encrypted_msg2_alice: Vec, - encrypted_msg1_bob: Vec, - encrypted_msg2_bob: Vec, - } - - let (ctx1, mut executor1) = NodeBuilder::new().build(); - let data = executor1 - .execute(async move { - let data = std::panic::AssertUnwindSafe(async { - let db_alice = - SqlxDatabase::create_sqlite(db_file_alice_path_clone.as_path()).await?; - let secure_channel_repository_alice = - Arc::new(SecureChannelSqlxDatabase::new(db_alice.clone())); - let secrets_repository_alice = Arc::new(SecretsSqlxDatabase::new(db_alice)); - let db_bob = SqlxDatabase::create_sqlite(db_file_bob_path_clone.as_path()).await?; - let secure_channel_repository_bob = - Arc::new(SecureChannelSqlxDatabase::new(db_bob.clone())); - let secrets_repository_bob = Arc::new(SecretsSqlxDatabase::new(db_bob)); - - let secure_channels_alice = SecureChannels::builder() - .await? - .with_secure_channel_repository(secure_channel_repository_alice.clone()) - .with_secrets_repository(secrets_repository_alice) - .build(); - let secure_channels_bob = SecureChannels::builder() - .await? - .with_secure_channel_repository(secure_channel_repository_bob.clone()) - .with_secrets_repository(secrets_repository_bob) - .build(); - - let alice = secure_channels_alice - .identities() - .identities_creation() - .create_identity() - .await?; - let bob = secure_channels_bob - .identities() - .identities_creation() - .create_identity() - .await?; - - let bob_options = SecureChannelListenerOptions::new() - .key_exchange_only() - .persist()?; - secure_channels_bob - .create_secure_channel_listener(&ctx1, &bob, "bob_listener", bob_options) - .await?; - - let alice_options = SecureChannelOptions::new().key_exchange_only().persist()?; - let alice_channel = secure_channels_alice - .create_secure_channel(&ctx1, &alice, route!["bob_listener"], alice_options) - .await?; - - ctx1.sleep(Duration::from_millis(200)).await; - - let bob_channel = secure_channels_bob - .secure_channel_registry() - .get_channel_list()[0] - .clone(); - - let msg1_alice = vec![1u8; 32]; - let msg2_alice = vec![2u8; 32]; - - let msg1_bob = vec![1u8; 32]; - let msg2_bob = vec![2u8; 32]; - - let EncryptionResponse::Ok(encrypted_msg1_alice) = ctx1 - .send_and_receive( - route![alice_channel.encryptor_api_address().clone()], - EncryptionRequest::Encrypt(msg1_alice.clone()), - ) - .await? - else { - panic!() - }; - let EncryptionResponse::Ok(encrypted_msg2_alice) = ctx1 - .send_and_receive( - route![alice_channel.encryptor_api_address().clone()], - EncryptionRequest::Encrypt(msg2_alice.clone()), - ) - .await? - else { - panic!() - }; - let EncryptionResponse::Ok(encrypted_msg1_bob) = ctx1 - .send_and_receive( - route![bob_channel.encryptor_api_address().clone()], - EncryptionRequest::Encrypt(msg1_bob.clone()), - ) - .await? - else { - panic!() - }; - let EncryptionResponse::Ok(encrypted_msg2_bob) = ctx1 - .send_and_receive( - route![bob_channel.encryptor_api_address().clone()], - EncryptionRequest::Encrypt(msg2_bob.clone()), - ) - .await? - else { - panic!() - }; - - let data = PassBetweenEnv { - decryptor_api_address_alice: alice_channel.decryptor_api_address().clone(), - decryptor_remote_address_alice: alice_channel - .decryptor_remote_address() - .clone(), - decryptor_api_address_bob: bob_channel.decryptor_api_address().clone(), - decryptor_remote_address_bob: bob_channel.decryptor_messaging_address().clone(), - msg1_alice, - msg2_alice, - msg1_bob, - msg2_bob, - encrypted_msg1_alice, - encrypted_msg2_alice, - encrypted_msg1_bob, - encrypted_msg2_bob, - }; - - Result::::Ok(data) - }) - .catch_unwind() - .await; - - ctx1.stop().await?; - - data.unwrap() - }) - .unwrap() - .unwrap(); - - let (ctx2, mut executor2) = NodeBuilder::new().build(); - executor2 - .execute(async move { - let res = std::panic::AssertUnwindSafe(async { - let db_alice = SqlxDatabase::create_sqlite(db_file_alice_path.as_path()).await?; - let secure_channel_repository_alice = - Arc::new(SecureChannelSqlxDatabase::new(db_alice.clone())); - let secrets_repository_alice = Arc::new(SecretsSqlxDatabase::new(db_alice)); - let db_bob = SqlxDatabase::create_sqlite(db_file_bob_path.as_path()).await?; - let secure_channel_repository_bob = - Arc::new(SecureChannelSqlxDatabase::new(db_bob.clone())); - let secrets_repository_bob = Arc::new(SecretsSqlxDatabase::new(db_bob)); - - let secure_channels_alice = SecureChannels::builder() - .await? - .with_secure_channel_repository(secure_channel_repository_alice.clone()) - .with_secrets_repository(secrets_repository_alice) - .build(); - let secure_channels_bob = SecureChannels::builder() - .await? - .with_secure_channel_repository(secure_channel_repository_bob.clone()) - .with_secrets_repository(secrets_repository_bob) - .build(); - - secure_channels_alice - .start_persisted_secure_channel_decryptor( - &ctx2, - &data.decryptor_remote_address_alice, - ) - .await?; - - secure_channels_bob - .start_persisted_secure_channel_decryptor( - &ctx2, - &data.decryptor_remote_address_bob, - ) - .await?; - - let DecryptionResponse::Ok(decrypted_msg1_alice) = ctx2 - .send_and_receive( - route![data.decryptor_api_address_bob.clone()], - DecryptionRequest::Decrypt { - ciphertext: data.encrypted_msg1_alice, - rekey_counter: None, - }, - ) - .await? - else { - panic!() - }; - - let DecryptionResponse::Ok(decrypted_msg2_alice) = ctx2 - .send_and_receive( - route![data.decryptor_api_address_bob.clone()], - DecryptionRequest::Decrypt { - ciphertext: data.encrypted_msg2_alice, - rekey_counter: None, - }, - ) - .await? - else { - panic!() - }; - - let DecryptionResponse::Ok(decrypted_msg1_bob) = ctx2 - .send_and_receive( - route![data.decryptor_api_address_alice.clone()], - DecryptionRequest::Decrypt { - ciphertext: data.encrypted_msg1_bob, - rekey_counter: None, - }, - ) - .await? - else { - panic!() - }; - - let DecryptionResponse::Ok(decrypted_msg2_bob) = ctx2 - .send_and_receive( - route![data.decryptor_api_address_alice.clone()], - DecryptionRequest::Decrypt { - ciphertext: data.encrypted_msg2_bob, - rekey_counter: None, - }, - ) - .await? - else { - panic!() - }; - - assert_eq!(data.msg1_alice, decrypted_msg1_alice); - assert_eq!(data.msg2_alice, decrypted_msg2_alice); - assert_eq!(data.msg1_bob, decrypted_msg1_bob); - assert_eq!(data.msg2_bob, decrypted_msg2_bob); - - ockam_core::Result::<()>::Ok(()) - }) - .catch_unwind() - .await; - - ctx2.stop().await?; - - res.unwrap() - }) - .unwrap() - .unwrap(); - - Ok(()) -} From 7b261e2186024052ef2b521d539a10f373765055 Mon Sep 17 00:00:00 2001 From: Davide Baldo Date: Mon, 23 Dec 2024 20:50:20 +0100 Subject: [PATCH 7/7] feat(rust): switching key negotiation to a double secure channel --- Cargo.lock | 1 - .../rust/ockam/ockam_api/Cargo.toml | 1 - .../src/kafka/key_exchange/controller.rs | 50 +++++++---- .../src/kafka/key_exchange/listener.rs | 87 +++++++++++++++---- .../src/kafka/key_exchange/secure_channels.rs | 83 ++++++++++++++++-- .../src/kafka/tests/integration_test.rs | 29 +++++++ .../src/nodes/service/kafka_services.rs | 24 ++--- .../ockam_api/src/proxy_vault/protocol.rs | 46 ++++++++-- .../rust/ockam/ockam_core/src/zeroize.rs | 12 +-- .../ockam_identity/src/secure_channel/api.rs | 15 ++-- .../src/secure_channel/decryptor.rs | 18 ++-- .../src/secure_channel/encryptor.rs | 4 +- .../src/secure_channel/encryptor_worker.rs | 10 ++- .../src/secure_channel/handshake/handshake.rs | 4 +- .../storage/secure_channel_repository.rs | 2 + .../rust/ockam/ockam_identity/src/vault.rs | 1 + .../ockam_node/src/context/send_message.rs | 2 +- .../vault_for_secure_channels/common.rs | 1 + .../vault_for_secure_channels/types.rs | 7 +- .../vault_for_encryption_at_rest.rs | 18 ++-- .../vault_for_secure_channels.rs | 83 +++++++++++------- .../traits/vault_for_at_rest_encryption.rs | 6 +- .../src/traits/vault_for_secure_channels.rs | 8 +- .../ockam/ockam_vault/src/types/hashes.rs | 7 +- .../ockam/ockam_vault/src/types/secrets.rs | 3 +- 25 files changed, 371 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d2fa786c82..f1fba42fcad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4786,7 +4786,6 @@ dependencies = [ "treeline", "url", "uuid", - "zeroize", ] [[package]] diff --git a/implementations/rust/ockam/ockam_api/Cargo.toml b/implementations/rust/ockam/ockam_api/Cargo.toml index efa7871963f..f078ac14a9c 100644 --- a/implementations/rust/ockam/ockam_api/Cargo.toml +++ b/implementations/rust/ockam/ockam_api/Cargo.toml @@ -112,7 +112,6 @@ tracing-error = "0.2.0" tracing-opentelemetry = "0.27.0" tracing-subscriber = { version = "0.3", features = ["json"] } url = "2.5.2" -zeroize = { version = "1.8.1", features = ["zeroize_derive"] } ockam_multiaddr = { path = "../ockam_multiaddr", version = "0.66.0", features = ["cbor", "serde"] } ockam_transport_core = { path = "../ockam_transport_core", version = "^0.99.0" } diff --git a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs index 7de12088288..f761df233b7 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/controller.rs @@ -251,7 +251,6 @@ mod test { use crate::kafka::key_exchange::listener::KafkaKeyExchangeListener; use crate::kafka::{ConsumerPublishing, ConsumerResolution}; use crate::test_utils::{AuthorityConfiguration, TestNode}; - use crate::DefaultAddress; use ockam::identity::Identifier; use ockam_abac::{Action, Env, Resource, ResourceType}; use ockam_core::compat::clock::test::TestClock; @@ -266,7 +265,6 @@ mod test { pub fn rekey_rotation() -> ockam_core::Result<()> { let runtime = Arc::new(Runtime::new().unwrap()); let runtime_cloned = runtime.clone(); - std::env::set_var("OCKAM_LOGGING", "false"); runtime_cloned.block_on(async move { let test_body = async move { @@ -291,30 +289,33 @@ mod test { ) .await; - let consumer_secure_channel_listener_flow_control_id = consumer_node - .context - .flow_controls() - .get_flow_control_with_spawner(&DefaultAddress::SECURE_CHANNEL_LISTENER.into()) - .unwrap(); + let test_clock = TestClock::new(0); KafkaKeyExchangeListener::create( + test_clock.clone(), &consumer_node.context, consumer_node .node_manager .secure_channels .vault() .encryption_at_rest_vault, + consumer_node + .node_manager + .secure_channels + .vault() + .secure_channel_vault, + consumer_node + .node_manager + .secure_channels + .secure_channel_registry(), + Duration::from_secs(5 * 60), //rotation + Duration::from_secs(10 * 60), //validity Duration::from_secs(60), - Duration::from_secs(60), - Duration::from_secs(60), - &consumer_secure_channel_listener_flow_control_id, AllowAll, AllowAll, ) .await?; - let test_clock = TestClock::new(0); - let destination = consumer_node.listen_address().await.multi_addr().unwrap(); let producer_secure_channel_controller = create_secure_channel_controller( test_clock.clone(), @@ -355,7 +356,10 @@ mod test { .await?; assert_eq!(third_key.rekey_counter, 1); - assert_eq!(first_key.secret_key_handle, third_key.secret_key_handle); + assert_eq!( + first_key.key_identifier_for_consumer, + third_key.key_identifier_for_consumer + ); // 04:00 - yet another rekey should happen, but no rotation test_clock.add_seconds(60 * 3); @@ -365,7 +369,10 @@ mod test { .await?; assert_eq!(fourth_key.rekey_counter, 2); - assert_eq!(first_key.secret_key_handle, fourth_key.secret_key_handle); + assert_eq!( + first_key.key_identifier_for_consumer, + fourth_key.key_identifier_for_consumer + ); // 05:00 - the default duration of the key is 10 minutes, // but the rotation should happen after 5 minutes @@ -375,7 +382,10 @@ mod test { .get_or_exchange_key(&mut producer_node.context, "topic_name") .await?; - assert_ne!(third_key.secret_key_handle, fifth_key.secret_key_handle); + assert_ne!( + third_key.key_identifier_for_consumer, + fifth_key.key_identifier_for_consumer + ); assert_eq!(fifth_key.rekey_counter, 0); // Now let's simulate a failure to rekey by shutting down the consumer @@ -389,7 +399,10 @@ mod test { .await?; assert_eq!(sixth_key.rekey_counter, 1); - assert_eq!(fifth_key.secret_key_handle, sixth_key.secret_key_handle); + assert_eq!( + fifth_key.key_identifier_for_consumer, + sixth_key.key_identifier_for_consumer + ); // 10:00 - Rotation fails, but the existing key is still valid // and needs to be rekeyed @@ -400,7 +413,10 @@ mod test { .await?; assert_eq!(seventh_key.rekey_counter, 2); - assert_eq!(fifth_key.secret_key_handle, seventh_key.secret_key_handle); + assert_eq!( + fifth_key.key_identifier_for_consumer, + seventh_key.key_identifier_for_consumer + ); // 15:00 - Rotation fails, and the existing key is no longer valid test_clock.add_seconds(60 * 5); diff --git a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/listener.rs b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/listener.rs index 5fefb017d78..87a2b577526 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/listener.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/listener.rs @@ -1,37 +1,42 @@ use crate::DefaultAddress; use minicbor::{CborLen, Decode, Encode}; -use ockam::identity::TimestampInSeconds; -use ockam_core::flow_control::FlowControlId; +use ockam::identity::{ + SecureChannelApiRequest, SecureChannelApiResponse, SecureChannelRegistry, TimestampInSeconds, +}; +use ockam_core::compat::clock::Clock; +use ockam_core::errcode::{Kind, Origin}; use ockam_core::{ - async_trait, Address, Decodable, Encodable, Encoded, IncomingAccessControl, Message, + async_trait, route, Address, Decodable, Encodable, Encoded, IncomingAccessControl, Message, OutgoingAccessControl, Routed, Worker, }; use ockam_node::{Context, WorkerBuilder}; -use ockam_vault::VaultForEncryptionAtRest; -use rand::Rng; +use ockam_vault::{VaultForEncryptionAtRest, VaultForSecureChannels}; use std::sync::Arc; use std::time::Duration; pub(crate) struct KafkaKeyExchangeListener { encryption_at_rest: Arc, + secure_channel_vault: Arc, + secure_channel_registry: SecureChannelRegistry, rekey_period: Duration, key_validity: Duration, key_rotation: Duration, + clock: Box, } #[derive(Debug, CborLen, Encode, Decode)] #[rustfmt::skip] pub(crate) struct KeyExchangeRequest { + #[n(1)] pub local_decryptor_address: Address, } #[derive(Debug, CborLen, Encode, Decode)] #[rustfmt::skip] pub(crate) struct KeyExchangeResponse { #[n(0)] pub key_identifier_for_consumer: Vec, - #[n(1)] pub secret_key: [u8; 32], - #[n(2)] pub valid_until: TimestampInSeconds, - #[n(3)] pub rotate_after: TimestampInSeconds, - #[n(4)] pub rekey_period: Duration, + #[n(1)] pub valid_until: TimestampInSeconds, + #[n(2)] pub rotate_after: TimestampInSeconds, + #[n(3)] pub rekey_period: Duration, } impl Encodable for KeyExchangeRequest { @@ -70,14 +75,43 @@ impl Worker for KafkaKeyExchangeListener { context: &mut Self::Context, message: Routed, ) -> ockam_core::Result<()> { - let mut secret_key = [0u8; 32]; - rand::thread_rng().fill(&mut secret_key[..]); - let handle = self - .encryption_at_rest - .import_aead_key(secret_key.to_vec()) - .await?; + let request: KeyExchangeRequest = minicbor::decode(message.payload())?; + let local_decryptor = Address::from_string(request.local_decryptor_address); + + let entry = self + .secure_channel_registry + .get_channel_by_decryptor_address(&local_decryptor); + let handle = match entry { + None => { + warn!("No secure channel found for local decryptor {local_decryptor}",); + return Ok(()); + } + Some(entry) => { + let response: SecureChannelApiResponse = context + .send_and_receive( + route![entry.decryptor_api_address().clone()], + SecureChannelApiRequest::ExtractKey, + ) + .await?; + + let key_identifier = match response { + SecureChannelApiResponse::Ok(key_identifier) => key_identifier, + SecureChannelApiResponse::Err(error) => { + error!("Error extracting key: {error}"); + return Ok(()); + } + }; - let now = TimestampInSeconds(ockam_core::compat::time::now()?); + let secret = self + .secure_channel_vault + .export_rekey(&key_identifier) + .await?; + + self.encryption_at_rest.import_aead_key(secret).await? + } + }; + + let now = TimestampInSeconds(self.clock.now()?); let valid_until = now + self.key_validity; let rotate_after = now + self.key_rotation; @@ -86,7 +120,6 @@ impl Worker for KafkaKeyExchangeListener { message.return_route().clone(), KeyExchangeResponse { key_identifier_for_consumer: handle.into_vec(), - secret_key, valid_until, rotate_after, rekey_period: self.rekey_period, @@ -101,25 +134,41 @@ impl Worker for KafkaKeyExchangeListener { impl KafkaKeyExchangeListener { #[allow(clippy::too_many_arguments)] pub async fn create( + clock: impl Clock, context: &Context, encryption_at_rest: Arc, + secure_channel_vault: Arc, + secure_channel_registry: SecureChannelRegistry, key_rotation: Duration, key_validity: Duration, rekey_period: Duration, - secure_channel_flow_control: &FlowControlId, incoming_access_control: impl IncomingAccessControl, outgoing_access_control: impl OutgoingAccessControl, ) -> ockam_core::Result<()> { let address = Address::from_string(DefaultAddress::KAFKA_CUSTODIAN); + let secure_channel_flow_control = context + .flow_controls() + .get_flow_control_with_spawner(&DefaultAddress::SECURE_CHANNEL_LISTENER.into()) + .ok_or_else(|| { + ockam_core::Error::new( + Origin::Channel, + Kind::NotFound, + "Secure channel listener flow control not found", + ) + })?; + context .flow_controls() - .add_consumer(address.clone(), secure_channel_flow_control); + .add_consumer(address.clone(), &secure_channel_flow_control); WorkerBuilder::new(KafkaKeyExchangeListener { encryption_at_rest, + secure_channel_vault, key_rotation, key_validity, rekey_period, + secure_channel_registry, + clock: Box::new(clock), }) .with_address(address) .with_incoming_access_control(incoming_access_control) diff --git a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs index f970bbfe641..fe295376b45 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/key_exchange/secure_channels.rs @@ -5,9 +5,9 @@ use crate::kafka::key_exchange::controller::{ use crate::kafka::key_exchange::listener::{KeyExchangeRequest, KeyExchangeResponse}; use crate::kafka::ConsumerResolution; use crate::DefaultAddress; -use ockam::identity::TimestampInSeconds; +use ockam::identity::{SecureChannelApiRequest, SecureChannelApiResponse, TimestampInSeconds}; use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{Error, Result}; +use ockam_core::{route, Error, Result}; use ockam_multiaddr::proto::{Secure, Service}; use ockam_multiaddr::MultiAddr; use ockam_node::{Context, MessageSendReceiveOptions}; @@ -35,6 +35,72 @@ impl KafkaKeyExchangeControllerImpl { destination.push_back(Secure::new(DefaultAddress::SECURE_CHANNEL_LISTENER))?; destination.push_back(Service::new(DefaultAddress::KAFKA_CUSTODIAN))?; if let Some(node_manager) = inner.node_manager.upgrade() { + // create a second secure channel to be used for key exchange + let (aead_secret_key_handle, their_decryptor_address) = { + let key_exchange_connection = node_manager + .make_connection(context, &destination, node_manager.identifier(), None, None) + .await?; + + let encryptor_address = key_exchange_connection + .secure_channel_encryptors + .first() + .expect("encryptor should be present"); + + let entry = node_manager + .secure_channels + .secure_channel_registry() + .get_channel_by_encryptor_address(encryptor_address) + .expect("channel should be present"); + + if !inner + .consumer_policy_access_control + .is_identity_authorized(entry.their_id()) + .await? + { + key_exchange_connection + .close(context, &node_manager) + .await?; + return Err(Error::new( + Origin::Channel, + Kind::Invalid, + "Consumer is not authorized to use the secure channel", + )); + } + + let response: SecureChannelApiResponse = context + .send_and_receive( + route![entry.encryptor_api_address().clone()], + SecureChannelApiRequest::ExtractKey, + ) + .await?; + + match response { + SecureChannelApiResponse::Ok(secret_handle) => { + let secret = node_manager + .secure_channels + .vault() + .secure_channel_vault + .export_rekey(&secret_handle) + .await?; + + key_exchange_connection + .close(context, &node_manager) + .await?; + + ( + self.encryption_at_rest.import_aead_key(secret).await?, + entry.their_decryptor_address(), + ) + } + SecureChannelApiResponse::Err(error) => { + key_exchange_connection + .close(context, &node_manager) + .await?; + return Err(error); + } + } + }; + let connection = node_manager .make_connection(context, &destination, node_manager.identifier(), None, None) .await?; @@ -52,14 +118,17 @@ impl KafkaKeyExchangeControllerImpl { let route = connection.route()?; let response: KeyExchangeResponse = context - .send_and_receive_extended(route, KeyExchangeRequest {}, send_and_receive_options) + .send_and_receive_extended( + route, + KeyExchangeRequest { + local_decryptor_address: their_decryptor_address, + }, + send_and_receive_options, + ) .await? .into_body()?; - let aead_secret_key_handle = self - .encryption_at_rest - .import_aead_key(response.secret_key.to_vec()) - .await?; + connection.close(context, &node_manager).await?; Ok(ExchangedKey { secret_key_handler: aead_secret_key_handle, diff --git a/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs b/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs index 79e12dae0a4..43376aa44da 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/tests/integration_test.rs @@ -24,6 +24,7 @@ use tokio::task::JoinHandle; use uuid::Uuid; use crate::kafka::key_exchange::controller::KafkaKeyExchangeControllerImpl; +use crate::kafka::key_exchange::listener::KafkaKeyExchangeListener; use crate::kafka::protocol_aware::inlet::KafkaInletInterceptorFactory; use crate::kafka::protocol_aware::utils::{encode_request, encode_response}; use crate::kafka::{ConsumerPublishing, ConsumerResolution, KafkaInletController}; @@ -32,6 +33,7 @@ use ockam::compat::tokio::io::DuplexStream; use ockam::tcp::{TcpInletOptions, TcpOutletOptions}; use ockam::Context; use ockam_abac::{Action, Resource, ResourceType}; +use ockam_core::compat::clock::ProductionClock; use ockam_core::compat::sync::Arc; use ockam_core::route; use ockam_core::Address; @@ -143,6 +145,33 @@ async fn producer__flow_with_mock_kafka__content_encryption_and_decryption( ) .await?; + { + KafkaKeyExchangeListener::create( + ProductionClock, + context, + handle + .node_manager + .secure_channels + .vault() + .encryption_at_rest_vault, + handle + .node_manager + .secure_channels + .vault() + .secure_channel_vault, + handle + .node_manager + .secure_channels + .secure_channel_registry(), + Duration::from_secs(5 * 60), //rotation + Duration::from_secs(10 * 60), //validity + Duration::from_secs(60), //rekey + AllowAll, + AllowAll, + ) + .await?; + } + // for the consumer to become available to the producer, the consumer has to issue a Fetch // request first, so the sidecar can react by creating the relay for topic 'my-topic' { diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs index 837227b17c2..93add9530b4 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs @@ -23,6 +23,7 @@ use ockam::{Address, Context, Result}; use ockam_abac::PolicyExpression; use ockam_abac::{Action, Resource, ResourceType}; use ockam_core::api::{Error, Response}; +use ockam_core::compat::clock::ProductionClock; use ockam_core::compat::rand::random_string; use ockam_core::flow_control::FlowControls; use ockam_core::route; @@ -188,21 +189,16 @@ impl InMemoryNode { inlet_policy_expression.clone(), ); - let default_secure_channel_listener_flow_control_id = context - .flow_controls() - .get_flow_control_with_spawner(&DefaultAddress::SECURE_CHANNEL_LISTENER.into()) - .ok_or_else(|| { - ApiError::core("Unable to get flow control for secure channel listener") - })?; - // TODO: remove key exchange from inlets KafkaKeyExchangeListener::create( + ProductionClock, context, vault.encryption_at_rest_vault, + self.secure_channels.vault().secure_channel_vault, + self.secure_channels.secure_channel_registry(), std::time::Duration::from_secs(60 * 60 * 24), std::time::Duration::from_secs(60 * 60 * 30), std::time::Duration::from_secs(60 * 60), - &default_secure_channel_listener_flow_control_id, producer_policy_access_control.create_incoming(), producer_policy_access_control .create_outgoing(context) @@ -365,14 +361,6 @@ impl InMemoryNode { request: StartKafkaCustodianRequest, ) -> Result<()> { //TODO: dedup code with start_kafka_inlet_service - - let default_secure_channel_listener_flow_control_id = context - .flow_controls() - .get_flow_control_with_spawner(&DefaultAddress::SECURE_CHANNEL_LISTENER.into()) - .ok_or_else(|| { - ApiError::core("Unable to get flow control for secure channel listener") - })?; - let vault = if let Some(vault) = request.vault { let named_vault = self.cli_state.get_named_vault(&vault).await?; self.cli_state @@ -395,12 +383,14 @@ impl InMemoryNode { .await?; KafkaKeyExchangeListener::create( + ProductionClock, context, vault.encryption_at_rest_vault, + self.secure_channels.vault().secure_channel_vault, + self.secure_channels.secure_channel_registry(), request.key_rotation, request.key_validity, request.rekey_period, - &default_secure_channel_listener_flow_control_id, producer_policy_access_control.create_incoming(), producer_policy_access_control .create_outgoing(context) diff --git a/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs b/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs index 09316951c21..f39ffd9ab30 100644 --- a/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs +++ b/implementations/rust/ockam/ockam_api/src/proxy_vault/protocol.rs @@ -485,8 +485,8 @@ pub mod vault_for_secure_channels { use minicbor::{CborLen, Decode, Encode}; use ockam_core::{async_trait, cbor_encode_preallocate, MaybeZeroizeOnDrop}; use ockam_vault::{ - AeadSecretKeyHandle, HKDFNumberOfOutputs, HashOutput, HkdfOutput, SecretBufferHandle, - VaultForSecureChannels, X25519PublicKey, X25519SecretKeyHandle, + AeadSecretKeyHandle, HKDFNumberOfOutputs, HashOutput, HkdfOutput, SecretBuffer, + SecretBufferHandle, VaultForSecureChannels, X25519PublicKey, X25519SecretKeyHandle, }; pub(super) async fn handle_request( @@ -617,7 +617,7 @@ pub mod vault_for_secure_channels { } Request::DeleteAeadSecretKey { secret_key_handle } => { trace!("delete_aead_secret_key request for {secret_key_handle:?}"); - let result = vault.delete_aead_secret_key(secret_key_handle).await; + let result = vault.delete_aead_secret_key(&secret_key_handle).await; Response::DeleteAeadSecretKey(result.map_err(Into::into)) } Request::Rekey { @@ -628,6 +628,11 @@ pub mod vault_for_secure_channels { let result = vault.rekey(&secret_key_handle, n).await; Response::Rekey(result.map_err(Into::into)) } + Request::ExportRekey { secret_key_handle } => { + trace!("export_rekey request for {secret_key_handle:?}"); + let result = vault.export_rekey(&secret_key_handle).await; + Response::ExportRekey(result.map_err(Into::into)) + } }; cbor_encode_preallocate(response) } @@ -695,6 +700,9 @@ pub mod vault_for_secure_channels { #[n(0)] secret_key_handle: AeadSecretKeyHandle, #[n(1)] n: u16, }, + #[n(18)] ExportRekey { + #[n(0)] secret_key_handle: AeadSecretKeyHandle + }, } #[derive(Encode, Decode, CborLen)] @@ -718,6 +726,7 @@ pub mod vault_for_secure_channels { #[n(15)] ConvertSecretBufferToAeadKey(#[n(0)] Result), #[n(16)] DeleteAeadSecretKey(#[n(0)] Result), #[n(17)] Rekey(#[n(0)] Result), + #[n(18)] ExportRekey(#[n(0)] Result), } #[async_trait] @@ -866,6 +875,25 @@ pub mod vault_for_secure_channels { Ok(result) } + async fn export_rekey( + &self, + secret_key_handle: &AeadSecretKeyHandle, + ) -> ockam_core::Result { + trace!("sending export_rekey request for {secret_key_handle:?}"); + let response: Response = self + .send_and_receive(Request::ExportRekey { + secret_key_handle: secret_key_handle.clone(), + }) + .await?; + + let result = match response { + Response::ExportRekey(result) => result?, + _ => Err(ProxyError::Protocol)?, + }; + + Ok(result) + } + async fn persist_aead_key( &self, secret_key_handle: &AeadSecretKeyHandle, @@ -1067,11 +1095,13 @@ pub mod vault_for_secure_channels { async fn delete_aead_secret_key( &self, - secret_key_handle: AeadSecretKeyHandle, + secret_key_handle: &AeadSecretKeyHandle, ) -> ockam_core::Result { trace!("sending delete_aead_secret_key request for {secret_key_handle:?}"); let response: Response = self - .send_and_receive(Request::DeleteAeadSecretKey { secret_key_handle }) + .send_and_receive(Request::DeleteAeadSecretKey { + secret_key_handle: secret_key_handle.clone(), + }) .await?; let result = match response { @@ -1183,7 +1213,7 @@ pub mod vault_for_encryption_at_rest { use crate::proxy_vault::protocol::{ProxyError, SpecificClient}; use minicbor::{CborLen, Decode, Encode}; use ockam_core::{async_trait, cbor_encode_preallocate, MaybeZeroizeOnDrop}; - use ockam_vault::{AeadSecretKeyHandle, VaultForEncryptionAtRest}; + use ockam_vault::{AeadSecretKeyHandle, SecretBuffer, VaultForEncryptionAtRest}; pub(super) async fn handle_request( vault: &dyn VaultForEncryptionAtRest, @@ -1246,7 +1276,7 @@ pub mod vault_for_encryption_at_rest { #[n(0)] secret_key_handle: AeadSecretKeyHandle, }, #[n(3)] ImportAeadKey { - #[n(0)] secret: Vec, + #[n(0)] secret: SecretBuffer, }, } @@ -1343,7 +1373,7 @@ pub mod vault_for_encryption_at_rest { async fn import_aead_key( &self, - secret: Vec, + secret: SecretBuffer, ) -> ockam_core::Result { trace!("sending import_aead_key request"); let response: Response = self diff --git a/implementations/rust/ockam/ockam_core/src/zeroize.rs b/implementations/rust/ockam/ockam_core/src/zeroize.rs index 2ad7d844942..cda70e4c524 100644 --- a/implementations/rust/ockam/ockam_core/src/zeroize.rs +++ b/implementations/rust/ockam/ockam_core/src/zeroize.rs @@ -1,9 +1,9 @@ use core::fmt::{Debug, Display, Formatter}; use core::hash::{Hash, Hasher}; use core::ops::Deref; +use core::ops::DerefMut; use minicbor::encode::{Error, Write}; use minicbor::{CborLen, Decode, Encode, Encoder}; -use std::ops::DerefMut; use zeroize::Zeroize; /// OnDrop is an enum to specify whether to zeroize the inner value when dropped. @@ -43,7 +43,7 @@ impl MaybeZeroizeOnDrop { /// Return the inner value regardless of the zeroize_on_drop flag. /// The caller has the responsibility to ensure that the inner value is zeroized when necessary. pub fn discard_zeroize(mut self) -> T { - std::mem::take(&mut self.target) + core::mem::take(&mut self.target) } } @@ -85,7 +85,7 @@ impl Default for MaybeZeroizeOnDrop { } impl Debug for MaybeZeroizeOnDrop { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.debug_struct("MaybeZeroizeOnDrop") .field("target", &self.target) .field("on_drop", &self.on_drop) @@ -94,7 +94,7 @@ impl Debug for MaybeZeroizeOnDrop { } impl Display for MaybeZeroizeOnDrop { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { self.target.fmt(f) } } @@ -117,13 +117,13 @@ impl PartialEq for MaybeZeroizeOnDrop { impl Eq for MaybeZeroizeOnDrop {} impl PartialOrd for MaybeZeroizeOnDrop { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { self.target.partial_cmp(&other.target) } } impl Ord for MaybeZeroizeOnDrop { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.target.cmp(&other.target) } } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs index a0fc95b3125..42ff40972a9 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/api.rs @@ -1,20 +1,19 @@ -use minicbor::{CborLen, Decode, Encode}; +use ockam_core::{Error, Message}; use ockam_vault::AeadSecretKeyHandle; +use serde::{Deserialize, Serialize}; /// Request type for `SecureChannel` API Address -#[derive(Encode, Decode, CborLen)] -#[rustfmt::skip] +#[derive(Serialize, Deserialize, Message)] pub enum SecureChannelApiRequest { /// Derive a new key from current key and shutdown the worker - #[n(0)] ExtractKey, + ExtractKey, } /// Response type for `SecureChannel` API Address -#[derive(Encode, Decode, CborLen)] -#[rustfmt::skip] +#[derive(Serialize, Deserialize, Message)] pub enum SecureChannelApiResponse { /// Success - #[n(0)] Ok(#[n(0)] AeadSecretKeyHandle), + Ok(AeadSecretKeyHandle), /// Error - #[n(1)] Err(#[n(0)] String), + Err(Error), } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs index 9971eb9c5ad..ec707458a8d 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/decryptor.rs @@ -1,7 +1,7 @@ use core::sync::atomic::Ordering; use ockam_core::compat::sync::Arc; -use ockam_core::{cbor_encode_preallocate, LocalMessage, NeutralMessage}; use ockam_core::{route, Any, OnDrop, Result, Route, Routed, SecureChannelLocalInfo}; +use ockam_core::{Decodable, LocalMessage}; use ockam_node::Context; use crate::models::Identifier; @@ -68,19 +68,23 @@ impl DecryptorHandler { let return_route = msg.return_route; // Decode raw payload binary - let request = minicbor::decode(msg.payload.as_slice())?; + let request = SecureChannelApiRequest::decode(&msg.payload)?; let response = match request { SecureChannelApiRequest::ExtractKey => { let handle = self.decryptor.derive_new_key().await?; SecureChannelApiResponse::Ok(handle) } }; - let response = NeutralMessage::from(cbor_encode_preallocate(&response)?); // Send reply to the caller ctx.send_from_address(return_route, response, self.addresses.decryptor_api.clone()) .await?; + // Avoid sending a Close message, to the other party can extract the key as wel + self.shared_state + .should_send_close + .store(false, Ordering::Relaxed); + // Once we have extracted the key, we can't use it anymore ctx.stop_worker(self.addresses.encryptor.clone()).await?; @@ -273,7 +277,7 @@ impl Decryptor { Ok(result) => { self.nonce_tracker = nonce_tracker; if let Some(key_to_delete) = self.key_tracker.update_key(&key.clone())? { - self.vault.delete_aead_secret_key(key_to_delete).await?; + self.vault.delete_aead_secret_key(&key_to_delete).await?; } Ok((result, nonce)) @@ -291,13 +295,11 @@ impl Decryptor { #[instrument(skip_all)] pub(crate) async fn shutdown(&self) -> Result<()> { self.vault - .delete_aead_secret_key(self.key_tracker.current_key.clone()) + .delete_aead_secret_key(&self.key_tracker.current_key) .await?; if let Some(previous_key) = &self.key_tracker.previous_key { - self.vault - .delete_aead_secret_key(previous_key.clone()) - .await?; + self.vault.delete_aead_secret_key(previous_key).await?; }; Ok(()) diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs index 8723cb896d3..de7dce64d44 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor.rs @@ -27,7 +27,7 @@ impl Encryptor { if current_nonce.value() > 0 && current_nonce.value() % KEY_RENEWAL_INTERVAL == 0 { let new_key = self.vault.rekey(&self.key, 1).await?; let old_key = core::mem::replace(&mut self.key, new_key); - self.vault.delete_aead_secret_key(old_key).await?; + self.vault.delete_aead_secret_key(&old_key).await?; } payload[..NOISE_NONCE_LEN].copy_from_slice(¤t_nonce.to_noise_nonce()); @@ -59,7 +59,7 @@ impl Encryptor { #[instrument(skip_all)] pub(crate) async fn shutdown(&self) -> Result<()> { - if !self.vault.delete_aead_secret_key(self.key.clone()).await? { + if !self.vault.delete_aead_secret_key(&self.key).await? { Err(Error::new( Origin::Ockam, Kind::Internal, diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs index e7e65659417..5de490081de 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs @@ -4,7 +4,7 @@ use ockam_core::compat::sync::{Arc, RwLock}; use ockam_core::compat::vec::Vec; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{ - async_trait, cbor_encode_preallocate, route, CowBytes, Error, LocalMessage, MaybeZeroizeOnDrop, + async_trait, route, CowBytes, Decodable, Error, LocalMessage, MaybeZeroizeOnDrop, NeutralMessage, OnDrop, Route, }; use ockam_core::{Any, Result, Routed, Worker}; @@ -135,7 +135,7 @@ impl EncryptorWorker { let return_route = msg.return_route; // Decode raw payload binary - let request = minicbor::decode(msg.payload.as_slice())?; + let request = SecureChannelApiRequest::decode(&msg.payload)?; // If encryption fails, that means we have some internal error, // and we may be in an invalid state, it's better to stop the Worker @@ -145,12 +145,16 @@ impl EncryptorWorker { SecureChannelApiResponse::Ok(handle) } }; - let response = NeutralMessage::from(cbor_encode_preallocate(&response)?); // Send the reply to the caller ctx.send_from_address(return_route, response, self.addresses.encryptor_api.clone()) .await?; + // Avoid sending a Close message, to the other party can extract the key as wel + self.shared_state + .should_send_close + .store(false, Ordering::Relaxed); + // Once we have extracted the key, we can't use it anymore ctx.stop_worker(self.addresses.encryptor.clone()).await?; diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs index 1fd2a7757c9..80cf2f59e99 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs @@ -307,7 +307,7 @@ impl Handshake { let old_k = state.k.replace(new_k); if let Some(old_k) = old_k { - self.vault.delete_aead_secret_key(old_k).await?; + self.vault.delete_aead_secret_key(&old_k).await?; } state.n = 0; @@ -334,7 +334,7 @@ impl Handshake { let k2 = self.vault.convert_secret_buffer_to_aead_key(k2).await?; self.vault.delete_secret_buffer(state.take_ck()?).await?; - self.vault.delete_aead_secret_key(state.take_k()?).await?; + self.vault.delete_aead_secret_key(&state.take_k()?).await?; Ok((k1, k2)) } diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channels/storage/secure_channel_repository.rs b/implementations/rust/ockam/ockam_identity/src/secure_channels/storage/secure_channel_repository.rs index ff8e6fbfa60..07928aa6a23 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channels/storage/secure_channel_repository.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channels/storage/secure_channel_repository.rs @@ -18,6 +18,8 @@ pub struct PersistedSecureChannel { } impl PersistedSecureChannel { + // TODO: remove when persistent secure channels are no longer required + #[allow(dead_code)] pub(crate) fn new( role: Role, my_identifier: Identifier, diff --git a/implementations/rust/ockam/ockam_identity/src/vault.rs b/implementations/rust/ockam/ockam_identity/src/vault.rs index c534dd5fc96..0392494a5a1 100644 --- a/implementations/rust/ockam/ockam_identity/src/vault.rs +++ b/implementations/rust/ockam/ockam_identity/src/vault.rs @@ -87,6 +87,7 @@ impl Vault { } /// Create [`SoftwareVaultForAtRestEncryption`] with an in-memory storage + #[cfg(feature = "storage")] pub async fn create_encryption_at_rest_vault() -> Result> { Ok(Arc::new(SoftwareVaultForAtRestEncryption::new(Arc::new( SecretsSqlxDatabase::create().await?, diff --git a/implementations/rust/ockam/ockam_node/src/context/send_message.rs b/implementations/rust/ockam/ockam_node/src/context/send_message.rs index f8f66733e5a..4457b17679e 100644 --- a/implementations/rust/ockam/ockam_node/src/context/send_message.rs +++ b/implementations/rust/ockam/ockam_node/src/context/send_message.rs @@ -159,7 +159,7 @@ impl Context { child_ctx.set_tracing_context(self.tracing_context()); child_ctx - .send_from_address_impl(route, msg, self.address(), vec![], options.on_drop) + .send_from_address_impl(route, msg, child_ctx.address(), vec![], options.on_drop) .await?; child_ctx .receive_extended::( diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/common.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/common.rs index 4c585c53d01..bed83547ac6 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/common.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/common.rs @@ -1,4 +1,5 @@ use crate::{AeadSecretKeyHandle, HandleToSecret, SecretBufferHandle}; +use alloc::vec; use ockam_core::compat::rand::{thread_rng, RngCore}; pub(super) fn generate_random_handle() -> HandleToSecret { diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/types.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/types.rs index 942640fc688..c40bb6e4f6d 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/types.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/types.rs @@ -1,4 +1,5 @@ use cfg_if::cfg_if; +use minicbor::{CborLen, Decode, Encode}; use zeroize::{Zeroize, ZeroizeOnDrop}; use ockam_core::compat::vec::Vec; @@ -22,10 +23,10 @@ impl X25519SecretKey { } /// Buffer with sensitive data, like HKDF output. -#[derive(Eq, PartialEq, Clone, Zeroize, ZeroizeOnDrop)] -pub struct BufferSecret(Vec); +#[derive(Eq, PartialEq, Clone, Encode, Decode, CborLen, Zeroize, ZeroizeOnDrop)] +pub struct SecretBuffer(#[n(1)] Vec); -impl BufferSecret { +impl SecretBuffer { /// Constructor. pub fn new(data: Vec) -> Self { Self(data) diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_encryption_at_rest.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_encryption_at_rest.rs index 48046ed0b47..9ba9bb34b09 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_encryption_at_rest.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_encryption_at_rest.rs @@ -1,15 +1,18 @@ +use alloc::boxed::Box; +use alloc::vec::Vec; use ockam_core::compat::collections::BTreeMap; use ockam_core::compat::rand::{thread_rng, RngCore}; use ockam_core::compat::sync::{Arc, RwLock}; -use ockam_core::compat::vec::Vec; use ockam_core::{async_trait, Result}; use super::make_aes; -use crate::storage::{SecretsRepository, SecretsSqlxDatabase}; +use crate::storage::SecretsRepository; +#[cfg(feature = "storage")] +use crate::storage::SecretsSqlxDatabase; use crate::software::vault_for_secure_channels::common::generate_aead_handle; use crate::{ - AeadSecret, AeadSecretKeyHandle, BufferSecret, VaultError, VaultForEncryptionAtRest, + AeadSecret, AeadSecretKeyHandle, SecretBuffer, VaultError, VaultForEncryptionAtRest, AEAD_SECRET_LENGTH, AES_GCM_TAGSIZE, AES_NONCE_LENGTH, }; @@ -18,6 +21,7 @@ pub struct SoftwareVaultForAtRestEncryption { ephemeral_aead_secrets: Arc>>, secrets_repository: Arc, } + #[async_trait] impl VaultForEncryptionAtRest for SoftwareVaultForAtRestEncryption { async fn aead_encrypt( @@ -63,7 +67,9 @@ impl VaultForEncryptionAtRest for SoftwareVaultForAtRestEncryption { ) -> Result { let secret = self.get_aead_secret(secret_key_handle).await?; let new_key_secret = self.rekey(secret, 1).await?; - let new_key_handle = self.import_aead_key(new_key_secret.0.to_vec()).await?; + let new_key_handle = self + .import_aead_key(SecretBuffer::new(new_key_secret.0.to_vec())) + .await?; self.secrets_repository .delete_aead_secret(secret_key_handle) .await?; @@ -74,9 +80,7 @@ impl VaultForEncryptionAtRest for SoftwareVaultForAtRestEncryption { Ok(new_key_handle) } - async fn import_aead_key(&self, secret: Vec) -> Result { - let secret = BufferSecret::new(secret); - + async fn import_aead_key(&self, secret: SecretBuffer) -> Result { if secret.data().len() != AEAD_SECRET_LENGTH { return Err(VaultError::InvalidSecretLength)?; } diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs index aca08877eba..f5aec44f2cc 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs @@ -1,3 +1,4 @@ +use alloc::boxed::Box; use sha2::{Digest, Sha256}; use tracing::instrument; @@ -18,15 +19,15 @@ use crate::software::vault_for_secure_channels::common::{ }; use crate::software::vault_for_secure_channels::types::AES_GCM_TAGSIZE; use crate::{ - AeadSecret, AeadSecretKeyHandle, BufferSecret, HKDFNumberOfOutputs, HandleToSecret, HashOutput, - HkdfOutput, SecretBufferHandle, SoftwareVaultForVerifyingSignatures, VaultError, + AeadSecret, AeadSecretKeyHandle, HKDFNumberOfOutputs, HandleToSecret, HashOutput, HkdfOutput, + SecretBuffer, SecretBufferHandle, SoftwareVaultForVerifyingSignatures, VaultError, VaultForSecureChannels, X25519PublicKey, X25519SecretKey, X25519SecretKeyHandle, - AEAD_SECRET_LENGTH, + AEAD_SECRET_LENGTH, AES_NONCE_LENGTH, }; /// [`SecureChannelVault`] implementation using software pub struct SoftwareVaultForSecureChannels { - ephemeral_buffer_secrets: Arc>>, + ephemeral_buffer_secrets: Arc>>, ephemeral_aead_secrets: Arc>>, ephemeral_x25519_secrets: Arc>>, secrets_repository: Arc, @@ -139,11 +140,11 @@ impl SoftwareVaultForSecureChannels { fn ecdh_internal( secret: X25519SecretKey, peer_public_key: X25519PublicKey, - ) -> Result { + ) -> Result { let peer_public_key = Self::import_x25519_public_key(peer_public_key); let secret_key = Self::import_x25519_secret_key(secret); let dh = secret_key.diffie_hellman(&peer_public_key); - Ok(BufferSecret::new(dh.as_bytes().to_vec())) + Ok(SecretBuffer::new(dh.as_bytes().to_vec())) } fn generate_x25519_secret() -> X25519SecretKey { @@ -152,7 +153,7 @@ impl SoftwareVaultForSecureChannels { X25519SecretKey::new(secret.to_bytes()) } - fn import_buffer_secret_impl(&self, secret: BufferSecret) -> SecretBufferHandle { + fn import_buffer_secret_impl(&self, secret: SecretBuffer) -> SecretBufferHandle { let handle = generate_buffer_handle(); self.ephemeral_buffer_secrets @@ -177,7 +178,7 @@ impl SoftwareVaultForSecureChannels { .ok_or_else(|| VaultError::KeyNotFound)?) } - async fn get_buffer_secret(&self, handle: &SecretBufferHandle) -> Result { + async fn get_buffer_secret(&self, handle: &SecretBufferHandle) -> Result { match self.ephemeral_buffer_secrets.read().unwrap().get(handle) { Some(secret) => Ok(secret.clone()), None => Err(VaultError::KeyNotFound)?, @@ -190,6 +191,31 @@ impl SoftwareVaultForSecureChannels { None => Err(VaultError::KeyNotFound)?, } } + + async fn raw_rekey(&self, secret_key_handle: &AeadSecretKeyHandle, n: u16) -> Result> { + if n == 0 { + return Err(VaultError::InvalidRekeyCount)?; + } + + const MAX_NONCE: [u8; AES_NONCE_LENGTH] = [ + 0x00, 0x00, 0x00, 0x00, // we only use 8 bytes of nonce + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // u64::MAX + ]; + + let mut new_key_buffer = vec![0u8; AEAD_SECRET_LENGTH + AES_GCM_TAGSIZE]; + let mut counter = n; + + while counter > 0 { + let secret = self.get_aead_secret(secret_key_handle).await?; + let aes = make_aes(&secret); + aes.encrypt_message(&mut new_key_buffer, &MAX_NONCE, &[])?; + + counter -= 1; + } + + new_key_buffer.truncate(AEAD_SECRET_LENGTH); + Ok(new_key_buffer) + } } #[async_trait] @@ -221,7 +247,7 @@ impl VaultForSecureChannels for SoftwareVaultForSecureChannels { let ikm = match input_key_material { Some(ikm) => self.get_buffer_secret(ikm).await?, - None => BufferSecret::new(vec![]), + None => SecretBuffer::new(vec![]), }; let salt = self.get_buffer_secret(salt).await?; @@ -252,7 +278,7 @@ impl VaultForSecureChannels for SoftwareVaultForSecureChannels { let output = chunks .into_iter() - .map(|chunk| self.import_buffer_secret_impl(BufferSecret::new(chunk.to_vec()))) + .map(|chunk| self.import_buffer_secret_impl(SecretBuffer::new(chunk.to_vec()))) .collect::>(); use crate::Sha256HkdfOutput; @@ -294,30 +320,18 @@ impl VaultForSecureChannels for SoftwareVaultForSecureChannels { secret_key_handle: &AeadSecretKeyHandle, n: u16, ) -> Result { - if n == 0 { - return Err(VaultError::InvalidRekeyCount)?; - } - - const MAX_NONCE: [u8; 12] = [ - 0x00, 0x00, 0x00, 0x00, // we only use 8 bytes of nonce - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // u64::MAX - ]; - - let mut new_key_buffer = vec![0u8; 32 + AES_GCM_TAGSIZE]; - let mut counter = n; - - while counter > 0 { - let secret = self.get_aead_secret(secret_key_handle).await?; - let aes = make_aes(&secret); - aes.encrypt_message(&mut new_key_buffer, &MAX_NONCE, &[])?; - - counter -= 1; - } - + let new_key_buffer = self.raw_rekey(secret_key_handle, n).await?; let buffer = self.import_secret_buffer(new_key_buffer).await?; self.convert_secret_buffer_to_aead_key(buffer).await } + #[instrument(skip_all)] + async fn export_rekey(&self, secret_key_handle: &AeadSecretKeyHandle) -> Result { + let buffer = SecretBuffer::new(self.raw_rekey(secret_key_handle, 1).await?); + self.delete_aead_secret_key(secret_key_handle).await?; + Ok(buffer) + } + #[instrument(skip_all)] async fn persist_aead_key(&self, secret_key_handle: &AeadSecretKeyHandle) -> Result<()> { let secret = self.get_aead_secret(secret_key_handle).await?; @@ -394,7 +408,7 @@ impl VaultForSecureChannels for SoftwareVaultForSecureChannels { } async fn import_secret_buffer(&self, buffer: Vec) -> Result { - Ok(self.import_buffer_secret_impl(BufferSecret::new(buffer))) + Ok(self.import_buffer_secret_impl(SecretBuffer::new(buffer))) } async fn delete_secret_buffer(&self, secret_buffer_handle: SecretBufferHandle) -> Result { @@ -440,12 +454,15 @@ impl VaultForSecureChannels for SoftwareVaultForSecureChannels { } #[instrument(skip_all)] - async fn delete_aead_secret_key(&self, secret_key_handle: AeadSecretKeyHandle) -> Result { + async fn delete_aead_secret_key( + &self, + secret_key_handle: &AeadSecretKeyHandle, + ) -> Result { Ok(self .ephemeral_aead_secrets .write() .unwrap() - .remove(&secret_key_handle) + .remove(secret_key_handle) .is_some()) } } diff --git a/implementations/rust/ockam/ockam_vault/src/traits/vault_for_at_rest_encryption.rs b/implementations/rust/ockam/ockam_vault/src/traits/vault_for_at_rest_encryption.rs index 3097dd2fbc6..5e1dee0f23f 100644 --- a/implementations/rust/ockam/ockam_vault/src/traits/vault_for_at_rest_encryption.rs +++ b/implementations/rust/ockam/ockam_vault/src/traits/vault_for_at_rest_encryption.rs @@ -1,5 +1,5 @@ -use crate::AeadSecretKeyHandle; - +use crate::{AeadSecretKeyHandle, SecretBuffer}; +use alloc::boxed::Box; use ockam_core::{async_trait, Result}; /// Vault for verifying signatures and computing SHA-256. @@ -34,5 +34,5 @@ pub trait VaultForEncryptionAtRest: Send + Sync + 'static { ) -> Result; /// Import an AES-GCM key and return a handle to it - async fn import_aead_key(&self, secret: Vec) -> Result; + async fn import_aead_key(&self, secret: SecretBuffer) -> Result; } diff --git a/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs b/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs index 76236478fc0..cfb026cb3ae 100644 --- a/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs +++ b/implementations/rust/ockam/ockam_vault/src/traits/vault_for_secure_channels.rs @@ -1,5 +1,5 @@ use crate::{ - AeadSecretKeyHandle, HashOutput, HkdfOutput, SecretBufferHandle, X25519PublicKey, + AeadSecretKeyHandle, HashOutput, HkdfOutput, SecretBuffer, SecretBufferHandle, X25519PublicKey, X25519SecretKeyHandle, }; use minicbor::{CborLen, Decode, Encode}; @@ -69,6 +69,9 @@ pub trait VaultForSecureChannels: Send + Sync + 'static { n: u16, ) -> Result; + /// Perform rekey, export the new key, and delete the previous key. + async fn export_rekey(&self, secret_key_handle: &AeadSecretKeyHandle) -> Result; + /// Persist an existing AEAD key. async fn persist_aead_key(&self, secret_key_handle: &AeadSecretKeyHandle) -> Result<()>; @@ -118,5 +121,6 @@ pub trait VaultForSecureChannels: Send + Sync + 'static { ) -> Result; /// Delete AEAD Key. - async fn delete_aead_secret_key(&self, secret_key_handle: AeadSecretKeyHandle) -> Result; + async fn delete_aead_secret_key(&self, secret_key_handle: &AeadSecretKeyHandle) + -> Result; } diff --git a/implementations/rust/ockam/ockam_vault/src/types/hashes.rs b/implementations/rust/ockam/ockam_vault/src/types/hashes.rs index fa83ffb9178..6503ed7a6e3 100644 --- a/implementations/rust/ockam/ockam_vault/src/types/hashes.rs +++ b/implementations/rust/ockam/ockam_vault/src/types/hashes.rs @@ -2,6 +2,7 @@ use crate::{HandleToSecret, SecretBufferHandle}; use cfg_if::cfg_if; use minicbor::{CborLen, Decode, Encode}; use ockam_core::compat::vec::Vec; +use serde::{Deserialize, Serialize}; /// SHA256 digest length pub const SHA256_LENGTH: usize = 32; @@ -11,7 +12,9 @@ pub const SHA256_LENGTH: usize = 32; pub struct Sha256Output(#[cbor(n(0), with = "minicbor::bytes")] pub [u8; SHA256_LENGTH]); /// Handle to an AES-256 Secret Key. -#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Encode, Decode, CborLen)] +#[derive( + Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize, Encode, Decode, CborLen, +)] pub struct AeadSecretKeyHandle(#[n(0)] pub AeadSecretKeyHandleType); impl AeadSecretKeyHandle { @@ -41,7 +44,7 @@ cfg_if! { pub struct HkdfOutput(#[n(0)] pub Sha256HkdfOutput); /// Handle to an AES-256 Secret Key. - #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Encode, Decode, CborLen)] + #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize, Encode, Decode, CborLen)] pub struct Aes256GcmSecretKeyHandle(#[n(0)] pub HandleToSecret); impl Aes256GcmSecretKeyHandle { diff --git a/implementations/rust/ockam/ockam_vault/src/types/secrets.rs b/implementations/rust/ockam/ockam_vault/src/types/secrets.rs index 492dd42eef7..3bbc4b5f427 100644 --- a/implementations/rust/ockam/ockam_vault/src/types/secrets.rs +++ b/implementations/rust/ockam/ockam_vault/src/types/secrets.rs @@ -1,10 +1,11 @@ use core::fmt::Debug; use minicbor::{CborLen, Decode, Encode}; use ockam_core::compat::vec::Vec; +use serde::{Deserialize, Serialize}; /// Implementation-specific arbitrary vector of bytes that allows a concrete Vault implementation /// to address a specific secret that it stores. -#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Encode, Decode, CborLen)] +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize, Encode, Decode, CborLen)] #[rustfmt::skip] pub struct HandleToSecret(#[cbor(n(0), with = "minicbor::bytes")] Vec);