lib

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

message_contract.rs (6584B)


      1 use nostr::{Keys, SecretKey};
      2 use radroots_identity::PublicKey as IdentityPublicKey;
      3 use radroots_nostr_connect::message::{
      4     PendingConnectionOutcome, REMOTE_CAPABILITY_RELAY_COUNT_MAX, REQUEST_ID_MAX_BYTES,
      5     RESPONSE_ERROR_MAX_BYTES, RemoteSessionCapability, RequestId, RequestMessage, ResponseEnvelope,
      6     ResponseValidator,
      7 };
      8 use radroots_nostr_connect::uri::RelayUrl;
      9 use radroots_nostr_connect::{Error, Method, Request, Response};
     10 use serde_json::{Value, json};
     11 use std::str::FromStr;
     12 
     13 fn keys(secret_hex: &str) -> Keys {
     14     Keys::new(SecretKey::from_hex(secret_hex).expect("secret key"))
     15 }
     16 
     17 fn identity_key(secret_hex: &str) -> IdentityPublicKey {
     18     radroots_nostr::key::public_key_from_nostr(keys(secret_hex).public_key())
     19         .expect("identity public key")
     20 }
     21 
     22 #[test]
     23 fn request_and_response_round_trip_with_bounded_correlation() {
     24     let request = RequestMessage::try_new("request-1", Request::Ping).expect("valid request");
     25     let encoded = serde_json::to_string(&request).expect("serialize request");
     26     assert_eq!(
     27         serde_json::from_str::<RequestMessage>(&encoded).expect("deserialize request"),
     28         request
     29     );
     30 
     31     let response = Response::Pong
     32         .into_envelope("request-1")
     33         .expect("valid response");
     34     assert_eq!(
     35         request.correlate(response).expect("correlated response"),
     36         Response::Pong
     37     );
     38 
     39     assert!(matches!(
     40         RequestId::parse(""),
     41         Err(Error::InvalidRequestId { .. })
     42     ));
     43     assert!(matches!(
     44         RequestId::parse(" request-1"),
     45         Err(Error::InvalidRequestId { .. })
     46     ));
     47     assert!(matches!(
     48         RequestId::parse("x".repeat(REQUEST_ID_MAX_BYTES + 1)),
     49         Err(Error::InvalidRequestId { .. })
     50     ));
     51 }
     52 
     53 #[test]
     54 fn correlation_rejects_wrong_id_wrong_signer_and_replay() {
     55     const SIGNER: &str = "0000000000000000000000000000000000000000000000000000000000000001";
     56     const OTHER: &str = "0000000000000000000000000000000000000000000000000000000000000002";
     57     let request = RequestMessage::try_new("request-2", Request::Ping).expect("request");
     58     let wrong_id = Response::Pong.into_envelope("request-3").expect("response");
     59     assert_eq!(
     60         request.correlate(wrong_id).expect_err("wrong id"),
     61         Error::WrongRequestId
     62     );
     63 
     64     let envelope = Response::Pong.into_envelope("request-2").expect("response");
     65     let mut validator = ResponseValidator::new(
     66         RequestId::parse("request-2").expect("request id"),
     67         identity_key(SIGNER),
     68     );
     69     assert_eq!(
     70         validator
     71             .validate(identity_key(OTHER), "event-1", &envelope)
     72             .expect_err("wrong signer"),
     73         Error::WrongResponseSigner
     74     );
     75     validator
     76         .validate(identity_key(SIGNER), "event-1", &envelope)
     77         .expect("first response");
     78     assert_eq!(
     79         validator
     80             .validate(identity_key(SIGNER), "event-1", &envelope)
     81             .expect_err("replay"),
     82         Error::ReplayedResponse
     83     );
     84 }
     85 
     86 #[test]
     87 fn malformed_envelopes_and_unsafe_auth_challenges_fail_closed() {
     88     assert!(matches!(
     89         ResponseEnvelope::try_new("request-3", None, Some(String::new())),
     90         Err(Error::InvalidResponseEnvelope { .. })
     91     ));
     92     assert!(matches!(
     93         ResponseEnvelope::try_new(
     94             "request-3",
     95             None,
     96             Some("x".repeat(RESPONSE_ERROR_MAX_BYTES + 1)),
     97         ),
     98         Err(Error::InvalidResponseEnvelope { .. })
     99     ));
    100     assert!(
    101         serde_json::from_value::<ResponseEnvelope>(json!({
    102             "id": "request-3",
    103             "result": "pong",
    104             "unexpected": true,
    105         }))
    106         .is_err()
    107     );
    108     assert!(matches!(
    109         Response::AuthUrl("file:///tmp/approval".to_owned()).into_envelope("request-3"),
    110         Err(Error::InvalidUrl { value, .. }) if value == "[redacted auth URL]"
    111     ));
    112 }
    113 
    114 #[test]
    115 fn unknown_methods_round_trip_only_when_canonical() {
    116     let method = Method::from_str("vendor_action").expect("canonical extension method");
    117     let request = RequestMessage::try_new(
    118         "request-custom",
    119         Request::Custom {
    120             method: method.clone(),
    121             params: vec!["alpha".to_owned()],
    122         },
    123     )
    124     .expect("custom request");
    125     let encoded = serde_json::to_value(&request).expect("serialize custom request");
    126     assert_eq!(encoded["method"], Value::String("vendor_action".to_owned()));
    127     assert_eq!(
    128         serde_json::from_value::<RequestMessage>(encoded)
    129             .expect("deserialize custom request")
    130             .payload()
    131             .method(),
    132         method
    133     );
    134     assert!(Method::from_str("Vendor-Action").is_err());
    135 }
    136 
    137 #[test]
    138 fn remote_capabilities_and_pending_outcomes_are_bounded_and_typed() {
    139     let relay = RelayUrl::parse("wss://relay.example.test").expect("relay");
    140     let capability = RemoteSessionCapability::try_new(
    141         identity_key("0000000000000000000000000000000000000000000000000000000000000003"),
    142         vec![relay.clone()],
    143         Default::default(),
    144     )
    145     .expect("capability");
    146     let response = Response::RemoteSessionCapability(capability.clone());
    147     assert_eq!(
    148         response.into_pending_connection_poll_outcome(),
    149         PendingConnectionOutcome::ApprovedCapability(capability)
    150     );
    151     assert!(matches!(
    152         RemoteSessionCapability::try_new(
    153             identity_key("0000000000000000000000000000000000000000000000000000000000000003"),
    154             vec![relay; REMOTE_CAPABILITY_RELAY_COUNT_MAX + 1],
    155             Default::default(),
    156         ),
    157         Err(Error::InvalidResponsePayload { .. })
    158     ));
    159 }
    160 
    161 #[test]
    162 fn diagnostics_redact_protocol_payloads() {
    163     const SECRET: &str = "do-not-log-connect-secret";
    164     let request = Request::Connect {
    165         remote_signer_public_key: identity_key(
    166             "0000000000000000000000000000000000000000000000000000000000000004",
    167         ),
    168         secret: Some(SECRET.to_owned()),
    169         requested_permissions: Default::default(),
    170         client_metadata: None,
    171     };
    172     assert!(!format!("{request:?}").contains(SECRET));
    173 
    174     let envelope = ResponseEnvelope::try_new(
    175         "request-redacted",
    176         Some(Value::String(SECRET.to_owned())),
    177         None,
    178     )
    179     .expect("response envelope");
    180     assert!(!format!("{envelope:?}").contains(SECRET));
    181 
    182     let response = Response::ConnectSecretEcho(SECRET.to_owned());
    183     assert!(!format!("{response:?}").contains(SECRET));
    184     assert_eq!(
    185         response.into_pending_connection_poll_outcome(),
    186         PendingConnectionOutcome::UnexpectedResponse {
    187             response: "connect_secret_echo".to_owned(),
    188         }
    189     );
    190 }