commit 9fdd88480b6aa683ddf9ec4213861c8c72ca176e
parent 41188033d11137b831ea25c5ddb7e1ffb2dfefd2
Author: triesap <tyson@radroots.org>
Date: Sat, 28 Mar 2026 14:49:39 +0000
simplex: add live smp auth and tls transport client
Diffstat:
14 files changed, 969 insertions(+), 181 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -140,6 +140,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
+name = "asn1-rs"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror 2.0.18",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -510,6 +549,12 @@ dependencies = [
]
[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -633,6 +678,33 @@ dependencies = [
]
[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "dary_heap"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -667,6 +739,30 @@ dependencies = [
]
[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "der-parser"
+version = "10.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
+[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -728,6 +824,30 @@ dependencies = [
]
[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -777,6 +897,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
name = "filetime"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1774,12 +1900,31 @@ dependencies = [
]
[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
name = "num-conv"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1790,6 +1935,15 @@ dependencies = [
]
[[package]]
+name = "oid-registry"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
+dependencies = [
+ "asn1-rs",
+]
+
+[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1905,6 +2059,16 @@ dependencies = [
]
[[package]]
+name = "pem"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+dependencies = [
+ "base64 0.22.1",
+ "serde_core",
+]
+
+[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1966,6 +2130,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2441,6 +2615,7 @@ name = "radroots-simplex-agent-store"
version = "0.1.0-alpha.1"
dependencies = [
"radroots-simplex-agent-proto",
+ "radroots-simplex-smp-crypto",
"radroots-simplex-smp-proto",
"serde",
"serde_json",
@@ -2475,6 +2650,8 @@ dependencies = [
name = "radroots-simplex-smp-crypto"
version = "0.1.0-alpha.1"
dependencies = [
+ "ed25519-dalek",
+ "getrandom 0.2.17",
"radroots-simplex-smp-proto",
"sha2",
]
@@ -2490,7 +2667,13 @@ dependencies = [
name = "radroots-simplex-smp-transport"
version = "0.1.0-alpha.1"
dependencies = [
+ "base64 0.22.1",
+ "radroots-simplex-smp-crypto",
"radroots-simplex-smp-proto",
+ "rcgen",
+ "rustls",
+ "sha2",
+ "x509-parser 0.17.0",
]
[[package]]
@@ -2636,6 +2819,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
[[package]]
+name = "rcgen"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e"
+dependencies = [
+ "pem",
+ "ring",
+ "rustls-pki-types",
+ "time",
+ "x509-parser 0.18.1",
+ "yasna",
+]
+
+[[package]]
name = "redox_syscall"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2831,6 +3028,15 @@ dependencies = [
]
[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom",
+]
+
+[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3127,6 +3333,15 @@ dependencies = [
]
[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
+[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3155,6 +3370,16 @@ dependencies = [
]
[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
name = "sqlite-wasm-rs"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4364,6 +4589,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
+name = "x509-parser"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460"
+dependencies = [
+ "asn1-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom",
+ "oid-registry",
+ "ring",
+ "rusticata-macros",
+ "thiserror 2.0.18",
+ "time",
+]
+
+[[package]]
+name = "x509-parser"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
+dependencies = [
+ "asn1-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom",
+ "oid-registry",
+ "ring",
+ "rusticata-macros",
+ "thiserror 2.0.18",
+ "time",
+]
+
+[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4396,6 +4657,15 @@ dependencies = [
]
[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+dependencies = [
+ "time",
+]
+
+[[package]]
name = "yoke"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -93,7 +93,9 @@ chrono = { version = "0.4" }
clap = { version = "4" }
config = { version = "0.14" }
directories = { version = "6" }
+ed25519-dalek = { version = "2.1.1", default-features = false }
futures = { version = "0.3" }
+getrandom = { version = "0.2", default-features = false }
hex = { version = "0.4" }
js-sys = { version = "0.3" }
keyring = { version = "3.6.3", default-features = false, features = [
@@ -115,6 +117,7 @@ serde_json = { version = "1", default-features = false, features = ["alloc"] }
serde-wasm-bindgen = { version = "0.6" }
sha2 = { version = "0.10", default-features = false }
reqwest = { version = "0.12", default-features = false }
+rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
rust_decimal = { version = "1", default-features = false }
rust_decimal_macros = { version = "1" }
sled = { version = "0.34" }
@@ -130,6 +133,7 @@ ts-rs = { version = "11.1" }
typeshare = { version = "1" }
url = { version = "2" }
uuid = { version = "1.22.0", features = ["v4", "v7"] }
+x509-parser = { version = "0.17", default-features = false }
zstd = { version = "0.13", default-features = false }
zeroize = { version = "1" }
uniffi = { version = "=0.29.4" }
diff --git a/crates/simplex-agent-runtime/src/runtime.rs b/crates/simplex-agent-runtime/src/runtime.rs
@@ -18,8 +18,7 @@ use radroots_simplex_agent_store::prelude::{
RadrootsSimplexAgentStore,
};
use radroots_simplex_smp_crypto::prelude::{
- RadrootsSimplexSmpQueueAuthorizationMaterial, RadrootsSimplexSmpQueueAuthorizationScope,
- RadrootsSimplexSmpRatchetState,
+ RadrootsSimplexSmpCommandAuthorization, RadrootsSimplexSmpRatchetState,
};
use radroots_simplex_smp_proto::prelude::{
RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, RadrootsSimplexSmpBrokerMessage,
@@ -157,6 +156,7 @@ impl RadrootsSimplexAgentRuntime {
contact_address,
};
self.store.connection_mut(&connection.id)?.invitation = Some(invitation.clone());
+ let receive_auth_state = self.store.generate_queue_auth_state()?;
let descriptor = RadrootsSimplexAgentQueueDescriptor {
queue_uri: invitation_queue,
replaced_queue: None,
@@ -168,6 +168,7 @@ impl RadrootsSimplexAgentRuntime {
descriptor.clone(),
RadrootsSimplexAgentQueueRole::Receive,
true,
+ receive_auth_state,
)?;
self.store.enqueue_command(
&connection.id,
@@ -222,15 +223,14 @@ impl RadrootsSimplexAgentRuntime {
Some(invitation.clone()),
ratchet_state,
);
+ let send_auth_state = self.store.generate_queue_auth_state()?;
let send_descriptor = RadrootsSimplexAgentQueueDescriptor {
queue_uri: invitation.invitation_queue.clone(),
replaced_queue: None,
primary: true,
- sender_key: Some(derive_material(
- b"join-sender-auth",
- &[invitation.connection_id.as_slice(), &now.to_be_bytes()],
- )),
+ sender_key: Some(send_auth_state.public_key.clone()),
};
+ let receive_auth_state = self.store.generate_queue_auth_state()?;
let receive_descriptor = RadrootsSimplexAgentQueueDescriptor {
queue_uri: reply_queue,
replaced_queue: None,
@@ -242,12 +242,14 @@ impl RadrootsSimplexAgentRuntime {
send_descriptor.clone(),
RadrootsSimplexAgentQueueRole::Send,
true,
+ send_auth_state,
)?;
self.store.add_queue(
&connection.id,
receive_descriptor.clone(),
RadrootsSimplexAgentQueueRole::Receive,
true,
+ receive_auth_state,
)?;
self.store.enqueue_command(
&connection.id,
@@ -486,11 +488,15 @@ impl RadrootsSimplexAgentRuntime {
}
RadrootsSimplexAgentDecryptedMessage::ConnectionInfoReply { reply_queues, info } => {
for descriptor in reply_queues {
+ let auth_state = self.store.generate_queue_auth_state()?;
+ let mut descriptor = descriptor;
+ descriptor.sender_key = Some(auth_state.public_key.clone());
self.store.add_queue(
connection_id,
descriptor,
RadrootsSimplexAgentQueueRole::Send,
true,
+ auth_state,
)?;
}
self.store.set_status(
@@ -634,11 +640,13 @@ impl RadrootsSimplexAgentRuntime {
match &command.kind {
RadrootsSimplexAgentPendingCommandKind::RotateQueues { descriptors } => {
for descriptor in descriptors.clone() {
+ let auth_state = self.store.generate_queue_auth_state()?;
self.store.add_queue(
&command.connection_id,
descriptor,
RadrootsSimplexAgentQueueRole::Receive,
true,
+ auth_state,
)?;
}
self.record_command_outcome(
@@ -697,30 +705,18 @@ impl RadrootsSimplexAgentRuntime {
)
})?;
let correlation_id = correlation_id_for_command(command.id);
- let scope = RadrootsSimplexSmpQueueAuthorizationScope::new(
- auth.session_identifier.clone(),
- correlation_id,
- queue_address.sender_id.clone(),
- )
- .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?;
- let material = RadrootsSimplexSmpQueueAuthorizationMaterial::for_command(
- &scope,
- &smp_command,
- RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION,
- auth.queue_key_material.clone(),
- auth.server_session_key.clone(),
- )
- .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?;
Ok(RadrootsSimplexSmpTransportRequest {
server: queue.descriptor.queue_uri.server.clone(),
transport_version: RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION,
- transmission:
- radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpCommandTransmission {
- authorization: material.authorized_digest.to_vec(),
- correlation_id: Some(correlation_id),
- entity_id: queue_address.sender_id,
- command: smp_command,
+ correlation_id: Some(correlation_id),
+ entity_id: queue_address.sender_id,
+ command: smp_command,
+ authorization: RadrootsSimplexSmpCommandAuthorization::Ed25519(
+ radroots_simplex_smp_crypto::prelude::RadrootsSimplexSmpEd25519Keypair {
+ public_key: auth.public_key,
+ private_key: auth.private_key,
},
+ ),
})
}
@@ -735,34 +731,39 @@ impl RadrootsSimplexAgentRuntime {
RadrootsSimplexAgentRuntimeError,
> {
match &command.kind {
- RadrootsSimplexAgentPendingCommandKind::CreateQueue { descriptor } => Ok((
- descriptor.queue_address(),
- RadrootsSimplexSmpCommand::New(RadrootsSimplexSmpNewQueueRequest {
- recipient_auth_public_key: descriptor.queue_uri.sender_id.as_bytes().to_vec(),
- recipient_dh_public_key: descriptor
- .queue_uri
- .recipient_dh_public_key
- .as_bytes()
- .to_vec(),
- basic_auth: None,
- subscription_mode: RadrootsSimplexSmpSubscriptionMode::Subscribe,
- queue_request_data: Some(
- match descriptor
+ RadrootsSimplexAgentPendingCommandKind::CreateQueue { descriptor } => {
+ let auth_state = self
+ .store
+ .queue_auth_state(&command.connection_id, &descriptor.queue_address())?;
+ Ok((
+ descriptor.queue_address(),
+ RadrootsSimplexSmpCommand::New(RadrootsSimplexSmpNewQueueRequest {
+ recipient_auth_public_key: auth_state.public_key,
+ recipient_dh_public_key: descriptor
.queue_uri
- .queue_mode
- .unwrap_or(RadrootsSimplexSmpQueueMode::Messaging)
- {
- RadrootsSimplexSmpQueueMode::Messaging => {
- RadrootsSimplexSmpQueueRequestData::Messaging(None)
- }
- RadrootsSimplexSmpQueueMode::Contact => {
- RadrootsSimplexSmpQueueRequestData::Contact(None)
- }
- },
- ),
- notifier_credentials: None,
- }),
- )),
+ .recipient_dh_public_key
+ .as_bytes()
+ .to_vec(),
+ basic_auth: None,
+ subscription_mode: RadrootsSimplexSmpSubscriptionMode::Subscribe,
+ queue_request_data: Some(
+ match descriptor
+ .queue_uri
+ .queue_mode
+ .unwrap_or(RadrootsSimplexSmpQueueMode::Messaging)
+ {
+ RadrootsSimplexSmpQueueMode::Messaging => {
+ RadrootsSimplexSmpQueueRequestData::Messaging(None)
+ }
+ RadrootsSimplexSmpQueueMode::Contact => {
+ RadrootsSimplexSmpQueueRequestData::Contact(None)
+ }
+ },
+ ),
+ notifier_credentials: None,
+ }),
+ ))
+ }
RadrootsSimplexAgentPendingCommandKind::SecureQueue { queue, sender_key } => Ok((
queue.clone(),
RadrootsSimplexSmpCommand::SKey(sender_key.clone().unwrap_or_default()),
@@ -923,6 +924,9 @@ fn correlation_id_for_command(command_id: u64) -> RadrootsSimplexSmpCorrelationI
mod tests {
use super::*;
use alloc::collections::VecDeque;
+ use radroots_simplex_smp_crypto::prelude::{
+ RadrootsSimplexSmpQueueAuthorizationMaterial, RadrootsSimplexSmpQueueAuthorizationScope,
+ };
use radroots_simplex_smp_proto::prelude::{
RadrootsSimplexSmpBrokerTransmission, RadrootsSimplexSmpQueueIdsResponse,
};
@@ -964,11 +968,33 @@ mod tests {
&mut self,
request: RadrootsSimplexSmpTransportRequest,
) -> Result<RadrootsSimplexSmpTransportResponse, Self::Error> {
- let block =
- RadrootsSimplexSmpTransportBlock::from_current_command_transmissions(&[request
- .transmission
- .clone()])
- .map_err(|error| error.to_string())?;
+ let correlation_id = request
+ .correlation_id
+ .ok_or_else(|| "missing scripted transport correlation id".to_owned())?;
+ let scope = RadrootsSimplexSmpQueueAuthorizationScope::new(
+ b"scripted-session".to_vec(),
+ correlation_id,
+ request.entity_id.clone(),
+ )
+ .map_err(|error| error.to_string())?;
+ let material = RadrootsSimplexSmpQueueAuthorizationMaterial::for_command(
+ &scope,
+ &request.command,
+ request.transport_version,
+ &request.authorization,
+ )
+ .map_err(|error| error.to_string())?;
+ let transmission =
+ radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpCommandTransmission {
+ authorization: material.authorization,
+ correlation_id: Some(correlation_id),
+ entity_id: request.entity_id.clone(),
+ command: request.command.clone(),
+ };
+ let block = RadrootsSimplexSmpTransportBlock::from_current_command_transmissions(&[
+ transmission.clone(),
+ ])
+ .map_err(|error| error.to_string())?;
let encoded = block.encode().map_err(|error| error.to_string())?;
let decoded = RadrootsSimplexSmpTransportBlock::decode(&encoded)
.map_err(|error| error.to_string())?;
@@ -976,7 +1002,7 @@ mod tests {
.decode_command_transmissions(request.transport_version)
.map_err(|error| error.to_string())?;
assert_eq!(decoded_transmissions.len(), 1);
- assert_eq!(decoded_transmissions[0], request.transmission);
+ assert_eq!(decoded_transmissions[0], transmission);
let response_message = self
.responses
@@ -984,8 +1010,8 @@ mod tests {
.ok_or_else(|| "missing scripted transport response".to_owned())?;
let response_transmission = RadrootsSimplexSmpBrokerTransmission {
authorization: Vec::new(),
- correlation_id: request.transmission.correlation_id,
- entity_id: request.transmission.entity_id.clone(),
+ correlation_id: Some(correlation_id),
+ entity_id: request.entity_id.clone(),
message: response_message,
};
let response_block = RadrootsSimplexSmpTransportBlock::from_broker_transmissions(
diff --git a/crates/simplex-agent-store/Cargo.toml b/crates/simplex-agent-store/Cargo.toml
@@ -17,6 +17,7 @@ readme.workspace = true
default = ["std"]
std = [
"radroots-simplex-agent-proto/std",
+ "radroots-simplex-smp-crypto/std",
"radroots-simplex-smp-proto/std",
"serde/std",
"serde_json/std",
@@ -25,6 +26,7 @@ std = [
[dependencies]
radroots-simplex-agent-proto = { workspace = true, default-features = false }
+radroots-simplex-smp-crypto = { workspace = true, default-features = false }
radroots-simplex-smp-proto = { workspace = true, default-features = false }
serde = { workspace = true }
serde_json = { workspace = true }
diff --git a/crates/simplex-agent-store/src/store.rs b/crates/simplex-agent-store/src/store.rs
@@ -10,12 +10,12 @@ use radroots_simplex_agent_proto::prelude::{
RadrootsSimplexSmpRatchetState, decode_connection_link, decode_envelope,
encode_connection_link, encode_envelope,
};
+use radroots_simplex_smp_crypto::prelude::RadrootsSimplexSmpEd25519Keypair;
use radroots_simplex_smp_proto::prelude::{
RadrootsSimplexSmpQueueUri, RadrootsSimplexSmpServerAddress,
};
#[cfg(feature = "std")]
use serde::{Deserialize, Serialize};
-use sha2::{Digest, Sha256};
#[cfg(feature = "std")]
use std::fs;
#[cfg(feature = "std")]
@@ -31,9 +31,8 @@ pub enum RadrootsSimplexAgentQueueRole {
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct RadrootsSimplexAgentQueueAuthState {
- pub session_identifier: Vec<u8>,
- pub queue_key_material: Vec<u8>,
- pub server_session_key: Vec<u8>,
+ pub public_key: Vec<u8>,
+ pub private_key: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -400,8 +399,8 @@ impl RadrootsSimplexAgentStore {
descriptor: RadrootsSimplexAgentQueueDescriptor,
role: RadrootsSimplexAgentQueueRole,
primary: bool,
+ auth_state: RadrootsSimplexAgentQueueAuthState,
) -> Result<(), RadrootsSimplexAgentStoreError> {
- let derived_auth = derive_queue_auth_state(connection_id, &descriptor, role);
let connection = self.connection_mut(connection_id)?;
let address = descriptor.queue_address();
if let Some(queue) = connection
@@ -412,9 +411,7 @@ impl RadrootsSimplexAgentStore {
queue.descriptor = descriptor;
queue.role = role;
queue.primary = primary;
- if queue.auth_state.is_none() {
- queue.auth_state = Some(derived_auth);
- }
+ queue.auth_state = Some(auth_state);
return Ok(());
}
connection.queues.push(RadrootsSimplexAgentQueueRecord {
@@ -423,11 +420,25 @@ impl RadrootsSimplexAgentStore {
subscribed: false,
primary,
tested: false,
- auth_state: Some(derived_auth),
+ auth_state: Some(auth_state),
});
Ok(())
}
+ pub fn generate_queue_auth_state(
+ &self,
+ ) -> Result<RadrootsSimplexAgentQueueAuthState, RadrootsSimplexAgentStoreError> {
+ let keypair = RadrootsSimplexSmpEd25519Keypair::generate().map_err(|error| {
+ RadrootsSimplexAgentStoreError::Persistence(format!(
+ "failed to generate SimpleX queue auth keypair: {error}"
+ ))
+ })?;
+ Ok(RadrootsSimplexAgentQueueAuthState {
+ public_key: keypair.public_key,
+ private_key: keypair.private_key,
+ })
+ }
+
pub fn queue_record(
&self,
connection_id: &str,
@@ -745,69 +756,6 @@ impl RadrootsSimplexAgentStore {
}
}
-fn derive_queue_auth_state(
- connection_id: &str,
- descriptor: &RadrootsSimplexAgentQueueDescriptor,
- role: RadrootsSimplexAgentQueueRole,
-) -> RadrootsSimplexAgentQueueAuthState {
- let address = descriptor.queue_address();
- let role_label = match role {
- RadrootsSimplexAgentQueueRole::Receive => b"receive".as_slice(),
- RadrootsSimplexAgentQueueRole::Send => b"send".as_slice(),
- };
- let session_identifier = hash_material(
- b"session",
- connection_id,
- &address,
- role_label,
- descriptor.sender_key.as_deref(),
- )[..24]
- .to_vec();
- let queue_key_material = hash_material(
- b"queue-key",
- connection_id,
- &address,
- role_label,
- descriptor.sender_key.as_deref(),
- );
- let server_session_key = hash_material(
- b"server-session",
- connection_id,
- &address,
- role_label,
- descriptor.sender_key.as_deref(),
- );
- RadrootsSimplexAgentQueueAuthState {
- session_identifier,
- queue_key_material,
- server_session_key,
- }
-}
-
-fn hash_material(
- label: &[u8],
- connection_id: &str,
- address: &RadrootsSimplexAgentQueueAddress,
- role_label: &[u8],
- sender_key: Option<&[u8]>,
-) -> Vec<u8> {
- let mut hasher = Sha256::new();
- hasher.update(label);
- hasher.update(connection_id.as_bytes());
- hasher.update(address.server.server_identity.as_bytes());
- for host in &address.server.hosts {
- hasher.update(host.as_bytes());
- hasher.update([0_u8]);
- }
- hasher.update(address.server.port.unwrap_or_default().to_be_bytes());
- hasher.update(&address.sender_id);
- hasher.update(role_label);
- if let Some(sender_key) = sender_key {
- hasher.update(sender_key);
- }
- hasher.finalize().to_vec()
-}
-
#[cfg(feature = "std")]
fn connection_to_snapshot(
record: RadrootsSimplexAgentConnectionRecord,
@@ -1272,6 +1220,13 @@ mod tests {
}
}
+ fn sample_auth_state() -> RadrootsSimplexAgentQueueAuthState {
+ RadrootsSimplexAgentQueueAuthState {
+ public_key: vec![7_u8; 32],
+ private_key: vec![9_u8; 32],
+ }
+ }
+
#[test]
fn stores_connections_queues_and_retryable_commands() {
let mut store = RadrootsSimplexAgentStore::new();
@@ -1287,6 +1242,7 @@ mod tests {
sample_descriptor(true),
RadrootsSimplexAgentQueueRole::Send,
true,
+ sample_auth_state(),
)
.unwrap();
let command = store
@@ -1367,6 +1323,7 @@ mod tests {
sample_descriptor(true),
RadrootsSimplexAgentQueueRole::Send,
true,
+ sample_auth_state(),
)
.unwrap();
let prepared = store
diff --git a/crates/simplex-smp-crypto/Cargo.toml b/crates/simplex-smp-crypto/Cargo.toml
@@ -15,8 +15,15 @@ readme.workspace = true
[features]
default = ["std"]
-std = ["radroots-simplex-smp-proto/std", "sha2/std"]
+std = [
+ "ed25519-dalek/std",
+ "getrandom/std",
+ "radroots-simplex-smp-proto/std",
+ "sha2/std",
+]
[dependencies]
+ed25519-dalek = { workspace = true, default-features = false, features = ["alloc"] }
+getrandom = { workspace = true, default-features = false }
radroots-simplex-smp-proto = { workspace = true, default-features = false }
sha2 = { workspace = true, default-features = false }
diff --git a/crates/simplex-smp-crypto/src/auth.rs b/crates/simplex-smp-crypto/src/auth.rs
@@ -1,9 +1,53 @@
use crate::error::RadrootsSimplexSmpCryptoError;
use alloc::vec::Vec;
+use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use radroots_simplex_smp_proto::prelude::{
RadrootsSimplexSmpBrokerMessage, RadrootsSimplexSmpCommand, RadrootsSimplexSmpCorrelationId,
};
-use sha2::{Digest, Sha512};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpEd25519Keypair {
+ pub public_key: Vec<u8>,
+ pub private_key: Vec<u8>,
+}
+
+impl RadrootsSimplexSmpEd25519Keypair {
+ pub fn generate() -> Result<Self, RadrootsSimplexSmpCryptoError> {
+ let mut secret = [0_u8; 32];
+ getrandom::getrandom(&mut secret)
+ .map_err(|_| RadrootsSimplexSmpCryptoError::EntropyUnavailable)?;
+ let signing_key = SigningKey::from_bytes(&secret);
+ Ok(Self {
+ public_key: signing_key.verifying_key().to_bytes().to_vec(),
+ private_key: secret.to_vec(),
+ })
+ }
+
+ pub fn signing_key(&self) -> Result<SigningKey, RadrootsSimplexSmpCryptoError> {
+ let bytes: [u8; 32] = self.private_key.as_slice().try_into().map_err(|_| {
+ RadrootsSimplexSmpCryptoError::InvalidPrivateKeyLength(self.private_key.len())
+ })?;
+ Ok(SigningKey::from_bytes(&bytes))
+ }
+
+ pub fn verifying_key(&self) -> Result<VerifyingKey, RadrootsSimplexSmpCryptoError> {
+ verifying_key_from_bytes(&self.public_key)
+ }
+
+ pub fn verify(
+ &self,
+ payload: &[u8],
+ signature: &[u8],
+ ) -> Result<(), RadrootsSimplexSmpCryptoError> {
+ verify_signature(payload, &self.public_key, signature)
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RadrootsSimplexSmpCommandAuthorization {
+ None,
+ Ed25519(RadrootsSimplexSmpEd25519Keypair),
+}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadrootsSimplexSmpQueueAuthorizationScope {
@@ -61,10 +105,7 @@ impl RadrootsSimplexSmpQueueAuthorizationScope {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadrootsSimplexSmpQueueAuthorizationMaterial {
pub authorized_body: Vec<u8>,
- pub authorized_digest: [u8; 64],
- pub nonce: [u8; 24],
- pub queue_key_material: Vec<u8>,
- pub server_session_key: Vec<u8>,
+ pub authorization: Vec<u8>,
}
impl RadrootsSimplexSmpQueueAuthorizationMaterial {
@@ -72,54 +113,65 @@ impl RadrootsSimplexSmpQueueAuthorizationMaterial {
scope: &RadrootsSimplexSmpQueueAuthorizationScope,
command: &RadrootsSimplexSmpCommand,
transport_version: u16,
- queue_key_material: Vec<u8>,
- server_session_key: Vec<u8>,
+ authorization: &RadrootsSimplexSmpCommandAuthorization,
) -> Result<Self, RadrootsSimplexSmpCryptoError> {
let authorized_body = scope.authorized_command_body(command, transport_version)?;
- Ok(Self::new(
- authorized_body,
- scope.correlation_id,
- queue_key_material,
- server_session_key,
- ))
+ Self::new(authorized_body, authorization)
}
pub fn for_broker_message(
scope: &RadrootsSimplexSmpQueueAuthorizationScope,
message: &RadrootsSimplexSmpBrokerMessage,
transport_version: u16,
- queue_key_material: Vec<u8>,
- server_session_key: Vec<u8>,
+ authorization: &RadrootsSimplexSmpCommandAuthorization,
) -> Result<Self, RadrootsSimplexSmpCryptoError> {
let authorized_body = scope.authorized_broker_body(message, transport_version)?;
- Ok(Self::new(
- authorized_body,
- scope.correlation_id,
- queue_key_material,
- server_session_key,
- ))
+ Self::new(authorized_body, authorization)
}
fn new(
authorized_body: Vec<u8>,
- correlation_id: RadrootsSimplexSmpCorrelationId,
- queue_key_material: Vec<u8>,
- server_session_key: Vec<u8>,
- ) -> Self {
- let digest = Sha512::digest(&authorized_body);
- let mut authorized_digest = [0_u8; 64];
- authorized_digest.copy_from_slice(&digest);
-
- Self {
+ authorization: &RadrootsSimplexSmpCommandAuthorization,
+ ) -> Result<Self, RadrootsSimplexSmpCryptoError> {
+ let authorization = match authorization {
+ RadrootsSimplexSmpCommandAuthorization::None => Vec::new(),
+ RadrootsSimplexSmpCommandAuthorization::Ed25519(keypair) => {
+ let signing_key = keypair.signing_key()?;
+ let signature = signing_key.sign(&authorized_body);
+ signature.to_bytes().to_vec()
+ }
+ };
+ Ok(Self {
authorized_body,
- authorized_digest,
- nonce: *correlation_id.as_bytes(),
- queue_key_material,
- server_session_key,
- }
+ authorization,
+ })
}
}
+pub fn verify_signature(
+ payload: &[u8],
+ public_key: &[u8],
+ signature: &[u8],
+) -> Result<(), RadrootsSimplexSmpCryptoError> {
+ let verifying_key = verifying_key_from_bytes(public_key)?;
+ let signature: [u8; 64] = signature
+ .try_into()
+ .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidSignatureLength(signature.len()))?;
+ verifying_key
+ .verify(payload, &Signature::from_bytes(&signature))
+ .map_err(|_| RadrootsSimplexSmpCryptoError::SignatureVerificationFailed)
+}
+
+fn verifying_key_from_bytes(
+ public_key: &[u8],
+) -> Result<VerifyingKey, RadrootsSimplexSmpCryptoError> {
+ let bytes: [u8; 32] = public_key
+ .try_into()
+ .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidPublicKeyLength(public_key.len()))?;
+ VerifyingKey::from_bytes(&bytes)
+ .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidPublicKeyLength(public_key.len()))
+}
+
fn validate_short_field(value: &[u8]) -> Result<(), RadrootsSimplexSmpCryptoError> {
if value.len() > u8::MAX as usize {
return Err(RadrootsSimplexSmpCryptoError::InvalidShortFieldLength(
@@ -147,26 +199,47 @@ mod tests {
};
#[test]
- fn builds_authorization_material_for_command_scope() {
+ fn builds_ed25519_authorization_for_command_scope() {
let scope = RadrootsSimplexSmpQueueAuthorizationScope::new(
- b"tls-unique".to_vec(),
+ b"tls-session".to_vec(),
RadrootsSimplexSmpCorrelationId::new([5_u8; 24]),
b"queue-id".to_vec(),
)
.unwrap();
+ let keypair = RadrootsSimplexSmpEd25519Keypair::generate().unwrap();
+
+ let material = RadrootsSimplexSmpQueueAuthorizationMaterial::for_command(
+ &scope,
+ &RadrootsSimplexSmpCommand::Ping,
+ RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION,
+ &RadrootsSimplexSmpCommandAuthorization::Ed25519(keypair.clone()),
+ )
+ .unwrap();
+
+ assert_eq!(material.authorized_body[0], b"tls-session".len() as u8);
+ assert_eq!(material.authorization.len(), 64);
+ keypair
+ .verify(&material.authorized_body, &material.authorization)
+ .unwrap();
+ }
+
+ #[test]
+ fn leaves_unsigned_authorization_empty() {
+ let scope = RadrootsSimplexSmpQueueAuthorizationScope::new(
+ b"tls-session".to_vec(),
+ RadrootsSimplexSmpCorrelationId::new([3_u8; 24]),
+ Vec::new(),
+ )
+ .unwrap();
let material = RadrootsSimplexSmpQueueAuthorizationMaterial::for_command(
&scope,
&RadrootsSimplexSmpCommand::Ping,
RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION,
- b"queue-private".to_vec(),
- b"server-session".to_vec(),
+ &RadrootsSimplexSmpCommandAuthorization::None,
)
.unwrap();
- assert_eq!(material.nonce, [5_u8; 24]);
- assert_eq!(material.authorized_body[0], b"tls-unique".len() as u8);
- assert_eq!(material.authorized_body[11], 24);
- assert_eq!(material.authorized_digest.len(), 64);
+ assert!(material.authorization.is_empty());
}
}
diff --git a/crates/simplex-smp-crypto/src/error.rs b/crates/simplex-smp-crypto/src/error.rs
@@ -6,12 +6,16 @@ use radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpProtoError;
pub enum RadrootsSimplexSmpCryptoError {
Proto(RadrootsSimplexSmpProtoError),
InvalidShortFieldLength(usize),
+ EntropyUnavailable,
MissingRatchetKey(&'static str),
IncompletePqHeader,
RatchetMessageRegression { received: u32, current: u32 },
InvalidSharedSecretLength(usize),
InvalidCiphertextLength(usize),
InvalidPublicKeyLength(usize),
+ InvalidPrivateKeyLength(usize),
+ InvalidSignatureLength(usize),
+ SignatureVerificationFailed,
InvalidSessionIdentifier(String),
}
@@ -28,6 +32,9 @@ impl fmt::Display for RadrootsSimplexSmpCryptoError {
Self::InvalidShortFieldLength(length) => {
write!(f, "invalid SMP short field length {length}")
}
+ Self::EntropyUnavailable => {
+ write!(f, "unable to obtain entropy for SimpleX SMP key generation")
+ }
Self::MissingRatchetKey(field) => write!(f, "missing SMP ratchet key `{field}`"),
Self::IncompletePqHeader => {
write!(
@@ -50,6 +57,15 @@ impl fmt::Display for RadrootsSimplexSmpCryptoError {
Self::InvalidPublicKeyLength(length) => {
write!(f, "invalid SMP public key length {length}")
}
+ Self::InvalidPrivateKeyLength(length) => {
+ write!(f, "invalid SMP private key length {length}")
+ }
+ Self::InvalidSignatureLength(length) => {
+ write!(f, "invalid SMP signature length {length}")
+ }
+ Self::SignatureVerificationFailed => {
+ write!(f, "failed to verify SMP signature")
+ }
Self::InvalidSessionIdentifier(value) => {
write!(f, "invalid SMP session identifier `{value}`")
}
diff --git a/crates/simplex-smp-crypto/src/lib.rs b/crates/simplex-smp-crypto/src/lib.rs
@@ -9,7 +9,9 @@ pub mod ratchet;
pub mod prelude {
pub use crate::auth::{
+ RadrootsSimplexSmpCommandAuthorization, RadrootsSimplexSmpEd25519Keypair,
RadrootsSimplexSmpQueueAuthorizationMaterial, RadrootsSimplexSmpQueueAuthorizationScope,
+ verify_signature,
};
pub use crate::error::RadrootsSimplexSmpCryptoError;
pub use crate::ratchet::{
diff --git a/crates/simplex-smp-transport/Cargo.toml b/crates/simplex-smp-transport/Cargo.toml
@@ -15,7 +15,22 @@ readme.workspace = true
[features]
default = ["std"]
-std = ["radroots-simplex-smp-proto/std"]
+std = [
+ "base64/std",
+ "radroots-simplex-smp-crypto/std",
+ "radroots-simplex-smp-proto/std",
+ "rustls/std",
+ "sha2/std",
+ "x509-parser/verify",
+]
[dependencies]
+base64 = { workspace = true }
+radroots-simplex-smp-crypto = { workspace = true, default-features = false }
radroots-simplex-smp-proto = { workspace = true, default-features = false }
+rustls = { workspace = true, default-features = false }
+sha2 = { workspace = true, default-features = false }
+x509-parser = { workspace = true, default-features = false }
+
+[dev-dependencies]
+rcgen = { version = "0.14" }
diff --git a/crates/simplex-smp-transport/src/client.rs b/crates/simplex-smp-transport/src/client.rs
@@ -0,0 +1,373 @@
+#![cfg(feature = "std")]
+
+use crate::error::RadrootsSimplexSmpTransportError;
+use crate::executor::{
+ RadrootsSimplexSmpCommandTransport, RadrootsSimplexSmpTransportRequest,
+ RadrootsSimplexSmpTransportResponse,
+};
+use crate::frame::{RADROOTS_SIMPLEX_SMP_TRANSPORT_BLOCK_SIZE, RadrootsSimplexSmpTransportBlock};
+use crate::handshake::{
+ RADROOTS_SIMPLEX_SMP_TLS_ALPN_V1, RadrootsSimplexSmpClientHello, RadrootsSimplexSmpServerHello,
+ RadrootsSimplexSmpTlsHandshakeEvidence, RadrootsSimplexSmpTlsPolicy, validate_tls_handshake,
+};
+use base64::Engine as _;
+use base64::engine::general_purpose::URL_SAFE_NO_PAD;
+use radroots_simplex_smp_crypto::prelude::{
+ RadrootsSimplexSmpQueueAuthorizationMaterial, RadrootsSimplexSmpQueueAuthorizationScope,
+};
+use radroots_simplex_smp_proto::prelude::{
+ RadrootsSimplexSmpCommandTransmission, RadrootsSimplexSmpServerAddress,
+};
+use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
+use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
+use rustls::{
+ ClientConfig, ClientConnection, DigitallySignedStruct, Error as RustlsError, SignatureScheme,
+ StreamOwned,
+};
+use sha2::{Digest, Sha256};
+use std::collections::BTreeMap;
+use std::io::{Read, Write};
+use std::net::{IpAddr, TcpStream, ToSocketAddrs};
+use std::sync::Arc;
+use std::time::Duration;
+use x509_parser::prelude::FromDer;
+
+#[derive(Default)]
+pub struct RadrootsSimplexSmpTlsCommandTransport {
+ sessions: BTreeMap<String, RadrootsSimplexSmpLiveSession>,
+}
+
+struct RadrootsSimplexSmpLiveSession {
+ stream: StreamOwned<ClientConnection, TcpStream>,
+ transport_version: u16,
+ session_identifier: Vec<u8>,
+}
+
+impl RadrootsSimplexSmpTlsCommandTransport {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ fn session_key(server: &RadrootsSimplexSmpServerAddress) -> String {
+ let mut key = server.server_identity.clone();
+ key.push('@');
+ key.push_str(&server.hosts.join(","));
+ key.push(':');
+ key.push_str(&server.port.unwrap_or(5223).to_string());
+ key
+ }
+
+ fn session_for(
+ &mut self,
+ server: &RadrootsSimplexSmpServerAddress,
+ ) -> Result<&mut RadrootsSimplexSmpLiveSession, RadrootsSimplexSmpTransportError> {
+ let key = Self::session_key(server);
+ if !self.sessions.contains_key(&key) {
+ let session = connect_live_session(server)?;
+ self.sessions.insert(key.clone(), session);
+ }
+ self.sessions.get_mut(&key).ok_or_else(|| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "missing live SMP session for `{}`",
+ server.server_identity
+ ))
+ })
+ }
+}
+
+impl RadrootsSimplexSmpCommandTransport for RadrootsSimplexSmpTlsCommandTransport {
+ type Error = RadrootsSimplexSmpTransportError;
+
+ fn execute(
+ &mut self,
+ request: RadrootsSimplexSmpTransportRequest,
+ ) -> Result<RadrootsSimplexSmpTransportResponse, Self::Error> {
+ let key = Self::session_key(&request.server);
+ match execute_live_request(self.session_for(&request.server)?, &request) {
+ Ok(response) => Ok(response),
+ Err(RadrootsSimplexSmpTransportError::LiveTransportIo(error)) => {
+ self.sessions.remove(&key);
+ let response = execute_live_request(self.session_for(&request.server)?, &request);
+ match response {
+ Ok(response) => Ok(response),
+ Err(RadrootsSimplexSmpTransportError::LiveTransportIo(_)) => {
+ Err(RadrootsSimplexSmpTransportError::LiveTransportIo(error))
+ }
+ Err(error) => Err(error),
+ }
+ }
+ Err(error) => Err(error),
+ }
+ }
+}
+
+fn execute_live_request(
+ session: &mut RadrootsSimplexSmpLiveSession,
+ request: &RadrootsSimplexSmpTransportRequest,
+) -> Result<RadrootsSimplexSmpTransportResponse, RadrootsSimplexSmpTransportError> {
+ let correlation_id = request
+ .correlation_id
+ .ok_or(RadrootsSimplexSmpTransportError::MissingCorrelationId)?;
+ let scope = RadrootsSimplexSmpQueueAuthorizationScope::new(
+ session.session_identifier.clone(),
+ correlation_id,
+ request.entity_id.clone(),
+ )?;
+ let material = RadrootsSimplexSmpQueueAuthorizationMaterial::for_command(
+ &scope,
+ &request.command,
+ session.transport_version,
+ &request.authorization,
+ )?;
+ let transmission = RadrootsSimplexSmpCommandTransmission {
+ authorization: material.authorization,
+ correlation_id: Some(correlation_id),
+ entity_id: request.entity_id.clone(),
+ command: request.command.clone(),
+ };
+ let block = RadrootsSimplexSmpTransportBlock::from_command_transmissions(
+ &[transmission],
+ session.transport_version,
+ )?;
+ let encoded = block.encode()?;
+ session
+ .stream
+ .write_all(&encoded)
+ .map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
+ session
+ .stream
+ .flush()
+ .map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
+
+ let mut response_block = vec![0_u8; RADROOTS_SIMPLEX_SMP_TRANSPORT_BLOCK_SIZE];
+ session
+ .stream
+ .read_exact(&mut response_block)
+ .map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
+ let response_hash = Sha256::digest(&response_block).to_vec();
+ let decoded = RadrootsSimplexSmpTransportBlock::decode(&response_block)?;
+ let transmissions = decoded.decode_broker_transmissions(session.transport_version)?;
+ if transmissions.len() != 1 {
+ return Err(
+ RadrootsSimplexSmpTransportError::UnexpectedBrokerTransmissionCount(
+ transmissions.len(),
+ ),
+ );
+ }
+ let transmission = transmissions.into_iter().next().expect("checked len");
+ if transmission.correlation_id != Some(correlation_id) {
+ return Err(RadrootsSimplexSmpTransportError::CorrelationIdMismatch);
+ }
+ Ok(RadrootsSimplexSmpTransportResponse {
+ server: request.server.clone(),
+ transport_version: session.transport_version,
+ transmission,
+ transport_hash: response_hash,
+ })
+}
+
+fn connect_live_session(
+ server: &RadrootsSimplexSmpServerAddress,
+) -> Result<RadrootsSimplexSmpLiveSession, RadrootsSimplexSmpTransportError> {
+ let mut last_error = None;
+ for host in &server.hosts {
+ match connect_live_session_host(server, host) {
+ Ok(session) => return Ok(session),
+ Err(error) => last_error = Some(error),
+ }
+ }
+
+ Err(last_error.unwrap_or_else(|| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "SMP server `{}` has no usable hosts",
+ server.server_identity
+ ))
+ }))
+}
+
+fn connect_live_session_host(
+ server: &RadrootsSimplexSmpServerAddress,
+ host: &str,
+) -> Result<RadrootsSimplexSmpLiveSession, RadrootsSimplexSmpTransportError> {
+ let port = server.port.unwrap_or(5223);
+ let mut addresses = (host, port).to_socket_addrs().map_err(|error| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "failed to resolve SMP server host `{host}:{port}`: {error}"
+ ))
+ })?;
+ let socket_addr = addresses.next().ok_or_else(|| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "failed to resolve SMP server host `{host}:{port}`"
+ ))
+ })?;
+ let tcp =
+ TcpStream::connect_timeout(&socket_addr, Duration::from_secs(5)).map_err(|error| {
+ RadrootsSimplexSmpTransportError::LiveTransportIo(format!(
+ "failed to connect to SMP server `{host}:{port}`: {error}"
+ ))
+ })?;
+ tcp.set_nodelay(true)
+ .map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
+ tcp.set_read_timeout(Some(Duration::from_secs(5)))
+ .map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
+ tcp.set_write_timeout(Some(Duration::from_secs(5)))
+ .map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
+
+ let server_name = match host.parse::<IpAddr>() {
+ Ok(address) => ServerName::IpAddress(address.into()),
+ Err(_) => ServerName::try_from(host.to_owned()).map_err(|_| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "invalid SMP server name `{host}`"
+ ))
+ })?,
+ };
+ let verifier = Arc::new(PermissiveSimplexServerVerifier);
+ let mut config = ClientConfig::builder()
+ .dangerous()
+ .with_custom_certificate_verifier(verifier)
+ .with_no_client_auth();
+ config.alpn_protocols = vec![RADROOTS_SIMPLEX_SMP_TLS_ALPN_V1.as_bytes().to_vec()];
+
+ let mut stream = StreamOwned::new(
+ ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
+ RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string())
+ })?,
+ tcp,
+ );
+ while stream.conn.is_handshaking() {
+ stream.conn.complete_io(&mut stream.sock).map_err(|error| {
+ RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string())
+ })?;
+ }
+
+ let peer_certs = stream
+ .conn
+ .peer_certificates()
+ .ok_or(RadrootsSimplexSmpTransportError::MissingPeerCertificates)?
+ .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());
+ policy.require_tls_unique_binding = false;
+ let transport_version = validate_tls_handshake(
+ &policy,
+ &server_hello,
+ &RadrootsSimplexSmpTlsHandshakeEvidence {
+ confirmed_alpn: stream
+ .conn
+ .alpn_protocol()
+ .map(|value| String::from_utf8_lossy(value).into_owned()),
+ session_resumed: false,
+ certificate_chain_length: peer_certs.len(),
+ online_certificate_fingerprint: actual_identity,
+ tls_unique_channel_binding: None,
+ },
+ )?;
+ let client_hello = RadrootsSimplexSmpClientHello {
+ chosen_version: transport_version,
+ client_key: None,
+ ignored_part: Vec::new(),
+ };
+ let encoded_client_hello = client_hello.encode()?;
+ stream
+ .write_all(&encoded_client_hello)
+ .map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
+ stream
+ .flush()
+ .map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
+
+ Ok(RadrootsSimplexSmpLiveSession {
+ stream,
+ transport_version,
+ session_identifier: server_hello.session_identifier,
+ })
+}
+
+fn read_server_hello(
+ stream: &mut StreamOwned<ClientConnection, TcpStream>,
+) -> Result<RadrootsSimplexSmpServerHello, RadrootsSimplexSmpTransportError> {
+ let mut block = vec![0_u8; RADROOTS_SIMPLEX_SMP_TRANSPORT_BLOCK_SIZE];
+ stream
+ .read_exact(&mut block)
+ .map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
+ RadrootsSimplexSmpServerHello::decode(&block)
+}
+
+fn matching_server_identity(
+ chain: &[CertificateDer<'static>],
+ expected_identity: &str,
+) -> Result<String, RadrootsSimplexSmpTransportError> {
+ for certificate in chain {
+ let identity = server_identity_from_certificate(certificate.as_ref())?;
+ if identity == expected_identity {
+ return Ok(identity);
+ }
+ }
+ Err(RadrootsSimplexSmpTransportError::ServerIdentityMismatch {
+ expected: expected_identity.into(),
+ actual: chain
+ .first()
+ .map(|certificate| server_identity_from_certificate(certificate.as_ref()))
+ .transpose()?
+ .unwrap_or_default(),
+ })
+}
+
+fn server_identity_from_certificate(
+ der: &[u8],
+) -> Result<String, RadrootsSimplexSmpTransportError> {
+ let (_, certificate) =
+ x509_parser::certificate::X509Certificate::from_der(der).map_err(|error| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "failed to parse SMP certificate: {error}"
+ ))
+ })?;
+ let digest = Sha256::digest(certificate.tbs_certificate.subject_pki.raw);
+ Ok(URL_SAFE_NO_PAD.encode(digest))
+}
+
+#[derive(Debug)]
+struct PermissiveSimplexServerVerifier;
+
+impl ServerCertVerifier for PermissiveSimplexServerVerifier {
+ fn verify_server_cert(
+ &self,
+ _end_entity: &CertificateDer<'_>,
+ _intermediates: &[CertificateDer<'_>],
+ _server_name: &ServerName<'_>,
+ _ocsp_response: &[u8],
+ _now: UnixTime,
+ ) -> Result<ServerCertVerified, RustlsError> {
+ Ok(ServerCertVerified::assertion())
+ }
+
+ fn verify_tls12_signature(
+ &self,
+ _message: &[u8],
+ _cert: &CertificateDer<'_>,
+ _dss: &DigitallySignedStruct,
+ ) -> Result<HandshakeSignatureValid, RustlsError> {
+ Ok(HandshakeSignatureValid::assertion())
+ }
+
+ fn verify_tls13_signature(
+ &self,
+ _message: &[u8],
+ _cert: &CertificateDer<'_>,
+ _dss: &DigitallySignedStruct,
+ ) -> Result<HandshakeSignatureValid, RustlsError> {
+ Ok(HandshakeSignatureValid::assertion())
+ }
+
+ fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
+ vec![
+ SignatureScheme::ED25519,
+ SignatureScheme::ECDSA_NISTP256_SHA256,
+ SignatureScheme::ECDSA_NISTP384_SHA384,
+ SignatureScheme::RSA_PSS_SHA256,
+ SignatureScheme::RSA_PSS_SHA384,
+ SignatureScheme::RSA_PKCS1_SHA256,
+ SignatureScheme::RSA_PKCS1_SHA384,
+ ]
+ }
+}
diff --git a/crates/simplex-smp-transport/src/error.rs b/crates/simplex-smp-transport/src/error.rs
@@ -1,10 +1,12 @@
use alloc::string::String;
use core::fmt;
+use radroots_simplex_smp_crypto::prelude::RadrootsSimplexSmpCryptoError;
use radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpProtoError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RadrootsSimplexSmpTransportError {
Proto(RadrootsSimplexSmpProtoError),
+ Crypto(RadrootsSimplexSmpCryptoError),
InvalidPaddedBlockLength { expected: usize, actual: usize },
TransportPayloadTooLarge(usize),
EmptyTransportBlock,
@@ -23,6 +25,12 @@ pub enum RadrootsSimplexSmpTransportError {
MissingChannelBinding,
SessionBindingMismatch,
NoMutualTransportVersion { offered: String, supported: String },
+ MissingCorrelationId,
+ InvalidServerAddress(String),
+ LiveTransportIo(String),
+ MissingPeerCertificates,
+ UnexpectedBrokerTransmissionCount(usize),
+ CorrelationIdMismatch,
}
impl From<RadrootsSimplexSmpProtoError> for RadrootsSimplexSmpTransportError {
@@ -31,10 +39,17 @@ impl From<RadrootsSimplexSmpProtoError> for RadrootsSimplexSmpTransportError {
}
}
+impl From<RadrootsSimplexSmpCryptoError> for RadrootsSimplexSmpTransportError {
+ fn from(value: RadrootsSimplexSmpCryptoError) -> Self {
+ Self::Crypto(value)
+ }
+}
+
impl fmt::Display for RadrootsSimplexSmpTransportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Proto(error) => write!(f, "{error}"),
+ Self::Crypto(error) => write!(f, "{error}"),
Self::InvalidPaddedBlockLength { expected, actual } => {
write!(
f,
@@ -99,6 +114,26 @@ impl fmt::Display for RadrootsSimplexSmpTransportError {
"no mutual SMP transport version between `{offered}` and `{supported}`"
)
}
+ Self::MissingCorrelationId => {
+ write!(f, "SMP transport request is missing a correlation id")
+ }
+ Self::InvalidServerAddress(message) => write!(f, "{message}"),
+ Self::LiveTransportIo(message) => write!(f, "{message}"),
+ Self::MissingPeerCertificates => {
+ write!(f, "SMP TLS peer certificate chain is missing")
+ }
+ Self::UnexpectedBrokerTransmissionCount(count) => {
+ write!(
+ f,
+ "expected exactly one SMP broker transmission, got {count}"
+ )
+ }
+ Self::CorrelationIdMismatch => {
+ write!(
+ f,
+ "SMP broker response correlation id did not match the request"
+ )
+ }
}
}
}
diff --git a/crates/simplex-smp-transport/src/executor.rs b/crates/simplex-smp-transport/src/executor.rs
@@ -1,14 +1,18 @@
use alloc::vec::Vec;
+use radroots_simplex_smp_crypto::prelude::RadrootsSimplexSmpCommandAuthorization;
use radroots_simplex_smp_proto::prelude::{
- RadrootsSimplexSmpBrokerTransmission, RadrootsSimplexSmpCommandTransmission,
- RadrootsSimplexSmpServerAddress,
+ RadrootsSimplexSmpBrokerTransmission, RadrootsSimplexSmpCommand,
+ RadrootsSimplexSmpCorrelationId, RadrootsSimplexSmpServerAddress,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadrootsSimplexSmpTransportRequest {
pub server: RadrootsSimplexSmpServerAddress,
pub transport_version: u16,
- pub transmission: RadrootsSimplexSmpCommandTransmission,
+ pub correlation_id: Option<RadrootsSimplexSmpCorrelationId>,
+ pub entity_id: Vec<u8>,
+ pub command: RadrootsSimplexSmpCommand,
+ pub authorization: RadrootsSimplexSmpCommandAuthorization,
}
#[derive(Debug, Clone, PartialEq, Eq)]
diff --git a/crates/simplex-smp-transport/src/lib.rs b/crates/simplex-smp-transport/src/lib.rs
@@ -3,12 +3,16 @@
extern crate alloc;
+#[cfg(feature = "std")]
+pub mod client;
pub mod error;
pub mod executor;
pub mod frame;
pub mod handshake;
pub mod prelude {
+ #[cfg(feature = "std")]
+ pub use crate::client::RadrootsSimplexSmpTlsCommandTransport;
pub use crate::error::RadrootsSimplexSmpTransportError;
pub use crate::executor::{
RadrootsSimplexSmpCommandTransport, RadrootsSimplexSmpTransportRequest,