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:
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");
+ }
+}