lib

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

commit 0dfe1fbee00440940826e57ec80a89a266d938bb
parent bf002de77c06790d55ded6a81cff197cd157ad31
Author: triesap <tyson@radroots.org>
Date:   Sat, 28 Mar 2026 17:11:15 +0000

simplex: expose dm receive helpers and accept padded identities

Diffstat:
Mcrates/simplex-agent-runtime/src/runtime.rs | 25++++++++++++++++++++++++-
Mcrates/simplex-agent-runtime/src/types.rs | 1+
Mcrates/simplex-smp-proto/src/uri.rs | 13+++++++++++++
Mcrates/simplex-smp-transport/src/client.rs | 29+++++++++++++++++++++++++++--
4 files changed, 65 insertions(+), 3 deletions(-)

diff --git a/crates/simplex-agent-runtime/src/runtime.rs b/crates/simplex-agent-runtime/src/runtime.rs @@ -491,6 +491,27 @@ impl RadrootsSimplexAgentRuntime { Ok(()) } + pub fn ack_last_received_message( + &mut self, + connection_id: &str, + message_id: u64, + receipt_info: Vec<u8>, + now: u64, + ) -> Result<(), RadrootsSimplexAgentRuntimeError> { + let message_hash = self + .store + .connection(connection_id)? + .delivery_cursor + .last_received_message_hash + .clone() + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{connection_id}` has no received message hash to acknowledge" + )) + })?; + self.ack_message(connection_id, message_id, message_hash, receipt_info, now) + } + pub fn reconnect_connection( &mut self, connection_id: &str, @@ -615,13 +636,15 @@ impl RadrootsSimplexAgentRuntime { }, ); } - _ => { + RadrootsSimplexAgentMessage::UserMessage(body) => { self.events .push_back(RadrootsSimplexAgentRuntimeEvent::MessageReceived { connection_id: connection_id.into(), message_id: frame.header.message_id, + body, }); } + _ => {} } } } diff --git a/crates/simplex-agent-runtime/src/types.rs b/crates/simplex-agent-runtime/src/types.rs @@ -25,6 +25,7 @@ pub enum RadrootsSimplexAgentRuntimeEvent { MessageReceived { connection_id: String, message_id: u64, + body: Vec<u8>, }, MessageAcknowledged { connection_id: String, diff --git a/crates/simplex-smp-proto/src/uri.rs b/crates/simplex-smp-proto/src/uri.rs @@ -295,6 +295,7 @@ fn validate_base64_url( ) -> Result<(), RadrootsSimplexSmpProtoError> { base64::engine::general_purpose::URL_SAFE_NO_PAD .decode(value) + .or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(value)) .map(|_| ()) .map_err(|_| RadrootsSimplexSmpProtoError::InvalidBase64Url { field, @@ -345,6 +346,18 @@ mod tests { } #[test] + fn parses_padded_server_identity_and_dh_public_key() { + let uri = RadrootsSimplexSmpQueueUri::parse( + "smp://YWJjZA==@server.example/cXVldWU=#/?v=4&dh=ZGhLZXk=", + ) + .unwrap(); + + assert_eq!(uri.server.server_identity, "YWJjZA=="); + assert_eq!(uri.sender_id, "cXVldWU="); + assert_eq!(uri.recipient_dh_public_key, "ZGhLZXk="); + } + + #[test] fn parses_legacy_sender_secure_queue_uri() { let uri = RadrootsSimplexSmpQueueUri::parse( "smp://YWJjZA@server1.example:5223/cXVldWU#/?v=1-3&dh=ZGhLZXk&k=s&srv=server2.example", diff --git a/crates/simplex-smp-transport/src/client.rs b/crates/simplex-smp-transport/src/client.rs @@ -11,7 +11,7 @@ use crate::handshake::{ RadrootsSimplexSmpTlsHandshakeEvidence, RadrootsSimplexSmpTlsPolicy, validate_tls_handshake, }; use base64::Engine as _; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::engine::general_purpose::{URL_SAFE, URL_SAFE_NO_PAD}; use radroots_simplex_smp_crypto::prelude::{ RadrootsSimplexSmpQueueAuthorizationMaterial, RadrootsSimplexSmpQueueAuthorizationScope, }; @@ -247,7 +247,8 @@ fn connect_live_session_host( .to_vec(); let server_hello = read_server_hello(&mut stream)?; let actual_identity = matching_server_identity(&peer_certs, &server.server_identity)?; - let mut policy = RadrootsSimplexSmpTlsPolicy::modern(server.server_identity.clone()); + let expected_identity = canonical_server_identity(&server.server_identity)?; + let mut policy = RadrootsSimplexSmpTlsPolicy::modern(expected_identity); policy.require_tls_unique_binding = false; let transport_version = validate_tls_handshake( &policy, @@ -297,6 +298,7 @@ fn matching_server_identity( chain: &[CertificateDer<'static>], expected_identity: &str, ) -> Result<String, RadrootsSimplexSmpTransportError> { + let expected_identity = canonical_server_identity(expected_identity)?; for certificate in chain { let identity = server_identity_from_certificate(certificate.as_ref())?; if identity == expected_identity { @@ -326,6 +328,18 @@ fn server_identity_from_certificate( Ok(URL_SAFE_NO_PAD.encode(digest)) } +fn canonical_server_identity(value: &str) -> Result<String, RadrootsSimplexSmpTransportError> { + URL_SAFE_NO_PAD + .decode(value) + .or_else(|_| URL_SAFE.decode(value)) + .map(|decoded| URL_SAFE_NO_PAD.encode(decoded)) + .map_err(|_| { + RadrootsSimplexSmpTransportError::InvalidServerAddress(format!( + "invalid base64url server identity `{value}`" + )) + }) +} + #[derive(Debug)] struct PermissiveSimplexServerVerifier; @@ -371,3 +385,14 @@ impl ServerCertVerifier for PermissiveSimplexServerVerifier { ] } } + +#[cfg(test)] +mod tests { + use super::canonical_server_identity; + + #[test] + fn canonicalizes_padded_and_unpadded_server_identity() { + assert_eq!(canonical_server_identity("YWJjZA").unwrap(), "YWJjZA"); + assert_eq!(canonical_server_identity("YWJjZA==").unwrap(), "YWJjZA"); + } +}