Merge branch 'dev' of github.com:zerotier/zssp-proto into dev

This commit is contained in:
Monica Moniot
2023-10-25 17:48:18 -04:00
8 changed files with 70 additions and 93 deletions
+25 -25
View File
@@ -1,24 +1,27 @@
use arrayvec::ArrayVec;
use std::mem::{needs_drop, zeroed, MaybeUninit};
use std::mem::{needs_drop, MaybeUninit};
use crate::crypto::AES_GCM_NONCE_SIZE;
use crate::proto::{MAX_FRAGMENTS, NONCE_SIZE_DIFF};
use crate::proto::MAX_FRAGMENTS;
pub type Assembled<Fragment> = ArrayVec<Fragment, MAX_FRAGMENTS>;
/// Fast packet defragmenter
/// Fast packet defragmenter.
pub struct Fragged<Fragment, const MAX_FRAGMENTS: usize> {
nonce: [u8; 10],
count: u8,
nonce: u64,
count: u32,
have: u64,
size: usize,
frags: [MaybeUninit<Fragment>; MAX_FRAGMENTS],
}
impl<Fragment, const MAX_FRAGMENTS: usize> Fragged<Fragment, MAX_FRAGMENTS> {
pub fn new() -> Self {
debug_assert!(MAX_FRAGMENTS <= 64);
unsafe { zeroed() }
Self {
nonce: u64::MAX,
count: 0,
have: 0,
frags: core::array::from_fn(|_| MaybeUninit::zeroed()),
}
}
/// Add a fragment and return an assembled packet container if all fragments have been received.
@@ -27,39 +30,37 @@ impl<Fragment, const MAX_FRAGMENTS: usize> Fragged<Fragment, MAX_FRAGMENTS> {
/// be reused to assemble another packet.
///
/// Will check that aad is the same for all fragments.
///
/// This function only takes the 8 byte counter rather than the full 10 byte packet nonce,
/// because it is used in places where that is the only value we expect to always change in ZSSP.
pub(crate) fn assemble(
&mut self,
nonce: &[u8; AES_GCM_NONCE_SIZE],
nonce: u64,
fragment: Fragment,
fragment_no: usize,
fragment_count: usize,
ret_assembled: &mut Assembled<Fragment>,
) {
if fragment_no < fragment_count && fragment_count <= MAX_FRAGMENTS {
let nonce = nonce[NONCE_SIZE_DIFF..].try_into().unwrap();
// If the counter has changed, reset the structure to receive a new packet.
if nonce != self.nonce {
self.drop_in_place();
self.count = fragment_count as u8;
self.count = fragment_count as u32;
self.nonce = nonce;
self.size = 0;
}
let got = 1u64.wrapping_shl(fragment_no as u32);
if got & self.have == 0 && self.count == fragment_count as u8 {
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.have = 0;
self.count = 0;
self.nonce = [0; 10];
self.size = 0;
// Setting 'have' to 0 resets the state of this object, and the fragments
// are effectively moved into the Assembled<> container and returned. That
// container will drop them when it is dropped.
unsafe {
if self.have == 1u64.wrapping_shl(self.count as u32) - 1 {
self.have = 0;
self.count = 0;
self.nonce = u64::MAX;
// Setting 'have' to 0 resets the state of this object, and the fragments
// are effectively moved into the Assembled<> container and returned. That
// container will drop them when it is dropped.
for i in 0..fragment_count {
ret_assembled.push(self.frags[i].assume_init_read());
}
@@ -85,8 +86,7 @@ impl<Fragment, const MAX_FRAGMENTS: usize> Fragged<Fragment, MAX_FRAGMENTS> {
}
self.have = 0;
self.count = 0;
self.nonce = [0; 10];
self.size = 0;
self.nonce = u64::MAX;
}
}
-2
View File
@@ -32,7 +32,6 @@ pub struct IndexedBinaryHeap<T, P> {
map: Vec<(usize, u64)>,
}
#[allow(unused)]
impl<T, P: Ord> IndexedBinaryHeap<T, P> {
/// Create a new, empty binary heap.
pub fn new() -> Self {
@@ -189,7 +188,6 @@ impl<T, P: Ord> IndexedBinaryHeap<T, P> {
/// found in the heap and the function returned `Some(_)`, choosing to replace it.
///
/// Amortized runtime: O(log(n)).
#[inline]
pub fn update_priority(&mut self, idx: BinaryHeapIndex, f: impl FnOnce(&P) -> Option<P>) -> Option<P> {
if let Some(data_idx) = self.deref_index(idx) {
if let Some(new_priority) = f(&self.data[data_idx].1) {
+4
View File
@@ -227,6 +227,10 @@ pub enum SessionEvent {
/// This return value cannot occur after a session is fully established.
Rejected,
/// The received packet was valid and a data payload was decoded and authenticated.
///
/// Keep in mind that due to out-of-order transport, Alice can receive data payloads before
/// their session is "established", and the `Established` event is returned.
/// Users are free to either treat such payloads as they would any other, or drop them.
Data,
/// The received packet was some authentic protocol control packet. No action needs to be taken.
Control,
+9 -2
View File
@@ -1828,9 +1828,16 @@ impl<Crypto: CryptoLayer> Session<Crypto> {
}
}
impl<Crypto: CryptoLayer> std::fmt::Debug for Session<Crypto> where Crypto::SessionData: std::fmt::Debug {
impl<Crypto: CryptoLayer> std::fmt::Debug for Session<Crypto>
where
Crypto::SessionData: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Session").field("session_data", &self.session_data).field("was_bob", &self.was_bob).field("state", &self.state.read().unwrap().beta).finish()
f.debug_struct("Session")
.field("session_data", &self.session_data)
.field("was_bob", &self.was_bob)
.field("state", &self.state.read().unwrap().beta)
.finish()
}
}
impl<Crypto: CryptoLayer> std::fmt::Debug for ZetaAutomata<Crypto> {
+3 -3
View File
@@ -275,7 +275,7 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
let fragments = if fragment_count > 1 {
let idx = incoming_counter as usize % session.defrag.len();
session.defrag[idx].lock().unwrap().assemble(
&nonce,
incoming_counter,
incoming_fragment_buf,
fragment_no,
fragment_count,
@@ -301,7 +301,7 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
let assembled_packet = if fragment_count > 1 {
let idx = incoming_counter as usize % session.defrag.len();
session.defrag[idx].lock().unwrap().assemble(
&nonce,
incoming_counter,
incoming_fragment_buf,
fragment_no,
fragment_count,
@@ -438,7 +438,7 @@ impl<Crypto: CryptoLayer> Context<Crypto> {
let mut buffer = ArrayVec::<u8, HANDSHAKE_COMPLETION_MAX_SIZE>::new();
let assembled_packet = if fragment_count > 1 {
zeta.defrag.lock().unwrap().assemble(
&nonce,
incoming_counter,
incoming_fragment_buf,
fragment_no,
fragment_count,
+4
View File
@@ -227,6 +227,10 @@ pub enum SessionEvent {
/// This return value cannot occur after a session is fully established.
Rejected,
/// The received packet was valid and a data payload was decoded and authenticated.
///
/// Keep in mind that due to out-of-order transport, Alice can receive data payloads before
/// their session is "established", and the `Established` event is returned.
/// Users are free to either treat such payloads as they would any other, or drop them.
Data(Vec<u8>),
/// The received packet was some authentic protocol control packet. No action needs to be taken.
Control,
Binary file not shown.
+25 -61
View File
@@ -56,7 +56,7 @@ Within ZSSP, initial contact between an initiator, Alice, and a responder, Bob,
$$\texttt{Noise\_XKhfs+psk2\_P384+Kyber1024\_AESGCM\_SHA512}.$$
\texttt{Noise\_XK} specifies we are implementing Noise XK. \texttt{psk2} specifies we are using a preshared symmetric key (PSK) used on the second message of the handshake. \texttt{P384} specifies we are using elliptic curve P384 as our FIPS compliant Diffie-Helman (DH) primitive \cite{fips_p384}. \texttt{AESGCM} specifies we are using the AES block cipher in Galois/Counter Mode as our symmetric primitive \cite{fips_aesgcm}. \texttt{SHA512} is our FIPS compliant hash function \cite{fips_sha2}. And finally, \texttt{hfs} and \texttt{+Kyber1024} specifies we are adding hybrid forward secrecy onto Noise using an ephemeral Kyber1024 handshake \cite{kyber}.
ZSSP is cryptographically opinionated, as in ZSSP is only implemented in terms of the P385, Kyber1024, AES, and SHA-512 cryptographic primitives. There is no cipher suite negotiation within ZSSP, if one of the cryptographic primitives of ZSSP contains a critical weakness a new version of ZSSP will have to be created. We believe that in the modern era, with strong, standardized cryptographic algorithms available, this is a strict advantage to security. Importantly, this will simplify tremendously any future mathematical analysis of ZSSP.
ZSSP is cryptographically opinionated, as in ZSSP is only implemented in terms of the P384, Kyber1024, AES, and SHA-512 cryptographic primitives. There is no cipher suite negotiation within ZSSP, if one of the cryptographic primitives of ZSSP contains a critical weakness a new version of ZSSP will have to be created. We believe that in the modern era, with strong, standardized cryptographic algorithms available, this is a strict advantage to security. Importantly, this will simplify tremendously any future mathematical analysis of ZSSP.
\subsection{Federal Information Processing Standards}
@@ -198,9 +198,9 @@ If we disable all security features, we get \emph{Opportunistic Mode} ZSSP. This
\item Double Key-Compromise MitM -- The attacker has the static private keys of two peers, and attempts to become a Man-in-the-Middle between them.
\end{itemize}
Opportunistic mode ZSSP is vulnerable to Compromise-and-Impersonate and Double Key-Compromise MitM because an attacker can perform a downgrade attack to reset one or both peer's ratchet keys to zero. Howeve because such a downgrade should not normally happen between honest peers, if one does occur we can warn the user it has occurred, allowing them to investigate out-of-band whether them or their peer has corrupted or lost their persistent storage. If both peers have not corrupted their persistent storage a downgrade attack has almost certainly occurred, and one or more static keys are compromised.
Opportunistic mode ZSSP is vulnerable to Compromise-and-Impersonate and Double Key-Compromise MitM because an attacker can perform a downgrade attack to reset one or both peer's ratchet keys to zero. However because such a downgrade should not normally happen between honest peers, if one does occur we can warn the user it has occurred, allowing them to investigate out-of-band whether them or their peer has corrupted or lost their persistent storage. If both peers have not corrupted their persistent storage a downgrade attack has almost certainly occurred, and one or more static keys are compromised.
Compromise-and-Impersonate and Double Key-Compromise MitM attacks are possible against persistent mode ZSSP for a brief window of time. When an attacker compromises a peer, and steals their ratchet keys along with their static private keys, they have a limited time during with they can perform an impersonation attack. Otherwise the peer will engage in new key exchanges and rotate out the compromised ratchet keys. Furthermore, if the attacker commits to an impersonation attack, this will permanently desynchronize the compromised peer's ratchet keys from the peer being impersonated to. If the attacker does not commit to becoming a permanent MitM from that point onwards, their impersonation attack will be detected.
Compromise-and-Impersonate and Double Key-Compromise MitM attacks are possible against persistent mode ZSSP for a brief window of time. When an attacker compromises a peer, and steals their ratchet keys along with their static private keys, they have a limited time during which they can perform an impersonation attack. Otherwise the peer will engage in new key exchanges and rotate out the compromised ratchet keys. Furthermore, if the attacker commits to an impersonation attack, this will permanently desynchronize the compromised peer's ratchet keys from the peer being impersonated to. If the attacker does not commit to becoming a permanent MitM from that point onwards, their impersonation attack will be detected.
For peers actively communicating with each other, the attacker's window of opportunity is at most an hour. For peers not actively communicating, the attacker has until those peers contact each other again.
@@ -243,7 +243,7 @@ ZSSP is a stack of three closely related, but independent protocols. These are t
The next section will be dedicated to describing ZKE in explicit detail. We will be making heavy use of common mathematical notions and notation surrounding Deterministic Finite Automata, State Machines and AKE security. The goal with this section is to describe ZKE at a level of detail necessary both to allow someone to implement ZKE, and to aide future mathematical analysis of ZKE.
\begin{definition}[ZKE packet types]
We define 8 different packet types that essential for the functioning of ZKE.
We define 8 different packet types that are essential for the functioning of ZKE.
\begin{itemize}
\item $X_1$: This packet type contains the first message of Noise XK. It is identified internally with packet type number `0'.
\item $X_2$: This packet type contains the second message of Noise XK. It is identified internally with packet type number `1'.
@@ -462,7 +462,7 @@ At the end of this section we will have defined $\zeta$, which is the high-level
\end{itemize}
\sectionref{sec:trans_alg} discusses how these packets are structured, and what algorithms are used to create and process each packet.
$\zeta$ has a timer system to automatically and locally trigger state transitions or packet resends. This system exists primarily as a solution to the ``two generals problem''. Since ZKE assumes a out-of-order, lossy transport environment it is necessary to have a system of resending lost messages and timing out those resends, similar to TCP. The ZKE timer system is designed to be lightweight, security focused, and simple. It provides strong guarantees about the maximum amount of time that may pass until certain security critical events, such as completing a handshake or ratcheting a session key, must occur.
$\zeta$ has a timer system to automatically and locally trigger state transitions or packet resends. This system exists primarily as a solution to the ``two generals problem''. Since ZKE assumes an out-of-order, lossy transport environment it is necessary to have a system of resending lost messages and timing out those resends, similar to TCP. The ZKE timer system is designed to be lightweight, security focused, and simple. It provides strong guarantees about the maximum amount of time that may pass until certain security critical events, such as completing a handshake or ratcheting a session key, must occur.
We assume that the underlying computer running $\zeta$ exposes some mechanism for creating a \emph{timer} which can be \emph{set} to some amount of time, and the computer will \emph{trigger} the timer after approximately that amount of real time has passed.
\begin{itemize}
@@ -470,7 +470,7 @@ At the end of this section we will have defined $\zeta$, which is the high-level
\item States $A_1$, $B_2$ and $A_3$ always set their timeout timer to 10 seconds.
\item States $S_1$, $R_1$ and $R_2$ always set their timeout timer to 1 minute.
\item State $S_2$ sets its timeout timer to a uniform random number in the range 50 minutes to 1 hour. This amount of time is randomly generated each time $\zeta$ enters state $S_2$.
\item In states $A_1$, $A_3$, $S_1$, $R_1$ and $R_2$, $\zeta$ runs an additional ``resend timer''. Whenever this timer triggers, a copy of the respective packet this state generated is sent, and the resend timer is reset. Packets $C_1$, $K_1$ and $K_2$ are re-encrypted under the key exchange key \texttt{kek}, and a fresh encryption counter. States $B_2$ and $S_2$ do not have a resend timer and do not resend packets. The resend timer is always set to 1 second.
\item In states $A_1$, $A_3$, $S_1$, $R_1$ and $R_2$, $\zeta$ runs an additional ``resend timer''. Whenever this timer triggers, a copy of the respective packet that this state generated is sent, and the resend timer is reset. Packets $C_1$, $K_1$ and $K_2$ are re-encrypted under the key exchange key \texttt{kek}, and a fresh encryption counter. States $B_2$ and $S_2$ do not have a resend timer and do not resend packets. The resend timer is always set to 1 second.
\item Any state may trigger timeout early for any reason. In particular it is recommended to timeout state $B_2$ early if memory is being filled with unfinished handshakes. This prevents memory exhaustion attacks.
\end{itemize}
The timer system has the property that all timers are generated locally, checked locally, and are never sent ``over the wire''. This has the advantage of making it difficult to impossible for a remote attacker to manipulate a peer's timers. The system has also been intentionally designed to make it impossible for $\zeta$ to ever get indefinitely stuck in one state. If it could, it would open up the possibility that an attacker could intentionally cause this to happen, which would be a form of a DOS attack. The rekeying timer for state $S_1$ is randomized to deter traffic analysis of ZSSP, and to prevent peers from simultaneously entering state $R_1$, which would be redundant and inefficient.
@@ -484,7 +484,7 @@ At the end of this section we will have defined $\zeta$, which is the high-level
\item When transition $\delta(A_1, \tau)=A_1$ or $\delta(A_3, \tau)=A_1$ occurs, $\zeta$ generates a new $X_1$ packet, and does not reuse the previous $X_1$ packet.
\item The transition, $\delta(R_1, K_1)=R_2$, is only allowed to occur if $\zeta$ acted as Bob, the responder, in the initial Noise XK handshake (i.e. if $\zeta$ has previously been in the $B_2$ state).
\item If $\zeta$ receives a valid $C_1$ packet while in state $S_1$, $S_2$, $R_1$ or $R_2$, $\zeta$ will reply with a new $C_2$ packet encrypted under the most recently created key exchange key, \texttt{kek}.
\item $\zeta$ may only send data transport packets on behalf of the upper protocol when in states $S_1$, $S_2$, $R_1$ and $R_2$.
\item $\zeta$ may only send data transport packets on behalf of the upper protocol when in states $S_1$, $S_2$, $R_1$ and $R_2$. Note that this implies a peer may receive data transport packets in states $A_3$, $S_1$, $S_2$, $R_1$ and $R_2$.
\item If $2^{30}$ or more data transport packets are sent using the same transport key, and $\zeta$ is in state $S_2$, then timeout is triggered early. If $2^{32} - 1$ data transport packets are sent using the same transport key then $\beta$ receives as input symbol $\chi$, which is to say it immediately transitions to state $\bot$ and ends the session.
\end{enumerate}
\end{definition}
@@ -559,7 +559,7 @@ With the necessary background established, we can now define the final component
\item Ephemeral Noise handshake hashes, $h$ and $h'$
\item Temporary ratchet key and fingerprint $\texttt{rk}'\andb \texttt{rf}'$, used so Bob can complete Noise XK before knowing Alice's identity.
\item Role identifier $\texttt{r}$, used so peers are aware whether they were Alice or Bob in Noise XK
\item Comfirmed key index $\texttt{i}$, used so peers know the most recently confirmed Noise key.
\item Confirmed key index $\texttt{i}$, used so peers know the most recently confirmed Noise key.
\end{itemize}
All of these variables are independent \emph{per instance of} $\zeta$. So each instance of $\zeta$ stores a unique and independent set of these variables, and they are never shared between separate instances of $\zeta$. The persistent state variables $\texttt{rk}$ and $\texttt{rf}$ are a partial exception to this rule, as they can be accessed by multiple instances of $\zeta$ over time. However each of these instances must be associated with a single, unique remote peer, and two instances of $\zeta$ must never access the same set of persistent state variables at the same time.
@@ -885,9 +885,9 @@ We want to minimize the number of Noise keys alive in memory for the sake of per
\subsection{Security Flags}\label{sec:security_flags}
ZKE has 4 security flags, $\pi_1, \pi_2, \pi_3 \andb \pi_4$. Each of these flags is a bit that, when sent to 1, change the behavior of the protocol to be more secure. In particular, setting $\pi_2 = \pi_3 = 1$ dramatically improve ZKE's post compromise resistance, and setting $\pi_1 = \pi_4 = 1$ obeys the ``Silence is a Virtue'' principle of security. However setting these flags to 1 come with usability downsides that could make ZKE unsuitable for particular applications or particular users. Hence why these features are flags that the upper protocol is allowed to choose between.
ZKE has 4 security flags, $\pi_1, \pi_2, \pi_3 \andb \pi_4$. Each of these flags is a bit that, when sent to 1, change the behavior of the protocol to be more secure. In particular, setting $\pi_2 = \pi_3 = 1$ dramatically improve ZKE's post-compromise resistance, and setting $\pi_1 = \pi_4 = 1$ obeys the ``Silence is a Virtue'' principle of security. However setting these flags to 1 come with usability downsides that could make ZKE unsuitable for particular applications or particular users. Hence why these features are flags that the upper protocol is allowed to choose between.
Below is a description of each security flag, and the security versus usability tradeoff that occurs when it is set to 1. It should be noted that all communicating peers need not have the exact same set of flags. It will be possible for them to communicate even if their flags differ. Also, A peer may set their flags per remote peer, this is why flags $\pi_3$ and $\pi_4$ are returned by a function that takes as input the identity of the remote peer. This would, for example, allow a user to place much stricter authentication requirements upon a credentials server than a friend's laptop.
Below is a description of each security flag, and the security versus usability tradeoff that occurs when it is set to 1. It should be noted that all communicating peers need not have the exact same set of flags. It will be possible for them to communicate even if their flags differ. Also, a peer may set their flags per remote peer, this is why flags $\pi_3$ and $\pi_4$ are returned by a function that takes as input the identity of the remote peer. This would, for example, allow a user to place much stricter authentication requirements upon a credentials server than a friend's laptop.
\begin{itemize}
\item Hello Requires Recognized Ratchet, $\pi_1$ -- Used in \algorithmref{alg:recv_x1}. When this flag is set Alice is required to present a recognized, nonzero ratchet fingerprint, or else Bob will remain silent. This satisfies the Silence is a Virtue principle of security, since a peer can only acquire a valid ratchet fingerprint through some pre-existing trust relationship. However, this means a legitimate Alice must establish out-of-band an initial ratchet key and fingerprint with Bob. Bob and Alice can share a one time password to be used as the first ratchet key and fingerprint, or Bob can connect to Alice first if Alice has $\pi_1=0$, or Bob can temporarily set $\pi_1=0$ until Alice has completed their first key exchange.
@@ -896,11 +896,11 @@ Below is a description of each security flag, and the security versus usability
\item Responder Silently Rejects, $\pi_4$ -- Used in \algorithmref{alg:recv_x3}. When this flag is set Bob will not send a session rejected packet to Alice, but instead just silently ignore Alice if Bob does not approve of their identity. This also follows the Silence is a Virtue principle, but it prevents Alice from knowing Bob will never allow Alice to connect in their current configuration. In this state Alice will keep attempting the handshake until the upper protocol times-out the session. It is recommended that $\pi_1=\pi_4$ for any given pair of peers.
\end{itemize}
As one might guess, security flag $\pi_1 = 1$ is completely circumvented if a peer acts as Alice in a key exchange. In some circumstances this can be beneficial for establishing the first ratchet key, but in general Alice ought to be more discerning about who they connect with if $\pi_1 = 1$. The upper protocol should take this into account.
As one might guess, having security flag $\pi_1 = 1$ will not protect a peer if they act as Alice in a key exchange. In some circumstances this can be beneficial for establishing the first ratchet key, but in general Alice ought to be more discerning about who they connect with if $\pi_1 = 1$. The upper protocol should take this into account.
When $\pi_2 = \pi_3 = 1$, ZKE strictly requires all users to either have access to reliable persistent storage, or have access to some out-of-band mechanism for one to be reauthenticated in the event they lose or corrupt their persistent storage. This, unfortunately, is not always feasible in a real-time environment. For one thing there are many embedded devices which only have access to persistent ROM, but for another, there are far too many realistic scenarios in which an inexperienced user accidentally loses or corrupts their persistent storage. People often do not realize that they cannot always rollback an application's data directory to a previously backed-up state, and expect the application to work. Furthermore, very few people have redundant drives, as in RAID, so a hard drive failure will likely cause them to lose most if not all of their ratchet keys. When a user can no longer connect with peers because they have corrupted their ratchet keys, some will perceive this as a bug with the application, rather than the protocol working as intended.
Furthermore there are many situations where implementing an out-of-band mechanism for reauthenticating peers would be too impractical or too insecure. While friends and medium sized-teams can simply message each other over a secure channel to request reauthentication, this solution might not scale to a network of thousands of devices, especially when one or more users don't understand why the application stopped working. This kind of situation often unintentionally incentivizes the use of insecure channels for user to request reauthentication, channels vulnerable to phishing or social engineering. In a sense, security flags $\pi_2 \andb \pi_3$ provide an optional, in-band mechanism for users to automatically request reauthentication. While this mechanism is inherently more vulnerable to post-compromise attacks, it is resistent to phishing and social engineering.
Furthermore there are many situations where implementing an out-of-band mechanism for reauthenticating peers would be too impractical or too insecure. While friends and medium sized-teams can simply message each other over a secure channel to request reauthentication, this solution might not scale to a network of thousands of devices, especially when one or more users don't understand why the application stopped working. This kind of situation often unintentionally incentivizes the use of insecure channels for user to request reauthentication, channels vulnerable to phishing or social engineering. In a sense, security flags $\pi_2 \andb \pi_3$ provide an optional, in-band mechanism for users to automatically request reauthentication. While this mechanism is inherently more vulnerable to post-compromise attacks, it is resistant to phishing and social engineering.
With all of those disclaimers out of the way, however, we still highly recommend setting as many of these flags to 1 as practical, and for every flag that is 0, always allow a power user to override the flag to 1. Since peers with different flags can still communicate there is not much point in preventing a confident user from improving their end to end security.
@@ -1001,10 +1001,12 @@ ZSSP supports the partitioning of large packets into smaller fragments, that can
ZFP is responsible for fragmentation within ZSSP. It sits at the very bottom of the ZSSP protocol stack, just above the datagram protocol. We have chosen to put the fragmentation protocol at the bottom of the ZSSP stack, instead of the top for a few reasons. First and foremost it allows key exchange packets to be as large as we need, larger than the MTU of the underlying datagram protocol. This allows us to use P384 and Kyber1024 keys, instead of their smaller and less secure variants. The second reason is that it allows the bandwidth overhead of fragmentation to be extremely small, just 16 bytes per fragment. If the fragmentation protocol were at the top of the stack instead, then each fragment would have required an additional 16 bytes for each MAC. This also benefits efficiency, because it reduces the number of MACs that must be computed and subsequently verified.
Given a packet $P$ from the upper protocol, and a MTU $M$, there is exactly one canonical way to fragment it to fit within the MTU. Set the \emph{fragment count}, $C$, to $\lceil|P|/(M - 16)\rceil$, where $|P|$ is the size of the packet in bytes. The fragments numbered 0 to $|P|\%C - 1$ must have a \emph{payload} of size $\lfloor|P|/C\rfloor + 1$. And fragments numbered $|P|\%C$ to $C - 1$ must have a payload of size $\lfloor|P|/C\rfloor$. When each payload is concatenate in fragment number order, the result must be equal to the original packet, $P$. This fragmentation scheme was chosen to maximize the interoperability of different implementations of this protocol.
Given a packet $P$ from the upper protocol, and a MTU $M$ that is greater than 127, there is exactly one canonical way to fragment it to fit within the MTU. Set the \emph{fragment count}, $C$, to $\lceil|P|/(M - 16)\rceil$, where $|P|$ is the size of the packet in bytes. The fragment count is the total number of fragments we will be splitting the original packet into. Each fragment will be assigned a \emph{fragment number}, from $0$ to $C - 1$, to indicate their order in the original packet. Each fragment will have a 16 byte \emph{fragment header} followed by a \emph{payload}. The fragments assigned numbers 0 to $|P|\%C - 1$ must have a \emph{payload} of size $\lfloor|P|/C\rfloor + 1$. And fragments assigned numbers $|P|\%C$ to $C - 1$ must have a payload of size $\lfloor|P|/C\rfloor$. It can be shown that this guarantees each fragment will at most $M$ bytes long. When each payload is concatenate in fragment number order, the result must be equal to the original packet, $P$. This fragmentation scheme was chosen to prevent miniscule payloads from being generated, to allow implementations to accurately estimate $|P|$ given any one fragment, and to keep fragments from the same packet roughly the same size.
The upper protocol must identify every packet with a \emph{packet nonce}. For each fragmented payload, take the fragment number, fragment count and a packet nonce, and combine them according to \figureref{packet:header} to create a \emph{fragment header}. Then append each payload to its header to create the final set of \emph{fragments}. These fragments are then individually sent to the remote peer over the lower datagram protocol.
Implementations must support a fragment count of at least 48. Implementations may choose and enforce a maximum length of the final reassembled packet. Since fragment numbers and counts are stored as 1 byte integers, the maximum possible fragment count that can be encoded is 256. If the fragment count for a packet is 256, the fragment count field of the packet header should be set to 0. Implementations that choose to support fragment counts of 256 must interpret a 0 in the fragment count field as a fragment count of 256.
\begin{figure}
\caption{Header of the ZeroTier Fragmentation Protocol -- The ``Peer Identifier'' is given by the upper protocol to allow for multiplexing between multiple peers. In ZSSP, the Peer Identifier is set to the most recent key id of the remote peer. The ``Packet Nonce'' is also given by the upper protocol, it is expected to contain metadata on behalf of the upper protocol, and as the name implies it should be unique per packet. \figureref{fig:header_nonce} describes how this Nonce is constructed in ZSSP.}\label{packet:header}
\centering
@@ -1038,7 +1040,7 @@ After all fragments are constructed, AES-256 \cite{fips_aes} is used to directly
Bytes 4 through 20 of a fragment include the fragment number, fragment count, packet nonce, and the first 4 byte of the payload. The packet nonce, in combination with the fragment number, guarantees each block fed into AES will be strictly unique, thereby guaranteeing every header will have a unique encryption. We allow the first 4 bytes of the payload to be included in the block because it is simpler to implement, and it could help in the event a packet nonce is reused due to implementation error.
We proof in \theoremref{theorem:frag_proof} that, under the assumption AES-256 is a pseudo-random permutation, and the header key is indistinguishable from random, that the ZSSP header authentication algorithm is existentially unforgeable under an adaptive chosen message attack. We also demonstrate that the probability an attacker is able to forge a valid header is bound above by the probability that the upper protocol considers a uniform random packet nonce to be valid. For ZSSP, this implies that the probability an attacker forges a valid header is bound above by $\algn{negl}(n) + 2^{-53}$, where $\algn{negl}(n)$ is the probability that an attacker is able to break AES-256.
We proof in \theoremref{theorem:frag_proof} that, under the assumption AES-256 is a pseudo-random permutation, and the header key is indistinguishable from random, that the ZSSP header authentication algorithm is existentially unforgeable under an adaptive chosen message attack. We also demonstrate that the probability an attacker is able to forge a valid header is bound above by the probability that the upper protocol considers a uniform random packet nonce to be valid. For ZSSP, this implies that the probability an attacker forges a valid header is bound above by $\algn{negl}(n) + 2^{-45}$, where $\algn{negl}(n)$ is the probability that an attacker is able to break AES-256.
While this probability is not low enough to be considered cryptographically secure, it is more than plenty for making a defragmentation DOS attack more expensive than a volumetric DDOS attack. It should be emphasized that this authentication mechanism is only sufficient for defending against DOS attacks, ZSSP does not rely on it for anything more than that.
@@ -1051,8 +1053,7 @@ Alice and Bob do not share a header key prior to Alice sending the first Hello p
The Hello packet defragmentation buffer is significantly easier to DOS than a session defragmentation buffer. But so long as Alice can get a single Hello packet through to Bob uncorrupted, Alice will be able to then onwards communicate with their own personal session defragmentation buffer using header authentication. Without at least a symmetric key, we do not believe much more can be done to mitigate DOS attacks against a defragmentation buffer. We cannot use a symmetric key derived from Bob's static public key or identity because this would violate identity hiding, by making it possible for an attacker to test candidate public keys.
The challenge packet, \figureref{packet:challenge}, is small enough that it never requires fragmentation. So Bob does not use the header key to authenticate the header of the challenge packet they send Alice. Bob would not be able to derive the header key anyways, since Bob has not processed Alice's Hello packet. The last 8 bytes of the packet nonce are randomized so challenge packets do not collide with each other.
The Noise XK handshake packets, as specified by Noise, do not use a unique counter, but instead use counter values of 0 or 1. This creates a problem for fragmenting Hello packets, $X_1$ and Response packets, $X_2$, because we need a way for each of these packets to be fragmented with unique packet nonce values, and not just packet nonce values of $0||0$ and $1||0$. Otherwise, these packets will end up colliding and corrupting each other in the defragmentation buffer. For $X_1$ packets, the last 8 bytes of the final AES-GCM MAC is used as the counter value within the packet nonce. This essentially treats the MAC as a random number which is attached to each fragment. The MAC was chosen instead of an actual random number because it allows Bob to trivially authenticate that the received packet nonce is exactly the packet nonce Alice sent, by simply checking if it is equal to the MAC. A similar method was used for $X_2$ packets. However since we use the ZSSP authenticated header algorithm with these fragments, to provide authentication we needed to make sure the counter value used is always in a $2^{24}$ integer range. For this reason we only use the last 3 bytes of the final AES-GCM MAC as the random counter value for the packet nonce. $X_3$ packets do not need a unique packet nonce since, due to the structure of the ZSSP key exchange, the key id will always be unique for each generated $X_3$ packets.
The Noise XK handshake packets, as specified by Noise, do not use a unique counter, but instead use counter values of 0 or 1. This creates a problem for fragmenting Hello packets, $X_1$ and Response packets, $X_2$, because we need a way for each of these packets to be fragmented with unique packet nonce values, and not just packet nonce values of $0||0$ and $1||0$. Otherwise, these packets will end up colliding and corrupting each other in the defragmentation buffer. For $X_1$ packets, the last 8 bytes of the final AES-GCM MAC is used as the counter value within the packet nonce. This essentially treats the MAC as a random number which is attached to each fragment. The MAC was chosen instead of an actual random number because it allows Bob to trivially authenticate that the received packet nonce is exactly the packet nonce Alice sent, by simply checking if it is equal to the MAC. A modification of this policy is used for $X_2$ packets. Since we use ZPF authenticated headers with the fragments of $X_2$ packets, to provide robust authentication we want the counter value used to always be in a $2^{16}$ integer range. For this reason we only use the last 2 bytes of the final AES-GCM MAC as the random counter value for the packet nonce of $X_2$ fragments. The risk of accidental collision is far, far less than that of $X_1$ packets, so we consider 2 bytes sufficient. $X_3$ use a counter value of 0, since, due to the structure of ZKE, a packet nonce of $2||0$ will always be unique for the $X_3$ packet of a given key exchange. Both peers have synchronized state at this point in the handshake, so Alice can statefully prevent two different, colliding $X_3$ packets from being generated.
\subsection{Proof of Security}
@@ -1083,7 +1084,7 @@ We are going to prove that the ZSSP header authentication algorithm is existenti
\end{definition}
\begin{definition}[Pseudorandom Permutation \cite{modern_crypto}]
Given the security paramter $n$, a keyed permutation, $F$, and a probabilistic poly-time distinguishers $D$, define the advantage of $D$ to be:
Given the security parameter $n$, a keyed permutation, $F$, and a probabilistic poly-time distinguishers $D$, define the advantage of $D$ to be:
$$\mathbf{Adv}^\text{ind-prp}_{D,\,F}(n) = |\prob[D^{F_k(\cdot), F_k^{-1}(\cdot)}(1^n) = 1] - \prob[D^{f(\cdot), f^{-1}(\cdot)}(1^n) = 1]|,$$
where $k\gets\$\,\{0,1\}^n$ and $f$ is a truly random permutation.
@@ -1102,7 +1103,7 @@ We are going to prove that the ZSSP header authentication algorithm is existenti
\end{algorithm}
\begin{algorithm}
\caption{$\algn{EncMac}_k(m)$ of the ZSSP header authentication algorithm -- Input $m$ is bytes 4 to 20 of a fragment. Function $F$ is AES-256. For the sake of the security proof we will assume $m$ grows porportionally to $n$.}\label{alg:header_encmac}
\caption{$\algn{EncMac}_k(m)$ of the ZSSP header authentication algorithm -- Input $m$ is bytes 4 to 20 of a fragment. Function $F$ is AES-256. For the sake of the security proof we will assume $m$ grows proportionally to $n$.}\label{alg:header_encmac}
\begin{algorithmic}
\Require $m$
\Ensure $F_k(m)$
@@ -1110,7 +1111,7 @@ We are going to prove that the ZSSP header authentication algorithm is existenti
\end{algorithm}
\begin{algorithm}
\caption{$\algn{Dec}_k(c)$ of the ZSSP header authentication algorithm -- Function $F^{-1}$ is inverse of AES-256. Function $\algn{Vrfy}$ is some algorithm provided by the upper protocol for verifying that the packet nonce is valid. For the sake of the security proof we will be assuming that the size of the packet nonce $N$ is porportional to the security parameter $n$. The version of $\algn{Vrfy}$ used by ZSSP is described by \algorithmref{alg:header_vrfy}.}\label{alg:header_dec}
\caption{$\algn{Dec}_k(c)$ of the ZSSP header authentication algorithm -- Function $F^{-1}$ is inverse of AES-256. Function $\algn{Vrfy}$ is some algorithm provided by the upper protocol for verifying that the packet nonce is valid. For the sake of the security proof we will be assuming that the size of the packet nonce $N$ is proportional to the security parameter $n$. The version of $\algn{Vrfy}$ used by ZSSP is described by \algorithmref{alg:header_vrfy}.}\label{alg:header_dec}
\begin{algorithmic}
\Require $c$
\State $m \gets F_k^{-1}(c)$
@@ -1170,7 +1171,7 @@ We are going to prove that the ZSSP header authentication algorithm is existenti
\end{proof}
\begin{algorithm}
\caption{The implementation of $\algn{Vrfy}(N)$ for ZSSP -- We are assuming that the input $N$ is being interpretted as $p||c$, the packet nonce construction of \figureref{fig:header_nonce}. $D$ is a stateful, finite array of integers, initialized to -1, that stores the value of previously authenticated counters. ZSSP explicitly does not verify that the padding is zero, for the sake of possible future revisions.}\label{alg:header_vrfy}
\caption{The implementation of $\algn{Vrfy}(N)$ for ZSSP -- We are assuming that the input $N$ is being interpreted as $p||c$, the packet nonce construction of \figureref{fig:header_nonce}. $D$ is a stateful, finite array of integers, initialized to -1, that stores the value of previously authenticated counters. $D$ is updated after ZSSP decrypts a received packet. ZSSP explicitly does not verify that the padding is zero, for the sake of possible future revisions.}\label{alg:header_vrfy}
\begin{algorithmic}
\Require $p||c$
\State $b \gets 0$
@@ -1196,51 +1197,14 @@ We are going to prove that the ZSSP header authentication algorithm is existenti
\begin{flalign*}
\prob[\algn{Auth}_{\mathcal{A},\, \Pi}(n) = 1] &\leq \mathbf{Adv}^\text{ind-prp}_{D,\,F}(n) + \prob[\algn{Vrfy}(N) = 1] \\
&\leq \algn{negl}(n) + \frac{8}{2^8}\cdot\frac{2^{24}}{2^{64}} \\
&= \algn{negl}(n) + 2^{-53}.
&\leq \algn{negl}(n) + 2^{-5}\cdot 2^{-40} \\
&= \algn{negl}(n) + 2^{-45}.
\end{flalign*}
If we assume the adversary does make oracle queries, then their advantage bound is only negligibly larger than the advantage above.
The security experiment we have used does not consider replay protection, however it should be clear that \algorithmref{alg:header_vrfy} does indeed provide replay protection. Once any packet is fully processed and authenticated by ZSSP, it is the case that its packet nonce will no longer be valid according to \algorithmref{alg:header_vrfy}. This property very much relies upon the fact that the packet nonce contains both the packet type and the counter. This prevents a type of DOS attack where the attacker simply replays valid fragments to keep the defragmentation buffer full.
%We believe that this fragmentation protocol has some advantages over the alternative solutions, like Noise-encrypting individual fragments. If we were to Noise-encrypt individual fragments, we would have to send at least the key id, the counter and the packet type in plaintext. This metadata is not particularly sensitive, but it does enable more efficient traffic analysis. The only plaintext data sent within ZSSP is the key id, and the one-time header Alice sends to Bob to initiate a session, \emph{everything} else is encrypted
%\subsection{Data Injection Attacks}\label{sec:frag_inject}
%Noise guarantees cryptographic authenticity of all of its transport packets. This guarantees that if an attacker is able to inject bad fragments into a ciphertext, the ciphertext will to fail to decrypt. When a ciphertext fails to decrypt, ZSSP will immediately drop it, as well as reset all state that was modified by it while it was being reassembled. This ensures that to the application, ZSSP behaves as though bad packets are never received in the first place.
%Defragmentation buffers are inherently vulnerable to data injection attacks, so the output of a session's defragmentation buffer is treated as hostile within ZSSP, and it will immediately pass through Noise authentication before being accepted.
%Within the first message of the Noise XK handshake, Alice generates a random ``header key'' and sends it as a Noise encrypted payload to Bob. Once Bob receives this key, from that point onward all fragments sent between Alice and Bob must have bytes 4 to 20 encrypted in place by the AES block cipher using the header key. When a party sends a packet, it will be Noise-encrypted, fragmented, have a plaintext header prepended to it, and as a final step, a majority of that header will be encrypted under the header key. When a party receives a packet, they will first use the unencrypted session id to identify which session sent it, use the header key associated with that session to decrypt the rest of the header, perform a validity check on the decryption, check the counter value for uniqueness, and if all that succeeds it will finally be allowed to enter the defragmentation buffer.
%Let us address now how this protocol mitigates our two attacks of concern. Metadata fingerprinting we believe is infeasible under this scheme. The only data sent in plaintext is the randomized session id number. So without the header key, all an attacker can learn from the fragmentation metadata is whether or not it is from the same ZSSP session as other fragments they have seen. Included in the encrypted section of the header is the Noise counter value, as well as the fragment number. These two numbers combined are strictly unique for every single fragment ZSSP produces. Therefore every 16 byte plaintext being encrypted is unique, which means no AES encrypted header will ever be repeated. This prevents an attacker from obtaining partial information about a fragment by exploiting repeated AES outputs. In this sense this method of encryption is AES-ECB encryption, where we have guaranteed each plaintext block going into AES-ECB is strictly unique.
%Under the assumption that the AES block cipher is a pseudo-random permutation, it can be straightforwardly demonstrated that this stateful fragmentation algorithm is CCA-indistinguishable authenticated encryption. This security experiment does not capture all possible forms of attack that can be performed against ZSSP fragmentation, but we consider it a good sanity check in leui of a full protocol security proof.
%In preparation for a full protocol security proof, our fragmentation algorithm has been specifically designed to be independent and ``outside the envelope'' of both the key exchange and of data transport. It follows then, in the security environments we care about, any attack against ZSSP with fragmentation can be formally reduced to an attack against ZSSP without fragmentation. This is due to the fact that any attacker can accurately simulate the ZSSP fragmentation algorithm to another attacker when given access to the header key. Since the header key is derived from one-way function KBKDF, it follows that an attacker with access to the header key has negligible advantage to an attacker without it. This rough proof sketch gives us high confidence, in leui of a full protocol security proof, that our fragmentation algorithm does not compromise the overall security of ZSSP.
%Our fragmentation algorithm makes manipulating the defragmentation buffer a non-viable DOS vector. An attacker cannot replay a valid fragment header because each header contains the Noise counter, and this fragment is dropped if this counter is replayed. And an attacker cannot forge a valid header without access to the header key. The only attack we believe this leaves an attacker is a brute force attack. The header key is 32 bytes long, so it is infeasible to brute force. Instead, the attacker must brute force the header itself. In order to brute force a valid header, the packet type must be a ZSSP-recognized type, and the Noise counter must be within the allowed window of counters. We do not allow a Noise counter value to be greater than $2^{24}$ plus the last received counter for this very reason. The exact size of the search space that would have to be brute forced is hard to compute since counters are nondeterministic in out-of-order Noise, but we estimate an attacker would have to send at least $2^{48}$ fragments to get just one of them injected into the defragmentation buffer. While this search space is not large enough to be cryptographically secure, it is large enough to make this a non-viable DOS vector.
%We have mentioned a few times that this protocol becomes broken if an attacker acquires the header key, so it is worth discussing how difficult that is. Since it is derived from the chaining key used to encrypt the first payload of the Noise handshake, we can determine its confidentiality based on Noise's confidentiality properties. We have that the header key is: encrypted to a known recipient, forward secrecy for sender compromise only, vulnerable to replay. Considering the classes of attacks we are trying to defend against, we are not concerned about the header key's lack of forward secrecy. Furthermore, Bob's reply to Alice's first message has a unique packet type that is put in the header of its fragments. So the lack of replay protection means that an attacker can cause Bob to send them valid headers marked with this specific packet type, but they cannot acquire valid headers marked with any other packet type. A fragment with this packet type is rejected by Alice if Bob has already completed the Noise handshake. Therefore, an attacker has only a brief window of time to obtain enough valid header encryptions to DOS the defragmentation buffer with. Once the handshake is finished, all of the headers obtained via replay attacks will be useless, as both Alice and Bob will instantly drop them.
%Another concern with this protocol is that it might not properly bind a fragment header to a specific fragment. So an attacker can swap the headers of fragments with the headers of other fragments. The packet type and Noise counter are authenticated during Noise decryption, so it is not possible for an attacker to manipulate these values. However an attacker could possibly swap around the fragment count and fragment number fields in a header to make a given packet be reassembled in the wrong order. Again this kind of attack would cause Noise decryption to safely fail, but it does pose as a DOS vector. Ultimately we have decided against attempting to mitigate this type of attack, because we believe it is impractical to defend against a DOS attack from an attacker with enough network control to corrupt fragments in flight.
%\subsection{Transport Guarantees}
%The upper protocol, as in the protocol that is running on top of ZSSP, is able to send data payloads through ZSSP, and receive them at their destination. ZSSP guarantees to the upper protocol authenticity and confidentiality for sent data payloads. ZSSP does not guarantee in-order transport of data payloads, nor does it guarantee losslessness of data payloads. Even if the underlying transport environment does guarantee in-order, lossless transport, there are rare situations where ZSSP can reorder or drop packets. These are usually either because of the presence of an attacker within the transport environment, or because of multithreading nondeterminism.
%Noise requires that if the 64 bit counter is replayed, that the packet be ignored. In an out of order environment however, where counters can arrive out-of-order, this is quite challenging to do without having to store in memory every single 64-bit counter previously seen by the decrypting party. To handle this, ZSSP uses a datastructure that only saves a fixed number of counters to memory, and is allowed to falsely reporting a counter as being replayed. This means that if many packets are encrypted by ZSSP, and the first packet encrypted arrives to the receiver last, it may falsely be considered replayed and be ignored.
%The current implementation of ZSSP guarantees if a packet is re-ordered by less than 32 packets, it will be accepted by the remote party. Any more than that and it is undefined whether or not the packet will be accepted. Users of ZSSP should be aware they cannot hold onto ZSSP encryptions and expect them to be acceptable by the remote party after some amount of time passes. ZSSP encrypted packets are expected to be immediately sent to the other party. If the upper protocol needs to hold onto a packet to be sent or resent at a later time, the upper protocol should hold onto the plaintext of that packet, and encrypted it with ZSSP the moment or moments they want to send it.
%\section{Conclusion}
%The ratchet key of ZSSP somewhat resembles a resumption PSK \url{https://github.com/noiseprotocol/noise_wiki/wiki/PSK-Resumption}. This suggests it is possible to extend ZSSP to allow for 0-RTT session resumption. However it is unclear whether or not such a protocol extension can be designed to preserve all of the security properties of ZSSP. It is also unlikely that 0-RTT session resumption could be implemented within a small code footprint.
%Another area that could be worth exploring is if the security properties of ZSSP can be achieved in a setting without a pre-shared static key. The most straightforward approach would be to replace the Noise XK handshake at the beginning of ZSSP with a Noise XX handshake, though it would have to be shown that the lack of authenticated encryption upon the ratchet fingerprint does not enable some form of replay attack. A different approach may be more fruitful.
%A formal proof of security is the clear next step for ZSSP.
The range $2^{24}$ was chosen because changing its value makes the protocol worst. One byte less of range, $2^{16}$, makes it possible for one peer to have $2^{16}$ of their packets dropped. This would put their counter out of the allowed range, and cause all future packets to be dropped. One byte more of range, $2^{32}$, makes it easier than it need to be for an attacker to forge a valid header.
The security experiment we have used does not consider replay protection, however it should be clear that \algorithmref{alg:header_vrfy} does indeed provide replay protection. Once any packet is fully authenticated by ZSSP, and its counter is added to array $D$, it is the case that its packet nonce will no longer be valid according to \algorithmref{alg:header_vrfy}. If a packet is dropped or corrupted, its fragments could only be replayed until some other received packet updated the relevant index of $D$. This prevents a type of DOS attack where the attacker simply replays valid fragments to keep the defragmentation buffer full.
\medskip