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:
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(¶ms.version_range.min.to_be_bytes());
+ buffer.extend_from_slice(¶ms.version_range.max.to_be_bytes());
+ push_short_bytes(&mut buffer, ¶ms.key_1)?;
+ push_short_bytes(&mut buffer, ¶ms.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);