diff --git a/rosenpass/src/protocol/mod.rs b/rosenpass/src/protocol/mod.rs index aa41d7736..e8079308c 100644 --- a/rosenpass/src/protocol/mod.rs +++ b/rosenpass/src/protocol/mod.rs @@ -16,7 +16,11 @@ //! # Example Handshake //! //! This example illustrates a minimal setup for a key-exchange between two -//! [CryptoServer]. +//! [CryptoServer]s; this is what we use for some testing purposes but it is not +//! what should be used in a real world application, as timing-based events +//! are handled by [CryptoServer::poll]. +//! +//! See [CryptoServer::poll] on how to use crypto server in polling mode for production usage. //! //! ``` //! use std::ops::DerefMut; diff --git a/rosenpass/src/protocol/protocol.rs b/rosenpass/src/protocol/protocol.rs index 5d15442ba..ba8fa21c9 100644 --- a/rosenpass/src/protocol/protocol.rs +++ b/rosenpass/src/protocol/protocol.rs @@ -1,3 +1,8 @@ +//! This module is a left-over from when [crate::protocol] was a monolithic file. +//! +//! It is merged entirely into [crate::protocol] and should be split up into multiple +//! files. + use std::borrow::Borrow; use std::convert::Infallible; use std::fmt::Debug; @@ -36,82 +41,147 @@ use crate::{hash_domains, msgs::*, RosenpassError}; /// A type for time, e.g. for backoff before re-tries pub type Timing = f64; -/// Before Common Era (or more practically: Definitely so old it needs refreshing) +/// Magic time stamp to indicate some object is ancient; "Before Common Era" +/// +/// This is for instance used as a magic time stamp indicating age when some +/// cryptographic object certainly needs to be refreshed. /// /// Using this instead of Timing::MIN or Timing::INFINITY to avoid floating /// point math weirdness. pub const BCE: Timing = -3600.0 * 24.0 * 356.0 * 10_000.0; -// Actually it's eight hours; This is intentional to avoid weirdness -// regarding unexpectedly large numbers in system APIs as this is < i16::MAX +/// Magic time stamp to indicate that some process is not time-limited +/// +/// Actually it's eight hours; This is intentional to avoid weirdness +/// regarding unexpectedly large numbers in system APIs as this is < i16::MAX pub const UNENDING: Timing = 3600.0 * 8.0; -// From the wireguard paper; rekey every two minutes, -// discard the key if no rekey is achieved within three +/// Time after which the responder attempts to rekey the session +/// +/// From the wireguard paper: rekey every two minutes, +/// discard the key if no rekey is achieved within three pub const REKEY_AFTER_TIME_RESPONDER: Timing = 120.0; +/// Time after which the initiator attempts to rekey the session. +/// +/// This happens ten seconds after [REKEY_AFTER_TIME_RESPONDER], so +/// parties would usually switch roles after every handshake. +/// +/// From the wireguard paper: rekey every two minutes, +/// discard the key if no rekey is achieved within three pub const REKEY_AFTER_TIME_INITIATOR: Timing = 130.0; +/// Time after which either party rejects the current key. +/// +/// At this point a new key should have been negotiated. +/// Rejection happens 50-60 seconds after key renegotiation +/// to allow for a graceful handover. +/// +/// From the wireguard paper: rekey every two minutes, +/// discard the key if no rekey is achieved within three pub const REJECT_AFTER_TIME: Timing = 180.0; -// From the wireguard paper; "under no circumstances send an initiation message more than once every 5 seconds" +/// Maximum period between sending rekey initiation messages +/// +/// From the wireguard paper; "under no circumstances send an initiation message more than once every 5 seconds" pub const REKEY_TIMEOUT: Timing = 5.0; -// Cookie Secret `cookie_secret` in the whitepaper +/// The length of the `cookie_secret` in the [whitepaper](https://rosenpass.eu/whitepaper.pdf) pub const COOKIE_SECRET_LEN: usize = MAC_SIZE; +/// The life time of the `cookie_secret` in the [whitepaper](https://rosenpass.eu/whitepaper.pdf) pub const COOKIE_SECRET_EPOCH: Timing = 120.0; -// Cookie value len in whitepaper +/// Length of a cookie value (see info about the cookie mechanism in the [whitepaper](https://rosenpass.eu/whitepaper.pdf)) pub const COOKIE_VALUE_LEN: usize = MAC_SIZE; -// Peer `cookie_value` validity +/// Time after which to delete a cookie, as the initiator, for a certain peer (see info about the cookie mechanism in the [whitepaper](https://rosenpass.eu/whitepaper.pdf)) pub const PEER_COOKIE_VALUE_EPOCH: Timing = 120.0; -// Seconds until the biscuit key is changed; we issue biscuits -// using one biscuit key for one epoch and store the biscuit for -// decryption for a second epoch +/// Seconds until the biscuit key is changed; we issue biscuits +/// using one biscuit key for one epoch and store the biscuit for +/// decryption for a second epoch +/// +/// The biscuit mechanism is used to make sure the responder is stateless in our protocol. pub const BISCUIT_EPOCH: Timing = 300.0; -// Retransmission pub constants; will retransmit for up to _ABORT seconds; -// starting with a delay of _DELAY_BEGIN seconds and increasing the delay -// exponentially by a factor of _DELAY_GROWTH up to _DELAY_END. -// An additional jitter factor of ±_DELAY_JITTER is added. +/// The initiator opportunistically retransmits their messages; it applies an increasing delay +/// between each retreansmission. This is the factor by which the delay grows after each +/// retransmission. pub const RETRANSMIT_DELAY_GROWTH: Timing = 2.0; +/// The initiator opportunistically retransmits their messages; it applies an increasing delay +/// between each retreansmission. This is the initial delay between retransmissions. pub const RETRANSMIT_DELAY_BEGIN: Timing = 0.5; +/// The initiator opportunistically retransmits their messages; it applies an increasing delay +/// between each retreansmission. This is the maximum delay between retransmissions. pub const RETRANSMIT_DELAY_END: Timing = 10.0; +/// The initiator opportunistically retransmits their messages; it applies an increasing delay +/// between each retreansmission. This is the jitter (randomness) applied to the retransmission +/// delay. pub const RETRANSMIT_DELAY_JITTER: Timing = 0.5; +/// This is the maximum delay that can separate two events for us to consider the events to have +/// happened at the same time. pub const EVENT_GRACE: Timing = 0.0025; // UTILITY FUNCTIONS ///////////////////////////// -// Event handling: For an event at T we sleep for T-now -// but we act on the event starting at T-EVENT_GRACE already -// to avoid sleeping for very short periods. This also avoids -// busy loop in case the sleep subsystem is imprecise. Our timing -// is therefor generally accurate up to ±2∙EVENT_GRACE +/// An even `ev` has happened relative to a point in time `now` +/// if the `ev` does not lie in the future relative to now. +/// +/// An event lies in the future relative to `now` if +/// does not lie in the past or present. +/// +/// An event `ev` lies in the past if `ev < now`. It lies in the +/// present if the absolute difference between `ev` and `now` is +/// smaller than [EVENT_GRACE]. +/// +/// Think of this as `ev <= now` for with [EVENT_GRACE] applied. +/// +/// # Examples +/// +/// ``` +/// use rosenpass::protocol::{has_happened, EVENT_GRACE}; +/// assert!(has_happened(EVENT_GRACE * -1.0, 0.0)); +/// assert!(has_happened(0.0, 0.0)); +/// assert!(has_happened(EVENT_GRACE * 0.999, 0.0)); +/// assert!(!has_happened(EVENT_GRACE * 1.001, 0.0)); +/// ``` pub fn has_happened(ev: Timing, now: Timing) -> bool { (ev - now) < EVENT_GRACE } // DATA STRUCTURES & BASIC TRAITS & ACCESSORS //// +/// Static public key +/// +/// Using [PublicBox] instead of [Public] because Classic McEliece keys are very large. pub type SPk = PublicBox<{ StaticKem::PK_LEN }>; +/// Static secret key pub type SSk = Secret<{ StaticKem::SK_LEN }>; +/// Ephemeral public key pub type EPk = Public<{ EphemeralKem::PK_LEN }>; +/// Ephemeral secret key pub type ESk = Secret<{ EphemeralKem::SK_LEN }>; +/// Symmetric key pub type SymKey = Secret; +/// Symmetric hash pub type SymHash = Public; +/// Peer ID (derived from the public key, see the hash derivations in the [whitepaper](https://rosenpass.eu/whitepaper.pdf)) pub type PeerId = Public; +/// Session ID pub type SessionId = Public; +/// Biscuit ID pub type BiscuitId = Public; +/// Nonce for use with random-nonce AEAD pub type XAEADNonce = Public<{ xaead::NONCE_LEN }>; +/// Buffer capably of holding any Rosenpass protocol message pub type MsgBuf = Public; +/// Server-local peer number; this is just the index in [CryptoServer::peers] pub type PeerNo = usize; -/// Implementation of the cryptographic protocol +/// This is the implementation of our cryptographic protocol. /// /// The scope of this is: /// @@ -122,64 +192,281 @@ pub type PeerNo = usize; /// Not in scope of this struct: /// /// - handling of external IO (like sockets etc.) +/// +/// +/// +/// # Example +/// +/// See the example on how to use this CryptoServer without [Self::poll] in [crate::protocol]. +/// +/// See [Self::poll] on how to use a CryptoServer with poll. #[derive(Debug)] pub struct CryptoServer { + /// The source of most timing information for the Rosenpass protocol + /// + /// We store most timing information in the form of f64 values, relative to a point stored in + /// this field. pub timebase: Timebase, - // Server Crypto + /// Static Secret Key Mine (our secret key) pub sskm: SSk, + /// Static Public Key Mine (our public key) pub spkm: SPk, + /// Counter used to fill the [Biscuit::biscuit_no] field for biscuits issued. + /// + /// Every [Biscuit] issued contains a biscuit number; this is the counter used to generate + /// the biscuit number. + /// + /// See [HandshakeState::store_biscuit] and [HandshakeState::load_biscuit] pub biscuit_ctr: BiscuitId, + /// Every [Biscuit] issued is encrypted before being transmitted to the initiator. + /// + /// The biscuit key used is rotated every [BISCUIT_EPOCH]. We store the previous + /// biscuit key for decryption only. + /// + /// See [HandshakeState::store_biscuit], [HandshakeState::load_biscuit], and + /// [CryptoServer::active_biscuit_key]. pub biscuit_keys: [BiscuitKey; 2], - // Peer/Handshake DB + /// List of peers and their session and handshake states pub peers: Vec, + /// Index into the list of peers. See [IndexKey] for details. pub index: HashMap, + /// Hash key for known responder confirmation responses. + /// + /// These hashes are then used for lookups in [Self::index] using + /// the [IndexKey::KnownInitConfResponse] enum case. + /// + /// This is used to allow for retransmission of responder confirmation (i.e. + /// [Envelope]<[EmptyData]>) messages in response to [Envelope]<[InitConf]> + /// without involving the cryptographic layer. + /// + /// See [KnownResponse], [KnownInitConfResponse], [KnownInitConfResponsePtr], + /// and the [whitepaper](https://rosenpass.eu/whitepaper.pdf) pub known_response_hasher: KnownResponseHasher, - // Tick handling + /// We poll peers in a round-robin fashion, but we can not poll + /// all peers in one go. If one of them returns a result, [CryptoServer::poll] + /// will return the particular peer's event. + /// + /// This value makes sure we start polling with the next peer on the next [CryptoServer::poll] + /// call. + /// + /// See [CryptoServer::poll], [CryptoServer::peer_ptrs_off]. pub peer_poll_off: usize, - // Random state which changes every COOKIE_SECRET_EPOCH seconds + /// Cookies issued for the purpose of DOS mitigations are derived from a + /// secret key. This field stores those secret keys. + /// + /// The value is rotated every [COOKIE_SECRET_EPOCH]. + /// + /// See [CryptoServer::handle_msg_under_load], and [CryptoServer::active_or_retired_cookie_secrets]. pub cookie_secrets: [CookieSecret; 2], } -/// Container for storing cookie types: Biscuit, CookieSecret, CookieValue +/// Container for storing cookie secrets like [BiscuitKey] or [CookieSecret]. +/// +/// This is really just a secret key and a time stamp of creation. Concrete +/// usages (such as for the biscuit key) impose a time limit about how long +/// a key can be used and the time of creation is used to impose that time limit. +/// +/// # Examples +/// +/// ``` +/// use rosenpass_util::time::Timebase; +/// use rosenpass::protocol::{BCE, SymKey, CookieStore}; +/// +/// rosenpass_secret_memory::secret_policy_try_use_memfd_secrets(); +/// +/// let fixed_secret = SymKey::random(); +/// let timebase = Timebase::default(); +/// +/// let mut store = CookieStore::<32>::new(); +/// assert_ne!(store.value.secret(), SymKey::zero().secret()); +/// assert_eq!(store.created_at, BCE); +/// +/// let time_before_call = timebase.now(); +/// store.update(&timebase, fixed_secret.secret()); +/// assert_eq!(store.value.secret(), fixed_secret.secret()); +/// assert!(store.created_at < timebase.now()); +/// assert!(store.created_at > time_before_call); +/// +/// // Same as new() +/// store.erase(); +/// assert_ne!(store.value.secret(), SymKey::zero().secret()); +/// assert_eq!(store.created_at, BCE); +/// +/// let secret_before_call = store.value.clone(); +/// let time_before_call = timebase.now(); +/// store.randomize(&timebase); +/// assert_ne!(store.value.secret(), secret_before_call.secret()); +/// assert!(store.created_at < timebase.now()); +/// assert!(store.created_at > time_before_call); +/// ``` #[derive(Debug)] pub struct CookieStore { + /// Time of creation of the secret key pub created_at: Timing, + /// The secret key pub value: Secret, } /// Stores cookie secret, which is used to create a rotating the cookie value +/// +/// Concrete value is in [CryptoServer::cookie_secrets]. +/// +/// The pointer type is [ServerCookieSecretPtr]. pub type CookieSecret = CookieStore; +/// Storage for our biscuit keys. +/// +/// The biscuit keys encrypt what we call "biscuits". +/// These biscuits contain the responder state for a particular handshake. By moving +/// state into these biscuits, we make sure the responder is stateless. +/// /// A Biscuit is like a fancy cookie. To avoid state disruption attacks, /// the responder doesn't store state. Instead the state is stored in a /// Biscuit, that is encrypted using the [BiscuitKey] which is only known to /// the Responder. Thus secrecy of the Responder state is not violated, still /// the responder can avoid storing this state. +/// +/// Concrete value is in [CryptoServer::biscuit_keys]. +/// +/// The pointer type is [BiscuitKeyPtr]. pub type BiscuitKey = CookieStore; +/// We maintain various indices in [CryptoServer::index], mapping some key to a particular +/// [PeerNo], i.e. to an index in [CryptoServer::peers]. These are the possible index key. #[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum IndexKey { + /// Lookup of a particular peer given the [PeerId], i.e. a value derived from the peers public + /// key as created by [CryptoServer::pidm] or [Peer::pidt]. + /// + /// The peer id is used by the initiator to tell the responder about its identity in + /// [crate::msgs::InitHello]. + /// + /// See also the pointer types [PeerPtr]. Peer(PeerId), + /// Lookup of a particular session id. + /// + /// This is used to look up both established sessions (see + /// [CryptoServer::lookup_session]) and ongoing handshakes (see [CryptoServer::lookup_handshake]). + /// + /// Lookup of a peer to get an established session or a handshake is sufficient, because a peer + /// contains a limited number of sessions and handshakes ([Peer::session] and [Peer::handshake] respectively). + /// + /// See also the pointer types [IniHsPtr] and [SessionPtr]. Sid(SessionId), + /// Lookup of a cached response ([Envelope]<[EmptyData]>) to an [InitConf] (i.e. + /// [Envelope]<[InitConf]>) message. + /// + /// See [KnownInitConfResponsePtr] on how this value is maintained. KnownInitConfResponse(KnownResponseHash), } +/// A peer that the server can execute a key exchange with. +/// +/// Peers generally live in [CryptoServer::peers]. [PeerNo] captures an array +/// into this field and [PeerPtr] is a wrapper around a [PeerNo] imbued with +/// peer specific functionality. [CryptoServer::index] contains a list of lookup-keys +/// for retrieving peers using various keys (see [IndexKey]). +/// +/// # Examples +/// +/// ``` +/// use std::ops::DerefMut; +/// use rosenpass::protocol::{SSk, SPk, SymKey, Peer}; +/// use rosenpass_ciphers::kem::StaticKem; +/// use rosenpass_cipher_traits::Kem; +/// +/// rosenpass_secret_memory::secret_policy_try_use_memfd_secrets(); +/// +/// let (mut sskt, mut spkt) = (SSk::zero(), SPk::zero()); +/// StaticKem::keygen(sskt.secret_mut(), spkt.deref_mut())?; +/// +/// let (mut sskt2, mut spkt2) = (SSk::zero(), SPk::zero()); +/// StaticKem::keygen(sskt2.secret_mut(), spkt2.deref_mut())?; +/// +/// let psk = SymKey::random(); +/// +/// // Creation with a PSK +/// let peer_psk = Peer::new(psk, spkt.clone()); +/// +/// // Creation without a PSK +/// let peer_nopsk = Peer::new(SymKey::zero(), spkt); +/// +/// // Create a second peer +/// let peer_psk_2 = Peer::new(SymKey::zero(), spkt2); +/// +/// // Peer ID does not depend on PSK, but it does depend on the public key +/// assert_eq!(peer_psk.pidt()?, peer_nopsk.pidt()?); +/// assert_ne!(peer_psk.pidt()?, peer_psk_2.pidt()?); +/// +/// Ok::<(), anyhow::Error>(()) +/// ``` #[derive(Debug)] pub struct Peer { + /// The pre-shared key shared with the peer. + /// + /// This is a symmetric key generated by the user upon setting up a peer. + /// It must be shared with both peers who wish to exchange keys. + /// + /// The Rosenpass protocol is secure if the pre-shared key was generated securely + /// and is known only to both peers, even if the peers secret keys are leaked or if + /// the asymmetric cryptographic algorithms are broken. pub psk: SymKey, + /// Static Public Key Theirs. This is the peer's public key. pub spkt: SPk, + /// The biscuit number ([Biscuit::biscuit_no]) last used during reception of a + /// [Biscuit] inside of a [InitConf] message. + /// + /// This field's job is to make sure that [CryptoServer::handle_init_conf] will never + /// accept the same biscuit twice; i.e. it is used to protect against [InitConf] replay + /// attacks. pub biscuit_used: BiscuitId, + /// The last established session + /// + /// This is indexed though [IndexKey::Sid]. pub session: Option, + /// Ongoing handshake, in initiator mode. + /// + /// There is no field for storing handshakes from the responder perspective, + /// because the state is stored inside a [Biscuit] to make sure the responder + /// is stateless. + /// + /// This is indexed though [IndexKey::Sid]. pub handshake: Option, + /// Used to make sure that the same [PollResult::SendInitiation] event is never issued twice. + /// + /// [CryptoServer::poll] will not produce a [InitHello] message, i.e. call + /// [CryptoServer::initiate_handshake] (and by proxy [CryptoServer::handle_initiation]), + /// on its own accord. Instead, it will issue a pub initiation_requested: bool, + /// Stores a known response for a [Envelope]<[InitConf]> message, i.e. a + /// [Envelope]<[EmptyData]>. + /// + /// Upon reception of an InitConf message, [CryptoServer::handle_msg] first checks + /// if a cached response exists through [IndexKey::KnownInitConfResponse]. If one exists, + /// then this field must be set to [Option::Some] and the cached response is returned. + /// + /// This allows us to perform retransmission for the purpose of dealing with packet loss + /// on the network without having to account for it in the cryptographic code itself. pub known_init_conf_response: Option, } impl Peer { + /// Zero initialize a peer + /// + /// This is dirty but allows us to perform easy incremental construction of [Self]. + /// + /// ``` + /// use rosenpass::protocol::{Peer, SymKey, SPk}; + /// rosenpass_secret_memory::secret_policy_try_use_memfd_secrets(); + /// let p = Peer::zero(); + /// assert_eq!(p.psk.secret(), SymKey::zero().secret()); + /// assert_eq!(p.spkt, SPk::zero()); + /// // etc. + /// ``` pub fn zero() -> Self { Self { psk: SymKey::zero(), @@ -193,23 +480,48 @@ impl Peer { } } +/// This represents the core state of an ongoing handshake. +/// +/// The responder is stateless, storing its state in a [Biscuit] inside [RespHello] +/// and [InitConf] respective. The initiator wraps this in [InitiatorHandshake]. +/// +/// # Examples +/// +/// The best way to understand how the handshake state is used is to study how they are used +/// in the protocol. Read the source code of +/// +/// - [CryptoServer::handle_initiation] +/// - [CryptoServer::handle_init_hello] +/// - [CryptoServer::handle_resp_hello] +/// - [CryptoServer::handle_init_conf] +/// - [CryptoServer::handle_resp_conf] #[derive(Debug, Clone)] pub struct HandshakeState { /// Session ID of Initiator pub sidi: SessionId, /// Session ID of Responder pub sidr: SessionId, - /// Chaining Key + /// Chaining Key; i.e. the core cryptographic state pub ck: SecretHashDomainNamespace, // TODO: We should probably add an abstr } +/// Indicates which role a party takes (or took) during the handshake #[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Copy, Clone)] pub enum HandshakeRole { + /// The party was the initiator Initiator, + /// The party was the responder Responder, } impl HandshakeRole { + /// Check if the value of this enum is [HandshakeRole::Initiator] + /// + /// ``` + /// use rosenpass::protocol::HandshakeRole; + /// assert!(HandshakeRole::Initiator.is_initiator()); + /// assert!(!HandshakeRole::Responder.is_initiator()); + /// ``` pub fn is_initiator(&self) -> bool { match *self { HandshakeRole::Initiator => true, @@ -218,39 +530,79 @@ impl HandshakeRole { } } +/// Used in [InitiatorHandshake] to keep track of which packet type +/// is expected next. #[derive(Copy, Clone, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum HandshakeStateMachine { + /// Expecting [RespHello] #[default] RespHello, + /// Expecting responder confirmation (i.e. [EmptyData]) RespConf, } +/// Stores all the information an initiator requires to +/// perform its handshake. +/// +/// The protocol is deliberately designed to minimize responder state +/// even at the expense of increased initiator state, because the +/// responder is stateless and stores its state in a [Biscuit] between +/// inside [RespHello] and [InitConf] respectively. #[derive(Debug)] pub struct InitiatorHandshake { + /// The time the handshake was created at pub created_at: Timing, + /// The package type expected next from the responder pub next: HandshakeStateMachine, + /// The core cryptographic data from the handshake pub core: HandshakeState, - /// Ephemeral Secret Key of Initiator + /// Ephemeral Secret Key Initiator; secret key of the ephemeral keypair pub eski: ESk, - /// Ephemeral Public Key of Initiator + /// Ephemeral Public Key Initiator; public key of the ephemeral keypair pub epki: EPk, - // Retransmission - // TODO: Ensure that this is correct by typing + /// Unused; TODO: Remove pub tx_at: Timing, + /// Retransmit the package in [Self::tx_buf] at this point in time pub tx_retry_at: Timing, + /// Number of times this message has been retransmitted pub tx_count: usize, + /// Size of the message inside [Self::tx_buf] pub tx_len: usize, + /// The message that should be retransmitted pub tx_buf: MsgBuf, - // Cookie storage for retransmission, expires PEER_COOKIE_VALUE_EPOCH seconds after creation + /// Cookie value used as part of the cookie retransmission mechanism. + /// + /// See the [whitepaper](https://rosenpass.eu/whitepaper.pdf) for details about the cookie + /// mechanism. + /// + /// TODO: cookie_value should be an Option<_> + /// + /// This value seems to default-initialized with a random value according to + /// [Self::zero_with_timestamp], which does not really make sense since this + /// is not a value that the responder sets. We also seem to use the cookie + /// value unconditionally (see [Envelope::seal]). This is not harmful as the + /// responder ignores the cookie field by default, but it is quite odd. + /// We should check the WireGuard whitepaper about which default value WireGuard + /// uses for the cookie values and potentially leave the cookie field empty by + /// default. pub cookie_value: CookieStore, } +/// Represents a known response to some network message identified by +/// the hash in [Self::request_mac]. +/// +/// Used as [KnownInitConfResponse] for now cache [EmptyData] (responder confirmation) +/// responses to [InitConf] pub struct KnownResponse { - received_at: Timing, - request_mac: KnownResponseHash, - response: Envelope, + /// When the response was initially computed + pub received_at: Timing, + /// Hash of the message that triggered the response; created using + /// the key in [CryptoServer::known_response_hasher] + pub request_mac: KnownResponseHash, + /// The cached response + pub response: Envelope, } impl Debug for KnownResponse { @@ -263,22 +615,78 @@ impl Debug for KnownResponse { } } +#[test] +fn known_response_format() { + use zerocopy::FromZeroes; + + let v = KnownResponse::<[u8; 32]> { + received_at: 42.0, + request_mac: Public::zero(), + response: Envelope::new_zeroed(), + }; + let s = format!("{v:?}"); + assert!(s.contains("response")); // Smoke test only, its a formatter +} + +/// Known [EmptyData] response to an [InitConf] message +/// +/// See [Peer::known_init_conf_response] pub type KnownInitConfResponse = KnownResponse; +/// The type used to represent the hash of a known response +/// in the context of [KnownResponse]/[IndexKey::KnownInitConfResponse] pub type KnownResponseHash = Public<16>; +/// Object that produces [KnownResponseHash]. +/// +/// Merely a key plus some utility functions. +/// +/// See [IndexKey::KnownInitConfResponse] and [KnownResponse::request_mac] +/// +/// # Examples +/// +/// ``` +/// use zerocopy::FromZeroes; +/// use rosenpass::protocol::KnownResponseHasher; +/// use rosenpass::msgs::{Envelope, InitConf}; +/// +/// rosenpass_secret_memory::secret_policy_try_use_memfd_secrets(); +/// +/// let h = KnownResponseHasher::new(); +/// +/// let v1 = Envelope::::new_zeroed(); +/// +/// let mut v2 = Envelope::::new_zeroed(); +/// assert_eq!(h.hash(&v1), h.hash(&v2)); +/// +/// v2.msg_type = 1; +/// assert_ne!(h.hash(&v1), h.hash(&v2)); +/// ``` #[derive(Debug)] pub struct KnownResponseHasher { + /// The key used for hashing pub key: SymKey, } impl KnownResponseHasher { - fn new() -> Self { + /// Construct a new hasher with a random key + /// + /// # Examples + /// + /// See [Self] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { Self { key: SymKey::random(), } } + /// Hash a message + /// + /// # Examples + /// + /// See [Self] + /// /// # Panic & Safety /// /// Panics in case of a problem with this underlying hash function @@ -291,14 +699,36 @@ impl KnownResponseHasher { } } +/// An established session +/// +/// Rosenpass is a key exchange and not transport protocol +/// though initially we still though Rosenpass might be expanded +/// into a transport protocol at some point. These plans are not +/// entirely abandoned, but if we do decide that Rosenpass should +/// support transport encryption then we will add this as a protocol +/// extension. For this reason, Rosenpass currently essentially features +/// stubs for transport data handling (and therefor session handling) +/// but they are not really put to good use; the session implementation +/// is overcomplicated for what we really use. +/// +/// Session encryption is used exclusively to transmit [EmptyData] which +/// tells the initiator to abort retransmission of [InitConf]. +/// +/// The session is also used to keep track of when a key renegotiation +/// needs to happen; see [PeerPtr::poll]. #[derive(Debug)] pub struct Session { - // Metadata + /// When the session was created pub created_at: Timing, + /// Session ID Mine; Our session ID pub sidm: SessionId, + /// Session ID Theirs; Peer's session ID pub sidt: SessionId, + /// Whether we where the initiator or responder during the handshake + /// (affects when we begin another initiation; by default, the initiator + /// waits a bit longer, allowing role switching) pub handshake_role: HandshakeRole, - // Crypto + /// Cryptographic key produced by the handshake pub ck: SecretHashDomainNamespace, /// Key for Transmission ("transmission key mine") pub txkm: SymKey, @@ -310,107 +740,271 @@ pub struct Session { pub txnt: u64, } -/// Lifecycle of a Secret +/// Lifecycle of a value /// -/// The order implies the readiness for usage of a secret, the highest/biggest +/// For secret keys whose life cycle is managed using this struct, +/// we impose very particular semantics: The order implies the readiness for usage of a secret, the highest/biggest /// variant ([Lifecycle::Young]) is the most preferable one in a class of /// equal-role secrets. #[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] -enum Lifecycle { - /// Not even generated +pub enum Lifecycle { + /// Empty value Void = 0, - /// Secret must be zeroized, disposal advised + /// The value should be deleted. + /// + /// If a secret, it must be zeroized and disposed. Dead, - /// Soon to be dead: the secret might be used for receiving - /// data, but must not be used for future sending + /// Soon to be dead. Do not use any more. + /// + /// If a secret, it might be used for decoding (decrypting) + /// data, but must not be used for encryption of cryptographic values. Retired, - /// The secret might be used unconditionally + /// The value is fresh and in active use. + /// + /// If a secret, it might be used unconditionally; in particular, the secret + /// can be used for the encoding (encryption) of cryptographic values. Young, } -/// Implemented for information (secret and public) that has an expire date -trait Mortal { +/// Life cycle management for values +/// +/// # Examples +/// +/// See [MortalExt] +pub trait Mortal { /// Time of creation, when [Lifecycle::Void] -> [Lifecycle::Young] + /// + /// # Examples + /// + /// See [MortalExt] fn created_at(&self, srv: &CryptoServer) -> Option; /// The time where [Lifecycle::Young] -> [Lifecycle::Retired] + /// + /// # Examples + /// + /// See [MortalExt] fn retire_at(&self, srv: &CryptoServer) -> Option; /// The time where [Lifecycle::Retired] -> [Lifecycle::Dead] + /// + /// # Examples + /// + /// See [MortalExt] fn die_at(&self, srv: &CryptoServer) -> Option; } // BUSINESS LOGIC DATA STRUCTURES //////////////// -/// Valid index to [CryptoServer::peers] +/// Valid index to [CryptoServer::peers], focusing on the peer itself. +/// +/// Provides appropriate utility functions, especially those that +/// somehow focus on the peer but require access to the [CryptoServer]. +/// +/// # Examples +/// +/// ``` +/// use std::ops::DerefMut; +/// use rosenpass_ciphers::kem::StaticKem; +/// use rosenpass::protocol::{SSk, SPk, testutils::ServerForTesting}; +/// +/// rosenpass_secret_memory::secret_policy_try_use_memfd_secrets(); +/// +/// let (peer, (_, spkt), mut srv) = ServerForTesting::new()?.tuple(); +/// +/// // Immutable access +/// assert_eq!(peer.get(&srv).spkt, spkt); +/// +/// // Mutable access +/// peer.get_mut(&mut srv).initiation_requested = true; +/// assert!(peer.get(&srv).initiation_requested); +/// +/// // Produce a session pointer for the particular peer +/// assert!(peer.session().get(&srv).is_none()); +/// assert!(peer.session().get_mut(&mut srv).is_none()); +/// +/// // Produce a handshake pointer for the particular peer +/// assert!(peer.hs().get(&srv).is_none()); +/// assert!(peer.hs().get_mut(&mut srv).is_none()); +/// +/// // Produce a cookie value pointer for the particular peer +/// assert!(peer.cv().get(&srv).is_none()); +/// +/// // The mutable getter for cookie values is a bit special; +/// // instead of getting access to the container value, you are +/// // given direct acccess to the underlying buffer. If the value +/// // is present; the function also updates the cookie value's +/// // `created_at` value. +/// // Though in this example, update_mut simply returns none. +/// assert!(peer.cv().update_mut(&mut srv).is_none()); +/// +/// // Produce a known init conf response pointer for the particular peer +/// assert!(peer.known_init_conf_response().get(&srv).is_none()); +/// assert!(peer.known_init_conf_response().get_mut(&mut srv).is_none()); +/// +/// // All the sub-pointers generally implement functions to get back to the +/// // peer that contains them +/// assert_eq!(peer.session().peer(), peer); +/// assert_eq!(peer.hs().peer(), peer); +/// //assert_eq!(peer.cv().peer(), peer); // Does not provide a link back right now +/// assert_eq!(peer.known_init_conf_response().peer(), peer); +/// +/// Ok::<(), anyhow::Error>(()) +/// ``` #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct PeerPtr(pub usize); -/// Valid index to [CryptoServer::peers] +/// Valid index to [CryptoServer::peers], focusing on [Peer::handshake]. +/// +/// Provides appropriate utility functions, especially those that +/// somehow focus on the handshake but require access to the [CryptoServer]. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct IniHsPtr(pub usize); -/// Valid index to [CryptoServer::peers] +/// Valid index to [CryptoServer::peers], focusing on [Peer::session]. +/// +/// Provides appropriate utility functions, especially those that +/// somehow focus on the handshake but require access to the [CryptoServer]. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct SessionPtr(pub usize); -/// Valid index to [CryptoServer::peers] cookie value -pub struct PeerCookieValuePtr(usize); // TODO: Change +/// Valid index to [CryptoServer::peers], focusing on [InitiatorHandshake::cookie_value] +/// inside [Peer::handshake]. +/// +/// Provides appropriate utility functions, especially those that +/// somehow focus on the cookie value but require access to the [CryptoServer]. +pub struct PeerCookieValuePtr(usize); -/// Valid index to [CryptoServer::peers] known init conf response +/// Valid index to [CryptoServer::peers], focusing on [Peer::known_init_conf_response]. +/// +/// Provides appropriate utility functions, especially those that +/// somehow focus on the known response value but require access to the [CryptoServer]. pub struct KnownInitConfResponsePtr(PeerNo); /// Valid index to [CryptoServer::biscuit_keys] +/// +/// Provides appropriate utility functions, especially those that +/// somehow focus on the biscuit key but require access to the [CryptoServer]. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct BiscuitKeyPtr(pub usize); -/// Valid index to [CryptoServer::cookie_secrets] cookie value +/// Valid index to [CryptoServer::cookie_secrets] +/// +/// Provides appropriate utility functions, especially those that +/// somehow focus on the cookie secret but require access to the [CryptoServer]. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct ServerCookieSecretPtr(pub usize); impl PeerPtr { + /// Access a peer + /// /// # Panic & Safety /// /// The function panics if the peer referenced by this PeerPtr does not exist. + /// + /// # Examples + /// + /// See [Self] pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a Peer { &srv.peers[self.0] } + /// Mutable access to a peer. + /// /// # Panic & Safety /// /// The function panics if the peer referenced by this PeerPtr does not exist. + /// + /// # Examples + /// + /// See [Self] pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut Peer { &mut srv.peers[self.0] } + /// Produce pointer to associated session + /// + /// # Examples + /// + /// See [Self] pub fn session(&self) -> SessionPtr { SessionPtr(self.0) } + /// Produce pointer to associated handshake + /// + /// # Examples + /// + /// See [Self] pub fn hs(&self) -> IniHsPtr { IniHsPtr(self.0) } + /// Produce pointer to associated cookie value + /// + /// # Examples + /// + /// See [Self] pub fn cv(&self) -> PeerCookieValuePtr { PeerCookieValuePtr(self.0) } + /// Produce pointer to associated known init conf response + /// + /// # Examples + /// + /// See [Self] pub fn known_init_conf_response(&self) -> KnownInitConfResponsePtr { KnownInitConfResponsePtr(self.0) } } impl IniHsPtr { + /// Access the handshake value + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced by this does not exist. + /// + /// # Examples + /// + /// See [PeerPtr] pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a Option { &srv.peers[self.0].handshake } + /// Mutable access to the handshake value + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced by this does not exist. + /// + /// # Examples + /// + /// See [PeerPtr] pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut Option { &mut srv.peers[self.0].handshake } + /// Access the associated peer + /// + /// # Examples + /// + /// See [PeerPtr] pub fn peer(&self) -> PeerPtr { PeerPtr(self.0) } + /// Insert a new handshake into the peer + /// + /// Note that this also registers the session with the server ([CryptoServer::register_session]), + /// so the handshake must be properly initialized. The [HandshakeState::sidi] value (inside + /// [InitiatorHandshake::core]) is used + /// to register the handshake in the session index via [CryptoServer::register_session] and the + /// peer's [Peer::initiation_requested] flag is set to false since any such request was just + /// acted upon by inserting this handshake. + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced by this does not exist. pub fn insert<'a>( &self, srv: &'a mut CryptoServer, @@ -422,6 +1016,13 @@ impl IniHsPtr { Ok(self.peer().get_mut(srv).handshake.insert(hs)) } + /// Take (remove and return) the current InititiatorHandshake from the peer. + /// + /// The handshake is also unregistered from the handshake index by using [CryptoServer::unregister_session_if_vacant]. + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced by this does not exist. pub fn take(&self, srv: &mut CryptoServer) -> Option { let r = self.peer().get_mut(srv).handshake.take(); if let Some(ref stale) = r { @@ -432,24 +1033,62 @@ impl IniHsPtr { } impl SessionPtr { + /// Access the session value + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced by this does not exist. + /// + /// # Examples + /// + /// See [PeerPtr] pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a Option { &srv.peers[self.0].session } + /// Mutable access to the session value + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced by this does not exist. + /// + /// # Examples + /// + /// See [PeerPtr] pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut Option { &mut srv.peers[self.0].session } + /// Access the associated peer + /// + /// # Examples + /// + /// See [PeerPtr] pub fn peer(&self) -> PeerPtr { PeerPtr(self.0) } + /// Insert a new session into the peer + /// + /// Note that this also registers the session with the server ([CryptoServer::register_session]), + /// using [Session::sidm], so the session must be properly initialized. + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced by this does not exist. pub fn insert<'a>(&self, srv: &'a mut CryptoServer, ses: Session) -> Result<&'a mut Session> { self.take(srv); srv.register_session(ses.sidm, self.peer())?; Ok(self.peer().get_mut(srv).session.insert(ses)) } + /// Take (remove and return) the current Session from the peer. + /// + /// The session is also unregistered from the session index by using [CryptoServer::unregister_session_if_vacant]. + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced by this does not exist. pub fn take(&self, srv: &mut CryptoServer) -> Option { let r = self.peer().get_mut(srv).session.take(); if let Some(ref stale) = r { @@ -460,26 +1099,39 @@ impl SessionPtr { } impl BiscuitKeyPtr { + /// Access the referenced biscuit key pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a BiscuitKey { &srv.biscuit_keys[self.0] } + /// Mutable access to the referenced biscuit key pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut BiscuitKey { &mut srv.biscuit_keys[self.0] } } impl ServerCookieSecretPtr { + /// Access the referenced cookie secret pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a CookieSecret { &srv.cookie_secrets[self.0] } + /// Mutable access to the referenced cookie secret pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut CookieSecret { &mut srv.cookie_secrets[self.0] } } impl PeerCookieValuePtr { + /// Access the peer cookie value + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced by this does not exist. + /// + /// # Examples + /// + /// See [PeerPtr] pub fn get<'a>(&self, srv: &'a CryptoServer) -> Option<&'a CookieStore> { srv.peers[self.0] .handshake @@ -487,6 +1139,17 @@ impl PeerCookieValuePtr { .map(|v| &v.cookie_value) } + /// Direct mutable access the peer cookie value + /// + /// Updates the [CookieStore::created_at] value of the cookie value. + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced by this does not exist. + /// + /// # Examples + /// + /// See [PeerPtr] pub fn update_mut<'a>(&self, srv: &'a mut CryptoServer) -> Option<&'a mut [u8]> { let timebase = srv.timebase.clone(); @@ -505,10 +1168,17 @@ impl PeerCookieValuePtr { } impl KnownInitConfResponsePtr { + /// Access the associated peer + /// + /// # Examples + /// + /// See [PeerPtr] pub fn peer(&self) -> PeerPtr { PeerPtr(self.0) } + /// Immutable access to the value + /// /// # Panic & Safety /// /// The function panics if the peer referenced by this KnownInitConfResponsePtr does not exist. @@ -516,6 +1186,8 @@ impl KnownInitConfResponsePtr { self.peer().get(srv).known_init_conf_response.as_ref() } + /// Mutable access to the value + /// /// # Panic & Safety /// /// The function panics if the peer referenced by this KnownInitConfResponsePtr does not exist. @@ -523,6 +1195,10 @@ impl KnownInitConfResponsePtr { self.peer().get_mut(srv).known_init_conf_response.as_mut() } + /// Remove the cached response for this peer + /// + /// Takes care of updating the indices in [CryptoServer::index] appropriately. + /// /// # Panic & Safety /// /// The function panics if @@ -538,6 +1214,17 @@ impl KnownInitConfResponsePtr { Some(val) } + /// Insert a cached response for this peer + /// + /// Takes care of updating the indices in [CryptoServer::index] appropriately. + /// + /// # Panic & Safety + /// + /// The function panics if + /// + /// - the peer referenced by this KnownInitConfResponsePtr does not exist + /// - the peer contains a KnownInitConfResponse (i.e. if [Peer::known_init_conf_response] is Some(...)), but the index to this KnownInitConfResponsePtr is missing (i.e. there is no appropriate index + /// value in [CryptoServer::index]) pub fn insert(&self, srv: &mut CryptoServer, known_response: KnownInitConfResponse) { self.remove(srv).discard_result(); @@ -569,6 +1256,14 @@ impl KnownInitConfResponsePtr { } } + /// Look up the right [Self] for a given request message + /// + /// Really just calls [Self::index_key_for_msg] to produce the right + /// index key followed by a lookup in [CryptoServer::index]. + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced in the index does not exist. pub fn lookup_for_request_msg( srv: &CryptoServer, req: &Envelope, @@ -578,6 +1273,14 @@ impl KnownInitConfResponsePtr { Some(Self(peer_no)) } + /// Look up a cached response for a given request message + /// + /// Really just calls [Self::lookup_for_request_msg] followed [Self::get] + /// and extracting [KnownResponse::response]. + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced in the index does not exist. pub fn lookup_response_for_request_msg<'a>( srv: &'a CryptoServer, req: &Envelope, @@ -587,6 +1290,16 @@ impl KnownInitConfResponsePtr { .map(|v| &v.response) } + /// Insert a cached response `res` for message `req` into [Peer::known_init_conf_response] + /// for the peer referenced to by [PeerPtr] `peer`. + /// + /// 1. Creates an index key for `req` using [Self::index_key_hash_for_msg] + /// 2. Constructs an appropriate [KnownInitConfResponse] + /// 3. Uses [Self::insert] to insert the message and update the indices + /// + /// # Panic & Safety + /// + /// The function panics if the peer referenced to by `peer` does not exist. pub fn insert_for_request_msg( srv: &mut CryptoServer, peer: PeerPtr, @@ -604,6 +1317,9 @@ impl KnownInitConfResponsePtr { ); } + /// Calculate an appropriate index key hash for `req` + /// + /// Merely forwards [KnownResponseHasher::hash] with hasher [CryptoServer::known_response_hasher] from `srv`. pub fn index_key_hash_for_msg( srv: &CryptoServer, req: &Envelope, @@ -611,6 +1327,9 @@ impl KnownInitConfResponsePtr { srv.known_response_hasher.hash(req) } + /// Calculate an appropriate index key for `req` + /// + /// Merely forwards [Self::index_key_for_msg] and wraps the result in [IndexKey::KnownInitConfResponse] pub fn index_key_for_msg(srv: &CryptoServer, req: &Envelope) -> IndexKey { Self::index_key_hash_for_msg(srv, req).apply(IndexKey::KnownInitConfResponse) } @@ -619,8 +1338,26 @@ impl KnownInitConfResponsePtr { // DATABASE ////////////////////////////////////// impl CryptoServer { - /// Initiate a new [CryptoServer] based on a secret key (`sk`) and a public key - /// (`pk`) + /// Constructing a CryptoServer + /// + /// # Examples + /// + /// ``` + /// use std::ops::DerefMut; + /// use rosenpass::protocol::{SSk, SPk, CryptoServer}; + /// use rosenpass_ciphers::kem::StaticKem; + /// use rosenpass_cipher_traits::Kem; + /// + /// rosenpass_secret_memory::secret_policy_try_use_memfd_secrets(); + /// + /// let (mut sskm, mut spkm) = (SSk::zero(), SPk::zero()); + /// StaticKem::keygen(sskm.secret_mut(), spkm.deref_mut())?; + /// + /// let srv = CryptoServer::new(sskm, spkm.clone()); + /// assert_eq!(srv.spkm, spkm); + /// + /// Ok::<(), anyhow::Error>(()) + /// ``` pub fn new(sk: SSk, pk: SPk) -> CryptoServer { let tb = Timebase::default(); CryptoServer { @@ -639,15 +1376,17 @@ impl CryptoServer { } } - /// Iterate over the many (2) biscuit keys + /// Iterate over the available biscuit keys by their pointers [BiscuitKeyPtr] pub fn biscuit_key_ptrs(&self) -> impl Iterator { (0..self.biscuit_keys.len()).map(BiscuitKeyPtr) } + /// Iterate over the available cookie secrets by their pointers [ServerCookieSecretPtr] pub fn cookie_secret_ptrs(&self) -> impl Iterator { (0..self.cookie_secrets.len()).map(ServerCookieSecretPtr) } + /// Calculate the peer ID of this CryptoServer #[rustfmt::skip] pub fn pidm(&self) -> Result { Ok(Public::new( @@ -665,6 +1404,30 @@ impl CryptoServer { } /// Add a peer with an optional pre shared key (`psk`) and its public key (`pk`) + /// + /// ``` + /// use std::ops::DerefMut; + /// use rosenpass::protocol::{SSk, SPk, SymKey, CryptoServer}; + /// use rosenpass_ciphers::kem::StaticKem; + /// use rosenpass_cipher_traits::Kem; + /// + /// rosenpass_secret_memory::secret_policy_try_use_memfd_secrets(); + /// + /// let (mut sskm, mut spkm) = (SSk::zero(), SPk::zero()); + /// StaticKem::keygen(sskm.secret_mut(), spkm.deref_mut())?; + /// let mut srv = CryptoServer::new(sskm, spkm); + /// + /// let (mut sskt, mut spkt) = (SSk::zero(), SPk::zero()); + /// StaticKem::keygen(sskt.secret_mut(), spkt.deref_mut())?; + /// + /// let psk = SymKey::random(); + /// + /// let peer = srv.add_peer(Some(psk), spkt.clone())?; + /// + /// assert_eq!(peer.get(&srv).spkt, spkt); + /// + /// Ok::<(), anyhow::Error>(()) + /// ``` pub fn add_peer(&mut self, psk: Option, pk: SPk) -> Result { let peer = Peer { psk: psk.unwrap_or_else(SymKey::zero), @@ -688,8 +1451,18 @@ impl CryptoServer { Ok(PeerPtr(peerno)) } - /// Register a new session (during a successful handshake, persisting longer - /// than the handshake). Might return an error on session id collision + /// Register a new session + /// + /// Used in [SessionPtr::insert] and [IniHsPtr::insert]. + /// + /// The function will raise an error if the given session ID is already used for a different + /// peer; if the session ID is already registered for the given peer, then the index is left + /// unchanged. + /// + /// The session id `id` must be chosen by the local peer; it is a "sidm" (Session ID Mine). + /// + /// To rgister a session, you should generally use [SessionPtr::insert] or [IniHsPtr::insert] + /// instead of this, more lower level function. pub fn register_session(&mut self, id: SessionId, peer: PeerPtr) -> Result<()> { match self.index.entry(IndexKey::Sid(id)) { Occupied(p) if PeerPtr(*p.get()) == peer => {} // Already registered @@ -701,12 +1474,31 @@ impl CryptoServer { Ok(()) } + /// Unregister a session previously registered using [Self::register_session] + /// + /// If the session is not registered, the index is left unchanged. + /// + /// This is generally used only in the context of [Self::unregister_session_if_vacant]. + /// + /// To unregister a session, you should generally use [SessionPtr::take] or [IniHsPtr::take] + /// instead of this, more lower level function. pub fn unregister_session(&mut self, id: SessionId) { self.index.remove(&IndexKey::Sid(id)); } - /// Remove the given session if it is neither an active session nor in - /// handshake phase + /// Unregister a session previously registered using [Self::register_session], + /// if and only if the associated sessions are no longer present in their peer. + /// + /// This means that the peer's [Peer::session] and [Peer::handshake] fields must + /// be cleared if they refer to this session before calling this function. + /// + /// In particular, this function checks: + /// + /// - For handshakes: [Peer::handshake] -> [InitiatorHandshake::core] -> [HandshakeState::sidi] + /// - For sessions: [Peer::session] -> [Session::sidm] + /// + /// To unregister a session, you should generally use [SessionPtr::take] or [IniHsPtr::take] + /// instead of this, more lower level function. pub fn unregister_session_if_vacant(&mut self, id: SessionId, peer: PeerPtr) { match (peer.session().get(self), peer.hs().get(self)) { (Some(ses), _) if ses.sidm == id => {} /* nop */ @@ -715,11 +1507,17 @@ impl CryptoServer { }; } + /// Find a peer given its peer ID as produced by [Peer::pidt] + /// + /// This function is used in cryptographic message processing + /// [CryptoServer::handle_init_hello], and [HandshakeState::load_biscuit] pub fn find_peer(&self, id: PeerId) -> Option { self.index.get(&IndexKey::Peer(id)).map(|no| PeerPtr(*no)) } - // lookup_session in whitepaper + /// Look up a handshake given its session id [HandshakeState::sidi] + /// + /// This is called `lookup_session` in [whitepaper](https://rosenpass.eu/whitepaper.pdf). pub fn lookup_handshake(&self, id: SessionId) -> Option { self.index .get(&IndexKey::Sid(id)) // lookup the session in the index @@ -733,7 +1531,9 @@ impl CryptoServer { }) } - // also lookup_session in the whitepaper + /// Look up a session given its session id [Session::sidm] + /// + /// This is called `lookup_session` in [whitepaper](https://rosenpass.eu/whitepaper.pdf). pub fn lookup_session(&self, id: SessionId) -> Option { self.index .get(&IndexKey::Sid(id)) @@ -746,6 +1546,19 @@ impl CryptoServer { }) } + /// Retrieve the active biscuit key, cycling biscuit keys if necessary. + /// + /// Two biscuit keys are maintained inside [Self::biscuit_keys]; they are + /// considered fresh ([Lifecycle::Young]) for one [BISCUIT_EPOCH] after creation + /// and they are considered stale ([Lifecycle::Retired]) for another [BISCUIT_EPOCH]. + /// + /// While young, they are used for encryption of biscuits and while retired they are + /// just use for decryption. Keeping stale biscuits keys around makes sure that + /// no handshakes are dropped when biscuit keys are changed. + /// + /// This function will return the newest fresh biscuit; if there are no fresh biscuits, + /// the oldest biscuit will be replaced with a fresh one using [CookieStore::randomize]. + /// /// Swap the biscuit keys, also advancing both biscuit key's mortality pub fn active_biscuit_key(&mut self) -> BiscuitKeyPtr { let (a, b) = (BiscuitKeyPtr(0), BiscuitKeyPtr(1)); @@ -766,7 +1579,13 @@ impl CryptoServer { r } - // Return cookie secrets in order of youthfulness (youngest first) + /// Return all fresh ([Lifecycle::Young]) or stale ([Lifecycle::Retired]) + /// cookie secrets (from [Self::cookie_secrets]) ordered by age, youngest ones + /// first. + /// + /// If none of the cookie secrets are fresh or stale, the oldest cookie secret will + /// be refreshed using [CookieSecret::randomize]; this function therefor always returns + /// at least one usable, fresh cookie secret. pub fn active_or_retired_cookie_secrets(&mut self) -> [Option; 2] { let (a, b) = (ServerCookieSecretPtr(0), ServerCookieSecretPtr(1)); let (t, u) = (a.get(self).created_at, b.get(self).created_at); @@ -798,6 +1617,13 @@ impl CryptoServer { } impl Peer { + /// Create a new peer from the peers keys. + /// + /// If no peer PSK is set, `psk` should be initialized to [SymKey::zero]. + /// + /// # Examples + /// + /// See example in [Self]. pub fn new(psk: SymKey, pk: SPk) -> Peer { Peer { psk, @@ -810,6 +1636,12 @@ impl Peer { } } + /// Compute the peer ID of the peer, + /// as specified in the the [whitepaper](https://rosenpass.eu/whitepaper.pdf). + /// + /// # Examples + /// + /// See example in [Self]. #[rustfmt::skip] pub fn pidt(&self) -> Result { Ok(Public::new( @@ -820,6 +1652,19 @@ impl Peer { } impl Session { + /// Zero initialization of a session + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{Session, HandshakeRole}; + /// + /// rosenpass_secret_memory::secret_policy_try_use_memfd_secrets(); + /// + /// let s = Session::zero(); + /// assert_eq!(s.created_at, 0.0); + /// assert_eq!(s.handshake_role, HandshakeRole::Initiator); + /// ``` pub fn zero() -> Self { Self { created_at: 0.0, @@ -837,8 +1682,12 @@ impl Session { // COOKIE STORE /////////////////////////////////// impl CookieStore { - // new creates a random value, that might be counterintuitive for a Default - // impl + /// Creates a new cookie store value with a random secret value + /// and a [Self::created_at] value of [BCE]. + /// + /// # Examples + /// + /// See [Self]. #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { @@ -847,16 +1696,44 @@ impl CookieStore { } } + /// Resets [Self] by randomizing [Self::value] and setting + /// [Self::created_at] to [BCE]. + /// + /// # Examples + /// + /// See [Self]. pub fn erase(&mut self) { self.value.randomize(); self.created_at = BCE; } + /// Refresh the cookie store with a random secret. + /// + /// [Self::value] will be a random key and [Self::created_at] + /// will be set to the current time. + /// + /// `tb`, the Timebase is the reference point for time keeping. + /// Usually this should be [CryptoServer::timebase]. + /// + /// # Examples + /// + /// See [Self]. pub fn randomize(&mut self, tb: &Timebase) { self.value.randomize(); self.created_at = tb.now(); } + /// Refresh the cookie store with a random secret. + /// + /// [Self::value] will be set to and [Self::created_at] + /// will be set to the current time. + /// + /// `tb`, the Timebase is the reference point for time keeping. + /// Usually this should be [CryptoServer::timebase]. + /// + /// # Examples + /// + /// See [Self]. pub fn update(&mut self, tb: &Timebase, value: &[u8]) { self.value.secret_mut().copy_from_slice(value); self.created_at = tb.now(); @@ -866,24 +1743,30 @@ impl CookieStore { // LIFECYCLE MANAGEMENT ////////////////////////// impl Mortal for IniHsPtr { + /// At [InitiatorHandshake::created_at] fn created_at(&self, srv: &CryptoServer) -> Option { self.get(srv).as_ref().map(|hs| hs.created_at) } + /// No retirement phase; same as [Self::die_at] fn retire_at(&self, srv: &CryptoServer) -> Option { self.die_at(srv) } + /// [Self::created_at] plus [REJECT_AFTER_TIME] fn die_at(&self, srv: &CryptoServer) -> Option { self.created_at(srv).map(|t| t + REJECT_AFTER_TIME) } } impl Mortal for SessionPtr { + /// At [Session::created_at] fn created_at(&self, srv: &CryptoServer) -> Option { self.get(srv).as_ref().map(|p| p.created_at) } + /// [Self::created_at] plus [REKEY_AFTER_TIME_INITIATOR] or [REKEY_AFTER_TIME_RESPONDER] + /// as appropriate. fn retire_at(&self, srv: &CryptoServer) -> Option { // If we were the initiator, wait an extra ten seconds to avoid // both parties starting the handshake at the same time. In most situations @@ -901,12 +1784,14 @@ impl Mortal for SessionPtr { }) } + /// [Self::created_at] plus [REJECT_AFTER_TIME] fn die_at(&self, srv: &CryptoServer) -> Option { self.created_at(srv).map(|t| t + REJECT_AFTER_TIME) } } impl Mortal for BiscuitKeyPtr { + /// At [BiscuitKey::created_at] fn created_at(&self, srv: &CryptoServer) -> Option { let t = self.get(srv).created_at; if t < 0.0 { @@ -916,16 +1801,19 @@ impl Mortal for BiscuitKeyPtr { } } + /// At [Self::created_at] plus [BISCUIT_EPOCH] fn retire_at(&self, srv: &CryptoServer) -> Option { self.created_at(srv).map(|t| t + BISCUIT_EPOCH) } + /// At [Self::retire_at] plus [BISCUIT_EPOCH] fn die_at(&self, srv: &CryptoServer) -> Option { self.retire_at(srv).map(|t| t + BISCUIT_EPOCH) } } impl Mortal for ServerCookieSecretPtr { + /// At [CookieSecret::created_at] fn created_at(&self, srv: &CryptoServer) -> Option { let t = self.get(srv).created_at; if t < 0.0 { @@ -934,15 +1822,20 @@ impl Mortal for ServerCookieSecretPtr { Some(t) } } + + /// At [Self::created_at] plus [COOKIE_SECRET_EPOCH] fn retire_at(&self, srv: &CryptoServer) -> Option { self.created_at(srv).map(|t| t + COOKIE_SECRET_EPOCH) } + + /// At [Self::retire_at] plus [COOKIE_SECRET_EPOCH] fn die_at(&self, srv: &CryptoServer) -> Option { self.retire_at(srv).map(|t| t + COOKIE_SECRET_EPOCH) } } impl Mortal for PeerCookieValuePtr { + /// At [CookieStore::created_at] fn created_at(&self, srv: &CryptoServer) -> Option { if let Some(cs) = self.get(srv) { if cs.created_at < 0.0 { @@ -954,16 +1847,19 @@ impl Mortal for PeerCookieValuePtr { } } + /// No retirement phase, so this is the same as [Self::die_at] fn retire_at(&self, srv: &CryptoServer) -> Option { self.die_at(srv) } + /// [Self::created_at] plus [PEER_COOKIE_VALUE_EPOCH] fn die_at(&self, srv: &CryptoServer) -> Option { self.created_at(srv).map(|t| t + PEER_COOKIE_VALUE_EPOCH) } } impl Mortal for KnownInitConfResponsePtr { + /// At [KnownInitConfResponse::received_at] fn created_at(&self, srv: &CryptoServer) -> Option { let t = self.get(srv)?.received_at; if t < 0.0 { @@ -973,10 +1869,12 @@ impl Mortal for KnownInitConfResponsePtr { } } + /// No retirement phase, so this is the same as [Self::die_at] fn retire_at(&self, srv: &CryptoServer) -> Option { self.die_at(srv) } + /// [Self::created_at] plus [REKEY_AFTER_TIME_RESPONDER] fn die_at(&self, srv: &CryptoServer) -> Option { self.created_at(srv).map(|t| t + REKEY_AFTER_TIME_RESPONDER) } @@ -984,9 +1882,98 @@ impl Mortal for KnownInitConfResponsePtr { /// Trait extension to the [Mortal] Trait, that enables nicer access to timing /// information -trait MortalExt: Mortal { +/// +/// # Examples +/// +/// ``` +/// use rosenpass::protocol::{Timing, Mortal, MortalExt, Lifecycle, CryptoServer}; +/// use rosenpass::protocol::testutils::{ServerForTesting, time_travel_forward}; +/// +/// rosenpass_secret_memory::secret_policy_try_use_memfd_secrets(); +/// +/// const M : Timing = 60.0; +/// const H : Timing = 60.0 * M; +/// const D : Timing = 24.0 * H; +/// const Y : Timing = 356.0 * D; +/// +/// let mut ts = ServerForTesting::new()?; +/// +/// fn eq_up_to_minute(a: Timing, b: Timing) -> bool { +/// (a - b) < M +/// } +/// +/// struct Hooman { +/// born: Timing, +/// works_for: Timing, +/// retires_for: Timing, +/// }; +/// +/// impl Mortal for Hooman { +/// fn created_at(&self, srv: &CryptoServer) -> Option { +/// Some(self.born) +/// } +/// +/// fn retire_at(&self, srv: &CryptoServer) -> Option { +/// Some(self.created_at(&srv)? + self.works_for) +/// } +/// +/// fn die_at(&self, srv: &CryptoServer) -> Option { +/// Some(self.retire_at(&srv)? + self.retires_for) +/// } +/// } +/// +/// let twist = Hooman { +/// born: ts.srv.timebase.now(), +/// works_for: 79.0 * Y, // Post-capitalist feudal state +/// retires_for: 4.0 * M, +/// }; +/// +/// assert!(eq_up_to_minute(twist.life_left(&ts.srv).unwrap(), twist.works_for + twist.retires_for)); +/// assert!(eq_up_to_minute(twist.youth_left(&ts.srv).unwrap(), twist.works_for)); +/// assert_eq!(twist.lifecycle(&ts.srv), Lifecycle::Young); +/// +/// // Travel forward by 4Y +/// time_travel_forward(&mut ts.srv, 4.0*Y); +/// assert!(eq_up_to_minute(twist.life_left(&ts.srv).unwrap(), twist.works_for + twist.retires_for - 4.0*Y)); +/// assert!(eq_up_to_minute(twist.youth_left(&ts.srv).unwrap(), twist.works_for - 4.0*Y)); +/// assert_eq!(twist.lifecycle(&ts.srv), Lifecycle::Young); +/// +/// // Travel forward past their retirement +/// let dst = twist.youth_left(&ts.srv).unwrap() + 1.0*M; +/// time_travel_forward(&mut ts.srv, dst); +/// assert!(eq_up_to_minute(twist.life_left(&ts.srv).unwrap(), 3.0*M)); +/// assert!(eq_up_to_minute(twist.youth_left(&ts.srv).unwrap(), -1.0*M)); +/// assert_eq!(twist.lifecycle(&ts.srv), Lifecycle::Retired); +/// +/// // Travel forward past their death +/// let dst = twist.life_left(&ts.srv).unwrap() + 1.0*Y; +/// time_travel_forward(&mut ts.srv, dst); +/// assert!(eq_up_to_minute(twist.life_left(&ts.srv).unwrap(), -1.0*Y)); +/// assert!(eq_up_to_minute(twist.youth_left(&ts.srv).unwrap(), -1.0*Y + 4.0*M)); +/// assert_eq!(twist.lifecycle(&ts.srv), Lifecycle::Dead); +/// +/// Ok::<(), anyhow::Error>(()) +/// ``` +pub trait MortalExt: Mortal { + /// Calculate the amount of time left before the object enters + /// lifecycle stage [Lifecycle::Dead]. + /// + /// # Examples + /// + /// See [Self]. fn life_left(&self, srv: &CryptoServer) -> Option; + /// Calculate the amount of time left before the object enters + /// lifecycle stage [Lifecycle::Retired]. + /// + /// # Examples + /// + /// See [Self]. fn youth_left(&self, srv: &CryptoServer) -> Option; + /// Retrieve the current [Lifecycle] stage + /// + /// # Examples + /// + /// See [Self]. fn lifecycle(&self, srv: &CryptoServer) -> Lifecycle; } @@ -1012,12 +1999,37 @@ impl MortalExt for T { // MESSAGE HANDLING ////////////////////////////// impl CryptoServer { - /// Initiate a new handshake, put it to the `tx_buf` __and__ to the - /// retransmission storage - // NOTE retransmission? yes if initiator, no if responder - // TODO remove unnecessary copying between global tx_buf and per-peer buf - // TODO move retransmission storage to io server + /// This is the function that a user of [Self] should use to start a new handshake. + /// + /// The generated message is put into the `tx_buf` parameter and the size of the message + /// is returned by the function. The buffer must be large enough to store a value of + /// [Envelope]<[InitHello]>. Usually, the same buffer would be used for all messages; + /// in this case allocating the message buffer with [MsgBuf] is easiest. + /// + /// If there already is an ongoing handshake in initiator role + /// for the given peer, this function will displace this other handshake, + /// causing any further packages that are part of the other handshake to be + /// rejected by [Self::handle_msg]. + /// + /// This can be called at any time, but most users may wish to call this function + /// after [Self::poll] returns [PollResult::SendInitiation]. Note that ignoring + /// [PollResult::SendInitiation] is explicitly supported. The Rosenpass application + /// does this for instance when there is no known address for the other peer. + /// + /// # Panic & Safety + /// + /// Will panic if the given buffer `tx_buf` is not large enough. + /// + /// # Example + /// + /// See the example on how to use this function without [Self::poll] in [crate::protocol]. + /// + /// See [Self::poll] on how to use this function with poll. pub fn initiate_handshake(&mut self, peer: PeerPtr, tx_buf: &mut [u8]) -> Result { + // NOTE retransmission? yes if initiator, no if responder + // TODO remove unnecessary copying between global tx_buf and per-peer buf + // TODO move retransmission storage to io server + // // Envelope::::default(); // TODO let mut msg = truncating_cast_into::>(tx_buf)?; self.handle_initiation(peer, &mut msg.payload)?; @@ -1028,29 +2040,64 @@ impl CryptoServer { } } +/// The type returned by [CryptoServer::handle_msg] #[derive(Debug)] pub struct HandleMsgResult { + /// If a key was successfully exchanged with another party as a result of + /// this network message, then this field indicates which peer. + /// + /// The key can then be accessed through the session; see [PeerPtr::session]. pub exchanged_with: Option, + /// If processing the message yielded a response, then this field indicates its size. + /// + /// The message data will be in a buffer given to function as a mutable parameter. + /// + /// This message should be sent to the other party on the channel that they used to send + /// the request. Note that, even if there is some known IP address for the peer, this address + /// should generally not be used as this would preclude IP switching. pub resp: Option, } -/// Trait for host identification types +/// Produces identifying information about the sender of a network package. +/// +/// Used in [CryptoServer::handle_msg_under_load] to perform proof-of-address-ownership +/// with the other party. +/// +/// The result is quite deliberately a byte slice, this allows users of [CryptoServer] +/// to provide support for arbitrary types of addresses. pub trait HostIdentification: Display { - // Byte slice representing the host identification encoding + /// Byte slice representing the host identification fn encode(&self) -> &[u8]; } impl CryptoServer { - /// Process a message under load - /// This is one of the main entry point for the protocol. + /// Process a message under load. + /// + /// This is one of the main entry points for the protocol. The function can be used + /// as a substitute for [CryptoServer::handle_msg] when the user of [CryptoServer] + /// has determined that a DOS attack is being performed. + /// /// Keeps track of messages processed, and qualifies messages using /// cookie based DoS mitigation. + /// /// If recieving a InitHello message, it dispatches message for further processing /// to `process_msg` handler if cookie is valid otherwise sends a cookie reply /// message for sender to process and verify for messages part of the handshake phase + /// /// Directly processes InitConf messages. + /// /// Bails on messages sent by responder and non-handshake messages. - + /// + /// # Examples + /// + /// Using this function is a bit complex and the toughest part is how to perform DOS + /// mitigation. + /// + /// The best places to check out to learn more about how this function can be used + /// are the tests: + /// + /// - test::cookie_reply_mechanism_responder_under_load + /// - test::cookie_reply_mechanism_initiator_bails_on_message_under_load pub fn handle_msg_under_load( &mut self, rx_buf: &[u8], @@ -1062,6 +2109,7 @@ impl CryptoServer { let mut rx_mac = [0u8; MAC_SIZE]; let mut rx_sid = [0u8; 4]; let msg_type: Result = rx_buf[0].try_into(); + // TODO: Th match msg_type { Ok(MsgType::InitConf) => { log::debug!( @@ -1203,6 +2251,12 @@ impl CryptoServer { /// | t1 | | <- | `RespHello` | /// | t2 | `InitConf` | -> | | /// | t3 | | <- | `EmptyData` | + /// + /// # Examples + /// + /// See the example on how to use this function without [Self::poll] in [crate::protocol]. + /// + /// See [Self::poll] on how to use this function with poll. pub fn handle_msg(&mut self, rx_buf: &[u8], tx_buf: &mut [u8]) -> Result { let seal_broken = "Message seal broken!"; // length of the response. We assume no response, so None for now @@ -1304,18 +2358,24 @@ impl CryptoServer { }) } - /// Serialize message to `tx_buf`, generating the `mac` in the process of - /// doing so. If `cookie_secret` is also present, a `cookie` value is also generated - /// and added to the message + /// This is used to finalize a message in a transmission buffer + /// while ensuring that the [Envelope::mac] and [Envelope::cookie] + /// fields are properly filled. + /// + /// The message type is explicitly required as a measure of defensive + /// programming, because it is very easy to forget setting the message type, + /// which creates subtle impactful. /// - /// The message type is explicitly required here because it is very easy to - /// forget setting that, which creates subtle but far ranging errors. + /// To save some code, the function returns the size of the message, + /// but the same could be easily achieved by calling [size_of] with the + /// message type or by calling [AsBytes::as_bytes] on the message reference. pub fn seal_and_commit_msg( &mut self, peer: PeerPtr, msg_type: MsgType, msg: &mut Ref<&mut [u8], Envelope>, ) -> Result { + // TODO: This function is too unspecific and does not do a lot. We should inline it. msg.msg_type = msg_type as u8; msg.seal(peer, self)?; Ok(size_of::>()) @@ -1324,19 +2384,52 @@ impl CryptoServer { // EVENT POLLING ///////////////////////////////// +/// Special, named type for representing waiting periods in the context +/// of producing a [PollResult] #[derive(Debug, Copy, Clone)] -pub struct Wait(Timing); +pub struct Wait(pub Timing); impl Wait { - fn immediate() -> Self { + /// Produce a zero-valued [Self], basically indicating that some [Pollable::poll] + /// or [CryptoServer::poll] should be called again immediately. + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::Wait; + /// + /// assert_eq!(Wait::immediate().0, 0.0); + /// ``` + pub fn immediate() -> Self { Wait(0.0) } - fn hibernate() -> Self { + /// Produce a [Self] valued [UNENDING], basically indicating that + /// no scheduled wakeup time could be determined + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{Wait, UNENDING}; + /// + /// assert_eq!(Wait::hibernate().0, UNENDING); + /// ``` + pub fn hibernate() -> Self { Wait(UNENDING) } - fn immediate_unless(cond: bool) -> Self { + /// Equivalent to [Self::immediate] or [Self::hibernate], depending + /// the given condition + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{Wait, UNENDING}; + /// + /// assert_eq!(Wait::immediate_unless(false).0, 0.0); + /// assert_eq!(Wait::immediate_unless(true).0, UNENDING); + /// ``` + pub fn immediate_unless(cond: bool) -> Self { if cond { Self::hibernate() } else { @@ -1344,59 +2437,140 @@ impl Wait { } } - fn or_hibernate(t: Option) -> Self { + /// Use the given timing value or hibernate if None + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{Wait, UNENDING}; + /// + /// assert_eq!(Wait::or_hibernate(None).0, UNENDING); + /// assert_eq!(Wait::or_hibernate(Some(20.0)).0, 20.0); + /// ``` + pub fn or_hibernate(t: Option) -> Self { match t { Some(u) => Wait(u), None => Wait::hibernate(), } } - fn or_immediate(t: Option) -> Self { + /// Use the given timing value or [Self::immediate] if none + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{Wait, UNENDING}; + /// + /// assert_eq!(Wait::or_immediate(None).0, 0.0); + /// assert_eq!(Wait::or_immediate(Some(20.0)).0, 20.0); + /// ``` + pub fn or_immediate(t: Option) -> Self { match t { Some(u) => Wait(u), None => Wait::immediate(), } } - fn and>(&self, o: T) -> Self { + /// Wait for the longer of two possible times + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{Wait, UNENDING}; + /// + /// + /// assert_eq!(Wait(20.0).and(30.0).0, 30.0); + /// ``` + pub fn and>(&self, o: T) -> Self { let (a, b) = (self.0, o.into().0); Wait(if a > b { a } else { b }) } } impl From for Wait { + /// Wraps [Timing] into [Self] fn from(t: Timing) -> Wait { Wait(t) } } impl From> for Wait { + /// Equivalent to [Wait::or_hibernate] fn from(t: Option) -> Wait { Wait::or_hibernate(t) } } -/// Result of a poll operation, containing prescriptive action for the outer -/// event loop +/// Result of a poll operation [Pollable::poll] or [CryptoServer::poll], +/// instructing the caller on how to proceed +/// +/// This type also contains a lot of handy functions for performing polling +/// in a nice style. The best place to see this in action is the source code +/// of [CryptoServer::poll]. #[derive(Debug, Copy, Clone)] pub enum PollResult { + /// The caller should wait for IO events until the indicated deadline. + /// + /// After the deadline, the caller should call poll again. + /// + /// If the IO operation produces events before the deadline, the IO messages + /// should be processed – likely by using [CryptoServer::handle_msg] before + /// immediately calling poll again. + /// + /// If the value is `Sleep(0.0)`, then the caller should immediately call + /// poll again. Sleep(Timing), + /// The caller should immediately erase any cryptographic keys exchanged with + /// the peer previously and then immediately call poll again. + /// + /// This is raised after [REKEY_TIMEOUT] if no successful rekey could be achieved. DeleteKey(PeerPtr), + /// The caller should invoke [CryptoServer::handle_initiation] and transmit the + /// initiation to the other party before invoking poll again. SendInitiation(PeerPtr), + /// The caller should invoke [CryptoServer::retransmit_handshake] and transmit the + /// resulting message to the other party before invoking poll again. SendRetransmission(PeerPtr), } impl Default for PollResult { + /// Equal to [Self::hibernate] fn default() -> Self { Self::hibernate() } } impl PollResult { + /// Produce a [Self::Sleep] valued [UNENDING], basically indicating that + /// no scheduled wakeup time could be determined and that the caller + /// should wait for IO operations indefinitely. + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{PollResult, UNENDING}; + /// + /// assert!(matches!(PollResult::hibernate(), PollResult::Sleep(UNENDING))); + /// ``` pub fn hibernate() -> Self { Self::Sleep(UNENDING) // Avoid excessive sleep times (might trigger bugs on some platforms) } + /// Returns the peer this poll result refers to some peer; i.e. if this poll result is not + /// [Self::Sleep]: + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{PollResult, PeerPtr, UNENDING}; + /// + /// let p = PeerPtr(0); + /// + /// assert_eq!(PollResult::Sleep(0.0).peer(), None); + /// assert_eq!(PollResult::DeleteKey(p).peer(), Some(p)); + /// assert_eq!(PollResult::SendInitiation(p).peer(), Some(p)); + /// assert_eq!(PollResult::SendRetransmission(p).peer(), Some(p)); + /// ``` pub fn peer(&self) -> Option { use PollResult::*; match *self { @@ -1405,6 +2579,41 @@ impl PollResult { } } + /// Select the higher-priority poll result from two poll results. + /// + /// - If both poll results are unsaturated (i.e. [Self::Sleep]), sleeps for + /// the shorter time + /// - If one of the poll results is [PollResult::saturated], returns that one + /// - If both results are saturated: Panics. If you need this functionality, see + /// [Self::try_fold_with]. + /// + /// This is enormously useful when dealing with many objects that need polling. + /// + /// # Panic & Safety + /// + /// Panics if both poll results are [PollResult::saturated] + /// + /// ```should_panic + /// use rosenpass::protocol::{PollResult, PeerPtr}; + /// + /// let p = PeerPtr(0); + /// + /// use PollResult as P; + /// P::DeleteKey(p).fold(P::SendInitiation(p)); // panic + /// ``` + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{PollResult, PeerPtr}; + /// + /// let p = PeerPtr(0); + /// + /// use PollResult as P; + /// assert!(matches!(P::Sleep(10.0).fold(P::Sleep(20.0)), P::Sleep(10.0))); + /// assert!(matches!(P::DeleteKey(p).fold(P::Sleep(20.0)), P::DeleteKey(_))); + /// assert!(matches!(P::Sleep(10.0).fold(P::SendInitiation(p)), P::SendInitiation(_))); + /// ``` pub fn fold(&self, otr: PollResult) -> PollResult { use PollResult::*; match (*self, otr) { @@ -1420,6 +2629,26 @@ impl PollResult { } } + /// Like [Self::fold], but takes function to conditionally execute, + /// supports error handling, and safely handles the case that both + /// poll results may be [Self::saturated], by never calling the given + /// function if this is saturated. + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{PollResult, PeerPtr}; + /// + /// let p = PeerPtr(0); + /// + /// use PollResult as P; + /// assert!(matches!(P::Sleep(50.0).try_fold_with(|| Ok(P::Sleep(20.0)))?, P::Sleep(20.0))); + /// assert!(matches!(P::DeleteKey(p).try_fold_with(|| Ok(P::Sleep(20.0)))?, P::DeleteKey(_))); + /// assert!(matches!(P::Sleep(10.0).try_fold_with(|| Ok(P::SendInitiation(p)))?, P::SendInitiation(_))); + /// assert!(matches!(P::DeleteKey(p).try_fold_with(|| Ok(P::SendInitiation(p)))?, P::DeleteKey(_))); + /// + /// Ok::<(), anyhow::Error>(()) + /// ``` pub fn try_fold_with Result>(&self, f: F) -> Result { if self.saturated() { Ok(*self) @@ -1428,10 +2657,22 @@ impl PollResult { } } + /// This is specifically made to invoke the [Pollable::poll] function when recursively checking + /// objects that might need polling. + /// + /// # Examples + /// + /// The best place to see this in action is the source code of [PeerPtr::poll] pub fn poll_child(&self, srv: &mut CryptoServer, p: &P) -> Result { self.try_fold_with(|| p.poll(srv)) } + /// This is specifically made to invoke the [Pollable::poll] function when recursively checking + /// lists of objects that might need polling. + /// + /// # Examples + /// + /// The best place to see this in action is the source code of [PeerPtr::poll] pub fn poll_children(&self, srv: &mut CryptoServer, iter: I) -> Result where P: Pollable, @@ -1447,7 +2688,38 @@ impl PollResult { Ok(acc) } - /// Execute `f` if it is ready, as indicated by `wait` + /// Execute the given polling function at a particular point. + /// + /// The function ignores the polling function if [Self] is [Self::saturated]. + /// + /// The function returns an appropriate [Self::Sleep] value if wait is greater than zero. + /// + /// The function executes the polling function only if wait smaller or equal to zero, modulo + /// [has_happened]. + /// + /// # Examples + /// + /// The best place to see this in action is the source code of [PeerPtr::poll] + /// + /// ``` + /// use rosenpass::protocol::{PollResult, PeerPtr}; + /// + /// let p = PeerPtr(0); + /// + /// use PollResult as P; + /// assert!(matches!(P::Sleep(50.0).sched(0.0, || P::Sleep(20.0)), P::Sleep(20.0))); + /// assert!(matches!(P::Sleep(50.0).sched(60.0, || P::Sleep(20.0)), P::Sleep(50.0))); + /// assert!(matches!(P::Sleep(50.0).sched(50.0, || P::Sleep(20.0)), P::Sleep(50.0))); + /// assert!(matches!(P::Sleep(50.0).sched(40.0, || P::Sleep(20.0)), P::Sleep(40.0))); + /// + /// assert!(matches!(P::Sleep(50.0).sched(0.0, || P::DeleteKey(p)), P::DeleteKey(p))); + /// assert!(matches!(P::Sleep(50.0).sched(60.0, || P::DeleteKey(p)), P::Sleep(50.0))); + /// assert!(matches!(P::Sleep(50.0).sched(50.0, || P::DeleteKey(p)), P::Sleep(50.0))); + /// assert!(matches!(P::Sleep(50.0).sched(40.0, || P::DeleteKey(p)), P::Sleep(40.0))); + /// + /// assert!(matches!(P::DeleteKey(p).sched(0.0, || P::SendInitiation(p)), P::DeleteKey(p))); + /// assert!(matches!(P::DeleteKey(p).sched(10.0, || P::SendInitiation(p)), P::DeleteKey(p))); + /// ``` pub fn sched, F: FnOnce() -> PollResult>(&self, wait: W, f: F) -> PollResult { let wait = wait.into().0; if self.saturated() { @@ -1459,6 +2731,33 @@ impl PollResult { } } + /// Like [Self::sched], but supports error handling with Result + /// + /// # Examples + /// + /// The best place to see this in action is the source code of [PeerPtr::poll] + /// + /// ``` + /// use rosenpass::protocol::{PollResult, PeerPtr}; + /// + /// let p = PeerPtr(0); + /// + /// use PollResult as P; + /// assert!(matches!(P::Sleep(50.0).try_sched(0.0, || Ok(P::Sleep(20.0)))?, P::Sleep(20.0))); + /// assert!(matches!(P::Sleep(50.0).try_sched(60.0, || Ok(P::Sleep(20.0)))?, P::Sleep(50.0))); + /// assert!(matches!(P::Sleep(50.0).try_sched(50.0, || Ok(P::Sleep(20.0)))?, P::Sleep(50.0))); + /// assert!(matches!(P::Sleep(50.0).try_sched(40.0, || Ok(P::Sleep(20.0)))?, P::Sleep(40.0))); + /// + /// assert!(matches!(P::Sleep(50.0).try_sched(0.0, || Ok(P::DeleteKey(p)))?, P::DeleteKey(p))); + /// assert!(matches!(P::Sleep(50.0).try_sched(60.0, || Ok(P::DeleteKey(p)))?, P::Sleep(50.0))); + /// assert!(matches!(P::Sleep(50.0).try_sched(50.0, || Ok(P::DeleteKey(p)))?, P::Sleep(50.0))); + /// assert!(matches!(P::Sleep(50.0).try_sched(40.0, || Ok(P::DeleteKey(p)))?, P::Sleep(40.0))); + /// + /// assert!(matches!(P::DeleteKey(p).try_sched(0.0, || Ok(P::SendInitiation(p)))?, P::DeleteKey(p))); + /// assert!(matches!(P::DeleteKey(p).try_sched(10.0, || Ok(P::SendInitiation(p)))?, P::DeleteKey(p))); + /// + /// Ok::<(), anyhow::Error>(()) + /// ``` pub fn try_sched, F: FnOnce() -> Result>( &self, wait: W, @@ -1474,22 +2773,83 @@ impl PollResult { } } + /// Convenience function to wrap a PollResult into a Result + /// + /// # Examples + /// + /// The best place to see this in action is the source code of [PeerPtr::poll] + /// + /// ``` + /// use rosenpass::protocol::{PollResult}; + /// + /// use PollResult as P; + /// assert!(matches!(P::Sleep(50.0).ok(), Ok(P::Sleep(50.0)))); + /// ``` pub fn ok(&self) -> Result { Ok(*self) } + /// A poll-result is considered to be saturated if it is something + /// other than [Self::Sleep] + /// + /// # Examples + /// + /// ``` + /// use rosenpass::protocol::{PollResult, PeerPtr}; + /// + /// let p = PeerPtr(0); + /// + /// use PollResult as P; + /// assert!(!P::Sleep(0.0).saturated()); + /// assert!(!P::Sleep(50.0).saturated()); + /// assert!(P::DeleteKey(p).saturated()); + /// assert!(P::SendRetransmission(p).saturated()); + /// assert!(P::SendInitiation(p).saturated()); + /// ``` pub fn saturated(&self) -> bool { use PollResult::*; !matches!(self, Sleep(_)) } } +/// Semantic wrapper around [PollResult::default] +/// +/// # Examples +/// +/// ``` +/// use rosenpass::protocol::{begin_poll, PollResult, UNENDING}; +/// +/// assert!(matches!(begin_poll(), PollResult::Sleep(UNENDING))); +/// ``` pub fn begin_poll() -> PollResult { PollResult::default() } /// Takes a closure `f`, returns another closure which internally calls f and /// then returns a default [PollResult] +/// +/// This is a convenience function in order to be able to encode side_effects in +/// the polling process. +/// +/// # Examples +/// +/// The best place to see this in action is the source code of [PeerPtr::poll] +/// +/// ``` +/// use rosenpass::protocol::{begin_poll, void_poll, PollResult, PeerPtr}; +/// +/// let mut x = 0; +/// +/// let poll_result = begin_poll() +/// .sched(20.0, void_poll(|| { x += 100 })) +/// .sched(0.0, void_poll(|| { x += 10 })) +/// .sched(0.0, void_poll(|| { x += 10 })) +/// .sched(0.0, void_poll(|| { x += 10 })) +/// .fold(PollResult::SendInitiation(PeerPtr(0))) +/// .sched(0.0, void_poll(|| { x += 1 })); +/// assert!(matches!(poll_result, PollResult::SendInitiation(_))); +/// assert_eq!(x, 30); +/// ``` pub fn void_poll T>(f: F) -> impl FnOnce() -> PollResult { || { f(); @@ -1497,15 +2857,51 @@ pub fn void_poll T>(f: F) -> impl FnOnce() -> PollResult { } } +/// Implemented for types that should be polled during recursive polling in [CryptoServer::poll] pub trait Pollable { + /// Poll this! + /// + /// # Examples + /// + /// The best place to see this in action is the source code of [PeerPtr::poll] fn poll(&self, srv: &mut CryptoServer) -> Result; } impl CryptoServer { - /// Implements something like [Pollable::poll] for the server, with a + /// Poll the CryptoServer for new events + /// + /// Handles all timing based events produced in the course of executing the Rosenpass + /// protocol such as: + /// + /// - Cycling of biscuit and cookie keys ([CryptoServer::biscuit_keys] and + /// [CryptoServer::cookie_secrets]) + /// - Scheduling of initiation key exchanges and key renegotiations ([PollResult::SendInitiation]) + /// - Scheduling of message retransmission ([PollResult::SendRetransmission]) + /// - Scheduling of key erasure ([PollResult::DeleteKey]) + /// + /// The correct way to use CryptoServer in production environments is to first + /// call poll and then react to the instructions issued by poll. [PollResult] documents + /// the actions that should be taken by the caller depending on the PollResult's value. + /// documents + /// the actions that should be taken by the caller depending on the PollResult's value. + /// + /// This is similar to [Pollable::poll] for the server, with a /// notable difference: since `self` already is the server, the signature /// has to be different; `self` must be a `&mut` and already is a borrow to /// the server, eluding the need for a second arg. + /// + /// # Examples + /// + /// Here is a complete example of how to use poll. It is written as a comprehensive integration + /// test providing transcript based testing of the rosenpass, showcasing how to set up an event + /// handling system integrating rosenpass. + /// + /// This is a lot of code. If you want to read the file outside of the documentation, + /// check out `rosenpass/tests/poll_example.rs" in the repository. + /// + #[doc = "```"] + #[doc = include_str!("../../tests/poll_example.rs")] + #[doc = "```"] pub fn poll(&mut self) -> Result { let r = begin_poll() // Poll each biscuit and peer until an event is found .poll_children(self, self.biscuit_key_ptrs())? @@ -1587,12 +2983,29 @@ impl Pollable for KnownInitConfResponsePtr { // MESSAGE RETRANSMISSION //////////////////////// impl CryptoServer { + /// Retransmits the current initiator-role handshake for the given peer. + /// + /// This should usually be called after [Self::poll] returns a + /// [PollResult::SendRetransmission]. + /// + /// # Examples + /// + /// For a full example of how to use the crypto server, including how to process retransmission + /// handling, see the example in [Self::poll]. pub fn retransmit_handshake(&mut self, peer: PeerPtr, tx_buf: &mut [u8]) -> Result { peer.hs().apply_retransmission(self, tx_buf) } } impl IniHsPtr { + /// Store a protocol message for retransmission in initiator mode. + /// + /// This is called by [CryptoServer::initiate_handshake] and [CryptoServer::handle_msg] + /// after producing an [InitHello] or [InitConf] message. + /// + /// # Examples + /// + /// This is internal business logic. Please refer to the source code of [CryptoServer::initiate_handshake] and [CryptoServer::handle_msg]. pub fn store_msg_for_retransmission(&self, srv: &mut CryptoServer, msg: &[u8]) -> Result<()> { let ih = self .get_mut(srv) @@ -1605,6 +3018,14 @@ impl IniHsPtr { Ok(()) } + /// [CryptoServer::retransmit_handshake] forwards this. You should use the function in + /// CryptoServer instead of this one. + /// + /// # Examples + /// + /// This is considered to be internal logic. + /// + /// See the examples and the source code of [CryptoServer::retransmit_handshake]. pub fn apply_retransmission(&self, srv: &mut CryptoServer, tx_buf: &mut [u8]) -> Result { let ih_tx_len: usize; @@ -1624,6 +3045,7 @@ impl IniHsPtr { Ok(ih_tx_len) } + /// Internal business logic; used to register the fact that a retransmission has happened. pub fn register_retransmission(&self, srv: &mut CryptoServer) -> Result<()> { let tb = srv.timebase.clone(); let ih = self @@ -1644,6 +3066,8 @@ impl IniHsPtr { Ok(()) } + /// Internal business logic; used to register the fact that an immediate retransmission + /// has happened in response to a [CookieReply] message pub fn register_immediate_retransmission(&self, srv: &mut CryptoServer) -> Result<()> { let tb = srv.timebase.clone(); let ih = self @@ -1655,6 +3079,8 @@ impl IniHsPtr { Ok(()) } + /// Internal business logic: Indicates when the next retransmission of the message + /// stored for the peer will happen pub fn retransmission_in(&self, srv: &mut CryptoServer) -> Option { self.get(srv) .as_ref() @@ -1668,7 +3094,7 @@ impl Envelope where M: AsBytes + FromBytes, { - /// Calculate the message authentication code (`mac`) and also append cookie value + /// Internal business logic: Calculate the message authentication code (`mac`) and also append cookie value pub fn seal(&mut self, peer: PeerPtr, srv: &CryptoServer) -> Result<()> { let mac = hash_domains::mac()? .mix(peer.get(srv).spkt.deref())? @@ -1678,7 +3104,9 @@ where Ok(()) } - /// Calculate and append the cookie value if `cookie_key` exists (`cookie`) + /// Internal business logic: Calculate and append the cookie value if `cookie_key` exists (`cookie`) + /// + /// This is called inside [Self::seal] and does not need to be called again separately. pub fn seal_cookie(&mut self, peer: PeerPtr, srv: &CryptoServer) -> Result<()> { if let Some(cookie_key) = &peer.cv().get(srv) { let cookie = hash_domains::cookie()? @@ -1695,7 +3123,7 @@ impl Envelope where M: AsBytes + FromBytes, { - /// Check the message authentication code + /// Internal business logic: Check the message authentication code produced by [Self::seal] pub fn check_seal(&self, srv: &CryptoServer) -> Result { let expected = hash_domains::mac()? .mix(srv.spkm.deref())? @@ -1708,6 +3136,7 @@ where } impl InitiatorHandshake { + /// Zero initialization of an InitiatorHandshake, with up to date timestamp pub fn zero_with_timestamp(srv: &CryptoServer) -> Self { InitiatorHandshake { created_at: srv.timebase.now(), @@ -1726,6 +3155,7 @@ impl InitiatorHandshake { } impl HandshakeState { + /// Zero initialization of an HandshakeState pub fn zero() -> Self { Self { sidi: SessionId::zero(), @@ -1734,32 +3164,48 @@ impl HandshakeState { } } + /// Securely erase the chaining key pub fn erase(&mut self) { self.ck = SecretHashDomain::zero().dup(); } + /// Initialize the handshake state with the responder public key and the protocol domain + /// separator pub fn init(&mut self, spkr: &[u8]) -> Result<&mut Self> { self.ck = hash_domains::ckinit()?.turn_secret().mix(spkr)?.dup(); Ok(self) } + /// Mix some data into the chaining key. This is used for mixing cryptographic keys and public + /// data alike into the chaining key pub fn mix(&mut self, a: &[u8]) -> Result<&mut Self> { self.ck = self.ck.mix(&hash_domains::mix()?)?.mix(a)?.dup(); Ok(self) } + /// Encrypt some data with a value derived from the current chaining key and mix that data + /// into the protocol state. pub fn encrypt_and_mix(&mut self, ct: &mut [u8], pt: &[u8]) -> Result<&mut Self> { let k = self.ck.mix(&hash_domains::hs_enc()?)?.into_secret(); aead::encrypt(ct, k.secret(), &[0u8; aead::NONCE_LEN], &[], pt)?; self.mix(ct) } + /// Decryption counterpart to [Self::encrypt_and_mix]. + /// + /// Makes sure that the same values are mixed into the chaining that where mixed in on the + /// sender side. pub fn decrypt_and_mix(&mut self, pt: &mut [u8], ct: &[u8]) -> Result<&mut Self> { let k = self.ck.mix(&hash_domains::hs_enc()?)?.into_secret(); aead::decrypt(pt, k.secret(), &[0u8; aead::NONCE_LEN], &[], ct)?; self.mix(ct) } + /// Encapsulate a secret with a KEM and mix the resulting secret into the chaining key. + /// + /// The ciphertext must be transmitted to the other party. + /// + /// This is used to include asymmetric cryptography in the rosenpass protocol // I loathe "error: constant expression depends on a generic parameter" pub fn encaps_and_mix, const SHK_LEN: usize>( &mut self, @@ -1771,6 +3217,10 @@ impl HandshakeState { self.mix(pk)?.mix(shk.secret())?.mix(ct) } + /// Decapsulation (decryption) counterpart to [Self::encaps_and_mix]. + /// + /// Makes sure that the same values are mixed into the chaining that where mixed in on the + /// sender side. pub fn decaps_and_mix, const SHK_LEN: usize>( &mut self, sk: &[u8], @@ -1782,6 +3232,13 @@ impl HandshakeState { self.mix(pk)?.mix(shk.secret())?.mix(ct) } + /// Store the chaining key inside a cookie value called a "biscuit". + /// + /// This biscuit can be transmitted to the other party and must be returned + /// by them on the next protocol message. + /// + /// This is used to store the responder state between [InitHello] and [InitConf] processing + /// to make sure the responder is stateless. pub fn store_biscuit( &mut self, srv: &mut CryptoServer, @@ -1825,8 +3282,7 @@ impl HandshakeState { self.mix(biscuit_ct) } - /// Takes an encrypted biscuit and tries to decrypt the contained - /// information + /// This is the counterpart to [Self::store_biscuit] that restores a stored biscuit pub fn load_biscuit( srv: &CryptoServer, biscuit_ct: &[u8], @@ -1871,6 +3327,11 @@ impl HandshakeState { Ok((peer, no, hs)) } + /// Initialize a [Session] after the key exchange was completed + /// + /// This called by either party. + /// + /// `role` indicates whether the local peer was an initiator or responder in the handshake. pub fn enter_live(self, srv: &CryptoServer, role: HandshakeRole) -> Result { let HandshakeState { ck, sidi, sidr } = self; let tki = ck.mix(&hash_domains::ini_enc()?)?.into_secret(); @@ -1899,6 +3360,13 @@ impl CryptoServer { /// Get the shared key that was established with given peer /// /// Fail if no session is available with the peer + /// + /// # Examples + /// + /// See the example in [crate::protocol] for an incomplete but working example + /// of how to perform a key exchange using Rosenpass. + /// + /// See the example in [CryptoServer::poll] for a complete example. pub fn osk(&self, peer: PeerPtr) -> Result { let session = peer .session() @@ -1910,8 +3378,8 @@ impl CryptoServer { } impl CryptoServer { - /// Implementation of the cryptographic protocol using the already - /// established primitives + /// Core cryptographic protocol implementation: Kicks of the handshake + /// on the initiator side, producing the InitHello message. pub fn handle_initiation(&mut self, peer: PeerPtr, ih: &mut InitHello) -> Result { let mut hs = InitiatorHandshake::zero_with_timestamp(self); @@ -1954,6 +3422,8 @@ impl CryptoServer { Ok(peer) } + /// Core cryptographic protocol implementation: Parses an [InitHello] message and produces a + /// [RespHello] message on the responder side. pub fn handle_init_hello(&mut self, ih: &InitHello, rh: &mut RespHello) -> Result { let mut core = HandshakeState::zero(); @@ -2013,6 +3483,8 @@ impl CryptoServer { Ok(peer) } + /// Core cryptographic protocol implementation: Parses an [RespHello] message and produces an + /// [InitConf] message on the initiator side. pub fn handle_resp_hello(&mut self, rh: &RespHello, ic: &mut InitConf) -> Result { // RHI2 let peer = self @@ -2105,6 +3577,11 @@ impl CryptoServer { Ok(peer) } + /// Core cryptographic protocol implementation: Parses an [InitConf] message and produces an + /// [EmptyData] (responder confimation) message on the responder side. + /// + /// This concludes the handshake on the cryptographic level; the [EmptyData] message is just + /// an acknowledgement message telling the initiator to stop performing retransmissions. pub fn handle_init_conf(&mut self, ic: &InitConf, rc: &mut EmptyData) -> Result { // (peer, bn) ← LoadBiscuit(InitConf.biscuit) // ICR1 @@ -2182,6 +3659,10 @@ impl CryptoServer { Ok(peer) } + /// Core cryptographic protocol implementation: Parses an [EmptyData] (responder confirmation) + /// message then terminates the handshake. + /// + /// The EmptyData message is just there to tell the initiator to abort retransmissions. pub fn handle_resp_conf(&mut self, rc: &EmptyData) -> Result { let sid = SessionId::from_slice(&rc.sid); let hs = self @@ -2225,6 +3706,11 @@ impl CryptoServer { Ok(hs.peer()) } + /// Core protocol implementation: This is not part of the cryptographic handshake itself, + /// instead this function is used to process [CookieReply] messages which is part of Rosenpass' + /// DOS mitigation features. + /// + /// See more on DOS mitigation in Rosenpass in the [whitepaper](https://rosenpass.eu/whitepaper.pdf). pub fn handle_cookie_reply(&mut self, cr: &CookieReply) -> Result { let peer_ptr: Option = self .lookup_session(Public::new(cr.inner.sid)) @@ -2285,15 +3771,60 @@ impl CryptoServer { } } +/// Used to parse a network message using [zerocopy] fn truncating_cast_into(buf: &mut [u8]) -> Result, RosenpassError> { Ref::new(&mut buf[..size_of::()]).ok_or(RosenpassError::BufferSizeMismatch) } -// TODO: This is bad… -fn truncating_cast_into_nomut(buf: &[u8]) -> Result, RosenpassError> { +/// Used to parse a network message using [zerocopy], mutably +pub fn truncating_cast_into_nomut( + buf: &[u8], +) -> Result, RosenpassError> { Ref::new(&buf[..size_of::()]).ok_or(RosenpassError::BufferSizeMismatch) } +pub mod testutils { + use std::ops::DerefMut; + + use super::*; + + /// Helper for tests and examples + pub struct ServerForTesting { + pub peer: PeerPtr, + pub peer_keys: (SSk, SPk), + pub srv: CryptoServer, + } + + impl ServerForTesting { + pub fn new() -> anyhow::Result { + let (mut sskm, mut spkm) = (SSk::zero(), SPk::zero()); + StaticKem::keygen(sskm.secret_mut(), spkm.deref_mut())?; + let mut srv = CryptoServer::new(sskm, spkm); + + let (mut sskt, mut spkt) = (SSk::zero(), SPk::zero()); + StaticKem::keygen(sskt.secret_mut(), spkt.deref_mut())?; + let peer = srv.add_peer(None, spkt.clone())?; + + let peer_keys = (sskt, spkt); + Ok(ServerForTesting { + peer, + peer_keys, + srv, + }) + } + + pub fn tuple(self) -> (PeerPtr, (SSk, SPk), CryptoServer) { + (self.peer, self.peer_keys, self.srv) + } + } + + /// Time travel forward in time + pub fn time_travel_forward(srv: &mut CryptoServer, secs: f64) { + let dur = std::time::Duration::from_secs_f64(secs); + srv.timebase.0 = srv.timebase.0.checked_sub(dur).unwrap(); + } +} + #[cfg(test)] mod test { use std::{borrow::BorrowMut, net::SocketAddrV4, ops::DerefMut, thread::sleep, time::Duration}; @@ -2794,11 +4325,6 @@ mod test { Ok(msg) } - fn time_travel_forward(srv: &mut CryptoServer, secs: f64) { - let dur = std::time::Duration::from_secs_f64(secs); - srv.timebase.0 = srv.timebase.0.checked_sub(dur).unwrap(); - } - fn check_faulty_proc_init_conf(srv: &mut CryptoServer, ic_broken: &Envelope) { let mut buf = MsgBuf::zero(); let res = srv.handle_msg(ic_broken.as_bytes(), buf.as_mut_slice()); @@ -2885,7 +4411,7 @@ mod test { // Except if we jump forward into the future past the point where the responder // starts to initiate rekeying; in this case, the automatic time out is triggered and the cache is cleared - time_travel_forward(&mut b, REKEY_AFTER_TIME_RESPONDER); + super::testutils::time_travel_forward(&mut b, REKEY_AFTER_TIME_RESPONDER); // As long as we do not call poll, everything is fine check_retransmission(&mut b, &ic1, &ic1_broken, &rc1)?; diff --git a/rosenpass/tests/poll_example.rs b/rosenpass/tests/poll_example.rs new file mode 100644 index 000000000..f9bc8772d --- /dev/null +++ b/rosenpass/tests/poll_example.rs @@ -0,0 +1,634 @@ +/// This file contains a correct simulation of a two-party key exchange using Poll +use std::{ + borrow::{Borrow, BorrowMut}, + collections::VecDeque, + fmt::{Debug, Write}, + ops::{DerefMut, RangeBounds}, +}; + +use rand::distributions::uniform::SampleBorrow; +use rosenpass_cipher_traits::Kem; +use rosenpass_ciphers::kem::StaticKem; +use rosenpass_util::result::OkExt; + +use rosenpass::protocol::{ + testutils::time_travel_forward, CryptoServer, HostIdentification, MsgBuf, PeerPtr, PollResult, + SPk, SSk, SymKey, Timing, UNENDING, +}; + +// TODO: Most of the utility functions in here should probably be moved to +// rosenpass::protocol::testutils; + +#[test] +fn test_successful_exchange_with_poll() -> anyhow::Result<()> { + // Set security policy for storing secrets; choose the one that is faster for testing + rosenpass_secret_memory::policy::secret_policy_use_only_malloc_secrets(); + + let mut sim = RosenpassSimulator::new()?; + sim.poll_loop(150)?; // Poll 75 times + let transcript = sim.transcript; + + let completions: Vec<_> = transcript + .iter() + .filter(|elm| matches!(elm, (_, TranscriptEvent::CompletedExchange(_)))) + .collect(); + + assert!( + !completions.is_empty(), + "\ + Should have performed a successful key exchanged!\n\ + Transcript: {transcript:?}\n\ + Completions: {completions:?}\ + " + ); + assert!( + completions[0].0 < 20.0, + "\ + First key exchange should happen in under twenty seconds!\n\ + Transcript: {transcript:?}\n\ + Completions: {completions:?}\ + " + ); + + assert!( + completions.len() >= 3, + "\ + Should have at least two renegotiations!\n\ + Transcript: {transcript:?}\n\ + Completions: {completions:?}\ + " + ); + assert!( + (110.0..175.0).contains(&completions[1].0), + "\ + First renegotiation should happen in between two and three minutes!\n\ + Transcript: {transcript:?}\n\ + Completions: {completions:?}\ + " + ); + assert!((110.0..175.0).contains(&(completions[2].0 - completions[1].0)), "\ + First renegotiation should happen in between two and three minutes after the first renegotiation!\n\ + Transcript: {transcript:?}\n\ + Completions: {completions:?}\ + "); + + Ok(()) +} + +#[test] +fn test_successful_exchange_under_packet_loss() -> anyhow::Result<()> { + // Set security policy for storing secrets; choose the one that is faster for testing + rosenpass_secret_memory::policy::secret_policy_use_only_malloc_secrets(); + + // Create the simulator + let mut sim = RosenpassSimulator::new()?; + + // Make sure the servers are set to under load condition + sim.srv_a.under_load = true; + sim.srv_b.under_load = false; // See Issue #539 -- https://github.com/rosenpass/rosenpass/issues/539 + + // Perform the key exchanges + let mut pkg_counter = 0usize; + for _ in 0..300 { + let ev = sim.poll()?; + if let TranscriptEvent::ServerEvent { + source, + event: ServerEvent::Transmit(_, _), + } = ev + { + // Drop every fifth package + if pkg_counter % 10 == 0 { + source.drop_outgoing_packet(&mut sim); + } + + pkg_counter += 1; + } + } + + let transcript = sim.transcript; + let completions: Vec<_> = transcript + .iter() + .filter(|elm| matches!(elm, (_, TranscriptEvent::CompletedExchange(_)))) + .collect(); + + assert!( + !completions.is_empty(), + "\ + Should have performed a successful key exchanged!\n\ + Transcript: {transcript:?}\n\ + Completions: {completions:?}\ + " + ); + assert!( + completions[0].0 < 10.0, + "\ + First key exchange should happen in under twenty seconds!\n\ + Transcript: {transcript:?}\n\ + Completions: {completions:?}\ + " + ); + + assert!( + completions.len() >= 3, + "\ + Should have at least two renegotiations!\n\ + Transcript: {transcript:?}\n\ + Completions: {completions:?}\ + " + ); + assert!( + (110.0..175.0).contains(&completions[1].0), + "\ + First renegotiation should happen in between two and three minutes!\n\ + Transcript: {transcript:?}\n\ + Completions: {completions:?}\ + " + ); + assert!((110.0..175.0).contains(&(completions[2].0 - completions[1].0)), "\ + First renegotiation should happen in between two and three minutes after the first renegotiation!\n\ + Transcript: {transcript:?}\n\ + Completions: {completions:?}\ + "); + + Ok(()) +} + +type MessageType = u8; + +/// Lets record the events that are produced by Rosenpass +#[derive(Debug)] +#[allow(unused)] +enum TranscriptEvent { + Wait(Timing), + ServerEvent { + source: ServerPtr, + event: ServerEvent, + }, + CompletedExchange(SymKey), +} + +#[derive(Debug)] +#[allow(unused)] +enum ServerEvent { + DeleteKey, + SendInitiationRequested, + SendRetransmissionRequested, + Exchanged(SymKey), + DiscardInvalidMessage(anyhow::Error), + Transmit(MessageType, SendMsgReason), + Receive(Option), + DroppedPackage, +} + +#[derive(Debug, Clone, Copy)] +enum SendMsgReason { + Initiation, + Response, + Retransmission, +} + +impl TranscriptEvent { + fn hibernate() -> Self { + Self::Wait(UNENDING) + } + + fn begin_poll() -> Self { + Self::hibernate() + } + + fn transmit(source: ServerPtr, buf: &[u8], reason: SendMsgReason) -> Self { + assert!(!buf.is_empty()); + let msg_type = buf[0]; + ServerEvent::Transmit(msg_type, reason).into_transcript_event(source) + } + + fn receive(source: ServerPtr, buf: &[u8]) -> Self { + let msg_type = (!buf.is_empty()).then(|| buf[0]); + ServerEvent::Receive(msg_type).into_transcript_event(source) + } + + pub fn try_fold_with anyhow::Result>( + self, + f: F, + ) -> anyhow::Result { + let wait_time_a = match self { + Self::Wait(wait_time_a) => wait_time_a, + els => return (els).ok(), + }; + + let wait_time_b = match f()? { + Self::Wait(wait_time_b) => wait_time_b, + els => return els.ok(), + }; + + let min_wt = if wait_time_a <= wait_time_b { + wait_time_a + } else { + wait_time_b + }; + Self::Wait(min_wt).ok() + } +} + +impl ServerEvent { + fn into_transcript_event(self, source: ServerPtr) -> TranscriptEvent { + let event = self; + TranscriptEvent::ServerEvent { source, event } + } +} + +#[derive(Debug)] +struct RosenpassSimulator { + transcript: Vec<(Timing, TranscriptEvent)>, + srv_a: SimulatorServer, + srv_b: SimulatorServer, + poll_focus: ServerPtr, +} + +#[derive(Debug)] +enum UpcomingPollResult { + IssueEvent(TranscriptEvent), + SendMessage(Vec, TranscriptEvent), +} + +#[derive(Debug)] +struct SimulatorServer { + /// We sometimes return multiple multiple events in one call, + /// but [ServerPtr::poll] should return just one event per call + upcoming_poll_results: VecDeque, + srv: CryptoServer, + rx_queue: VecDeque>, + other_peer: PeerPtr, + under_load: bool, +} + +impl RosenpassSimulator { + /// Set up the simulator + fn new() -> anyhow::Result { + // Set up the first server + let (mut peer_a_sk, mut peer_a_pk) = (SSk::zero(), SPk::zero()); + StaticKem::keygen(peer_a_sk.secret_mut(), peer_a_pk.deref_mut())?; + let mut srv_a = CryptoServer::new(peer_a_sk, peer_a_pk.clone()); + + // …and the second server. + let (mut peer_b_sk, mut peer_b_pk) = (SSk::zero(), SPk::zero()); + StaticKem::keygen(peer_b_sk.secret_mut(), peer_b_pk.deref_mut())?; + let mut srv_b = CryptoServer::new(peer_b_sk, peer_b_pk.clone()); + + // Generate a PSK and introduce the Peers to each other. + let psk = SymKey::random(); + let peer_a = srv_a.add_peer(Some(psk.clone()), peer_b_pk)?; + let peer_b = srv_b.add_peer(Some(psk), peer_a_pk)?; + + // Set up the individual server data structures + let srv_a = SimulatorServer::new(srv_a, peer_b); + let srv_b = SimulatorServer::new(srv_b, peer_a); + + // Initialize transcript and polling state + let transcript = Vec::new(); + let poll_focus = ServerPtr::A; + + // Construct the simulator itself + Self { + transcript, + poll_focus, + srv_a, + srv_b, + } + .ok() + } + + /// Call [poll] a fixed number of times + fn poll_loop(&mut self, times: u64) -> anyhow::Result<()> { + for _ in 0..times { + self.poll()?; + } + Ok(()) + } + + /// Every call to poll produces one [TranscriptEvent] and + /// and implicitly adds it to [Self:::transcript] + fn poll(&mut self) -> anyhow::Result<&TranscriptEvent> { + let ev = TranscriptEvent::begin_poll() + .try_fold_with(|| self.poll_focus.poll(self))? + .try_fold_with(|| { + self.poll_focus = self.poll_focus.other(); + self.poll_focus.poll(self) + })?; + + // Generate up a time stamp + let now = self.srv_a.srv.timebase.now(); + + // Push the event onto the transcript + self.transcript.push((now, ev)); + // We can unwrap; we just pushed the event ourselves + let ev = self.transcript.last().unwrap().1.borrow(); + + // Time travel instead of waiting + if let TranscriptEvent::Wait(secs) = ev { + time_travel_forward(&mut self.srv_a.srv, *secs); + time_travel_forward(&mut self.srv_b.srv, *secs); + } + + ev.ok() + } +} + +impl SimulatorServer { + fn new(srv: CryptoServer, other_peer: PeerPtr) -> Self { + let upcoming_poll_results = VecDeque::new(); + let rx_queue = VecDeque::new(); + let under_load = false; + Self { + upcoming_poll_results, + srv, + rx_queue, + other_peer, + under_load, + } + } +} + +/// Straightforward way of accessing either of the two servers +/// with associated data +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum ServerPtr { + A, + B, +} + +impl std::fmt::Display for ServerPtr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{:?}", self)) + } +} + +impl HostIdentification for ServerPtr { + fn encode(&self) -> &[u8] { + match *self { + Self::A => b"ServerPtr::A", + Self::B => b"ServerPtr::B", + } + } +} + +impl ServerPtr { + fn poll(self, sim: &mut RosenpassSimulator) -> anyhow::Result { + TranscriptEvent::begin_poll() + .try_fold_with(|| self.flush_upcoming_events(sim).ok())? + .try_fold_with(|| self.poll_for_timed_events(sim))? + .try_fold_with(|| self.process_incoming_messages(sim)) + } + + /// Returns and applies the first upcoming event + fn flush_upcoming_events(self, sim: &mut RosenpassSimulator) -> TranscriptEvent { + use UpcomingPollResult as R; + match self.get_mut(sim).upcoming_poll_results.pop_front() { + None => TranscriptEvent::hibernate(), + Some(R::IssueEvent(ev)) => ev, + Some(R::SendMessage(msg, ev)) => { + self.transmit(sim, msg); + ev + } + } + } + + fn poll_for_timed_events( + self, + sim: &mut RosenpassSimulator, + ) -> anyhow::Result { + use PollResult as P; + use ServerEvent as SE; + use TranscriptEvent as TE; + + let other_peer = self.peer(sim); + + // Check if there are events to process from poll() + loop { + match self.srv_mut(sim).poll()? { + // Poll just told us to immediately call poll again + P::Sleep(0.0) => continue, + + // No event to handle immediately. We can now check to see if there are some + // messages to be handled + P::Sleep(wait_time) => { + return TE::Wait(wait_time).ok(); + } + + // Not deleting any keys in practice here, since we just push events to the + // transcript + P::DeleteKey(_) => { + return SE::DeleteKey.into_transcript_event(self).ok(); + } + + P::SendInitiation(_) => { + self.enqueue_upcoming_poll_event( + sim, + SE::SendInitiationRequested.into_transcript_event(self), + ); + + let mut buf = MsgBuf::zero(); + let len = self + .srv_mut(sim) + .initiate_handshake(other_peer, &mut buf[..])?; + self.enqueue_upcoming_poll_transmission( + sim, + buf[..len].to_vec(), + SendMsgReason::Initiation, + ); + + return self.flush_upcoming_events(sim).ok(); // Just added them + } + + P::SendRetransmission(_) => { + self.enqueue_upcoming_poll_event( + sim, + SE::SendRetransmissionRequested.into_transcript_event(self), + ); + + let mut buf = MsgBuf::zero(); + let len = self + .srv_mut(sim) + .retransmit_handshake(other_peer, &mut buf[..])?; + self.enqueue_upcoming_poll_transmission( + sim, + buf[..len].to_vec(), + SendMsgReason::Retransmission, + ); + + return self.flush_upcoming_events(sim).ok(); // Just added them + } + }; + } + } + + fn process_incoming_messages( + self, + sim: &mut RosenpassSimulator, + ) -> anyhow::Result { + use ServerEvent as SE; + use TranscriptEvent as TE; + + // Check for a message or exit + let rx_msg = match self.recv(sim) { + None => return TE::hibernate().ok(), + // Actually received a message + Some(rx_msg) => rx_msg, + }; + + // Add info that a message was received into the transcript + self.enqueue_upcoming_poll_event(sim, TE::receive(self, rx_msg.borrow())); + + // Let the crypto server handle the message now + let mut tx_buf = MsgBuf::zero(); + let handle_msg_result = if self.get(sim).under_load { + self.srv_mut(sim) + .handle_msg_under_load(rx_msg.borrow(), tx_buf.borrow_mut(), &self) + } else { + self.srv_mut(sim) + .handle_msg(rx_msg.borrow(), tx_buf.borrow_mut()) + }; + + // Handle bad messages + let handle_msg_result = match handle_msg_result { + Ok(res) => res, + Err(e) => { + self.enqueue_upcoming_poll_event( + sim, + SE::DiscardInvalidMessage(e).into_transcript_event(self), + ); + return self.flush_upcoming_events(sim).ok(); // Just added them + } + }; + + // Successful key exchange; emit the appropriate event + if handle_msg_result.exchanged_with.is_some() { + self.enqueue_on_exchanged_events(sim)?; + } + + // Handle message responses + if let Some(len) = handle_msg_result.resp { + let resp = &tx_buf[..len]; + self.enqueue_upcoming_poll_transmission(sim, resp.to_vec(), SendMsgReason::Response); + }; + + // Return the first of the events we just enqueued + self.flush_upcoming_events(sim).ok() + } + + fn enqueue_on_exchanged_events(self, sim: &mut RosenpassSimulator) -> anyhow::Result<()> { + use ServerEvent as SE; + use TranscriptEvent as TE; + + // Retrieve the key exchanged; this function will panic if the OSK is missing + let osk = self.osk(sim).unwrap(); + + // Issue the `Exchanged` + self.enqueue_upcoming_poll_event( + sim, + SE::Exchanged(osk.clone()).into_transcript_event(self), + ); + + // Retrieve the other osk + let other_osk = match self.other().try_osk(sim) { + Some(other_osk) => other_osk, + None => return Ok(()), + }; + + // Issue the successful exchange event if the OSKs are equal; + // be careful to use constant time comparison for things like this! + if rosenpass_constant_time::memcmp(osk.secret(), other_osk.secret()) { + self.enqueue_upcoming_poll_event(sim, TE::CompletedExchange(osk)); + } + + Ok(()) + } + + fn enqueue_upcoming_poll_event(self, sim: &mut RosenpassSimulator, ev: TranscriptEvent) { + let upcoming = UpcomingPollResult::IssueEvent(ev); + self.get_mut(sim).upcoming_poll_results.push_back(upcoming); + } + + fn enqueue_upcoming_poll_transmission( + self, + sim: &mut RosenpassSimulator, + msg: Vec, + reason: SendMsgReason, + ) { + let ev = TranscriptEvent::transmit(self, msg.borrow(), reason); + let upcoming = UpcomingPollResult::SendMessage(msg, ev); + self.get_mut(sim).upcoming_poll_results.push_back(upcoming); + } + + fn try_osk(self, sim: &RosenpassSimulator) -> Option { + let peer = self.peer(sim); + let has_osk = peer.session().get(self.srv(sim)).is_some(); + + has_osk.then(|| { + // We already checked whether the OSK is present; there should be no other errors + self.osk(sim).unwrap() + }) + } + + fn osk(self, sim: &RosenpassSimulator) -> anyhow::Result { + self.srv(sim).osk(self.peer(sim)) + } + + fn drop_outgoing_packet(self, sim: &mut RosenpassSimulator) -> Option> { + let pkg = self.tx_queue_mut(sim).pop_front(); + self.enqueue_upcoming_poll_event( + sim, + ServerEvent::DroppedPackage.into_transcript_event(self), + ); + pkg + } + + fn other(self) -> Self { + match self { + Self::A => Self::B, + Self::B => Self::A, + } + } + + fn get(self, sim: &RosenpassSimulator) -> &SimulatorServer { + match self { + ServerPtr::A => sim.srv_a.borrow(), + ServerPtr::B => sim.srv_b.borrow(), + } + } + + fn get_mut(self, sim: &mut RosenpassSimulator) -> &mut SimulatorServer { + match self { + ServerPtr::A => sim.srv_a.borrow_mut(), + ServerPtr::B => sim.srv_b.borrow_mut(), + } + } + + fn srv(self, sim: &RosenpassSimulator) -> &CryptoServer { + self.get(sim).srv.borrow() + } + + fn srv_mut(self, sim: &mut RosenpassSimulator) -> &mut CryptoServer { + self.get_mut(sim).srv.borrow_mut() + } + + fn peer(self, sim: &RosenpassSimulator) -> PeerPtr { + self.get(sim).other_peer + } + + fn recv(self, sim: &mut RosenpassSimulator) -> Option> { + self.rx_queue_mut(sim).pop_front() + } + + fn transmit(self, sim: &mut RosenpassSimulator, msg: Vec) { + self.tx_queue_mut(sim).push_back(msg); + } + + fn rx_queue_mut(self, sim: &mut RosenpassSimulator) -> &mut VecDeque> { + self.get_mut(sim).rx_queue.borrow_mut() + } + + fn tx_queue_mut(self, sim: &mut RosenpassSimulator) -> &mut VecDeque> { + self.other().rx_queue_mut(sim) + } +}