fixed ratchet sync bug

This commit is contained in:
Monica Moniot
2024-01-17 17:43:53 -05:00
parent ef5973c42a
commit 8f40d08d5b
18 changed files with 770 additions and 505 deletions
+23 -7
View File
@@ -1,3 +1,4 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::iter::ExactSizeIterator;
use std::str::FromStr;
@@ -10,7 +11,7 @@ use rand_core::OsRng;
use rand_core::RngCore;
use zssp::application::{
AcceptAction, ApplicationLayer, CryptoLayer, IncomingSessionAction, RatchetState, RatchetStates, RatchetUpdate,
AcceptAction, ApplicationLayer, CompareAndSwap, CryptoLayer, IncomingSessionAction, RatchetState, RatchetStates,
Settings, RATCHET_SIZE,
};
use zssp::crypto::P384KeyPair;
@@ -114,14 +115,29 @@ impl ApplicationLayer<TestApplication> for &TestApplication {
&mut self,
remote_static_key: &CrateP384PublicKey,
session_data: &u128,
update_data: RatchetUpdate<'_>,
) -> Result<(), std::io::Error> {
update_data: CompareAndSwap<'_>,
) -> Result<bool, std::io::Error> {
let mut ratchets = self.ratchets.lock().unwrap();
ratchets.peer_map.insert(*session_data, update_data.to_states());
match ratchets.peer_map.entry(*session_data) {
Entry::Occupied(mut entry) => {
if update_data.compare(&entry.get()) {
entry.insert(update_data.to_new_states());
} else {
return Ok(false);
}
}
Entry::Vacant(entry) => {
if update_data.cur_is_initial_states() {
entry.insert(update_data.to_new_states());
} else {
return Ok(false);
}
}
}
if let Some(rf) = update_data.added_fingerprint() {
ratchets.rf_map.insert(*rf, update_data.state1.clone());
println!("[{}] new ratchet #{}", self.name, update_data.state1.chain_len());
ratchets.rf_map.insert(*rf, update_data.new_state1.clone());
println!("[{}] new ratchet #{}", self.name, update_data.new_state1.chain_len());
}
if let Some(rf) = update_data.deleted_fingerprint1() {
ratchets.rf_map.remove(rf);
@@ -129,7 +145,7 @@ impl ApplicationLayer<TestApplication> for &TestApplication {
if let Some(rf) = update_data.deleted_fingerprint2() {
ratchets.rf_map.remove(rf);
}
Ok(())
Ok(true)
}
fn time(&mut self) -> i64 {
+4 -4
View File
@@ -8,7 +8,7 @@ use rand_core::OsRng;
use rand_core::RngCore;
use zssp::application::{
AcceptAction, ApplicationLayer, IncomingSessionAction, RatchetState, RatchetStates, RatchetUpdate, RATCHET_SIZE,
AcceptAction, ApplicationLayer, CompareAndSwap, IncomingSessionAction, RatchetState, RatchetStates, RATCHET_SIZE,
};
use zssp::crypto::P384KeyPair;
use zssp::crypto_impl::*;
@@ -106,9 +106,9 @@ impl ApplicationLayer<TestApplication> for &TestApplication {
&mut self,
remote_static_key: &CrateP384PublicKey,
session_data: &(),
update_data: RatchetUpdate<'_>,
) -> Result<(), std::io::Error> {
Ok(())
update_data: CompareAndSwap<'_>,
) -> Result<bool, std::io::Error> {
Ok(true)
}
fn time(&mut self) -> i64 {
+19 -6
View File
@@ -296,11 +296,16 @@ pub trait ApplicationLayer<C: CryptoLayer>: Sized {
session_data: &C::SessionData,
fingerprint_data: Option<&C::FingerprintData>,
) -> Result<Option<RatchetStates>, std::io::Error>;
/// Atomically commit the update specified by `update_data` to storage, or return an error if
/// the update could not be made.
/// The implementor is free to choose how to apply these updates to storage.
/// Atomically compare-and-swap (a.k.a. compare-exchange) `update` to storage.
///
/// If this returns `Err(())`, the packet which triggered this function to be called will be
/// If `update.cur_state1` and `update.cur_state2` are currently in storage, they must
/// be swapped with `update.new_state1` and `update.new_state2`.
/// Otherwise, storage must remain unchanged.
///
/// `Ok(true)` must be returned if the compare-and-swap was successful.
/// `Ok(false)` must be returned if comparison failed and the swap was cancelled.
///
/// If this returns `Err`, the packet which triggered this function to be called will be
/// dropped, and no session state will be mutated, preserving synchronization. The remote peer
/// will eventually resend that packet and so this function will be called again.
///
@@ -312,12 +317,20 @@ pub trait ApplicationLayer<C: CryptoLayer>: Sized {
/// This function may also save state to volatile storage, in which case all peers which connect
/// to us will have to allow downgrade across the board.
/// Otherwise, when we restart, we will not be allowed to reconnect.
///
/// # Security
/// Implementations must not perform comparison operations (equals, less than, etc.) directly
/// on two ratchet keys. Instead, comparison operations must be performed indirectly
/// upon their ratchet fingerprints. If two ratchet states have the same ratchet fingerprint,
/// it should be assumed that they also have the same ratchet key.
///
/// The implementations of `PartialEq` for `RatchetState` and `RatchetStates` do this by default.
fn save_ratchet_state(
&mut self,
remote_static_key: &C::PublicKey,
session_data: &C::SessionData,
update_data: RatchetUpdate<'_>,
) -> Result<(), std::io::Error>;
update: CompareAndSwap<'_>,
) -> Result<bool, std::io::Error>;
/// Receives a stream of events that occur during an execution of ZSSP.
/// These are provided for debugging, logging or metrics purposes, and must be used for
+5 -7
View File
@@ -124,14 +124,12 @@ pub(crate) const PACKET_TYPE_DATA: u8 = 8;
pub(crate) const PACKET_TYPE_CHALLENGE: u8 = 9;
pub(crate) const PACKET_TYPE_USES_COUNTER_RANGE: std::ops::Range<u8> = 3..9;
pub(crate) const HANDSHAKE_HELLO_MIN_SIZE: usize =
KID_SIZE + P384_PUBLIC_KEY_SIZE + KYBER_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + AES_GCM_TAG_SIZE;
pub(crate) const HANDSHAKE_HELLO_MAX_SIZE: usize = HANDSHAKE_HELLO_MIN_SIZE + RATCHET_SIZE + RATCHET_SIZE;
pub(crate) const HANDSHAKE_HELLO_SIZE: usize =
KID_SIZE + P384_PUBLIC_KEY_SIZE + KYBER_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + 2 * RATCHET_SIZE + AES_GCM_TAG_SIZE;
pub(crate) const HANDSHAKE_HELLO_CHALLENGE_MIN_SIZE: usize = HANDSHAKE_HELLO_MIN_SIZE + CHALLENGE_SIZE;
pub(crate) const HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE: usize = HANDSHAKE_HELLO_MAX_SIZE + CHALLENGE_SIZE;
pub(crate) const HANDSHAKE_HELLO_CHALLENGE_SIZE: usize = HANDSHAKE_HELLO_SIZE + CHALLENGE_SIZE;
pub(crate) const HEADERED_HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE: usize = HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE + HEADER_SIZE;
pub(crate) const HEADERED_HANDSHAKE_HELLO_CHALLENGE_SIZE: usize = HANDSHAKE_HELLO_CHALLENGE_SIZE + HEADER_SIZE;
pub(crate) const HANDSHAKE_RESPONSE_SIZE: usize =
P384_PUBLIC_KEY_SIZE + KYBER_CIPHERTEXT_SIZE + AES_GCM_TAG_SIZE + KID_SIZE + AES_GCM_TAG_SIZE;
@@ -184,6 +182,6 @@ pub(crate) const MAX_UNASSOCIATED_FRAGMENTS: usize = 32 * 32;
/// The maximum size a packet that is not associated to a session may be.
/// Excludes the size of headers for fragmentation.
pub(crate) const MAX_UNASSOCIATED_PACKET_SIZE: usize = HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE;
pub(crate) const MAX_UNASSOCIATED_PACKET_SIZE: usize = HANDSHAKE_HELLO_CHALLENGE_SIZE;
pub(crate) const SESSION_MAX_FRAGMENTS_OOO: usize = 64;
+169 -90
View File
@@ -14,30 +14,23 @@ use crate::proto::*;
#[derive(Clone, Eq)]
pub struct RatchetState {
pub(crate) key: Zeroizing<[u8; RATCHET_SIZE]>,
pub(crate) fingerprint: Option<Zeroizing<[u8; RATCHET_SIZE]>>,
pub(crate) fingerprint: Zeroizing<[u8; RATCHET_SIZE]>,
pub(crate) chain_len: u64,
}
impl PartialEq for RatchetState {
fn eq(&self, other: &Self) -> bool {
let ret = match (self.fingerprint.as_ref(), other.fingerprint.as_ref()) {
(Some(rf1), Some(rf2)) => secure_eq(rf1, rf2),
(None, None) => true,
_ => false,
};
ret & secure_eq(&self.key, &other.key) & (self.chain_len == other.chain_len)
self.fingerprint.eq(&other.fingerprint) & (self.chain_len == other.chain_len)
}
}
impl std::hash::Hash for RatchetState {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if let Some(rf) = &self.fingerprint {
state.write_u64(u64::from_ne_bytes(rf[..8].try_into().unwrap()))
}
state.write(self.fingerprint.as_ref())
}
}
impl RatchetState {
/// Creates a new ratchet state from the given ratchet key, ratchet fingerprint and chain length.
pub fn new(key: Zeroizing<[u8; RATCHET_SIZE]>, fingerprint: Zeroizing<[u8; RATCHET_SIZE]>, chain_len: u64) -> Self {
RatchetState { key, fingerprint: Some(fingerprint), chain_len }
RatchetState { key, fingerprint, chain_len }
}
/// Creates a new ratchet state from the given ratchet key, ratchet fingerprint and chain length.
///
@@ -46,28 +39,10 @@ impl RatchetState {
pub fn new_raw(key: [u8; RATCHET_SIZE], fingerprint: [u8; RATCHET_SIZE], chain_len: u64) -> Self {
RatchetState {
key: Zeroizing::new(key),
fingerprint: Some(Zeroizing::new(fingerprint)),
fingerprint: Zeroizing::new(fingerprint),
chain_len,
}
}
/// The ratchet key for this ratchet state. This is directly mixed into the master secret of a
/// session and so is very sensitive. All operations upon a ratchet key must be implemented
/// in constant time. The user should prefer to do nothing with the ratchet key besides copying
/// it to or from a storage device.
///
/// If `fingerprint` returns `None` then this is the "empty" ratchet state and the key will be
/// all zeros.
pub fn key(&self) -> &[u8; RATCHET_SIZE] {
&self.key
}
/// Ratchet keys and fingerprints are "chained together", where each set is derived from the
/// previous set.
///
/// This function outputs the total length of that chain, as in the total number of previous
/// ratchet states that this ratchet state was derived from.
pub fn chain_len(&self) -> u64 {
self.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.
///
@@ -75,7 +50,7 @@ impl RatchetState {
pub fn empty() -> Self {
RatchetState {
key: Zeroizing::new([0u8; RATCHET_SIZE]),
fingerprint: None,
fingerprint: Zeroizing::new([0u8; RATCHET_SIZE]),
chain_len: 0,
}
}
@@ -99,15 +74,15 @@ impl RatchetState {
Self::new(rk, rf, 1)
}
/// 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 {
self.fingerprint.is_none()
}
/// Checks if the fingerprint of this ratchet state equals the fingerprint contained in argument
/// `rf`. Uses constant time equality.
pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool {
self.fingerprint.as_ref().map_or(false, |rf0| secure_eq(rf0, rf))
/// The ratchet key for this ratchet state. This is directly mixed into the master secret of a
/// session and so is very sensitive. All operations upon a ratchet key must be implemented
/// in constant time. The user should prefer to do nothing with the ratchet key besides copying
/// it to or from a storage device.
///
/// If `fingerprint` returns `None` then this is the "empty" ratchet state and the key will be
/// all zeros.
pub fn key(&self) -> &[u8; RATCHET_SIZE] {
&self.key
}
/// The ratchet fingerprint for this ratchet state.
///
@@ -118,8 +93,43 @@ impl RatchetState {
/// but the security of ZSSP can survive having this value leaked.
/// Operations on a ratchet fingerprint should be implemented in constant time,
/// but it is ok if they are not.
pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> {
self.fingerprint.as_deref()
pub fn fingerprint(&self) -> &[u8; RATCHET_SIZE] {
&self.fingerprint
}
/// Ratchet keys and fingerprints are "chained together", where each set is derived from the
/// previous set.
///
/// This function outputs the total length of that chain, as in the total number of previous
/// ratchet states that this ratchet state was derived from.
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 {
secure_eq(self.fingerprint(), &[0u8; RATCHET_SIZE])
}
/// Checks if the fingerprint of this ratchet state equals the
/// fingerprint contained in argument `rf`.
///
/// Uses constant time equality.
pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool {
secure_eq(self.fingerprint(), rf)
}
/// Returns true if the fingerprint of argument `this` equals the fingerprint represented by
/// argument `rf`.
/// If `this` is `None`, `this` is considered to be "null", as in a ratchet state that does not
/// exist or has been deleted.
/// If `rf` is `None`, `rf` is considered to be "null" as well.
/// If `rf` is `Some(None)`, `rf` is considered to be the empty ratchet fingerprint.
///
/// Uses constant time equality.
pub fn fingerprint_eq_nullable(this: Option<&Self>, rf: Option<&[u8; RATCHET_SIZE]>) -> bool {
match (this, rf) {
(Some(this), Some(rf)) => this.fingerprint_eq(rf),
(None, None) => true,
_ => false,
}
}
}
impl Default for RatchetState {
@@ -137,7 +147,8 @@ impl Default for RatchetState {
pub struct RatchetStates {
/// The first ratchet state from the pair.
pub state1: RatchetState,
/// The second ratchet state from the pair. It can, and usually will be `None`.
/// The second ratchet state from the pair.
/// It can, and usually will be `None`, which means that this ratchet state is "null".
pub state2: Option<RatchetState>,
}
impl RatchetStates {
@@ -172,7 +183,6 @@ impl Default for RatchetStates {
Self::new_initial_states()
}
}
/// A set of references to ratchet states specifying how a remote peer's persistent storage should
/// be updated. This struct is designed to provide any and all potentially needed data for
/// maintaining a store of these ratchet states. It should be straightforward to commit these updates
@@ -181,69 +191,138 @@ impl Default for RatchetStates {
/// There will only be up to two ratchet states saved to storage at a time per peer.
/// Every time a third ratchet state is generated, a previous ratchet state will be deleted.
///
/// These are sensitive values should they ought to be securely stored, with restricted read-write
/// permissions if stored on disk.
/// As the name implies, these updates should be applied as one atomic compare-and-swap operation.
/// If the two ratchet states currently in storage equal `cur_state1` and `cur_state2`,
/// then `new_state1` and `new_state2` should overwrite them.
/// If not, then storage must not be modified.
///
/// To prevent desync or resource leakage, these updates should be committed atomically. If that is
/// not possible, then new ratchet states should be written before old ratchet states are deleted
/// Ratchet keys must never be checked for equality. Instead, if two ratchet states have equal
/// ratchet fingerprints, it should be assumed that they also have equal ratchet keys.
/// This policy exists to reduce the security impact of timing and other side-channel attacks.
///
/// These are sensitive values and ought to be securely stored, with restricted read-write
/// permissions if stored on disk.
#[derive(Clone)]
pub struct RatchetUpdate<'a> {
/// The ratchet key and fingerprint to store in the first slot.
pub state1: &'a RatchetState,
/// The ratchet key and fingerprint to store in the second slot.
pub state2: Option<&'a RatchetState>,
/// Whether `state1` is a brand new ratchet state, or if it was previously saved.
pub state1_was_just_added: bool,
/// A previous ratchet key and fingerprint that now must be deleted from storage.
/// This will have been a previously given value of `state1` or `state2`.
pub deleted_state1: Option<&'a RatchetState>,
/// A previous ratchet key and fingerprint that now must be deleted from storage.
/// It is extremely rare that this field is occupied.
pub deleted_state2: Option<&'a RatchetState>,
pub struct CompareAndSwap<'a> {
/// The ratchet state to store in the first slot.
pub new_state1: &'a RatchetState,
/// The ratchet state to store in the second slot.
/// A value of `None` implies that the second slot should be set to null.
pub new_state2: Option<&'a RatchetState>,
/// This field is `true` if and only if `new_state1 != cur_state1` and `new_state1 != cur_state2`.
///
/// This implies whether `state1` is a brand new ratchet state, or if it was previously saved.
pub new_state1_was_just_added: bool,
/// The ratchet state that we expect to see in the first slot.
/// If this value is not currently stored in the first slot, the entire update must be aborted.
pub cur_state1: &'a RatchetState,
/// The ratchet state that we expect to see in the second slot.
/// A value of `None` implies we expect the second slot to be set to "null".
///
/// If this value is not currently stored in the second slot, the entire update must be aborted.
pub cur_state2: Option<&'a RatchetState>,
/// This field is `true` if and only if `cur_state1 != new_state1` and `cur_state1 != new_state2`.
pub cur_state1_was_just_deleted: bool,
/// This field is `true` if and only if `cur_state2 != new_state1` and `cur_state2 != new_state2`.
pub cur_state2_was_just_deleted: bool,
}
impl<'a> RatchetUpdate<'a> {
/// Returns the final `RatchetStates` that should be the only thing saved after this update is
/// fully committed. Future calls to `ApplicationLayer::restore_by_identity` should return this struct.
pub fn to_states(&self) -> RatchetStates {
RatchetStates::new(self.state1.clone(), self.state2.cloned())
impl<'a> CompareAndSwap<'a> {
pub(crate) fn new(
new_state1: &'a RatchetState,
new_state2: Option<&'a RatchetState>,
new_state1_was_just_added: bool,
cur_state1: &'a RatchetState,
cur_state2: Option<&'a RatchetState>,
cur_state1_was_just_deleted: bool,
cur_state2_was_just_deleted: bool,
) -> Self {
Self {
new_state1,
new_state2,
new_state1_was_just_added,
cur_state1,
cur_state2,
cur_state1_was_just_deleted,
cur_state2_was_just_deleted,
}
}
/// Returns the final `RatchetStates` that must be swapped into storage if this update is
/// fully committed.
/// Future calls to `ApplicationLayer::restore_by_identity` should return this struct.
pub fn to_new_states(&self) -> RatchetStates {
RatchetStates::new(self.new_state1.clone(), self.new_state2.cloned())
}
/// Returns the `RatchetStates` that is expected to currently be in storage for this peer.
/// This value must be compared with the current value in storage, and if they are equal,
/// the return value of `CompareAndSwap::to_new_states` must overwrite it.
pub fn to_cur_states(&self) -> RatchetStates {
RatchetStates::new(self.cur_state1.clone(), self.cur_state2.cloned())
}
/// If this update specifies adding a brand new ratchet fingerprint, this function will return it.
/// The returned ratchet fingerprint will always be the ratchet fingerprint of field `state1`.
///
/// If a fingerprint is returned then it is guaranteed that `state1_was_just_added` will be true.
/// If a fingerprint is returned then it is guaranteed that `state1_was_just_added` will be `true`.
pub fn added_fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> {
if self.state1_was_just_added {
self.state1.fingerprint()
} else {
None
}
self.new_state1_was_just_added.then_some(self.new_state1.fingerprint())
}
/// If this updated specifies to delete an old ratchet fingerprint, this function will return it.
/// The returned ratchet fingerprint will always be the ratchet fingerprint of field
/// `deleted_state1`.
/// If this updated specifies to delete `cur_state1`, and `cur_state1` has a non-zero ratchet
/// fingerprint, this function will return the ratchet fingerprint.
///
/// If `cur_state1` is not being deleted (i.e. `cur_state1 == new_state1` or
/// `cur_state1 == new_state2`), then this will return `None`.
///
/// 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 let Some(rs) = &self.deleted_state1 {
rs.fingerprint()
} else {
None
if self.cur_state1_was_just_deleted {
if !self.cur_state1.is_empty() {
return Some(self.cur_state1.fingerprint());
}
}
None
}
/// If this updated specifies to delete two ratchet fingerprints, this function will return the
/// second one. `deleted_fingerprint1` will return the first one.
/// The returned ratchet fingerprint will always be the ratchet fingerprint of field
/// `deleted_state2`.
/// If this updated specifies to delete `cur_state2`, and `cur_state2` has a non-zero ratchet
/// fingerprint, this function will return the ratchet fingerprint.
///
/// It is exceptionally rare that there will be more than one ratchet fingerprint to be deleted.
/// Care should be taken to make sure that this update will still be correctly committed in the
/// rare event that this returns `Some`.
/// If `cur_state2` is not being deleted (i.e. `cur_state2 == new_state1` or
/// `cur_state2 == new_state2`), then this will return `None`.
///
/// There may be a second ratchet fingerprint to be deleted, which function
/// `deleted_fingerprint1` will return.
pub fn deleted_fingerprint2(&self) -> Option<&[u8; RATCHET_SIZE]> {
if let Some(rs) = &self.deleted_state2 {
rs.fingerprint()
} else {
None
if self.cur_state2_was_just_deleted {
if let Some(rf) = self.cur_state2 {
if !rf.is_empty() {
return Some(rf.fingerprint());
}
}
}
None
}
/// Returns true if the currently stored ratchet states are expected to be the initial ratchet
/// states. This is the default value for a peer's ratchet states in the event they could not
/// be found in storage.
///
/// If a peer could not be found in storage, and this function returns true,
/// 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()
}
/// Compares the ratchet fingerprints of `cur_state1` and `cur_state2` with `rf1` and `rf2`.
/// If they are equal this function will return `true`.
///
/// If this function returns `true`, then the implementation may proceed to swap out the ratchet
/// states these fingerprints come from with `new_state1` and `new_state2`.
pub fn compare_fingerprints(&self, rf1: &[u8; RATCHET_SIZE], rf2: Option<&[u8; RATCHET_SIZE]>) -> bool {
self.cur_state1.fingerprint_eq(rf1) & RatchetState::fingerprint_eq_nullable(self.cur_state2, rf2)
}
/// Compares `cur_state1` and `cur_state2` with `other.state1` and `other.state2`.
/// If they are equal this function will return `true`.
///
/// If this function returns `true`, then the implementation may proceed to swap `other.state1`
/// and `other.state2` with `new_state1` and `new_state2`.
pub fn compare(&self, other: &RatchetStates) -> bool {
self.cur_state1.eq(&other.state1) & self.cur_state2.eq(&other.state2.as_ref())
}
}
+104 -79
View File
@@ -97,7 +97,7 @@ pub(crate) struct StateA1<C: CryptoLayer> {
e_secret: C::KeyPair,
e1_secret: C::Kem,
identity: ArrayVec<u8, IDENTITY_MAX_SIZE>,
x1: ArrayVec<u8, HEADERED_HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE>,
x1: ArrayVec<u8, HEADERED_HANDSHAKE_HELLO_CHALLENGE_SIZE>,
}
pub(crate) struct StateA3 {
@@ -280,7 +280,7 @@ fn create_a1_state<C: CryptoLayer>(
// ...
// -> e, es, e1
let mut noise = SymmetricState::<C>::initialize(PROTOCOL_NAME_NOISE_XK);
let mut x1 = ArrayVec::<u8, HEADERED_HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE>::new();
let mut x1 = ArrayVec::<u8, HEADERED_HANDSHAKE_HELLO_CHALLENGE_SIZE>::new();
x1.extend([0u8; HEADER_SIZE]);
// Noise process prologue.
let kid = kid_recv.get().to_ne_bytes();
@@ -299,12 +299,9 @@ fn create_a1_state<C: CryptoLayer>(
x1.extend(tag);
// Process message pattern 1 payload.
let i = x1.len();
if let Some(rf) = ratchet_state1.fingerprint() {
x1.try_extend_from_slice(rf).unwrap();
}
if let Some(Some(rf)) = ratchet_state2.map(|rs| rs.fingerprint()) {
x1.try_extend_from_slice(rf).unwrap();
}
x1.try_extend_from_slice(ratchet_state1.fingerprint()).unwrap();
x1.try_extend_from_slice(ratchet_state2.map_or(&[0u8; RATCHET_SIZE], |r| r.fingerprint()))
.unwrap();
let tag = noise.encrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 1), &mut x1[i..]);
x1.extend(tag);
@@ -426,11 +423,11 @@ pub(crate) fn received_x1_trans<C: CryptoLayer, App: ApplicationLayer<C>>(
// ...
// -> e, es, e1
// <- e, ee, ekem1, psk
if !(HANDSHAKE_HELLO_MIN_SIZE..=HANDSHAKE_HELLO_MAX_SIZE).contains(&x1.len()) {
if x1.len() != HANDSHAKE_HELLO_SIZE {
return Err(fault!(InvalidPacket, true));
}
if n[AES_GCM_NONCE_SIZE - 8..] != x1[x1.len() - 8..] {
if !secure_eq(&n[AES_GCM_NONCE_SIZE - 8..], &x1[x1.len() - 8..]) {
return Err(fault!(FailedAuth, true));
}
let hmac = &mut C::Hmac::new();
@@ -460,35 +457,48 @@ pub(crate) fn received_x1_trans<C: CryptoLayer, App: ApplicationLayer<C>>(
let e1_end = j;
i = k;
// Process message pattern 1 payload.
let k = x1.len();
let j = k - AES_GCM_TAG_SIZE;
let j = i + RATCHET_SIZE + RATCHET_SIZE;
let k = j + AES_GCM_TAG_SIZE;
let tag = x1[j..k].try_into().unwrap();
if !noise.decrypt_and_hash_in_place(hash, to_nonce(PACKET_TYPE_HANDSHAKE_HELLO, 1), &mut x1[i..j], tag) {
return Err(fault!(FailedAuth, true));
}
debug_assert_eq!(k, x1.len());
let rf1 = &x1[i..i + RATCHET_SIZE];
let rf2 = &x1[i + RATCHET_SIZE..j];
let mut lookup_data = None;
let mut ratchet_state = None;
while i + RATCHET_SIZE <= j {
match app.restore_by_fingerprint((&x1[i..i + RATCHET_SIZE]).try_into().unwrap()) {
if !secure_eq(rf1, &[0u8; RATCHET_SIZE]) {
match app.restore_by_fingerprint(rf1.try_into().unwrap()) {
Ok(None) => {}
Ok(Some((rs, data))) => {
lookup_data = Some(data);
ratchet_state = Some(rs);
break;
}
Err(e) => return Err(ReceiveError::StorageError(e)),
}
i += RATCHET_SIZE;
}
let ratchet_state = if let Some(rs) = ratchet_state {
rs
} else {
if app.hello_requires_recognized_ratchet() {
return Err(fault!(FailedAuth, true));
if ratchet_state.is_none() {
if !secure_eq(rf2, &[0u8; RATCHET_SIZE]) {
match app.restore_by_fingerprint(rf1.try_into().unwrap()) {
Ok(None) => {}
Ok(Some((rs, data))) => {
lookup_data = Some(data);
ratchet_state = Some(rs);
}
Err(e) => return Err(ReceiveError::StorageError(e)),
}
}
RatchetState::empty()
};
if ratchet_state.is_none() {
if app.hello_requires_recognized_ratchet() {
return Err(fault!(FailedAuth, true));
}
}
}
// If we get to this point and haven't found a full ratchet state,
// set it to the empty ratchet state.
let ratchet_state = ratchet_state.unwrap_or_default();
let mut hk_recv = Zeroizing::new([0u8; HASHLEN]);
let mut hk_send = Zeroizing::new([0u8; HASHLEN]);
@@ -589,7 +599,7 @@ pub(crate) fn received_x2_trans<C: CryptoLayer, App: ApplicationLayer<C>>(
return Err(fault!(UnknownLocalKeyId, true, session));
}
let (_, c) = from_nonce(n);
if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD || n[AES_GCM_NONCE_SIZE - 3..] != x2[x2.len() - 3..] {
if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD || !secure_eq(&n[AES_GCM_NONCE_SIZE - 3..], &x2[x2.len() - 3..]) {
return Err(fault!(FailedAuth, true, session));
}
@@ -690,23 +700,27 @@ pub(crate) fn received_x2_trans<C: CryptoLayer, App: ApplicationLayer<C>>(
let new_ratchet_state = create_ratchet_state(hmac, &noise, chain_len);
let (ratchet_to_preserve, ratchet_to_delete) = if ratchet_i == 1 {
(Some(&state.ratchet_state1), state.ratchet_state2.as_ref())
let ratchet_to_preserve = if ratchet_i == 1 {
Some(&state.ratchet_state1)
} else {
(state.ratchet_state2.as_ref(), Some(&state.ratchet_state1))
state.ratchet_state2.as_ref()
};
app.save_ratchet_state(
let result = app.save_ratchet_state(
&session.s_remote,
&session.session_data,
RatchetUpdate {
state1: &new_ratchet_state,
state2: ratchet_to_preserve,
state1_was_just_added: true,
deleted_state1: ratchet_to_delete,
deleted_state2: None,
},
)
.map_err(|e| ReceiveError::StorageError(e))?;
CompareAndSwap::new(
&new_ratchet_state,
ratchet_to_preserve,
true,
&state.ratchet_state1,
state.ratchet_state2.as_ref(),
ratchet_i == 2,
ratchet_i == 1,
),
);
if !result.map_err(ReceiveError::StorageError)? {
return Err(fault!(OutOfSequence, true, session, true));
}
let mut kek_recv = Zeroizing::new([0u8; HASHLEN]);
let mut kek_send = Zeroizing::new([0u8; HASHLEN]);
@@ -736,7 +750,7 @@ pub(crate) fn received_x2_trans<C: CryptoLayer, App: ApplicationLayer<C>>(
a1
} else {
// This return is unreachable.
return Err(fault!(FailedAuth, true, session));
return Err(fault!(FailedAuth, true, session, true));
};
state.beta = ZetaAutomata::A3(Box::new(StateA3 { identity: a1.identity.clone(), x3: x3.clone() }));
resend_timer
@@ -858,7 +872,7 @@ pub(crate) fn received_x3_trans<C: CryptoLayer, App: ApplicationLayer<C>>(
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.fingerprint().is_none() {
if !responder_disallows_downgrade && zeta.ratchet_state.is_empty() {
should_warn_missing_ratchet = true;
} else {
if !responder_silently_rejects {
@@ -877,18 +891,14 @@ pub(crate) fn received_x3_trans<C: CryptoLayer, App: ApplicationLayer<C>>(
noise.split(hmac, &mut nk_send, &mut nk_recv);
// We must make sure the ratchet key is saved before we transition.
app.save_ratchet_state(
let result = app.save_ratchet_state(
&s_remote,
&session_data,
RatchetUpdate {
state1: &new_ratchet_state,
state2: None,
state1_was_just_added: true,
deleted_state1: Some(&state1),
deleted_state2: state2.as_ref(),
},
)
.map_err(|e| ReceiveError::StorageError(e))?;
CompareAndSwap::new(&new_ratchet_state, None, true, &state1, state2.as_ref(), true, true),
);
if !result.map_err(ReceiveError::StorageError)? {
return Err(fault!(OutOfSequence, true));
}
let (session, reduced_service_time) = {
let mut session_map = ctx.session_map.write().unwrap();
@@ -1004,18 +1014,25 @@ pub(crate) fn received_c1_trans<C: CryptoLayer, App: ApplicationLayer<C>>(
if is_other {
if let ZetaAutomata::A3 { .. } | ZetaAutomata::R2 { .. } = &state.beta {
if state.ratchet_state2.is_some() {
app.save_ratchet_state(
let result = app.save_ratchet_state(
&session.s_remote,
&session.session_data,
RatchetUpdate {
state1: &state.ratchet_state1,
state2: None,
state1_was_just_added: false,
deleted_state1: state.ratchet_state2.as_ref(),
deleted_state2: None,
},
)
.map_err(|e| ReceiveError::StorageError(e))?;
CompareAndSwap::new(
&state.ratchet_state1,
None,
false,
&state.ratchet_state1,
state.ratchet_state2.as_ref(),
false,
true,
),
);
if !result.map_err(ReceiveError::StorageError)? {
drop(state);
drop(kex_lock);
session.expire();
return Err(fault!(OutOfSequence, true, session, true));
}
}
drop(state);
let timeout_timer = {
@@ -1415,18 +1432,22 @@ pub(crate) fn received_k1_trans<C: CryptoLayer, App: ApplicationLayer<C>>(
k2.extend(tag);
let new_ratchet_state = create_ratchet_state(hmac, &noise, state.ratchet_state1.chain_len);
app.save_ratchet_state(
let result = app.save_ratchet_state(
&session.s_remote,
&session.session_data,
RatchetUpdate {
state1: &new_ratchet_state,
state2: Some(&state.ratchet_state1),
state1_was_just_added: true,
deleted_state1: state.ratchet_state2.as_ref(),
deleted_state2: None,
},
)
.map_err(|e| ReceiveError::StorageError(e))?;
CompareAndSwap::new(
&new_ratchet_state,
Some(&state.ratchet_state1),
true,
&state.ratchet_state1,
state.ratchet_state2.as_ref(),
false,
true,
),
);
if !result.map_err(ReceiveError::StorageError)? {
return Err(fault!(OutOfSequence, true, session, true));
}
let mut kek_recv = Zeroizing::new([0u8; HASHLEN]);
let mut kek_send = Zeroizing::new([0u8; HASHLEN]);
@@ -1534,18 +1555,22 @@ pub(crate) fn received_k2_trans<C: CryptoLayer, App: ApplicationLayer<C>>(
.ok_or_else(|| fault!(InvalidPacket, true, session, true))?;
let new_ratchet_state = create_ratchet_state(hmac, &noise, state.ratchet_state1.chain_len);
app.save_ratchet_state(
let result = app.save_ratchet_state(
&session.s_remote,
&session.session_data,
RatchetUpdate {
state1: &new_ratchet_state,
state2: None,
state1_was_just_added: true,
deleted_state1: Some(&state.ratchet_state1),
deleted_state2: state.ratchet_state2.as_ref(),
},
)
.map_err(|e| ReceiveError::StorageError(e))?;
CompareAndSwap::new(
&new_ratchet_state,
None,
true,
&state.ratchet_state1,
state.ratchet_state2.as_ref(),
true,
true,
),
);
if !result.map_err(ReceiveError::StorageError)? {
return Err(fault!(OutOfSequence, true, session, true));
}
let mut kek_recv = Zeroizing::new([0u8; HASHLEN]);
let mut kek_send = Zeroizing::new([0u8; HASHLEN]);
+14 -7
View File
@@ -173,16 +173,25 @@ impl<C: CryptoLayer> Context<C> {
.restore_by_identity(&static_remote_key, &session_data, None)
.map_err(OpenError::StorageError)?
.unwrap_or_default();
self.open_with_ratchet(app, send, mtu, static_remote_key, session_data, identity, ratchet_states)
self.open_with_ratchet(
app,
send,
mtu,
static_remote_key,
session_data,
identity,
ratchet_states,
)
}
/// Create a new session and send initialization packets to Bob, our remote peer.
/// This function will use the specified `ratchet_states` to connect to Bob, as opposed to
/// calling `app.restore_by_identity`. This can be used to open a session with a specific
/// one-time-password using `RatchetStates::new_otp_states()`, or in situations where it is
/// desireable to avoid having Alice call `app.restore_by_identity`.
/// desireable to avoid having Alice call `app.restore_by_identity`. Keep in mind that
/// `save_ratchet_state` likely will eventually be called to delete this ratchet state.
///
/// If using a one-time-password, a call to `app.initiator_disallows_downgrade` on the created
/// session **MUST** return `true`. Otherwise the remote peer would be able to avoid having
/// session **must** return `true`. Otherwise the remote peer would be able to avoid having
/// to demonstrate knowledge of the otp by requesting a ratchet downgrade.
/// Only after the created session is established may a call to
/// `app.initiator_disallows_downgrade` return false.
@@ -546,7 +555,7 @@ impl<C: CryptoLayer> Context<C> {
return Err(fault!(InvalidPacket, true));
}
let mut buffer = ArrayVec::<u8, HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE>::new();
let mut buffer = ArrayVec::<u8, HANDSHAKE_HELLO_CHALLENGE_SIZE>::new();
let assembled_packet = if fragment_count > 1 {
let mut next_service_time = self.0.unassociated_defrag_cache.lock().unwrap().assemble(
&nonce,
@@ -578,9 +587,7 @@ impl<C: CryptoLayer> Context<C> {
if packet_type == PACKET_TYPE_HANDSHAKE_HELLO {
log!(app, ReceivedRawX1);
if !(HANDSHAKE_HELLO_CHALLENGE_MIN_SIZE..=HANDSHAKE_HELLO_CHALLENGE_MAX_SIZE)
.contains(&assembled_packet.len())
{
if HANDSHAKE_HELLO_CHALLENGE_SIZE != assembled_packet.len() {
return Err(fault!(InvalidPacket, true));
}
// Process recv challenge layer.
+26 -9
View File
@@ -6,6 +6,7 @@
* https://www.zerotier.com/
*/
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::iter::ExactSizeIterator;
use std::str::FromStr;
@@ -18,7 +19,7 @@ use rand_core::OsRng;
use rand_core::RngCore;
use zssp_proto::application::{
AcceptAction, ApplicationLayer, CryptoLayer, RatchetState, RatchetStates, RatchetUpdate, Settings, RATCHET_SIZE,
AcceptAction, ApplicationLayer, CompareAndSwap, CryptoLayer, RatchetState, RatchetStates, Settings, RATCHET_SIZE,
};
use zssp_proto::crypto::P384KeyPair;
use zssp_proto::crypto_impl::{
@@ -105,21 +106,37 @@ impl ApplicationLayer<TestApplication> for &mut TestApplication {
&mut self,
remote_static_key: &P384CratePublicKey,
session_data: &u128,
update_data: RatchetUpdate<'_>,
) -> Result<(), std::io::Error> {
self.ratchets.peer_map.insert(*session_data, update_data.to_states());
update_data: CompareAndSwap<'_>,
) -> Result<bool, std::io::Error> {
let mut ratchets = &mut self.ratchets;
match ratchets.peer_map.entry(*session_data) {
Entry::Occupied(mut entry) => {
if update_data.compare(&entry.get()) {
entry.insert(update_data.to_new_states());
} else {
return Ok(false);
}
}
Entry::Vacant(entry) => {
if update_data.cur_is_initial_states() {
entry.insert(update_data.to_new_states());
} else {
return Ok(false);
}
}
}
if let Some(rf) = update_data.added_fingerprint() {
self.ratchets.rf_map.insert(*rf, update_data.state1.clone());
println!("[{}] new ratchet #{}", self.name, update_data.state1.chain_len());
ratchets.rf_map.insert(*rf, update_data.new_state1.clone());
println!("[{}] new ratchet #{}", self.name, update_data.new_state1.chain_len());
}
if let Some(rf) = update_data.deleted_fingerprint1() {
self.ratchets.rf_map.remove(rf);
ratchets.rf_map.remove(rf);
}
if let Some(rf) = update_data.deleted_fingerprint2() {
self.ratchets.rf_map.remove(rf);
ratchets.rf_map.remove(rf);
}
Ok(())
Ok(true)
}
fn time(&mut self) -> i64 {
+4 -4
View File
@@ -12,7 +12,7 @@ use std::sync::Arc;
use std::time::Instant;
use zssp_proto::application::{
AcceptAction, ApplicationLayer, CryptoLayer, RatchetState, RatchetStates, RatchetUpdate, RATCHET_SIZE,
AcceptAction, ApplicationLayer, CompareAndSwap, CryptoLayer, RatchetState, RatchetStates, RATCHET_SIZE,
};
use zssp_proto::crypto::{rand_core::OsRng, P384KeyPair};
use zssp_proto::crypto_impl::{
@@ -81,9 +81,9 @@ impl ApplicationLayer<MyApp> for &mut MyApp {
&mut self,
remote_static_key: &P384CratePublicKey,
session_data: &(),
update_data: RatchetUpdate<'_>,
) -> Result<(), std::io::Error> {
Ok(())
update_data: CompareAndSwap<'_>,
) -> Result<bool, std::io::Error> {
Ok(true)
}
fn time(&mut self) -> i64 {
+19 -6
View File
@@ -213,11 +213,16 @@ pub trait ApplicationLayer<C: CryptoLayer>: Sized {
remote_static_key: &<C as CryptoLayer>::PublicKey,
session_data: &<C as CryptoLayer>::SessionData,
) -> Result<Option<RatchetStates>, std::io::Error>;
/// Atomically commit the update specified by `update_data` to storage, or return an error if
/// the update could not be made.
/// The implementor is free to choose how to apply these updates to storage.
/// Atomically compare-and-swap (a.k.a. compare-exchange) `update` to storage.
///
/// If this returns `Err(IoError)`, the packet which triggered this function to be called will be
/// If `update.cur_state1` and `update.cur_state2` are currently in storage, they must
/// be swapped with `update.new_state1` and `update.new_state2`.
/// Otherwise, storage must remain unchanged.
///
/// `Ok(true)` must be returned if the compare-and-swap was successful.
/// `Ok(false)` must be returned if comparison failed and the swap was cancelled.
///
/// If this returns `Err`, the packet which triggered this function to be called will be
/// dropped, and no session state will be mutated, preserving synchronization. The remote peer
/// will eventually resend that packet and so this function will be called again.
///
@@ -229,12 +234,20 @@ pub trait ApplicationLayer<C: CryptoLayer>: Sized {
/// This function may also save state to volatile storage, in which case all peers which connect
/// to us will have to allow downgrade across the board.
/// Otherwise, when we restart, we will not be allowed to reconnect.
///
/// # Security
/// Implementations must not perform comparison operations (equals, less than, etc.) directly
/// on two ratchet keys. Instead, comparison operations must be performed indirectly
/// upon their ratchet fingerprints. If two ratchet states have the same ratchet fingerprint,
/// it should be assumed that they also have the same ratchet key.
///
/// The implementations of `PartialEq` for `RatchetState` and `RatchetStates` do this by default.
fn save_ratchet_state(
&mut self,
remote_static_key: &<C as CryptoLayer>::PublicKey,
session_data: &<C as CryptoLayer>::SessionData,
update_data: RatchetUpdate<'_>,
) -> Result<(), std::io::Error>;
update_data: CompareAndSwap<'_>,
) -> Result<bool, std::io::Error>;
/// Receives a stream of events that occur during an execution of ZSSP.
/// These are provided for debugging, logging or metrics purposes, and must be used for
+14 -14
View File
@@ -11,7 +11,7 @@ use crate::challenge::ChallengeContext;
use crate::crypto::{AES_256_KEY_SIZE, AES_GCM_NONCE_SIZE};
use crate::fragmentation::{send_with_fragmentation, DefragBuffer};
use crate::proto::*;
use crate::result::{byzantine_fault, OpenError, ReceiveError, ReceiveOk, SendError, SessionEvent};
use crate::result::{fault, OpenError, ReceiveError, ReceiveOk, SendError, SessionEvent};
use crate::zeta::*;
#[cfg(feature = "logging")]
use crate::LogEvent::*;
@@ -161,24 +161,24 @@ impl<C: CryptoLayer> Context<C> {
if !matches!(&zeta.beta, ZetaAutomata::A1(_)) {
// A resent handshake response from Bob may have arrived out of order,
// after we already received one.
return Err(byzantine_fault!(OutOfSequence, false));
return Err(fault!(OutOfSequence, false));
}
if c >= COUNTER_WINDOW_MAX_SKIP_AHEAD {
return Err(byzantine_fault!(ExpiredCounter, true));
return Err(fault!(ExpiredCounter, true));
}
Ok(())
} else if PACKET_TYPE_USES_COUNTER_RANGE.contains(&p) {
if !zeta.check_counter_window(c) {
// The counter window has finite memory and so will occasionally give
// false positives on very out-of-order packets.
return Err(byzantine_fault!(ExpiredCounter, false));
return Err(fault!(ExpiredCounter, false));
}
Ok(())
} else if p == PACKET_TYPE_HANDSHAKE_COMPLETION {
// The handshake completion packet could have been resent.
return Err(byzantine_fault!(InvalidPacket, false));
return Err(fault!(InvalidPacket, false));
} else {
return Err(byzantine_fault!(InvalidPacket, true));
return Err(fault!(InvalidPacket, true));
}
})?;
if let Some((pn, mut assembled_packet)) = result {
@@ -295,7 +295,7 @@ impl<C: CryptoLayer> Context<C> {
log!(app, DIsAuthClosedSession(&session));
SessionEvent::Rejected
}
_ => return Err(byzantine_fault!(InvalidPacket, true)), // This is unreachable.
_ => return Err(fault!(InvalidPacket, true)), // This is unreachable.
};
drop(zeta);
Ok(ReceiveOk::Session(session, ret))
@@ -315,7 +315,7 @@ impl<C: CryptoLayer> Context<C> {
if p == PACKET_TYPE_HANDSHAKE_COMPLETION && c == 0 {
Ok(())
} else {
Err(byzantine_fault!(InvalidPacket, true))
Err(fault!(InvalidPacket, true))
}
})?;
if let Some((_, assembled_packet)) = result {
@@ -351,7 +351,7 @@ impl<C: CryptoLayer> Context<C> {
} else {
// When sessions are added or dropped or packets arrive extremely delayed it is
// possible to receive no longer recognized key ids.
Err(byzantine_fault!(UnknownLocalKeyId, false))
Err(fault!(UnknownLocalKeyId, false))
}
}
} else {
@@ -365,7 +365,7 @@ impl<C: CryptoLayer> Context<C> {
if p == PACKET_TYPE_HANDSHAKE_HELLO || p == PACKET_TYPE_CHALLENGE {
Ok(())
} else {
Err(byzantine_fault!(InvalidPacket, true))
Err(fault!(InvalidPacket, true))
}
},
)?;
@@ -394,7 +394,7 @@ impl<C: CryptoLayer> Context<C> {
None,
);
// If we issue a challenge the first hello packet will always fail.
return Err(byzantine_fault!(FailedAuth, false));
return Err(fault!(FailedAuth, false));
} else if let Ok(true) = result {
log!(app, X1SucceededChallenge);
}
@@ -423,7 +423,7 @@ impl<C: CryptoLayer> Context<C> {
log!(app, ReceivedRawChallenge);
// Process recv challenge layer.
if assembled_packet.len() != KID_SIZE + CHALLENGE_SIZE {
return Err(byzantine_fault!(InvalidPacket, true));
return Err(fault!(InvalidPacket, true));
}
if let Some(kid_recv) =
NonZeroU32::new(u32::from_be_bytes(assembled_packet[..KID_SIZE].try_into().unwrap()))
@@ -439,9 +439,9 @@ impl<C: CryptoLayer> Context<C> {
return Ok(ReceiveOk::Unassociated);
}
}
Err(byzantine_fault!(UnknownLocalKeyId, true))
Err(fault!(UnknownLocalKeyId, true))
} else {
Err(byzantine_fault!(InvalidPacket, true))
Err(fault!(InvalidPacket, true))
}
} else {
Ok(ReceiveOk::Unassociated)
+4 -4
View File
@@ -7,7 +7,7 @@ use zeroize::Zeroizing;
use crate::application::CryptoLayer;
use crate::crypto::{Aes256Prp, AES_256_KEY_SIZE};
use crate::proto::*;
use crate::result::{byzantine_fault, ReceiveError};
use crate::result::{fault, ReceiveError};
/// Corresponds to Figure 13 found in Section 6.
fn create_fragment_header(
@@ -94,7 +94,7 @@ impl DefragBuffer {
) -> Result<Option<([u8; PACKET_NONCE_SIZE], Vec<u8>)>, ReceiveError> {
use crate::result::FaultType::*;
if raw_fragment.len() < MIN_PACKET_SIZE {
return Err(byzantine_fault!(InvalidPacket, true));
return Err(fault!(InvalidPacket, true));
}
if let Some(hk_recv) = self.hk_recv.as_ref() {
@@ -109,7 +109,7 @@ impl DefragBuffer {
let fragment_no = raw_fragment[FRAGMENT_NO_IDX] as usize;
let fragment_count = raw_fragment[FRAGMENT_COUNT_IDX] as usize;
if fragment_no >= fragment_count || fragment_count > MAX_FRAGMENTS {
return Err(byzantine_fault!(InvalidPacket, true));
return Err(fault!(InvalidPacket, true));
}
let n = raw_fragment[PACKET_NONCE_START..HEADER_SIZE].try_into().unwrap();
@@ -125,7 +125,7 @@ impl DefragBuffer {
|| raw_fragment.len() > buffer.fragment_max_size
{
// Some parts of the protocol can cause duplicate fragments to be sent.
return Err(byzantine_fault!(InvalidPacket, false));
return Err(fault!(InvalidPacket, false));
}
buffer.fragments[fragment_no] = Some(raw_fragment);
buffer.total += 1;
+2 -3
View File
@@ -112,9 +112,8 @@ pub(crate) const PACKET_TYPE_DATA: u8 = 8;
pub(crate) const PACKET_TYPE_CHALLENGE: u8 = 9;
pub(crate) const PACKET_TYPE_USES_COUNTER_RANGE: std::ops::Range<u8> = 3..9;
pub(crate) const HANDSHAKE_HELLO_MIN_SIZE: usize =
KID_SIZE + P384_PUBLIC_KEY_SIZE + KYBER_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + AES_GCM_TAG_SIZE;
pub(crate) const HANDSHAKE_HELLO_MAX_SIZE: usize = HANDSHAKE_HELLO_MIN_SIZE + RATCHET_SIZE + RATCHET_SIZE;
pub(crate) const HANDSHAKE_HELLO_SIZE: usize =
KID_SIZE + P384_PUBLIC_KEY_SIZE + KYBER_PUBLIC_KEY_SIZE + AES_GCM_TAG_SIZE + 2 * RATCHET_SIZE + AES_GCM_TAG_SIZE;
pub(crate) const HANDSHAKE_RESPONSE_SIZE: usize =
P384_PUBLIC_KEY_SIZE + KYBER_CIPHERTEXT_SIZE + AES_GCM_TAG_SIZE + KID_SIZE + AES_GCM_TAG_SIZE;
+173 -94
View File
@@ -12,31 +12,24 @@ use crate::proto::*;
/// Corresponds to the Ratchet Key and Ratchet Fingerprint described in Section 3.
#[derive(Clone, Eq)]
pub struct RatchetState {
key: Zeroizing<[u8; RATCHET_SIZE]>,
fingerprint: Option<Zeroizing<[u8; RATCHET_SIZE]>>,
chain_len: u64,
pub(crate) key: Zeroizing<[u8; RATCHET_SIZE]>,
pub(crate) fingerprint: Zeroizing<[u8; RATCHET_SIZE]>,
pub(crate) chain_len: u64,
}
impl PartialEq for RatchetState {
fn eq(&self, other: &Self) -> bool {
let ret = match (self.fingerprint.as_ref(), other.fingerprint.as_ref()) {
(Some(rf1), Some(rf2)) => secure_eq(rf1, rf2),
(None, None) => true,
_ => false,
};
ret & secure_eq(&self.key, &other.key) & (self.chain_len == other.chain_len)
self.fingerprint.eq(&other.fingerprint) & (self.chain_len == other.chain_len)
}
}
impl std::hash::Hash for RatchetState {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if let Some(rf) = &self.fingerprint {
state.write_u64(u64::from_ne_bytes(rf[..8].try_into().unwrap()))
}
state.write(self.fingerprint.as_ref())
}
}
impl RatchetState {
/// Creates a new ratchet state from the given ratchet key, ratchet fingerprint and chain length.
pub fn new(key: Zeroizing<[u8; RATCHET_SIZE]>, fingerprint: Zeroizing<[u8; RATCHET_SIZE]>, chain_len: u64) -> Self {
RatchetState { key, fingerprint: Some(fingerprint), chain_len }
RatchetState { key, fingerprint, chain_len }
}
/// Creates a new ratchet state from the given ratchet key, ratchet fingerprint and chain length.
///
@@ -45,28 +38,10 @@ impl RatchetState {
pub fn new_raw(key: [u8; RATCHET_SIZE], fingerprint: [u8; RATCHET_SIZE], chain_len: u64) -> Self {
RatchetState {
key: Zeroizing::new(key),
fingerprint: Some(Zeroizing::new(fingerprint)),
fingerprint: Zeroizing::new(fingerprint),
chain_len,
}
}
/// The ratchet key for this ratchet state. This is directly mixed into the master secret of a
/// session and so is very sensitive. All operations upon a ratchet key must be implemented
/// in constant time. The user should prefer to do nothing with the ratchet key besides copying
/// it to or from a storage device.
///
/// If `fingerprint` returns `None` then this is the "empty" ratchet state and the key will be
/// all zeros.
pub fn key(&self) -> &[u8; RATCHET_SIZE] {
&self.key
}
/// Ratchet keys and fingerprints are "chained together", where each set is derived from the
/// previous set.
///
/// This function outputs the total length of that chain, as in the total number of previous
/// ratchet states that this ratchet state was derived from.
pub fn chain_len(&self) -> u64 {
self.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.
///
@@ -74,7 +49,7 @@ impl RatchetState {
pub fn empty() -> Self {
RatchetState {
key: Zeroizing::new([0u8; RATCHET_SIZE]),
fingerprint: None,
fingerprint: Zeroizing::new([0u8; RATCHET_SIZE]),
chain_len: 0,
}
}
@@ -84,7 +59,7 @@ impl RatchetState {
pub fn new_from_otp<Hmac: Sha512Hash>(otp: &[u8]) -> RatchetState {
let mut buffer = Vec::new();
buffer.push(1);
buffer.extend(LABEL_OTP_TO_RATCHET);
buffer.extend(*LABEL_OTP_TO_RATCHET);
buffer.push(0x00);
buffer.extend((1024u16).to_be_bytes());
let r1 = Hmac::hmac(otp, &buffer);
@@ -96,15 +71,15 @@ impl RatchetState {
1,
)
}
/// 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 {
self.fingerprint.is_none()
}
/// Checks if the fingerprint of this ratchet state equals the fingerprint contained in argument
/// `rf`. Uses constant time equality.
pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool {
self.fingerprint.as_ref().map_or(false, |rf0| secure_eq(rf0, rf))
/// The ratchet key for this ratchet state. This is directly mixed into the master secret of a
/// session and so is very sensitive. All operations upon a ratchet key must be implemented
/// in constant time. The user should prefer to do nothing with the ratchet key besides copying
/// it to or from a storage device.
///
/// If `fingerprint` returns `None` then this is the "empty" ratchet state and the key will be
/// all zeros.
pub fn key(&self) -> &[u8; RATCHET_SIZE] {
&self.key
}
/// The ratchet fingerprint for this ratchet state.
///
@@ -115,8 +90,43 @@ impl RatchetState {
/// but the security of ZSSP can survive having this value leaked.
/// Operations on a ratchet fingerprint should be implemented in constant time,
/// but it is ok if they are not.
pub fn fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> {
self.fingerprint.as_deref()
pub fn fingerprint(&self) -> &[u8; RATCHET_SIZE] {
&self.fingerprint
}
/// Ratchet keys and fingerprints are "chained together", where each set is derived from the
/// previous set.
///
/// This function outputs the total length of that chain, as in the total number of previous
/// ratchet states that this ratchet state was derived from.
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 {
secure_eq(self.fingerprint(), &[0u8; RATCHET_SIZE])
}
/// Checks if the fingerprint of this ratchet state equals the
/// fingerprint contained in argument `rf`.
///
/// Uses constant time equality.
pub fn fingerprint_eq(&self, rf: &[u8; RATCHET_SIZE]) -> bool {
secure_eq(self.fingerprint(), rf)
}
/// Returns true if the fingerprint of argument `this` equals the fingerprint represented by
/// argument `rf`.
/// If `this` is `None`, `this` is considered to be "null", as in a ratchet state that does not
/// exist or has been deleted.
/// If `rf` is `None`, `rf` is considered to be "null" as well.
/// If `rf` is `Some(None)`, `rf` is considered to be the empty ratchet fingerprint.
///
/// Uses constant time equality.
pub fn fingerprint_eq_nullable(this: Option<&Self>, rf: Option<&[u8; RATCHET_SIZE]>) -> bool {
match (this, rf) {
(Some(this), Some(rf)) => this.fingerprint_eq(rf),
(None, None) => true,
_ => false,
}
}
}
impl Default for RatchetState {
@@ -134,7 +144,8 @@ impl Default for RatchetState {
pub struct RatchetStates {
/// The first ratchet state from the pair.
pub state1: RatchetState,
/// The second ratchet state from the pair. It can, and usually will be `None`.
/// The second ratchet state from the pair.
/// It can, and usually will be `None`, which means that this ratchet state is "null".
pub state2: Option<RatchetState>,
}
impl RatchetStates {
@@ -169,7 +180,6 @@ impl Default for RatchetStates {
Self::new_initial_states()
}
}
/// A set of references to ratchet states specifying how a remote peer's persistent storage should
/// be updated. This struct is designed to provide any and all potentially needed data for
/// maintaining a store of these ratchet states. It should be straightforward to commit these updates
@@ -178,69 +188,138 @@ impl Default for RatchetStates {
/// There will only be up to two ratchet states saved to storage at a time per peer.
/// Every time a third ratchet state is generated, a previous ratchet state will be deleted.
///
/// These are sensitive values should they ought to be securely stored, with restricted read-write
/// permissions if stored on disk.
/// As the name implies, these updates should be applied as one atomic compare-and-swap operation.
/// If the two ratchet states currently in storage equal `cur_state1` and `cur_state2`,
/// then `new_state1` and `new_state2` should overwrite them.
/// If not, then storage must not be modified.
///
/// To prevent desync or resource leakage, these updates should be committed atomically. If that is
/// not possible, then new ratchet states should be written before old ratchet states are deleted
#[derive(Clone, Copy)]
pub struct RatchetUpdate<'a> {
/// The ratchet key and fingerprint to store in the first slot.
pub state1: &'a RatchetState,
/// The ratchet key and fingerprint to store in the second slot.
pub state2: Option<&'a RatchetState>,
/// Whether `state1` is a brand new ratchet state, or if it was previously saved.
pub state1_was_just_added: bool,
/// A previous ratchet key and fingerprint that now must be deleted from storage.
/// This will have been a previously given value of `state1` or `state2`.
pub deleted_state1: Option<&'a RatchetState>,
/// A previous ratchet key and fingerprint that now must be deleted from storage.
/// It is extremely rare that this field is occupied.
pub deleted_state2: Option<&'a RatchetState>,
/// Ratchet keys must never be checked for equality. Instead, if two ratchet states have equal
/// ratchet fingerprints, it should be assumed that they also have equal ratchet keys.
/// This policy exists to reduce the security impact of timing and other side-channel attacks.
///
/// These are sensitive values and ought to be securely stored, with restricted read-write
/// permissions if stored on disk.
#[derive(Clone)]
pub struct CompareAndSwap<'a> {
/// The ratchet state to store in the first slot.
pub new_state1: &'a RatchetState,
/// The ratchet state to store in the second slot.
/// A value of `None` implies that the second slot should be set to null.
pub new_state2: Option<&'a RatchetState>,
/// This field is `true` if and only if `new_state1 != cur_state1` and `new_state1 != cur_state2`.
///
/// This implies whether `state1` is a brand new ratchet state, or if it was previously saved.
pub new_state1_was_just_added: bool,
/// The ratchet state that we expect to see in the first slot.
/// If this value is not currently stored in the first slot, the entire update must be aborted.
pub cur_state1: &'a RatchetState,
/// The ratchet state that we expect to see in the second slot.
/// A value of `None` implies we expect the second slot to be set to "null".
///
/// If this value is not currently stored in the second slot, the entire update must be aborted.
pub cur_state2: Option<&'a RatchetState>,
/// This field is `true` if and only if `cur_state1 != new_state1` and `cur_state1 != new_state2`.
pub cur_state1_was_just_deleted: bool,
/// This field is `true` if and only if `cur_state2 != new_state1` and `cur_state2 != new_state2`.
pub cur_state2_was_just_deleted: bool,
}
impl<'a> RatchetUpdate<'a> {
/// Returns the final `RatchetStates` that should be the only thing saved after this update is
/// fully committed. Future calls to `ApplicationLayer::restore_by_identity` should return this struct.
pub fn to_states(&self) -> RatchetStates {
RatchetStates::new(self.state1.clone(), self.state2.cloned())
impl<'a> CompareAndSwap<'a> {
pub(crate) fn new(
new_state1: &'a RatchetState,
new_state2: Option<&'a RatchetState>,
new_state1_was_just_added: bool,
cur_state1: &'a RatchetState,
cur_state2: Option<&'a RatchetState>,
cur_state1_was_just_deleted: bool,
cur_state2_was_just_deleted: bool,
) -> Self {
Self {
new_state1,
new_state2,
new_state1_was_just_added,
cur_state1,
cur_state2,
cur_state1_was_just_deleted,
cur_state2_was_just_deleted,
}
}
/// Returns the final `RatchetStates` that must be swapped into storage if this update is
/// fully committed.
/// Future calls to `ApplicationLayer::restore_by_identity` should return this struct.
pub fn to_new_states(&self) -> RatchetStates {
RatchetStates::new(self.new_state1.clone(), self.new_state2.cloned())
}
/// Returns the `RatchetStates` that is expected to currently be in storage for this peer.
/// This value must be compared with the current value in storage, and if they are equal,
/// the return value of `CompareAndSwap::to_new_states` must overwrite it.
pub fn to_cur_states(&self) -> RatchetStates {
RatchetStates::new(self.cur_state1.clone(), self.cur_state2.cloned())
}
/// If this update specifies adding a brand new ratchet fingerprint, this function will return it.
/// The returned ratchet fingerprint will always be the ratchet fingerprint of field `state1`.
///
/// If a fingerprint is returned then it is guaranteed that `state1_was_just_added` will be true.
/// If a fingerprint is returned then it is guaranteed that `state1_was_just_added` will be `true`.
pub fn added_fingerprint(&self) -> Option<&[u8; RATCHET_SIZE]> {
if self.state1_was_just_added {
self.state1.fingerprint()
} else {
None
}
self.new_state1_was_just_added.then_some(self.new_state1.fingerprint())
}
/// If this updated specifies to delete an old ratchet fingerprint, this function will return it.
/// The returned ratchet fingerprint will always be the ratchet fingerprint of field
/// `deleted_state1`.
/// If this updated specifies to delete `cur_state1`, and `cur_state1` has a non-zero ratchet
/// fingerprint, this function will return the ratchet fingerprint.
///
/// If `cur_state1` is not being deleted (i.e. `cur_state1 == new_state1` or
/// `cur_state1 == new_state2`), then this will return `None`.
///
/// 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 let Some(rs) = &self.deleted_state1 {
rs.fingerprint()
} else {
None
if self.cur_state1_was_just_deleted {
if !self.cur_state1.is_empty() {
return Some(self.cur_state1.fingerprint());
}
}
None
}
/// If this updated specifies to delete two ratchet fingerprints, this function will return the
/// second one. `deleted_fingerprint1` will return the first one.
/// The returned ratchet fingerprint will always be the ratchet fingerprint of field
/// `deleted_state2`.
/// If this updated specifies to delete `cur_state2`, and `cur_state2` has a non-zero ratchet
/// fingerprint, this function will return the ratchet fingerprint.
///
/// It is exceptionally rare that there will be more than one ratchet fingerprint to be deleted.
/// Care should be taken to make sure that this update will still be correctly committed in the
/// rare event that this returns `Some`.
/// If `cur_state2` is not being deleted (i.e. `cur_state2 == new_state1` or
/// `cur_state2 == new_state2`), then this will return `None`.
///
/// There may be a second ratchet fingerprint to be deleted, which function
/// `deleted_fingerprint1` will return.
pub fn deleted_fingerprint2(&self) -> Option<&[u8; RATCHET_SIZE]> {
if let Some(rs) = &self.deleted_state2 {
rs.fingerprint()
} else {
None
if self.cur_state2_was_just_deleted {
if let Some(rf) = self.cur_state2 {
if !rf.is_empty() {
return Some(rf.fingerprint());
}
}
}
None
}
/// Returns true if the currently stored ratchet states are expected to be the initial ratchet
/// states. This is the default value for a peer's ratchet states in the event they could not
/// be found in storage.
///
/// If a peer could not be found in storage, and this function returns true,
/// 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()
}
/// Compares the ratchet fingerprints of `cur_state1` and `cur_state2` with `rf1` and `rf2`.
/// If they are equal this function will return `true`.
///
/// If this function returns `true`, then the implementation may proceed to swap out the ratchet
/// states these fingerprints come from with `new_state1` and `new_state2`.
pub fn compare_fingerprints(&self, rf1: &[u8; RATCHET_SIZE], rf2: Option<&[u8; RATCHET_SIZE]>) -> bool {
self.cur_state1.fingerprint_eq(rf1) & RatchetState::fingerprint_eq_nullable(self.cur_state2, rf2)
}
/// Compares `cur_state1` and `cur_state2` with `other.state1` and `other.state2`.
/// If they are equal this function will return `true`.
///
/// If this function returns `true`, then the implementation may proceed to swap `other.state1`
/// and `other.state2` with `new_state1` and `new_state2`.
pub fn compare(&self, other: &RatchetStates) -> bool {
self.cur_state1.eq(&other.state1) & self.cur_state2.eq(&other.state2.as_ref())
}
}
+2 -2
View File
@@ -166,7 +166,7 @@ pub enum ReceiveError {
WriteError(std::io::Error),
}
macro_rules! byzantine_fault {
macro_rules! fault {
($name:expr, $unnatural:ident) => {
ReceiveError::ByzantineFault(crate::result::ByzantineFault {
#[cfg(feature = "debug")]
@@ -178,7 +178,7 @@ macro_rules! byzantine_fault {
})
};
}
pub(crate) use byzantine_fault;
pub(crate) use fault;
/// Result generated by the context packet receive function, with possible payloads.
#[derive(Clone)]
+145 -135
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+43 -34
View File
@@ -214,13 +214,13 @@ So Alice and Bob will need to allow potentially the last two derived ratchet key
To minimize the number of ratchet keys Alice and Bob have to store in memory, the final step of every key exchange in ZSSP is to delete the previous ratchet key, once both peers are certain the other peer has derived the new one. So usually Alice and Bob will only have and use the most recently derived ratchet key, but if a peer disappears during key exchange, Alice and Bob will have up to the last two ratchet keys, and will be able to coordinate using the one they are certain both peers have access to.
If this is the first time Alice and Bob are communicating, Alice will send an empty string instead of a ratchet fingerprint to signal this fact. Both Alice and Bob understand this to mean they ought to complete the key exchange with a ratchet key of all zeros. When Alice reveals their identity in the third message of Noise XK, Bob will have to check that it is indeed true that Alice has never communicated with Bob before, and they don't already have a ratchet key and fingerprint. Similarly, if Alice does use a valid ratchet fingerprint and key, Bob must check if the pair is actually associated with Alice, and isn't being stolen from a different peer.
If this is the first time Alice and Bob are communicating, Alice will send a string of all zeros instead of a ratchet fingerprint to signal this fact. Both Alice and Bob understand this to mean they ought to complete the key exchange with a ratchet key of all zeros. When Alice reveals their identity in the third message of Noise XK, Bob will have to check that it is indeed true that Alice has never communicated with Bob before, and they don't already have a ratchet key and fingerprint. Similarly, if Alice does use a valid ratchet fingerprint and key, Bob must check if the pair is actually associated with Alice, and isn't being stolen from a different peer.
Since the ratchet fingerprint is included in the first message of Noise XK, with weak forward secrecy, there is some concern it could violate identity hiding. However, since the ratchet fingerprint is a constantly rotating ASK, information about the communicating peers can only be leaked by it if the same ratchet fingerprint is used twice. In normal operation, not only will the ratchet fingerprint only be used once, but it will be deleted with the ratchet key when the key exchange is completed, providing replay protection. Furthermore, if the same ratchet fingerprint is used more than once, all this will reveal is that the same pair of peers are initiating a session with each other, and only if Bob's private key is compromised.
Actually this protocol give ZSSP a lot of useful properties. For one thing Bob can require Alice to include a recognized, nonempty ratchet fingerprint in their first message. This is how ZSSP can achieve ``Silence is Golden'' despite the fact that the first Noise XK packet is usually anonymous and replayable. It also allows Bob to authenticate Alice's identity immediately, as opposed to waiting for Alice's next message. This protocol also is integral to how Alice and Bob are able to coordinate the ratchet key management described in \sectionref{sec:key_management} without any additional round trips.
Actually this protocol give ZSSP a lot of useful properties. For one thing Bob can require Alice to include a recognized, non-zero ratchet fingerprint in their first message. This is how ZSSP can achieve ``Silence is Golden'' despite the fact that the first Noise XK packet is usually anonymous and replayable. It also allows Bob to authenticate Alice's identity immediately, as opposed to waiting for Alice's next message. This protocol also is integral to how Alice and Bob are able to coordinate the ratchet key management described in \sectionref{sec:key_management} without any additional round trips.
If Alice has corrupted their storage, Alice can either send the empty string or send whatever corrupted ratchet fingerprint they still have to Bob. If Bob sees the empty string, and Bob is in opportunistic mode, Bob completes the key exchange with the zero ratchet key, making it possible for Alice to connect. If Alice sent a corrupted ratchet fingerprint, or Bob is the one who corrupted their storage, and if Bob is in opportunistic mode, Bob will ignore the unrecognized ratchet fingerprint and instead uses the zero ratchet key as the PSK. When Alice receives Bob's reply, if Alice is in opportunistic mode, Alice will decrypt Bob's reply with both the ratchet key they intended to use and the zero ratchet key, and go with the one that successfully decrypts the reply. This allows Alice and Bob to reset their ratchet keys in such a way that an attacker cannot perform a downgrade attack without at least one peer's static private key. And if Alice and Bob are in persistent mode, a downgrade attack is simply not possible.
If Alice has corrupted their storage, Alice can either send zeros or send whatever corrupted ratchet fingerprint they still have to Bob. If Bob sees zeros instead of a ratchet fingerprint, and Bob is in opportunistic mode, Bob completes the key exchange with the zero ratchet key, making it possible for Alice to connect. If Alice sent a corrupted ratchet fingerprint, or Bob is the one who corrupted their storage, and if Bob is in opportunistic mode, Bob will ignore the unrecognized ratchet fingerprint and instead uses the zero ratchet key as the PSK. When Alice receives Bob's reply, if Alice is in opportunistic mode, Alice will decrypt Bob's reply with both the ratchet key they intended to use and the zero ratchet key, and go with the one that successfully decrypts the reply. This allows Alice and Bob to reset their ratchet keys in such a way that an attacker cannot perform a downgrade attack without at least one peer's static private key. And if Alice and Bob are in persistent mode, a downgrade attack is simply not possible.
\section{Zeta Key Exchange}
@@ -271,8 +271,8 @@ The next section will be dedicated to describing ZKE in explicit detail. We will
\wordbox[tlr]{2}{\texttt{e1} (1568 bytes)} \\
\bitbox[blr]{2}{} & \bitbox{2}{\texttt{e1\_tag} (16 bytes)} \\
\begin{rightwordgroup}{Noise XK\\Payload}
\wordbox{2}{$\texttt{rf}_1$ (optional 32 bytes)} \\
\wordbox{2}{$\texttt{rf}_2$ (optional 32 bytes)} \\
\wordbox{2}{$\texttt{rf}_1$ (32 bytes)} \\
\wordbox{2}{$\texttt{rf}_2$ (32 bytes)} \\
\wordbox{1}{\texttt{rf\_tag} (16 bytes)}
\end{rightwordgroup} \\
\end{bytefield}
@@ -542,7 +542,9 @@ Given $(x, g^x)$ and $(y, g^y)$, two output keypairs from $\DHGEN()$, a \emph{ke
\subsection{Transition Algorithms}\label{sec:trans_alg}
With the necessary background established, we can now define the final component of the Zeta state machine, the state transition algorithms. As is described in \definitionref{def:state_machine}, each state transition within $\zeta$ has a corresponding algorithm that must be computed. All of these algorithms are described in this section. They have access to a collection of global variables, defined below.
With the necessary background established, we can now define the final component of the Zeta state machine, the state transition algorithms. Anyone looking to implement the Zeta state machine must read this section in its entireity. Many paragraphs contain rules not represented within the diagrams or algorithm psuedocode. Each rule qualified with \emph{must} must be implemented.
As is described in \definitionref{def:state_machine}, each state transition within $\zeta$ has a corresponding algorithm. These algorithms \emph{must} be computed atomically with the state transition. If the algorithm fails or aborts, then the corresponding state transition \emph{must} not occur. All of these algorithms are described in this section. They have access to a collection of global variables, defined below.
\begin{itemize}
\item Persistent ratchet keys $\texttt{rk}_1, \texttt{rk}_2$, to be used as a handshake PSK
@@ -562,16 +564,20 @@ With the necessary background established, we can now define the final component
\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.
All of these variables, except $\texttt{rk}$ and $\texttt{rf}$, 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$.
What constitutes a ``single, unique remote peer'' is up to the upper protocol to decide. Usually remote peers are identified and differentiated by their static public keys ($g^u$ or $g^v$). However ZKE does not strictly enforce a one-to-one relationship between static public keys and identity specifically so the upper protocol is able to rotate the static public keys of peers.
Variables $\texttt{rk}$ and $\texttt{rf}$ are stored on a persistent storage device, and can be accessed by multiple instances of $\zeta$ over time. Implementations \emph{must} guarantee each instance of persistent variables $\texttt{rk}$ and $\texttt{rf}$ is associated with a single, unique remote peer. When implementing $\zeta$, each instance of $\zeta$ \emph{must} store a local copy of $\texttt{rf}$. Writes to $\texttt{rk}$ and $\texttt{rf}$ \emph{must} be batched together as one atomic operation. Implementations \emph{must} check that the local copy of $\texttt{rf}$ is consistent with $\texttt{rf}$ prior to every write. If they are inconsistent, then the entire write \emph{must} be cancelled, and $\zeta$ \emph{must} immediately abort the transition algorithm which triggered the write. Then, $\zeta$ \emph{must} be deleted (i.e. $\zeta$ \emph{must} perform the timeout transition, $\tau$). This policy guarantees that if multiple, concurrent instances of $\zeta$ exist for the same remote peer, only one of them will be allowed to write to $\texttt{rk}$ and $\texttt{rf}$, while the others will be deleted.
It \emph{must} be infeasible to recover a non-zero value of $\texttt{rk}$ or $\texttt{rf}$ based on the amount of time read and write operations on $\texttt{rk}$ or $\texttt{rf}$ take. To this end, implementations must not directly operate on ratchet keys (check for equality, create a search index, etc.). Instead, these operations must be performed indirectly upon their ratchet fingerprints. This is to ensure that, in the event a side-channel attack is found, only the ratchet fingerprint can be leaked.
What constitutes a ``single, unique remote peer'' is up to the upper protocol to decide. Usually remote peers are identified and differentiated by their static public keys ($g^u$ or $g^v$). However ZKE does not strictly enforce a one-to-one relationship between static public keys and identity specifically so the upper protocol is able to rotate the static public keys of peers. There \emph{must} be a well-defined equality relation between peers.
\begin{definition}[H][Session Counter]
Every instance of $\zeta$ contains a single monotonically increasing counter, that is initialized to 0. We will use the notation $\COUNTER()$ to represent incrementing this counter by 1 and returning the previous counter value.
So the very first call to $\COUNTER()$ within $\zeta$ will return 0, the second call will return 1, and so on.
It is assumed that that output of $\COUNTER()$, the counter value, will be used as an AES-GCM nonce. When a remote peer decrypts an AES-GCM ciphertext that uses a counter value nonce, they must first statefully check that this counter has never been received before, and fail authentication if it has been replayed.
It is assumed that that output of $\COUNTER()$, the counter value, will be used as an AES-GCM nonce. When a remote peer decrypts an AES-GCM ciphertext that uses a counter value nonce, they \emph{must} first statefully check that this counter has never been received before, and fail authentication if it has been replayed.
\end{definition}
ZKE uses a 64-bit (8 byte) counter, but AES-GCM uses a 96-bit (12 byte) nonce/IV. The Noise protocol does specify that, in order to use a counter value as an AES nonce, it should be encoded as a big-endian integer with 4 bytes of zero padding to the left of it. However, for good reason, we have decided to utilize the rightmost byte of that 4 byte padding to store the packet type number, as depicted in \figureref{fig:nonce}. This accomplishes two goals. First it explicitly authenticates the packet type of a packet, which makes it infeasible for an attacker to change the packet type field of a header without triggering an authentication failure. We could have used the additional authentication data parameter of AES-GCM to accomplish this, but the Noise protocol already makes extensive use of this parameter. Second, this nonce construction reduces the risk of catastrophic nonce reuse due to implementation error, specifically during key exchanges. While a correctly implemented counter would be sufficient for security, this adds an extra layer of defence. Given a packet type number $p$, and a counter value $c$, we will use the notation $p||c$ to represent this nonce construction.
@@ -587,16 +593,15 @@ ZKE uses a 64-bit (8 byte) counter, but AES-GCM uses a 96-bit (12 byte) nonce/IV
\end{figure}
\begin{definition}[H][Key Id Generation]
\algn{KID-Gen} is a stateful algorithm that outputs a locally unique, uniform random 32-bit string. The output of this algorithm will be used as the \emph{key id} for a session key, allowing the local peer to multiplex many keys for many remote peers simultaneously. The output of $\algn{KID-Gen}()$ must be locally unique from all other key ids currently in use. $\algn{KID-Gen}()$ may only repeat an output if that output is currently not being used as a key id.
\algn{KID-Gen} is a stateful algorithm that outputs a locally unique, uniform random 32-bit string. The output of this algorithm will be used as the \emph{key id} for a session key, allowing the local peer to multiplex many keys for many remote peers simultaneously. The output of $\algn{KID-Gen}()$ \emph{must} be locally unique from all other key ids currently in use. $\algn{KID-Gen}()$ may only repeat an output if that output is currently not being used as a key id.
\end{definition}
\begin{definition}[H][Persistent State]
The variables $\texttt{rk}$ and $\texttt{rf}$ are considered the \emph{persistent state} of ZKE. The values stored in these variables are not lost when a session ends, and can be \emph{restored} at any time to initiate a new session.
$\texttt{rk}$ is a tuple of up to two 256-bit \emph{ratchet keys}. Similarly, $\texttt{rf}$ is a tuple of up to two 256-bit \emph{ratchet fingerprints}. $\texttt{rk}$ is initialized to $(0^{256}, \bot)$ and $\texttt{rf}$ is initialized to $(\varepsilon, \bot)$.
$\texttt{rk}$ is a tuple of up to two 256-bit \emph{ratchet keys}. Similarly, $\texttt{rf}$ is a tuple of up to two 256-bit \emph{ratchet fingerprints}. Both $\texttt{rk}$ and $\texttt{rf}$ are initialized to $(0^{256}, \bot)$.
If two peers are in persistent mode, and wish to connect for the first time, they may use a preshared one time password, $p$, to do so. $p$ must be a random string with at least 256-bits of entropy. If a one time password is being used then $r\gets \KDF(p, \texttt{"ZSSP\_OTP\_TO\_RATCHET"}, \varepsilon, 2)$ must be computed. Both peers then initialize $\texttt{rk}$ to $(r_1, \bot)$ and $\texttt{rf}$ to $(r_2, \bot)$. $p$ must only be used once; A fresh instance of $p$ must be generated for every new pair of peers who wish to connect.
If two peers are in persistent mode, and wish to connect for the first time, they may use a preshared one time password, $p$, to do so. $p$ \emph{must} be a random string with at least 256-bits of entropy. If a one time password is being used then $r\gets \KDF(p, \texttt{"ZSSP\_OTP\_TO\_RATCHET"}, \varepsilon, 2)$ \emph{must} be computed. Both peers then initialize $\texttt{rk}$ to $(r_1, \bot)$ and $\texttt{rf}$ to $(r_2, \bot)$. $p$ \emph{must} only be used once; A fresh instance of $p$ \emph{must} be generated for every new pair of peers who wish to connect.
\end{definition}
$\bot$ represents the ``null'' value. When a variable is set to $\bot$, it means whatever was previous stored within the variable is being permanently deleted from memory. All variables are initialized to $\bot$ unless otherwise stated. $\varepsilon$ is the empty string.
@@ -666,7 +671,11 @@ Prior to the execution of any of the following algorithms, $\zeta$ will check if
\State $X_1\texttt{.key\_id} \gets \texttt{kid}^1_1$
\State $X_1\texttt{.e} \gets g^x$
\State $X_1\texttt{.e1} \gets \AEAD(k_4, 0||0, h_3, e_{pub})$
\State $X_1\texttt{.rf} \gets \AEAD(k_4, 0||1, h_5, \texttt{rf})$
\If{$\texttt{rf}_2=\bot$}
\State $X_1\texttt{.rf} \gets \AEAD(k_4, 0||1, h_5,\,\texttt{rf}_1||0^{256})$
\Else
\State $X_1\texttt{.rf} \gets \AEAD(k_4, 0||1, h_5,\,\texttt{rf}_1||\texttt{rf}_2)$
\EndIf
\State $\texttt{hk} \gets \KDF(h_6, \texttt{"ASKH"}, c_4, 2)$
\State $c \gets X_1\texttt{.rf\_tag}\ \text{(last 8 bytes)}$
\State $\textbf{Send } (0,\ 0||c,\ X_1)$
@@ -686,15 +695,15 @@ Including the ratchet fingerprint in the Hello packet does not compromise identi
\EndIf
\State $\textbf{Decrypt } X_1$ according to Noise XK
\State $\texttt{rf}' \gets X_1\texttt{.rf}_1$
\State $\textbf{Restore } \texttt{rk}' \text{ based on } \texttt{rf}'$
\State $\textbf{Restore } \texttt{rk}' \text{ based on } \texttt{rf}'$ (or $\texttt{rk}'\gets \bot$ if $\texttt{rf}' = 0^{256}$)
\If{$\texttt{rk}' = \bot$}
\State $\texttt{rf}' \gets X_1\texttt{.rf}_2$
\State $\textbf{Restore } \texttt{rk}' \text{ based on } \texttt{rf}'$
\State $\textbf{Restore } \texttt{rk}' \text{ based on } \texttt{rf}'$ (or $\texttt{rk}'\gets \bot$ if $\texttt{rf}' = 0^{256}$)
\If{$\texttt{rk}' = \bot$}
\If{$\pi_1=1$}
\State \textbf{Fail authentication}
\Else
\State $\texttt{rf}' \gets \varepsilon$
\State $\texttt{rf}' \gets 0^{256}$
\State $\texttt{rk}' \gets 0^{256}$
\EndIf
\EndIf
@@ -719,12 +728,12 @@ Including the ratchet fingerprint in the Hello packet does not compromise identi
\If{$c \geq 2^{24} \orb c \neq X_2\texttt{.key\_id\_tag}$ (last 3 bytes)}
\State \textbf{Fail authentication}
\EndIf
\State $\textbf{Decrypt } X_2$ according to Noise XK using PSK $\texttt{rk}_1$
\State $i \gets 1$
\If{$X_2$ failed authentication}
\State $\textbf{Decrypt } X_2$ according to Noise XK using PSK $\texttt{rk}_2$
\State $\textbf{Decrypt } X_2$ according to Noise XK using PSK $\texttt{rk}_1$
\If{$X_2$ failed to decrypt}
\State $i \gets 2$
\If{$X_2$ failed authentication}
\State $\textbf{Decrypt } X_2$ according to Noise XK using PSK $\texttt{rk}_2$
\If{$X_2$ failed to decrypt}
\If{$\pi_2=1$}
\State \textbf{Fail authentication}
\Else
@@ -736,10 +745,10 @@ Including the ratchet fingerprint in the Hello packet does not compromise identi
\State $\texttt{kid}^1_2 \gets X_2\texttt{.key\_id}$
\State $X_3\texttt{.s} \gets \AEAD(k_{10}, 2||1, h_{11}, g^u)$
\State $X_3\texttt{.identity} \gets \AEAD(k_{13}, 2||0, h_{12}, \texttt{identity})$
\State $\texttt{kek}^1 \gets \KDF(h_{14}, \texttt{"ASKK"}, c_{13}, 2)$
\State $\texttt{nk}^1 \gets \KDF(\varepsilon, L, c_{13}, 2)$
\State $(\texttt{rk}_2, \texttt{rf}_2) \gets (\texttt{rk}_i, \texttt{rf}_i)$
\State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{14}, \texttt{"ASKR"}, c_{13}, 2)$
\State $\texttt{kek}^1 \gets \KDF(h_{14}, \texttt{"ASKK"}, c_{13}, 2)$
\State $\texttt{nk}^1 \gets \KDF(\varepsilon, L, c_{13}, 2)$
\State $\texttt{r} \gets 1$
\State $\texttt{i} \gets 0$
\State $\textbf{Send } (\texttt{kid}^1_2,\ 2||0,\ X_3)$
@@ -748,7 +757,7 @@ Including the ratchet fingerprint in the Hello packet does not compromise identi
Packet types $X_2$ and $K_1$ represent the second to last message of Noise XK and Noise KK. The remote peer will not have derived the Noise key yet, so if this peer attempted to encrypt with this key immediately, there is a risk its packets will arrive out of order and be undecryptable. Receiving a valid $C_1$ packet from the remote peer confirms that they have successfully derived the correct Noise key, which signals it can now be reliably used for encryption.
Similarly, the new ratchet key and fingerprint will not have been derived by the remote peer yet. So this peer will have to save these keys to persistent storage, but not yet delete the previous ratchet key and fingerprint until the remote peer sends a key confirmation. This means that for a brief period of time Alice has two sets of ratchet keys and fingerprints saved, instead of just one. We want to make sure Alice and Bob never have more than two of these keys saved at a time. So during the Noise XK handshake if Alice has two sets of keys and fingerprints, they must send both fingerprints to Bob, and delete the key and fingerprint that Bob chooses not to use as the PSK.
Similarly, the new ratchet key and fingerprint will not have been derived by the remote peer yet. So this peer will have to save these keys to persistent storage, but not yet delete the previous ratchet key and fingerprint until the remote peer sends a key confirmation. This means that for a brief period of time Alice has two sets of ratchet keys and fingerprints saved, instead of just one. We want to make sure Alice and Bob never have more than two of these keys saved at a time. So during the Noise XK handshake if Alice has two sets of keys and fingerprints, they \emph{must} send both fingerprints to Bob, and delete the key and fingerprint that Bob chooses not to use as the PSK.
\begin{algorithm}[H]
\caption{Transition $\delta(B_2, X_3)=S_1$ -- Bob receives the final packet of Noise XK. Input $\algn{Accept}$ is a function provided by the upper protocol that takes as input Alice's identity and outputs security flags $(\pi_3, \pi_4)$, $\pi_3$ can be $\bot$. If $\pi_3=\bot$, Bob will reject Alice's session. If $\pi_3=1$, and Bob is aware of a ratchet key Alice should have, but Alice is not connecting with it, Bob will reject Alice's session (similar to when $\pi_2=1$). If Bob rejects Alice's session and $\pi_4 = 0$, Bob will send a packet to Alice explicitly rejecting their session.}\label{alg:recv_x3}
@@ -760,8 +769,8 @@ Similarly, the new ratchet key and fingerprint will not have been derived by the
\State $(\pi_3, \pi_4) \gets \algn{Accept}(X_3\texttt{.s}, X_3\texttt{.identity})$
\If{$\pi_3 \neq \bot$}
\State $\textbf{Restore } (\texttt{rk}, \texttt{rf}) \text{ based on } (X_3\texttt{.s}, X_3\texttt{.identity})$
\If{$(\texttt{rf}', \texttt{rk}') \neq (\texttt{rf}_1, \texttt{rk}_1) \andb (\texttt{rf}', \texttt{rk}') \neq (\texttt{rf}_2, \texttt{rk}_2)$}
\If{$\pi_3 = 0 \andb \texttt{rf}' = \varepsilon$}
\If{$\texttt{rf}' \neq \texttt{rf}_1 \andb \texttt{rf}' \neq \texttt{rf}_2$}
\If{$\pi_3 = 0 \andb \texttt{rf}' = 0^{256}$}
\State \textbf{Warn} the upper protocol that Alice does not have the ratchet key
\Else
\If{$\pi_4 = 0$}
@@ -771,9 +780,9 @@ Similarly, the new ratchet key and fingerprint will not have been derived by the
\State \textbf{Fail authentication}
\EndIf
\EndIf
\State $\texttt{nk}^1 \gets \KDF(\varepsilon, L, c_{13}, 2)$
\State $(\texttt{rk}_2, \texttt{rf}_2) \gets \bot$
\State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{14}, \texttt{"ASKR"}, c_{13}, 2)$
\State $\texttt{nk}^1 \gets \KDF(\varepsilon, L, c_{13}, 2)$
\State $\texttt{r} \gets 2$
\State $\texttt{i} \gets 1$
\State $C_1\texttt{.kek\_tag} \gets \AEAD(\texttt{kek}^1_1, 3||c, \varepsilon, \varepsilon)$
@@ -788,12 +797,14 @@ Similarly, the new ratchet key and fingerprint will not have been derived by the
\end{algorithmic}
\end{algorithm}
When packet $X_3$ is received, Bob might refuse to start a session with Alice based on their static key and identity. The preferred way for Bob to do this is to simply do nothing, and ignore Alice's packets. This would follow the ``Silence is Golden" principle of security. However there are some applications where, usually for UX reasons, Alice needs to be able tell apart having a bad connection with Bob from Bob rejecting Alice's identity. If Bob always goes silent Alice has no reliable way to tell these two situations apart. Since both Bob and Alice have the key exchange key at this point in the handshake, it was a very natural and secure extension of the protocol to allow Bob to notify Alice that the session has been refused.
If, when function $\algn{Accept}$ is called, any other instance of $\zeta$ exists with the same remote peer, then $\algn{Accept}$ \emph{must} be implemented to do one of the following: Either the other instance of $\zeta$ is deleted, or $\algn{Accept}$ returns $\pi_3=\bot$ to reject the new session. Instances of $\zeta$ in state $B_2$ \emph{must} be ignored in regards to the prior rule, since the identity of the remote peer will not have been confirmed and might not be known. Keep in mind that this rule implies that if Alice attempts to open multiple sessions with Bob at once, only one of them will be accepted.
When packet $X_3$ is received, Bob might refuse to start a session with Alice based on their static key and identity. The preferred way for Bob to do this is to simply do nothing, and ignore Alice's packets. This would follow the ``Silence is Golden" principle of security. However there are some applications where, usually for UX reasons, Alice needs to be able tell apart having a bad connection with Bob from Bob rejecting Alice's identity. If Bob always goes silent, Alice has no reliable way to tell these two situations apart. This is the reason why packet $D$ is created and sent at the bottom of \algorithmref{alg:recv_x3}. Receipt of packet $D$ implies Bob has explicitly rejected a session with Alice.
Packet types $X_3$ and $K_2$ represent the final messages of Noise XK and Noise KK, respectively. The peer who receives these packets can be sure that the remote peer has derived an identical Noise key already, and so the new key can be reliably used immediately.
\begin{algorithm}[H]
\caption{Transition $\delta(A_3, C_1)=S_2$ and $\delta(R_2, C_1)=S_2$ -- A key confirmation for either the Noise XK initial handshake or Noise KK rekeying has been received. This peer must send an Acknowledgement, \figureref{packet:key_conf}, to let the remote peer know that it has been received.}\label{alg:key_conf}
\caption{Transition $\delta(A_3, C_1)=S_2$ and $\delta(R_2, C_1)=S_2$ -- A key confirmation for either the Noise XK initial handshake or Noise KK rekeying has been received. This peer \emph{must} send an Acknowledgement, \figureref{packet:key_conf}, to let the remote peer know that it has been received.}\label{alg:key_conf}
\begin{algorithmic}
\Require $(\texttt{kid}^{\texttt{i} + 1}_{\texttt{r}},\ 3||c,\ C_1)$
\State $\textbf{Decrypt } C_1$ using $\texttt{kek}^{\texttt{i} + 1}_{\texttt{r}}$
@@ -820,7 +831,6 @@ Packet types $C_2$ and $D$ have trivial transition algorithms. They merely decry
\end{algorithmic}
\end{algorithm}
\begin{algorithm}[H]
\caption{Transition $\delta(S_2, K_1)=R_2$ and $\delta(R_1, K_1)=R_2$ -- Bob has received Alice's request to rekey and sends a Rekey Completion packet, \figureref{packet:rekey}, in reply. Bob has now finished Noise KK and has obtained a new Noise key. A new pair of key ids are generated so Bob can tell apart packets encrypted with the new key from those encrypted with the previous key.}
\begin{algorithmic}
@@ -833,28 +843,27 @@ Packet types $C_2$ and $D$ have trivial transition algorithms. They merely decry
\State $K_2\texttt{.key\_id} \gets \AEAD(k_{10}', 6||0, h_{8}, \texttt{kid}^{\texttt{i} + 1}_\texttt{r})$
\State $c \gets \COUNTER()$
\State $K_2 \gets \AEAD(\texttt{kek}^\texttt{i}_{3 - \texttt{r}}, 6||c, \varepsilon, K_2)$
\State $(\texttt{rk}_2, \texttt{rf}_2) \gets (\texttt{rk}_1, \texttt{rf}_1)$
\State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{11}', \texttt{"ASKR"}, c_{10}', 2)$
\State $\texttt{kek}^{\texttt{i} - 1} \gets \bot$
\State $(\texttt{kek}^{\texttt{i} + 1}_{3 - \texttt{r}}, \texttt{kek}^{\texttt{i} + 1}_\texttt{r}) \gets \KDF(h_{11}', \texttt{"ASKK"}, c_{10}', 2)$
\State $\texttt{nk}^{\texttt{i} - 1} \gets \bot$
\State $(\texttt{nk}^{\texttt{i} + 1}_{3 - \texttt{r}}, \texttt{nk}^{\texttt{i} + 1}_\texttt{r}) \gets \KDF(\varepsilon, L, c_{10}', 2)$
\State $(\texttt{rk}_2, \texttt{rf}_2) \gets (\texttt{rk}_1, \texttt{rf}_1)$
\State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{11}', \texttt{"ASKR"}, c_{10}', 2)$
\State $\textbf{Send } (\texttt{kid}^\texttt{i}_{3 - \texttt{r}},\ 6||c,\ K_2)$
\end{algorithmic}
\end{algorithm}
\begin{algorithm}[H]
\caption{Transition $\delta(R_1, K_2)=S_1$ -- Alice has received Bob's reply and so can also finish Noise KK. They send a Key Confirmation, \figureref{packet:key_conf}, to signal the completion of the handshake. The second to last Noise key is deleted for forward secrecy.}
\begin{algorithmic}
\Require $(\texttt{kid}^\texttt{i}_\texttt{r},\ 6||c,\ K_2)$
\State $\textbf{Decrypt } K_2$ using $\texttt{kek}^\texttt{i}_\texttt{r}$, and Noise KK
\State $\texttt{kid}^{\texttt{i} + 1}_{3 - \texttt{r}} \gets K_2\texttt{.key\_id}$
\State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{11}', \texttt{"ASKR"}, c_{10}', 2)$
\State $\texttt{kek}^{\texttt{i} - 1} \gets \bot$
\State $(\texttt{kek}^{\texttt{i} + 1}_\texttt{r}, \texttt{kek}^{\texttt{i} + 1}_{3 - \texttt{r}}) \gets \KDF(h_{11}', \texttt{"ASKK"}, c_{10}', 2)$
\State $\texttt{nk}^{\texttt{i} - 1} \gets \bot$
\State $(\texttt{nk}^{\texttt{i} + 1}_\texttt{r}, \texttt{nk}^{\texttt{i} + 1}_{3 - \texttt{r}}) \gets \KDF(\varepsilon, L, c_{10}', 2)$
\State $(\texttt{rk}_1, \texttt{rf}_1) \gets \KDF(h_{11}', \texttt{"ASKR"}, c_{10}', 2)$
\State $\texttt{i} \gets \texttt{i} + 1$
\State $c \gets \COUNTER()$
\State $C_1\texttt{.kek\_tag} \gets \AEAD(\texttt{kek}^\texttt{i}_{3 - \texttt{r}}, 3||c, \varepsilon, \varepsilon)$