diff --git a/performance/src/fragged.rs b/performance/src/fragged.rs index b3c293a..59b876d 100644 --- a/performance/src/fragged.rs +++ b/performance/src/fragged.rs @@ -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 = ArrayVec; -/// Fast packet defragmenter +/// Fast packet defragmenter. pub struct Fragged { - nonce: [u8; 10], - count: u8, + nonce: u64, + count: u32, have: u64, - size: usize, frags: [MaybeUninit; MAX_FRAGMENTS], } impl Fragged { 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 Fragged { /// 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, ) { 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 Fragged { } self.have = 0; self.count = 0; - self.nonce = [0; 10]; - self.size = 0; + self.nonce = u64::MAX; } } diff --git a/performance/src/indexed_heap.rs b/performance/src/indexed_heap.rs index 2b6b0a1..b37dc70 100644 --- a/performance/src/indexed_heap.rs +++ b/performance/src/indexed_heap.rs @@ -32,7 +32,6 @@ pub struct IndexedBinaryHeap { map: Vec<(usize, u64)>, } -#[allow(unused)] impl IndexedBinaryHeap { /// Create a new, empty binary heap. pub fn new() -> Self { @@ -189,7 +188,6 @@ impl IndexedBinaryHeap { /// 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

) -> Option

{ if let Some(data_idx) = self.deref_index(idx) { if let Some(new_priority) = f(&self.data[data_idx].1) { diff --git a/performance/src/result.rs b/performance/src/result.rs index 061c89a..007d9f2 100644 --- a/performance/src/result.rs +++ b/performance/src/result.rs @@ -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, diff --git a/performance/src/zeta.rs b/performance/src/zeta.rs index 91a9e36..ce2d6d4 100644 --- a/performance/src/zeta.rs +++ b/performance/src/zeta.rs @@ -1828,9 +1828,16 @@ impl Session { } } -impl std::fmt::Debug for Session where Crypto::SessionData: std::fmt::Debug { +impl std::fmt::Debug for Session +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 std::fmt::Debug for ZetaAutomata { diff --git a/performance/src/zssp.rs b/performance/src/zssp.rs index f633f5e..6ae7018 100644 --- a/performance/src/zssp.rs +++ b/performance/src/zssp.rs @@ -275,7 +275,7 @@ impl Context { 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 Context { 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 Context { let mut buffer = ArrayVec::::new(); let assembled_packet = if fragment_count > 1 { zeta.defrag.lock().unwrap().assemble( - &nonce, + incoming_counter, incoming_fragment_buf, fragment_no, fragment_count, diff --git a/reference/src/result.rs b/reference/src/result.rs index 2606e3b..4fa54f3 100644 --- a/reference/src/result.rs +++ b/reference/src/result.rs @@ -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), /// The received packet was some authentic protocol control packet. No action needs to be taken. Control, diff --git a/whitepaper/zssp.pdf b/whitepaper/zssp.pdf index 0b93555..694dd6c 100644 Binary files a/whitepaper/zssp.pdf and b/whitepaper/zssp.pdf differ diff --git a/whitepaper/zssp.tex b/whitepaper/zssp.tex index 8a3fe82..4ecf5a5 100644 --- a/whitepaper/zssp.tex +++ b/whitepaper/zssp.tex @@ -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} @@ -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} @@ -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 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. $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