mirror of
https://github.com/rosenpass/rosenpass.git
synced 2025-12-12 15:49:22 -08:00
Adds a check for empty messages as well as unit test verifying that empty messages are handled as desired.
1727 lines
54 KiB
Rust
1727 lines
54 KiB
Rust
//! Module containing the cryptographic protocol implementation
|
||
//!
|
||
//! # Overview
|
||
//!
|
||
//! The most important types in this module probably are [PollResult]
|
||
//! & [CryptoServer]. Once a [CryptoServer] is created, the server is
|
||
//! provided with new messages via the [CyptoServer::handle_msg] method.
|
||
//! The [CryptoServer::poll] method can be used to let the server work, which
|
||
//! will eventually yield a [PollResult]. Said [PollResult] contains
|
||
//! prescriptive activities to be carried out. [CryptoServer::osk] can than
|
||
//! be used to extract the shared key for two peers, once a key-exchange was
|
||
//! succesfull.
|
||
//!
|
||
//! TODO explain briefly the role of epki
|
||
//!
|
||
//! # Example Handshake
|
||
//!
|
||
//! This example illustrates a minimal setup for a key-exchange between two
|
||
//! [CryptoServer].
|
||
//!
|
||
//! ```
|
||
//! use rosenpass::{
|
||
//! pqkem::{StaticKEM, KEM},
|
||
//! protocol::{SSk, SPk, MsgBuf, PeerPtr, CryptoServer, SymKey},
|
||
//! };
|
||
//! # fn main() -> Result<(), rosenpass::RosenpassError> {
|
||
//!
|
||
//! // always init libsodium before anything
|
||
//! rosenpass::sodium::sodium_init().unwrap();
|
||
//!
|
||
//! // initialize public and private key for peer a ...
|
||
//! let (mut peer_a_sk, mut peer_a_pk) = (SSk::zero(), SPk::zero());
|
||
//! StaticKEM::keygen(peer_a_sk.secret_mut(), peer_a_pk.secret_mut())?;
|
||
//!
|
||
//! // ... and for peer b
|
||
//! let (mut peer_b_sk, mut peer_b_pk) = (SSk::zero(), SPk::zero());
|
||
//! StaticKEM::keygen(peer_b_sk.secret_mut(), peer_b_pk.secret_mut())?;
|
||
//!
|
||
//! // initialize server and a pre-shared key
|
||
//! let psk = SymKey::random();
|
||
//! let mut a = CryptoServer::new(peer_a_sk, peer_a_pk.clone());
|
||
//! let mut b = CryptoServer::new(peer_b_sk, peer_b_pk.clone());
|
||
//!
|
||
//! // introduce peers to each other
|
||
//! a.add_peer(Some(psk.clone()), peer_b_pk).unwrap();
|
||
//! b.add_peer(Some(psk), peer_a_pk).unwrap();
|
||
//!
|
||
//! // declare buffers for message exchange
|
||
//! let (mut a_buf, mut b_buf) = (MsgBuf::zero(), MsgBuf::zero());
|
||
//!
|
||
//! // let a initiate a handshake
|
||
//! let length = a.initiate_handshake(PeerPtr(0), a_buf.as_mut_slice());
|
||
//!
|
||
//! // let b respond to a and a respond to b, in two rounds
|
||
//! for _ in 0..2 {
|
||
//! b.handle_msg(&a_buf[..], &mut b_buf[..]);
|
||
//! a.handle_msg(&b_buf[..], &mut a_buf[..]);
|
||
//! }
|
||
//!
|
||
//! // all done! Extract the shared keys and ensure they are identical
|
||
//! let a_key = a.osk(PeerPtr(0));
|
||
//! let b_key = b.osk(PeerPtr(0));
|
||
//! assert_eq!(a_key.unwrap().secret(), b_key.unwrap().secret(),
|
||
//! "the key exchanged failed to establish a shared secret");
|
||
//! # Ok(())
|
||
//! # }
|
||
//! ```
|
||
|
||
use crate::{
|
||
coloring::*,
|
||
labeled_prf as lprf,
|
||
msgs::*,
|
||
pqkem::*,
|
||
prftree::{SecretPrfTree, SecretPrfTreeBranch},
|
||
sodium::*,
|
||
util::*,
|
||
};
|
||
use anyhow::{bail, ensure, Context, Result};
|
||
use std::collections::hash_map::{
|
||
Entry::{Occupied, Vacant},
|
||
HashMap,
|
||
};
|
||
|
||
// CONSTANTS & SETTINGS //////////////////////////
|
||
|
||
/// Size required to fit any message in binary form
|
||
pub const RTX_BUFFER_SIZE: usize = max_usize(
|
||
<Envelope<(), InitHello<()>> as LenseView>::LEN,
|
||
<Envelope<(), InitConf<()>> as LenseView>::LEN,
|
||
);
|
||
|
||
/// 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)
|
||
///
|
||
/// 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
|
||
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
|
||
pub const REKEY_AFTER_TIME: Timing = 120.0;
|
||
pub const REJECT_AFTER_TIME: Timing = 180.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
|
||
pub const BISCUIT_EPOCH: Timing = 300.0;
|
||
|
||
// Retransmission pub constants; will retransmit for up to _ABORT ms; starting with a delay of
|
||
// _DELAY_BEG ms and increasing the delay exponentially by a factor of
|
||
// _DELAY_GROWTH up to _DELAY_END. An .secretadditional jitter factor of ±_DELAY_JITTER
|
||
// is added.
|
||
pub const RETRANSMIT_ABORT: Timing = 120.0;
|
||
pub const RETRANSMIT_DELAY_GROWTH: Timing = 2.0;
|
||
pub const RETRANSMIT_DELAY_BEGIN: Timing = 0.5;
|
||
pub const RETRANSMIT_DELAY_END: Timing = 10.0;
|
||
pub const RETRANSMIT_DELAY_JITTER: Timing = 0.5;
|
||
|
||
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
|
||
pub fn has_happened(ev: Timing, now: Timing) -> bool {
|
||
(ev - now) < EVENT_GRACE
|
||
}
|
||
|
||
// DATA STRUCTURES & BASIC TRAITS & ACCESSORS ////
|
||
|
||
pub type SPk = Secret<{ StaticKEM::PK_LEN }>; // Just Secret<> instead of Public<> so it gets allocated on the heap
|
||
pub type SSk = Secret<{ StaticKEM::SK_LEN }>;
|
||
pub type EPk = Public<{ EphemeralKEM::PK_LEN }>;
|
||
pub type ESk = Secret<{ EphemeralKEM::SK_LEN }>;
|
||
|
||
pub type SymKey = Secret<KEY_SIZE>;
|
||
pub type SymHash = Public<KEY_SIZE>;
|
||
|
||
pub type PeerId = Public<KEY_SIZE>;
|
||
pub type SessionId = Public<SESSION_ID_LEN>;
|
||
pub type BiscuitId = Public<BISCUIT_ID_LEN>;
|
||
|
||
pub type XAEADNonce = Public<XAEAD_NONCE_LEN>;
|
||
|
||
pub type MsgBuf = Public<MAX_MESSAGE_LEN>;
|
||
|
||
pub type PeerNo = usize;
|
||
|
||
/// Implementation of the cryptographic protocol
|
||
///
|
||
/// The scope of this is:
|
||
///
|
||
/// - logical protocol flow
|
||
/// - timeout handling
|
||
/// - key exchange
|
||
///
|
||
/// Not in scope of this struct:
|
||
///
|
||
/// - handling of external IO (like sockets etc.)
|
||
#[derive(Debug)]
|
||
pub struct CryptoServer {
|
||
pub timebase: Timebase,
|
||
|
||
// Server Crypto
|
||
pub sskm: SSk,
|
||
pub spkm: SPk,
|
||
pub biscuit_ctr: BiscuitId,
|
||
pub biscuit_keys: [BiscuitKey; 2],
|
||
|
||
// Peer/Handshake DB
|
||
pub peers: Vec<Peer>,
|
||
pub index: HashMap<IndexKey, PeerNo>,
|
||
|
||
// Tick handling
|
||
pub peer_poll_off: usize,
|
||
}
|
||
|
||
/// 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 Respnder. Thus secrecy of the Responder state is not violated, still
|
||
/// the responder can avoid storing this state.
|
||
#[derive(Debug)]
|
||
pub struct BiscuitKey {
|
||
pub created_at: Timing,
|
||
pub key: SymKey,
|
||
}
|
||
|
||
#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||
pub enum IndexKey {
|
||
Peer(PeerId),
|
||
Sid(SessionId),
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct Peer {
|
||
pub psk: SymKey,
|
||
pub spkt: SPk,
|
||
pub biscuit_used: BiscuitId,
|
||
pub session: Option<Session>,
|
||
pub handshake: Option<InitiatorHandshake>,
|
||
pub initiation_requested: bool,
|
||
}
|
||
|
||
impl Peer {
|
||
pub fn zero() -> Self {
|
||
Self {
|
||
psk: SymKey::zero(),
|
||
spkt: SPk::zero(),
|
||
biscuit_used: BiscuitId::zero(),
|
||
session: None,
|
||
initiation_requested: false,
|
||
handshake: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct HandshakeState {
|
||
/// Session ID of Initiator
|
||
pub sidi: SessionId,
|
||
/// Session ID of Responder
|
||
pub sidr: SessionId,
|
||
/// Chaining Key
|
||
pub ck: SecretPrfTreeBranch,
|
||
}
|
||
|
||
#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||
pub enum HandshakeRole {
|
||
Initiator,
|
||
Responder,
|
||
}
|
||
|
||
impl HandshakeRole {
|
||
pub fn is_initiator(&self) -> bool {
|
||
match *self {
|
||
HandshakeRole::Initiator => true,
|
||
HandshakeRole::Responder => false,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||
pub enum HandshakeStateMachine {
|
||
RespHello,
|
||
RespConf,
|
||
}
|
||
|
||
impl Default for HandshakeStateMachine {
|
||
fn default() -> Self {
|
||
HandshakeStateMachine::RespHello
|
||
}
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct InitiatorHandshake {
|
||
pub created_at: Timing,
|
||
pub next: HandshakeStateMachine,
|
||
pub core: HandshakeState,
|
||
/// Ephemeral Secret Key of Initiator
|
||
pub eski: ESk,
|
||
/// Ephemeral Public Key of Initiator
|
||
pub epki: EPk,
|
||
|
||
// Retransmission
|
||
// TODO: Ensure that this is correct by typing
|
||
pub tx_at: Timing,
|
||
pub tx_retry_at: Timing,
|
||
pub tx_count: usize,
|
||
pub tx_len: usize,
|
||
pub tx_buf: MsgBuf,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct Session {
|
||
// Metadata
|
||
pub created_at: Timing,
|
||
pub sidm: SessionId,
|
||
pub sidt: SessionId,
|
||
// Crypto
|
||
pub ck: SecretPrfTreeBranch,
|
||
/// Key for Transmission ("transmission key mine")
|
||
pub txkm: SymKey,
|
||
/// Key for Receival ("transmission key theirs")
|
||
pub txkt: SymKey,
|
||
/// Nonce for Transmission ("transmission nonce mine")
|
||
pub txnm: u64,
|
||
/// Nonce for Receival ("transmission nonce theirs")
|
||
pub txnt: u64,
|
||
}
|
||
|
||
/// Lifecycle of a Secret
|
||
///
|
||
/// The order implies the readiness for usage of a secret, the highes/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
|
||
Void = 0,
|
||
/// Secret must be zeroized, disposal adviced
|
||
Dead,
|
||
/// Soon to be dead: the secret might be used for receiving
|
||
/// data, but must not be used for future sending
|
||
Retired,
|
||
/// The secret might be used unconditionally
|
||
Young,
|
||
}
|
||
|
||
/// Implemented for information (secret and public) that has an expire date
|
||
trait Mortal {
|
||
/// Time of creation, when [Lifecycle::Void] -> [Lifecycle::Young]
|
||
fn created_at(&self, srv: &CryptoServer) -> Option<Timing>;
|
||
/// The time where [Lifecycle::Young] -> [Lifecycle::Retired]
|
||
fn retire_at(&self, srv: &CryptoServer) -> Option<Timing>;
|
||
/// The time where [Lifecycle::Retired] -> [Lifecycle::Dead]
|
||
fn die_at(&self, srv: &CryptoServer) -> Option<Timing>;
|
||
}
|
||
|
||
// BUSINESS LOGIC DATA STRUCTURES ////////////////
|
||
|
||
/// Valid index to [Server::peers]
|
||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||
pub struct PeerPtr(pub usize);
|
||
|
||
/// Valid index to [Server::peers]
|
||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||
pub struct IniHsPtr(pub usize);
|
||
|
||
/// Valid index to [Server::peers]
|
||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||
pub struct SessionPtr(pub usize);
|
||
|
||
/// Valid index to [Server::biscuit_keys]
|
||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||
pub struct BiscuitKeyPtr(pub usize);
|
||
|
||
impl PeerPtr {
|
||
pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a Peer {
|
||
&srv.peers[self.0]
|
||
}
|
||
|
||
pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut Peer {
|
||
&mut srv.peers[self.0]
|
||
}
|
||
|
||
pub fn session(&self) -> SessionPtr {
|
||
SessionPtr(self.0)
|
||
}
|
||
|
||
pub fn hs(&self) -> IniHsPtr {
|
||
IniHsPtr(self.0)
|
||
}
|
||
}
|
||
|
||
impl IniHsPtr {
|
||
pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a Option<InitiatorHandshake> {
|
||
&srv.peers[self.0].handshake
|
||
}
|
||
|
||
pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut Option<InitiatorHandshake> {
|
||
&mut srv.peers[self.0].handshake
|
||
}
|
||
|
||
pub fn peer(&self) -> PeerPtr {
|
||
PeerPtr(self.0)
|
||
}
|
||
|
||
pub fn insert<'a>(
|
||
&self,
|
||
srv: &'a mut CryptoServer,
|
||
hs: InitiatorHandshake,
|
||
) -> Result<&'a mut InitiatorHandshake> {
|
||
srv.register_session(hs.core.sidi, self.peer())?;
|
||
self.take(srv);
|
||
self.peer().get_mut(srv).initiation_requested = false;
|
||
Ok(self.peer().get_mut(srv).handshake.insert(hs))
|
||
}
|
||
|
||
pub fn take(&self, srv: &mut CryptoServer) -> Option<InitiatorHandshake> {
|
||
let r = self.peer().get_mut(srv).handshake.take();
|
||
if let Some(ref stale) = r {
|
||
srv.unregister_session_if_vacant(stale.core.sidi, self.peer());
|
||
}
|
||
r
|
||
}
|
||
}
|
||
|
||
impl SessionPtr {
|
||
pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a Option<Session> {
|
||
&srv.peers[self.0].session
|
||
}
|
||
|
||
pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut Option<Session> {
|
||
&mut srv.peers[self.0].session
|
||
}
|
||
|
||
pub fn peer(&self) -> PeerPtr {
|
||
PeerPtr(self.0)
|
||
}
|
||
|
||
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))
|
||
}
|
||
|
||
pub fn take(&self, srv: &mut CryptoServer) -> Option<Session> {
|
||
let r = self.peer().get_mut(srv).session.take();
|
||
if let Some(ref stale) = r {
|
||
srv.unregister_session_if_vacant(stale.sidm, self.peer());
|
||
}
|
||
r
|
||
}
|
||
}
|
||
|
||
impl BiscuitKeyPtr {
|
||
pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a BiscuitKey {
|
||
&srv.biscuit_keys[self.0]
|
||
}
|
||
|
||
pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut BiscuitKey {
|
||
&mut srv.biscuit_keys[self.0]
|
||
}
|
||
}
|
||
|
||
// DATABASE //////////////////////////////////////
|
||
|
||
impl CryptoServer {
|
||
/// Initiate a new [Server] based on a secret key (`sk`) and a public key
|
||
/// (`pk`)
|
||
pub fn new(sk: SSk, pk: SPk) -> CryptoServer {
|
||
let tb = Timebase::default();
|
||
CryptoServer {
|
||
sskm: sk,
|
||
spkm: pk,
|
||
|
||
// Defaults
|
||
timebase: tb,
|
||
biscuit_ctr: BiscuitId::new([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), // 1, LSB
|
||
biscuit_keys: [BiscuitKey::new(), BiscuitKey::new()],
|
||
peers: Vec::new(),
|
||
index: HashMap::new(),
|
||
peer_poll_off: 0,
|
||
}
|
||
}
|
||
|
||
/// Iterate over the many (2) biscuit keys
|
||
pub fn biscuit_key_ptrs(&self) -> impl Iterator<Item = BiscuitKeyPtr> {
|
||
(0..self.biscuit_keys.len()).map(BiscuitKeyPtr)
|
||
}
|
||
|
||
#[rustfmt::skip]
|
||
pub fn pidm(&self) -> Result<PeerId> {
|
||
Ok(Public::new(
|
||
lprf::peerid()?
|
||
.mix(self.spkm.secret())?
|
||
.into_value()))
|
||
}
|
||
|
||
/// Iterate over all peers, starting with the `n`th peer, wrapping at the
|
||
/// end of the peers vec so that also all peers from index 0 to `n - 1` are
|
||
/// yielded
|
||
pub fn peer_ptrs_off(&self, n: usize) -> impl Iterator<Item = PeerPtr> {
|
||
let l = self.peers.len();
|
||
(0..l).map(move |i| PeerPtr((i + n) % l))
|
||
}
|
||
|
||
/// Add a peer with an optional pre shared key (`psk`) and its public key (`pk`)
|
||
pub fn add_peer(&mut self, psk: Option<SymKey>, pk: SPk) -> Result<PeerPtr> {
|
||
let peer = Peer {
|
||
psk: psk.unwrap_or_else(SymKey::zero),
|
||
spkt: pk,
|
||
biscuit_used: BiscuitId::zero(),
|
||
session: None,
|
||
handshake: None,
|
||
initiation_requested: false,
|
||
};
|
||
let peerid = peer.pidt()?;
|
||
let peerno = self.peers.len();
|
||
match self.index.entry(IndexKey::Peer(peerid)) {
|
||
Occupied(_) => bail!(
|
||
"Cannot insert peer with id {:?}; peer with this id already registered.",
|
||
peerid
|
||
),
|
||
Vacant(e) => e.insert(peerno),
|
||
};
|
||
self.peers.push(peer);
|
||
Ok(PeerPtr(peerno))
|
||
}
|
||
|
||
/// Register a new session (during a sucesfull handshake, persisting longer
|
||
/// than the handshake). Might return an error on session id collision
|
||
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
|
||
Occupied(_) => bail!("Cannot insert session with id {:?}; id is in use.", id),
|
||
Vacant(e) => {
|
||
e.insert(peer.0);
|
||
}
|
||
};
|
||
Ok(())
|
||
}
|
||
|
||
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
|
||
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 */
|
||
(_, Some(hs)) if hs.core.sidi == id => {} /* nop */
|
||
_ => self.unregister_session(id),
|
||
};
|
||
}
|
||
|
||
pub fn find_peer(&self, id: PeerId) -> Option<PeerPtr> {
|
||
self.index.get(&IndexKey::Peer(id)).map(|no| PeerPtr(*no))
|
||
}
|
||
|
||
// lookup_session in whitepaper
|
||
pub fn lookup_handshake(&self, id: SessionId) -> Option<IniHsPtr> {
|
||
self.index
|
||
.get(&IndexKey::Sid(id)) // lookup the session in the index
|
||
.map(|no| IniHsPtr(*no)) // convert to peer pointer
|
||
.filter(|hsptr| {
|
||
hsptr
|
||
.get(self) // lookup in the server
|
||
.as_ref()
|
||
.map(|hs| hs.core.sidi == id) // check that handshake id matches as well
|
||
.unwrap_or(false) // it didn't match?!
|
||
})
|
||
}
|
||
|
||
// also lookup_session in the whitepaper
|
||
pub fn lookup_session(&self, id: SessionId) -> Option<SessionPtr> {
|
||
self.index
|
||
.get(&IndexKey::Sid(id))
|
||
.map(|no| SessionPtr(*no))
|
||
.filter(|sptr| {
|
||
sptr.get(self)
|
||
.as_ref()
|
||
.map(|ses| ses.sidm == id)
|
||
.unwrap_or(false)
|
||
})
|
||
}
|
||
|
||
/// 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));
|
||
let (t, u) = (a.get(self).created_at, b.get(self).created_at);
|
||
|
||
// Return the youngest but only if it's youthful
|
||
// first being returned in case of a tie
|
||
let r = if t >= u { a } else { b };
|
||
if r.lifecycle(self) == Lifecycle::Young {
|
||
return r;
|
||
}
|
||
|
||
// Reap the oldest biscuit key and spawn a new young one otherwise
|
||
// last one being reaped in case of a tie
|
||
let r = if t < u { a } else { b };
|
||
let tb = self.timebase.clone();
|
||
r.get_mut(self).randomize(&tb);
|
||
r
|
||
}
|
||
}
|
||
|
||
impl Peer {
|
||
pub fn new(psk: SymKey, pk: SPk) -> Peer {
|
||
Peer {
|
||
psk,
|
||
spkt: pk,
|
||
biscuit_used: BiscuitId::zero(),
|
||
session: None,
|
||
handshake: None,
|
||
initiation_requested: false,
|
||
}
|
||
}
|
||
|
||
#[rustfmt::skip]
|
||
pub fn pidt(&self) -> Result<PeerId> {
|
||
Ok(Public::new(
|
||
lprf::peerid()?
|
||
.mix(self.spkt.secret())?
|
||
.into_value()))
|
||
}
|
||
}
|
||
|
||
impl Session {
|
||
pub fn zero() -> Self {
|
||
Self {
|
||
created_at: 0.0,
|
||
sidm: SessionId::zero(),
|
||
sidt: SessionId::zero(),
|
||
ck: SecretPrfTree::zero().dup(),
|
||
txkm: SymKey::zero(),
|
||
txkt: SymKey::zero(),
|
||
txnm: 0,
|
||
txnt: 0,
|
||
}
|
||
}
|
||
}
|
||
|
||
// BISCUIT KEY ///////////////////////////////////
|
||
|
||
/// Biscuit Keys are always randomized, so that even if through a bug some
|
||
/// secrete is encrypted with an unitialized [BiscuitKey], nobody instead of
|
||
/// everybody may read the secret.
|
||
impl BiscuitKey {
|
||
// new creates a random value, that might be counterintuitive for a Default
|
||
// impl
|
||
#[allow(clippy::new_without_default)]
|
||
pub fn new() -> Self {
|
||
Self {
|
||
created_at: BCE,
|
||
key: SymKey::random(),
|
||
}
|
||
}
|
||
|
||
pub fn erase(&mut self) {
|
||
self.key.randomize();
|
||
self.created_at = BCE;
|
||
}
|
||
|
||
pub fn randomize(&mut self, tb: &Timebase) {
|
||
self.key.randomize();
|
||
self.created_at = tb.now();
|
||
}
|
||
}
|
||
|
||
// LIFECYCLE MANAGEMENT //////////////////////////
|
||
|
||
impl Mortal for IniHsPtr {
|
||
fn created_at(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
self.get(srv).as_ref().map(|hs| hs.created_at)
|
||
}
|
||
|
||
fn retire_at(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
self.die_at(srv)
|
||
}
|
||
|
||
fn die_at(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
self.created_at(srv).map(|t| t + REJECT_AFTER_TIME)
|
||
}
|
||
}
|
||
|
||
impl Mortal for SessionPtr {
|
||
fn created_at(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
self.get(srv).as_ref().map(|p| p.created_at)
|
||
}
|
||
|
||
fn retire_at(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
self.created_at(srv).map(|t| t + REKEY_AFTER_TIME)
|
||
}
|
||
|
||
fn die_at(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
self.created_at(srv).map(|t| t + REJECT_AFTER_TIME)
|
||
}
|
||
}
|
||
|
||
impl Mortal for BiscuitKeyPtr {
|
||
fn created_at(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
let t = self.get(srv).created_at;
|
||
if t < 0.0 {
|
||
None
|
||
} else {
|
||
Some(t)
|
||
}
|
||
}
|
||
|
||
fn retire_at(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
self.created_at(srv).map(|t| t + BISCUIT_EPOCH)
|
||
}
|
||
|
||
fn die_at(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
self.retire_at(srv).map(|t| t + BISCUIT_EPOCH)
|
||
}
|
||
}
|
||
|
||
/// Trait extension to the [Mortal] Trait, that enables nicer access to timing
|
||
/// information
|
||
trait MortalExt: Mortal {
|
||
fn life_left(&self, srv: &CryptoServer) -> Option<Timing>;
|
||
fn youth_left(&self, srv: &CryptoServer) -> Option<Timing>;
|
||
fn lifecycle(&self, srv: &CryptoServer) -> Lifecycle;
|
||
}
|
||
|
||
impl<T: Mortal> MortalExt for T {
|
||
fn life_left(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
self.die_at(srv).map(|t| t - srv.timebase.now())
|
||
}
|
||
|
||
fn youth_left(&self, srv: &CryptoServer) -> Option<Timing> {
|
||
self.retire_at(srv).map(|t| t - srv.timebase.now())
|
||
}
|
||
|
||
fn lifecycle(&self, srv: &CryptoServer) -> Lifecycle {
|
||
match (self.youth_left(srv), self.life_left(srv)) {
|
||
(_, Some(t)) if has_happened(t, 0.0) => Lifecycle::Dead,
|
||
(Some(t), _) if has_happened(t, 0.0) => Lifecycle::Retired,
|
||
(Some(_), Some(_)) => Lifecycle::Young,
|
||
_ => Lifecycle::Void,
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 unecessary copying between global tx_buf and per-peer buf
|
||
// TODO move retransmission storage to io server
|
||
pub fn initiate_handshake(&mut self, peer: PeerPtr, tx_buf: &mut [u8]) -> Result<usize> {
|
||
let mut msg = tx_buf.envelope::<InitHello<()>>()?; // Envelope::<InitHello>::default(); // TODO
|
||
self.handle_initiation(peer, msg.payload_mut().init_hello()?)?;
|
||
let len = self.seal_and_commit_msg(peer, MsgType::InitHello, msg)?;
|
||
peer.hs()
|
||
.store_msg_for_retransmission(self, &tx_buf[..len])?;
|
||
Ok(len)
|
||
}
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct HandleMsgResult {
|
||
pub exchanged_with: Option<PeerPtr>,
|
||
pub resp: Option<usize>,
|
||
}
|
||
|
||
impl CryptoServer {
|
||
/// Repsond to an incoming message
|
||
///
|
||
/// # Overview
|
||
///
|
||
/// The response is only dependent on the incoming message, thus this
|
||
/// function is called regardless of whether the the the calling context is
|
||
/// an iniator, a responder or both. The flow of is as follows:
|
||
///
|
||
/// 1. check incoming message for valid [MsgType]
|
||
/// 2. check that the seal is intact, e.g. that the message is
|
||
/// authenticated
|
||
/// 3. call the respective handler function for this message (for example
|
||
/// [Self::handle_init_hello])
|
||
/// 4. if the protocol foresees a response to this message, generate one
|
||
/// 5. seal the response with cryptographic authentication
|
||
/// 6. if the response is a ResponseHello, store the sealed response for
|
||
/// further retransmision
|
||
/// 7. return some peerpointer if the exchange completed with this message
|
||
/// 8. return the length of the response generated
|
||
///
|
||
/// This is the sequence of a succesful handshake:
|
||
///
|
||
/// | time | Initiator | direction | Responder |
|
||
/// | --- | ---: | :---: | :--- |
|
||
/// | t0 | `InitHello` | -> | |
|
||
/// | t1 | | <- | `RespHello` |
|
||
/// | t2 | `InitConf` | -> | |
|
||
/// | t3 | | <- | `EmptyData` |
|
||
pub fn handle_msg(&mut self, rx_buf: &[u8], tx_buf: &mut [u8]) -> Result<HandleMsgResult> {
|
||
let seal_broken = "Message seal broken!";
|
||
// lengt of the response. We assume no response, so None for now
|
||
let mut len = 0;
|
||
let mut exchanged = false;
|
||
|
||
ensure!(!rx_buf.is_empty(), "received empty message, ignoring it");
|
||
|
||
let peer = match rx_buf[0].try_into() {
|
||
Ok(MsgType::InitHello) => {
|
||
let msg_in = rx_buf.envelope::<InitHello<&[u8]>>()?;
|
||
ensure!(msg_in.check_seal(self)?, seal_broken);
|
||
|
||
let mut msg_out = tx_buf.envelope::<RespHello<&mut [u8]>>()?;
|
||
let peer = self.handle_init_hello(
|
||
msg_in.payload().init_hello()?,
|
||
msg_out.payload_mut().resp_hello()?,
|
||
)?;
|
||
len = self.seal_and_commit_msg(peer, MsgType::RespHello, msg_out)?;
|
||
peer
|
||
}
|
||
Ok(MsgType::RespHello) => {
|
||
let msg_in = rx_buf.envelope::<RespHello<&[u8]>>()?;
|
||
ensure!(msg_in.check_seal(self)?, seal_broken);
|
||
|
||
let mut msg_out = tx_buf.envelope::<InitConf<&mut [u8]>>()?;
|
||
let peer = self.handle_resp_hello(
|
||
msg_in.payload().resp_hello()?,
|
||
msg_out.payload_mut().init_conf()?,
|
||
)?;
|
||
len = self.seal_and_commit_msg(peer, MsgType::InitConf, msg_out)?;
|
||
peer.hs()
|
||
.store_msg_for_retransmission(self, &tx_buf[..len])?;
|
||
exchanged = true;
|
||
peer
|
||
}
|
||
Ok(MsgType::InitConf) => {
|
||
let msg_in = rx_buf.envelope::<InitConf<&[u8]>>()?;
|
||
ensure!(msg_in.check_seal(self)?, seal_broken);
|
||
|
||
let mut msg_out = tx_buf.envelope::<EmptyData<&mut [u8]>>()?;
|
||
let peer = self.handle_init_conf(
|
||
msg_in.payload().init_conf()?,
|
||
msg_out.payload_mut().empty_data()?,
|
||
)?;
|
||
len = self.seal_and_commit_msg(peer, MsgType::EmptyData, msg_out)?;
|
||
exchanged = true;
|
||
peer
|
||
}
|
||
Ok(MsgType::EmptyData) => {
|
||
let msg_in = rx_buf.envelope::<EmptyData<&[u8]>>()?;
|
||
ensure!(msg_in.check_seal(self)?, seal_broken);
|
||
|
||
self.handle_resp_conf(msg_in.payload().empty_data()?)?
|
||
}
|
||
Ok(MsgType::DataMsg) => bail!("DataMsg handling not implemented!"),
|
||
Ok(MsgType::CookieReply) => bail!("CookieReply handling not implemented!"),
|
||
Err(_) => {
|
||
bail!("CookieReply handling not implemented!")
|
||
}
|
||
};
|
||
|
||
Ok(HandleMsgResult {
|
||
exchanged_with: exchanged.then_some(peer),
|
||
resp: if len == 0 { None } else { Some(len) },
|
||
})
|
||
}
|
||
|
||
/// Serialize message to `tx_buf`, generating the `mac` in the process of
|
||
/// doing so
|
||
///
|
||
/// The message type is explicitly required here because it is very easy to
|
||
/// forget setting that, which creates subtle but far ranging errors.
|
||
pub fn seal_and_commit_msg<M: LenseView>(
|
||
&mut self,
|
||
peer: PeerPtr,
|
||
msg_type: MsgType,
|
||
mut msg: Envelope<&mut [u8], M>,
|
||
) -> Result<usize> {
|
||
msg.msg_type_mut()[0] = msg_type as u8;
|
||
msg.seal(peer, self)?;
|
||
Ok(<Envelope<(), M> as LenseView>::LEN)
|
||
}
|
||
}
|
||
|
||
// EVENT POLLING /////////////////////////////////
|
||
|
||
#[derive(Debug, Copy, Clone)]
|
||
pub struct Wait(Timing);
|
||
|
||
impl Wait {
|
||
fn immediate() -> Self {
|
||
Wait(0.0)
|
||
}
|
||
|
||
fn hibernate() -> Self {
|
||
Wait(UNENDING)
|
||
}
|
||
|
||
fn immediate_unless(cond: bool) -> Self {
|
||
if cond {
|
||
Self::hibernate()
|
||
} else {
|
||
Self::immediate()
|
||
}
|
||
}
|
||
|
||
fn or_hibernate(t: Option<Timing>) -> Self {
|
||
match t {
|
||
Some(u) => Wait(u),
|
||
None => Wait::hibernate(),
|
||
}
|
||
}
|
||
|
||
fn or_immediate(t: Option<Timing>) -> Self {
|
||
match t {
|
||
Some(u) => Wait(u),
|
||
None => Wait::immediate(),
|
||
}
|
||
}
|
||
|
||
fn and<T: Into<Wait>>(&self, o: T) -> Self {
|
||
let (a, b) = (self.0, o.into().0);
|
||
Wait(if a > b { a } else { b })
|
||
}
|
||
}
|
||
|
||
impl From<Timing> for Wait {
|
||
fn from(t: Timing) -> Wait {
|
||
Wait(t)
|
||
}
|
||
}
|
||
|
||
impl From<Option<Timing>> for Wait {
|
||
fn from(t: Option<Timing>) -> Wait {
|
||
Wait::or_hibernate(t)
|
||
}
|
||
}
|
||
|
||
/// Result of a poll operation, containing prescriptive action for the outer
|
||
/// event loop
|
||
#[derive(Debug, Copy, Clone)]
|
||
pub enum PollResult {
|
||
Sleep(Timing),
|
||
DeleteKey(PeerPtr),
|
||
SendInitiation(PeerPtr),
|
||
SendRetransmission(PeerPtr),
|
||
}
|
||
|
||
impl Default for PollResult {
|
||
fn default() -> Self {
|
||
Self::hibernate()
|
||
}
|
||
}
|
||
|
||
impl PollResult {
|
||
pub fn hibernate() -> Self {
|
||
Self::Sleep(UNENDING) // Avoid excessive sleep times (might trigger bugs on some platforms)
|
||
}
|
||
|
||
pub fn peer(&self) -> Option<PeerPtr> {
|
||
use PollResult::*;
|
||
match *self {
|
||
DeleteKey(p) | SendInitiation(p) | SendRetransmission(p) => Some(p),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
pub fn fold(&self, otr: PollResult) -> PollResult {
|
||
use PollResult::*;
|
||
match (*self, otr) {
|
||
(Sleep(a), Sleep(b)) => Sleep(f64::min(a, b)),
|
||
(a, Sleep(_b)) if a.saturated() => a,
|
||
(Sleep(_a), b) if b.saturated() => b,
|
||
_ => panic!(
|
||
"Do not fold two saturated poll results! It doesn't make sense; \
|
||
we would have to discard one of the events. \
|
||
As soon as some result that requires an action (i.e. something other than sleep \
|
||
is reached you should just return and have the API consumer poll again."
|
||
),
|
||
}
|
||
}
|
||
|
||
pub fn try_fold_with<F: FnOnce() -> Result<PollResult>>(&self, f: F) -> Result<PollResult> {
|
||
if self.saturated() {
|
||
Ok(*self)
|
||
} else {
|
||
Ok(self.fold(f()?))
|
||
}
|
||
}
|
||
|
||
pub fn poll_child<P: Pollable>(&self, srv: &mut CryptoServer, p: &P) -> Result<PollResult> {
|
||
self.try_fold_with(|| p.poll(srv))
|
||
}
|
||
|
||
pub fn poll_children<P, I>(&self, srv: &mut CryptoServer, iter: I) -> Result<PollResult>
|
||
where
|
||
P: Pollable,
|
||
I: Iterator<Item = P>,
|
||
{
|
||
let mut acc = *self;
|
||
for e in iter {
|
||
if acc.saturated() {
|
||
break;
|
||
}
|
||
acc = acc.fold(e.poll(srv)?);
|
||
}
|
||
Ok(acc)
|
||
}
|
||
|
||
/// Execute `f` if it is ready, as indicated by `wait`
|
||
pub fn sched<W: Into<Wait>, F: FnOnce() -> PollResult>(&self, wait: W, f: F) -> PollResult {
|
||
let wait = wait.into().0;
|
||
if self.saturated() {
|
||
*self
|
||
} else if has_happened(wait, 0.0) {
|
||
self.fold(f())
|
||
} else {
|
||
self.fold(Self::Sleep(wait))
|
||
}
|
||
}
|
||
|
||
pub fn try_sched<W: Into<Wait>, F: FnOnce() -> Result<PollResult>>(
|
||
&self,
|
||
wait: W,
|
||
f: F,
|
||
) -> Result<PollResult> {
|
||
let wait = wait.into().0;
|
||
if self.saturated() {
|
||
Ok(*self)
|
||
} else if has_happened(wait, 0.0) {
|
||
Ok(self.fold(f()?))
|
||
} else {
|
||
Ok(self.fold(Self::Sleep(wait)))
|
||
}
|
||
}
|
||
|
||
pub fn ok(&self) -> Result<PollResult> {
|
||
Ok(*self)
|
||
}
|
||
|
||
pub fn saturated(&self) -> bool {
|
||
use PollResult::*;
|
||
!matches!(self, Sleep(_))
|
||
}
|
||
}
|
||
|
||
pub fn begin_poll() -> PollResult {
|
||
PollResult::default()
|
||
}
|
||
|
||
/// Takes a closure `f`, returns another closure which internally calls f and
|
||
/// then returns a default [PollResult]
|
||
pub fn void_poll<T, F: FnOnce() -> T>(f: F) -> impl FnOnce() -> PollResult {
|
||
|| {
|
||
f();
|
||
PollResult::default()
|
||
}
|
||
}
|
||
|
||
pub trait Pollable {
|
||
fn poll(&self, srv: &mut CryptoServer) -> Result<PollResult>;
|
||
}
|
||
|
||
impl CryptoServer {
|
||
/// Implements something like [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.
|
||
pub fn poll(&mut self) -> Result<PollResult> {
|
||
let r = begin_poll() // Poll each biscuit and peer until an event is found
|
||
.poll_children(self, self.biscuit_key_ptrs())?
|
||
.poll_children(self, self.peer_ptrs_off(self.peer_poll_off))?;
|
||
self.peer_poll_off = match r.peer() {
|
||
Some(p) => p.0 + 1, // Event found while polling peer p; will poll peer p+1 next
|
||
None => 0, // No peer ev found. Resetting to 0 out of an irrational fear of non-zero numbers
|
||
};
|
||
r.ok()
|
||
}
|
||
}
|
||
|
||
impl Pollable for BiscuitKeyPtr {
|
||
fn poll(&self, srv: &mut CryptoServer) -> Result<PollResult> {
|
||
begin_poll()
|
||
.sched(self.life_left(srv), void_poll(|| self.get_mut(srv).erase())) // Erase stale biscuits
|
||
.ok()
|
||
}
|
||
}
|
||
|
||
impl Pollable for PeerPtr {
|
||
fn poll(&self, srv: &mut CryptoServer) -> Result<PollResult> {
|
||
let (ses, hs) = (self.session(), self.hs());
|
||
begin_poll()
|
||
.sched(hs.life_left(srv), void_poll(|| hs.take(srv))) // Silently erase old handshakes
|
||
.sched(ses.life_left(srv), || {
|
||
// Erase old sessions
|
||
ses.take(srv);
|
||
PollResult::DeleteKey(*self)
|
||
})
|
||
// Initialize the handshake
|
||
// IF if initiation hasn't been requested (consumer of the API is free to
|
||
// ignore the request hence there is a need to do record keeping on that)
|
||
// AND after the existing session becomes stale or if there is session at all
|
||
// AND after the current handshake becomes stale or there is no handshake at all
|
||
.sched(
|
||
Wait::immediate_unless(self.get(srv).initiation_requested)
|
||
.and(Wait::or_immediate(ses.youth_left(srv)))
|
||
.and(Wait::or_immediate(hs.youth_left(srv))),
|
||
|| {
|
||
self.get_mut(srv).initiation_requested = true;
|
||
PollResult::SendInitiation(*self)
|
||
},
|
||
)
|
||
.poll_child(srv, &hs) // Defer to the handshake for polling (retransmissions)
|
||
}
|
||
}
|
||
|
||
impl Pollable for IniHsPtr {
|
||
fn poll(&self, srv: &mut CryptoServer) -> Result<PollResult> {
|
||
begin_poll().try_sched(self.retransmission_in(srv), || {
|
||
// Registering retransmission even if app does not retransmit.
|
||
// This explicitly permits applications to ignore the event.
|
||
self.register_retransmission(srv)?;
|
||
Ok(PollResult::SendRetransmission(self.peer()))
|
||
})
|
||
}
|
||
}
|
||
|
||
// MESSAGE RETRANSMISSION ////////////////////////
|
||
|
||
impl CryptoServer {
|
||
pub fn retransmit_handshake(&mut self, peer: PeerPtr, tx_buf: &mut [u8]) -> Result<usize> {
|
||
peer.hs().apply_retransmission(self, tx_buf)
|
||
}
|
||
}
|
||
|
||
impl IniHsPtr {
|
||
pub fn store_msg_for_retransmission(&self, srv: &mut CryptoServer, msg: &[u8]) -> Result<()> {
|
||
let ih = self
|
||
.get_mut(srv)
|
||
.as_mut()
|
||
.with_context(|| format!("No current handshake for peer {:?}", self.peer()))?;
|
||
cpy_min(msg, &mut *ih.tx_buf);
|
||
ih.tx_count = 0;
|
||
ih.tx_len = msg.len();
|
||
self.register_retransmission(srv)?;
|
||
Ok(())
|
||
}
|
||
|
||
pub fn apply_retransmission(&self, srv: &mut CryptoServer, tx_buf: &mut [u8]) -> Result<usize> {
|
||
let ih = self
|
||
.get_mut(srv)
|
||
.as_mut()
|
||
.with_context(|| format!("No current handshake for peer {:?}", self.peer()))?;
|
||
cpy_min(&ih.tx_buf[..ih.tx_len], tx_buf);
|
||
Ok(ih.tx_len)
|
||
}
|
||
|
||
pub fn register_retransmission(&self, srv: &mut CryptoServer) -> Result<()> {
|
||
let tb = srv.timebase.clone();
|
||
let ih = self
|
||
.get_mut(srv)
|
||
.as_mut()
|
||
.with_context(|| format!("No current handshake for peer {:?}", self.peer()))?;
|
||
// Base delay, exponential increase, ±50% jitter
|
||
ih.tx_retry_at = tb.now()
|
||
+ RETRANSMIT_DELAY_BEGIN
|
||
* RETRANSMIT_DELAY_GROWTH.powf(
|
||
(RETRANSMIT_DELAY_END / RETRANSMIT_DELAY_BEGIN)
|
||
.log(RETRANSMIT_DELAY_GROWTH)
|
||
.min(ih.tx_count as f64),
|
||
)
|
||
* RETRANSMIT_DELAY_JITTER
|
||
* (rand_f64() + 1.0);
|
||
ih.tx_count += 1;
|
||
Ok(())
|
||
}
|
||
|
||
pub fn retransmission_in(&self, srv: &mut CryptoServer) -> Option<Timing> {
|
||
self.get(srv)
|
||
.as_ref()
|
||
.map(|hs| hs.tx_retry_at - srv.timebase.now())
|
||
}
|
||
}
|
||
|
||
// CRYPTO/HANDSHAKE HANDLING /////////////////////
|
||
|
||
impl<M> Envelope<&mut [u8], M>
|
||
where
|
||
M: LenseView,
|
||
{
|
||
/// Calculate the message authentication code (`mac`)
|
||
pub fn seal(&mut self, peer: PeerPtr, srv: &CryptoServer) -> Result<()> {
|
||
let mac = lprf::mac()?
|
||
.mix(peer.get(srv).spkt.secret())?
|
||
.mix(self.until_mac())?;
|
||
self.mac_mut()
|
||
.copy_from_slice(mac.into_value()[..16].as_ref());
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl<M> Envelope<&[u8], M>
|
||
where
|
||
M: LenseView,
|
||
{
|
||
/// Check the message authentication code
|
||
pub fn check_seal(&self, srv: &CryptoServer) -> Result<bool> {
|
||
let expected = lprf::mac()?.mix(srv.spkm.secret())?.mix(self.until_mac())?;
|
||
Ok(sodium_memcmp(self.mac(), &expected.into_value()[..16]))
|
||
}
|
||
}
|
||
|
||
impl InitiatorHandshake {
|
||
pub fn zero_with_timestamp(srv: &CryptoServer) -> Self {
|
||
InitiatorHandshake {
|
||
created_at: srv.timebase.now(),
|
||
next: HandshakeStateMachine::RespHello,
|
||
core: HandshakeState::zero(),
|
||
eski: ESk::zero(),
|
||
epki: EPk::zero(),
|
||
tx_at: 0.0,
|
||
tx_retry_at: 0.0,
|
||
tx_count: 0,
|
||
tx_len: 0,
|
||
tx_buf: MsgBuf::zero(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl HandshakeState {
|
||
pub fn zero() -> Self {
|
||
Self {
|
||
sidi: SessionId::zero(),
|
||
sidr: SessionId::zero(),
|
||
ck: SecretPrfTree::zero().dup(),
|
||
}
|
||
}
|
||
|
||
pub fn erase(&mut self) {
|
||
self.ck = SecretPrfTree::zero().dup();
|
||
}
|
||
|
||
pub fn init(&mut self, spkr: &[u8]) -> Result<&mut Self> {
|
||
self.ck = lprf::ckinit()?.mix(spkr)?.into_secret_prf_tree().dup();
|
||
Ok(self)
|
||
}
|
||
|
||
pub fn mix(&mut self, a: &[u8]) -> Result<&mut Self> {
|
||
self.ck = self.ck.mix(&lprf::mix()?)?.mix(a)?.dup();
|
||
Ok(self)
|
||
}
|
||
|
||
pub fn encrypt_and_mix(&mut self, ct: &mut [u8], pt: &[u8]) -> Result<&mut Self> {
|
||
let k = self.ck.mix(&lprf::hs_enc()?)?.into_secret();
|
||
aead_enc_into(ct, k.secret(), &NONCE0, &NOTHING, pt)?;
|
||
self.mix(ct)
|
||
}
|
||
|
||
pub fn decrypt_and_mix(&mut self, pt: &mut [u8], ct: &[u8]) -> Result<&mut Self> {
|
||
let k = self.ck.mix(&lprf::hs_enc()?)?.into_secret();
|
||
aead_dec_into(pt, k.secret(), &NONCE0, &NOTHING, ct)?;
|
||
self.mix(ct)
|
||
}
|
||
|
||
// I loathe "error: constant expression depends on a generic parameter"
|
||
pub fn encaps_and_mix<T: KEM, const SHK_LEN: usize>(
|
||
&mut self,
|
||
ct: &mut [u8],
|
||
pk: &[u8],
|
||
) -> Result<&mut Self> {
|
||
let mut shk = Secret::<SHK_LEN>::zero();
|
||
T::encaps(shk.secret_mut(), ct, pk)?;
|
||
self.mix(pk)?.mix(shk.secret())?.mix(ct)
|
||
}
|
||
|
||
pub fn decaps_and_mix<T: KEM, const SHK_LEN: usize>(
|
||
&mut self,
|
||
sk: &[u8],
|
||
pk: &[u8],
|
||
ct: &[u8],
|
||
) -> Result<&mut Self> {
|
||
let mut shk = Secret::<SHK_LEN>::zero();
|
||
T::decaps(shk.secret_mut(), sk, ct)?;
|
||
self.mix(pk)?.mix(shk.secret())?.mix(ct)
|
||
}
|
||
|
||
pub fn store_biscuit(
|
||
&mut self,
|
||
srv: &mut CryptoServer,
|
||
peer: PeerPtr,
|
||
biscuit_ct: &mut [u8],
|
||
) -> Result<&mut Self> {
|
||
let mut biscuit = Secret::<BISCUIT_PT_LEN>::zero(); // pt buffer
|
||
let mut biscuit = (&mut biscuit.secret_mut()[..]).biscuit()?; // lens view
|
||
|
||
// calculate pt contents
|
||
biscuit
|
||
.pidi_mut()
|
||
.copy_from_slice(peer.get(srv).pidt()?.as_slice());
|
||
biscuit.biscuit_no_mut().copy_from_slice(&*srv.biscuit_ctr);
|
||
biscuit
|
||
.ck_mut()
|
||
.copy_from_slice(self.ck.clone().danger_into_secret().secret());
|
||
|
||
// calculate ad contents
|
||
let ad = lprf::biscuit_ad()?
|
||
.mix(srv.spkm.secret())?
|
||
.mix(self.sidi.as_slice())?
|
||
.mix(self.sidr.as_slice())?
|
||
.into_value();
|
||
|
||
// consume biscuit no
|
||
sodium_bigint_inc(&mut *srv.biscuit_ctr);
|
||
|
||
// The first bit of the nonce indicates which biscuit key was used
|
||
// TODO: This is premature optimiaztion. Remove!
|
||
let bk = srv.active_biscuit_key();
|
||
let mut n = XAEADNonce::random();
|
||
n[0] &= 0b0111_1111;
|
||
n[0] |= (bk.0 as u8 & 0x1) << 7;
|
||
|
||
let k = bk.get(srv).key.secret();
|
||
let pt = biscuit.all_bytes();
|
||
xaead_enc_into(biscuit_ct, k, &*n, &ad, pt)?;
|
||
|
||
self.mix(biscuit_ct)
|
||
}
|
||
|
||
/// Takes an encrypted biscuit and tries to decrypt the contained
|
||
/// information
|
||
pub fn load_biscuit(
|
||
srv: &CryptoServer,
|
||
biscuit_ct: &[u8],
|
||
sidi: SessionId,
|
||
sidr: SessionId,
|
||
) -> Result<(PeerPtr, BiscuitId, HandshakeState)> {
|
||
// The first bit of the biscuit indicates which biscuit key was used
|
||
let bk = BiscuitKeyPtr(((biscuit_ct[0] & 0b1000_0000) >> 7) as usize);
|
||
|
||
// Calculate addtional data fields
|
||
let ad = lprf::biscuit_ad()?
|
||
.mix(srv.spkm.secret())?
|
||
.mix(sidi.as_slice())?
|
||
.mix(sidr.as_slice())?
|
||
.into_value();
|
||
|
||
// Allocate and decrypt the biscuit data
|
||
let mut biscuit = Secret::<BISCUIT_PT_LEN>::zero(); // pt buf
|
||
let mut biscuit = (&mut biscuit.secret_mut()[..]).biscuit()?; // slice
|
||
xaead_dec_into(
|
||
biscuit.all_bytes_mut(),
|
||
bk.get(srv).key.secret(),
|
||
&ad,
|
||
biscuit_ct,
|
||
)?;
|
||
|
||
// Reconstruct the biscuit fields
|
||
let no = BiscuitId::from_slice(biscuit.biscuit_no());
|
||
let ck = SecretPrfTree::danger_from_secret(Secret::from_slice(biscuit.ck())).dup();
|
||
let pid = PeerId::from_slice(biscuit.pidi());
|
||
|
||
// Reconstruct the handshake state
|
||
let mut hs = Self { sidi, sidr, ck };
|
||
hs.mix(biscuit_ct)?;
|
||
|
||
// Look up the associated peer
|
||
let peer = srv
|
||
.find_peer(pid) // TODO: FindPeer should return a Result<()>
|
||
.with_context(|| format!("Could not decode biscuit for peer {pid:?}: No such peer."))?;
|
||
|
||
// Defense against replay attacks; implementations may accept
|
||
// the most recent biscuit no again (bn = peer.bn_{prev}) which
|
||
// indicates retransmission
|
||
// TODO: Handle retransmissions without involving the crypto code
|
||
ensure!(
|
||
sodium_bigint_cmp(biscuit.biscuit_no(), &*peer.get(srv).biscuit_used) >= 0,
|
||
"Rejecting biscuit: Outdated biscuit number"
|
||
);
|
||
|
||
Ok((peer, no, hs))
|
||
}
|
||
|
||
pub fn enter_live(self, srv: &CryptoServer, role: HandshakeRole) -> Result<Session> {
|
||
let HandshakeState { ck, sidi, sidr } = self;
|
||
let tki = ck.mix(&lprf::ini_enc()?)?.into_secret();
|
||
let tkr = ck.mix(&lprf::res_enc()?)?.into_secret();
|
||
let created_at = srv.timebase.now();
|
||
let (ntx, nrx) = (0, 0);
|
||
let (mysid, peersid, ktx, krx) = match role {
|
||
HandshakeRole::Initiator => (sidi, sidr, tki, tkr),
|
||
HandshakeRole::Responder => (sidr, sidi, tkr, tki),
|
||
};
|
||
Ok(Session {
|
||
created_at,
|
||
sidm: mysid,
|
||
sidt: peersid,
|
||
ck,
|
||
txkm: ktx,
|
||
txkt: krx,
|
||
txnm: ntx,
|
||
txnt: nrx,
|
||
})
|
||
}
|
||
}
|
||
|
||
impl CryptoServer {
|
||
/// Get the shared key that was established with given peer
|
||
///
|
||
/// Fail if no session is available with the peer
|
||
pub fn osk(&self, peer: PeerPtr) -> Result<SymKey> {
|
||
let session = peer
|
||
.session()
|
||
.get(self)
|
||
.as_ref()
|
||
.with_context(|| format!("No current session for peer {:?}", peer))?;
|
||
Ok(session.ck.mix(&lprf::osk()?)?.into_secret())
|
||
}
|
||
}
|
||
|
||
impl CryptoServer {
|
||
/// Implementation of the cryptographic protocol using the already
|
||
/// established primitives
|
||
pub fn handle_initiation(
|
||
&mut self,
|
||
peer: PeerPtr,
|
||
mut ih: InitHello<&mut [u8]>,
|
||
) -> Result<PeerPtr> {
|
||
let mut hs = InitiatorHandshake::zero_with_timestamp(self);
|
||
|
||
// IHI1
|
||
hs.core.init(peer.get(self).spkt.secret())?;
|
||
|
||
// IHI2
|
||
hs.core.sidi.randomize();
|
||
ih.sidi_mut().copy_from_slice(&hs.core.sidi.value);
|
||
|
||
// IHI3
|
||
EphemeralKEM::keygen(hs.eski.secret_mut(), &mut *hs.epki)?;
|
||
ih.epki_mut().copy_from_slice(&hs.epki.value);
|
||
|
||
// IHI4
|
||
hs.core.mix(ih.sidi())?.mix(ih.epki())?;
|
||
|
||
// IHI5
|
||
hs.core
|
||
.encaps_and_mix::<StaticKEM, { StaticKEM::SHK_LEN }>(
|
||
ih.sctr_mut(),
|
||
peer.get(self).spkt.secret(),
|
||
)?;
|
||
|
||
// IHI6
|
||
hs.core
|
||
.encrypt_and_mix(ih.pidic_mut(), self.pidm()?.as_ref())?;
|
||
|
||
// IHI7
|
||
hs.core
|
||
.mix(self.spkm.secret())?
|
||
.mix(peer.get(self).psk.secret())?;
|
||
|
||
// IHI8
|
||
hs.core.encrypt_and_mix(ih.auth_mut(), &NOTHING)?;
|
||
|
||
// Update the handshake hash last (not changing any state on prior error
|
||
peer.hs().insert(self, hs)?;
|
||
|
||
Ok(peer)
|
||
}
|
||
|
||
pub fn handle_init_hello(
|
||
&mut self,
|
||
ih: InitHello<&[u8]>,
|
||
mut rh: RespHello<&mut [u8]>,
|
||
) -> Result<PeerPtr> {
|
||
let mut core = HandshakeState::zero();
|
||
|
||
core.sidi = SessionId::from_slice(ih.sidi());
|
||
|
||
// IHR1
|
||
core.init(self.spkm.secret())?;
|
||
|
||
// IHR4
|
||
core.mix(ih.sidi())?.mix(ih.epki())?;
|
||
|
||
// IHR5
|
||
core.decaps_and_mix::<StaticKEM, { StaticKEM::SHK_LEN }>(
|
||
self.sskm.secret(),
|
||
self.spkm.secret(),
|
||
ih.sctr(),
|
||
)?;
|
||
|
||
// IHR6
|
||
let peer = {
|
||
let mut peerid = PeerId::zero();
|
||
core.decrypt_and_mix(&mut *peerid, ih.pidic())?;
|
||
self.find_peer(peerid)
|
||
.with_context(|| format!("No such peer {peerid:?}."))?
|
||
};
|
||
|
||
// IHR7
|
||
core.mix(peer.get(self).spkt.secret())?
|
||
.mix(peer.get(self).psk.secret())?;
|
||
|
||
// IHR8
|
||
core.decrypt_and_mix(&mut [0u8; 0], ih.auth())?;
|
||
|
||
// RHR1
|
||
core.sidr.randomize();
|
||
rh.sidi_mut().copy_from_slice(core.sidi.as_ref());
|
||
rh.sidr_mut().copy_from_slice(core.sidr.as_ref());
|
||
|
||
// RHR3
|
||
core.mix(rh.sidr())?.mix(rh.sidi())?;
|
||
|
||
// RHR4
|
||
core.encaps_and_mix::<EphemeralKEM, { EphemeralKEM::SHK_LEN }>(rh.ecti_mut(), ih.epki())?;
|
||
|
||
// RHR5
|
||
core.encaps_and_mix::<StaticKEM, { StaticKEM::SHK_LEN }>(
|
||
rh.scti_mut(),
|
||
peer.get(self).spkt.secret(),
|
||
)?;
|
||
|
||
// RHR6
|
||
core.store_biscuit(self, peer, rh.biscuit_mut())?;
|
||
|
||
// RHR7
|
||
core.encrypt_and_mix(rh.auth_mut(), &NOTHING)?;
|
||
|
||
Ok(peer)
|
||
}
|
||
|
||
pub fn handle_resp_hello(
|
||
&mut self,
|
||
rh: RespHello<&[u8]>,
|
||
mut ic: InitConf<&mut [u8]>,
|
||
) -> Result<PeerPtr> {
|
||
// RHI2
|
||
let peer = self
|
||
.lookup_handshake(SessionId::from_slice(rh.sidi()))
|
||
.with_context(|| {
|
||
format!(
|
||
"Got RespHello packet for non-existent session {:?}",
|
||
rh.sidi()
|
||
)
|
||
})?
|
||
.peer();
|
||
|
||
macro_rules! hs {
|
||
() => {
|
||
peer.hs().get(self).as_ref().unwrap()
|
||
};
|
||
}
|
||
macro_rules! hs_mut {
|
||
() => {
|
||
peer.hs().get_mut(self).as_mut().unwrap()
|
||
};
|
||
}
|
||
|
||
// TODO: Is this really necessary? The only possible state is "awaits resp hello";
|
||
// no initiation created should be modeled as an Null option and a Session means
|
||
// we will not be able to find the handshake
|
||
let exp = hs!().next;
|
||
let got = HandshakeStateMachine::RespHello;
|
||
|
||
ensure!(
|
||
exp == got,
|
||
"Unexpected package in session {:?}. Expected {:?}, got {:?}.",
|
||
SessionId::from_slice(rh.sidi()),
|
||
exp,
|
||
got
|
||
);
|
||
|
||
let mut core = hs!().core.clone();
|
||
core.sidr.copy_from_slice(rh.sidr());
|
||
|
||
// TODO: decaps_and_mix should take Secret<> directly
|
||
// to save us from the repetitive secret unwrapping
|
||
|
||
// RHI3
|
||
core.mix(rh.sidr())?.mix(rh.sidi())?;
|
||
|
||
// RHI4
|
||
core.decaps_and_mix::<EphemeralKEM, { EphemeralKEM::SHK_LEN }>(
|
||
hs!().eski.secret(),
|
||
&*hs!().epki,
|
||
rh.ecti(),
|
||
)?;
|
||
|
||
// RHI5
|
||
core.decaps_and_mix::<StaticKEM, { StaticKEM::SHK_LEN }>(
|
||
self.sskm.secret(),
|
||
self.spkm.secret(),
|
||
rh.scti(),
|
||
)?;
|
||
|
||
// RHI6
|
||
core.mix(rh.biscuit())?;
|
||
|
||
// RHI7
|
||
core.decrypt_and_mix(&mut [0u8; 0], rh.auth())?;
|
||
|
||
// TODO: We should just authenticate the entire network package up to the auth
|
||
// tag as a pattern instead of mixing in fields separately
|
||
|
||
ic.sidi_mut().copy_from_slice(rh.sidi());
|
||
ic.sidr_mut().copy_from_slice(rh.sidr());
|
||
|
||
// ICI3
|
||
core.mix(ic.sidi())?.mix(ic.sidr())?;
|
||
ic.biscuit_mut().copy_from_slice(rh.biscuit());
|
||
|
||
// ICI4
|
||
core.encrypt_and_mix(ic.auth_mut(), &NOTHING)?;
|
||
|
||
// Split() – We move the secrets into the session; we do not
|
||
// delete the InitiatorHandshake, just clear it's secrets because
|
||
// we still need it for InitConf message retransmission to function.
|
||
|
||
// ICI7
|
||
peer.session()
|
||
.insert(self, core.enter_live(self, HandshakeRole::Initiator)?)?;
|
||
hs_mut!().core.erase();
|
||
hs_mut!().next = HandshakeStateMachine::RespConf;
|
||
|
||
Ok(peer)
|
||
}
|
||
|
||
pub fn handle_init_conf(
|
||
&mut self,
|
||
ic: InitConf<&[u8]>,
|
||
mut rc: EmptyData<&mut [u8]>,
|
||
) -> Result<PeerPtr> {
|
||
// (peer, bn) ← LoadBiscuit(InitConf.biscuit)
|
||
// ICR1
|
||
let (peer, biscuit_no, mut core) = HandshakeState::load_biscuit(
|
||
self,
|
||
ic.biscuit(),
|
||
SessionId::from_slice(ic.sidi()),
|
||
SessionId::from_slice(ic.sidr()),
|
||
)?;
|
||
|
||
// ICR2
|
||
core.encrypt_and_mix(&mut [0u8; AEAD_TAG_LEN], &NOTHING)?;
|
||
|
||
// ICR3
|
||
core.mix(ic.sidi())?.mix(ic.sidr())?;
|
||
|
||
// ICR4
|
||
core.decrypt_and_mix(&mut [0u8; 0], ic.auth())?;
|
||
|
||
// ICR5
|
||
if sodium_bigint_cmp(&*biscuit_no, &*peer.get(self).biscuit_used) > 0 {
|
||
// ICR6
|
||
peer.get_mut(self).biscuit_used = biscuit_no;
|
||
|
||
// ICR7
|
||
peer.session()
|
||
.insert(self, core.enter_live(self, HandshakeRole::Responder)?)?;
|
||
}
|
||
|
||
// TODO: Implementing RP should be possible without touching the live session stuff
|
||
|
||
// Send ack – Implementing sending the empty acknowledgement here
|
||
// instead of a generic PeerPtr::send(&Server, Option<&[u8]>) -> Either<EmptyData, Data>
|
||
// because data transmission is a stub currently. This software is supposed to be used
|
||
// as a key exchange service feeding a PSK into some classical (i.e. non post quantum)
|
||
let ses = peer
|
||
.session()
|
||
.get_mut(self)
|
||
.as_mut()
|
||
.context("Cannot send acknoledgement. No session.")?;
|
||
rc.sid_mut().copy_from_slice(&ses.sidt.value);
|
||
rc.ctr_mut().copy_from_slice(&ses.txnm.to_le_bytes());
|
||
ses.txnm += 1; // Increment nonce before encryption, just in case an error is raised
|
||
|
||
let n = cat!(AEAD_NONCE_LEN; rc.ctr(), &[0u8; 4]);
|
||
let k = ses.txkm.secret();
|
||
aead_enc_into(rc.auth_mut(), k, &n, &NOTHING, &NOTHING)?; // ct, k, n, ad, pt
|
||
|
||
Ok(peer)
|
||
}
|
||
|
||
pub fn handle_resp_conf(&mut self, rc: EmptyData<&[u8]>) -> Result<PeerPtr> {
|
||
let sid = SessionId::from_slice(rc.sid());
|
||
let hs = self
|
||
.lookup_handshake(sid)
|
||
.with_context(|| format!("Got RespConf packet for non-existent session {sid:?}"))?;
|
||
let ses = hs.peer().session();
|
||
|
||
let exp = hs.get(self).as_ref().map(|h| h.next);
|
||
let got = Some(HandshakeStateMachine::RespConf);
|
||
ensure!(
|
||
exp == got,
|
||
"Unexpected package in session {:?}. Expected {:?}, got {:?}.",
|
||
sid,
|
||
exp,
|
||
got
|
||
);
|
||
|
||
// Validate the message
|
||
{
|
||
let s = ses.get_mut(self).as_mut().with_context(|| {
|
||
format!("Cannot validate EmptyData message. Missing encryption session for {sid:?}")
|
||
})?;
|
||
// the unwrap can not fail, because the slice returned by ctr() is
|
||
// guaranteed to have the correct size
|
||
let n = u64::from_le_bytes(rc.ctr().try_into().unwrap());
|
||
ensure!(n >= s.txnt, "Stale nonce");
|
||
s.txnt = n;
|
||
aead_dec_into(
|
||
// pt, k, n, ad, ct
|
||
&mut [0u8; 0],
|
||
s.txkt.secret(),
|
||
&cat!(AEAD_NONCE_LEN; rc.ctr(), &[0u8; 4]),
|
||
&NOTHING,
|
||
rc.auth(),
|
||
)?;
|
||
}
|
||
|
||
// We can now stop retransmitting RespConf
|
||
hs.take(self);
|
||
|
||
Ok(hs.peer())
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod test {
|
||
use super::*;
|
||
|
||
fn init_crypto_server() -> CryptoServer {
|
||
// always init libsodium before anything
|
||
crate::sodium::sodium_init().unwrap();
|
||
|
||
// initialize public and private key for the crypto server
|
||
let (mut sk, mut pk) = (SSk::zero(), SPk::zero());
|
||
StaticKEM::keygen(sk.secret_mut(), pk.secret_mut()).expect("unable to generate keys");
|
||
|
||
CryptoServer::new(sk, pk)
|
||
}
|
||
|
||
/// The determination of the message type relies on reading the first byte of the message. Only
|
||
/// after that the length of the message is checked against the specified message type. This
|
||
/// test ensures that nothing breaks in the case of an empty message.
|
||
#[test]
|
||
#[should_panic = "called `Result::unwrap()` on an `Err` value: received empty message, ignoring it"]
|
||
fn handle_empty_message() {
|
||
let mut crypt = init_crypto_server();
|
||
let empty_rx_buf = [0u8; 0];
|
||
let mut tx_buf = [0u8; 0];
|
||
|
||
crypt.handle_msg(&empty_rx_buf, &mut tx_buf).unwrap();
|
||
}
|
||
}
|