lib.rs (20757B)
1 #![cfg_attr(not(feature = "std"), no_std)] 2 #![forbid(unsafe_code)] 3 #![doc = r#" 4 `radroots_simplex_interop_tests` owns the synthetic fixture policy for the rr-rs 5 SimpleX stack. 6 7 Rules: 8 - committed fixtures must use the `rr-synth/*` namespace. 9 - committed server hosts must stay in obviously synthetic domains such as 10 `.invalid`, `.example`, or `.test`. 11 - committed tests must not copy or derive realistic queue URIs, certificates, 12 ciphertext, or traffic from `refs/*` or external captures. 13 - black-box local upstream checks are opt-in through environment variables and 14 are never required for the default workspace verify lane. 15 "#] 16 17 extern crate alloc; 18 19 pub mod fixtures; 20 pub mod policy; 21 22 #[cfg(test)] 23 mod tests { 24 use crate::fixtures::{ 25 synthetic_chat_messages, synthetic_connection_id, synthetic_fixture_id, 26 synthetic_invitation_queue, synthetic_reply_queue, 27 }; 28 use crate::policy::{RadrootsSimplexInteropFixturePolicy, RadrootsSimplexInteropLocalUpstream}; 29 use alloc::collections::VecDeque; 30 use radroots_simplex_agent_proto::prelude::{ 31 RadrootsSimplexAgentDecryptedMessage, RadrootsSimplexAgentEncryptedPayload, 32 RadrootsSimplexAgentEnvelope, RadrootsSimplexAgentMessage, 33 RadrootsSimplexAgentMessageFrame, RadrootsSimplexAgentMessageHeader, 34 decode_agent_message_frame, decode_decrypted_message, decode_envelope, 35 encode_agent_message_frame, encode_decrypted_message, encode_envelope, 36 }; 37 use radroots_simplex_agent_runtime::prelude::{ 38 RadrootsSimplexAgentRuntime, RadrootsSimplexAgentRuntimeBuilder, 39 RadrootsSimplexAgentRuntimeEvent, 40 }; 41 use radroots_simplex_chat_proto::prelude::{decode_messages, encode_compressed_batch}; 42 use radroots_simplex_smp_crypto::prelude::{ 43 RadrootsSimplexSmpCommandAuthorization, RadrootsSimplexSmpEd25519Keypair, 44 RadrootsSimplexSmpQueueAuthorizationMaterial, RadrootsSimplexSmpQueueAuthorizationScope, 45 RadrootsSimplexSmpX25519Keypair, encode_ed25519_public_key_x509, 46 encode_x25519_public_key_x509, 47 }; 48 use radroots_simplex_smp_proto::prelude::{ 49 RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, RadrootsSimplexSmpBrokerMessage, 50 RadrootsSimplexSmpBrokerTransmission, RadrootsSimplexSmpCommand, 51 RadrootsSimplexSmpCommandTransmission, RadrootsSimplexSmpCorrelationId, 52 RadrootsSimplexSmpMessageFlags, RadrootsSimplexSmpNewQueueRequest, 53 RadrootsSimplexSmpQueueIdsResponse, RadrootsSimplexSmpQueueMode, 54 RadrootsSimplexSmpQueueRequestData, RadrootsSimplexSmpSendCommand, 55 RadrootsSimplexSmpServerAddress, RadrootsSimplexSmpSubscriptionMode, 56 }; 57 use radroots_simplex_smp_transport::prelude::{ 58 RadrootsSimplexSmpCommandTransport, RadrootsSimplexSmpSubscriptionReceiveRequest, 59 RadrootsSimplexSmpSubscriptionTransport, RadrootsSimplexSmpTlsCommandTransport, 60 RadrootsSimplexSmpTransportBlock, RadrootsSimplexSmpTransportRequest, 61 RadrootsSimplexSmpTransportResponse, 62 }; 63 64 fn ids_response( 65 recipient_id: &[u8], 66 sender_id: &[u8], 67 seed: &[u8], 68 ) -> RadrootsSimplexSmpBrokerMessage { 69 RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse { 70 recipient_id: recipient_id.to_vec(), 71 sender_id: sender_id.to_vec(), 72 server_dh_public_key: RadrootsSimplexSmpX25519Keypair::from_seed(seed).public_key, 73 queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging), 74 link_id: Some(synthetic_link_id(seed)), 75 service_id: None, 76 server_notification_credentials: None, 77 }) 78 } 79 80 fn synthetic_link_id(seed: &[u8]) -> Vec<u8> { 81 let mut link_id = vec![0_u8; 24]; 82 for (index, byte) in seed.iter().enumerate() { 83 link_id[index % 24] ^= *byte; 84 link_id[(index * 7 + 3) % 24] = link_id[(index * 7 + 3) % 24].wrapping_add(*byte); 85 } 86 link_id 87 } 88 89 fn correlation_id(byte: u8) -> RadrootsSimplexSmpCorrelationId { 90 RadrootsSimplexSmpCorrelationId::new([byte; RadrootsSimplexSmpCorrelationId::LENGTH]) 91 } 92 93 fn live_transport_request( 94 server: RadrootsSimplexSmpServerAddress, 95 correlation_id: RadrootsSimplexSmpCorrelationId, 96 entity_id: Vec<u8>, 97 command: RadrootsSimplexSmpCommand, 98 authorization: RadrootsSimplexSmpCommandAuthorization, 99 ) -> RadrootsSimplexSmpTransportRequest { 100 RadrootsSimplexSmpTransportRequest { 101 server, 102 transport_version: RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, 103 correlation_id: Some(correlation_id), 104 entity_id, 105 command, 106 authorization, 107 } 108 } 109 110 #[cfg(feature = "std")] 111 fn local_upstream_target() -> Option<RadrootsSimplexInteropLocalUpstream> { 112 RadrootsSimplexInteropLocalUpstream::required_from_env().unwrap() 113 } 114 115 #[derive(Default)] 116 struct ScriptedTransport { 117 responses: VecDeque<RadrootsSimplexSmpBrokerMessage>, 118 requests: Vec<RadrootsSimplexSmpTransportRequest>, 119 } 120 121 impl ScriptedTransport { 122 fn with_responses(responses: Vec<RadrootsSimplexSmpBrokerMessage>) -> Self { 123 Self { 124 responses: responses.into(), 125 requests: Vec::new(), 126 } 127 } 128 } 129 130 impl RadrootsSimplexSmpCommandTransport for ScriptedTransport { 131 type Error = String; 132 133 fn execute( 134 &mut self, 135 request: RadrootsSimplexSmpTransportRequest, 136 ) -> Result<RadrootsSimplexSmpTransportResponse, Self::Error> { 137 let correlation_id = request 138 .correlation_id 139 .ok_or_else(|| "missing scripted transport correlation id".to_owned())?; 140 let scope = RadrootsSimplexSmpQueueAuthorizationScope::new( 141 b"scripted-session".to_vec(), 142 correlation_id, 143 request.entity_id.clone(), 144 ) 145 .map_err(|error| error.to_string())?; 146 let material = RadrootsSimplexSmpQueueAuthorizationMaterial::for_command( 147 &scope, 148 &request.command, 149 request.transport_version, 150 &request.authorization, 151 ) 152 .map_err(|error| error.to_string())?; 153 let transmission = RadrootsSimplexSmpCommandTransmission { 154 authorization: material.authorization, 155 correlation_id: Some(correlation_id), 156 entity_id: request.entity_id.clone(), 157 command: request.command.clone(), 158 }; 159 let block = RadrootsSimplexSmpTransportBlock::from_current_command_transmissions(&[ 160 transmission.clone(), 161 ]) 162 .map_err(|error| error.to_string())?; 163 let encoded = block.encode().map_err(|error| error.to_string())?; 164 let decoded = RadrootsSimplexSmpTransportBlock::decode(&encoded) 165 .map_err(|error| error.to_string())?; 166 let decoded_transmissions = decoded 167 .decode_command_transmissions(request.transport_version) 168 .map_err(|error| error.to_string())?; 169 assert_eq!(decoded_transmissions, vec![transmission.clone()]); 170 171 let response_message = self 172 .responses 173 .pop_front() 174 .ok_or_else(|| "missing scripted transport response".to_owned())?; 175 let response_transmission = RadrootsSimplexSmpBrokerTransmission { 176 authorization: Vec::new(), 177 correlation_id: Some(correlation_id), 178 entity_id: request.entity_id.clone(), 179 message: response_message, 180 }; 181 let response_block = RadrootsSimplexSmpTransportBlock::from_broker_transmissions( 182 &[response_transmission.clone()], 183 request.transport_version, 184 ) 185 .map_err(|error| error.to_string())?; 186 let response_encoded = response_block.encode().map_err(|error| error.to_string())?; 187 self.requests.push(request.clone()); 188 Ok(RadrootsSimplexSmpTransportResponse { 189 server: request.server, 190 transport_version: request.transport_version, 191 transmission: response_transmission, 192 transport_hash: response_encoded, 193 }) 194 } 195 } 196 197 #[test] 198 fn synthetic_policy_accepts_only_rr_owned_fixtures() { 199 let policy = RadrootsSimplexInteropFixturePolicy::default(); 200 policy.assert_fixture_id(synthetic_fixture_id()).unwrap(); 201 policy 202 .assert_queue_uri(&synthetic_invitation_queue()) 203 .unwrap(); 204 policy.assert_queue_uri(&synthetic_reply_queue()).unwrap(); 205 206 let error = policy.assert_fixture_id("copied-from-refs"); 207 assert!(error.is_err()); 208 } 209 210 #[test] 211 fn synthetic_stack_roundtrip_exercises_smp_agent_and_chat_layers() { 212 let correlation_id = RadrootsSimplexSmpCorrelationId::new([7_u8; 24]); 213 let send_command = RadrootsSimplexSmpCommand::Send(RadrootsSimplexSmpSendCommand { 214 flags: RadrootsSimplexSmpMessageFlags::notifications_enabled(), 215 message_body: b"rr-synth-body".to_vec(), 216 }); 217 let transmission = RadrootsSimplexSmpCommandTransmission { 218 authorization: b"rr-synth-auth".to_vec(), 219 correlation_id: Some(correlation_id), 220 entity_id: b"rr-synth-queue".to_vec(), 221 command: send_command.clone(), 222 }; 223 let block = RadrootsSimplexSmpTransportBlock::from_current_command_transmissions(&[ 224 transmission.clone(), 225 ]) 226 .unwrap(); 227 let decoded = RadrootsSimplexSmpTransportBlock::decode(&block.encode().unwrap()) 228 .unwrap() 229 .decode_command_transmissions(RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION) 230 .unwrap(); 231 assert_eq!(decoded, vec![transmission]); 232 233 let scope = RadrootsSimplexSmpQueueAuthorizationScope::new( 234 b"rr-synth-session".to_vec(), 235 correlation_id, 236 b"rr-synth-queue".to_vec(), 237 ) 238 .unwrap(); 239 let auth = RadrootsSimplexSmpQueueAuthorizationMaterial::for_command( 240 &scope, 241 &send_command, 242 RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, 243 &RadrootsSimplexSmpCommandAuthorization::None, 244 ) 245 .unwrap(); 246 assert_eq!(auth.authorized_body[0], b"rr-synth-session".len() as u8); 247 assert!(auth.authorization.is_empty()); 248 249 let chat_messages = synthetic_chat_messages(); 250 let compressed_chat = encode_compressed_batch(&chat_messages).unwrap(); 251 let decoded_chat = decode_messages(&compressed_chat).unwrap(); 252 assert_eq!(decoded_chat, chat_messages); 253 254 let frame = RadrootsSimplexAgentMessageFrame { 255 header: RadrootsSimplexAgentMessageHeader { 256 message_id: 1, 257 previous_message_hash: synthetic_connection_id().as_bytes().to_vec(), 258 }, 259 message: RadrootsSimplexAgentMessage::UserMessage(compressed_chat.clone()), 260 padding: Vec::new(), 261 }; 262 let encoded_frame = encode_agent_message_frame(&frame).unwrap(); 263 let decoded_frame = decode_agent_message_frame(&encoded_frame).unwrap(); 264 assert_eq!(decoded_frame.header, frame.header); 265 assert_eq!(decoded_frame.message, frame.message); 266 267 let decrypted = RadrootsSimplexAgentDecryptedMessage::Message(frame.clone()); 268 let encoded_decrypted = encode_decrypted_message(&decrypted).unwrap(); 269 let envelope = 270 RadrootsSimplexAgentEnvelope::Message(RadrootsSimplexAgentEncryptedPayload { 271 ratchet_header: None, 272 official_message: None, 273 ciphertext: b"opaque-agent-ciphertext".to_vec(), 274 }); 275 let decoded_envelope = decode_envelope(&encode_envelope(&envelope).unwrap()).unwrap(); 276 let RadrootsSimplexAgentEnvelope::Message(payload) = decoded_envelope else { 277 panic!("expected message envelope"); 278 }; 279 assert_eq!(payload.ciphertext, b"opaque-agent-ciphertext".to_vec()); 280 let decoded_decrypted = decode_decrypted_message(&encoded_decrypted).unwrap(); 281 assert_eq!(decoded_decrypted, decrypted); 282 } 283 284 #[test] 285 fn synthetic_runtime_flow_stays_fixture_owned() { 286 let mut runtime: RadrootsSimplexAgentRuntime = 287 RadrootsSimplexAgentRuntimeBuilder::new().build().unwrap(); 288 let created = runtime 289 .create_connection( 290 synthetic_invitation_queue(), 291 b"rr-synth-e2e".to_vec(), 292 false, 293 10, 294 ) 295 .unwrap(); 296 let mut invitation_transport = ScriptedTransport::with_responses(vec![ids_response( 297 b"recipient", 298 b"sender", 299 b"server-dh", 300 )]); 301 runtime 302 .execute_ready_commands(&mut invitation_transport, 20, 16) 303 .unwrap(); 304 let events = runtime.drain_events(8); 305 let short_invitation = events 306 .into_iter() 307 .find_map(|event| match event { 308 RadrootsSimplexAgentRuntimeEvent::InvitationReady { invitation, .. } => { 309 Some(invitation) 310 } 311 _ => None, 312 }) 313 .expect("invitation event"); 314 assert!( 315 short_invitation 316 .render() 317 .unwrap() 318 .starts_with("simplex:/i#") 319 ); 320 let RadrootsSimplexSmpCommand::New(create_request) = 321 &invitation_transport.requests[0].command 322 else { 323 panic!("first synthetic runtime command should create the invite queue"); 324 }; 325 assert!(matches!( 326 create_request.queue_request_data.as_ref(), 327 Some(RadrootsSimplexSmpQueueRequestData::Messaging(Some(_))) 328 )); 329 assert!(created.starts_with("conn-")); 330 } 331 332 #[cfg(feature = "std")] 333 #[test] 334 fn local_upstream_contract_is_opt_in() { 335 let Some(target) = local_upstream_target() else { 336 return; 337 }; 338 target.assert_reachable().unwrap(); 339 } 340 341 #[cfg(feature = "std")] 342 #[test] 343 fn required_local_upstream_contract_is_enforced() { 344 let Some(target) = local_upstream_target() else { 345 return; 346 }; 347 target.assert_reachable().unwrap(); 348 assert!(target.server_address().is_some()); 349 } 350 351 #[cfg(feature = "std")] 352 #[test] 353 fn local_upstream_ping_round_trips_when_configured() { 354 let Some(target) = local_upstream_target() else { 355 return; 356 }; 357 target.assert_reachable().unwrap(); 358 let Some(server) = target.server_address() else { 359 return; 360 }; 361 362 let response = RadrootsSimplexSmpTlsCommandTransport::new() 363 .execute(live_transport_request( 364 server, 365 correlation_id(1), 366 Vec::new(), 367 RadrootsSimplexSmpCommand::Ping, 368 RadrootsSimplexSmpCommandAuthorization::None, 369 )) 370 .unwrap(); 371 assert!(matches!( 372 response.transmission.message, 373 RadrootsSimplexSmpBrokerMessage::Pong 374 )); 375 } 376 377 #[cfg(feature = "std")] 378 #[test] 379 fn local_upstream_create_subscribe_send_receive_ack_and_resubscribe_when_configured() { 380 let Some(target) = local_upstream_target() else { 381 return; 382 }; 383 target.assert_reachable().unwrap(); 384 let Some(server) = target.server_address() else { 385 return; 386 }; 387 388 let recipient_auth = RadrootsSimplexSmpEd25519Keypair::generate().unwrap(); 389 let recipient_dh = RadrootsSimplexSmpX25519Keypair::generate().unwrap(); 390 let mut recipient_transport = RadrootsSimplexSmpTlsCommandTransport::new(); 391 let create_response = recipient_transport 392 .execute(live_transport_request( 393 server.clone(), 394 correlation_id(1), 395 Vec::new(), 396 RadrootsSimplexSmpCommand::New(RadrootsSimplexSmpNewQueueRequest { 397 recipient_auth_public_key: encode_ed25519_public_key_x509( 398 &recipient_auth.public_key, 399 ) 400 .unwrap(), 401 recipient_dh_public_key: encode_x25519_public_key_x509( 402 &recipient_dh.public_key, 403 ) 404 .unwrap(), 405 basic_auth: None, 406 subscription_mode: RadrootsSimplexSmpSubscriptionMode::OnlyCreate, 407 queue_request_data: Some(RadrootsSimplexSmpQueueRequestData::Messaging(None)), 408 notifier_credentials: None, 409 }), 410 RadrootsSimplexSmpCommandAuthorization::Ed25519(recipient_auth.clone()), 411 )) 412 .unwrap(); 413 let RadrootsSimplexSmpBrokerMessage::Ids(ids) = create_response.transmission.message else { 414 panic!("expected IDS response from live SMP queue creation"); 415 }; 416 417 let subscribe_response = recipient_transport 418 .execute(live_transport_request( 419 server.clone(), 420 correlation_id(2), 421 ids.recipient_id.clone(), 422 RadrootsSimplexSmpCommand::Sub, 423 RadrootsSimplexSmpCommandAuthorization::Ed25519(recipient_auth.clone()), 424 )) 425 .unwrap(); 426 match subscribe_response.transmission.message { 427 RadrootsSimplexSmpBrokerMessage::Ok 428 | RadrootsSimplexSmpBrokerMessage::Sok(_) 429 | RadrootsSimplexSmpBrokerMessage::Msg(_) => {} 430 other => panic!("expected live SMP subscription readiness response, got {other:?}"), 431 } 432 433 let mut sender_transport = RadrootsSimplexSmpTlsCommandTransport::new(); 434 let send_response = sender_transport 435 .execute(live_transport_request( 436 server.clone(), 437 correlation_id(3), 438 ids.sender_id.clone(), 439 RadrootsSimplexSmpCommand::Send(RadrootsSimplexSmpSendCommand { 440 flags: RadrootsSimplexSmpMessageFlags::notifications_enabled(), 441 message_body: b"rr-synth-live-subscribe-message".to_vec(), 442 }), 443 RadrootsSimplexSmpCommandAuthorization::None, 444 )) 445 .unwrap(); 446 assert!(matches!( 447 send_response.transmission.message, 448 RadrootsSimplexSmpBrokerMessage::Ok 449 )); 450 451 let subscription_response = recipient_transport 452 .receive_subscription(RadrootsSimplexSmpSubscriptionReceiveRequest { 453 server: server.clone(), 454 }) 455 .unwrap() 456 .expect("expected live SMP subscription message"); 457 let RadrootsSimplexSmpBrokerMessage::Msg(message) = 458 subscription_response.transmission.message 459 else { 460 panic!("expected MSG response from live SMP subscription"); 461 }; 462 assert!(!message.message_id.is_empty()); 463 assert!(!message.encrypted_body.is_empty()); 464 465 let ack_response = recipient_transport 466 .execute(live_transport_request( 467 server.clone(), 468 correlation_id(4), 469 ids.recipient_id.clone(), 470 RadrootsSimplexSmpCommand::Ack(message.message_id), 471 RadrootsSimplexSmpCommandAuthorization::Ed25519(recipient_auth.clone()), 472 )) 473 .unwrap(); 474 match ack_response.transmission.message { 475 RadrootsSimplexSmpBrokerMessage::Ok => {} 476 other => panic!("expected live SMP ACK response, got {other:?}"), 477 } 478 479 let mut reconnect_transport = RadrootsSimplexSmpTlsCommandTransport::new(); 480 let resubscribe_response = reconnect_transport 481 .execute(live_transport_request( 482 server, 483 correlation_id(5), 484 ids.recipient_id, 485 RadrootsSimplexSmpCommand::Sub, 486 RadrootsSimplexSmpCommandAuthorization::Ed25519(recipient_auth), 487 )) 488 .unwrap(); 489 match resubscribe_response.transmission.message { 490 RadrootsSimplexSmpBrokerMessage::Ok 491 | RadrootsSimplexSmpBrokerMessage::Sok(_) 492 | RadrootsSimplexSmpBrokerMessage::Msg(_) => {} 493 other => panic!("expected live SMP resubscription readiness response, got {other:?}"), 494 } 495 } 496 }