Merge pull request #28 from zerotier/dev

Added Better Visibility into ZSSP Errors and Ratchet Fingerprints
This commit is contained in:
Monica Moniot
2023-11-21 15:25:55 -05:00
committed by GitHub
19 changed files with 584 additions and 404 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ authors = ["ZeroTier, Inc. <contact@zerotier.com>", "Adam Ierymenko <adam.ieryme
edition = "2021"
license = "MPL-2.0"
name = "zssp"
version = "0.1.0"
version = "0.3.0"
[lib]
name = "zssp"
+11 -8
View File
@@ -63,6 +63,7 @@ impl CryptoLayer for TestApplication {
type SessionData = u128;
type IncomingPacketBuffer = Vec<u8>;
type FingerprintData = ();
}
#[allow(unused)]
impl ApplicationLayer<TestApplication> for &TestApplication {
@@ -82,6 +83,7 @@ impl ApplicationLayer<TestApplication> for &TestApplication {
&mut self,
remote_static_key: &CrateP384PublicKey,
identity: &[u8],
_: Option<&()>,
) -> AcceptAction<TestApplication> {
AcceptAction {
session_data: Some(1),
@@ -93,15 +95,16 @@ impl ApplicationLayer<TestApplication> for &TestApplication {
fn restore_by_fingerprint(
&mut self,
ratchet_fingerprint: &[u8; RATCHET_SIZE],
) -> Result<Option<RatchetState>, std::io::Error> {
) -> Result<Option<(RatchetState, ())>, std::io::Error> {
let ratchets = self.ratchets.lock().unwrap();
Ok(ratchets.rf_map.get(ratchet_fingerprint).cloned())
Ok(ratchets.rf_map.get(ratchet_fingerprint).cloned().map(|r| (r, ())))
}
fn restore_by_identity(
&mut self,
remote_static_key: &CrateP384PublicKey,
session_data: &u128,
_: Option<&()>,
) -> Result<Option<RatchetStates>, std::io::Error> {
let ratchets = self.ratchets.lock().unwrap();
Ok(ratchets.peer_map.get(session_data).cloned())
@@ -187,9 +190,6 @@ fn alice_main(
pkt,
&mut output_data,
) {
Ok((Unassociated, _)) => {
//println!("[alice] ok");
}
Ok((Associated(_, event), _)) => match event {
Established => {
up = true;
@@ -201,10 +201,13 @@ fn alice_main(
Control => (),
_ => panic!(),
},
Ok(_) => {
//println!("[alice] ok");
}
Err(e) => {
println!("[alice] ERROR {:?}", e);
if let ReceiveError::ByzantineFault(e) = e {
assert!(!e.unnatural())
assert!(!e.unnatural)
}
}
}
@@ -278,7 +281,6 @@ fn bob_main(
pkt,
&mut output_data,
) {
Ok((Unassociated, _)) => {}
Ok((Associated(s, event), _)) => match event {
NewSession | NewDowngradedSession => {
println!("[bob] new session, took {}s", current_time as f32 / 1000.0);
@@ -300,10 +302,11 @@ fn bob_main(
Control => (),
_ => panic!(),
},
Ok(_) => {}
Err(e) => {
println!("[bob] ERROR {:?}", e);
if let ReceiveError::ByzantineFault(e) = e {
assert!(!e.unnatural())
assert!(!e.unnatural)
}
}
}
+8 -7
View File
@@ -53,6 +53,7 @@ impl AsRef<[u8]> for PooledVec {
#[allow(unused)]
impl DefaultCrypto for TestApplication {
type SessionData = ();
type LookupData = ();
type IncomingPacketBuffer = PooledVec;
}
@@ -76,6 +77,7 @@ impl ApplicationLayer<TestApplication> for &TestApplication {
&mut self,
remote_static_key: &CrateP384PublicKey,
identity: &[u8],
_: Option<&()>,
) -> AcceptAction<TestApplication> {
AcceptAction {
session_data: Some(()),
@@ -87,7 +89,7 @@ impl ApplicationLayer<TestApplication> for &TestApplication {
fn restore_by_fingerprint(
&mut self,
ratchet_fingerprint: &[u8; RATCHET_SIZE],
) -> Result<Option<RatchetState>, std::io::Error> {
) -> Result<Option<(RatchetState, ())>, std::io::Error> {
Ok(None)
}
@@ -95,6 +97,7 @@ impl ApplicationLayer<TestApplication> for &TestApplication {
&mut self,
remote_static_key: &CrateP384PublicKey,
session_data: &(),
_: Option<&()>,
) -> Result<Option<RatchetStates>, std::io::Error> {
Ok(None)
}
@@ -156,9 +159,6 @@ fn alice_main(
pkt,
&mut output_data,
) {
Ok((Unassociated, _)) => {
//println!("[alice] ok");
}
Ok((Associated(_, event), _)) => match event {
Established => {
up = true;
@@ -170,10 +170,11 @@ fn alice_main(
Control => (),
_ => panic!(),
},
Ok(_) => {}
Err(e) => {
println!("[alice] ERROR {:?}", e);
if let ReceiveError::ByzantineFault(e) = e {
assert!(!e.unnatural())
assert!(!e.unnatural)
}
}
}
@@ -238,7 +239,6 @@ fn bob_main(
pkt,
&mut output_data,
) {
Ok((Unassociated, _)) => {}
Ok((Associated(s, event), _)) => match event {
NewSession | NewDowngradedSession => {
println!("[bob] new session, took {}s", current_time as f32 / 1000.0);
@@ -260,10 +260,11 @@ fn bob_main(
Control => (),
_ => panic!(),
},
Ok(_) => {}
Err(e) => {
println!("[bob] ERROR {:?}", e);
if let ReceiveError::ByzantineFault(e) = e {
assert!(!e.unnatural())
assert!(!e.unnatural)
}
}
}
+62 -6
View File
@@ -153,6 +153,22 @@ pub trait CryptoLayer: Sized {
/// each session.
type SessionData;
/// Type for arbitrary opaque object that is attached to a new connection attempt if Alice sends
/// us a ratchet fingerprint recognized by `restore_by_fingerprint`.
///
/// If Alice continues to connect
/// with us, then this object will be passed to `check_accept_session` and `restore_by_identity`.
/// This is useful if the ratchet fingerprint was derived from a one-time password, in which
/// case `FingerprintData` can contain metadata regarding the one-time password. This can be
/// used by `check_accept_session` and `restore_by_identity` to perform additional
/// authentication checks, such as validating the one-time password as an invitation code.
///
/// `FingerprintData` can also be used with extreme caution to cache database resources that can
/// speed up the expected future calls to `check_accept_session` and `restore_by_identity`.
/// If this is done, the implementor is required in `check_accept_session` to verify that the
/// cached resources in `FingerprintData` indeed belong to the specified remote peer.
type FingerprintData;
/// Data type for incoming packet buffers.
///
/// This can be something like `Vec<u8>` or `Box<[u8]>` or it can be something like a pooled
@@ -190,6 +206,9 @@ pub trait ApplicationLayer<Crypto: CryptoLayer>: Sized {
/// If this function is configured to always return true, it means peers will not be able to
/// connect to us unless they had a prior-established ratchet key with us. This is the best way
/// for the paranoid to enforce a manual allow-list.
///
/// Corresponds to the "Hello Requires Recognized Ratchet, π_1" security flag of Transition
/// Algorithm 2 within the ZSSP whitepaper.
fn hello_requires_recognized_ratchet(&mut self) -> bool;
/// This function is called if we, as Alice, attempted to open a session with Bob using a
/// non-empty ratchet key, but Bob does not have this ratchet key and wants to downgrade
@@ -206,15 +225,29 @@ pub trait ApplicationLayer<Crypto: CryptoLayer>: Sized {
/// least one party is misconfigured and got their ratchet keys corrupted or lost, or Bob has
/// been compromised and is being impersonated. An attacker must at least have Bob's private
/// static key to be able to ask Alice to downgrade.
///
/// Corresponds to the "Initiator Disallows Downgrade, π_2" security flag of Transition
/// Algorithm 3 within the ZSSP whitepaper.
fn initiator_disallows_downgrade(&mut self, session: &Arc<Session<Crypto>>) -> bool;
/// Function to accept sessions after final negotiation.
/// The second argument is the identity that the remote peer sent us. The application
/// must verify this identity is associated with the remote peer's static key.
///
/// The implementor must verify that three arguments, `remote_static_key`, `identity` and
/// optionally `fingerprint_data` all belong to the same remote peer, using whatever definition
/// of "same remote peer" that the upper protocol chooses.
/// `fingerprint_data` is an opaque type that is only `Some` if Alice sent us a ratchet
/// fingerprint that was successfully restored by `restore_by_fingerprint`.
///
/// To prevent desync, if this function specifies that we should connect, no other open session
/// with the same remote peer must exist. Drop or call expire on any pre-existing sessions
/// before returning.
fn check_accept_session(&mut self, remote_static_key: &Crypto::PublicKey, identity: &[u8]) -> AcceptAction<Crypto>;
///
/// Corresponds to the **Accept** call of Transition Algorithm 4 within the ZSSP whitepaper.
fn check_accept_session(
&mut self,
remote_static_key: &Crypto::PublicKey,
identity: &[u8],
fingerprint_data: Option<&Crypto::FingerprintData>,
) -> AcceptAction<Crypto>;
/// Lookup a specific ratchet state based on its ratchet fingerprint.
/// This function will be called whenever Alice attempts to connect to us with a non-empty
@@ -222,10 +255,24 @@ pub trait ApplicationLayer<Crypto: CryptoLayer>: Sized {
///
/// If a ratchet state with a matching fingerprint could not be found, this function should
/// return `Ok(None)`.
///
/// This function can also return an opaque `FingerprintData` object. If Alice continues to connect
/// with us, then this object will be passed to `check_accept_session` and `restore_by_identity`.
/// This is useful if the ratchet fingerprint was derived from a one-time password, in which
/// case `FingerprintData` can contain metadata regarding the one-time password. This can be
/// used by `check_accept_session` and `restore_by_identity` to perform additional
/// authentication checks, such as validating the one-time password as an invitation code.
///
/// `FingerprintData` can also be used with extreme caution to cache database resources that can
/// speed up the expected future calls to `check_accept_session` and `restore_by_identity`.
/// If this is done, the implementor is required in `check_accept_session` to verify that the
/// cached resources in `FingerprintData` indeed belong to the specified remote peer.
///
/// Corresponds to the **Restore** call of Transition Algorithm 2 within the ZSSP whitepaper.
fn restore_by_fingerprint(
&mut self,
ratchet_fingerprint: &[u8; RATCHET_SIZE],
) -> Result<Option<RatchetState>, std::io::Error>;
) -> Result<Option<(RatchetState, Crypto::FingerprintData)>, std::io::Error>;
/// Lookup the specific ratchet states based on the identity of the peer being communicated with.
/// This function will be called whenever Alice attempts to open a session, or Bob attempts
/// to verify Alice's identity.
@@ -240,10 +287,13 @@ pub trait ApplicationLayer<Crypto: CryptoLayer>: Sized {
/// This function is not responsible for deciding whether or not to connect to this remote peer.
/// Filtering peers should be done by the caller to `Context::open` as well as by the
/// function `ApplicationLayer::check_accept_session`.
///
/// Corresponds to the **Restore** call of Transition Algorithm 1 and 4 within the ZSSP whitepaper.
fn restore_by_identity(
&mut self,
remote_static_key: &Crypto::PublicKey,
session_data: &Crypto::SessionData,
fingerprint_data: Option<&Crypto::FingerprintData>,
) -> Result<Option<RatchetStates>, std::io::Error>;
/// Atomically commit the update specified by `update_data` to storage, or return an error if
/// the update could not be made.
@@ -306,12 +356,18 @@ pub struct AcceptAction<Crypto: CryptoLayer> {
pub session_data: Option<Crypto::SessionData>,
/// Whether or not we will accept a connection with the remote peer when they do not have a
/// ratchet key that we think they should have.
///
/// Corresponds to the "Responder Disallows Downgrade, π_3" security flag of Transition
/// Algorithm 4 within the ZSSP whitepaper.
pub responder_disallows_downgrade: bool,
/// Whether or not to send an explicit rejection packet to the remote peer if we do not create
/// a session with them.
///
/// This field will not be used if `session_data` is `Some` and the remote peer passes all other
/// authentication checks.
///
/// Corresponds to the "Responder Silently Rejects, π_4" security flag of Transition
/// Algorithm 4 within the ZSSP whitepaper.
pub responder_silently_rejects: bool,
}
@@ -323,7 +379,7 @@ pub trait Sender {
/// Send the given fragment on this interface and then return whether or not an error occured.
///
/// If `true` is returned then sending is cancelled and this instance of `Sender` is dropped.
fn send_frag<'a>(&'a mut self, frag: &mut [u8]) -> bool;
fn send_frag(&mut self, frag: &mut [u8]) -> bool;
}
/// A trait to genericize the process of borrowing the resources necessary to repeatedly
@@ -353,7 +409,7 @@ pub trait SendTo<Crypto: CryptoLayer> {
}
impl<F: FnMut(&mut [u8]) -> bool> Sender for F {
fn send_frag<'a>(&'a mut self, frag: &mut [u8]) -> bool {
fn send_frag(&mut self, frag: &mut [u8]) -> bool {
self(frag)
}
}
+2
View File
@@ -38,6 +38,7 @@ pub use openssl_sys;
#[cfg(feature = "default-crypto")]
pub trait DefaultCrypto {
type SessionData;
type LookupData;
type IncomingPacketBuffer: AsMut<[u8]> + AsRef<[u8]>;
}
#[cfg(feature = "default-crypto")]
@@ -54,5 +55,6 @@ impl<C: DefaultCrypto> crate::application::CryptoLayer for C {
type Kem = CrateKyber1024PrivateKey;
type SessionData = C::SessionData;
type FingerprintData = C::LookupData;
type IncomingPacketBuffer = C::IncomingPacketBuffer;
}
+1
View File
@@ -177,6 +177,7 @@ impl<'a> AesGcmDecContext for OpenSSLAesGcmDec<'a> {
}
}
/// A pool of OpenSSL AES-GCM ciphers.
pub struct OpenSSLAesGcmPool {
enc: [Mutex<OpenSSLCtx>; 8],
dec: [Mutex<OpenSSLCtx>; 8],
+2
View File
@@ -3,6 +3,7 @@ use rand_core::{CryptoRng, RngCore};
use crate::crypto::*;
/// An alias for the P384PublicKey type from the p384 crate.
pub type CrateP384PublicKey = PublicKey;
impl P384PublicKey for CrateP384PublicKey {
fn from_bytes(raw_key: &[u8; P384_PUBLIC_KEY_SIZE]) -> Option<Self> {
@@ -15,6 +16,7 @@ impl P384PublicKey for CrateP384PublicKey {
}
}
/// An alias for the P384KeyPair type from the p384 crate.
pub type CrateP384KeyPair = EphemeralSecret;
impl<Rng: RngCore + CryptoRng> P384KeyPair<Rng> for CrateP384KeyPair {
type PublicKey = PublicKey;
+2 -1
View File
@@ -2,7 +2,7 @@ use hmac::{Hmac, Mac};
use sha2::{Digest, Sha512};
use crate::crypto::*;
/// An alias for the Sha512 type from the sha2 crate.
pub type CrateSha512 = Sha512;
impl Sha512Hash for CrateSha512 {
fn new() -> Self {
@@ -20,6 +20,7 @@ impl Sha512Hash for CrateSha512 {
}
}
/// A type that implements HMAC SHA512 using the hmac and sha2 crates.
pub struct CrateHmacSha512;
impl Sha512Hmac for CrateHmacSha512 {
fn new() -> Self {
+5 -4
View File
@@ -93,7 +93,7 @@ impl<Crypto: CryptoLayer> UnassociatedFragCache<Crypto> {
} else if self.map[idx1].key == key {
idx1
} else if self.map[idx0].key == 0 || self.map[idx1].key == 0 {
if (fragment_count as usize) > self.frags_unused_size {
if fragment_count > self.frags_unused_size {
// There are not enough free fragment slots so attempt to expire a bunch of entries.
let _ = self.check_for_expiry_inner(Crypto::SETTINGS.resend_time as i64, current_time);
}
@@ -117,7 +117,7 @@ impl<Crypto: CryptoLayer> UnassociatedFragCache<Crypto> {
let mut new_expiry = None;
if self.map[idx].key == 0 {
// This is a new entry so initialize it.
if (fragment_count as usize) <= self.frags_unused_size {
if fragment_count <= self.frags_unused_size {
new_expiry = Some(current_time + Crypto::SETTINGS.fragment_assembly_timeout as i64);
let entry = &mut self.map[idx];
entry.key = key;
@@ -148,7 +148,7 @@ impl<Crypto: CryptoLayer> UnassociatedFragCache<Crypto> {
entry.packet_size = new_size;
entry.fragment_have |= got;
let frag_idx = (entry.frags_idx as usize + fragment_no as usize) % self.frags.len();
let frag_idx = (entry.frags_idx as usize + fragment_no) % self.frags.len();
self.frags[frag_idx].write(fragment);
if entry.fragment_have == 1u64.wrapping_shl(fragment_count as u32) - 1 {
@@ -183,7 +183,7 @@ impl<Crypto: CryptoLayer> UnassociatedFragCache<Crypto> {
return expiry;
}
}
return i64::MAX;
i64::MAX
}
fn invalidate<const DROP: bool>(&mut self, idx: usize) {
@@ -257,6 +257,7 @@ fn test_cache() {
type Kem = CrateKyber1024PrivateKey;
type SessionData = ();
type FingerprintData = ();
type IncomingPacketBuffer = Vec<u8>;
}
+2 -2
View File
@@ -53,8 +53,8 @@ impl<Fragment, const MAX_FRAGMENTS: usize> Fragged<Fragment, MAX_FRAGMENTS> {
if got & self.have == 0 && self.count == fragment_count as u32 {
self.have |= got;
unsafe {
self.frags.get_unchecked_mut(fragment_no as usize).write(fragment);
if self.have == 1u64.wrapping_shl(self.count as u32) - 1 {
self.frags.get_unchecked_mut(fragment_no).write(fragment);
if self.have == 1u64.wrapping_shl(self.count) - 1 {
self.have = 0;
self.count = 0;
self.nonce = u64::MAX;
+1 -1
View File
@@ -61,7 +61,7 @@ impl<Application: CryptoLayer> UnassociatedHandshakeCache<Application> {
cache.expiries[idx] = expiry;
cache.handshakes[idx] = Some(state);
self.has_pending.store(true, Ordering::Release);
return Some(expiry);
Some(expiry)
}
pub(crate) fn remove(&self, local_id: NonZeroU32) -> bool {
let mut cache = self.cache.write().unwrap();
+3 -3
View File
@@ -94,8 +94,8 @@ pub(crate) const LABEL_RATCHET_STATE: &[u8; 4] = b"ASKR";
pub(crate) const LABEL_HEADER_KEY: &[u8; 4] = b"ASKH";
pub(crate) const LABEL_KEX_KEY: &[u8; 4] = b"ASKK";
pub(crate) const EXPIRE_AFTER_USES: u64 = 1 << 32 - 1;
pub(crate) const THREAD_SAFE_COUNTER_HARD_EXPIRE: u64 = u64::MAX - 1 << 16;
pub(crate) const EXPIRE_AFTER_USES: u64 = (1 << 32) - 1;
pub(crate) const THREAD_SAFE_COUNTER_HARD_EXPIRE: u64 = u64::MAX - (1 << 16);
/// Determines the number of counters a session will remember. If a counter arrives over
/// this amount out of order relative to other received counters, it is likely to be
/// rejected on the basis that the session can't remember if this counter was replayed.
@@ -137,7 +137,7 @@ pub(crate) const HANDSHAKE_RESPONSE_SIZE: usize =
P384_PUBLIC_KEY_SIZE + KYBER_CIPHERTEXT_SIZE + AES_GCM_TAG_SIZE + KID_SIZE + AES_GCM_TAG_SIZE;
pub(crate) const HEADERED_HANDSHAKE_RESPONSE_SIZE: usize = HANDSHAKE_RESPONSE_SIZE + HEADER_SIZE;
pub(crate) const HANDSHAKE_COMPLETION_MIN_SIZE: usize = P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + 0 + AES_GCM_TAG_SIZE;
pub(crate) const HANDSHAKE_COMPLETION_MIN_SIZE: usize = P384_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + AES_GCM_TAG_SIZE;
pub(crate) const HANDSHAKE_COMPLETION_MAX_SIZE: usize = HANDSHAKE_COMPLETION_MIN_SIZE + IDENTITY_MAX_SIZE;
pub(crate) const HEADERED_HANDSHAKE_COMPLETION_MAX_SIZE: usize = HANDSHAKE_COMPLETION_MAX_SIZE + HEADER_SIZE;
+135 -68
View File
@@ -1,5 +1,5 @@
use std::error::Error;
use std::fmt::Display;
use std::fmt;
use std::sync::Arc;
use crate::application::CryptoLayer;
@@ -16,7 +16,6 @@ pub enum OpenError {
/// The session could not be openned as a result.
StorageError(std::io::Error),
}
/// An error that can occur when attempting to send data over a session.
/// Depending on the error type trying again may not work.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
@@ -41,6 +40,14 @@ pub enum SendError {
DataTooLarge,
}
/// The contained session has just expired.
///
/// An expired session is no longer "owned" by the ZSSP context.
/// Therefore it is no longer capable of sending, receiving or being serviced,
/// so it should be dropped.
#[derive(Clone)]
pub struct ExpiredError<Crypto: CryptoLayer>(pub Arc<Session<Crypto>>);
/// A type of fault occurred because we received a bad packet.
///
/// An unauthenticated attacker can intentionally trigger any of these, so it is best to
@@ -71,11 +78,22 @@ pub enum FaultType {
/// Because an unauthenticated remote peer can force these to occur with specific
/// contained information, it is recommended in production to either drop these
/// immediately, or log them safely to a local output stream and then drop them.
#[derive(Debug)]
pub struct ByzantineFault {
pub struct ByzantineFault<Crypto: CryptoLayer> {
/// The session associated with this fault, if there was one.
///
/// The sender specified this session within their packet, but they were not authenticated,
/// so the sender could theoretically be anyone.
pub session: Option<Arc<Session<Crypto>>>,
/// This field is true if this fault caused the session it was attached to to expire.
/// An expired session is no longer "owned" by the ZSSP context.
/// Therefore it is no longer capable of sending, receiving or being serviced,
/// so it should be dropped.
///
/// If this returns true then it is guaranteed that the `session` field is occupied.
pub caused_expiration: bool,
/// The type of fault that has occurred. Be cautious if you choose to read this
/// value, as an attacker has control over it.
pub(crate) error: FaultType,
pub error: FaultType,
/// Some byzantine faults within ZSSP are naturally occurring, i.e. they can occur
/// between two well behaved and trusted parties executing the protocol.
/// This boolean is false if this is one of these faults. If you go to the file and
@@ -88,7 +106,7 @@ pub struct ByzantineFault {
/// persevered (i.e. bits have been flipped) to be unnatural.
/// ZSSP also considers collisions of what are supposed to be uniform random
/// numbers to be unnatural.
pub(crate) unnatural: bool,
pub unnatural: bool,
/// The file of this implementation of ZSSP from which this error was generated.
#[cfg(feature = "debug")]
pub(crate) file: &'static str,
@@ -99,47 +117,13 @@ pub struct ByzantineFault {
#[cfg(feature = "debug")]
pub(crate) line: u32,
}
// I don't like getter methods but in this case they are the only way to implement
// conditionally compiled struct fields without the feature flag causing breaking changes.
impl ByzantineFault {
/// The type of fault that has occurred. Be cautious if you choose to read this
/// value, as an attacker has control over it.
pub fn error(&self) -> FaultType {
self.error
}
/// Some byzantine faults within ZSSP are naturally occurring, i.e. they can occur
/// between two well behaved and trusted parties executing the protocol.
/// This boolean is false if this is one of these faults. If you go to the file and
/// line number specified by this error you will find a comment describing
/// how and why exactly this fault can occur naturally.
///
/// Faults that can occur because the underlying communication medium is lossy and
/// sequentially inconsistent (as in UDP) are considered naturally occurring.
/// However ZSSP considers faults that occur because data integrity has not been
/// persevered (i.e. bits have been flipped) to be unnatural.
/// ZSSP also considers collisions of what are supposed to be uniform random
/// numbers to be unnatural.
pub fn unnatural(&self) -> bool {
self.unnatural
}
/// The file of this implementation of ZSSP from which this error was generated.
#[cfg(feature = "debug")]
pub fn file(&self) -> &'static str {
self.file
}
/// The line number of this implementation of ZSSP from which this error was
/// generated. As such this number uniquely identifies each possible fault that
/// can occur during ZSSP. Advanced user can use this information to debug more
/// complicated usages of ZSSP.
#[cfg(feature = "debug")]
pub fn line(&self) -> u32 {
self.line
}
}
/// An error that occurred during the receipt of a given packet.
#[derive(Debug)]
pub enum ReceiveError {
///
/// Keep in mind that when one of these occurs it inherently means that the packet from the remote
/// peer has either not been authenticated or has failed authentication. As such, an attacker could
/// trigger any of these. These errors should only be used for debugging and tracing.
pub enum ReceiveError<Crypto: CryptoLayer> {
/// A type of fault that can occur because a remote peer sent us a bad packet.
/// Such packets will be ignored by ZSSP but a user of ZSSP might want to log
/// them for debugging or tracing.
@@ -147,11 +131,11 @@ pub enum ReceiveError {
/// Because an unauthenticated remote peer can force these to occur with specific
/// contained information, it is recommended in production to either drop these
/// immediately, or log them safely to a local output stream and then drop them.
ByzantineFault(ByzantineFault),
ByzantineFault(ByzantineFault<Crypto>),
/// Rekeying failed and session secret has reached its hard usage count limit.
/// The associated session will no longer function and has to be dropped.
MaxKeyLifetimeExceeded,
MaxKeyLifetimeExceeded(Arc<Session<Crypto>>),
/// Either the `ApplicationLayer::incoming_session` or `ApplicationLayer::check_accept_session`
/// callback rejected the remote peer's attempt to establish a new session.
@@ -163,7 +147,7 @@ pub enum ReceiveError {
/// An error was returned by the `output_buffer` passed to receive.
/// The received packet was dropped.
WriteError(std::io::Error),
WriteError(std::io::Error, Arc<Session<Crypto>>),
}
macro_rules! fault {
@@ -175,6 +159,23 @@ macro_rules! fault {
line: line!(),
error: $name,
unnatural: $unnatural,
session: None,
caused_expiration: false,
})
};
($name:expr, $unnatural:ident, $session:ident) => {
fault!($name, $unnatural, $session, false)
};
($name:expr, $unnatural:ident, $session:ident, $e:ident) => {
ReceiveError::ByzantineFault(crate::result::ByzantineFault {
#[cfg(feature = "debug")]
file: file!(),
#[cfg(feature = "debug")]
line: line!(),
error: $name,
unnatural: $unnatural,
session: Some($session.clone()),
caused_expiration: $e,
})
};
}
@@ -183,12 +184,17 @@ pub(crate) use fault;
/// Result generated by the context packet receive function, with possible payloads.
#[derive(Clone)]
pub enum ReceiveOk<Crypto: CryptoLayer> {
/// Packet superficially appeared valid but is not associated with a session yet.
/// The received packet superficially appeared valid but is not associated with a session yet.
/// This can occur because the packet was only a fragment of a larger packet,
/// or if it was a control packet that does not go through full Noise authentication.
Unassociated,
/// Packet was authentic and belongs to this specific session.
/// The received packet was authentic and belongs to this specific session.
Associated(Arc<Session<Crypto>>, SessionEvent),
/// The received packet was a fragment of a larger packet.
///
/// ***The authenticity of this fragment cannot be fully known yet.***
/// This return value should only be used for debugging and tracing purposes.
Fragment(Arc<Session<Crypto>>),
}
/// Something that can occur to an associated session when a packet is received successfully,
/// including receiving a payload of decrypted, authenticated data.
@@ -241,8 +247,8 @@ pub enum SessionEvent {
DowngradedRatchetKey,
}
impl Display for OpenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for OpenError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OpenError::IdentityTooLarge => f.write_str("identity too large"),
OpenError::StorageError(e) => e.fmt(f),
@@ -251,8 +257,8 @@ impl Display for OpenError {
}
impl Error for OpenError {}
impl Display for SendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for SendError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let str = match self {
SendError::MtuTooSmall => "mtu too small",
SendError::SessionExpired => "session has expired",
@@ -264,8 +270,23 @@ impl Display for SendError {
}
impl Error for SendError {}
impl Display for FaultType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl<Crypto: CryptoLayer> fmt::Debug for ExpiredError<Crypto>
where
Session<Crypto>: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("ExpiredError").field(&self.0).finish()
}
}
impl<Crypto: CryptoLayer> fmt::Display for ExpiredError<Crypto> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "session expired")
}
}
impl<Crypto: CryptoLayer> Error for ExpiredError<Crypto> where Session<Crypto>: fmt::Debug {}
impl fmt::Display for FaultType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let str = match self {
FaultType::UnknownLocalKeyId => "packet contained an unknown key id",
FaultType::InvalidPacket => "invalid packet received",
@@ -278,27 +299,73 @@ impl Display for FaultType {
}
impl Error for FaultType {}
impl Display for ByzantineFault {
// I don't like getter methods but in this case they are the only way to implement
// conditionally compiled struct fields without the feature flag causing breaking changes.
impl<Crypto: CryptoLayer> ByzantineFault<Crypto> {
/// The file of this implementation of ZSSP from which this error was generated.
#[cfg(feature = "debug")]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
pub fn file(&self) -> &'static str {
self.file
}
/// The line number of this implementation of ZSSP from which this error was
/// generated. As such this number uniquely identifies each possible fault that
/// can occur during ZSSP. Advanced user can use this information to debug more
/// complicated usages of ZSSP.
#[cfg(feature = "debug")]
pub fn line(&self) -> u32 {
self.line
}
}
impl<Crypto: CryptoLayer> fmt::Debug for ByzantineFault<Crypto>
where
Session<Crypto>: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ByzantineFault")
.field("session", &self.session)
.field("caused_expiration", &self.caused_expiration)
.field("error", &self.error)
.field("unnatural", &self.unnatural)
.field("file", &self.file)
.field("line", &self.line)
.finish()
}
}
impl<Crypto: CryptoLayer> fmt::Display for ByzantineFault<Crypto> {
#[cfg(feature = "debug")]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({}:{})", self.error, self.file, self.line)
}
#[cfg(not(feature = "debug"))]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.error.fmt(f)
}
}
impl Error for ByzantineFault {}
impl<Crypto: CryptoLayer> Error for ByzantineFault<Crypto> where Session<Crypto>: fmt::Debug {}
impl Display for ReceiveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl<Crypto: CryptoLayer> fmt::Debug for ReceiveError<Crypto>
where
Crypto::SessionData: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReceiveError::ByzantineFault(e) => e.fmt(f),
ReceiveError::MaxKeyLifetimeExceeded => f.write_str("max key lifetime exceeded"),
ReceiveError::Rejected => f.write_str("attempt to establish session rejected"),
ReceiveError::StorageError(e) => e.fmt(f),
ReceiveError::WriteError(e) => e.fmt(f),
Self::ByzantineFault(arg) => f.debug_tuple("ByzantineFault").field(arg).finish(),
Self::MaxKeyLifetimeExceeded(arg0) => f.debug_tuple("MaxKeyLifetimeExceeded").field(arg0).finish(),
Self::Rejected => f.write_str("Rejected"),
Self::StorageError(arg0) => f.debug_tuple("StorageError").field(arg0).finish(),
Self::WriteError(arg0, arg1) => f.debug_tuple("WriteError").field(arg0).field(arg1).finish(),
}
}
}
impl Error for ReceiveError {}
impl<Crypto: CryptoLayer> fmt::Display for ReceiveError<Crypto> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReceiveError::ByzantineFault(e) => e.fmt(f),
ReceiveError::MaxKeyLifetimeExceeded(_) => f.write_str("max key lifetime exceeded"),
ReceiveError::Rejected => f.write_str("attempt to establish session rejected"),
ReceiveError::StorageError(e) => e.fmt(f),
ReceiveError::WriteError(e, _) => e.fmt(f),
}
}
}
impl<Crypto: CryptoLayer> Error for ReceiveError<Crypto> where Crypto::SessionData: fmt::Debug {}
+1 -1
View File
@@ -19,7 +19,7 @@ impl<Crypto: CryptoLayer> Clone for SymmetricState<Crypto> {
Self {
k: self.k.clone(),
ck: self.ck.clone(),
h: self.h.clone(),
h: self.h,
_app: PhantomData,
}
}
+191 -162
View File
File diff suppressed because it is too large Load Diff
+75 -58
View File
@@ -17,7 +17,7 @@ use crate::fragged::Assembled;
use crate::handshake_cache::UnassociatedHandshakeCache;
use crate::indexed_heap::IndexedBinaryHeap;
use crate::proto::*;
use crate::result::{fault, FaultType, OpenError, ReceiveError, ReceiveOk, SendError, SessionEvent};
use crate::result::{fault, ExpiredError, FaultType, OpenError, ReceiveError, ReceiveOk, SendError, SessionEvent};
use crate::zeta::*;
#[cfg(feature = "logging")]
use crate::LogEvent::*;
@@ -71,7 +71,9 @@ impl<Crypto: CryptoLayer> ContextInner<Crypto> {
}
}
fn parse_fragment_header(incoming_fragment: &[u8]) -> Result<(usize, usize, [u8; AES_GCM_NONCE_SIZE]), ReceiveError> {
fn parse_fragment_header<Crypto: CryptoLayer>(
incoming_fragment: &[u8],
) -> Result<(usize, usize, [u8; AES_GCM_NONCE_SIZE]), ReceiveError<Crypto>> {
let fragment_no = incoming_fragment[FRAGMENT_NO_IDX] as usize;
let fragment_count = incoming_fragment[FRAGMENT_COUNT_IDX] as usize;
if fragment_no >= fragment_count || fragment_count > MAX_FRAGMENTS {
@@ -206,7 +208,7 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
remote_address: &impl Hash,
mut incoming_fragment_buf: Crypto::IncomingPacketBuffer,
output_buffer: impl Write,
) -> Result<(ReceiveOk<Crypto>, Option<i64>), ReceiveError> {
) -> Result<(ReceiveOk<Crypto>, Option<i64>), ReceiveError<Crypto>> {
use crate::result::FaultType::*;
let ctx = &self.0;
send_unassociated_mtu = send_unassociated_mtu.max(MIN_TRANSPORT_MTU);
@@ -234,40 +236,38 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
);
}
{
//vrfy
if packet_type == PACKET_TYPE_HANDSHAKE_RESPONSE {
if !matches!(&state.beta, ZetaAutomata::A1(_)) {
// A resent handshake response from Bob may have arrived out of order,
// after we already received one.
return Err(fault!(OutOfSequence, false));
}
if incoming_counter >= COUNTER_WINDOW_MAX_SKIP_AHEAD {
return Err(fault!(ExpiredCounter, true));
}
} else if PACKET_TYPE_USES_COUNTER_RANGE.contains(&packet_type) {
// For DOS resistant reply-protection we need to check that the given counter is
// in the window of valid counters immediately.
// But for packets larger than 1 fragment we can't actually record the
// counter as received until we've authenticated the packet.
// So we check the counter window twice, and only update it the second time
// after the packet has been authenticated.
if !session.window.check(incoming_counter) {
// This can occur naturally if packets arrive way out of order, or
// if they are duplicates.
// This can also be naturally triggered if Bob has just successfully
// received the first session key and is reject all of Alice's resends.
// This can also occur if a session was manually expired, but not
// dropped, and the remote party is still sending us data.
return Err(fault!(ExpiredCounter, false));
}
} else if packet_type == PACKET_TYPE_HANDSHAKE_COMPLETION {
// This can be triggered if Bob successfully received a session key and
// needs to reject all of Alice's resends of PACKET_TYPE_NOISE_XK_PATTERN_3.
return Err(fault!(InvalidPacket, false));
} else {
return Err(fault!(InvalidPacket, true));
//vrfy
if packet_type == PACKET_TYPE_HANDSHAKE_RESPONSE {
if !matches!(&state.beta, ZetaAutomata::A1(_)) {
// A resent handshake response from Bob may have arrived out of order,
// after we already received one.
return Err(fault!(OutOfSequence, false, session));
}
if incoming_counter >= COUNTER_WINDOW_MAX_SKIP_AHEAD {
return Err(fault!(ExpiredCounter, true, session));
}
} else if PACKET_TYPE_USES_COUNTER_RANGE.contains(&packet_type) {
// For DOS resistant reply-protection we need to check that the given counter is
// in the window of valid counters immediately.
// But for packets larger than 1 fragment we can't actually record the
// counter as received until we've authenticated the packet.
// So we check the counter window twice, and only update it the second time
// after the packet has been authenticated.
if !session.window.check(incoming_counter) {
// This can occur naturally if packets arrive way out of order, or
// if they are duplicates.
// This can also be naturally triggered if Bob has just successfully
// received the first session key and is reject all of Alice's resends.
// This can also occur if a session was manually expired, but not
// dropped, and the remote party is still sending us data.
return Err(fault!(ExpiredCounter, false, session));
}
} else if packet_type == PACKET_TYPE_HANDSHAKE_COMPLETION {
// This can be triggered if Bob successfully received a session key and
// needs to reject all of Alice's resends of PACKET_TYPE_NOISE_XK_PATTERN_3.
return Err(fault!(InvalidPacket, false, session));
} else {
return Err(fault!(InvalidPacket, true, session));
}
// Handle defragmentation.
@@ -282,7 +282,8 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
&mut fragment_buffer,
);
if fragment_buffer.is_empty() {
return Ok((ReceiveOk::Unassociated, None));
drop(state);
return Ok((ReceiveOk::Fragment(session), None));
} else {
// We have not yet authenticated the sender so we do not report
// receiving a packet from them.
@@ -308,12 +309,12 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
&mut fragment_buffer,
);
if fragment_buffer.is_empty() {
return Ok((ReceiveOk::Unassociated, None));
return Ok((ReceiveOk::Fragment(session), None));
} else {
for fragment in fragment_buffer.as_ref() {
buffer
.try_extend_from_slice(&fragment.as_ref()[HEADER_SIZE..])
.map_err(|_| fault!(InvalidPacket, true))?;
.map_err(|_| fault!(InvalidPacket, true, session))?;
}
// We have not yet authenticated the sender so we do not report
// receiving a packet from them.
@@ -407,7 +408,7 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
log!(app, DIsAuthClosedSession(&session));
(SessionEvent::Rejected, None)
}
_ => return Err(fault!(InvalidPacket, true)), // This is unreachable.
_ => return Err(fault!(InvalidPacket, true, session)), // This is unreachable.
}
};
Ok((ReceiveOk::Associated(session, ret.0), ret.1))
@@ -428,11 +429,9 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
ReceivedRawFragment(packet_type, incoming_counter, fragment_no, fragment_count)
);
{
//vrfy
if packet_type != PACKET_TYPE_HANDSHAKE_COMPLETION || incoming_counter != 0 {
return Err(fault!(InvalidPacket, true));
}
//vrfy
if packet_type != PACKET_TYPE_HANDSHAKE_COMPLETION || incoming_counter != 0 {
return Err(fault!(InvalidPacket, true));
}
let mut buffer = ArrayVec::<u8, HANDSHAKE_COMPLETION_MAX_SIZE>::new();
@@ -484,7 +483,7 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
// This can occur naturally because either Bob's incoming_sessions cache got
// full so Alice's incoming session was dropped, or the session this packet
// was for was dropped by the application.
return Err(fault!(UnknownLocalKeyId, false));
Err(fault!(UnknownLocalKeyId, false))
}
}
} else {
@@ -632,9 +631,14 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
///
/// * `app` - Interface to application using ZSSP
/// * `send_to` - Function to get a sender and an MTU to send something over an active session
pub fn service<App: ApplicationLayer<Crypto>>(&self, mut app: App, send_to: impl SendTo<Crypto>) -> i64 {
pub fn service<App: ApplicationLayer<Crypto>>(&self, mut app: App, mut send_to: impl SendTo<Crypto>) -> i64 {
let current_time = app.time();
let next_service_time = self.service_inner(app, send_to, current_time);
let next_service_time = loop {
match self.service_inner(&mut app, send_to, current_time) {
Ok(ts) => break ts,
Err((_, s)) => send_to = s,
}
};
let max_interval = Crypto::SETTINGS
.fragment_assembly_timeout
.min(Crypto::SETTINGS.rekey_timeout)
@@ -650,21 +654,33 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
/// an `Option<i64>`. This option can contain an updated, reduced timestamp at which this
/// function ought to be called again.
///
/// If this returns an error then it means that a session has timed-out and has been expired.
/// An expired session is no longer "owned" by the ZSSP context.
/// Therefore it is no longer capable of sending, receiving or being serviced,
/// so it should be dropped.
///
/// A return type of `Err` effectively means that this function should be called again immediately.
/// This function should be called repeatedly in a loop until `Ok` is returned.
///
/// This function should only be used if the caller has direct access to a scheduler that allows
/// them to dynamically modify the interval at which this function is repeatedly called.
///
/// * `app` - Interface to application using ZSSP
/// * `send_to` - Function to get a sender and an MTU to send something over an active session
pub fn service_scheduled<App: ApplicationLayer<Crypto>>(&self, mut app: App, send_to: impl SendTo<Crypto>) -> i64 {
let current_time = app.time();
self.service_inner(app, send_to, current_time)
}
fn service_inner<App: ApplicationLayer<Crypto>>(
pub fn service_scheduled<App: ApplicationLayer<Crypto>>(
&self,
mut app: App,
mut send_to: impl SendTo<Crypto>,
send_to: impl SendTo<Crypto>,
) -> Result<i64, ExpiredError<Crypto>> {
let current_time = app.time();
self.service_inner(&mut app, send_to, current_time).map_err(|e| e.0)
}
fn service_inner<App: ApplicationLayer<Crypto>, F: SendTo<Crypto>>(
&self,
app: &mut App,
mut send_to: F,
current_time: i64,
) -> i64 {
) -> Result<i64, (ExpiredError<Crypto>, F)> {
let ctx = &self.0;
let mut session_queue = ctx.session_queue.lock().unwrap();
let mut queue_service_time = i64::MAX;
@@ -683,17 +699,18 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
continue;
}
};
let result = process_timers(&mut app, ctx, &session, current_time, |packet, hk_send| {
let result = process_timers(app, ctx, &session, current_time, |packet, hk_send| {
if let Some((sender, mut mtu)) = send_to.init_send(&session) {
mtu = mtu.max(MIN_TRANSPORT_MTU);
send_with_fragmentation(sender, mtu, packet, hk_send);
}
});
if let Some(next_timer) = result {
if let Ok(next_timer) = result {
queue_service_time = queue_service_time.min(next_timer);
session_queue.change_priority(queue_idx, Reverse(next_timer));
} else {
session.expire_inner(Some(ctx), Some(&mut session_queue));
return Err((ExpiredError(session), send_to));
}
}
// This is the only place where `ctx.next_service_time` can be increased. This only works
@@ -714,7 +731,7 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
let t2 = defrag_service_time.min(handshake_service_time);
let t1 = ctx.next_service_time.fetch_min(t2, Ordering::Relaxed);
t1.min(t2)
Ok(t1.min(t2))
}
/// Returns the exact timestamp at which either `Context::service` or
/// `Context::service_scheduled` should be called again.
-1
View File
@@ -83,7 +83,6 @@ pub(crate) const LABEL_RATCHET_STATE: &[u8; 4] = b"ASKR";
pub(crate) const LABEL_HEADER_KEY: &[u8; 4] = b"ASKH";
pub(crate) const LABEL_KEX_KEY: &[u8; 4] = b"ASKK";
//pub(crate) const EXPIRE_AFTER_USES: u64 = (1 << 32) - 1;
pub(crate) const HARD_EXPIRATION: u64 = u64::MAX;
/// Determines the number of counters a session will remember. If a counter arrives over
/// this amount out of order relative to other received counters, it is likely to be
Binary file not shown.
+82 -81
View File
File diff suppressed because it is too large Load Diff