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 }