diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 57a9914a7..618d8cfca 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -124,7 +124,7 @@ jobs: misc_checks: name: miscellaneous - runs-on: ubuntu-latest + runs-on: macos-latest steps: - uses: Avarok-Cybersecurity/gh-actions-deps@master # - name: Install Valgrind diff --git a/Cargo.toml b/Cargo.toml index 4b0903ffb..f53b80b98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,8 @@ enum_primitive = { default-features = false, version = "0.1.1" } aes-gcm = { version = "0.10.3", default-features = false } chacha20poly1305 = { version = "0.10.1", default-features = false } log = { default-features = false, version = "0.4.17" } -strum = { version = "0.26.2", default-features = false } +strum = { version = "0.27.1", default-features = false } +strum_macros = { version = "0.27.1", default-features = false } sha3 = { version = "0.10", default-features = false } kyber-pke = { version = "0.5.0", default-features = false } packed_struct = { version = "0.10.1" } diff --git a/Makefile.toml b/Makefile.toml index 41ba9ab24..b3af99155 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -80,23 +80,13 @@ command = "choco" args = ["install", "-y", "llvm", "openssl", "cmake"] [tasks.docs] -script_runner = "@rust" -env = { "CARGO_MAKE_RUST_SCRIPT_PROVIDER" = "cargo-script" } +script_runner = "@duckscript" dependencies = ["docs-html"] script = ''' -//! ```cargo -fn main() { - std::fs::copy( - "./resources/avarok.png", - "./target/doc/citadel_sdk/avarok.png", - ) - .expect("Failed to copy crate logo when building documentation."); - std::fs::copy( - "./resources/favicon.png", - "./target/doc/citadel_sdk/favicon.png", - ) - .expect("Failed to copy crate favicon when building documentation."); -} +# Copy logo +cp ./resources/avarok.png ./target/doc/citadel_sdk/avarok.png +# Copy favicon +cp ./resources/favicon.png ./target/doc/citadel_sdk/favicon.png ''' [tasks.install-llvm-tools] diff --git a/citadel_crypt/src/scramble/crypt_splitter.rs b/citadel_crypt/src/scramble/crypt_splitter.rs index d0bf9d74e..a5c10abd2 100644 --- a/citadel_crypt/src/scramble/crypt_splitter.rs +++ b/citadel_crypt/src/scramble/crypt_splitter.rs @@ -283,7 +283,7 @@ where debug_assert_ne!(cfg.last_plaintext_wave_length, 0); - if msg_pqc.params.encryption_algorithm != EncryptionAlgorithm::Kyber + if msg_pqc.params.encryption_algorithm != EncryptionAlgorithm::KyberHybrid && matches!(&transfer_type, TransferType::FileTransfer) { debug_assert_eq!(cfg.packets_needed, packets.len() as _); @@ -430,6 +430,7 @@ pub struct GroupReceiver { packets_received_order: BitVec, waves_received: BitVec, packets_needed: usize, + packets_received: usize, last_packet_recv_time: Instant, max_payload_size: usize, /// All packets will necessarily be the same size, except for the last packet (although, it is possible for it to be the same size) @@ -623,6 +624,7 @@ impl GroupReceiver { unified_plaintext_slab, temp_wave_store, packets_received_order, + packets_received: 0, packets_needed: cfg.packets_needed as usize, last_packet_recv_time, max_payload_size: cfg.max_payload_size as usize, @@ -658,6 +660,8 @@ impl GroupReceiver { }; if !is_received { + self.packets_received += 1; + log::trace!(target: "citadel", "[FILE TRANSFER] Packet {}/{} received", self.packets_received, self.packets_needed); // Now, take the ciphertext and place it into the buffer let wave_store = self.temp_wave_store.get_mut(&wave_id); @@ -847,7 +851,6 @@ impl GroupReceiver { plaintext: &[u8], max_plaintext_wave_length: usize, ) -> Range { - // TODO!!!!! remove unwrap let plaintext_length = plaintext.len(); let start_idx = wave_id as usize * max_plaintext_wave_length; let end_idx = start_idx + plaintext_length; diff --git a/citadel_crypt/tests/primary.rs b/citadel_crypt/tests/primary.rs index bd21c8f6c..f1c853762 100644 --- a/citadel_crypt/tests/primary.rs +++ b/citadel_crypt/tests/primary.rs @@ -380,7 +380,7 @@ mod tests { SigAlgorithm::None )] #[case( - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, KemAlgorithm::Kyber, SigAlgorithm::Falcon1024 )] @@ -524,7 +524,7 @@ mod tests { SigAlgorithm::None )] #[case( - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, KemAlgorithm::Kyber, SigAlgorithm::Falcon1024 )] @@ -618,7 +618,7 @@ mod tests { SigAlgorithm::None )] #[case( - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, KemAlgorithm::Kyber, SigAlgorithm::Falcon1024 )] @@ -660,7 +660,7 @@ mod tests { SigAlgorithm::None )] #[case( - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, KemAlgorithm::Kyber, SigAlgorithm::Falcon1024 )] @@ -810,7 +810,7 @@ mod tests { SigAlgorithm::None )] #[case( - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, KemAlgorithm::Kyber, SigAlgorithm::Falcon1024 )] @@ -852,7 +852,7 @@ mod tests { SigAlgorithm::None )] #[case( - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, KemAlgorithm::Kyber, SigAlgorithm::Falcon1024 )] @@ -1000,7 +1000,7 @@ mod tests { SigAlgorithm::None )] #[case( - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, KemAlgorithm::Kyber, SigAlgorithm::Falcon1024 )] @@ -1059,7 +1059,7 @@ mod tests { SigAlgorithm::None )] #[case( - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, KemAlgorithm::Kyber, SigAlgorithm::Falcon1024 )] diff --git a/citadel_pqcrypto/src/export.rs b/citadel_pqcrypto/src/export.rs index 13de6a6ad..9fbc5097b 100644 --- a/citadel_pqcrypto/src/export.rs +++ b/citadel_pqcrypto/src/export.rs @@ -189,7 +189,7 @@ pub(crate) fn keys_to_aead_store( ) } - EncryptionAlgorithm::Kyber => { + EncryptionAlgorithm::KyberHybrid => { let kem_alg = params.kem_algorithm; let sig_alg = params.sig_algorithm; diff --git a/citadel_pqcrypto/src/lib.rs b/citadel_pqcrypto/src/lib.rs index 791c7f7e4..cac674762 100644 --- a/citadel_pqcrypto/src/lib.rs +++ b/citadel_pqcrypto/src/lib.rs @@ -472,7 +472,7 @@ impl PostQuantumContainer { } fn get_decryption_key(&self) -> Option<&dyn AeadModule> { - if let EncryptionAlgorithm::Kyber = self.params.encryption_algorithm { + if let EncryptionAlgorithm::KyberHybrid = self.params.encryption_algorithm { // use multi-modal asymmetric + symmetric ratcheted encryption // alice's key is in alice, bob's key is in bob. Thus, use encryption key self.get_encryption_key() @@ -1145,7 +1145,7 @@ impl EncryptionAlgorithmExt for EncryptionAlgorithm { match self { Self::AES_GCM_256 => AES_GCM_NONCE_LENGTH_BYTES, Self::ChaCha20Poly_1305 => CHA_CHA_NONCE_LENGTH_BYTES, - Self::Kyber => KYBER_NONCE_LENGTH_BYTES, + Self::KyberHybrid => KYBER_NONCE_LENGTH_BYTES, Self::Ascon80pq => ASCON_NONCE_LENGTH_BYTES, } } @@ -1159,7 +1159,7 @@ impl EncryptionAlgorithmExt for EncryptionAlgorithm { Self::ChaCha20Poly_1305 => plaintext_length + SYMMETRIC_CIPHER_OVERHEAD, Self::Ascon80pq => plaintext_length + SYMMETRIC_CIPHER_OVERHEAD, // Add 32 for internal apendees - Self::Kyber => { + Self::KyberHybrid => { const LENGTH_FIELD: usize = 8; let signature_len = functions::signature_bytes(); @@ -1183,7 +1183,7 @@ impl EncryptionAlgorithmExt for EncryptionAlgorithm { Self::AES_GCM_256 => Some(ciphertext.len() - 16), Self::ChaCha20Poly_1305 => Some(ciphertext.len() - 16), Self::Ascon80pq => Some(ciphertext.len() - 16), - Self::Kyber => kyber_pke::plaintext_len(ciphertext), + Self::KyberHybrid => kyber_pke::plaintext_len(ciphertext), } } } diff --git a/citadel_pqcrypto/tests/primary.rs b/citadel_pqcrypto/tests/primary.rs index f015658ce..176f5ac9e 100644 --- a/citadel_pqcrypto/tests/primary.rs +++ b/citadel_pqcrypto/tests/primary.rs @@ -210,7 +210,7 @@ mod tests { const HEADER_LEN: usize = 50; let kem_algorithm = KemAlgorithm::Kyber; - let encryption_algorithm = EncryptionAlgorithm::Kyber; + let encryption_algorithm = EncryptionAlgorithm::KyberHybrid; let signature_algorithm = SigAlgorithm::Falcon1024; let (alice_container, bob_container) = gen( @@ -415,7 +415,7 @@ mod tests { if algorithm == KemAlgorithm::Kyber { run::>( algorithm.as_u8(), - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, SigAlgorithm::Falcon1024, &PRE_SHARED_KEYS, &PRE_SHARED_KEYS, @@ -430,7 +430,7 @@ mod tests { citadel_logging::setup_log(); run::>( KemAlgorithm::Kyber.as_u8(), - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, SigAlgorithm::Falcon1024, &PRE_SHARED_KEYS, &PRE_SHARED_KEYS, @@ -572,7 +572,7 @@ mod tests { #[test] fn test_bad_crypto_params() { - let bad_params = EncryptionAlgorithm::Kyber + KemAlgorithm::Kyber; + let bad_params = EncryptionAlgorithm::KyberHybrid + KemAlgorithm::Kyber; assert!(validate_crypto_params(&bad_params).is_err()); } } diff --git a/citadel_proto/src/error.rs b/citadel_proto/src/error.rs index 7fbd1f4bd..d176a7ab9 100644 --- a/citadel_proto/src/error.rs +++ b/citadel_proto/src/error.rs @@ -33,6 +33,7 @@ use crate::prelude::NodeRequest; use citadel_crypt::misc::CryptError; +use citadel_crypt::ratchets::Ratchet; use citadel_io::tokio::sync::mpsc::error::SendError; use citadel_user::misc::AccountError; use std::error::Error; @@ -131,6 +132,13 @@ impl From> for NetworkErro } } +// Specific implementation for Box>> +impl From>>> for NetworkError { + fn from(err: Box>>) -> Self { + NetworkError::Generic(err.to_string()) + } +} + impl From for NetworkError { fn from(err: AccountError) -> Self { NetworkError::Generic(err.into_string()) diff --git a/citadel_proto/src/proto/misc/net.rs b/citadel_proto/src/proto/misc/net.rs index 82f159672..e4bdcb53e 100644 --- a/citadel_proto/src/proto/misc/net.rs +++ b/citadel_proto/src/proto/misc/net.rs @@ -78,7 +78,7 @@ pub fn safe_split_stream), + Tls(Box>), // local addr is first addr, remote addr is final addr Quic( SendStream, @@ -307,14 +307,25 @@ impl GenericNetworkListener { let future = async move { loop { - let (stream, addr) = listener + let next_connection = listener .next() .await - .ok_or_else(|| generic_error("TLS listener died"))??; - log::trace!(target: "citadel", "Received raw TLS stream from {:?}: {:?}", addr, stream); - send.send(Ok((GenericNetworkStream::Tls(stream.into()), addr))) - .await - .map_err(|err| generic_error(err.to_string()))?; + .ok_or_else(|| generic_error("TLS listener died"))?; + + match next_connection { + Ok((stream, addr)) => { + log::trace!(target: "citadel", "Received raw TLS stream from {:?}: {:?}", addr, stream); + if let Err(err) = send + .send(Ok((GenericNetworkStream::Tls(Box::new(citadel_wire::exports::tokio_rustls::TlsStream::Server(stream))), addr))) + .await + { + log::error!(target: "citadel", "Failed to send TLS handshake packet: {err:?}"); + } + } + Err(err) => { + log::debug!(target: "citadel", "TLS Stream died: {err:?}") + } + } } }; @@ -485,7 +496,7 @@ impl QuicListener { log::trace!(target: "citadel", "RECV {:?} from {:?}", &conn, addr); send.send(Ok((conn, tx, rx, addr, endpoint.clone()))) .await - .map_err(|err| generic_error(err.to_string())) + .map_err(generic_error) }) .await?; } diff --git a/citadel_proto/src/proto/misc/udp_internal_interface.rs b/citadel_proto/src/proto/misc/udp_internal_interface.rs index a1e562c86..7f40c0d2b 100644 --- a/citadel_proto/src/proto/misc/udp_internal_interface.rs +++ b/citadel_proto/src/proto/misc/udp_internal_interface.rs @@ -137,7 +137,7 @@ impl QuicUdpSocketConnector { yield conn_stream.read_datagram() .await .map(|packet| (BytesMut::from(&packet[..]), addr)) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?; + .map_err(|err| std::io::Error::other(err.to_string()))?; } }); diff --git a/citadel_proto/src/proto/node.rs b/citadel_proto/src/proto/node.rs index 48ae098bd..4c1d9cbcd 100644 --- a/citadel_proto/src/proto/node.rs +++ b/citadel_proto/src/proto/node.rs @@ -375,8 +375,7 @@ impl CitadelNode { match underlying_proto { ServerUnderlyingProtocol::Tcp(Some(listener)) => { let listener = listener.lock().take().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::Other, + std::io::Error::other( "TCP listener already taken", ) })?; @@ -566,7 +565,7 @@ impl CitadelNode { ) .await .map_err(|err| io::Error::new(io::ErrorKind::ConnectionRefused, err))?; - Ok((GenericNetworkStream::Tls(stream.into()), None)) + Ok((GenericNetworkStream::Tls(Box::new(citadel_wire::exports::tokio_rustls::TlsStream::Client(stream))), None)) } FirstPacket::Quic { domain, diff --git a/citadel_proto/src/proto/node_request.rs b/citadel_proto/src/proto/node_request.rs index 29e813813..b6a3f59ad 100644 --- a/citadel_proto/src/proto/node_request.rs +++ b/citadel_proto/src/proto/node_request.rs @@ -28,12 +28,10 @@ use crate::auth::AuthenticationRequest; use crate::prelude::{GroupBroadcast, PeerSignal, VirtualTargetType}; use crate::proto::state_container::VirtualConnectionType; use citadel_crypt::scramble::streaming_crypt_scrambler::ObjectSource; -use citadel_types::crypto::SecurityLevel; +use citadel_types::crypto::{PreSharedKey, SecurityLevel}; use citadel_types::proto::TransferType; use citadel_types::proto::{ConnectMode, SessionSecuritySettings, UdpMode}; use citadel_user::auth::proposed_credentials::ProposedCredentials; -use serde::{Deserialize, Serialize}; -use sha3::Digest; use std::fmt::{Debug, Formatter}; use std::net::SocketAddr; use std::path::PathBuf; @@ -160,32 +158,3 @@ impl Debug for NodeRequest { write!(f, "NodeRequest") } } - -#[derive(Default, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)] -pub struct PreSharedKey { - passwords: Vec>, -} - -impl PreSharedKey { - /// Adds a password to the session password list. Both connecting nodes - /// must have matching passwords in order to establish a connection. - /// Note: The password is hashed using SHA-256 before being added to the list to increase security. - pub fn add_password>(mut self, password: T) -> Self { - let mut hasher = sha3::Sha3_256::default(); - hasher.update(password.as_ref()); - self.passwords.push(hasher.finalize().to_vec()); - self - } -} - -impl AsRef<[Vec]> for PreSharedKey { - fn as_ref(&self) -> &[Vec] { - &self.passwords - } -} - -impl> From for PreSharedKey { - fn from(password: T) -> Self { - PreSharedKey::default().add_password(password) - } -} diff --git a/citadel_proto/src/proto/node_result.rs b/citadel_proto/src/proto/node_result.rs index eaf7b597e..d2aba725e 100644 --- a/citadel_proto/src/proto/node_result.rs +++ b/citadel_proto/src/proto/node_result.rs @@ -182,7 +182,7 @@ pub enum NodeResult { /// When de-registration occurs. Third is_personal, Fourth is true if success, false otherwise DeRegistration(DeRegistration), /// Connection succeeded for the cid self.0. bool is "is personal" - ConnectSuccess(ConnectSuccess), + ConnectSuccess(Box>), /// The connection was a failure ConnectFail(ConnectFail), ReKeyResult(ReKeyResult), @@ -204,7 +204,7 @@ pub enum NodeResult { /// An internal error occurred InternalServerError(InternalServerError), /// A channel was created, with channel_id = ticket (same as post-connect ticket received) - PeerChannelCreated(PeerChannelCreated), + PeerChannelCreated(Box>), /// A list of running sessions SessionList(SessionList), /// For shutdowns @@ -213,7 +213,7 @@ pub enum NodeResult { impl NodeResult { pub fn is_connect_success_type(&self) -> bool { - matches!(self, NodeResult::ConnectSuccess(ConnectSuccess { .. })) + matches!(self, NodeResult::ConnectSuccess(..)) } pub fn callback_key(&self) -> Option { @@ -232,11 +232,9 @@ impl NodeResult { ticket_opt: t, .. }) => Some(CallbackKey::new((*t)?, *session_cid)), - NodeResult::ConnectSuccess(ConnectSuccess { - ticket: t, - session_cid, - .. - }) => Some(CallbackKey::new(*t, *session_cid)), + NodeResult::ConnectSuccess(ref cs) => { + Some(CallbackKey::new(cs.ticket, cs.session_cid)) + } NodeResult::ConnectFail(ConnectFail { ticket: t, cid_opt, @@ -269,9 +267,9 @@ impl NodeResult { ticket: t, event: _, }) => Some(CallbackKey::new(*t, *session_cid)), - NodeResult::PeerChannelCreated(PeerChannelCreated { - ticket: t, channel, .. - }) => Some(CallbackKey::new(*t, channel.get_session_cid())), + NodeResult::PeerChannelCreated(ref pcc) => { + Some(CallbackKey::new(pcc.ticket, pcc.channel.get_session_cid())) + } NodeResult::GroupChannelCreated(GroupChannelCreated { ticket: t, channel: _, diff --git a/citadel_proto/src/proto/packet_crafter.rs b/citadel_proto/src/proto/packet_crafter.rs index ee3b893a5..8bb667d11 100644 --- a/citadel_proto/src/proto/packet_crafter.rs +++ b/citadel_proto/src/proto/packet_crafter.rs @@ -391,6 +391,7 @@ pub(crate) mod do_connect { use zerocopy::{I64, U128, U32, U64}; use crate::constants::HDP_HEADER_BYTE_LEN; + use crate::prelude::Ticket; use crate::proto::packet::{packet_flags, HdpHeader}; use crate::proto::peer::peer_layer::MailboxTransfer; use citadel_crypt::ratchets::Ratchet; @@ -416,6 +417,7 @@ pub(crate) mod do_connect { timestamp: i64, security_level: SecurityLevel, backend_type: &BackendType, + ticket: Ticket, ) -> BytesMut { let header = HdpHeader { protocol_version: (*crate::constants::PROTOCOL_VERSION).into(), @@ -424,7 +426,7 @@ pub(crate) mod do_connect { algorithm: 0, security_level: security_level.value(), // place username len here to allow the other end to know where to split the payload - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(ratchet.get_cid()), @@ -484,6 +486,7 @@ pub(crate) mod do_connect { timestamp: i64, security_level: SecurityLevel, backend_type: &BackendType, + ticket: Ticket, ) -> BytesMut { let payload = DoConnectFinalStatusPacket { mailbox, @@ -506,7 +509,7 @@ pub(crate) mod do_connect { cmd_aux, algorithm: 0, security_level: security_level.value(), - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(is_filesystem as u64), wave_id: U32::new(0), session_cid: U64::new(ratchet.get_cid()), @@ -533,6 +536,7 @@ pub(crate) mod do_connect { ratchet: &R, timestamp: i64, security_level: SecurityLevel, + ticket: Ticket, ) -> BytesMut { let header = HdpHeader { protocol_version: (*crate::constants::PROTOCOL_VERSION).into(), @@ -540,7 +544,7 @@ pub(crate) mod do_connect { cmd_aux: packet_flags::cmd::aux::do_connect::SUCCESS_ACK, algorithm: 0, security_level: security_level.value(), - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(ratchet.get_cid()), @@ -602,6 +606,7 @@ pub(crate) mod do_register { use zerocopy::{I64, U128, U32, U64}; use crate::constants::HDP_HEADER_BYTE_LEN; + use crate::prelude::Ticket; use crate::proto::packet::{packet_flags, HdpHeader}; use citadel_crypt::endpoint_crypto_container::EndpointRatchetConstructor; use citadel_crypt::ratchets::Ratchet; @@ -628,6 +633,7 @@ pub(crate) mod do_register { transfer: >::AliceToBobWireTransfer, passwordless: bool, proposed_cid: u64, + ticket: Ticket, ) -> BytesMut { let header = HdpHeader { protocol_version: (*crate::constants::PROTOCOL_VERSION).into(), @@ -635,7 +641,7 @@ pub(crate) mod do_register { cmd_aux: packet_flags::cmd::aux::do_register::STAGE0, algorithm, security_level: 0, - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(proposed_cid), @@ -663,6 +669,7 @@ pub(crate) mod do_register { timestamp: i64, transfer: >::BobToAliceWireTransfer, proposed_cid: u64, + ticket: Ticket, ) -> BytesMut { let header = HdpHeader { protocol_version: (*crate::constants::PROTOCOL_VERSION).into(), @@ -670,7 +677,7 @@ pub(crate) mod do_register { cmd_aux: packet_flags::cmd::aux::do_register::STAGE1, algorithm, security_level: 0, - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(proposed_cid), @@ -701,6 +708,7 @@ pub(crate) mod do_register { timestamp: i64, credentials: &ProposedCredentials, security_level: SecurityLevel, + ticket: Ticket, ) -> BytesMut { let header = HdpHeader { protocol_version: (*crate::constants::PROTOCOL_VERSION).into(), @@ -708,7 +716,7 @@ pub(crate) mod do_register { cmd_aux: packet_flags::cmd::aux::do_register::STAGE2, algorithm, security_level: security_level.value(), - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(ratchet.get_cid()), @@ -739,6 +747,7 @@ pub(crate) mod do_register { timestamp: i64, success_message: T, security_level: SecurityLevel, + ticket: Ticket, ) -> BytesMut { let success_message = success_message.as_ref(); let success_message_len = success_message.len(); @@ -748,7 +757,7 @@ pub(crate) mod do_register { cmd_aux: packet_flags::cmd::aux::do_register::SUCCESS, algorithm, security_level: security_level.value(), - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(ratchet.get_cid()), @@ -774,6 +783,7 @@ pub(crate) mod do_register { timestamp: i64, error_message: T, proposed_cid: u64, + ticket: Ticket, ) -> BytesMut { let error_message = error_message.as_ref(); @@ -783,7 +793,7 @@ pub(crate) mod do_register { cmd_aux: packet_flags::cmd::aux::do_register::FAILURE, algorithm, security_level: 0, - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(proposed_cid), @@ -956,8 +966,10 @@ pub(crate) mod pre_connect { use citadel_wire::hypernode_type::NodeType; use crate::constants::HDP_HEADER_BYTE_LEN; + use crate::prelude::Ticket; use crate::proto::packet::packet_flags::payload_identifiers; use crate::proto::packet::{packet_flags, HdpHeader}; + use crate::proto::packet_crafter::peer_cmd::C2S_IDENTITY_CID; use citadel_crypt::endpoint_crypto_container::EndpointRatchetConstructor; use citadel_crypt::ratchets::Ratchet; use citadel_types::crypto::SecurityLevel; @@ -994,6 +1006,7 @@ pub(crate) mod pre_connect { session_security_settings: SessionSecuritySettings, peer_only_connect_protocol: ConnectProtocol, connect_mode: ConnectMode, + ticket: Ticket, ) -> BytesMut { let header = HdpHeader { protocol_version: (*crate::constants::PROTOCOL_VERSION).into(), @@ -1001,7 +1014,7 @@ pub(crate) mod pre_connect { cmd_aux: packet_flags::cmd::aux::do_preconnect::SYN, algorithm: 0, security_level: security_level.value(), - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(static_aux_hr.get_cid()), @@ -1045,6 +1058,7 @@ pub(crate) mod pre_connect { nat_type: NatType, timestamp: i64, security_level: SecurityLevel, + ticket: Ticket, ) -> BytesMut { let header = HdpHeader { protocol_version: (*crate::constants::PROTOCOL_VERSION).into(), @@ -1052,7 +1066,7 @@ pub(crate) mod pre_connect { cmd_aux: packet_flags::cmd::aux::do_preconnect::SYN_ACK, algorithm: 0, security_level: security_level.value(), - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(static_aux_hr.get_cid()), @@ -1087,6 +1101,7 @@ pub(crate) mod pre_connect { timestamp: i64, node_type: NodeType, security_level: SecurityLevel, + ticket: Ticket, ) -> BytesMut { let header = HdpHeader { protocol_version: (*crate::constants::PROTOCOL_VERSION).into(), @@ -1094,7 +1109,7 @@ pub(crate) mod pre_connect { cmd_aux: packet_flags::cmd::aux::do_preconnect::STAGE0, algorithm: 0, security_level: security_level.value(), - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(ratchet.get_cid()), @@ -1124,6 +1139,7 @@ pub(crate) mod pre_connect { tcp_only: bool, timestamp: i64, security_level: SecurityLevel, + ticket: Ticket, ) -> BytesMut { let cmd_aux = if success { packet_flags::cmd::aux::do_preconnect::SUCCESS @@ -1143,7 +1159,7 @@ pub(crate) mod pre_connect { cmd_aux, algorithm, security_level: security_level.value(), - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(ratchet.get_cid()), @@ -1165,6 +1181,7 @@ pub(crate) mod pre_connect { ratchet: &R, timestamp: i64, security_level: SecurityLevel, + ticket: Ticket, ) -> BytesMut { let header = HdpHeader { protocol_version: (*crate::constants::PROTOCOL_VERSION).into(), @@ -1172,7 +1189,7 @@ pub(crate) mod pre_connect { cmd_aux: packet_flags::cmd::aux::do_preconnect::BEGIN_CONNECT, algorithm: 0, security_level: security_level.value(), - context_info: U128::new(0), + context_info: U128::new(ticket.0), group: U64::new(0), wave_id: U32::new(0), session_cid: U64::new(ratchet.get_cid()), @@ -1202,7 +1219,7 @@ pub(crate) mod pre_connect { session_cid: prev_header.session_cid, entropy_bank_version: prev_header.entropy_bank_version, timestamp: prev_header.timestamp, - target_cid: U64::new(0), + target_cid: U64::new(C2S_IDENTITY_CID), }; let fail_reason = fail_reason.as_ref(); diff --git a/citadel_proto/src/proto/packet_processor/connect_packet.rs b/citadel_proto/src/proto/packet_processor/connect_packet.rs index a65bb4a62..813a4f85c 100644 --- a/citadel_proto/src/proto/packet_processor/connect_packet.rs +++ b/citadel_proto/src/proto/packet_processor/connect_packet.rs @@ -34,6 +34,7 @@ use super::includes::*; use crate::error::NetworkError; +use crate::prelude::Ticket; use crate::proto::node_result::{ConnectFail, ConnectSuccess, MailboxDelivery}; use crate::proto::packet_crafter::peer_cmd::C2S_IDENTITY_CID; use crate::proto::packet_processor::primary_group_packet::get_orientation_safe_ratchet; @@ -90,7 +91,7 @@ pub async fn process_connect( ); let header = header.clone(); let security_level = header.security_level.into(); - + let ticket = Ticket::from(header.context_info.get()); let time_tracker = session.time_tracker; let task = async move { @@ -116,6 +117,11 @@ pub async fn process_connect( let is_personal = !session.is_server; let kernel_ticket = session.kernel_ticket.get(); + if kernel_ticket != ticket { + const REASON: &str = "Ticket mismatch"; + return Ok(PrimaryProcessorResult::EndSession(REASON)); + } + state_container.connect_state.last_stage = packet_flags::cmd::aux::do_connect::SUCCESS; state_container.connect_state.fail_time = None; @@ -177,6 +183,7 @@ pub async fn process_connect( success_time, security_level, session.account_manager.get_backend_type(), + kernel_ticket, ); session.session_cid.set(Some(cid)); @@ -184,7 +191,7 @@ pub async fn process_connect( let cxn_type = VirtualConnectionType::LocalGroupServer { session_cid: cid }; - let channel_signal = NodeResult::ConnectSuccess(ConnectSuccess { + let channel_signal = NodeResult::ConnectSuccess(Box::new(ConnectSuccess { ticket: kernel_ticket, session_cid: cid, remote_addr: addr, @@ -195,7 +202,7 @@ pub async fn process_connect( channel, udp_rx_opt: udp_channel_rx, session_security_settings, - }); + })); // safe unwrap. Store the signal inner_mut_state!(session.state_container) .get_endpoint_container_mut(C2S_IDENTITY_CID) @@ -210,7 +217,6 @@ pub async fn process_connect( log::error!(target: "citadel", "Error validating stage2 packet. Reason: {err}"); let fail_time = time_tracker.get_global_time_ns(); - //session.state = SessionState::NeedsConnect; let packet = packet_crafter::do_connect::craft_final_status_packet( &ratchet, false, @@ -221,6 +227,7 @@ pub async fn process_connect( fail_time, security_level, session.account_manager.get_backend_type(), + ticket, ); return Ok(PrimaryProcessorResult::ReplyToSender(packet)); } @@ -346,10 +353,11 @@ pub async fn process_connect( &ratchet, timestamp, security_level, + ticket, ); session.send_to_primary_stream(None, success_ack)?; - session.send_to_kernel(NodeResult::ConnectSuccess(ConnectSuccess { + session.send_to_kernel(NodeResult::ConnectSuccess(Box::new(ConnectSuccess { ticket: kernel_ticket, session_cid: cid, remote_addr: addr, @@ -360,7 +368,7 @@ pub async fn process_connect( channel, udp_rx_opt: udp_channel_rx, session_security_settings, - }))?; + })))?; //finally, if there are any mailbox items, send them to the kernel for processing if let Some(mailbox_delivery) = payload.mailbox { session.send_to_kernel(NodeResult::MailboxDelivery( diff --git a/citadel_proto/src/proto/packet_processor/mod.rs b/citadel_proto/src/proto/packet_processor/mod.rs index f3bf3f399..54eeca26f 100644 --- a/citadel_proto/src/proto/packet_processor/mod.rs +++ b/citadel_proto/src/proto/packet_processor/mod.rs @@ -132,6 +132,8 @@ pub enum PrimaryProcessorResult { EndSession(&'static str), /// A response packet should be sent back to the sender ReplyToSender(BytesMut), + /// A response packet should be sent back to the sender, terminating the session soon thereafter + EndSessionAndReplyToSender(BytesMut, &'static str), } impl std::fmt::Debug for PrimaryProcessorResult { @@ -150,6 +152,13 @@ impl std::fmt::Debug for PrimaryProcessorResult { packet.len() ) } + PrimaryProcessorResult::EndSessionAndReplyToSender(packet, reason) => { + write!( + f, + "PrimaryProcessorResult::EndSessionAndReplyToSender(len: {}, {reason})", + packet.len() + ) + } } } } diff --git a/citadel_proto/src/proto/packet_processor/peer/peer_cmd_packet.rs b/citadel_proto/src/proto/packet_processor/peer/peer_cmd_packet.rs index 6cb16bb98..069b0cd86 100644 --- a/citadel_proto/src/proto/packet_processor/peer/peer_cmd_packet.rs +++ b/citadel_proto/src/proto/packet_processor/peer/peer_cmd_packet.rs @@ -638,11 +638,11 @@ pub async fn process_peer_cmd( }; let channel_signal = - NodeResult::PeerChannelCreated(PeerChannelCreated { + NodeResult::PeerChannelCreated(Box::new(PeerChannelCreated { ticket: init_ticket, channel, udp_rx_opt, - }); + })); if needs_turn && !cfg!(feature = "localhost-testing") { log::warn!(target: "citadel", "This p2p connection requires TURN-like routing"); @@ -793,11 +793,11 @@ pub async fn process_peer_cmd( }; let channel_signal = - NodeResult::PeerChannelCreated(PeerChannelCreated { + NodeResult::PeerChannelCreated(Box::new(PeerChannelCreated { ticket: init_ticket, channel, udp_rx_opt, - }); + })); if needs_turn && !cfg!(feature = "localhost-testing") { log::warn!(target: "citadel", "This p2p connection requires TURN-like routing"); diff --git a/citadel_proto/src/proto/packet_processor/preconnect_packet.rs b/citadel_proto/src/proto/packet_processor/preconnect_packet.rs index 2553975cf..7cac28059 100644 --- a/citadel_proto/src/proto/packet_processor/preconnect_packet.rs +++ b/citadel_proto/src/proto/packet_processor/preconnect_packet.rs @@ -45,6 +45,7 @@ use crate::proto::state_container::{StateContainerInner, VirtualTargetType}; use citadel_types::proto::UdpMode; use super::includes::*; +use crate::prelude::Ticket; use crate::proto::node_result::ConnectFail; use crate::proto::packet_processor::primary_group_packet::get_orientation_safe_ratchet; use crate::proto::state_subcontainers::preconnect_state_container::UdpChannelSender; @@ -77,8 +78,9 @@ pub async fn process_preconnect( let task = async move { let session = &session; let (header_main, payload) = return_if_none!(packet.parse(), "Unable to parse packet"); - let header = header_main; + let header = header_main.clone(); let security_level = header.security_level.into(); + let ticket = Ticket::from(header.context_info.get()); match header.cmd_aux { packet_flags::cmd::aux::do_preconnect::SYN => { @@ -94,18 +96,10 @@ pub async fn process_preconnect( .session_manager .session_active(header.session_cid.get()); let account_manager = session.account_manager.clone(); - let header_if_err_occurs = header.clone(); - - let error = |err: NetworkError| { - let packet = packet_crafter::pre_connect::craft_halt( - &header_if_err_occurs, - err.into_string(), - ); - Ok(PrimaryProcessorResult::ReplyToSender(packet)) - }; if session_already_active { - return error(NetworkError::InvalidRequest("Session Already Connected")); + const REASON: &str = "Session Already Connected"; + return send_error_and_end_session(&header, None, REASON); } if let Some(cnac) = account_manager @@ -132,6 +126,7 @@ pub async fn process_preconnect( new_ratchet, )) => { session.adjacent_nat_type.set_once(Some(nat_type)); + session.kernel_ticket.set(ticket); state_container.pre_connect_state.generated_ratchet = Some(new_ratchet); // since the SYN's been validated, the CNACs toolset has been updated let new_session_sec_lvl = transfer.security_level(); @@ -154,6 +149,7 @@ pub async fn process_preconnect( session.local_nat_type.clone(), timestamp, security_level, + ticket, ); state_container.udp_mode = udp_mode; @@ -168,15 +164,16 @@ pub async fn process_preconnect( } Err(err) => { - log::error!(target: "citadel", "Invalid SYN packet received: {:?}", &err); - error(err) + const REASON: &str = "Invalid SYN Packet received. Bad PSK or keys?"; + send_error_and_end_session(&header, Some(err), REASON) } } } else { + const REASON: &str = "CID not registered to this node"; let bad_cid = header.session_cid.get(); - let error = format!("CID {bad_cid} is not registered to this node"); - let packet = packet_crafter::pre_connect::craft_halt(&header, error); - Ok(PrimaryProcessorResult::ReplyToSender(packet)) + let error_message = format!("CID {bad_cid} is not registered to this node"); + let error = NetworkError::Generic(error_message); + send_error_and_end_session(&header, Some(error), REASON) } } @@ -221,6 +218,7 @@ pub async fn process_preconnect( timestamp, local_node_type, security_level, + ticket, ); state_container.pre_connect_state.last_stage = packet_flags::cmd::aux::do_preconnect::SUCCESS; @@ -244,6 +242,7 @@ pub async fn process_preconnect( security_level, session_cid, &mut state_container, + ticket, ); } @@ -253,6 +252,7 @@ pub async fn process_preconnect( timestamp, local_node_type, security_level, + ticket, ); let to_primary_stream = return_if_none!( session.to_primary_stream.clone(), @@ -269,8 +269,9 @@ pub async fn process_preconnect( ); (stream, new_ratchet) } else { - log::error!(target: "citadel", "Invalid SYN_ACK"); - return Ok(PrimaryProcessorResult::Void); + const REASON: &str = + "Invalid SYN_ACK Packet received. Bad PSK or keys?"; + return send_error_and_end_session(&header, None, REASON); } } else { log::error!(target: "citadel", "Expected stage SYN_ACK, but local state was not valid"); @@ -302,6 +303,7 @@ pub async fn process_preconnect( security_level, session_cid, &mut inner_mut_state!(session.state_container), + ticket, ) } @@ -314,6 +316,7 @@ pub async fn process_preconnect( security_level, session_cid, &mut inner_mut_state!(session.state_container), + ticket, ) } } @@ -352,6 +355,7 @@ pub async fn process_preconnect( &ratchet, timestamp, security_level, + ticket, ); return Ok(PrimaryProcessorResult::ReplyToSender(packet)); } // .. otherwise, continue logic below to punch a hole through the firewall @@ -371,8 +375,9 @@ pub async fn process_preconnect( ); (ratchet, stream) } else { - log::error!(target: "citadel", "Unable to validate stage 0 packet"); - return Ok(PrimaryProcessorResult::Void); + const REASON: &str = + "Invalid STAGE0 Packet received. Bad PSK, keys, or serialization?"; + return send_error_and_end_session(&header, None, REASON); } } else { log::error!(target: "citadel", "Packet state 0, last stage not 0. Dropping"); @@ -448,6 +453,7 @@ pub async fn process_preconnect( &ratchet, timestamp, security_level, + ticket, ); return Ok(PrimaryProcessorResult::ReplyToSender(begin_connect)); } @@ -474,7 +480,6 @@ pub async fn process_preconnect( .ticket .unwrap_or_else(|| session.kernel_ticket.get()); drop(state_container); - //session.needs_close_message.set(false); session.send_to_kernel(NodeResult::ConnectFail(ConnectFail { ticket, cid_opt: Some(cnac.get_cid()), @@ -488,6 +493,7 @@ pub async fn process_preconnect( &ratchet, timestamp, security_level, + ticket, ); Ok(PrimaryProcessorResult::ReplyToSender(begin_connect)) } @@ -515,7 +521,7 @@ pub async fn process_preconnect( state_container.pre_connect_state.success = true; std::mem::drop(state_container); // now, begin stage 0 connect - begin_connect_process(session, &ratchet, security_level) + begin_connect_process(session, &ratchet, security_level, ticket) } else { log::error!(target: "citadel", "Unable to validate success_ack packet. Dropping"); Ok(PrimaryProcessorResult::Void) @@ -535,7 +541,6 @@ pub async fn process_preconnect( cid_opt: Some(header.session_cid.get()), error_message: message, }))?; - //session.needs_close_message.set(false); Ok(PrimaryProcessorResult::EndSession( "Preconnect signalled to halt", )) @@ -555,8 +560,9 @@ fn begin_connect_process( session: &CitadelSession, ratchet: &R, security_level: SecurityLevel, + ticket: Ticket, ) -> Result { - // at this point, the session keys have already been re-established. We just need to begin the login stage + // At this point, the session keys have already been re-established. We just need to begin the login stage let mut state_container = inner_mut_state!(session.state_container); let timestamp = session.time_tracker.get_global_time_ns(); let proposed_credentials = return_if_none!( @@ -570,6 +576,7 @@ fn begin_connect_process( timestamp, security_level, session.account_manager.get_backend_type(), + ticket, ); state_container.connect_state.last_stage = packet_flags::cmd::aux::do_connect::STAGE1; // we now store the pqc temporarily in the state container @@ -590,6 +597,7 @@ fn send_success_as_initiator( security_level: SecurityLevel, session_cid: u64, state_container: &mut StateContainerInner, + ticket: Ticket, ) -> Result { let _ = handle_success_as_receiver(udp_splittable, session, session_cid, state_container)?; @@ -599,6 +607,7 @@ fn send_success_as_initiator( false, session.time_tracker.get_global_time_ns(), security_level, + ticket, ); Ok(PrimaryProcessorResult::ReplyToSender(success_packet)) } @@ -708,6 +717,26 @@ fn get_quic_udp_interface(quic_conn: Connection, local_addr: SocketAddr) -> UdpS UdpSplittableTypes::Quic(QuicUdpSocketConnector::new(quic_conn, local_addr)) } +pub fn send_error_and_end_session( + header: &HdpHeader, + err: Option, + reason: &'static str, +) -> Result { + let err = if let Some(err) = err { + format!("{reason}: {err}") + } else { + reason.to_string() + }; + + log::error!(target: "citadel", "{err}"); + + let packet = packet_crafter::pre_connect::craft_halt(header, err); + + Ok(PrimaryProcessorResult::EndSessionAndReplyToSender( + packet, reason, + )) +} + #[cfg(test)] mod tests { use crate::constants::PROTOCOL_VERSION; diff --git a/citadel_proto/src/proto/packet_processor/register_packet.rs b/citadel_proto/src/proto/packet_processor/register_packet.rs index 66b64db10..f2d9e24e6 100644 --- a/citadel_proto/src/proto/packet_processor/register_packet.rs +++ b/citadel_proto/src/proto/packet_processor/register_packet.rs @@ -30,6 +30,7 @@ use super::includes::*; use crate::error::NetworkError; +use crate::prelude::Ticket; use crate::proto::node_result::{RegisterFailure, RegisterOkay}; use citadel_crypt::endpoint_crypto_container::{ AssociatedCryptoParams, AssociatedSecurityLevel, EndpointRatchetConstructor, PeerSessionCrypto, @@ -70,6 +71,7 @@ pub async fn process_register( as Ref<&[u8], HdpHeader>; debug_assert_eq!(packet_flags::cmd::primary::DO_REGISTER, header.cmd_primary); let security_level = header.security_level.into(); + let ticket = Ticket::from(header.context_info.get()); match header.cmd_aux { packet_flags::cmd::aux::do_register::STAGE0 => { @@ -83,20 +85,31 @@ pub async fn process_register( let algorithm = header.algorithm; match validation::do_register::validate_stage0::(&payload) { - Some((transfer, passwordless)) => { + Some((transfer, transient_mode_requested)) => { // Now, create a stage 1 packet let timestamp = session.time_tracker.get_global_time_ns(); - state_container.register_state.passwordless = Some(passwordless); + state_container.register_state.transient_mode = + Some(transient_mode_requested); - if passwordless + if transient_mode_requested && !session .account_manager .get_misc_settings() - .allow_passwordless + .allow_transient_connections { - // passwordless is not allowed on this node - let err = packet_crafter::do_register::craft_failure(algorithm, timestamp, "Passwordless connections are not enabled on the target node", header.session_cid.get()); - return Ok(PrimaryProcessorResult::ReplyToSender(err)); + // Transient connections are not allowed on this node + const REASON: &str = + "Transient connections are not allowed on this node"; + let err = packet_crafter::do_register::craft_failure( + algorithm, + timestamp, + REASON, + header.session_cid.get(), + ticket, + ); + return Ok(PrimaryProcessorResult::EndSessionAndReplyToSender( + err, REASON, + )); } std::mem::drop(state_container); @@ -126,6 +139,7 @@ pub async fn process_register( timestamp, transfer, header.session_cid.get(), + ticket, ); let mut state_container = @@ -203,6 +217,7 @@ pub async fn process_register( timestamp, proposed_credentials, security_level, + ticket, ); //let mut state_container = inner_mut!(session.state_container); @@ -274,6 +289,7 @@ pub async fn process_register( timestamp, success_message, security_level, + ticket, ); // Do not shutdown the session here. It is up to the client to decide // how to shutdown, or continue (in the case of passwordless mode), the session @@ -282,12 +298,14 @@ pub async fn process_register( Err(err) => { let err = err.into_string(); - log::error!(target: "citadel", "Server unsuccessfully created a CNAC during the DO_REGISTER process. Reason: {}", &err); + const REASON: &str = "Server unable to create client account during the DO_REGISTER process"; + log::error!(target: "citadel", "{REASON}: {err}"); let packet = packet_crafter::do_register::craft_failure( algorithm, timestamp, err, header.session_cid.get(), + ticket, ); session.session_manager.clear_provisional_session( @@ -342,7 +360,7 @@ pub async fn process_register( ); let passwordless = return_if_none!( - state_container.register_state.passwordless, + state_container.register_state.transient_mode, "Passwordless unset (reg)" ); diff --git a/citadel_proto/src/proto/peer/p2p_conn_handler.rs b/citadel_proto/src/proto/peer/p2p_conn_handler.rs index d3203eb44..1a5a3ab1e 100644 --- a/citadel_proto/src/proto/peer/p2p_conn_handler.rs +++ b/citadel_proto/src/proto/peer/p2p_conn_handler.rs @@ -434,5 +434,5 @@ pub(crate) async fn attempt_simultaneous_hole_punch( pub(crate) fn generic_error>>( err: E, ) -> std::io::Error { - std::io::Error::new(std::io::ErrorKind::Other, err) + std::io::Error::other(err) } diff --git a/citadel_proto/src/proto/peer/peer_layer.rs b/citadel_proto/src/proto/peer/peer_layer.rs index ea6f64e2c..965efe251 100644 --- a/citadel_proto/src/proto/peer/peer_layer.rs +++ b/citadel_proto/src/proto/peer/peer_layer.rs @@ -32,7 +32,6 @@ This module implements the core peer-to-peer networking layer for the Citadel Pr use crate::error::NetworkError; use crate::macros::SyncContextRequirements; -use crate::prelude::PreSharedKey; use crate::proto::packet_processor::peer::group_broadcast::GroupBroadcast; use crate::proto::peer::message_group::{MessageGroup, MessageGroupPeer}; use crate::proto::peer::peer_crypt::KeyExchangeProcess; @@ -42,6 +41,7 @@ use citadel_crypt::ratchets::Ratchet; use citadel_io::tokio::time::error::Error; use citadel_io::tokio::time::Duration; use citadel_io::tokio_util::time::{delay_queue, delay_queue::DelayQueue}; +use citadel_types::crypto::PreSharedKey; use citadel_types::prelude::PeerInfo; use citadel_types::proto::{ GroupType, MessageGroupKey, MessageGroupOptions, SessionSecuritySettings, UdpMode, diff --git a/citadel_proto/src/proto/session.rs b/citadel_proto/src/proto/session.rs index fdfd837d1..7b26cbe58 100644 --- a/citadel_proto/src/proto/session.rs +++ b/citadel_proto/src/proto/session.rs @@ -57,7 +57,7 @@ use crate::constants::{ LOGIN_EXPIRATION_TIME, REKEY_UPDATE_FREQUENCY_STANDARD, }; use crate::error::NetworkError; -use crate::prelude::{GroupBroadcast, PeerEvent, PeerResponse, PreSharedKey}; +use crate::prelude::{GroupBroadcast, PeerEvent, PeerResponse}; use crate::proto::endpoint_crypto_accessor::EndpointCryptoAccessor; //use futures_codec::Framed; use crate::proto::misc; @@ -105,7 +105,7 @@ use citadel_crypt::messaging::MessengerLayerOrderedMessage; use citadel_crypt::prelude::{ConstructorOpts, FixedSizedSource}; use citadel_crypt::ratchets::ratchet_manager::RatchetMessage; use citadel_crypt::scramble::streaming_crypt_scrambler::{scramble_encrypt_source, ObjectSource}; -use citadel_types::crypto::{HeaderObfuscatorSettings, SecBuffer, SecurityLevel}; +use citadel_types::crypto::{HeaderObfuscatorSettings, PreSharedKey, SecBuffer, SecurityLevel}; use citadel_types::proto::ConnectMode; use citadel_types::proto::SessionSecuritySettings; use citadel_types::proto::TransferType; @@ -592,9 +592,9 @@ impl CitadelSession { match state { SessionState::NeedsRegister => { log::trace!(target: "citadel", "Beginning registration subroutine"); - let session_ref = session; - let mut state_container = inner_mut_state!(session_ref.state_container); + let mut state_container = inner_mut_state!(session.state_container); let session_security_settings = state_container.session_security_settings.unwrap(); + let ticket = session.kernel_ticket.get(); let proposed_username = state_container .connect_state .proposed_credentials @@ -606,7 +606,7 @@ impl CitadelSession { let proposed_cid = persistence_handler.get_cid_by_username(proposed_username); let passwordless = state_container .register_state - .passwordless + .transient_mode .ok_or(NetworkError::InternalError("Passwordless state not loaded"))?; // we supply 0,0 for cid and new entropy_bank vers by default, even though it will be reset by bob let alice_constructor = @@ -631,14 +631,14 @@ impl CitadelSession { "Unable to construct AliceToBob transfer", ))?; - let stage0_register_packet = - crate::proto::packet_crafter::do_register::craft_stage0::( - session_security_settings.crypto_params.into(), - timestamp, - transfer, - passwordless, - proposed_cid, - ); + let stage0_register_packet = packet_crafter::do_register::craft_stage0::( + session_security_settings.crypto_params.into(), + timestamp, + transfer, + passwordless, + proposed_cid, + ticket, + ); to_outbound .unbounded_send(stage0_register_packet) .map_err(|_| NetworkError::InternalError("Writer stream corrupted"))?; @@ -676,17 +676,17 @@ impl CitadelSession { cnac: &ClientNetworkAccount, ) -> Result<(), NetworkError> { log::trace!(target: "citadel", "Beginning pre-connect subroutine"); - let session_ref = session; session.state.set(SessionState::NeedsConnect); let connect_mode = (*inner!(session.connect_mode)) .ok_or(NetworkError::InternalError("Connect mode not loaded"))?; - let mut state_container = inner_mut_state!(session_ref.state_container); + let mut state_container = inner_mut_state!(session.state_container); state_container.store_session_password(C2S_IDENTITY_CID, session.session_password.clone()); let udp_mode = state_container.udp_mode; - let timestamp = session_ref.time_tracker.get_global_time_ns(); + let timestamp = session.time_tracker.get_global_time_ns(); let session_security_settings = state_container.session_security_settings.unwrap(); - let peer_only_connect_mode = session_ref.peer_only_connect_protocol.get().unwrap(); + let peer_only_connect_mode = session.peer_only_connect_protocol.get().unwrap(); + let ticket = session.kernel_ticket.get(); // reset the toolset's ARA let static_aux_hr = &cnac.refresh_static_ratchet(); // security level inside static hr may not be what the declared session security level for this session is. Session security level can be no higher than the initial static HR level, since the chain requires recursion from the initial value @@ -708,7 +708,7 @@ impl CitadelSession { ))?; // encrypts the entire connect process with the highest possible security level let max_usable_level = static_aux_hr.get_default_security_level(); - let nat_type = session_ref.local_nat_type.clone(); + let nat_type = session.local_nat_type.clone(); if udp_mode == UdpMode::Enabled { state_container.pre_connect_state.udp_channel_oneshot_tx = UdpChannelSender::default(); @@ -728,6 +728,7 @@ impl CitadelSession { session_security_settings, peer_only_connect_mode, connect_mode, + ticket, ); state_container.pre_connect_state.last_stage = @@ -867,7 +868,7 @@ impl CitadelSession { } }; - // unlike TCP, we will not use [LengthDelimitedCodec] because there is no guarantee that packets + // Unlike TCP, we will not use [LengthDelimitedCodec] because there is no guarantee that packets // will arrive in order let (writer, reader) = udp_conn.split(); @@ -890,19 +891,25 @@ impl CitadelSession { log::trace!(target: "citadel", "[Q-UDP] Initiated UDP subsystem..."); let stopper = async move { - stopper_rx - .await - .map_err(|err| NetworkError::Generic(err.to_string())) + let _ = stopper_rx.await; }; citadel_io::tokio::select! { res0 = listener => res0, res1 = udp_sender_future => res1, - res2 = stopper => res2 + _ = stopper => Ok(()) } }; - spawn!(task); + let wrapped_task = async move { + if let Err(err) = task.await { + log::error!(target: "citadel", "UDP task failed: {err:?}"); + } else { + log::trace!(target: "citadel", "UDP ended without error"); + } + }; + + spawn!(wrapped_task); } #[cfg_attr( @@ -996,8 +1003,21 @@ impl CitadelSession { session: &CitadelSession, cid_opt: Option, ) -> std::io::Result<()> { - match result { - Ok(PrimaryProcessorResult::ReplyToSender(return_packet)) => { + let mut session_closing_error: Option = None; + match &result { + Ok( + PrimaryProcessorResult::ReplyToSender { .. } + | PrimaryProcessorResult::EndSessionAndReplyToSender { .. }, + ) => { + let return_packet = match result { + Ok(PrimaryProcessorResult::ReplyToSender(packet)) => packet, + Ok(PrimaryProcessorResult::EndSessionAndReplyToSender(packet, err)) => { + session_closing_error = Some(err.to_string()); + packet + } + _ => unreachable!(), + }; + CitadelSession::::send_to_primary_stream_closure( primary_stream, kernel_tx, @@ -1005,32 +1025,28 @@ impl CitadelSession { None, cid_opt, ) - .map_err(|err| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Unable to ReplyToSender: {:?}", err.into_string()), - ) - }) + .map_err(|err| std::io::Error::other(format!("Unable to ReplyToSender: {:?}", err)))?; } Err(reason) => { - log::error!(target: "citadel", "[PrimaryProcessor] session ending: {:?} | Session end state: {:?}", reason, session.state.get()); - Err(std::io::Error::new( - std::io::ErrorKind::Other, - reason.into_string(), - )) + session_closing_error = Some(reason.to_string()); } Ok(PrimaryProcessorResult::EndSession(reason)) => { - log::warn!(target: "citadel", "[PrimaryProcessor] session ending: {}", reason); - Err(std::io::Error::new(std::io::ErrorKind::Other, reason)) + session_closing_error = Some(reason.to_string()); } Ok(PrimaryProcessorResult::Void) => { // this implies that the packet processor found no reason to return a message - Ok(()) } } + + if let Some(err) = session_closing_error { + log::error!(target: "citadel", "[PrimaryProcessor] session ending: {err:?} | Session end state: {:?}", session.state.get()); + Err(std::io::Error::other(err)) + } else { + Ok(()) + } } fn handle_session_terminating_error( @@ -1103,7 +1119,7 @@ impl CitadelSession { if !header_obfuscator .on_packet_received(&mut packet) .map_err(|err| { - std::io::Error::new(std::io::ErrorKind::Other, err.into_string()) + std::io::Error::other(err.into_string()) })? { return Ok(()); @@ -1118,7 +1134,15 @@ impl CitadelSession { packet, ) .await; - evaluate_result(result, primary_stream, kernel_tx, this_main, session_cid) + + let res = + evaluate_result(result, primary_stream, kernel_tx, this_main, session_cid); + if res.is_err() { + // TODO: remove this waiting logic for better code. Wait for any outgoing packets to get flushed + citadel_io::tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + + res }) .map_err(|err| handle_session_terminating_error(this_main, err, is_server, peer_cid)) .await; @@ -2322,7 +2346,8 @@ impl CitadelSessionInner { /// Stores the proposed credentials into the register state container pub(crate) fn store_proposed_credentials(&mut self, proposed_credentials: ProposedCredentials) { let mut state_container = inner_mut_state!(self.state_container); - state_container.register_state.passwordless = Some(proposed_credentials.is_passwordless()); + state_container.register_state.transient_mode = + Some(proposed_credentials.is_passwordless()); state_container.connect_state.proposed_credentials = Some(proposed_credentials); } @@ -2353,8 +2378,8 @@ impl CitadelSessionInner { /// This will panic if cannot be sent #[inline] - pub fn send_to_kernel(&self, msg: NodeResult) -> Result<(), SendError>> { - self.kernel_tx.unbounded_send(msg) + pub fn send_to_kernel(&self, msg: NodeResult) -> Result<(), Box>>> { + self.kernel_tx.unbounded_send(msg).map_err(Box::new) } /// Will send the message to the primary stream, and will alert the kernel if the stream's connector is full diff --git a/citadel_proto/src/proto/session_manager.rs b/citadel_proto/src/proto/session_manager.rs index 352954892..5f5ceb527 100644 --- a/citadel_proto/src/proto/session_manager.rs +++ b/citadel_proto/src/proto/session_manager.rs @@ -53,7 +53,7 @@ use crate::auth::AuthenticationRequest; use crate::constants::{DO_CONNECT_EXPIRE_TIME_MS, KEEP_ALIVE_TIMEOUT_NS}; use crate::error::NetworkError; use crate::macros::{FutureRequirements, SyncContextRequirements}; -use crate::prelude::{Disconnect, PreSharedKey}; +use crate::prelude::Disconnect; use crate::proto::endpoint_crypto_accessor::EndpointCryptoAccessor; use crate::proto::misc::net::GenericNetworkStream; use crate::proto::misc::underlying_proto::ServerUnderlyingProtocol; @@ -76,7 +76,7 @@ use crate::proto::session::{ use crate::proto::state_container::{VirtualConnectionType, VirtualTargetType}; use citadel_crypt::scramble::streaming_crypt_scrambler::ObjectSource; use citadel_io::tokio::sync::broadcast::Sender; -use citadel_types::crypto::SecurityLevel; +use citadel_types::crypto::{PreSharedKey, SecurityLevel}; use citadel_types::proto::ConnectMode; use citadel_types::proto::SessionSecuritySettings; use citadel_types::proto::TransferType; diff --git a/citadel_proto/src/proto/state_container.rs b/citadel_proto/src/proto/state_container.rs index a3c992e3b..15006aa9e 100644 --- a/citadel_proto/src/proto/state_container.rs +++ b/citadel_proto/src/proto/state_container.rs @@ -60,7 +60,7 @@ use crate::constants::{ }; use crate::error::NetworkError; use crate::functional::IfEqConditional; -use crate::prelude::{InternalServerError, PreSharedKey, ReKeyResult, ReKeyReturnType}; +use crate::prelude::{InternalServerError, ReKeyResult, ReKeyReturnType}; use crate::proto::misc::dual_cell::DualCell; use crate::proto::misc::dual_late_init::DualLateInit; use crate::proto::misc::dual_rwlock::DualRwLock; @@ -98,8 +98,8 @@ use citadel_crypt::ratchets::Ratchet; use citadel_io::tokio::sync::mpsc::unbounded_channel; use citadel_io::tokio_stream::wrappers::UnboundedReceiverStream; use citadel_io::{tokio, Mutex}; -use citadel_types::crypto::SecBuffer; use citadel_types::crypto::SecurityLevel; +use citadel_types::crypto::{PreSharedKey, SecBuffer}; use citadel_types::prelude::ObjectId; use citadel_types::proto::{ MessageGroupKey, ObjectTransferOrientation, ObjectTransferStatus, SessionSecuritySettings, @@ -1271,14 +1271,14 @@ impl StateContainerInner { let is_server = self.is_server; let task = async move { - log::info!(target: "citadel", "File transfer initiated, awaiting acceptance ... | revfs_pull: {is_revfs_pull}"); + log::debug!(target: "citadel", "File transfer initiated, awaiting acceptance ... | revfs_pull: {is_revfs_pull}"); let res = if let Some(start_rx) = start_recv_rx { start_rx.await } else { Ok(true) }; - log::info!(target: "citadel", "File transfer initiated! | revfs_pull: {is_revfs_pull}"); + log::debug!(target: "citadel", "File transfer initiated! | revfs_pull: {is_revfs_pull}"); let accepted = res.as_ref().map(|r| *r).unwrap_or(false); // first, send a rebound signal immediately to the sender diff --git a/citadel_proto/src/proto/state_subcontainers/peer_kem_state_container.rs b/citadel_proto/src/proto/state_subcontainers/peer_kem_state_container.rs index d7e2c43bb..a4f95c189 100644 --- a/citadel_proto/src/proto/state_subcontainers/peer_kem_state_container.rs +++ b/citadel_proto/src/proto/state_subcontainers/peer_kem_state_container.rs @@ -30,9 +30,9 @@ This module manages the state of peer-to-peer key exchange processes in the Cita */ -use crate::prelude::PreSharedKey; use crate::proto::state_subcontainers::preconnect_state_container::UdpChannelSender; use citadel_crypt::ratchets::Ratchet; +use citadel_types::crypto::PreSharedKey; use citadel_types::proto::SessionSecuritySettings; pub struct PeerKemStateContainer { diff --git a/citadel_proto/src/proto/state_subcontainers/register_state_container.rs b/citadel_proto/src/proto/state_subcontainers/register_state_container.rs index ea3605ba5..e2e8b3748 100644 --- a/citadel_proto/src/proto/state_subcontainers/register_state_container.rs +++ b/citadel_proto/src/proto/state_subcontainers/register_state_container.rs @@ -41,7 +41,7 @@ pub struct RegisterState { pub(crate) constructor: Option, pub(crate) created_ratchet: Option, pub(crate) last_packet_time: Option, - pub(crate) passwordless: Option, + pub(crate) transient_mode: Option, } impl Default for RegisterState { @@ -51,7 +51,7 @@ impl Default for RegisterState { constructor: None, created_ratchet: None, last_packet_time: None, - passwordless: None, + transient_mode: None, } } } diff --git a/citadel_proto/src/proto/validation.rs b/citadel_proto/src/proto/validation.rs index ed132fb23..06b3a2dca 100644 --- a/citadel_proto/src/proto/validation.rs +++ b/citadel_proto/src/proto/validation.rs @@ -214,12 +214,12 @@ pub(crate) mod pre_connect { use citadel_wire::hypernode_type::NodeType; use crate::error::NetworkError; - use crate::prelude::PreSharedKey; use crate::proto::packet::HdpPacket; use crate::proto::packet_crafter::pre_connect::{PreConnectStage0, SynPacket}; use crate::proto::packet_processor::includes::packet_crafter::pre_connect::SynAckPacket; use crate::proto::session_manager::CitadelSessionManager; use citadel_crypt::ratchets::Ratchet; + use citadel_types::crypto::PreSharedKey; use citadel_types::proto::ConnectMode; use citadel_types::proto::SessionSecuritySettings; use citadel_types::proto::UdpMode; diff --git a/citadel_sdk/src/builder/node_builder.rs b/citadel_sdk/src/builder/node_builder.rs index 5fa8cbb05..81e17761b 100644 --- a/citadel_sdk/src/builder/node_builder.rs +++ b/citadel_sdk/src/builder/node_builder.rs @@ -40,7 +40,7 @@ use citadel_proto::prelude::*; use citadel_proto::kernel::KernelExecutorArguments; use citadel_proto::macros::{ContextRequirements, LocalContextRequirements}; use citadel_proto::re_imports::RustlsClientConfig; -use citadel_types::crypto::HeaderObfuscatorSettings; +use citadel_types::crypto::{HeaderObfuscatorSettings, PreSharedKey}; use futures::Future; use std::fmt::{Debug, Formatter}; use std::marker::PhantomData; diff --git a/citadel_sdk/src/fs.rs b/citadel_sdk/src/fs.rs index 70b757b51..49eadad5d 100644 --- a/citadel_sdk/src/fs.rs +++ b/citadel_sdk/src/fs.rs @@ -157,7 +157,7 @@ mod tests { SigAlgorithm::None )] #[case( - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, KemAlgorithm::Kyber, SigAlgorithm::Falcon1024 )] @@ -504,7 +504,6 @@ mod tests { log::info!(target: "citadel", "***CLIENT B {cid} FILE TRANSFER SUCCESS***"); // Wait some time for the file to synchronize tokio::time::sleep(Duration::from_secs(1)).await; - tokio::time::sleep(Duration::from_secs(1)).await; wait_for_peers().await; // now, pull it let save_dir = crate::fs::read(&remote, virtual_path).await?; diff --git a/citadel_sdk/src/lib.rs b/citadel_sdk/src/lib.rs index 31f64da9b..dd23a7f45 100644 --- a/citadel_sdk/src/lib.rs +++ b/citadel_sdk/src/lib.rs @@ -38,10 +38,10 @@ //! - AES-256-GCM //! - Chacha20Poly-1305 //! - Ascon-80pq -//! - Kyber "scramcryption" (see below for explanation) +//! - Kyber Hybrid Encryption (see below for explanation) //! //! Whereas AES-GCM and ChaCha are only quantum resistant (as opposed to post-quantum), a novel method of encryption may be used that -//! combines the post-quantum asymmetric encryption algorithm Kyber coupled with AES. When Kyber "scramcryption" is used, several modifications to the protocol outlined in the whitepaper +//! combines the post-quantum asymmetric encryption algorithm Kyber coupled with AES. When Kyber Hybrid Encryption is used, several modifications to the protocol outlined in the whitepaper //! is applied. The first modification is the use of Falcon-1024 to sign each message to ensure non-repudiation. The second modification is more complex. Ciphertext is first encrypted by AES-GCM, then, randomly shifted using modular arithmetic //! in 32-byte blocks using a 32-byte long quasi one-time pad (OTP). The OTP is unique for each ciphertext, and, is appended at the end of the ciphertext in encrypted form (using Kyber1024 encryption). Even if the attacker uses Grover's algorithm to //! discover the AES key, the attacker would also have to break the lattice-based Kyber cryptography in order to properly order @@ -155,7 +155,7 @@ //! ## Remote Encrypted Virtual Filesystem (RE-VFS) //! The RE-VFS allows clients, servers, and peers to treat each other as remote endpoints for encrypted file storage. //! Since encrypting data locally using a symmetric key poses a vulnerability if the local node is compromised, The -//! Citadel Protocol solves this issue by using a local 1024-Kyber public key to encrypt the data (via Kyber scramcryption for +//! Citadel Protocol solves this issue by using a local 1024-Kyber public key to encrypt the data (via Kyber Hybrid Encryption for //! keeping the data size to a minimum), then, sending the contents to the adjacent endpoint. By doing this, the private decryption //! key and the contents are kept separate, forcing the hacker to compromise both endpoints. //! @@ -271,5 +271,8 @@ pub mod test_common; #[macro_use] pub(crate) mod macros; + +pub use citadel_logging as logging; /// Convenience for SDK users pub use citadel_proto::prelude::async_trait; +pub use citadel_types as types; diff --git a/citadel_sdk/src/prefabs/client/single_connection.rs b/citadel_sdk/src/prefabs/client/single_connection.rs index 0cfa5ecb9..449ea8835 100644 --- a/citadel_sdk/src/prefabs/client/single_connection.rs +++ b/citadel_sdk/src/prefabs/client/single_connection.rs @@ -61,6 +61,7 @@ use crate::remote_ext::CitadelClientServerConnection; use crate::remote_ext::ProtocolRemoteExt; use citadel_io::Mutex; use citadel_proto::prelude::*; +use citadel_types::crypto::PreSharedKey; use futures::Future; use std::marker::PhantomData; use std::net::SocketAddr; diff --git a/citadel_sdk/src/remote_ext.rs b/citadel_sdk/src/remote_ext.rs index 89656a0a2..da93744c0 100644 --- a/citadel_sdk/src/remote_ext.rs +++ b/citadel_sdk/src/remote_ext.rs @@ -1443,7 +1443,7 @@ mod tests { SigAlgorithm::None )] #[case( - EncryptionAlgorithm::Kyber, + EncryptionAlgorithm::KyberHybrid, KemAlgorithm::Kyber, SigAlgorithm::Falcon1024 )] diff --git a/citadel_sdk/tests/stress_tests.rs b/citadel_sdk/tests/stress_tests.rs index 23306b779..2772652c2 100644 --- a/citadel_sdk/tests/stress_tests.rs +++ b/citadel_sdk/tests/stress_tests.rs @@ -307,7 +307,7 @@ mod tests { #[case] secrecy_mode: SecrecyMode, #[case] server_password: Option<&'static str>, #[values(KemAlgorithm::Kyber)] kem: KemAlgorithm, - #[values(EncryptionAlgorithm::Kyber)] enx: EncryptionAlgorithm, + #[values(EncryptionAlgorithm::KyberHybrid)] enx: EncryptionAlgorithm, ) { citadel_logging::setup_log(); citadel_sdk::test_common::TestBarrier::setup(2); diff --git a/citadel_types/Cargo.toml b/citadel_types/Cargo.toml index e9adc75cd..42855ef52 100644 --- a/citadel_types/Cargo.toml +++ b/citadel_types/Cargo.toml @@ -14,11 +14,13 @@ license = "MIT OR Apache-2.0" [dependencies] serde = { workspace = true, features = ["derive"]} strum = { workspace = true, features = ["derive"] } +strum_macros = { workspace = true } bytes = { workspace = true, features = ["serde"] } twox-hash = { workspace = true } packed_struct = { workspace = true, features = ["serde"] } uuid = { workspace = true, features = ["v4"] } -bincode = { workspace = true} +bincode = { workspace = true } +sha3 = { workspace = true } [target.'cfg(target_family = "unix")'.dependencies] libc = { workspace = true } diff --git a/citadel_types/src/crypto/mod.rs b/citadel_types/src/crypto/mod.rs index 572aba0c7..ba6567413 100644 --- a/citadel_types/src/crypto/mod.rs +++ b/citadel_types/src/crypto/mod.rs @@ -46,6 +46,7 @@ //! - Encryption //! - ChaCha20-Poly1305 //! - AES-GCM +//! - Kyber Hybrid Encryption //! - Ascon //! - Signatures //! - Falcon @@ -63,9 +64,10 @@ use bytes::{Bytes, BytesMut}; use packed_struct::derive::{PackedStruct, PrimitiveEnum_u8}; use packed_struct::{PackedStruct, PrimitiveEnum}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use sha3::Digest; use std::fmt::{Debug, Formatter}; use std::ops::{Add, Deref, DerefMut}; -use strum::{EnumCount, ParseError}; +use strum::{EnumCount, IntoEnumIterator, ParseError, VariantNames}; use uuid::Uuid; pub const LARGEST_NONCE_LEN: usize = KYBER_NONCE_LENGTH_BYTES; @@ -119,30 +121,6 @@ pub fn add_inner(lhs: L, rhs: R) -> CryptoPa ret } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)] -pub enum SecrecyMode { - /// Slowest, but ensures each packet gets encrypted with a unique symmetrical key - Perfect, - /// Fastest. Meant for high-throughput environments. Each message will attempt to get re-keyed, but if not possible, will use the most recent symmetrical key - #[default] - BestEffort, -} - -impl TryFrom for SecrecyMode { - type Error = crate::errors::Error; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(Self::Perfect), - 1 => Ok(Self::BestEffort), - _ => Err(Self::Error::Other(format!( - "Cannot cast `{}` into SecrecyMode", - value - ))), - } - } -} - /// A memory-secure wrapper for shipping around Bytes pub struct SecBuffer { pub inner: BytesMut, @@ -326,36 +304,26 @@ impl<'de> Deserialize<'de> for SecBuffer { } } -impl SecurityLevel { - /// Returns byte representation of self - pub fn value(self) -> u8 { - match self { - SecurityLevel::Standard => 0, - SecurityLevel::Reinforced => 1, - SecurityLevel::High => 2, - SecurityLevel::Ultra => 3, - SecurityLevel::Extreme => 4, - SecurityLevel::Custom(val) => val, - } - } - - /// Possibly returns the security_level given an input value - pub fn for_value(val: usize) -> Option { - Some(SecurityLevel::from(u8::try_from(val).ok()?)) - } -} - #[derive(PackedStruct, Default, Serialize, Deserialize, Copy, Clone, Debug)] #[packed_struct(bit_numbering = "msb0")] pub struct CryptoParameters { - #[packed_field(bits = "0..=2", ty = "enum")] + #[packed_field(bits = "0..=3", ty = "enum")] pub encryption_algorithm: EncryptionAlgorithm, - #[packed_field(bits = "3..=5", ty = "enum")] + #[packed_field(bits = "4..=5", ty = "enum")] pub kem_algorithm: KemAlgorithm, #[packed_field(bits = "6..=7", ty = "enum")] pub sig_algorithm: SigAlgorithm, } +const _: () = _compile_time_check(); +#[allow(clippy::assertions_on_constants)] +const fn _compile_time_check() { + assert!( + EncryptionAlgorithm::COUNT + KemAlgorithm::COUNT + SigAlgorithm::COUNT <= 8, + "Too many algorithms to fit inside 8 bits" + ); +} + impl TryFrom for CryptoParameters { type Error = crate::errors::Error; @@ -369,7 +337,6 @@ impl TryFrom for CryptoParameters { } #[derive( - PrimitiveEnum_u8, Default, Copy, Clone, @@ -378,19 +345,71 @@ impl TryFrom for CryptoParameters { PartialEq, Serialize, Deserialize, + PrimitiveEnum_u8, strum::EnumString, strum::EnumIter, + strum::EnumCount, + strum_macros::VariantNames, )] pub enum EncryptionAlgorithm { #[default] AES_GCM_256 = 0, ChaCha20Poly_1305 = 1, - Kyber = 2, + KyberHybrid = 2, Ascon80pq = 3, } +impl EncryptionAlgorithm { + pub fn variants() -> Vec { + Self::VARIANTS.iter().map(|s| s.to_string()).collect() + } +} + #[derive( + Copy, + Clone, + Debug, + Serialize, + Deserialize, + Eq, + PartialEq, + Default, PrimitiveEnum_u8, + strum::EnumString, + strum::EnumIter, + strum::EnumCount, + strum_macros::VariantNames, +)] +pub enum SecrecyMode { + /// Fastest. Meant for high-throughput environments. Each message will attempt to get re-keyed, but if not possible, will use the most recent symmetrical key + #[default] + BestEffort, + /// Slowest, but ensures each packet gets encrypted with a unique symmetrical key + Perfect, +} + +impl SecrecyMode { + pub fn variants() -> Vec { + Self::VARIANTS.iter().map(|s| s.to_string()).collect() + } +} + +impl TryFrom for SecrecyMode { + type Error = crate::errors::Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Perfect), + 1 => Ok(Self::BestEffort), + _ => Err(Self::Error::Other(format!( + "Cannot cast `{}` into SecrecyMode", + value + ))), + } + } +} + +#[derive( Default, Copy, Clone, @@ -399,9 +418,11 @@ pub enum EncryptionAlgorithm { PartialEq, Serialize, Deserialize, + PrimitiveEnum_u8, strum::EnumString, strum::EnumIter, strum::EnumCount, + strum_macros::VariantNames, )] pub enum KemAlgorithm { #[strum(ascii_case_insensitive)] @@ -409,10 +430,13 @@ pub enum KemAlgorithm { Kyber = 0, } +impl KemAlgorithm { + pub fn variants() -> Vec { + Self::VARIANTS.iter().map(|s| s.to_string()).collect() + } +} + #[derive( - PrimitiveEnum_u8, - strum::EnumString, - strum::EnumIter, Default, Serialize, Deserialize, @@ -421,6 +445,11 @@ pub enum KemAlgorithm { Debug, Eq, PartialEq, + PrimitiveEnum_u8, + strum::EnumString, + strum::EnumIter, + strum::EnumCount, + strum_macros::VariantNames, )] pub enum SigAlgorithm { #[default] @@ -428,6 +457,103 @@ pub enum SigAlgorithm { Falcon1024 = 1, } +impl SigAlgorithm { + pub fn variants() -> Vec { + Self::VARIANTS.iter().map(|s| s.to_string()).collect() + } +} + +/// Provides the enumeration for all security levels +#[derive( + Serialize, + Deserialize, + Copy, + Clone, + Debug, + Default, + strum::EnumString, + strum::EnumIter, + strum::EnumCount, +)] +pub enum SecurityLevel { + #[default] + Standard, + Reinforced, + High, + Ultra, + Extreme, + Custom(u8), +} + +impl SecurityLevel { + pub fn variants() -> Vec { + Self::iter().collect() + } + + /// Returns byte representation of self + pub fn value(self) -> u8 { + match self { + SecurityLevel::Standard => 0, + SecurityLevel::Reinforced => 1, + SecurityLevel::High => 2, + SecurityLevel::Ultra => 3, + SecurityLevel::Extreme => 4, + SecurityLevel::Custom(val) => val, + } + } + + /// Possibly returns the security_level given an input value + pub fn for_value(val: usize) -> Option { + Some(SecurityLevel::from(u8::try_from(val).ok()?)) + } +} + +impl From for SecurityLevel { + fn from(val: u8) -> Self { + match val { + 0 => SecurityLevel::Standard, + 1 => SecurityLevel::Reinforced, + 2 => SecurityLevel::High, + 3 => SecurityLevel::Ultra, + 4 => SecurityLevel::Extreme, + n => SecurityLevel::Custom(n), + } + } +} + +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Default)] +pub enum HeaderObfuscatorSettings { + #[default] + /// Disables header obfuscation (default) + Disabled, + /// Enables header obfuscation to help mitigate some deep packet inspection techniques using a pseudorandom key + Enabled, + /// Enables header obfuscation with a specific key. This value must be symmetric between both endpoints, otherwise the obfuscation will fail + EnabledWithKey(u128), +} + +impl From for HeaderObfuscatorSettings { + fn from(val: u128) -> Self { + HeaderObfuscatorSettings::EnabledWithKey(val) + } +} + +impl From for HeaderObfuscatorSettings { + fn from(value: bool) -> Self { + if value { + HeaderObfuscatorSettings::Enabled + } else { + HeaderObfuscatorSettings::Disabled + } + } +} + +impl From for HeaderObfuscatorSettings { + fn from(value: Uuid) -> Self { + HeaderObfuscatorSettings::EnabledWithKey(value.as_u128()) + } +} + impl AlgorithmsExt for KemAlgorithm { fn set_crypto_param(&self, params: &mut CryptoParameters) { params.kem_algorithm = *self; @@ -487,64 +613,6 @@ impl From for CryptoParameters { } } -/// Provides the enumeration for all security levels -#[derive(Serialize, Deserialize, Copy, Clone, Debug, Default)] -pub enum SecurityLevel { - #[default] - Standard, - Reinforced, - High, - Ultra, - Extreme, - Custom(u8), -} - -impl From for SecurityLevel { - fn from(val: u8) -> Self { - match val { - 0 => SecurityLevel::Standard, - 1 => SecurityLevel::Reinforced, - 2 => SecurityLevel::High, - 3 => SecurityLevel::Ultra, - 4 => SecurityLevel::Extreme, - n => SecurityLevel::Custom(n), - } - } -} - -#[derive(Serialize, Deserialize, Copy, Clone, Debug, Default)] -pub enum HeaderObfuscatorSettings { - /// Enables header obfuscation to help mitigate some deep packet inspection techniques using a pseudorandom key - Enabled, - #[default] - /// Disables header obfuscation (default) - Disabled, - /// Enables header obfuscation with a specific key. This value must be symmetric between both endpoints, otherwise the obfuscation will fail - EnabledWithKey(u128), -} - -impl From for HeaderObfuscatorSettings { - fn from(val: u128) -> Self { - HeaderObfuscatorSettings::EnabledWithKey(val) - } -} - -impl From for HeaderObfuscatorSettings { - fn from(value: bool) -> Self { - if value { - HeaderObfuscatorSettings::Enabled - } else { - HeaderObfuscatorSettings::Disabled - } - } -} - -impl From for HeaderObfuscatorSettings { - fn from(value: Uuid) -> Self { - HeaderObfuscatorSettings::EnabledWithKey(value.as_u128()) - } -} - #[cfg(test)] mod test { use crate::crypto::SecBuffer; @@ -577,3 +645,32 @@ mod test { assert_ne!(a, b); } } + +#[derive(Default, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)] +pub struct PreSharedKey { + passwords: Vec>, +} + +impl PreSharedKey { + /// Adds a password to the session password list. Both connecting nodes + /// must have matching passwords in order to establish a connection. + /// Note: The password is hashed using SHA-256 before being added to the list to increase security. + pub fn add_password>(mut self, password: T) -> Self { + let mut hasher = sha3::Sha3_256::default(); + hasher.update(password.as_ref()); + self.passwords.push(hasher.finalize().to_vec()); + self + } +} + +impl AsRef<[Vec]> for PreSharedKey { + fn as_ref(&self) -> &[Vec] { + &self.passwords + } +} + +impl> From for PreSharedKey { + fn from(password: T) -> Self { + PreSharedKey::default().add_password(password) + } +} diff --git a/citadel_types/src/proto/mod.rs b/citadel_types/src/proto/mod.rs index ac1f674d6..7583e64b0 100644 --- a/citadel_types/src/proto/mod.rs +++ b/citadel_types/src/proto/mod.rs @@ -1,9 +1,11 @@ use crate::crypto::{CryptoParameters, SecrecyMode, SecurityLevel}; use crate::prelude::HeaderObfuscatorSettings; use crate::utils; +use packed_struct::derive::PrimitiveEnum_u8; use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display, Formatter}; use std::path::PathBuf; +use strum::VariantNames; use uuid::Uuid; #[derive(Copy, Clone, Serialize, Deserialize, Debug)] @@ -164,11 +166,31 @@ pub struct SessionSecuritySettings { pub header_obfuscator_settings: HeaderObfuscatorSettings, } -#[derive(Debug, Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Default)] +#[derive( + Debug, + Serialize, + Deserialize, + Copy, + Clone, + Eq, + PartialEq, + Default, + PrimitiveEnum_u8, + strum::EnumString, + strum::EnumIter, + strum::EnumCount, + strum_macros::VariantNames, +)] pub enum UdpMode { - Enabled, #[default] Disabled, + Enabled, +} + +impl UdpMode { + pub fn variants() -> Vec { + Self::VARIANTS.iter().map(|s| s.to_string()).collect() + } } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/citadel_types/src/utils/mod.rs b/citadel_types/src/utils/mod.rs index c4d6cedfc..1c1b6e79e 100644 --- a/citadel_types/src/utils/mod.rs +++ b/citadel_types/src/utils/mod.rs @@ -66,13 +66,13 @@ pub fn const_time_compare(this: &[u8], other: &[u8]) -> bool { pub fn validate_crypto_params(params: &CryptoParameters) -> Result<(), Error> { let uses_kyber_kem = params.kem_algorithm == KemAlgorithm::Kyber; - if params.encryption_algorithm == EncryptionAlgorithm::Kyber && !uses_kyber_kem { + if params.encryption_algorithm == EncryptionAlgorithm::KyberHybrid && !uses_kyber_kem { return Err(Error::Generic( "Invalid crypto parameter combination. Kyber encryption must be paired with Kyber KEM", )); } - if params.encryption_algorithm == EncryptionAlgorithm::Kyber + if params.encryption_algorithm == EncryptionAlgorithm::KyberHybrid && params.sig_algorithm == SigAlgorithm::None { return Err(Error::Generic( diff --git a/citadel_user/src/account_manager.rs b/citadel_user/src/account_manager.rs index 9c33af73a..50610a2e4 100644 --- a/citadel_user/src/account_manager.rs +++ b/citadel_user/src/account_manager.rs @@ -91,6 +91,7 @@ //! use crate::auth::proposed_credentials::ProposedCredentials; +use crate::auth::DeclaredAuthenticationMode; use crate::backend::memory::MemoryBackend; use crate::backend::{BackendType, PersistenceHandler}; use crate::client_account::ClientNetworkAccount; @@ -188,6 +189,9 @@ impl AccountManager { server_misc_settings: server_misc_settings.unwrap_or_default(), }; + // Allow the local node to use the backend to store arbitrary data + this.setup_local_only_account().await?; + Ok(this) } @@ -209,6 +213,14 @@ impl AccountManager { let reserved_cid = self .persistence_handler .get_cid_by_username(creds.username()); + + if reserved_cid == 0 { + return Err(AccountError::Generic( + "Cannot call register_impersonal_hyperlan_client_network_account with a CID of 0" + .to_string(), + )); + } + let auth_store = creds .derive_server_container(&self.node_argon_settings, self.get_misc_settings()) .await?; @@ -237,7 +249,7 @@ impl AccountManager { false, conn_info, auth_store, - session_crypto_state, + Some(session_crypto_state), ) .await?; log::trace!(target: "citadel", "Created impersonal CNAC ..."); @@ -257,10 +269,17 @@ impl AccountManager { let valid_cid = self .persistence_handler .get_cid_by_username(creds.username()); + + if valid_cid == 0 { + return Err(AccountError::Generic( + "Cannot call register_personal_hyperlan_server with a CID of 0".to_string(), + )); + } + let client_auth_store = creds.into_auth_store(); let cnac = ClientNetworkAccount::::new_from_network_personal( valid_cid, - session_crypto_state, + Some(session_crypto_state), client_auth_store, conn_info, ) @@ -270,6 +289,28 @@ impl AccountManager { Ok(cnac) } + /// Sets up a local-only account that should only be accessed by a local handle. Network requests + /// to the zero CID will be rejected by the networking layer to deter any malicious behavior. However, + /// it is highly advisable to independently use and store an encryption key outside of the local program such + /// that all stored data is encrypted in case of attack. This function needs to be used with caution. + async fn setup_local_only_account(&self) -> Result<(), AccountError> { + // Setup mock CNAC + let cnac = ClientNetworkAccount::::new_from_network_personal( + 0, + None, + DeclaredAuthenticationMode::Transient { + username: Default::default(), + full_name: Default::default(), + }, + ConnectionInfo::new("127.0.0.1:12345").expect("Should be valid addr"), + ) + .await?; + + self.persistence_handler.save_cnac(&cnac).await?; + + Ok(()) + } + /// Determines if the HyperLAN client is registered /// Impersonal mode pub async fn hyperlan_cid_is_registered(&self, cid: u64) -> Result { diff --git a/citadel_user/src/auth/proposed_credentials.rs b/citadel_user/src/auth/proposed_credentials.rs index 485d64c40..dc9996f6e 100644 --- a/citadel_user/src/auth/proposed_credentials.rs +++ b/citadel_user/src/auth/proposed_credentials.rs @@ -240,7 +240,7 @@ impl ProposedCredentials { ) -> Result { match self { Self::Disabled { .. } => { - if server_misc_settings.allow_passwordless { + if server_misc_settings.allow_transient_connections { Ok(self.into_auth_store()) } else { Err(AccountError::msg( diff --git a/citadel_user/src/backend/filesystem_backend.rs b/citadel_user/src/backend/filesystem_backend.rs index 5d4245173..d2d93d438 100644 --- a/citadel_user/src/backend/filesystem_backend.rs +++ b/citadel_user/src/backend/filesystem_backend.rs @@ -485,7 +485,7 @@ impl BackendConnection for FilesystemBackend BackendConnection for FilesystemBackend>, std::io::Error> }), diff --git a/citadel_user/src/backend/memory.rs b/citadel_user/src/backend/memory.rs index cfe48d0d4..3223615f1 100644 --- a/citadel_user/src/backend/memory.rs +++ b/citadel_user/src/backend/memory.rs @@ -225,9 +225,14 @@ impl BackendConnection for MemoryBackend BackendConnection for RedisBackend BackendConnection for SqlBackend limit: Option, ) -> Result, AccountError> { let conn = &(self.get_conn().await?); - // cnacs(cid VARCHAR(20) NOT NULL, is_connected BOOL, is_personal BOOL, username VARCHAR({}) UNIQUE, full_name TEXT, creation_date TEXT, bin LONGTEXT, PRIMARY KEY (cid)) let query = if let Some(limit) = limit { format!( "SELECT cid, is_personal, username, full_name, creation_date FROM cnacs LIMIT {limit}", @@ -530,6 +529,11 @@ impl BackendConnection for SqlBackend .into_iter() .filter_map(|row| { let cid = try_get_cid_from_row(&row, "cid")?; + + if cid == 0 { + return None; + } + let is_personal = self.get_bool(&row, "is_personal").ok()?; let username = try_get_blob_as_utf8("username", &row).ok()?; let full_name = try_get_blob_as_utf8("full_name", &row).ok()?; @@ -613,7 +617,7 @@ impl BackendConnection for SqlBackend Ok(peers .iter() - .map(|cid| results.iter().any(|peer_cid| *cid == *peer_cid)) + .map(|cid| results.contains(cid)) .collect()) } diff --git a/citadel_user/src/client_account.rs b/citadel_user/src/client_account.rs index 0afe8f138..2360be43f 100644 --- a/citadel_user/src/client_account.rs +++ b/citadel_user/src/client_account.rs @@ -157,7 +157,7 @@ struct ClientNetworkAccountInner, + pub crypto_session_state: Option>, /// For storing critical ID information for this CNAC pub auth_store: DeclaredAuthenticationMode, peer_state: RwLock, @@ -173,8 +173,14 @@ impl ClientNetworkAccount { is_personal: bool, adjacent_nac: ConnectionInfo, auth_store: DeclaredAuthenticationMode, - session_crypto_state: PeerSessionCrypto, + crypto_session_state: Option>, ) -> Result { + if valid_cid == 0 && crypto_session_state.is_some() { + return Err(AccountError::Generic( + "Cannot create a cryptographically secure CNAC with a CID of 0".to_string(), + )); + } + log::trace!(target: "citadel", "Creating CNAC w/valid cid: {:?}", valid_cid); let creation_date = get_present_formatted_timestamp(); @@ -194,7 +200,7 @@ impl ClientNetworkAccount { adjacent_nac, is_personal, is_transient, - crypto_session_state: session_crypto_state, + crypto_session_state, peer_state: RwLock::new(peer_state), _phantom: PhantomData, }), @@ -202,7 +208,7 @@ impl ClientNetworkAccount { } pub fn get_session_crypto(&self) -> &PeerSessionCrypto { - &self.inner.crypto_session_state + self.inner.crypto_session_state.as_ref().expect("Unauthorized access to the zero CID. Raising to panic. Access to the zero CID is prohibited.") } /// Resets the toolset, if necessary. If the CNAC was freshly serialized, the hyper ratchet @@ -234,7 +240,7 @@ impl ClientNetworkAccount { /// Towards the end of the registration phase, the [`AccountState`] gets transmitted to Alice. pub async fn new_from_network_personal( valid_cid: u64, - session_crypto_state: PeerSessionCrypto, + session_crypto_state: Option>, auth_store: DeclaredAuthenticationMode, conn_info: ConnectionInfo, ) -> Result { diff --git a/citadel_user/src/server_misc_settings.rs b/citadel_user/src/server_misc_settings.rs index 45a8afa14..b63e7682c 100644 --- a/citadel_user/src/server_misc_settings.rs +++ b/citadel_user/src/server_misc_settings.rs @@ -17,18 +17,18 @@ //! //! // Create custom server settings //! let settings = ServerMiscSettings { -//! allow_passwordless: false, +//! allow_transient_connections: false, //! credential_requirements: CredentialRequirements::default(), //! }; //! //! // Or use default settings //! let default_settings = ServerMiscSettings::default(); -//! assert!(default_settings.allow_passwordless); // Passwordless auth is enabled by default +//! assert!(default_settings.allow_transient_connections); // Passwordless auth is enabled by default //! ``` //! //! # Important Notes //! -//! * Enabling passwordless authentication (`allow_passwordless`) should be done with caution +//! * Enabling passwordless authentication (`allow_transient_connections`) should be done with caution //! and only in trusted environments //! * Credential requirements are enforced even when creating new accounts //! * Default settings prioritize ease of use over security - modify as needed for production @@ -45,7 +45,7 @@ use crate::credentials::CredentialRequirements; #[derive(Clone)] pub struct ServerMiscSettings { /// If enabled, allows inbound connections to use no credentials when logging-in - pub allow_passwordless: bool, + pub allow_transient_connections: bool, /// Enforces specific requirements on credentials pub credential_requirements: CredentialRequirements, } @@ -53,7 +53,7 @@ pub struct ServerMiscSettings { impl Default for ServerMiscSettings { fn default() -> Self { Self { - allow_passwordless: true, + allow_transient_connections: true, credential_requirements: Default::default(), } } diff --git a/citadel_user/tests/primary.rs b/citadel_user/tests/primary.rs index 339767054..fa1cc38e3 100644 --- a/citadel_user/tests/primary.rs +++ b/citadel_user/tests/primary.rs @@ -582,6 +582,51 @@ mod tests { .await } + #[tokio::test] + async fn test_byte_map_local() -> Result<(), AccountError> { + test_harness(|_container, pers_cl, _pers_se| async move { + let dummy = Vec::from("Hello, world!"); + + assert!(pers_cl + .store_byte_map_value(0, 0, "thekey", "sub_key", dummy.clone()) + .await + .unwrap() + .is_none()); + assert_eq!( + pers_cl + .get_byte_map_value(0, 0, "thekey", "sub_key") + .await + .unwrap() + .unwrap(), + dummy.clone() + ); + assert_eq!( + pers_cl + .get_byte_map_values_by_key(0, 0, "thekey") + .await + .unwrap() + .remove("sub_key") + .unwrap(), + dummy.clone() + ); + assert_eq!( + pers_cl + .remove_byte_map_value(0, 0, "thekey", "sub_key") + .await + .unwrap() + .unwrap(), + dummy.clone() + ); + assert!(pers_cl + .remove_byte_map_value(0, 0, "thekey", "sub_key") + .await + .unwrap() + .is_none()); + Ok(()) + }) + .await + } + #[tokio::test] async fn test_byte_map2() -> Result<(), AccountError> { test_harness(|container, pers_cl, _pers_se| async move { diff --git a/citadel_wire/src/error.rs b/citadel_wire/src/error.rs index 5a33ec0a1..1905cd0e5 100644 --- a/citadel_wire/src/error.rs +++ b/citadel_wire/src/error.rs @@ -71,7 +71,7 @@ impl std::error::Error for FirewallError {} impl From for std::io::Error { fn from(val: FirewallError) -> Self { - std::io::Error::new(std::io::ErrorKind::Other, val.to_string()) + std::io::Error::other(val.to_string()) } } diff --git a/citadel_wire/src/standard/quic.rs b/citadel_wire/src/standard/quic.rs index dd5a55fdd..17f2a5b0c 100644 --- a/citadel_wire/src/standard/quic.rs +++ b/citadel_wire/src/standard/quic.rs @@ -335,7 +335,7 @@ pub fn rustls_client_config_to_quinn_config( ) -> std::io::Result { Ok(ClientConfig::new(Arc::new( QuicClientConfig::try_from(cfg) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?, + .map_err(std::io::Error::other)?, ))) } diff --git a/citadel_wire/src/standard/tls.rs b/citadel_wire/src/standard/tls.rs index 1094ffc1a..a307c87bb 100644 --- a/citadel_wire/src/standard/tls.rs +++ b/citadel_wire/src/standard/tls.rs @@ -182,7 +182,7 @@ pub fn create_server_config( pub async fn load_native_certs_async() -> Result>, Error> { citadel_io::tokio::task::spawn_blocking(load_native_certs) .await - .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, format!("{err:?}")))? + .map_err(|err| std::io::Error::other(format!("{err:?}")))? } /// Loads native certs. This is an expensive operation, and should be called once per node