lib

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

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 }