diff --git a/performance/src/application.rs b/performance/src/application.rs index b69a699..b1f8bc3 100644 --- a/performance/src/application.rs +++ b/performance/src/application.rs @@ -231,7 +231,7 @@ pub trait ApplicationLayer: Sized { fn initiator_disallows_downgrade(&mut self, session: &Arc>) -> bool; /// Function to accept sessions after final negotiation. /// - /// The implementor must verify that three arguments, `remote_static_key`, `identity` and + /// The implementor must verify that the 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 @@ -359,14 +359,19 @@ pub trait ApplicationLayer: Sized { /// .map_err(to_err)?; /// let mut rows = stmt.query([peer]).map_err(to_err)?; /// if let Some(row) = rows.next().map_err(to_err)? { - /// let peer_fp1: &[u8] = row.get_ref(0).map_err(to_err)?.as_bytes().map_err(to_err)?; - /// let peer_fp2: Option<&[u8]> = row.get_ref(1).map_err(to_err)?.as_bytes_or_null().map_err(to_err)?; + /// let peer_fp1 = row.get_ref(0).map_err(to_err)?.as_bytes_or_null().map_err(to_err)?; + /// let peer_fp2 = row.get_ref(1).map_err(to_err)?.as_bytes_or_null().map_err(to_err)?; + /// let peer_fp1 = if let Some(fp1) = peer_fp1 { + /// fp1.try_into().map_err(to_err)? + /// } else { + /// &[0u8; RATCHET_SIZE] + /// }; /// let peer_fp2 = if let Some(fp2) = peer_fp2 { /// Some(fp2.try_into().map_err(to_err)?) /// } else { /// None /// }; - /// if !update_data.compare_fingerprints(peer_fp1.try_into().map_err(to_err)?, peer_fp2) { + /// if !update_data.compare_fingerprints(peer_fp1, peer_fp2) { /// return Ok(false); /// } /// } else { diff --git a/performance/src/ratchet_state.rs b/performance/src/ratchet_state.rs index 3f07290..08aee23 100644 --- a/performance/src/ratchet_state.rs +++ b/performance/src/ratchet_state.rs @@ -43,11 +43,11 @@ impl RatchetState { chain_len, } } - /// Creates a new "empty" ratchet state, where the ratchet fingerprint is the - /// empty string, the ratchet key is all zeros, and the chain length is 0. + /// Creates a new "zero" ratchet state, where the ratchet fingerprint is all zeros, + /// the ratchet key is all zeros, and the chain length is 0. /// /// This value is the default value of `RatchetState`. - pub fn empty() -> Self { + pub fn zero() -> Self { RatchetState { key: Zeroizing::new([0u8; RATCHET_SIZE]), fingerprint: Zeroizing::new([0u8; RATCHET_SIZE]), @@ -85,9 +85,7 @@ impl RatchetState { &self.key } /// The ratchet fingerprint for this ratchet state. - /// - /// If this returns `None` then the ratchet fingerprint is the empty string. - /// This is the "empty" ratchet state and the key will be all zeros. + /// It could be all zeros if this is the "zero" ratchet state. /// /// The ratchet fingerprint value is sensitive and should be hidden, /// but the security of ZSSP can survive having this value leaked. @@ -104,13 +102,13 @@ impl RatchetState { pub fn chain_len(&self) -> u64 { self.chain_len } - /// Returns true if this is the "empty" ratchet state, where the ratchet fingerprint is the - /// empty string, the ratchet key is all zeros, and the chain length is 0. - pub fn is_empty(&self) -> bool { + /// Returns true if this is the "zero" ratchet state, where the ratchet fingerprint is all zeros, + /// the ratchet key is all zeros, and the chain length is 0. + pub fn is_zero(&self) -> bool { secure_eq(self.fingerprint(), &[0u8; RATCHET_SIZE]) } /// Checks if the fingerprint of this ratchet state equals the - /// fingerprint contained in argument `rf`. + /// fingerprint contained in argument `rf`. /// /// Uses constant time equality. pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool { @@ -134,7 +132,7 @@ impl RatchetState { } impl Default for RatchetState { fn default() -> Self { - Self::empty() + Self::zero() } } @@ -162,7 +160,7 @@ impl RatchetStates { /// /// This value is the default value of `RatchetStates`. pub fn new_initial_states() -> Self { - Self { state1: RatchetState::empty(), state2: None } + Self { state1: RatchetState::zero(), state2: None } } /// Creates a new initial pair of ratchet states from a one-time password. /// The first ratchet state will be derived from this password, while the second will be `None`. @@ -274,7 +272,7 @@ impl<'a> CompareAndSwap<'a> { /// There may be a second ratchet fingerprint to be deleted, which function /// `deleted_fingerprint2` will return. pub fn deleted_fingerprint1(&self) -> Option<&[u8; RATCHET_SIZE]> { - if self.cur_state1_was_just_deleted && !self.cur_state1.is_empty() { + if self.cur_state1_was_just_deleted && !self.cur_state1.is_zero() { return Some(self.cur_state1.fingerprint()); } None @@ -290,7 +288,7 @@ impl<'a> CompareAndSwap<'a> { pub fn deleted_fingerprint2(&self) -> Option<&[u8; RATCHET_SIZE]> { if self.cur_state2_was_just_deleted { if let Some(rf) = self.cur_state2 { - if !rf.is_empty() { + if !rf.is_zero() { return Some(rf.fingerprint()); } } @@ -305,7 +303,7 @@ impl<'a> CompareAndSwap<'a> { /// then the peer should be added to storage with the new ratchet states specified by this /// `CompareAndSwap` struct. pub fn cur_is_initial_states(&self) -> bool { - self.cur_state1.is_empty() && self.cur_state2.is_none() + self.cur_state1.is_zero() && self.cur_state2.is_none() } /// Compares the ratchet fingerprints of `cur_state1` and `cur_state2` with `rf1` and `rf2`. /// If they are equal this function will return `true`. diff --git a/performance/src/zeta.rs b/performance/src/zeta.rs index bd8f7d2..1d42bc4 100644 --- a/performance/src/zeta.rs +++ b/performance/src/zeta.rs @@ -870,7 +870,7 @@ pub(crate) fn received_x3_trans>( let mut should_warn_missing_ratchet = false; if (zeta.ratchet_state != state1) & (Some(&zeta.ratchet_state) != state2.as_ref()) { - if !responder_disallows_downgrade && zeta.ratchet_state.is_empty() { + if !responder_disallows_downgrade && zeta.ratchet_state.is_zero() { should_warn_missing_ratchet = true; } else { if !responder_silently_rejects { diff --git a/reference/src/application.rs b/reference/src/application.rs index 67c0151..d3acf59 100644 --- a/reference/src/application.rs +++ b/reference/src/application.rs @@ -155,6 +155,9 @@ pub trait ApplicationLayer: 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 @@ -171,13 +174,23 @@ pub trait ApplicationLayer: 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>) -> 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 `remote_static_key` and `identity` 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. + /// + /// Corresponds to the **Accept** call of Transition Algorithm 4 within the ZSSP whitepaper. fn check_accept_session( &mut self, remote_static_key: &::PublicKey, @@ -190,6 +203,20 @@ pub trait ApplicationLayer: 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], @@ -198,8 +225,9 @@ pub trait ApplicationLayer: Sized { /// This function will be called whenever Alice attempts to open a session, or Bob attempts /// to verify Alice's identity. /// - /// If the peer's ratchet states could not be could, this function should return - /// `RatchetState::new_initial_states()`. + /// If the peer's ratchet states could not be found, this function should return `None`. + /// A return value of `None` is equivalent to a return value of + /// `Some(RatchetState::new_initial_states())`. /// /// If a one-time-password has been pre-shared with this peer, `RatchetState::new_otp_states(...)` /// should be pre-saved to the storage backend as if it is a normal ratchet state. @@ -208,6 +236,8 @@ pub trait ApplicationLayer: 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: &::PublicKey, @@ -242,6 +272,81 @@ pub trait ApplicationLayer: Sized { /// it should be assumed that they also have the same ratchet key. /// /// The implementations of `PartialEq` for `RatchetState` and `RatchetStates` do this by default. + /// + /// # Example + /// The following code is an example of how to implement this database operation with the + /// library `rustqlite`, an interface for SQLite in rust. + /// This code will not work out-of-the-box, it must be adapted based on how your application + /// structures its SQLite database. + /// Notably, this code lacks a means of securely indexing ratchet fingerprints. + /// The function `get_peer_primary_key` is a placeholder for however your application defines + /// the SQL primary key for a peer. The function `get_sql_conn` is a placeholder getter method + /// on `self` for retrieving the database connection object. + /// ```rs + /// fn to_err(e: E) -> Error { + /// Error::new(std::io::ErrorKind::Other, e) + /// } + /// + /// fn save_ratchet_state( + /// &mut self, + /// remote_static_key: &P384PublicKey, + /// session_data: &Peer, + /// update_data: CompareAndSwap<'_>, + /// ) -> Result { + /// let peer = get_peer_primary_key(remote_static_key, session_data); + /// let mut conn = get_sql_conn(self); + /// // rustqlite will rollback this transaction if this struct is dropped. + /// let trans = conn + /// .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) + /// .map_err(to_err)?; + /// { + /// // Compare + /// let mut stmt = trans + /// .prepare_cached("SELECT ratchet_fp1, ratchet_fp2 FROM peers WHERE peer = ?1") + /// .map_err(to_err)?; + /// let mut rows = stmt.query([peer]).map_err(to_err)?; + /// if let Some(row) = rows.next().map_err(to_err)? { + /// let peer_fp1 = row.get_ref(0).map_err(to_err)?.as_bytes_or_null().map_err(to_err)?; + /// let peer_fp2 = row.get_ref(1).map_err(to_err)?.as_bytes_or_null().map_err(to_err)?; + /// let peer_fp1 = if let Some(fp1) = peer_fp1 { + /// fp1.try_into().map_err(to_err)? + /// } else { + /// &[0u8; RATCHET_SIZE] + /// }; + /// let peer_fp2 = if let Some(fp2) = peer_fp2 { + /// Some(fp2.try_into().map_err(to_err)?) + /// } else { + /// None + /// }; + /// if !update_data.compare_fingerprints(peer_fp1, peer_fp2) { + /// return Ok(false); + /// } + /// } else { + /// return Ok(false); + /// } + /// } + /// { + /// // Swap + /// let ns1 = update_data.new_state1; + /// let ns2 = update_data.new_state2; + /// let mut stmt = trans.prepare_cached("UPDATE peers SET ratchet_fp1 = ?1, ratchet_key1 = ?2, chain_len1 = ?3, ratchet_fp2 = ?4, ratchet_key2 = ?5, chain_len2 = ?6 WHERE salted_addr = ?7").map_err(to_err)?; + /// stmt.execute(( + /// ns1.fingerprint(), + /// ns1.key(), + /// ns1.chain_len(), + /// ns2.map(RatchetState::fingerprint), + /// ns2.map(RatchetState::key), + /// ns2.map(RatchetState::chain_len), + /// salted_addr, + /// )) + /// .map_err(to_err)?; + /// } + /// // SQLite does its best to make sure committed transactions are actually written + /// // to disk: https://www.sqlite.org/howtocorrupt.html. + /// trans.commit().map_err(to_err)?; + /// Ok(true) + /// } + /// ``` fn save_ratchet_state( &mut self, remote_static_key: &::PublicKey, diff --git a/reference/src/ratchet_state.rs b/reference/src/ratchet_state.rs index 204852e..573767a 100644 --- a/reference/src/ratchet_state.rs +++ b/reference/src/ratchet_state.rs @@ -42,11 +42,11 @@ impl RatchetState { chain_len, } } - /// Creates a new "empty" ratchet state, where the ratchet fingerprint is the - /// empty string, the ratchet key is all zeros, and the chain length is 0. + /// Creates a new "zero" ratchet state, where the ratchet fingerprint is all zeros, + /// the ratchet key is all zeros, and the chain length is 0. /// /// This value is the default value of `RatchetState`. - pub fn empty() -> Self { + pub fn zero() -> Self { RatchetState { key: Zeroizing::new([0u8; RATCHET_SIZE]), fingerprint: Zeroizing::new([0u8; RATCHET_SIZE]), @@ -82,9 +82,7 @@ impl RatchetState { &self.key } /// The ratchet fingerprint for this ratchet state. - /// - /// If this returns `None` then the ratchet fingerprint is the empty string. - /// This is the "empty" ratchet state and the key will be all zeros. + /// It could be all zeros if this is the "zero" ratchet state. /// /// The ratchet fingerprint value is sensitive and should be hidden, /// but the security of ZSSP can survive having this value leaked. @@ -101,13 +99,13 @@ impl RatchetState { pub fn chain_len(&self) -> u64 { self.chain_len } - /// Returns true if this is the "empty" ratchet state, where the ratchet fingerprint is the - /// empty string, the ratchet key is all zeros, and the chain length is 0. - pub fn is_empty(&self) -> bool { + /// Returns true if this is the "zero" ratchet state, where the ratchet fingerprint is all zeros, + /// the ratchet key is all zeros, and the chain length is 0. + pub fn is_zero(&self) -> bool { secure_eq(self.fingerprint(), &[0u8; RATCHET_SIZE]) } /// Checks if the fingerprint of this ratchet state equals the - /// fingerprint contained in argument `rf`. + /// fingerprint contained in argument `rf`. /// /// Uses constant time equality. pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool { @@ -131,7 +129,7 @@ impl RatchetState { } impl Default for RatchetState { fn default() -> Self { - Self::empty() + Self::zero() } } @@ -159,7 +157,7 @@ impl RatchetStates { /// /// This value is the default value of `RatchetStates`. pub fn new_initial_states() -> Self { - Self { state1: RatchetState::empty(), state2: None } + Self { state1: RatchetState::zero(), state2: None } } /// Creates a new initial pair of ratchet states from a one-time password. /// The first ratchet state will be derived from this password, while the second will be `None`. @@ -271,7 +269,7 @@ impl<'a> CompareAndSwap<'a> { /// There may be a second ratchet fingerprint to be deleted, which function /// `deleted_fingerprint2` will return. pub fn deleted_fingerprint1(&self) -> Option<&[u8; RATCHET_SIZE]> { - if self.cur_state1_was_just_deleted && !self.cur_state1.is_empty() { + if self.cur_state1_was_just_deleted && !self.cur_state1.is_zero() { return Some(self.cur_state1.fingerprint()); } None @@ -287,7 +285,7 @@ impl<'a> CompareAndSwap<'a> { pub fn deleted_fingerprint2(&self) -> Option<&[u8; RATCHET_SIZE]> { if self.cur_state2_was_just_deleted { if let Some(rf) = self.cur_state2 { - if !rf.is_empty() { + if !rf.is_zero() { return Some(rf.fingerprint()); } } @@ -302,7 +300,7 @@ impl<'a> CompareAndSwap<'a> { /// then the peer should be added to storage with the new ratchet states specified by this /// `CompareAndSwap` struct. pub fn cur_is_initial_states(&self) -> bool { - self.cur_state1.is_empty() && self.cur_state2.is_none() + self.cur_state1.is_zero() && self.cur_state2.is_none() } /// Compares the ratchet fingerprints of `cur_state1` and `cur_state2` with `rf1` and `rf2`. /// If they are equal this function will return `true`. diff --git a/reference/src/zeta.rs b/reference/src/zeta.rs index 776e194..8f50a3f 100644 --- a/reference/src/zeta.rs +++ b/reference/src/zeta.rs @@ -734,7 +734,7 @@ pub(crate) fn received_x3_trans>( let mut should_warn_missing_ratchet = false; if (zeta.ratchet_state != state1) & (Some(&zeta.ratchet_state) != state2.as_ref()) { - if !responder_disallows_downgrade && zeta.ratchet_state.is_empty() { + if !responder_disallows_downgrade && zeta.ratchet_state.is_zero() { should_warn_missing_ratchet = true; } else { if !responder_silently_rejects { diff --git a/whitepaper/zssp.pdf b/whitepaper/zssp.pdf index dbc27e1..03489f3 100644 Binary files a/whitepaper/zssp.pdf and b/whitepaper/zssp.pdf differ diff --git a/whitepaper/zssp.tex b/whitepaper/zssp.tex index 43fb96f..d83ee9e 100644 --- a/whitepaper/zssp.tex +++ b/whitepaper/zssp.tex @@ -98,7 +98,7 @@ To motivate the need for ASKs further, consider that many authenticated key exch The maintainers of the Noise protocol are aware of the need for ASKs, and are in the process of extending the protocol with an ASK mechanism \cite{noise_ask}. However, at this point in time this mechanism is still unofficial and may be replaced. It also uses specific capabilities of \text{HKDF} that \text{KBKDF} does not have. For that reason we have created our own ASK extension to the Noise protocol, based on the unofficial mechanism. -We add a function, $\text{GetAsk}(\texttt{label})$, to the Noise protocol that takes as input a static label, and produces as output two 256-bit keys. This function is implemented as a component of the Noise handshake state machine, and so it has access to the most recent values of the Noise handshake hash, \texttt{h}, and the Noise chaining key, \texttt{ck}. To preserve consistency with `MixKey' and `Split', we will produce two 512-bit hashes, and then truncate each to 256-bits to produce two keys. +We add a function, $\text{GetAsk}(\texttt{label})$, to the Noise protocol that takes as input a static string, and produces as output two 256-bit keys. This function is implemented as a component of the Noise handshake state machine, and so it has access to the most recent values of the Noise handshake hash, \texttt{h}, and the Noise chaining key, \texttt{ck}. To preserve consistency with `MixKey' and `Split', we will produce two 512-bit hashes, and then truncate each to 256-bits to produce two keys. $$\text{GetAsk}(\texttt{label}) := \text{KBKDF}(\texttt{h}, \texttt{label}, \texttt{ck}, 1024).$$ @@ -519,15 +519,15 @@ The following are the 4 cryptographic primitives ZKE is based upon. We will be a \end{definition} \begin{definition}[H][KBKDF \cite{fips_kbkdf}] - Let $\KDF(\textit{K}_{\textit{IN}}, \textit{Label}, \textit{Context}, N)$ be the KBKDF key derivation algorithm, instantiated in HMAC-Counter mode using SHA-512. Variable $\textit{K}_{\textit{IN}}$ is the input key material, $\textit{Label}$ is some static label, $\textit{Context}$ is additional input key material, and $N$ is the number of 512-bit outputs to be produced. $\KDF(\textit{K}_{\textit{IN}}, \textit{Label}, \textit{Context}, N)$ produces as output $(x_1,\ldots,x_N)$, a tuple of $N$ 512-bit outputs. + Let $\KDF(\textit{K}_{\textit{IN}}, \textit{Label}, \textit{Context}, N)$ be the KBKDF key derivation algorithm, instantiated in HMAC-Counter mode using SHA-512. Variable $\textit{K}_{\textit{IN}}$ is the input key material, $\textit{Label}$ is some static string, $\textit{Context}$ is additional input key material, and $N$ is the number of 512-bit outputs to be produced. $\KDF(\textit{K}_{\textit{IN}}, \textit{Label}, \textit{Context}, N)$ produces as output $(x_1,\ldots,x_N)$, a tuple of $N$ 512-bit outputs. - Given $i\in \{1,\ldots,N\}$, we will use the notation $\KDF(\textit{K}_{\textit{IN}}, \textit{Label}, \textit{Context}, N)_i$ to refer to the $i$th output of $\KDF$. When assigning a single 512-bit output to a variable or field that is 256-bits in size, it is assumed that the output is being truncated to just the first 256-bits. + Given $i\in \{1,\ldots,N\}$, we will use the notation $\KDF(\textit{K}_{\textit{IN}}, \textit{Label}, \textit{Context}, N)_i$ to refer to the $i$th output of $\KDF$. When assigning a 512-bit output to a variable or field that is 256-bits in size, it is assumed that the output is being truncated to just the first 256-bits. So $k\gets \KDF(x, l, y, 3)_2$ represents computing KBKDF on inputs $\textit{K}_{\textit{IN}}= x,\, \textit{Label}=l,\, \textit{Context}=y$ and $L=3\cdot 512$, and setting variable $k$ equal to bits $512$ to $1023$ of the output. If $k$ is a variable of size 256-bits, then $k$ is set equal to bits $512$ to $767$ of the output. \end{definition} \begin{definition}[H][AES-GCM \cite{fips_aesgcm}] - Let $\AEAD(K, N, H, M)$ be the AES-GCM Authenticated Encryption with Additional Data algorithm, where $K$ is the encryption key, $N$ is the nonce or IV, $H$ is the additional authentication data, and $M$ is the plaintext message to be encrypted. $\AEAD(K, N, H, M)$ produces as output $c||t$, a ciphertext, $c$, concatenated with its 128-bit (16 byte) authentication tag, $t$. + Let $\AEAD(K, N, H, M)$ be the AES-GCM Authenticated Encryption with Additional Data algorithm, where $K$ is the encryption key, $N$ is the AEAD nonce, $H$ is the additional authentication data, and $M$ is the plaintext message to be encrypted. $\AEAD(K, N, H, M)$ produces as output $c||t$, a ciphertext, $c$, concatenated with its 128-bit (16 byte) authentication tag, $t$. \end{definition} \begin{definition}[H][P384 \cite{fips_p384}]