lib

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

commit 959622c8304bab5c5f9cff08636ac44a6fddee50
parent a6d107eff1815c5699b6799c95d5478dbe1927a7
Author: triesap <tyson@radroots.org>
Date:   Mon, 22 Jun 2026 23:45:12 +0000

simplex: derive official no-pq X3DH init keys

- add sender and receiver X3DH initialization helpers
- derive associated data, ratchet key, and header keys from X448 secrets
- reject PQ X3DH fields until the PQ ratchet slice wires them through
- cover sender and receiver convergence with deterministic crypto tests

Diffstat:
Mcrates/simplex_smp_crypto/src/lib.rs | 27++++++++++++++-------------
Mcrates/simplex_smp_crypto/src/official_ratchet.rs | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 161 insertions(+), 13 deletions(-)

diff --git a/crates/simplex_smp_crypto/src/lib.rs b/crates/simplex_smp_crypto/src/lib.rs @@ -40,19 +40,20 @@ pub mod prelude { RadrootsSimplexOfficialChainKdfOutput, RadrootsSimplexOfficialEncryptedHeader, RadrootsSimplexOfficialEncryptedMessage, RadrootsSimplexOfficialMsgHeader, RadrootsSimplexOfficialRootKdfOutput, RadrootsSimplexOfficialSntrup761Keypair, - RadrootsSimplexOfficialX3dhParams, RadrootsSimplexOfficialX448Keypair, - decapsulate_official_sntrup761, decode_official_encrypted_header, - decode_official_encrypted_message, decode_official_msg_header, - decode_official_x3dh_params_uri, decode_official_x448_public_key_der, - derive_official_x448_shared_secret, encapsulate_official_sntrup761, - encode_official_encrypted_header, encode_official_encrypted_message, - encode_official_msg_header, encode_official_x3dh_params_uri, - encode_official_x448_public_key_der, generate_official_sntrup761_keypair, - generate_official_x448_keypair, official_aes_gcm_decrypt_padded, - official_aes_gcm_encrypt_padded, official_chain_kdf, official_encoded_encrypted_header_len, - official_encoded_encrypted_message_len, official_full_header_len, - official_ratchet_header_len, official_root_kdf, official_sntrup761_keypair_from_seed, - official_x448_keypair_from_seed, + RadrootsSimplexOfficialX3dhInit, RadrootsSimplexOfficialX3dhParams, + RadrootsSimplexOfficialX448Keypair, decapsulate_official_sntrup761, + decode_official_encrypted_header, decode_official_encrypted_message, + decode_official_msg_header, decode_official_x3dh_params_uri, + decode_official_x448_public_key_der, derive_official_x448_shared_secret, + encapsulate_official_sntrup761, encode_official_encrypted_header, + encode_official_encrypted_message, encode_official_msg_header, + encode_official_x3dh_params_uri, encode_official_x448_public_key_der, + generate_official_sntrup761_keypair, generate_official_x448_keypair, + official_aes_gcm_decrypt_padded, official_aes_gcm_encrypt_padded, official_chain_kdf, + official_encoded_encrypted_header_len, official_encoded_encrypted_message_len, + official_full_header_len, official_ratchet_header_len, official_root_kdf, + official_sntrup761_keypair_from_seed, official_x3dh_receiver_init, + official_x3dh_sender_init, official_x448_keypair_from_seed, }; pub use crate::ratchet::{ RadrootsSimplexSmpRatchetHeader, RadrootsSimplexSmpRatchetRole, diff --git a/crates/simplex_smp_crypto/src/official_ratchet.rs b/crates/simplex_smp_crypto/src/official_ratchet.rs @@ -82,6 +82,14 @@ pub struct RadrootsSimplexOfficialX3dhParams { } #[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsSimplexOfficialX3dhInit { + pub associated_data: Vec<u8>, + pub ratchet_key: Vec<u8>, + pub sending_header_key: Vec<u8>, + pub receiving_next_header_key: Vec<u8>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub struct RadrootsSimplexOfficialMsgHeader { pub max_version: u16, pub dh_public_key: Vec<u8>, @@ -322,6 +330,50 @@ pub fn decode_official_x3dh_params_uri( Ok(params) } +pub fn official_x3dh_sender_init( + local_key_1: &RadrootsSimplexOfficialX448Keypair, + local_key_2: &RadrootsSimplexOfficialX448Keypair, + remote_params: &RadrootsSimplexOfficialX3dhParams, +) -> Result<RadrootsSimplexOfficialX3dhInit, RadrootsSimplexSmpCryptoError> { + validate_official_x3dh_keypair(local_key_1)?; + validate_official_x3dh_keypair(local_key_2)?; + validate_official_x3dh_params(remote_params)?; + if remote_params.pq_public_key.is_some() || remote_params.pq_ciphertext.is_some() { + return Err(RadrootsSimplexSmpCryptoError::IncompletePqHeader); + } + official_x3dh_init( + &local_key_1.public_key, + &remote_params.key_1, + &[ + derive_official_x448_shared_secret(&local_key_2.private_key, &remote_params.key_1)?, + derive_official_x448_shared_secret(&local_key_1.private_key, &remote_params.key_2)?, + derive_official_x448_shared_secret(&local_key_2.private_key, &remote_params.key_2)?, + ], + ) +} + +pub fn official_x3dh_receiver_init( + local_key_1: &RadrootsSimplexOfficialX448Keypair, + local_key_2: &RadrootsSimplexOfficialX448Keypair, + remote_params: &RadrootsSimplexOfficialX3dhParams, +) -> Result<RadrootsSimplexOfficialX3dhInit, RadrootsSimplexSmpCryptoError> { + validate_official_x3dh_keypair(local_key_1)?; + validate_official_x3dh_keypair(local_key_2)?; + validate_official_x3dh_params(remote_params)?; + if remote_params.pq_public_key.is_some() || remote_params.pq_ciphertext.is_some() { + return Err(RadrootsSimplexSmpCryptoError::IncompletePqHeader); + } + official_x3dh_init( + &remote_params.key_1, + &local_key_1.public_key, + &[ + derive_official_x448_shared_secret(&local_key_1.private_key, &remote_params.key_2)?, + derive_official_x448_shared_secret(&local_key_2.private_key, &remote_params.key_1)?, + derive_official_x448_shared_secret(&local_key_2.private_key, &remote_params.key_2)?, + ], + ) +} + pub fn official_sntrup761_keypair_from_seed( seed: &[u8], ) -> RadrootsSimplexOfficialSntrup761Keypair { @@ -761,6 +813,52 @@ fn validate_official_x3dh_params( Ok(()) } +fn validate_official_x3dh_keypair( + keypair: &RadrootsSimplexOfficialX448Keypair, +) -> Result<(), RadrootsSimplexSmpCryptoError> { + if keypair.public_key.len() != RADROOTS_SIMPLEX_OFFICIAL_X448_KEY_LENGTH { + return Err(RadrootsSimplexSmpCryptoError::InvalidPublicKeyLength( + keypair.public_key.len(), + )); + } + if keypair.private_key.len() != RADROOTS_SIMPLEX_OFFICIAL_X448_KEY_LENGTH { + return Err(RadrootsSimplexSmpCryptoError::InvalidPrivateKeyLength( + keypair.private_key.len(), + )); + } + Ok(()) +} + +fn official_x3dh_init( + sender_key_1: &[u8], + receiver_key_1: &[u8], + shared_secrets: &[Vec<u8>; 3], +) -> Result<RadrootsSimplexOfficialX3dhInit, RadrootsSimplexSmpCryptoError> { + let mut associated_data = Vec::with_capacity(sender_key_1.len() + receiver_key_1.len()); + associated_data.extend_from_slice(sender_key_1); + associated_data.extend_from_slice(receiver_key_1); + let mut input = Vec::with_capacity( + RADROOTS_SIMPLEX_OFFICIAL_X448_SHARED_SECRET_LENGTH * shared_secrets.len(), + ); + for shared_secret in shared_secrets { + if shared_secret.len() != RADROOTS_SIMPLEX_OFFICIAL_X448_SHARED_SECRET_LENGTH { + return Err(RadrootsSimplexSmpCryptoError::InvalidSharedSecretLength( + shared_secret.len(), + )); + } + input.extend_from_slice(shared_secret); + } + let zero_salt = [0_u8; 64]; + let (sending_header_key, receiving_next_header_key, ratchet_key) = + official_hkdf3(&zero_salt, &input, RADROOTS_SIMPLEX_OFFICIAL_X3DH_INFO)?; + Ok(RadrootsSimplexOfficialX3dhInit { + associated_data, + ratchet_key, + sending_header_key, + receiving_next_header_key, + }) +} + fn encode_official_urlsafe_bytes(bytes: &[u8]) -> String { URL_SAFE.encode(bytes) } @@ -1060,6 +1158,55 @@ mod tests { } #[test] + fn official_x3dh_no_pq_init_matches_on_both_sides() { + let receiver_key_1 = official_x448_keypair_from_seed(b"rr-synth-x3dh-rcv-1"); + let receiver_key_2 = official_x448_keypair_from_seed(b"rr-synth-x3dh-rcv-2"); + let sender_key_1 = official_x448_keypair_from_seed(b"rr-synth-x3dh-snd-1"); + let sender_key_2 = official_x448_keypair_from_seed(b"rr-synth-x3dh-snd-2"); + let receiver_params = RadrootsSimplexOfficialX3dhParams { + version_range: RadrootsSimplexSmpVersionRange::new( + RADROOTS_SIMPLEX_OFFICIAL_E2E_KDF_VERSION, + RADROOTS_SIMPLEX_OFFICIAL_E2E_CURRENT_VERSION, + ) + .unwrap(), + key_1: receiver_key_1.public_key.clone(), + key_2: receiver_key_2.public_key.clone(), + pq_public_key: None, + pq_ciphertext: None, + }; + let sender_params = RadrootsSimplexOfficialX3dhParams { + version_range: receiver_params.version_range, + key_1: sender_key_1.public_key.clone(), + key_2: sender_key_2.public_key.clone(), + pq_public_key: None, + pq_ciphertext: None, + }; + + let sender_init = + official_x3dh_sender_init(&sender_key_1, &sender_key_2, &receiver_params).unwrap(); + let receiver_init = + official_x3dh_receiver_init(&receiver_key_1, &receiver_key_2, &sender_params).unwrap(); + + assert_eq!(sender_init, receiver_init); + assert_eq!( + sender_init.associated_data, + [sender_key_1.public_key, receiver_key_1.public_key].concat() + ); + assert_eq!( + sender_init.ratchet_key.len(), + RADROOTS_SIMPLEX_OFFICIAL_AES_KEY_LENGTH + ); + assert_eq!( + sender_init.sending_header_key.len(), + RADROOTS_SIMPLEX_OFFICIAL_AES_KEY_LENGTH + ); + assert_eq!( + sender_init.receiving_next_header_key.len(), + RADROOTS_SIMPLEX_OFFICIAL_AES_KEY_LENGTH + ); + } + + #[test] fn sntrup761_encapsulation_roundtrips() { let recipient = official_sntrup761_keypair_from_seed(b"rr-synth-official-pq-recipient"); let (ciphertext, sender_secret) =