lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 0812e5a463997eebcf414404f0c52414a1ba20a6
parent 1d5afb26133755df00a3cdb0cbd5d9b99cf094ac
Author: triesap <tyson@radroots.org>
Date:   Tue, 23 Jun 2026 00:53:56 +0000

simplex: enable initial runtime pq handshake

- persist local SNTRUP keypairs with agent connections
- publish proposed KEM params from created invitations
- stage accepted KEM params on join confirmations
- initialize creator ratchets from accepted PQ confirmations

Diffstat:
Mcrates/simplex_agent_proto/src/codec.rs | 59++++++++++++++++++++++++++++++++++++++---------------------
Mcrates/simplex_agent_runtime/src/runtime.rs | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mcrates/simplex_agent_store/src/lib.rs | 9+++++----
Mcrates/simplex_agent_store/src/store.rs | 23+++++++++++++++++++++++
Mcrates/simplex_smp_crypto/src/ratchet.rs | 10+++++++++-
5 files changed, 223 insertions(+), 57 deletions(-)

diff --git a/crates/simplex_agent_proto/src/codec.rs b/crates/simplex_agent_proto/src/codec.rs @@ -11,10 +11,9 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; use radroots_simplex_smp_crypto::prelude::{ RadrootsSimplexOfficialX3dhParams, RadrootsSimplexSmpRatchetHeader, - decode_official_x3dh_params_uri, encode_official_x3dh_params_uri, }; use radroots_simplex_smp_proto::prelude::{ - RadrootsSimplexSmpQueueUri, RadrootsSimplexSmpServerAddress, + RadrootsSimplexSmpQueueUri, RadrootsSimplexSmpServerAddress, RadrootsSimplexSmpVersionRange, }; pub fn encode_connection_link( @@ -23,9 +22,8 @@ pub fn encode_connection_link( let mut buffer = Vec::new(); push_short_bytes(&mut buffer, link.invitation_queue.to_string().as_bytes())?; push_short_bytes(&mut buffer, &link.connection_id)?; - let e2e_ratchet_params = encode_official_x3dh_params_uri(&link.e2e_ratchet_params) - .map_err(|error| RadrootsSimplexAgentProtoError::InvalidE2eParameters(error.to_string()))?; - push_short_bytes(&mut buffer, e2e_ratchet_params.as_bytes())?; + let e2e_ratchet_params = encode_x3dh_params_binary(&link.e2e_ratchet_params)?; + push_large_bytes(&mut buffer, &e2e_ratchet_params)?; buffer.push(encode_bool(link.contact_address)); Ok(buffer) } @@ -39,11 +37,7 @@ pub fn decode_connection_link( let link = RadrootsSimplexAgentConnectionLink { invitation_queue: RadrootsSimplexSmpQueueUri::parse(&invitation_queue)?, connection_id: cursor.read_short_bytes()?, - e2e_ratchet_params: decode_official_x3dh_params_uri( - &String::from_utf8(cursor.read_short_bytes()?) - .map_err(|error| RadrootsSimplexAgentProtoError::InvalidUtf8(error.to_string()))?, - ) - .map_err(|error| RadrootsSimplexAgentProtoError::InvalidE2eParameters(error.to_string()))?, + e2e_ratchet_params: decode_x3dh_params_binary(&cursor.read_large_bytes()?)?, contact_address: decode_bool(cursor.read_byte()?)?, }; cursor.finish()?; @@ -358,10 +352,8 @@ fn encode_optional_x3dh_params( match params { Some(params) => { buffer.push(b'1'); - let encoded = encode_official_x3dh_params_uri(params).map_err(|error| { - RadrootsSimplexAgentProtoError::InvalidE2eParameters(error.to_string()) - })?; - push_short_bytes(buffer, encoded.as_bytes()) + let encoded = encode_x3dh_params_binary(params)?; + push_large_bytes(buffer, &encoded) } None => { buffer.push(b'0'); @@ -370,19 +362,44 @@ fn encode_optional_x3dh_params( } } +fn encode_x3dh_params_binary( + params: &RadrootsSimplexOfficialX3dhParams, +) -> Result<Vec<u8>, RadrootsSimplexAgentProtoError> { + let mut buffer = Vec::new(); + buffer.extend_from_slice(&params.version_range.min.to_be_bytes()); + buffer.extend_from_slice(&params.version_range.max.to_be_bytes()); + push_short_bytes(&mut buffer, &params.key_1)?; + push_short_bytes(&mut buffer, &params.key_2)?; + push_maybe_large_bytes(&mut buffer, params.pq_public_key.as_deref())?; + push_maybe_large_bytes(&mut buffer, params.pq_ciphertext.as_deref())?; + Ok(buffer) +} + +fn decode_x3dh_params_binary( + bytes: &[u8], +) -> Result<RadrootsSimplexOfficialX3dhParams, RadrootsSimplexAgentProtoError> { + let mut cursor = Cursor::new(bytes); + let version_range = RadrootsSimplexSmpVersionRange::new(cursor.read_u16()?, cursor.read_u16()?) + .map_err(|error| RadrootsSimplexAgentProtoError::InvalidE2eParameters(error.to_string()))?; + let params = RadrootsSimplexOfficialX3dhParams { + version_range, + key_1: cursor.read_short_bytes()?, + key_2: cursor.read_short_bytes()?, + pq_public_key: cursor.read_maybe(decode_large_bytes)?, + pq_ciphertext: cursor.read_maybe(decode_large_bytes)?, + }; + cursor.finish()?; + Ok(params) +} + fn decode_optional_x3dh_params( cursor: &mut Cursor<'_>, ) -> Result<Option<RadrootsSimplexOfficialX3dhParams>, RadrootsSimplexAgentProtoError> { match cursor.read_byte()? { b'0' => Ok(None), b'1' => { - let encoded = String::from_utf8(cursor.read_short_bytes()?) - .map_err(|error| RadrootsSimplexAgentProtoError::InvalidUtf8(error.to_string()))?; - decode_official_x3dh_params_uri(&encoded) - .map(Some) - .map_err(|error| { - RadrootsSimplexAgentProtoError::InvalidE2eParameters(error.to_string()) - }) + let encoded = cursor.read_large_bytes()?; + decode_x3dh_params_binary(&encoded).map(Some) } tag => Err(RadrootsSimplexAgentProtoError::InvalidTag( String::from_utf8_lossy(&[tag]).into_owned(), diff --git a/crates/simplex_agent_runtime/src/runtime.rs b/crates/simplex_agent_runtime/src/runtime.rs @@ -17,18 +17,20 @@ use radroots_simplex_agent_proto::prelude::{ }; use radroots_simplex_agent_store::prelude::{ RadrootsSimplexAgentOutboundMessage, RadrootsSimplexAgentPendingCommand, - RadrootsSimplexAgentPendingCommandKind, RadrootsSimplexAgentQueueRole, - RadrootsSimplexAgentStore, RadrootsSimplexAgentX3dhKeypair, + RadrootsSimplexAgentPendingCommandKind, RadrootsSimplexAgentPqKeypair, + RadrootsSimplexAgentQueueRole, RadrootsSimplexAgentStore, RadrootsSimplexAgentX3dhKeypair, }; use radroots_simplex_smp_crypto::prelude::{ RADROOTS_SIMPLEX_OFFICIAL_E2E_CURRENT_VERSION, RADROOTS_SIMPLEX_OFFICIAL_E2E_KDF_VERSION, - RADROOTS_SIMPLEX_SMP_NONCE_LENGTH, RadrootsSimplexOfficialX3dhParams, - RadrootsSimplexOfficialX448Keypair, RadrootsSimplexSmpCommandAuthorization, - RadrootsSimplexSmpCryptoError, RadrootsSimplexSmpRatchetState, RadrootsSimplexSmpX25519Keypair, - decode_x25519_public_key_x509, decrypt_padded, derive_shared_secret, - encode_ed25519_public_key_x509, encode_x25519_public_key_x509, encrypt_padded, - official_x3dh_receiver_init, official_x3dh_sender_init, official_x448_keypair_from_seed, - random_nonce, + RADROOTS_SIMPLEX_SMP_NONCE_LENGTH, RadrootsSimplexOfficialSntrup761Keypair, + RadrootsSimplexOfficialX3dhParams, RadrootsSimplexOfficialX448Keypair, + RadrootsSimplexSmpCommandAuthorization, RadrootsSimplexSmpCryptoError, + RadrootsSimplexSmpRatchetState, RadrootsSimplexSmpX25519Keypair, decode_x25519_public_key_x509, + decrypt_padded, derive_shared_secret, encode_ed25519_public_key_x509, + encode_x25519_public_key_x509, encrypt_padded, official_sntrup761_keypair_from_seed, + official_x3dh_receiver_init, official_x3dh_receiver_init_accepting_pq, + official_x3dh_sender_init, official_x3dh_sender_init_accepting_pq, + official_x448_keypair_from_seed, random_nonce, }; use radroots_simplex_smp_proto::prelude::{ RADROOTS_SIMPLEX_SMP_CURRENT_CLIENT_VERSION, RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, @@ -187,6 +189,14 @@ impl RadrootsSimplexAgentRuntime { &now.to_be_bytes(), ], )); + let pq_keypair = official_sntrup761_keypair_from_seed(&derive_material( + b"connection-create-pq-kem", + &[ + invitation_queue.to_string().as_bytes(), + &e2e_keypair.public_key, + &now.to_be_bytes(), + ], + )); let e2e_ratchet_params = RadrootsSimplexOfficialX3dhParams { version_range: RadrootsSimplexSmpVersionRange::new( RADROOTS_SIMPLEX_OFFICIAL_E2E_KDF_VERSION, @@ -195,15 +205,18 @@ impl RadrootsSimplexAgentRuntime { .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?, key_1: x3dh_key_1.public_key.clone(), key_2: x3dh_key_2.public_key.clone(), - pq_public_key: None, + pq_public_key: Some(pq_keypair.public_key.clone()), pq_ciphertext: None, }; - let ratchet_state = RadrootsSimplexSmpRatchetState::initiator( + let mut ratchet_state = RadrootsSimplexSmpRatchetState::initiator( x3dh_key_2.public_key.clone(), x3dh_key_1.public_key.clone(), None, ) .ok(); + if let Some(ratchet_state) = ratchet_state.as_mut() { + ratchet_state.current_pq_public_key = Some(pq_keypair.public_key.clone()); + } let connection = self.store.create_connection( if contact_address { RadrootsSimplexAgentConnectionMode::ContactAddress @@ -243,6 +256,7 @@ impl RadrootsSimplexAgentRuntime { connection.local_e2e_private_key = Some(e2e_keypair.private_key); connection.local_x3dh_key_1 = Some(agent_x3dh_keypair(x3dh_key_1)); connection.local_x3dh_key_2 = Some(agent_x3dh_keypair(x3dh_key_2)); + connection.local_pq_keypair = Some(agent_pq_keypair(pq_keypair)); let queue = connection .queues .iter_mut() @@ -297,21 +311,63 @@ impl RadrootsSimplexAgentRuntime { &now.to_be_bytes(), ], )); + let local_pq_keypair = invitation + .e2e_ratchet_params + .pq_public_key + .as_ref() + .map(|_| { + official_sntrup761_keypair_from_seed(&derive_material( + b"connection-join-pq-kem", + &[ + invitation.connection_id.as_slice(), + reply_queue.to_string().as_bytes(), + &now.to_be_bytes(), + ], + )) + }); let mut ratchet_state = RadrootsSimplexSmpRatchetState::responder( local_x3dh_key_2.public_key.clone(), invitation.e2e_ratchet_params.key_2.clone(), - None, - ) - .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; - let sender_init = official_x3dh_sender_init( - &local_x3dh_key_1, - &local_x3dh_key_2, - &invitation.e2e_ratchet_params, + local_pq_keypair + .as_ref() + .map(|keypair| keypair.public_key.clone()), ) .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; - ratchet_state - .initialize_official_sender(local_x3dh_key_2.private_key.clone(), sender_init) + let local_pq_keypair = if let Some(local_pq_keypair) = local_pq_keypair { + let sender_init = official_x3dh_sender_init_accepting_pq( + &local_x3dh_key_1, + &local_x3dh_key_2, + local_pq_keypair, + &invitation.e2e_ratchet_params, + &derive_material( + b"connection-join-pq-encapsulation", + &[ + invitation.connection_id.as_slice(), + reply_queue.to_string().as_bytes(), + &now.to_be_bytes(), + ], + ), + ) + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + ratchet_state + .initialize_official_sender(local_x3dh_key_2.private_key.clone(), sender_init.init) + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + ratchet_state.current_pq_public_key = sender_init.sender_params.pq_public_key.clone(); + ratchet_state.pending_outbound_pq_ciphertext = + sender_init.sender_params.pq_ciphertext.clone(); + Some(sender_init.local_pq_keypair) + } else { + let sender_init = official_x3dh_sender_init( + &local_x3dh_key_1, + &local_x3dh_key_2, + &invitation.e2e_ratchet_params, + ) .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + ratchet_state + .initialize_official_sender(local_x3dh_key_2.private_key.clone(), sender_init) + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + None + }; let connection = self.store.create_connection( RadrootsSimplexAgentConnectionMode::Direct, RadrootsSimplexAgentConnectionStatus::JoinPending, @@ -354,6 +410,7 @@ impl RadrootsSimplexAgentRuntime { connection.local_e2e_private_key = Some(local_e2e_keypair.private_key); connection.local_x3dh_key_1 = Some(agent_x3dh_keypair(local_x3dh_key_1)); connection.local_x3dh_key_2 = Some(agent_x3dh_keypair(local_x3dh_key_2)); + connection.local_pq_keypair = local_pq_keypair.map(agent_pq_keypair); connection.shared_secret = Some(shared_secret); let queue = connection .queues @@ -1426,11 +1483,19 @@ impl RadrootsSimplexAgentRuntime { command.connection_id )) })?; + let ratchet_state = connection.ratchet_state.as_ref().ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{}` missing ratchet state", + command.connection_id + )) + })?; join_confirmation = Some(( queue.descriptor.clone(), - official_x3dh_params_from_keys( + official_x3dh_params_from_parts( &local_x3dh_key_1.public_key, &local_x3dh_key_2.public_key, + ratchet_state.current_pq_public_key.clone(), + ratchet_state.pending_outbound_pq_ciphertext.clone(), )?, )); } @@ -1624,10 +1689,27 @@ impl RadrootsSimplexAgentRuntime { "SimpleX connection `{connection_id}` missing local X3DH key 2" )) })?; + let local_pq_keypair = connection.local_pq_keypair.clone(); let local_key_1 = official_x3dh_keypair_from_agent(local_key_1); let local_key_2 = official_x3dh_keypair_from_agent(local_key_2); - let receiver_init = official_x3dh_receiver_init(&local_key_1, &local_key_2, params) - .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + let receiver_init = if params.pq_public_key.is_some() || params.pq_ciphertext.is_some() { + let local_pq_keypair = local_pq_keypair.ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{connection_id}` missing local PQ keypair" + )) + })?; + official_x3dh_receiver_init_accepting_pq( + &local_key_1, + &local_key_2, + &official_pq_keypair_from_agent(local_pq_keypair), + params, + ) + .map(|init| init.init) + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))? + } else { + official_x3dh_receiver_init(&local_key_1, &local_key_2, params) + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))? + }; self.store .connection_mut(connection_id)? .ratchet_state @@ -1809,9 +1891,29 @@ fn official_x3dh_keypair_from_agent( } } -fn official_x3dh_params_from_keys( +fn agent_pq_keypair( + keypair: RadrootsSimplexOfficialSntrup761Keypair, +) -> RadrootsSimplexAgentPqKeypair { + RadrootsSimplexAgentPqKeypair { + public_key: keypair.public_key, + private_key: keypair.private_key, + } +} + +fn official_pq_keypair_from_agent( + keypair: RadrootsSimplexAgentPqKeypair, +) -> RadrootsSimplexOfficialSntrup761Keypair { + RadrootsSimplexOfficialSntrup761Keypair { + public_key: keypair.public_key, + private_key: keypair.private_key, + } +} + +fn official_x3dh_params_from_parts( key_1: &[u8], key_2: &[u8], + pq_public_key: Option<Vec<u8>>, + pq_ciphertext: Option<Vec<u8>>, ) -> Result<RadrootsSimplexOfficialX3dhParams, RadrootsSimplexAgentRuntimeError> { Ok(RadrootsSimplexOfficialX3dhParams { version_range: RadrootsSimplexSmpVersionRange::new( @@ -1821,8 +1923,8 @@ fn official_x3dh_params_from_keys( .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?, key_1: key_1.to_vec(), key_2: key_2.to_vec(), - pq_public_key: None, - pq_ciphertext: None, + pq_public_key, + pq_ciphertext, }) } @@ -2362,6 +2464,13 @@ mod tests { .local_x3dh_key_2 .clone() .unwrap(); + let local_pq_keypair = runtime + .store + .connection(&joined) + .unwrap() + .local_pq_keypair + .clone() + .unwrap(); let ready = runtime.retry_pending(30, 16); let confirmation_params = ready .into_iter() @@ -2381,8 +2490,11 @@ mod tests { assert_eq!(confirmation_params.key_1, local_key_1.public_key); assert_eq!(confirmation_params.key_2, local_key_2.public_key); - assert!(confirmation_params.pq_public_key.is_none()); - assert!(confirmation_params.pq_ciphertext.is_none()); + assert_eq!( + confirmation_params.pq_public_key, + Some(local_pq_keypair.public_key) + ); + assert!(confirmation_params.pq_ciphertext.is_some()); } #[test] @@ -2404,9 +2516,14 @@ mod tests { let joined_connection = runtime.store.connection(&joined).unwrap(); let joined_key_1 = joined_connection.local_x3dh_key_1.as_ref().unwrap(); let joined_key_2 = joined_connection.local_x3dh_key_2.as_ref().unwrap(); - let e2e_ratchet_params = - official_x3dh_params_from_keys(&joined_key_1.public_key, &joined_key_2.public_key) - .unwrap(); + let joined_ratchet = joined_connection.ratchet_state.as_ref().unwrap(); + let e2e_ratchet_params = official_x3dh_params_from_parts( + &joined_key_1.public_key, + &joined_key_2.public_key, + joined_ratchet.current_pq_public_key.clone(), + joined_ratchet.pending_outbound_pq_ciphertext.clone(), + ) + .unwrap(); let envelope = RadrootsSimplexAgentEnvelope::Confirmation { reply_queue: true, e2e_ratchet_params: Some(e2e_ratchet_params), diff --git a/crates/simplex_agent_store/src/lib.rs b/crates/simplex_agent_store/src/lib.rs @@ -11,9 +11,10 @@ pub mod prelude { pub use crate::store::{ RadrootsSimplexAgentConnectionRecord, RadrootsSimplexAgentDeliveryCursor, RadrootsSimplexAgentOutboundMessage, RadrootsSimplexAgentPendingCommand, - RadrootsSimplexAgentPendingCommandKind, RadrootsSimplexAgentPreparedOutboundMessage, - RadrootsSimplexAgentQueueAuthState, RadrootsSimplexAgentQueueRecord, - RadrootsSimplexAgentQueueRole, RadrootsSimplexAgentRecentMessageRecord, - RadrootsSimplexAgentStore, RadrootsSimplexAgentX3dhKeypair, + RadrootsSimplexAgentPendingCommandKind, RadrootsSimplexAgentPqKeypair, + RadrootsSimplexAgentPreparedOutboundMessage, RadrootsSimplexAgentQueueAuthState, + RadrootsSimplexAgentQueueRecord, RadrootsSimplexAgentQueueRole, + RadrootsSimplexAgentRecentMessageRecord, RadrootsSimplexAgentStore, + RadrootsSimplexAgentX3dhKeypair, }; } diff --git a/crates/simplex_agent_store/src/store.rs b/crates/simplex_agent_store/src/store.rs @@ -89,6 +89,13 @@ pub struct RadrootsSimplexAgentX3dhKeypair { } #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct RadrootsSimplexAgentPqKeypair { + pub public_key: Vec<u8>, + pub private_key: Vec<u8>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub enum RadrootsSimplexAgentPendingCommandKind { CreateQueue { descriptor: RadrootsSimplexAgentQueueDescriptor, @@ -143,6 +150,7 @@ pub struct RadrootsSimplexAgentConnectionRecord { pub local_e2e_private_key: Option<Vec<u8>>, pub local_x3dh_key_1: Option<RadrootsSimplexAgentX3dhKeypair>, pub local_x3dh_key_2: Option<RadrootsSimplexAgentX3dhKeypair>, + pub local_pq_keypair: Option<RadrootsSimplexAgentPqKeypair>, pub shared_secret: Option<Vec<u8>>, pub delivery_cursor: RadrootsSimplexAgentDeliveryCursor, pub last_received_queue: Option<RadrootsSimplexAgentQueueAddress>, @@ -175,6 +183,7 @@ struct RadrootsSimplexAgentConnectionSnapshot { local_e2e_private_key: Option<Vec<u8>>, local_x3dh_key_1: Option<RadrootsSimplexAgentX3dhKeypair>, local_x3dh_key_2: Option<RadrootsSimplexAgentX3dhKeypair>, + local_pq_keypair: Option<RadrootsSimplexAgentPqKeypair>, shared_secret: Option<Vec<u8>>, delivery_cursor: RadrootsSimplexAgentDeliveryCursor, last_received_queue: Option<RadrootsSimplexAgentQueueAddressSnapshot>, @@ -406,6 +415,7 @@ impl RadrootsSimplexAgentStore { local_e2e_private_key: None, local_x3dh_key_1: None, local_x3dh_key_2: None, + local_pq_keypair: None, shared_secret: None, delivery_cursor: RadrootsSimplexAgentDeliveryCursor { last_sent_message_id: None, @@ -883,6 +893,7 @@ fn connection_to_snapshot( local_e2e_private_key: record.local_e2e_private_key, local_x3dh_key_1: record.local_x3dh_key_1, local_x3dh_key_2: record.local_x3dh_key_2, + local_pq_keypair: record.local_pq_keypair, shared_secret: record.shared_secret, delivery_cursor: record.delivery_cursor, last_received_queue: record.last_received_queue.map(queue_address_to_snapshot), @@ -926,6 +937,7 @@ fn connection_from_snapshot( local_e2e_private_key: snapshot.local_e2e_private_key, local_x3dh_key_1: snapshot.local_x3dh_key_1, local_x3dh_key_2: snapshot.local_x3dh_key_2, + local_pq_keypair: snapshot.local_pq_keypair, shared_secret: snapshot.shared_secret, delivery_cursor: snapshot.delivery_cursor, last_received_queue: snapshot @@ -1593,6 +1605,10 @@ mod tests { public_key: b"x3dh-public-2".to_vec(), private_key: b"x3dh-private-2".to_vec(), }); + connection.local_pq_keypair = Some(RadrootsSimplexAgentPqKeypair { + public_key: b"pq-public".to_vec(), + private_key: b"pq-private".to_vec(), + }); } store.flush().unwrap(); @@ -1643,6 +1659,13 @@ mod tests { .map(|key| (key.public_key.as_slice(), key.private_key.as_slice())), Some((&b"x3dh-public-2"[..], &b"x3dh-private-2"[..])) ); + assert_eq!( + loaded_connection + .local_pq_keypair + .as_ref() + .map(|key| (key.public_key.as_slice(), key.private_key.as_slice())), + Some((&b"pq-public"[..], &b"pq-private"[..])) + ); assert_eq!(loaded.pending_commands.len(), 2); assert!(loaded.pending_commands.values().any(|command| matches!( &command.kind, diff --git a/crates/simplex_smp_crypto/src/ratchet.rs b/crates/simplex_smp_crypto/src/ratchet.rs @@ -575,7 +575,13 @@ impl RadrootsSimplexSmpRatchetState { )?; let receiving_dh = derive_official_x448_shared_secret(&local_private_key, &header.dh_public_key)?; - let receiving_root = official_root_kdf(&root_key, &receiving_dh, None)?; + let receiving_pq_shared_secret = if header.pq_ciphertext.is_some() { + self.current_pq_shared_secret.as_deref() + } else { + None + }; + let receiving_root = + official_root_kdf(&root_key, &receiving_dh, receiving_pq_shared_secret)?; let next_local_keypair = generate_official_x448_keypair()?; let sending_dh = derive_official_x448_shared_secret( &next_local_keypair.private_key, @@ -586,6 +592,8 @@ impl RadrootsSimplexSmpRatchetState { self.sending_chain_length = 0; self.receiving_chain_length = 0; self.remote_dh_public_key = header.dh_public_key.clone(); + self.remote_pq_public_key = header.pq_public_key.clone(); + self.pending_inbound_pq_ciphertext = header.pq_ciphertext.clone(); self.local_dh_public_key = next_local_keypair.public_key; self.local_dh_private_key = Some(next_local_keypair.private_key); self.official_root_key = Some(sending_root.root_key);