commit 69f0e42ceb59bb03b18aaf7b944586b85a214d23
parent 84e28c29959f44d3d9ce2d7a7c730f626ab417a1
Author: triesap <tyson@radroots.org>
Date: Sun, 29 Mar 2026 01:21:42 +0000
simplex: advance encrypted transport block interop
Diffstat:
12 files changed, 866 insertions(+), 54 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1193,6 +1193,15 @@ dependencies = [
]
[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2653,6 +2662,7 @@ version = "0.1.0-alpha.1"
dependencies = [
"ed25519-dalek",
"getrandom 0.2.17",
+ "hkdf",
"radroots-simplex-smp-proto",
"sha2",
"x25519-dalek",
diff --git a/Cargo.toml b/Cargo.toml
@@ -96,6 +96,7 @@ directories = { version = "6" }
ed25519-dalek = { version = "2.1.1", default-features = false }
futures = { version = "0.3" }
getrandom = { version = "0.2", default-features = false }
+hkdf = { version = "0.12", default-features = false }
hex = { version = "0.4" }
js-sys = { version = "0.3" }
keyring = { version = "3.6.3", default-features = false, features = [
diff --git a/crates/simplex-agent-runtime/src/runtime.rs b/crates/simplex-agent-runtime/src/runtime.rs
@@ -22,8 +22,9 @@ use radroots_simplex_agent_store::prelude::{
};
use radroots_simplex_smp_crypto::prelude::{
RADROOTS_SIMPLEX_SMP_NONCE_LENGTH, RadrootsSimplexSmpCommandAuthorization,
- RadrootsSimplexSmpRatchetState, RadrootsSimplexSmpX25519Keypair, decrypt_padded,
- derive_shared_secret, encrypt_padded, random_nonce,
+ RadrootsSimplexSmpRatchetState, RadrootsSimplexSmpX25519Keypair, decode_x25519_public_key_x509,
+ decrypt_padded, derive_shared_secret, encode_ed25519_public_key_x509,
+ encode_x25519_public_key_x509, encrypt_padded, random_nonce,
};
use radroots_simplex_smp_proto::prelude::{
RADROOTS_SIMPLEX_SMP_CURRENT_CLIENT_VERSION, RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION,
@@ -782,7 +783,7 @@ impl RadrootsSimplexAgentRuntime {
&self,
command: &RadrootsSimplexAgentPendingCommand,
) -> Result<RadrootsSimplexSmpTransportRequest, RadrootsSimplexAgentRuntimeError> {
- let (queue_address, _entity_id, smp_command) = self.command_transport_parts(command)?;
+ let (queue_address, entity_id, smp_command) = self.command_transport_parts(command)?;
let queue = self
.store
.queue_record(&command.connection_id, &queue_address)?;
@@ -815,7 +816,7 @@ impl RadrootsSimplexAgentRuntime {
server: queue.descriptor.queue_uri.server.clone(),
transport_version: RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION,
correlation_id: Some(correlation_id),
- entity_id: queue.entity_id,
+ entity_id,
command: smp_command,
authorization,
})
@@ -850,14 +851,23 @@ impl RadrootsSimplexAgentRuntime {
descriptor.queue_address(),
Vec::new(),
RadrootsSimplexSmpCommand::New(RadrootsSimplexSmpNewQueueRequest {
- recipient_auth_public_key: auth_state.public_key,
- recipient_dh_public_key:
- RadrootsSimplexSmpX25519Keypair::public_key_from_private(
+ recipient_auth_public_key: encode_ed25519_public_key_x509(
+ &auth_state.public_key,
+ )
+ .map_err(|error| {
+ RadrootsSimplexAgentRuntimeError::Runtime(error.to_string())
+ })?,
+ recipient_dh_public_key: encode_x25519_public_key_x509(
+ &RadrootsSimplexSmpX25519Keypair::public_key_from_private(
&delivery_private_key,
)
.map_err(|error| {
RadrootsSimplexAgentRuntimeError::Runtime(error.to_string())
})?,
+ )
+ .map_err(|error| {
+ RadrootsSimplexAgentRuntimeError::Runtime(error.to_string())
+ })?,
basic_auth: None,
subscription_mode: RadrootsSimplexSmpSubscriptionMode::Subscribe,
queue_request_data: Some(
@@ -881,7 +891,12 @@ impl RadrootsSimplexAgentRuntime {
RadrootsSimplexAgentPendingCommandKind::SecureQueue { queue, sender_key } => Ok((
queue.clone(),
queue.sender_id.clone(),
- RadrootsSimplexSmpCommand::SKey(sender_key.clone().unwrap_or_default()),
+ RadrootsSimplexSmpCommand::SKey(
+ encode_ed25519_public_key_x509(sender_key.as_deref().unwrap_or_default())
+ .map_err(|error| {
+ RadrootsSimplexAgentRuntimeError::Runtime(error.to_string())
+ })?,
+ ),
)),
RadrootsSimplexAgentPendingCommandKind::SendEnvelope {
queue, envelope, ..
@@ -1095,8 +1110,10 @@ impl RadrootsSimplexAgentRuntime {
"SimpleX receive queue missing delivery private key".into(),
)
})?;
+ let server_dh_public_key = decode_x25519_public_key_x509(&ids.server_dh_public_key)
+ .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?;
queue.delivery_shared_secret = Some(
- derive_shared_secret(&delivery_private_key, &ids.server_dh_public_key).map_err(
+ derive_shared_secret(&delivery_private_key, &server_dh_public_key).map_err(
|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()),
)?,
);
diff --git a/crates/simplex-smp-crypto/Cargo.toml b/crates/simplex-smp-crypto/Cargo.toml
@@ -26,6 +26,7 @@ std = [
[dependencies]
ed25519-dalek = { workspace = true, default-features = false, features = ["alloc"] }
getrandom = { workspace = true, default-features = false }
+hkdf = { workspace = true, default-features = false }
radroots-simplex-smp-proto = { workspace = true, default-features = false }
sha2 = { workspace = true, default-features = false }
xsalsa20poly1305 = { workspace = true, default-features = false }
diff --git a/crates/simplex-smp-crypto/src/auth.rs b/crates/simplex-smp-crypto/src/auth.rs
@@ -4,6 +4,17 @@ use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use radroots_simplex_smp_proto::prelude::{
RadrootsSimplexSmpBrokerMessage, RadrootsSimplexSmpCommand, RadrootsSimplexSmpCorrelationId,
};
+use x25519_dalek::PublicKey as X25519PublicKey;
+
+const ED25519_SPKI_DER_PREFIX: &[u8] = &[
+ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
+];
+const X25519_SPKI_DER_PREFIX: &[u8] = &[
+ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00,
+];
+const X25519_SPKI_DER_PREFIX_WRAPPED: &[u8] = &[
+ 0x30, 0x2c, 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00,
+];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadrootsSimplexSmpEd25519Keypair {
@@ -148,6 +159,49 @@ impl RadrootsSimplexSmpQueueAuthorizationMaterial {
}
}
+pub fn encode_ed25519_public_key_x509(
+ public_key: &[u8],
+) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> {
+ let _: [u8; 32] = public_key
+ .try_into()
+ .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidPublicKeyLength(public_key.len()))?;
+ let mut encoded = Vec::with_capacity(ED25519_SPKI_DER_PREFIX.len() + public_key.len());
+ encoded.extend_from_slice(ED25519_SPKI_DER_PREFIX);
+ encoded.extend_from_slice(public_key);
+ Ok(encoded)
+}
+
+pub fn encode_x25519_public_key_x509(
+ public_key: &[u8],
+) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> {
+ let _: [u8; 32] = public_key
+ .try_into()
+ .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidPublicKeyLength(public_key.len()))?;
+ let mut encoded = Vec::with_capacity(X25519_SPKI_DER_PREFIX.len() + public_key.len());
+ encoded.extend_from_slice(X25519_SPKI_DER_PREFIX);
+ encoded.extend_from_slice(public_key);
+ Ok(encoded)
+}
+
+pub fn decode_x25519_public_key_x509(
+ encoded: &[u8],
+) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> {
+ if encoded.len() == 32 {
+ let key: [u8; 32] = encoded
+ .try_into()
+ .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidPublicKeyLength(encoded.len()))?;
+ return Ok(X25519PublicKey::from(key).as_bytes().to_vec());
+ }
+ let raw = encoded
+ .strip_prefix(X25519_SPKI_DER_PREFIX)
+ .or_else(|| encoded.strip_prefix(X25519_SPKI_DER_PREFIX_WRAPPED))
+ .ok_or_else(|| RadrootsSimplexSmpCryptoError::InvalidPublicKeyLength(encoded.len()))?;
+ let key: [u8; 32] = raw
+ .try_into()
+ .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidPublicKeyLength(encoded.len()))?;
+ Ok(X25519PublicKey::from(key).as_bytes().to_vec())
+}
+
pub fn verify_signature(
payload: &[u8],
public_key: &[u8],
@@ -242,4 +296,34 @@ mod tests {
assert!(material.authorization.is_empty());
}
+
+ #[test]
+ fn ed25519_public_key_x509_roundtrips_shape() {
+ let keypair = RadrootsSimplexSmpEd25519Keypair::generate().unwrap();
+ let encoded = encode_ed25519_public_key_x509(&keypair.public_key).unwrap();
+
+ assert_eq!(
+ &encoded[..ED25519_SPKI_DER_PREFIX.len()],
+ ED25519_SPKI_DER_PREFIX
+ );
+ assert_eq!(
+ &encoded[ED25519_SPKI_DER_PREFIX.len()..],
+ keypair.public_key
+ );
+ }
+
+ #[test]
+ fn x25519_public_key_x509_roundtrips_and_accepts_raw() {
+ let keypair = crate::message::RadrootsSimplexSmpX25519Keypair::generate().unwrap();
+ let encoded = encode_x25519_public_key_x509(&keypair.public_key).unwrap();
+
+ assert_eq!(
+ decode_x25519_public_key_x509(&encoded).unwrap(),
+ keypair.public_key
+ );
+ assert_eq!(
+ decode_x25519_public_key_x509(&keypair.public_key).unwrap(),
+ keypair.public_key
+ );
+ }
}
diff --git a/crates/simplex-smp-crypto/src/error.rs b/crates/simplex-smp-crypto/src/error.rs
@@ -19,6 +19,8 @@ pub enum RadrootsSimplexSmpCryptoError {
InvalidSignatureLength(usize),
SignatureVerificationFailed,
InvalidSessionIdentifier(String),
+ InvalidKeyDerivationLength(usize),
+ InvalidSecretBoxChainKeyLength(usize),
}
impl From<RadrootsSimplexSmpProtoError> for RadrootsSimplexSmpCryptoError {
@@ -80,6 +82,12 @@ impl fmt::Display for RadrootsSimplexSmpCryptoError {
Self::InvalidSessionIdentifier(value) => {
write!(f, "invalid SMP session identifier `{value}`")
}
+ Self::InvalidKeyDerivationLength(length) => {
+ write!(f, "invalid SMP key derivation length {length}")
+ }
+ Self::InvalidSecretBoxChainKeyLength(length) => {
+ write!(f, "invalid SMP secretbox chain key length {length}")
+ }
}
}
}
diff --git a/crates/simplex-smp-crypto/src/lib.rs b/crates/simplex-smp-crypto/src/lib.rs
@@ -12,13 +12,15 @@ pub mod prelude {
pub use crate::auth::{
RadrootsSimplexSmpCommandAuthorization, RadrootsSimplexSmpEd25519Keypair,
RadrootsSimplexSmpQueueAuthorizationMaterial, RadrootsSimplexSmpQueueAuthorizationScope,
- verify_signature,
+ decode_x25519_public_key_x509, encode_ed25519_public_key_x509,
+ encode_x25519_public_key_x509, verify_signature,
};
pub use crate::error::RadrootsSimplexSmpCryptoError;
pub use crate::message::{
RADROOTS_SIMPLEX_SMP_NONCE_LENGTH, RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH,
- RadrootsSimplexSmpX25519Keypair, decrypt_no_pad, decrypt_padded, derive_shared_secret,
- encrypt_no_pad, encrypt_padded, random_nonce,
+ RadrootsSimplexSmpSecretBoxChainKey, RadrootsSimplexSmpX25519Keypair,
+ advance_secretbox_chain, decrypt_no_pad, decrypt_padded, derive_shared_secret,
+ encrypt_no_pad, encrypt_padded, init_secretbox_chain, random_nonce,
};
pub use crate::ratchet::{
RadrootsSimplexSmpRatchetHeader, RadrootsSimplexSmpRatchetRole,
diff --git a/crates/simplex-smp-crypto/src/message.rs b/crates/simplex-smp-crypto/src/message.rs
@@ -1,7 +1,8 @@
use crate::error::RadrootsSimplexSmpCryptoError;
use alloc::vec::Vec;
use getrandom::getrandom;
-use sha2::{Digest, Sha256};
+use hkdf::Hkdf;
+use sha2::{Digest, Sha256, Sha512};
use x25519_dalek::{PublicKey, StaticSecret};
use xsalsa20poly1305::aead::{AeadInPlace, KeyInit};
use xsalsa20poly1305::{Tag, XSalsa20Poly1305};
@@ -9,6 +10,11 @@ use xsalsa20poly1305::{Tag, XSalsa20Poly1305};
pub const RADROOTS_SIMPLEX_SMP_NONCE_LENGTH: usize = 24;
pub const RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH: usize = 32;
const RADROOTS_SIMPLEX_SMP_AUTH_TAG_LENGTH: usize = 16;
+const RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_INIT_INFO: &[u8] = b"SimpleXSbChainInit";
+const RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_INFO: &[u8] = b"SimpleXSbChain";
+const RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_KEY_LENGTH: usize = 32;
+const RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_INIT_OUTPUT_LENGTH: usize = 64;
+const RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_STEP_OUTPUT_LENGTH: usize = 88;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadrootsSimplexSmpX25519Keypair {
@@ -52,6 +58,74 @@ impl RadrootsSimplexSmpX25519Keypair {
}
}
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexSmpSecretBoxChainKey {
+ bytes: [u8; RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_KEY_LENGTH],
+}
+
+impl RadrootsSimplexSmpSecretBoxChainKey {
+ fn from_slice(value: &[u8]) -> Result<Self, RadrootsSimplexSmpCryptoError> {
+ let bytes: [u8; RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_KEY_LENGTH] =
+ value.try_into().map_err(|_| {
+ RadrootsSimplexSmpCryptoError::InvalidSecretBoxChainKeyLength(value.len())
+ })?;
+ Ok(Self { bytes })
+ }
+}
+
+pub fn init_secretbox_chain(
+ session_identifier: &[u8],
+ shared_secret: &[u8],
+) -> Result<
+ (
+ RadrootsSimplexSmpSecretBoxChainKey,
+ RadrootsSimplexSmpSecretBoxChainKey,
+ ),
+ RadrootsSimplexSmpCryptoError,
+> {
+ let output = hkdf_expand(
+ session_identifier,
+ shared_secret,
+ RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_INIT_INFO,
+ RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_INIT_OUTPUT_LENGTH,
+ )?;
+ let (first, second) = output.split_at(RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_KEY_LENGTH);
+ Ok((
+ RadrootsSimplexSmpSecretBoxChainKey::from_slice(first)?,
+ RadrootsSimplexSmpSecretBoxChainKey::from_slice(second)?,
+ ))
+}
+
+pub fn advance_secretbox_chain(
+ chain_key: &RadrootsSimplexSmpSecretBoxChainKey,
+) -> Result<
+ (
+ (Vec<u8>, [u8; RADROOTS_SIMPLEX_SMP_NONCE_LENGTH]),
+ RadrootsSimplexSmpSecretBoxChainKey,
+ ),
+ RadrootsSimplexSmpCryptoError,
+> {
+ let output = hkdf_expand(
+ b"",
+ &chain_key.bytes,
+ RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_INFO,
+ RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_STEP_OUTPUT_LENGTH,
+ )?;
+ let (next_chain_key, remainder) =
+ output.split_at(RADROOTS_SIMPLEX_SMP_SECRETBOX_CHAIN_KEY_LENGTH);
+ let (secretbox_key, nonce_bytes) =
+ remainder.split_at(RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH);
+ Ok((
+ (
+ secretbox_key.to_vec(),
+ nonce_bytes.try_into().map_err(|_| {
+ RadrootsSimplexSmpCryptoError::InvalidNonceLength(nonce_bytes.len())
+ })?,
+ ),
+ RadrootsSimplexSmpSecretBoxChainKey::from_slice(next_chain_key)?,
+ ))
+}
+
pub fn derive_shared_secret(
private_key: &[u8],
public_key: &[u8],
@@ -169,6 +243,19 @@ fn nonce_array(
.map_err(|_| RadrootsSimplexSmpCryptoError::InvalidNonceLength(nonce.len()))
}
+fn hkdf_expand(
+ salt: &[u8],
+ ikm: &[u8],
+ info: &[u8],
+ output_len: usize,
+) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> {
+ let hkdf = Hkdf::<Sha512>::new(Some(salt), ikm);
+ let mut output = vec![0_u8; output_len];
+ hkdf.expand(info, &mut output)
+ .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidKeyDerivationLength(output_len))?;
+ Ok(output)
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -193,4 +280,21 @@ mod tests {
let plaintext = decrypt_padded(&bob_secret, &nonce, &ciphertext).unwrap();
assert_eq!(plaintext, b"hello");
}
+
+ #[test]
+ fn derives_repeatable_secretbox_chain_progression() {
+ let (rcv_first, snd_first) = init_secretbox_chain(b"session", b"shared-secret").unwrap();
+ let (rcv_second, snd_second) = init_secretbox_chain(b"session", b"shared-secret").unwrap();
+ assert_eq!(rcv_first, rcv_second);
+ assert_eq!(snd_first, snd_second);
+
+ let ((send_key, send_nonce), next_send) = advance_secretbox_chain(&snd_first).unwrap();
+ let ((recv_key, recv_nonce), next_recv) = advance_secretbox_chain(&rcv_first).unwrap();
+
+ assert_eq!(send_key.len(), RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH);
+ assert_eq!(recv_key.len(), RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH);
+ assert_ne!(send_nonce, recv_nonce);
+ assert_ne!(next_send, snd_first);
+ assert_ne!(next_recv, rcv_first);
+ }
}
diff --git a/crates/simplex-smp-proto/src/wire.rs b/crates/simplex-smp-proto/src/wire.rs
@@ -781,6 +781,7 @@ impl RadrootsSimplexSmpCommandTransmission {
transport_version: u16,
) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
encode_transmission(
+ transport_version,
&self.authorization,
self.correlation_id,
&self.entity_id,
@@ -796,7 +797,8 @@ impl RadrootsSimplexSmpCommandTransmission {
transport_version: u16,
bytes: &[u8],
) -> Result<Self, RadrootsSimplexSmpProtoError> {
- let (authorization, correlation_id, entity_id, frame) = decode_transmission(bytes)?;
+ let (authorization, correlation_id, entity_id, frame) =
+ decode_transmission(transport_version, bytes)?;
Ok(Self {
authorization,
correlation_id,
@@ -816,6 +818,7 @@ impl RadrootsSimplexSmpBrokerTransmission {
transport_version: u16,
) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
encode_transmission(
+ transport_version,
&self.authorization,
self.correlation_id,
&self.entity_id,
@@ -831,7 +834,8 @@ impl RadrootsSimplexSmpBrokerTransmission {
transport_version: u16,
bytes: &[u8],
) -> Result<Self, RadrootsSimplexSmpProtoError> {
- let (authorization, correlation_id, entity_id, frame) = decode_transmission(bytes)?;
+ let (authorization, correlation_id, entity_id, frame) =
+ decode_transmission(transport_version, bytes)?;
Ok(Self {
authorization,
correlation_id,
@@ -1295,6 +1299,7 @@ fn is_valid_domain_transport_host(host: &str) -> bool {
}
fn encode_transmission(
+ transport_version: u16,
authorization: &[u8],
correlation_id: Option<RadrootsSimplexSmpCorrelationId>,
entity_id: &[u8],
@@ -1302,6 +1307,11 @@ fn encode_transmission(
) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> {
let mut buffer = Vec::new();
push_short_bytes(&mut buffer, authorization)?;
+ if transport_version >= RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION
+ && !authorization.is_empty()
+ {
+ push_maybe_short_bytes(&mut buffer, None)?;
+ }
push_short_bytes(
&mut buffer,
correlation_id
@@ -1315,6 +1325,7 @@ fn encode_transmission(
}
fn decode_transmission(
+ transport_version: u16,
bytes: &[u8],
) -> Result<
(
@@ -1327,6 +1338,11 @@ fn decode_transmission(
> {
let mut cursor = Cursor::new(bytes);
let authorization = cursor.read_short_bytes()?;
+ if transport_version >= RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION
+ && !authorization.is_empty()
+ {
+ let _ = cursor.read_maybe(Cursor::read_short_bytes)?;
+ }
let correlation_id = match cursor.read_short_bytes()?.as_slice() {
[] => None,
value => Some(RadrootsSimplexSmpCorrelationId::from_slice(value)?),
@@ -2187,6 +2203,24 @@ mod tests {
}
#[test]
+ fn current_authenticated_transmission_encodes_absent_service_signature_as_maybe_none() {
+ let transmission = RadrootsSimplexSmpCommandTransmission {
+ authorization: vec![1, 2, 3],
+ correlation_id: Some(correlation_id(7)),
+ entity_id: Vec::new(),
+ command: RadrootsSimplexSmpCommand::Ping,
+ };
+
+ let encoded = transmission.encode().unwrap();
+ assert_eq!(encoded[0], 3);
+ assert_eq!(&encoded[1..4], &[1, 2, 3]);
+ assert_eq!(encoded[4], b'0');
+
+ let decoded = RadrootsSimplexSmpCommandTransmission::decode(&encoded).unwrap();
+ assert_eq!(decoded, transmission);
+ }
+
+ #[test]
fn round_trips_current_ids_broker_transmission() {
let transmission = RadrootsSimplexSmpBrokerTransmission {
authorization: Vec::new(),
diff --git a/crates/simplex-smp-transport/src/client.rs b/crates/simplex-smp-transport/src/client.rs
@@ -8,15 +8,21 @@ use crate::executor::{
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,
+ RadrootsSimplexSmpTlsHandshakeEvidence, RadrootsSimplexSmpTlsPolicy,
+ RadrootsSimplexSmpTransportServerProof, validate_tls_handshake,
};
use base64::Engine as _;
use base64::engine::general_purpose::{URL_SAFE, URL_SAFE_NO_PAD};
use radroots_simplex_smp_crypto::prelude::{
RadrootsSimplexSmpQueueAuthorizationMaterial, RadrootsSimplexSmpQueueAuthorizationScope,
+ RadrootsSimplexSmpSecretBoxChainKey, RadrootsSimplexSmpX25519Keypair, advance_secretbox_chain,
+ decode_x25519_public_key_x509, derive_shared_secret, encode_x25519_public_key_x509,
+ encrypt_padded, init_secretbox_chain, verify_signature,
};
use radroots_simplex_smp_proto::prelude::{
- RadrootsSimplexSmpCommandTransmission, RadrootsSimplexSmpServerAddress,
+ RADROOTS_SIMPLEX_SMP_AUTH_COMMANDS_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_ENCRYPTED_BLOCK_TRANSPORT_VERSION, RadrootsSimplexSmpCommandTransmission,
+ RadrootsSimplexSmpServerAddress,
};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
@@ -41,6 +47,9 @@ struct RadrootsSimplexSmpLiveSession {
stream: StreamOwned<ClientConnection, TcpStream>,
transport_version: u16,
session_identifier: Vec<u8>,
+ send_chain_key: Option<RadrootsSimplexSmpSecretBoxChainKey>,
+ receive_chain_key: Option<RadrootsSimplexSmpSecretBoxChainKey>,
+ debug_shared_secret: Option<Vec<u8>>,
}
impl RadrootsSimplexSmpTlsCommandTransport {
@@ -129,7 +138,7 @@ fn execute_live_request(
&[transmission],
session.transport_version,
)?;
- let encoded = block.encode()?;
+ let encoded = encode_live_transport_block(session, &block)?;
session
.stream
.write_all(&encoded)
@@ -145,7 +154,7 @@ fn execute_live_request(
.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 decoded = decode_live_transport_block(session, &response_block)?;
let transmissions = decoded.decode_broker_transmissions(session.transport_version)?;
if transmissions.len() != 1 {
return Err(
@@ -166,6 +175,127 @@ fn execute_live_request(
})
}
+fn transport_debug_enabled() -> bool {
+ std::env::var_os("RADROOTS_SIMPLEX_DEBUG_TRANSPORT").is_some()
+}
+
+fn debug_sha256_label(label: &str, value: &[u8]) {
+ if transport_debug_enabled() {
+ eprintln!(
+ "[simplex-smp-transport] {label}: len={} sha256={}",
+ value.len(),
+ URL_SAFE_NO_PAD.encode(Sha256::digest(value)),
+ );
+ }
+}
+
+fn encode_live_transport_block(
+ session: &mut RadrootsSimplexSmpLiveSession,
+ block: &RadrootsSimplexSmpTransportBlock,
+) -> Result<Vec<u8>, RadrootsSimplexSmpTransportError> {
+ if session.transport_version >= RADROOTS_SIMPLEX_SMP_ENCRYPTED_BLOCK_TRANSPORT_VERSION {
+ if let Some(chain_key) = session.send_chain_key.as_ref() {
+ let ((secretbox_key, nonce), next_chain_key) = advance_secretbox_chain(chain_key)?;
+ session.send_chain_key = Some(next_chain_key);
+ return encrypt_padded(
+ &secretbox_key,
+ &nonce,
+ &block.encode_payload()?,
+ RADROOTS_SIMPLEX_SMP_TRANSPORT_BLOCK_SIZE - 16,
+ )
+ .map_err(Into::into);
+ }
+ }
+ block.encode()
+}
+
+fn decode_live_transport_block(
+ session: &mut RadrootsSimplexSmpLiveSession,
+ bytes: &[u8],
+) -> Result<RadrootsSimplexSmpTransportBlock, RadrootsSimplexSmpTransportError> {
+ if session.transport_version >= RADROOTS_SIMPLEX_SMP_ENCRYPTED_BLOCK_TRANSPORT_VERSION {
+ if let Some(chain_key) = session.receive_chain_key.as_ref() {
+ let ((secretbox_key, nonce), next_chain_key) = advance_secretbox_chain(chain_key)?;
+ match radroots_simplex_smp_crypto::prelude::decrypt_padded(
+ &secretbox_key,
+ &nonce,
+ bytes,
+ ) {
+ Ok(payload) => {
+ session.receive_chain_key = Some(next_chain_key);
+ debug_sha256_label("live-response-payload", &payload);
+ return RadrootsSimplexSmpTransportBlock::from_payload(&payload);
+ }
+ Err(error) => {
+ if transport_debug_enabled() {
+ eprintln!("[simplex-smp-transport] live response decrypt failed: {error}");
+ debug_sha256_label("live-response-ciphertext", bytes);
+ }
+ if let Some(send_chain_key) = session.send_chain_key.as_ref() {
+ let ((alt_secretbox_key, alt_nonce), _) =
+ advance_secretbox_chain(send_chain_key)?;
+ if radroots_simplex_smp_crypto::prelude::decrypt_padded(
+ &alt_secretbox_key,
+ &alt_nonce,
+ bytes,
+ )
+ .is_ok()
+ {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "server response decrypted with the outbound chain key; live SMP block direction is assigned incorrectly".into(),
+ ));
+ }
+ }
+ debug_probe_transport_candidates(session, bytes);
+ if let Ok(block) = RadrootsSimplexSmpTransportBlock::decode(bytes) {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ format!(
+ "server returned plaintext SMP block while encrypted transport was expected: {:?}",
+ block.transmissions.first().map(|t| &t[..t.len().min(8)])
+ ),
+ ));
+ }
+ return Err(error.into());
+ }
+ }
+ }
+ }
+ RadrootsSimplexSmpTransportBlock::decode(bytes)
+}
+
+fn debug_probe_transport_candidates(session: &mut RadrootsSimplexSmpLiveSession, bytes: &[u8]) {
+ if !transport_debug_enabled() {
+ return;
+ }
+ let Some(shared_secret) = session.debug_shared_secret.as_ref() else {
+ return;
+ };
+ let Ok((first_chain_key, second_chain_key)) =
+ init_secretbox_chain(&session.session_identifier, shared_secret)
+ else {
+ return;
+ };
+ for (label, chain_key) in [
+ ("initial-first", first_chain_key),
+ ("initial-second", second_chain_key),
+ ] {
+ let Ok(((secretbox_key, nonce), _)) = advance_secretbox_chain(&chain_key) else {
+ continue;
+ };
+ let result =
+ radroots_simplex_smp_crypto::prelude::decrypt_padded(&secretbox_key, &nonce, bytes);
+ match result {
+ Ok(payload) => {
+ eprintln!("[simplex-smp-transport] debug candidate {label} decrypted live block");
+ debug_sha256_label("debug-candidate-payload", &payload);
+ }
+ Err(error) => {
+ eprintln!("[simplex-smp-transport] debug candidate {label} failed: {error}");
+ }
+ }
+ }
+}
+
fn connect_live_session(
server: &RadrootsSimplexSmpServerAddress,
) -> Result<RadrootsSimplexSmpLiveSession, RadrootsSimplexSmpTransportError> {
@@ -248,7 +378,7 @@ fn connect_live_session_host(
let server_hello = read_server_hello(&mut stream)?;
let actual_identity = matching_server_identity(&peer_certs, &server.server_identity)?;
let expected_identity = canonical_server_identity(&server.server_identity)?;
- let mut policy = RadrootsSimplexSmpTlsPolicy::modern(expected_identity);
+ let mut policy = RadrootsSimplexSmpTlsPolicy::modern(expected_identity.clone());
policy.require_tls_unique_binding = false;
let transport_version = validate_tls_handshake(
&policy,
@@ -264,12 +394,27 @@ fn connect_live_session_host(
tls_unique_channel_binding: None,
},
)?;
+ let transport_keypair =
+ if transport_version >= RADROOTS_SIMPLEX_SMP_AUTH_COMMANDS_TRANSPORT_VERSION {
+ Some(RadrootsSimplexSmpX25519Keypair::generate()?)
+ } else {
+ None
+ };
let client_hello = RadrootsSimplexSmpClientHello {
chosen_version: transport_version,
- client_key: None,
+ server_key_hash: decode_server_identity(&expected_identity)?,
+ client_key: transport_keypair
+ .as_ref()
+ .map(|keypair| encode_x25519_public_key_x509(&keypair.public_key))
+ .transpose()?,
+ proxy_server: false,
ignored_part: Vec::new(),
};
let encoded_client_hello = client_hello.encode()?;
+ if transport_debug_enabled() {
+ debug_sha256_label("client-hello", &encoded_client_hello);
+ debug_sha256_label("server-session-id", &server_hello.session_identifier);
+ }
stream
.write_all(&encoded_client_hello)
.map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
@@ -277,13 +422,232 @@ fn connect_live_session_host(
.flush()
.map_err(|error| RadrootsSimplexSmpTransportError::LiveTransportIo(error.to_string()))?;
+ let mut debug_shared_secret = None;
+ let (receive_chain_key, send_chain_key) =
+ if transport_version >= RADROOTS_SIMPLEX_SMP_ENCRYPTED_BLOCK_TRANSPORT_VERSION {
+ let server_key = decode_server_transport_public_key(
+ server_hello
+ .server_proof
+ .as_ref()
+ .ok_or(RadrootsSimplexSmpTransportError::MissingServerProof)?,
+ )?;
+ let shared_secret = derive_shared_secret(
+ &transport_keypair
+ .as_ref()
+ .ok_or(RadrootsSimplexSmpTransportError::MissingServerProof)?
+ .private_key,
+ &server_key,
+ )?;
+ if transport_debug_enabled() {
+ if let Some(keypair) = transport_keypair.as_ref() {
+ debug_sha256_label("client-transport-public-key", &keypair.public_key);
+ }
+ debug_sha256_label("server-transport-public-key", &server_key);
+ debug_sha256_label("transport-shared-secret", &shared_secret);
+ }
+ debug_shared_secret = transport_debug_enabled().then_some(shared_secret.clone());
+ let (receive_chain_key, send_chain_key) =
+ init_secretbox_chain(&server_hello.session_identifier, &shared_secret)?;
+ (Some(receive_chain_key), Some(send_chain_key))
+ } else {
+ (None, None)
+ };
+
Ok(RadrootsSimplexSmpLiveSession {
stream,
transport_version,
session_identifier: server_hello.session_identifier,
+ send_chain_key,
+ receive_chain_key,
+ debug_shared_secret,
})
}
+fn decode_server_transport_public_key(
+ proof: &RadrootsSimplexSmpTransportServerProof,
+) -> Result<Vec<u8>, RadrootsSimplexSmpTransportError> {
+ let (signed_object, signature) = decode_signed_server_key_parts(&proof.signed_server_key)?;
+ if !proof.certificate_payload.is_empty() {
+ let verify_key = decode_server_certificate_verify_key(&proof.certificate_payload)?;
+ verify_signature(signed_object, &verify_key, signature).map_err(|error| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "failed to verify SMP server transport key signature: {error}"
+ ))
+ })?;
+ }
+
+ decode_x25519_public_key_x509(signed_object)
+ .or_else(|_| {
+ first_der_sequence_element(signed_object)
+ .and_then(|candidate| decode_x25519_public_key_x509(candidate).map_err(Into::into))
+ })
+ .map_err(|error: RadrootsSimplexSmpTransportError| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "failed to decode verified SMP server transport key: {error}"
+ ))
+ })
+}
+
+fn first_der_sequence_element(bytes: &[u8]) -> Result<&[u8], RadrootsSimplexSmpTransportError> {
+ let (sequence_tag, _, sequence_header_end, sequence_content_end) = parse_der_element(bytes, 0)?;
+ if sequence_tag != 0x30 {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: expected DER sequence".into(),
+ ));
+ }
+ let (_, element_start, _, element_end) = parse_der_element(bytes, sequence_header_end)?;
+ if element_end > sequence_content_end {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: first element exceeds sequence bounds".into(),
+ ));
+ }
+ Ok(&bytes[element_start..element_end])
+}
+
+fn decode_signed_server_key_parts(
+ bytes: &[u8],
+) -> Result<(&[u8], &[u8]), RadrootsSimplexSmpTransportError> {
+ let (sequence_tag, _, sequence_header_end, sequence_content_end) = parse_der_element(bytes, 0)?;
+ if sequence_tag != 0x30 {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: signed key is not a DER sequence".into(),
+ ));
+ }
+
+ let (_, signed_object_start, _, signed_object_end) =
+ parse_der_element(bytes, sequence_header_end)?;
+ let (_, _, _, algorithm_end) = parse_der_element(bytes, signed_object_end)?;
+ if algorithm_end > sequence_content_end {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: signature algorithm exceeds sequence bounds".into(),
+ ));
+ }
+ let (signature_tag, _, signature_value_start, signature_end) =
+ parse_der_element(bytes, algorithm_end)?;
+ if signature_tag != 0x03 {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: expected DER bit string signature".into(),
+ ));
+ }
+ if signature_end > sequence_content_end {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: signature exceeds sequence bounds".into(),
+ ));
+ }
+ let signature_value = bytes
+ .get(signature_value_start..signature_end)
+ .ok_or_else(|| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: truncated signature".into(),
+ )
+ })?;
+ let (unused_bits, signature) = signature_value.split_first().ok_or_else(|| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: missing signature payload".into(),
+ )
+ })?;
+ if *unused_bits != 0 {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: unsupported signature bit padding".into(),
+ ));
+ }
+ Ok((&bytes[signed_object_start..signed_object_end], signature))
+}
+
+fn decode_server_certificate_verify_key(
+ certificate_payload: &[u8],
+) -> Result<Vec<u8>, RadrootsSimplexSmpTransportError> {
+ let Some(&cert_count) = certificate_payload.first() else {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: missing certificate chain".into(),
+ ));
+ };
+ if cert_count == 0 {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: empty certificate chain".into(),
+ ));
+ }
+ let (certificate_der, _) = read_large_handshake_field(certificate_payload, 1)?;
+ let (_, certificate) = x509_parser::certificate::X509Certificate::from_der(&certificate_der)
+ .map_err(|error| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "failed to parse SMP proof certificate: {error}"
+ ))
+ })?;
+ Ok(certificate
+ .tbs_certificate
+ .subject_pki
+ .subject_public_key
+ .data
+ .to_vec())
+}
+
+fn read_large_handshake_field(
+ bytes: &[u8],
+ offset: usize,
+) -> Result<(Vec<u8>, usize), RadrootsSimplexSmpTransportError> {
+ let Some(length_bytes) = bytes.get(offset..offset + 2) else {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: truncated certificate length".into(),
+ ));
+ };
+ let length = u16::from_be_bytes([length_bytes[0], length_bytes[1]]) as usize;
+ let start = offset + 2;
+ let end = start + length;
+ let value = bytes.get(start..end).ok_or_else(|| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: certificate exceeds payload".into(),
+ )
+ })?;
+ Ok((value.to_vec(), end))
+}
+
+fn parse_der_element(
+ bytes: &[u8],
+ offset: usize,
+) -> Result<(u8, usize, usize, usize), RadrootsSimplexSmpTransportError> {
+ let tag = *bytes.get(offset).ok_or_else(|| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: truncated DER element".into(),
+ )
+ })?;
+ let length_offset = offset + 1;
+ let length_tag = *bytes.get(length_offset).ok_or_else(|| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: missing DER length".into(),
+ )
+ })?;
+ let (value_len, header_len) = if length_tag & 0x80 == 0 {
+ (length_tag as usize, 2)
+ } else {
+ let length_bytes = (length_tag & 0x7f) as usize;
+ if length_bytes == 0 || length_bytes > 4 {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: unsupported DER length encoding".into(),
+ ));
+ }
+ let length_start = length_offset + 1;
+ let length_end = length_start + length_bytes;
+ let encoded_length = bytes.get(length_start..length_end).ok_or_else(|| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: truncated DER length".into(),
+ )
+ })?;
+ let value_len = encoded_length
+ .iter()
+ .fold(0_usize, |acc, byte| (acc << 8) | (*byte as usize));
+ (value_len, 2 + length_bytes)
+ };
+ let value_start = offset + header_len;
+ let value_end = value_start + value_len;
+ if value_end > bytes.len() {
+ return Err(RadrootsSimplexSmpTransportError::InvalidServerAddress(
+ "invalid SMP server proof: DER element exceeds input".into(),
+ ));
+ }
+ Ok((tag, offset, value_start, value_end))
+}
+
fn read_server_hello(
stream: &mut StreamOwned<ClientConnection, TcpStream>,
) -> Result<RadrootsSimplexSmpServerHello, RadrootsSimplexSmpTransportError> {
@@ -318,13 +682,12 @@ fn matching_server_identity(
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);
+ x509_parser::certificate::X509Certificate::from_der(der).map_err(|error| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "failed to parse SMP certificate: {error}"
+ ))
+ })?;
+ let digest = Sha256::digest(der);
Ok(URL_SAFE_NO_PAD.encode(digest))
}
@@ -340,6 +703,17 @@ fn canonical_server_identity(value: &str) -> Result<String, RadrootsSimplexSmpTr
})
}
+fn decode_server_identity(value: &str) -> Result<Vec<u8>, RadrootsSimplexSmpTransportError> {
+ URL_SAFE_NO_PAD
+ .decode(value)
+ .or_else(|_| URL_SAFE.decode(value))
+ .map_err(|_| {
+ RadrootsSimplexSmpTransportError::InvalidServerAddress(format!(
+ "invalid base64url server identity `{value}`"
+ ))
+ })
+}
+
#[derive(Debug)]
struct PermissiveSimplexServerVerifier;
@@ -388,11 +762,70 @@ impl ServerCertVerifier for PermissiveSimplexServerVerifier {
#[cfg(test)]
mod tests {
- use super::canonical_server_identity;
+ use super::{canonical_server_identity, decode_server_transport_public_key};
+ use crate::handshake::RadrootsSimplexSmpTransportServerProof;
+ use radroots_simplex_smp_crypto::prelude::{
+ RadrootsSimplexSmpX25519Keypair, encode_x25519_public_key_x509,
+ };
#[test]
fn canonicalizes_padded_and_unpadded_server_identity() {
assert_eq!(canonical_server_identity("YWJjZA").unwrap(), "YWJjZA");
assert_eq!(canonical_server_identity("YWJjZA==").unwrap(), "YWJjZA");
}
+
+ #[test]
+ fn extracts_spki_from_signed_server_key_sequence() {
+ let keypair = RadrootsSimplexSmpX25519Keypair::from_seed(b"transport-proof");
+ let spki = encode_x25519_public_key_x509(&keypair.public_key).unwrap();
+ let empty_sequence = der_sequence(core::iter::once(&[][..]));
+ let signature = [0x03, 0x01, 0x00];
+ let signed_object = der_sequence([
+ spki.as_slice(),
+ empty_sequence.as_slice(),
+ signature.as_slice(),
+ ]);
+ let proof = RadrootsSimplexSmpTransportServerProof {
+ certificate_payload: Vec::new(),
+ signed_server_key: signed_object,
+ };
+ assert_eq!(
+ decode_server_transport_public_key(&proof).unwrap(),
+ keypair.public_key
+ );
+ }
+
+ fn der_sequence<'a, I>(elements: I) -> Vec<u8>
+ where
+ I: IntoIterator<Item = &'a [u8]>,
+ {
+ let mut body = Vec::new();
+ for element in elements {
+ if element.is_empty() {
+ body.extend_from_slice(&[0x30, 0x00]);
+ } else {
+ body.extend_from_slice(element);
+ }
+ }
+ let mut sequence = vec![0x30];
+ push_der_length(&mut sequence, body.len());
+ sequence.extend_from_slice(&body);
+ sequence
+ }
+
+ fn push_der_length(buffer: &mut Vec<u8>, len: usize) {
+ if len < 0x80 {
+ buffer.push(len as u8);
+ return;
+ }
+ let mut bytes = Vec::new();
+ let mut remaining = len;
+ while remaining > 0 {
+ bytes.push((remaining & 0xff) as u8);
+ remaining >>= 8;
+ }
+ bytes.reverse();
+ buffer.push(0x80 | (bytes.len() as u8));
+ buffer.extend_from_slice(&bytes);
+ }
}
diff --git a/crates/simplex-smp-transport/src/frame.rs b/crates/simplex-smp-transport/src/frame.rs
@@ -48,7 +48,7 @@ impl RadrootsSimplexSmpTransportBlock {
}
pub fn encode(&self) -> Result<Vec<u8>, RadrootsSimplexSmpTransportError> {
- let payload = encode_transport_payload(&self.transmissions)?;
+ let payload = self.encode_payload()?;
encode_padded_bytes(
&payload,
RADROOTS_SIMPLEX_SMP_TRANSPORT_BLOCK_SIZE,
@@ -62,7 +62,15 @@ impl RadrootsSimplexSmpTransportBlock {
RADROOTS_SIMPLEX_SMP_TRANSPORT_BLOCK_SIZE,
RADROOTS_SIMPLEX_SMP_TRANSPORT_PAD_BYTE,
)?;
- let transmissions = decode_transport_payload(&payload)?;
+ Self::from_payload(&payload)
+ }
+
+ pub fn encode_payload(&self) -> Result<Vec<u8>, RadrootsSimplexSmpTransportError> {
+ encode_transport_payload(&self.transmissions)
+ }
+
+ pub fn from_payload(payload: &[u8]) -> Result<Self, RadrootsSimplexSmpTransportError> {
+ let transmissions = decode_transport_payload(payload)?;
Self::new(transmissions)
}
@@ -313,4 +321,13 @@ mod tests {
RadrootsSimplexSmpTransportError::InvalidPadding { .. }
));
}
+
+ #[test]
+ fn roundtrips_transport_payload_without_padding() {
+ let block =
+ RadrootsSimplexSmpTransportBlock::new(vec![b"one".to_vec(), b"two".to_vec()]).unwrap();
+ let payload = block.encode_payload().unwrap();
+ let decoded = RadrootsSimplexSmpTransportBlock::from_payload(&payload).unwrap();
+ assert_eq!(decoded, block);
+ }
}
diff --git a/crates/simplex-smp-transport/src/handshake.rs b/crates/simplex-smp-transport/src/handshake.rs
@@ -7,7 +7,8 @@ use alloc::string::{String, ToString};
use alloc::vec::Vec;
use radroots_simplex_smp_proto::prelude::{
RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_INITIAL_TRANSPORT_VERSION,
- RadrootsSimplexSmpVersionRange,
+ RADROOTS_SIMPLEX_SMP_PROXY_SERVER_HANDSHAKE_TRANSPORT_VERSION,
+ RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION, RadrootsSimplexSmpVersionRange,
};
pub const RADROOTS_SIMPLEX_SMP_TLS_ALPN_V1: &str = "smp/1";
@@ -32,7 +33,9 @@ pub struct RadrootsSimplexSmpServerHello {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadrootsSimplexSmpClientHello {
pub chosen_version: u16,
+ pub server_key_hash: Vec<u8>,
pub client_key: Option<Vec<u8>>,
+ pub proxy_server: bool,
pub ignored_part: Vec<u8>,
}
@@ -79,10 +82,8 @@ impl RadrootsSimplexSmpServerHello {
payload.extend_from_slice(&self.version_range.max.to_be_bytes());
push_short_bytes(&mut payload, &self.session_identifier)?;
if let Some(proof) = &self.server_proof {
- payload.extend_from_slice(&(proof.certificate_payload.len() as u16).to_be_bytes());
payload.extend_from_slice(&proof.certificate_payload);
- payload.extend_from_slice(&(proof.signed_server_key.len() as u16).to_be_bytes());
- payload.extend_from_slice(&proof.signed_server_key);
+ push_large_bytes(&mut payload, &proof.signed_server_key)?;
}
payload.extend_from_slice(&self.ignored_part);
encode_padded_bytes(
@@ -130,9 +131,16 @@ impl RadrootsSimplexSmpClientHello {
pub fn encode(&self) -> Result<Vec<u8>, RadrootsSimplexSmpTransportError> {
let mut payload = Vec::new();
payload.extend_from_slice(&self.chosen_version.to_be_bytes());
+ push_short_bytes(&mut payload, &self.server_key_hash)?;
if let Some(client_key) = &self.client_key {
push_short_bytes(&mut payload, client_key)?;
}
+ if self.chosen_version >= RADROOTS_SIMPLEX_SMP_PROXY_SERVER_HANDSHAKE_TRANSPORT_VERSION {
+ payload.push(if self.proxy_server { b'T' } else { b'F' });
+ }
+ if self.chosen_version >= RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION {
+ payload.push(b'0');
+ }
payload.extend_from_slice(&self.ignored_part);
encode_padded_bytes(
&payload,
@@ -153,11 +161,57 @@ impl RadrootsSimplexSmpClientHello {
));
};
let chosen_version = u16::from_be_bytes([version_bytes[0], version_bytes[1]]);
- let (client_key, ignored_part) = parse_optional_client_key(&payload[2..]);
+ let (server_key_hash, mut cursor) = read_short_bytes(&payload, 2)?;
+ let client_key = if chosen_version
+ >= RADROOTS_SIMPLEX_SMP_PROXY_SERVER_HANDSHAKE_TRANSPORT_VERSION
+ && matches!(payload.get(cursor), Some(b'T' | b'F'))
+ {
+ None
+ } else {
+ let (client_key, consumed) = parse_optional_client_key(&payload[cursor..]);
+ cursor += consumed;
+ client_key
+ };
+ let proxy_server =
+ if chosen_version >= RADROOTS_SIMPLEX_SMP_PROXY_SERVER_HANDSHAKE_TRANSPORT_VERSION {
+ let Some(value) = payload.get(cursor) else {
+ return Err(RadrootsSimplexSmpTransportError::MissingHandshakeField(
+ "proxy_server",
+ ));
+ };
+ cursor += 1;
+ match *value {
+ b'T' => true,
+ b'F' => false,
+ _ => {
+ return Err(RadrootsSimplexSmpTransportError::MissingHandshakeField(
+ "proxy_server",
+ ));
+ }
+ }
+ } else {
+ false
+ };
+ if chosen_version >= RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION {
+ let Some(tag) = payload.get(cursor) else {
+ return Err(RadrootsSimplexSmpTransportError::MissingHandshakeField(
+ "client_service",
+ ));
+ };
+ cursor += 1;
+ if *tag != b'0' {
+ return Err(RadrootsSimplexSmpTransportError::MissingHandshakeField(
+ "client_service",
+ ));
+ }
+ }
+ let ignored_part = payload[cursor..].to_vec();
Ok(Self {
chosen_version,
+ server_key_hash,
client_key,
+ proxy_server,
ignored_part,
})
}
@@ -254,6 +308,18 @@ fn push_short_bytes(
Ok(())
}
+fn push_large_bytes(
+ buffer: &mut Vec<u8>,
+ bytes: &[u8],
+) -> Result<(), RadrootsSimplexSmpTransportError> {
+ let len = u16::try_from(bytes.len()).map_err(|_| {
+ RadrootsSimplexSmpTransportError::InvalidSessionIdentifierLength(bytes.len())
+ })?;
+ buffer.extend_from_slice(&len.to_be_bytes());
+ buffer.extend_from_slice(bytes);
+ Ok(())
+}
+
fn read_short_bytes(
payload: &[u8],
offset: usize,
@@ -273,41 +339,63 @@ fn read_short_bytes(
Ok((value.to_vec(), end))
}
+fn read_large_bytes(
+ payload: &[u8],
+ offset: usize,
+) -> Result<(Vec<u8>, usize), RadrootsSimplexSmpTransportError> {
+ let Some(length_bytes) = payload.get(offset..offset + 2) else {
+ return Err(RadrootsSimplexSmpTransportError::MissingHandshakeField(
+ "large_field",
+ ));
+ };
+ let length = u16::from_be_bytes([length_bytes[0], length_bytes[1]]) as usize;
+ let start = offset + 2;
+ let end = start + length;
+ let Some(value) = payload.get(start..end) else {
+ return Err(
+ radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpProtoError::UnexpectedEof.into(),
+ );
+ };
+ Ok((value.to_vec(), end))
+}
+
fn parse_optional_server_proof(
remainder: &[u8],
) -> (Option<RadrootsSimplexSmpTransportServerProof>, Vec<u8>) {
- if remainder.len() < 4 {
+ let Some(&cert_count) = remainder.first() else {
return (None, remainder.to_vec());
- }
- let cert_len = u16::from_be_bytes([remainder[0], remainder[1]]) as usize;
- let cert_end = 2 + cert_len;
- if cert_len == 0 || cert_end + 2 > remainder.len() {
+ };
+ if cert_count == 0 {
return (None, remainder.to_vec());
}
- let key_len = u16::from_be_bytes([remainder[cert_end], remainder[cert_end + 1]]) as usize;
- let key_start = cert_end + 2;
- let key_end = key_start + key_len;
- if key_len == 0 || key_end > remainder.len() {
- return (None, remainder.to_vec());
+ let mut cursor = 1;
+ for _ in 0..cert_count {
+ let Ok((_, next_cursor)) = read_large_bytes(remainder, cursor) else {
+ return (None, remainder.to_vec());
+ };
+ cursor = next_cursor;
}
+ let Ok((signed_server_key, cursor)) = read_large_bytes(remainder, cursor) else {
+ return (None, remainder.to_vec());
+ };
(
Some(RadrootsSimplexSmpTransportServerProof {
- certificate_payload: remainder[2..cert_end].to_vec(),
- signed_server_key: remainder[key_start..key_end].to_vec(),
+ certificate_payload: remainder[..cursor - signed_server_key.len() - 2].to_vec(),
+ signed_server_key,
}),
- remainder[key_end..].to_vec(),
+ remainder[cursor..].to_vec(),
)
}
-fn parse_optional_client_key(remainder: &[u8]) -> (Option<Vec<u8>>, Vec<u8>) {
+fn parse_optional_client_key(remainder: &[u8]) -> (Option<Vec<u8>>, usize) {
let Some(&length) = remainder.first() else {
- return (None, Vec::new());
+ return (None, 0);
};
let end = 1 + length as usize;
if length == 0 || end > remainder.len() {
- return (None, remainder.to_vec());
+ return (None, remainder.len());
}
- (Some(remainder[1..end].to_vec()), remainder[end..].to_vec())
+ (Some(remainder[1..end].to_vec()), end)
}
#[cfg(test)]
@@ -320,7 +408,7 @@ mod tests {
version_range: RadrootsSimplexSmpVersionRange::new(6, 17).unwrap(),
session_identifier: b"tls-unique-binding".to_vec(),
server_proof: Some(RadrootsSimplexSmpTransportServerProof {
- certificate_payload: b"cert-chain".to_vec(),
+ certificate_payload: encode_certificate_chain_payload([b"cert-chain".as_slice()]),
signed_server_key: b"signed-key".to_vec(),
}),
ignored_part: b"ignored".to_vec(),
@@ -390,4 +478,17 @@ mod tests {
RadrootsSimplexSmpTransportError::ServerIdentityMismatch { .. }
));
}
+
+ fn encode_certificate_chain_payload<'a, I>(certificates: I) -> Vec<u8>
+ where
+ I: IntoIterator<Item = &'a [u8]>,
+ {
+ let certificates: Vec<&[u8]> = certificates.into_iter().collect();
+ let mut payload = vec![certificates.len() as u8];
+ for certificate in certificates {
+ payload.extend_from_slice(&(certificate.len() as u16).to_be_bytes());
+ payload.extend_from_slice(certificate);
+ }
+ payload
+ }
}