commit c8acf17e291877855fdf0301fb74f58165c64955
parent 853ef12beecd5d700336e2998b6382ba91df1838
Author: triesap <tyson@radroots.org>
Date: Fri, 31 Jul 2026 18:33:48 +0000
nostr-connect: stabilize request and response envelopes
- validate bounded request IDs, parameters, response bodies, and capabilities
- own protocol identity, relay, and event representations with redacted diagnostics
- add signer-aware correlation and replay rejection without serializing timeout policy
- migrate NIP-46 client and signer consumers with malformed-response coverage
Diffstat:
12 files changed, 1108 insertions(+), 280 deletions(-)
diff --git a/crates/nostr_connect/src/client.rs b/crates/nostr_connect/src/client.rs
@@ -1,9 +1,5 @@
use crate::error::RadrootsNostrConnectError;
-use crate::message::{
- RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectRequest,
- RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
- RadrootsNostrConnectResponseEnvelope,
-};
+use crate::message::{RPC_KIND, Request, RequestMessage, Response, ResponseEnvelope};
use crate::method::Method;
use nostr::nips::nip44::{self, Version};
use nostr::{Event, EventBuilder, Keys, Kind, PublicKey, RelayUrl, Tag};
@@ -31,11 +27,11 @@ impl RadrootsNostrConnectClientTarget {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadrootsNostrConnectClientRequest {
pub request_id: String,
- pub request: RadrootsNostrConnectRequest,
+ pub request: Request,
}
impl RadrootsNostrConnectClientRequest {
- pub fn new(request_id: impl Into<String>, request: RadrootsNostrConnectRequest) -> Self {
+ pub fn new(request_id: impl Into<String>, request: Request) -> Self {
Self {
request_id: request_id.into(),
request,
@@ -46,8 +42,8 @@ impl RadrootsNostrConnectClientRequest {
self.request.method()
}
- pub fn into_message(self) -> RadrootsNostrConnectRequestMessage {
- RadrootsNostrConnectRequestMessage::new(self.request_id, self.request)
+ pub fn into_message(self) -> RequestMessage {
+ RequestMessage::new(self.request_id, self.request)
}
}
@@ -60,7 +56,7 @@ pub enum RadrootsNostrConnectClientProgress {
pub enum RadrootsNostrConnectClientEventOutcome {
Ignore,
Progress(RadrootsNostrConnectClientProgress),
- Response(RadrootsNostrConnectResponse),
+ Response(Response),
}
pub trait RadrootsNostrConnectClientTransport {
@@ -77,7 +73,7 @@ pub trait RadrootsNostrConnectClientTransport {
pub fn build_request_event(
client_keys: &Keys,
target: &RadrootsNostrConnectClientTarget,
- message: RadrootsNostrConnectRequestMessage,
+ message: RequestMessage,
) -> Result<Event, RadrootsNostrConnectError> {
let payload = serde_json::to_string(&message).map_err(RadrootsNostrConnectError::from)?;
let ciphertext = nip44::encrypt(
@@ -88,7 +84,7 @@ pub fn build_request_event(
)
.map_err(encrypt_error)?;
- EventBuilder::new(Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), ciphertext)
+ EventBuilder::new(Kind::Custom(RPC_KIND), ciphertext)
.tag(Tag::public_key(target.remote_signer_public_key))
.sign_with_keys(client_keys)
.map_err(sign_error)
@@ -101,7 +97,7 @@ pub fn parse_response_event(
method: &Method,
event: &Event,
) -> Result<RadrootsNostrConnectClientEventOutcome, RadrootsNostrConnectError> {
- if event.kind != Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) {
+ if event.kind != Kind::Custom(RPC_KIND) {
return Ok(RadrootsNostrConnectClientEventOutcome::Ignore);
}
@@ -127,19 +123,17 @@ pub fn parse_response_event(
reason: error.to_string(),
})?;
- let envelope: RadrootsNostrConnectResponseEnvelope =
+ let envelope: ResponseEnvelope =
serde_json::from_str(&decrypted).map_err(RadrootsNostrConnectError::from)?;
if envelope.id != request_id {
return Ok(RadrootsNostrConnectClientEventOutcome::Ignore);
}
- let response = RadrootsNostrConnectResponse::from_envelope(method, envelope)?;
+ let response = Response::from_envelope(method, envelope)?;
Ok(match response {
- RadrootsNostrConnectResponse::AuthUrl(url) => {
- RadrootsNostrConnectClientEventOutcome::Progress(
- RadrootsNostrConnectClientProgress::AuthChallenge { url },
- )
- }
+ Response::AuthUrl(url) => RadrootsNostrConnectClientEventOutcome::Progress(
+ RadrootsNostrConnectClientProgress::AuthChallenge { url },
+ ),
response => RadrootsNostrConnectClientEventOutcome::Response(response),
})
}
@@ -150,7 +144,7 @@ pub async fn execute_request_with_transport<T, F>(
request: RadrootsNostrConnectClientRequest,
transport: &mut T,
mut on_progress: F,
-) -> Result<RadrootsNostrConnectResponse, RadrootsNostrConnectError>
+) -> Result<Response, RadrootsNostrConnectError>
where
T: RadrootsNostrConnectClientTransport,
F: FnMut(RadrootsNostrConnectClientProgress) -> Result<(), RadrootsNostrConnectError>,
diff --git a/crates/nostr_connect/src/error.rs b/crates/nostr_connect/src/error.rs
@@ -12,6 +12,16 @@ pub enum RadrootsNostrConnectError {
Transport { reason: String },
#[error("NIP-46 request timed out")]
RequestTimedOut,
+ #[error("invalid NIP-46 request id: {reason}")]
+ InvalidRequestId { reason: &'static str },
+ #[error("NIP-46 response id does not match the request")]
+ WrongRequestId,
+ #[error("NIP-46 response signer does not match the expected signer")]
+ WrongResponseSigner,
+ #[error("replayed NIP-46 response")]
+ ReplayedResponse,
+ #[error("invalid NIP-46 response envelope: {reason}")]
+ InvalidResponseEnvelope { reason: &'static str },
#[error("invalid NIP-46 method `{0}`")]
InvalidMethod(String),
#[error("invalid NIP-46 permission `{0}`")]
diff --git a/crates/nostr_connect/src/lib.rs b/crates/nostr_connect/src/lib.rs
@@ -10,6 +10,7 @@ pub mod server;
pub mod uri;
pub use error::RadrootsNostrConnectError as Error;
+pub use message::{Request, Response};
pub use method::Method;
pub use permission::Permission;
pub use uri::{BunkerUri, ClientUri};
@@ -26,11 +27,21 @@ pub mod prelude {
};
pub use crate::error::RadrootsNostrConnectError;
pub use crate::message::{
- RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR, RADROOTS_NOSTR_CONNECT_RPC_KIND,
- RadrootsNostrConnectPendingConnectionPollOutcome,
- RadrootsNostrConnectRemoteSessionCapability, RadrootsNostrConnectRequest,
- RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
- RadrootsNostrConnectResponseEnvelope,
+ PENDING_CONNECTION_ERROR, PendingConnectionOutcome, REMOTE_CAPABILITY_RELAY_COUNT_MAX,
+ REQUEST_ID_MAX_BYTES, REQUEST_PARAM_COUNT_MAX, REQUEST_PARAM_MAX_BYTES,
+ REQUEST_PARAMS_MAX_BYTES, RESPONSE_ERROR_MAX_BYTES, RESPONSE_RESULT_MAX_BYTES, RPC_KIND,
+ RemoteSessionCapability, Request, RequestId, RequestMessage, Response, ResponseEnvelope,
+ ResponseValidator, SignedEvent, UnsignedEvent,
+ };
+ pub use crate::message::{
+ PENDING_CONNECTION_ERROR as RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR,
+ PendingConnectionOutcome as RadrootsNostrConnectPendingConnectionPollOutcome,
+ RPC_KIND as RADROOTS_NOSTR_CONNECT_RPC_KIND,
+ RemoteSessionCapability as RadrootsNostrConnectRemoteSessionCapability,
+ Request as RadrootsNostrConnectRequest,
+ RequestMessage as RadrootsNostrConnectRequestMessage,
+ Response as RadrootsNostrConnectResponse,
+ ResponseEnvelope as RadrootsNostrConnectResponseEnvelope,
};
pub use crate::method::Method;
pub use crate::method::Method as RadrootsNostrConnectMethod;
diff --git a/crates/nostr_connect/src/message.rs b/crates/nostr_connect/src/message.rs
@@ -1,24 +1,224 @@
use crate::error::RadrootsNostrConnectError;
use crate::method::Method;
use crate::permission::Permissions;
-use crate::uri::ClientMetadata;
-use nostr::{Event, JsonUtil, PublicKey, RelayUrl, UnsignedEvent};
+use crate::uri::{ClientMetadata, RelayUrl};
+use nostr::JsonUtil;
+use radroots_identity::PublicKey;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Value, json};
+use std::collections::BTreeSet;
+use std::fmt;
use std::str::FromStr;
use url::Url;
-pub const RADROOTS_NOSTR_CONNECT_RPC_KIND: u16 = 24_133;
+pub const RPC_KIND: u16 = 24_133;
+pub const REQUEST_ID_MAX_BYTES: usize = 128;
+pub const REQUEST_PARAM_COUNT_MAX: usize = 64;
+pub const REQUEST_PARAM_MAX_BYTES: usize = 65_536;
+pub const REQUEST_PARAMS_MAX_BYTES: usize = 262_144;
+pub const RESPONSE_ERROR_MAX_BYTES: usize = 4_096;
+pub const RESPONSE_RESULT_MAX_BYTES: usize = 262_144;
+pub const REMOTE_CAPABILITY_RELAY_COUNT_MAX: usize = 32;
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsNostrConnectRemoteSessionCapability {
+/// A validated NIP-46 unsigned-event payload with package-owned representation.
+#[derive(Clone, PartialEq, Eq)]
+pub struct UnsignedEvent(nostr::UnsignedEvent);
+
+impl fmt::Debug for UnsignedEvent {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("UnsignedEvent(<redacted>)")
+ }
+}
+
+impl UnsignedEvent {
+ pub fn from_json(value: &str) -> Result<Self, RadrootsNostrConnectError> {
+ serde_json::from_str(value).map(Self).map_err(|error| {
+ RadrootsNostrConnectError::InvalidRequestPayload {
+ method: Method::SignEvent.to_string(),
+ reason: error.to_string(),
+ }
+ })
+ }
+
+ #[must_use]
+ pub fn as_json(&self) -> String {
+ self.0.as_json()
+ }
+
+ #[must_use]
+ pub fn kind(&self) -> u16 {
+ self.0.kind.as_u16()
+ }
+}
+
+/// A validated NIP-46 signed-event payload with package-owned representation.
+#[derive(Clone, PartialEq, Eq)]
+pub struct SignedEvent(nostr::Event);
+
+impl fmt::Debug for SignedEvent {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("SignedEvent(<redacted>)")
+ }
+}
+
+impl SignedEvent {
+ pub fn from_json(value: &str) -> Result<Self, RadrootsNostrConnectError> {
+ serde_json::from_str(value).map(Self).map_err(|error| {
+ RadrootsNostrConnectError::InvalidResponsePayload {
+ method: Method::SignEvent.to_string(),
+ reason: error.to_string(),
+ }
+ })
+ }
+
+ #[must_use]
+ pub fn as_json(&self) -> String {
+ self.0.as_json()
+ }
+}
+
+/// A bounded correlation identifier carried by a NIP-46 request and response.
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RequestId(String);
+
+impl RequestId {
+ pub fn parse(value: impl Into<String>) -> Result<Self, RadrootsNostrConnectError> {
+ let value = value.into();
+ validate_request_id(&value)?;
+ Ok(Self(value))
+ }
+
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+impl fmt::Display for RequestId {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(&self.0)
+ }
+}
+
+impl FromStr for RequestId {
+ type Err = RadrootsNostrConnectError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ Self::parse(value)
+ }
+}
+
+impl Serialize for RequestId {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.as_str())
+ }
+}
+
+impl<'de> Deserialize<'de> for RequestId {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+ Self::parse(value).map_err(serde::de::Error::custom)
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RemoteSessionCapability {
+ #[doc(hidden)]
pub user_public_key: PublicKey,
+ #[doc(hidden)]
pub relays: Vec<RelayUrl>,
+ #[doc(hidden)]
pub permissions: Permissions,
}
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsNostrConnectRequest {
+impl RemoteSessionCapability {
+ pub fn try_new(
+ user_public_key: PublicKey,
+ relays: Vec<RelayUrl>,
+ permissions: Permissions,
+ ) -> Result<Self, RadrootsNostrConnectError> {
+ let capability = Self {
+ user_public_key,
+ relays,
+ permissions,
+ };
+ capability.validate()?;
+ Ok(capability)
+ }
+
+ #[must_use]
+ pub const fn user_public_key(&self) -> PublicKey {
+ self.user_public_key
+ }
+
+ #[must_use]
+ pub fn relays(&self) -> &[RelayUrl] {
+ &self.relays
+ }
+
+ #[must_use]
+ pub fn permissions(&self) -> &Permissions {
+ &self.permissions
+ }
+
+ fn validate(&self) -> Result<(), RadrootsNostrConnectError> {
+ if self.relays.len() > REMOTE_CAPABILITY_RELAY_COUNT_MAX {
+ return Err(RadrootsNostrConnectError::InvalidResponsePayload {
+ method: Method::GetSessionCapability.to_string(),
+ reason: "remote capability relay count exceeds its limit".to_owned(),
+ });
+ }
+ self.permissions
+ .to_string()
+ .parse::<Permissions>()
+ .map(|_| ())
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct RemoteSessionCapabilitySerde {
+ user_public_key: String,
+ relays: Vec<RelayUrl>,
+ permissions: Permissions,
+}
+
+impl Serialize for RemoteSessionCapability {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ self.validate().map_err(serde::ser::Error::custom)?;
+ RemoteSessionCapabilitySerde {
+ user_public_key: self.user_public_key.to_hex(),
+ relays: self.relays.clone(),
+ permissions: self.permissions.clone(),
+ }
+ .serialize(serializer)
+ }
+}
+
+impl<'de> Deserialize<'de> for RemoteSessionCapability {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let raw = RemoteSessionCapabilitySerde::deserialize(deserializer)?;
+ let user_public_key =
+ parse_public_key(&raw.user_public_key).map_err(serde::de::Error::custom)?;
+ Self::try_new(user_public_key, raw.relays, raw.permissions)
+ .map_err(serde::de::Error::custom)
+ }
+}
+
+#[derive(Clone, PartialEq, Eq)]
+pub enum Request {
Connect {
remote_signer_public_key: PublicKey,
secret: Option<String>,
@@ -53,7 +253,19 @@ pub enum RadrootsNostrConnectRequest {
},
}
-impl RadrootsNostrConnectRequest {
+impl fmt::Debug for Request {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("Request")
+ .field("method", &self.method())
+ .field("payload", &"<redacted>")
+ .finish()
+ }
+}
+
+impl Request {
+ /// Returns the canonical NIP-46 method represented by this payload.
+ #[must_use]
pub fn method(&self) -> Method {
match self {
Self::Connect { .. } => Method::Connect,
@@ -119,6 +331,7 @@ impl RadrootsNostrConnectRequest {
} => vec![public_key.to_hex(), ciphertext.clone()],
Self::Custom { params, .. } => params.clone(),
};
+ validate_params(¶ms)?;
Ok(params)
}
@@ -126,6 +339,7 @@ impl RadrootsNostrConnectRequest {
method: Method,
params: Vec<String>,
) -> Result<Self, RadrootsNostrConnectError> {
+ validate_params(¶ms)?;
match method {
Method::Connect => {
if params.is_empty() || params.len() > 4 {
@@ -162,12 +376,7 @@ impl RadrootsNostrConnectRequest {
}
Method::SignEvent => {
expect_param_count(&method, ¶ms, 1)?;
- let unsigned_event = serde_json::from_str(¶ms[0]).map_err(|error| {
- RadrootsNostrConnectError::InvalidRequestPayload {
- method: method.to_string(),
- reason: error.to_string(),
- }
- })?;
+ let unsigned_event = UnsignedEvent::from_json(¶ms[0])?;
Ok(Self::SignEvent(unsigned_event))
}
Method::Nip04Encrypt => {
@@ -219,20 +428,58 @@ impl RadrootsNostrConnectRequest {
}
#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsNostrConnectRequestMessage {
+pub struct RequestMessage {
+ #[doc(hidden)]
pub id: String,
- pub request: RadrootsNostrConnectRequest,
+ #[doc(hidden)]
+ pub request: Request,
}
-impl RadrootsNostrConnectRequestMessage {
- pub fn new(id: impl Into<String>, request: RadrootsNostrConnectRequest) -> Self {
+impl RequestMessage {
+ /// Creates and validates a serialized request envelope.
+ pub fn try_new(
+ id: impl Into<String>,
+ request: Request,
+ ) -> Result<Self, RadrootsNostrConnectError> {
+ let id = id.into();
+ validate_request_id(&id)?;
+ request.to_params()?;
+ Ok(Self { id, request })
+ }
+
+ /// Compatibility constructor retained until the Step 141 consumer cutover.
+ #[doc(hidden)]
+ #[must_use]
+ pub fn new(id: impl Into<String>, request: Request) -> Self {
Self {
id: id.into(),
request,
}
}
+ pub fn request_id(&self) -> Result<RequestId, RadrootsNostrConnectError> {
+ RequestId::parse(self.id.clone())
+ }
+
+ #[must_use]
+ pub fn payload(&self) -> &Request {
+ &self.request
+ }
+
+ /// Correlates and decodes a response using this request's method.
+ pub fn correlate(
+ &self,
+ envelope: ResponseEnvelope,
+ ) -> Result<Response, RadrootsNostrConnectError> {
+ envelope.validate()?;
+ if envelope.id != self.id {
+ return Err(RadrootsNostrConnectError::WrongRequestId);
+ }
+ Response::from_envelope(&self.request.method(), envelope)
+ }
+
fn into_raw(self) -> Result<RawRequestMessage, RadrootsNostrConnectError> {
+ validate_request_id(&self.id)?;
Ok(RawRequestMessage {
id: self.id,
method: self.request.method(),
@@ -241,14 +488,12 @@ impl RadrootsNostrConnectRequestMessage {
}
fn from_raw(raw: RawRequestMessage) -> Result<Self, RadrootsNostrConnectError> {
- Ok(Self {
- id: raw.id,
- request: RadrootsNostrConnectRequest::from_parts(raw.method, raw.params)?,
- })
+ let request = Request::from_parts(raw.method, raw.params)?;
+ Self::try_new(raw.id, request)
}
}
-impl Serialize for RadrootsNostrConnectRequestMessage {
+impl Serialize for RequestMessage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -260,7 +505,7 @@ impl Serialize for RadrootsNostrConnectRequestMessage {
}
}
-impl<'de> Deserialize<'de> for RadrootsNostrConnectRequestMessage {
+impl<'de> Deserialize<'de> for RequestMessage {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@@ -270,36 +515,178 @@ impl<'de> Deserialize<'de> for RadrootsNostrConnectRequestMessage {
}
}
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsNostrConnectResponseEnvelope {
+#[derive(Clone, PartialEq, Eq)]
+pub struct ResponseEnvelope {
+ #[doc(hidden)]
pub id: String,
- #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[doc(hidden)]
pub result: Option<Value>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[doc(hidden)]
pub error: Option<String>,
}
-pub const RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR: &str = "connection is pending";
+impl fmt::Debug for ResponseEnvelope {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("ResponseEnvelope")
+ .field("id", &self.id)
+ .field("has_result", &self.result.is_some())
+ .field("has_error", &self.error.is_some())
+ .finish()
+ }
+}
+
+impl ResponseEnvelope {
+ pub fn try_new(
+ id: impl Into<String>,
+ result: Option<Value>,
+ error: Option<String>,
+ ) -> Result<Self, RadrootsNostrConnectError> {
+ let envelope = Self {
+ id: id.into(),
+ result,
+ error,
+ };
+ envelope.validate()?;
+ Ok(envelope)
+ }
+
+ pub fn request_id(&self) -> Result<RequestId, RadrootsNostrConnectError> {
+ RequestId::parse(self.id.clone())
+ }
+
+ #[must_use]
+ pub fn result(&self) -> Option<&Value> {
+ self.result.as_ref()
+ }
+
+ #[must_use]
+ pub fn error(&self) -> Option<&str> {
+ self.error.as_deref()
+ }
+
+ pub fn validate(&self) -> Result<(), RadrootsNostrConnectError> {
+ validate_request_id(&self.id)?;
+ if let Some(error) = self.error.as_deref()
+ && (error.is_empty()
+ || error.len() > RESPONSE_ERROR_MAX_BYTES
+ || error.chars().any(char::is_control))
+ {
+ return Err(RadrootsNostrConnectError::InvalidResponseEnvelope {
+ reason: "error must be non-empty, bounded, and control-free",
+ });
+ }
+ if let Some(result) = self.result.as_ref()
+ && serde_json::to_vec(result)
+ .map_err(RadrootsNostrConnectError::from)?
+ .len()
+ > RESPONSE_RESULT_MAX_BYTES
+ {
+ return Err(RadrootsNostrConnectError::InvalidResponseEnvelope {
+ reason: "result exceeds its byte limit",
+ });
+ }
+ Ok(())
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ResponseEnvelopeSerde {
+ id: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ result: Option<Value>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ error: Option<String>,
+}
+
+impl Serialize for ResponseEnvelope {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ self.validate().map_err(serde::ser::Error::custom)?;
+ ResponseEnvelopeSerde {
+ id: self.id.clone(),
+ result: self.result.clone(),
+ error: self.error.clone(),
+ }
+ .serialize(serializer)
+ }
+}
+
+impl<'de> Deserialize<'de> for ResponseEnvelope {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let raw = ResponseEnvelopeSerde::deserialize(deserializer)?;
+ Self::try_new(raw.id, raw.result, raw.error).map_err(serde::de::Error::custom)
+ }
+}
+
+/// Correlation state supplied by a caller that owns response-event identity.
+#[derive(Debug)]
+pub struct ResponseValidator {
+ request_id: RequestId,
+ expected_signer: radroots_identity::PublicKey,
+ seen_fingerprints: BTreeSet<String>,
+}
+
+impl ResponseValidator {
+ #[must_use]
+ pub fn new(request_id: RequestId, expected_signer: radroots_identity::PublicKey) -> Self {
+ Self {
+ request_id,
+ expected_signer,
+ seen_fingerprints: BTreeSet::new(),
+ }
+ }
+
+ /// Validates signer and request correlation and rejects a repeated event fingerprint.
+ pub fn validate(
+ &mut self,
+ signer: radroots_identity::PublicKey,
+ response_fingerprint: impl Into<String>,
+ envelope: &ResponseEnvelope,
+ ) -> Result<(), RadrootsNostrConnectError> {
+ if signer != self.expected_signer {
+ return Err(RadrootsNostrConnectError::WrongResponseSigner);
+ }
+ envelope.validate()?;
+ if envelope.id != self.request_id.as_str() {
+ return Err(RadrootsNostrConnectError::WrongRequestId);
+ }
+ let fingerprint = response_fingerprint.into();
+ validate_response_fingerprint(&fingerprint)?;
+ if !self.seen_fingerprints.insert(fingerprint) {
+ return Err(RadrootsNostrConnectError::ReplayedResponse);
+ }
+ Ok(())
+ }
+}
+
+pub const PENDING_CONNECTION_ERROR: &str = "connection is pending";
#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsNostrConnectPendingConnectionPollOutcome {
+pub enum PendingConnectionOutcome {
PendingApproval,
Approved(PublicKey),
- ApprovedCapability(RadrootsNostrConnectRemoteSessionCapability),
+ ApprovedCapability(RemoteSessionCapability),
Rejected { message: String },
AuthChallenge { url: String },
UnexpectedResponse { response: String },
}
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsNostrConnectResponse {
+#[derive(Clone, PartialEq, Eq)]
+pub enum Response {
ConnectAcknowledged,
ConnectSecretEcho(String),
LogoutAcknowledged,
PendingConnection,
UserPublicKey(PublicKey),
- RemoteSessionCapability(RadrootsNostrConnectRemoteSessionCapability),
- SignedEvent(Event),
+ RemoteSessionCapability(RemoteSessionCapability),
+ SignedEvent(SignedEvent),
Pong,
Nip04Encrypt(String),
Nip04Decrypt(String),
@@ -318,33 +705,53 @@ pub enum RadrootsNostrConnectResponse {
},
}
-impl RadrootsNostrConnectResponse {
- pub fn into_pending_connection_poll_outcome(
- self,
- ) -> RadrootsNostrConnectPendingConnectionPollOutcome {
+impl fmt::Debug for Response {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("Response")
+ .field("kind", &self.kind_name())
+ .field("payload", &"<redacted>")
+ .finish()
+ }
+}
+
+impl Response {
+ const fn kind_name(&self) -> &'static str {
match self {
- Self::PendingConnection => {
- RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval
- }
- Self::UserPublicKey(public_key) => {
- RadrootsNostrConnectPendingConnectionPollOutcome::Approved(public_key)
- }
+ Self::ConnectAcknowledged => "connect_acknowledged",
+ Self::ConnectSecretEcho(_) => "connect_secret_echo",
+ Self::LogoutAcknowledged => "logout_acknowledged",
+ Self::PendingConnection => "pending_connection",
+ Self::UserPublicKey(_) => "user_public_key",
+ Self::RemoteSessionCapability(_) => "remote_session_capability",
+ Self::SignedEvent(_) => "signed_event",
+ Self::Pong => "pong",
+ Self::Nip04Encrypt(_) => "nip04_encrypt",
+ Self::Nip04Decrypt(_) => "nip04_decrypt",
+ Self::Nip44Encrypt(_) => "nip44_encrypt",
+ Self::Nip44Decrypt(_) => "nip44_decrypt",
+ Self::RelayList(_) => "relay_list",
+ Self::RelayListUnchanged => "relay_list_unchanged",
+ Self::AuthUrl(_) => "auth_url",
+ Self::Error { .. } => "error",
+ Self::Custom { .. } => "custom",
+ }
+ }
+
+ pub fn into_pending_connection_poll_outcome(self) -> PendingConnectionOutcome {
+ match self {
+ Self::PendingConnection => PendingConnectionOutcome::PendingApproval,
+ Self::UserPublicKey(public_key) => PendingConnectionOutcome::Approved(public_key),
Self::RemoteSessionCapability(capability) => {
- RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability(capability)
- }
- Self::Error { error, .. }
- if error == RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR =>
- {
- RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval
- }
- Self::Error { error, .. } => {
- RadrootsNostrConnectPendingConnectionPollOutcome::Rejected { message: error }
+ PendingConnectionOutcome::ApprovedCapability(capability)
}
- Self::AuthUrl(url) => {
- RadrootsNostrConnectPendingConnectionPollOutcome::AuthChallenge { url }
+ Self::Error { error, .. } if error == PENDING_CONNECTION_ERROR => {
+ PendingConnectionOutcome::PendingApproval
}
- other => RadrootsNostrConnectPendingConnectionPollOutcome::UnexpectedResponse {
- response: format!("{other:?}"),
+ Self::Error { error, .. } => PendingConnectionOutcome::Rejected { message: error },
+ Self::AuthUrl(url) => PendingConnectionOutcome::AuthChallenge { url },
+ other => PendingConnectionOutcome::UnexpectedResponse {
+ response: other.kind_name().to_owned(),
},
}
}
@@ -352,42 +759,40 @@ impl RadrootsNostrConnectResponse {
pub fn into_envelope(
self,
id: impl Into<String>,
- ) -> Result<RadrootsNostrConnectResponseEnvelope, RadrootsNostrConnectError> {
+ ) -> Result<ResponseEnvelope, RadrootsNostrConnectError> {
let id = id.into();
let envelope = match self {
- Self::ConnectAcknowledged | Self::LogoutAcknowledged => {
- RadrootsNostrConnectResponseEnvelope {
- id,
- result: Some(Value::String("ack".to_owned())),
- error: None,
- }
- }
- Self::ConnectSecretEcho(secret) => RadrootsNostrConnectResponseEnvelope {
+ Self::ConnectAcknowledged | Self::LogoutAcknowledged => ResponseEnvelope {
+ id,
+ result: Some(Value::String("ack".to_owned())),
+ error: None,
+ },
+ Self::ConnectSecretEcho(secret) => ResponseEnvelope {
id,
result: Some(Value::String(secret)),
error: None,
},
- Self::PendingConnection => RadrootsNostrConnectResponseEnvelope {
+ Self::PendingConnection => ResponseEnvelope {
id,
result: None,
- error: Some(RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR.to_owned()),
+ error: Some(PENDING_CONNECTION_ERROR.to_owned()),
},
- Self::UserPublicKey(public_key) => RadrootsNostrConnectResponseEnvelope {
+ Self::UserPublicKey(public_key) => ResponseEnvelope {
id,
result: Some(Value::String(public_key.to_hex())),
error: None,
},
- Self::RemoteSessionCapability(capability) => RadrootsNostrConnectResponseEnvelope {
+ Self::RemoteSessionCapability(capability) => ResponseEnvelope {
id,
result: Some(remote_session_capability_value(capability)),
error: None,
},
- Self::SignedEvent(event) => RadrootsNostrConnectResponseEnvelope {
+ Self::SignedEvent(event) => ResponseEnvelope {
id,
result: Some(Value::String(event.as_json())),
error: None,
},
- Self::Pong => RadrootsNostrConnectResponseEnvelope {
+ Self::Pong => ResponseEnvelope {
id,
result: Some(Value::String("pong".to_owned())),
error: None,
@@ -395,7 +800,7 @@ impl RadrootsNostrConnectResponse {
Self::Nip04Encrypt(text)
| Self::Nip04Decrypt(text)
| Self::Nip44Encrypt(text)
- | Self::Nip44Decrypt(text) => RadrootsNostrConnectResponseEnvelope {
+ | Self::Nip44Decrypt(text) => ResponseEnvelope {
id,
result: Some(Value::String(text)),
error: None,
@@ -405,7 +810,7 @@ impl RadrootsNostrConnectResponse {
.into_iter()
.map(|relay| relay.to_string())
.collect::<Vec<_>>();
- RadrootsNostrConnectResponseEnvelope {
+ ResponseEnvelope {
id,
result: Some(Value::Array(
relays.into_iter().map(Value::String).collect(),
@@ -413,35 +818,35 @@ impl RadrootsNostrConnectResponse {
error: None,
}
}
- Self::RelayListUnchanged => RadrootsNostrConnectResponseEnvelope {
+ Self::RelayListUnchanged => ResponseEnvelope {
id,
result: Some(Value::Null),
error: None,
},
Self::AuthUrl(url) => {
let normalized = validate_url(&url)?;
- RadrootsNostrConnectResponseEnvelope {
+ ResponseEnvelope {
id,
result: Some(Value::String("auth_url".to_owned())),
error: Some(normalized),
}
}
- Self::Error { result, error } => RadrootsNostrConnectResponseEnvelope {
+ Self::Error { result, error } => ResponseEnvelope {
id,
result,
error: Some(error),
},
- Self::Custom { result, error } => {
- RadrootsNostrConnectResponseEnvelope { id, result, error }
- }
+ Self::Custom { result, error } => ResponseEnvelope { id, result, error },
};
+ envelope.validate()?;
Ok(envelope)
}
pub fn from_envelope(
method: &Method,
- envelope: RadrootsNostrConnectResponseEnvelope,
+ envelope: ResponseEnvelope,
) -> Result<Self, RadrootsNostrConnectError> {
+ envelope.validate()?;
if let (Some(Value::String(result)), Some(url)) = (&envelope.result, &envelope.error)
&& result == "auth_url"
{
@@ -451,7 +856,7 @@ impl RadrootsNostrConnectResponse {
if let Some(error) = envelope.error {
if matches!(method, Method::GetPublicKey | Method::GetSessionCapability)
&& envelope.result.is_none()
- && error == RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR
+ && error == PENDING_CONNECTION_ERROR
{
return Ok(Self::PendingConnection);
}
@@ -485,7 +890,8 @@ impl RadrootsNostrConnectResponse {
Ok(Self::RemoteSessionCapability(capability))
}
Method::SignEvent => {
- let event = parse_json_string_result::<Event>(method, envelope.result)?;
+ let value = expect_json_string_or_value(method, envelope.result)?;
+ let event = SignedEvent::from_json(&value)?;
Ok(Self::SignedEvent(event))
}
Method::Ping => {
@@ -493,7 +899,7 @@ impl RadrootsNostrConnectResponse {
if result != "pong" {
return Err(RadrootsNostrConnectError::InvalidResponsePayload {
method: method.to_string(),
- reason: format!("expected `pong`, got `{result}`"),
+ reason: "expected canonical `pong` result".to_owned(),
});
}
Ok(Self::Pong)
@@ -520,7 +926,7 @@ impl RadrootsNostrConnectResponse {
if result != "ack" {
return Err(RadrootsNostrConnectError::InvalidResponsePayload {
method: method.to_string(),
- reason: format!("expected `ack`, got `{result}`"),
+ reason: "expected canonical `ack` result".to_owned(),
});
}
Ok(Self::LogoutAcknowledged)
@@ -533,23 +939,76 @@ impl RadrootsNostrConnectResponse {
}
}
-fn remote_session_capability_value(
- capability: RadrootsNostrConnectRemoteSessionCapability,
-) -> Value {
+fn remote_session_capability_value(capability: RemoteSessionCapability) -> Value {
json!({
- "user_public_key": capability.user_public_key,
+ "user_public_key": capability.user_public_key.to_hex(),
"relays": capability.relays,
"permissions": capability.permissions,
})
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
struct RawRequestMessage {
id: String,
method: Method,
params: Vec<String>,
}
+fn validate_request_id(value: &str) -> Result<(), RadrootsNostrConnectError> {
+ if value.is_empty() {
+ return Err(RadrootsNostrConnectError::InvalidRequestId {
+ reason: "request id cannot be empty",
+ });
+ }
+ if value.len() > REQUEST_ID_MAX_BYTES {
+ return Err(RadrootsNostrConnectError::InvalidRequestId {
+ reason: "request id exceeds its byte limit",
+ });
+ }
+ if value.trim() != value || value.chars().any(char::is_control) {
+ return Err(RadrootsNostrConnectError::InvalidRequestId {
+ reason: "request id must be canonical and control-free",
+ });
+ }
+ Ok(())
+}
+
+fn validate_params(params: &[String]) -> Result<(), RadrootsNostrConnectError> {
+ if params.len() > REQUEST_PARAM_COUNT_MAX {
+ return Err(RadrootsNostrConnectError::InvalidRequestPayload {
+ method: "custom".to_owned(),
+ reason: "parameter count exceeds its limit".to_owned(),
+ });
+ }
+ if params
+ .iter()
+ .any(|param| param.len() > REQUEST_PARAM_MAX_BYTES)
+ {
+ return Err(RadrootsNostrConnectError::InvalidRequestPayload {
+ method: "unknown".to_owned(),
+ reason: "a parameter exceeds its byte limit".to_owned(),
+ });
+ }
+ if params.iter().map(String::len).sum::<usize>() > REQUEST_PARAMS_MAX_BYTES {
+ return Err(RadrootsNostrConnectError::InvalidRequestPayload {
+ method: "unknown".to_owned(),
+ reason: "serialized parameters exceed their byte limit".to_owned(),
+ });
+ }
+ Ok(())
+}
+
+fn validate_response_fingerprint(value: &str) -> Result<(), RadrootsNostrConnectError> {
+ if value.is_empty() || value.len() > REQUEST_ID_MAX_BYTES || value.chars().any(char::is_control)
+ {
+ return Err(RadrootsNostrConnectError::InvalidResponseEnvelope {
+ reason: "response fingerprint must be non-empty, bounded, and control-free",
+ });
+ }
+ Ok(())
+}
+
fn expect_param_count(
method: &Method,
params: &[String],
@@ -573,12 +1032,12 @@ fn expect_param_count(
}
fn parse_public_key(value: &str) -> Result<PublicKey, RadrootsNostrConnectError> {
- PublicKey::parse(value)
- .or_else(|_| PublicKey::from_hex(value))
- .map_err(|error| RadrootsNostrConnectError::InvalidPublicKey {
+ radroots_nostr::key::parse_public_key(value).map_err(|error| {
+ RadrootsNostrConnectError::InvalidPublicKey {
value: value.to_owned(),
reason: error.to_string(),
- })
+ }
+ })
}
fn expect_string_result(
@@ -589,7 +1048,7 @@ fn expect_string_result(
Some(Value::String(value)) => Ok(value),
Some(other) => Err(RadrootsNostrConnectError::InvalidResponsePayload {
method: method.to_string(),
- reason: format!("expected string result, got {other}"),
+ reason: format!("expected string result, got {}", json_type(&other)),
}),
None => Err(RadrootsNostrConnectError::MissingResult),
}
@@ -619,19 +1078,33 @@ where
}
}
+fn expect_json_string_or_value(
+ method: &Method,
+ result: Option<Value>,
+) -> Result<String, RadrootsNostrConnectError> {
+ match result {
+ Some(Value::String(value)) => Ok(value),
+ Some(value) => serde_json::to_string(&value).map_err(|error| {
+ RadrootsNostrConnectError::InvalidResponsePayload {
+ method: method.to_string(),
+ reason: error.to_string(),
+ }
+ }),
+ None => Err(RadrootsNostrConnectError::MissingResult),
+ }
+}
+
fn parse_switch_relays_response(
result: Option<Value>,
-) -> Result<RadrootsNostrConnectResponse, RadrootsNostrConnectError> {
+) -> Result<Response, RadrootsNostrConnectError> {
let method = Method::SwitchRelays;
match result {
- None | Some(Value::Null) => Ok(RadrootsNostrConnectResponse::RelayListUnchanged),
+ None | Some(Value::Null) => Ok(Response::RelayListUnchanged),
Some(Value::Array(values)) => {
let relays = parse_relay_values(values)?;
- Ok(RadrootsNostrConnectResponse::RelayList(relays))
- }
- Some(Value::String(value)) if value == "null" => {
- Ok(RadrootsNostrConnectResponse::RelayListUnchanged)
+ Ok(Response::RelayList(relays))
}
+ Some(Value::String(value)) if value == "null" => Ok(Response::RelayListUnchanged),
Some(Value::String(value)) => {
let parsed = serde_json::from_str::<Value>(&value).map_err(|error| {
RadrootsNostrConnectError::InvalidResponsePayload {
@@ -643,7 +1116,7 @@ fn parse_switch_relays_response(
}
Some(other) => Err(RadrootsNostrConnectError::InvalidResponsePayload {
method: method.to_string(),
- reason: format!("expected relay list or null, got {other}"),
+ reason: format!("expected relay list or null, got {}", json_type(&other)),
}),
}
}
@@ -652,25 +1125,42 @@ fn parse_relay_values(values: Vec<Value>) -> Result<Vec<RelayUrl>, RadrootsNostr
values
.into_iter()
.map(|value| match value {
- Value::String(value) => RelayUrl::parse(&value).map_err(|error| {
- RadrootsNostrConnectError::InvalidRelayUrl {
- value,
- reason: error.to_string(),
- }
- }),
+ Value::String(value) => RelayUrl::parse(&value),
other => Err(RadrootsNostrConnectError::InvalidResponsePayload {
method: Method::SwitchRelays.to_string(),
- reason: format!("expected relay string, got {other}"),
+ reason: format!("expected relay string, got {}", json_type(&other)),
}),
})
.collect()
}
fn validate_url(value: &str) -> Result<String, RadrootsNostrConnectError> {
- Url::parse(value)
- .map(|url| url.to_string())
- .map_err(|error| RadrootsNostrConnectError::InvalidUrl {
- value: value.to_owned(),
- reason: error.to_string(),
- })
+ if value.len() > crate::uri::CLIENT_URL_MAX_BYTES || value.chars().any(char::is_control) {
+ return Err(RadrootsNostrConnectError::InvalidUrl {
+ value: "[redacted auth URL]".to_owned(),
+ reason: "auth URL is oversized or contains control characters".to_owned(),
+ });
+ }
+ let url = Url::parse(value).map_err(|error| RadrootsNostrConnectError::InvalidUrl {
+ value: "[redacted auth URL]".to_owned(),
+ reason: error.to_string(),
+ })?;
+ if !matches!(url.scheme(), "http" | "https") {
+ return Err(RadrootsNostrConnectError::InvalidUrl {
+ value: "[redacted auth URL]".to_owned(),
+ reason: "auth URL scheme must be http or https".to_owned(),
+ });
+ }
+ Ok(url.to_string())
+}
+
+fn json_type(value: &Value) -> &'static str {
+ match value {
+ Value::Null => "null",
+ Value::Bool(_) => "boolean",
+ Value::Number(_) => "number",
+ Value::String(_) => "string",
+ Value::Array(_) => "array",
+ Value::Object(_) => "object",
+ }
}
diff --git a/crates/nostr_connect/tests/client.rs b/crates/nostr_connect/tests/client.rs
@@ -3,7 +3,8 @@ mod test_fixtures;
use nostr::nips::nip44::{self, Version};
use nostr::{
- Event, EventBuilder, Keys, Kind, PublicKey, RelayUrl, SecretKey, Tag, Timestamp, UnsignedEvent,
+ Event, EventBuilder, JsonUtil, Keys, Kind, PublicKey, RelayUrl, SecretKey, Tag, Timestamp,
+ UnsignedEvent,
};
use radroots_nostr_connect::prelude::{
Method, RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectClientEventOutcome,
@@ -11,7 +12,8 @@ use radroots_nostr_connect::prelude::{
RadrootsNostrConnectClientTarget, RadrootsNostrConnectClientTransport,
RadrootsNostrConnectClientTransportFuture, RadrootsNostrConnectError,
RadrootsNostrConnectRemoteSessionCapability, RadrootsNostrConnectRequest,
- RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, build_request_event,
+ RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
+ SignedEvent as ConnectSignedEvent, UnsignedEvent as ConnectUnsignedEvent, build_request_event,
execute_request_with_transport, parse_response_event,
};
use std::collections::VecDeque;
@@ -38,6 +40,10 @@ fn relay() -> RelayUrl {
RelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay")
}
+fn identity_public_key(public_key: PublicKey) -> radroots_identity::PublicKey {
+ radroots_nostr::key::public_key_from_nostr(public_key).expect("identity public key")
+}
+
fn target(remote_keys: &Keys) -> RadrootsNostrConnectClientTarget {
RadrootsNostrConnectClientTarget::new(remote_keys.public_key(), vec![relay()])
}
@@ -104,8 +110,10 @@ fn untagged_response_event(
fn remote_session_capability(remote_keys: &Keys) -> RadrootsNostrConnectRemoteSessionCapability {
RadrootsNostrConnectRemoteSessionCapability {
- user_public_key: remote_keys.public_key(),
- relays: vec![relay()],
+ user_public_key: identity_public_key(remote_keys.public_key()),
+ relays: vec![
+ radroots_nostr_connect::uri::RelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay"),
+ ],
permissions: Vec::new().into(),
}
}
@@ -159,7 +167,7 @@ async fn executes_connect_request_and_secret_echo_response() {
RadrootsNostrConnectClientRequest::new(
"req-connect",
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: remote_keys.public_key(),
+ remote_signer_public_key: identity_public_key(remote_keys.public_key()),
secret: Some("connect-secret".to_owned()),
requested_permissions: Vec::new().into(),
client_metadata: None,
@@ -321,7 +329,9 @@ async fn executes_request_through_transport_with_auth_progress() {
&remote_keys,
client_keys.public_key(),
"req-sign",
- RadrootsNostrConnectResponse::SignedEvent(signed.clone()),
+ RadrootsNostrConnectResponse::SignedEvent(
+ ConnectSignedEvent::from_json(&signed.as_json()).expect("signed event payload"),
+ ),
),
];
let mut transport = MockTransport::new(inbound);
@@ -332,7 +342,12 @@ async fn executes_request_through_transport_with_auth_progress() {
&target,
RadrootsNostrConnectClientRequest::new(
"req-sign",
- RadrootsNostrConnectRequest::SignEvent(unsigned_event(remote_keys.public_key())),
+ RadrootsNostrConnectRequest::SignEvent(
+ ConnectUnsignedEvent::from_json(
+ &unsigned_event(remote_keys.public_key()).as_json(),
+ )
+ .expect("unsigned event payload"),
+ ),
),
&mut transport,
|event| {
@@ -350,7 +365,12 @@ async fn executes_request_through_transport_with_auth_progress() {
url: "https://auth.example.com/challenge".to_owned()
}]
);
- assert_eq!(response, RadrootsNostrConnectResponse::SignedEvent(signed));
+ assert_eq!(
+ response,
+ RadrootsNostrConnectResponse::SignedEvent(
+ ConnectSignedEvent::from_json(&signed.as_json()).expect("signed event payload")
+ )
+ );
}
#[tokio::test]
diff --git a/crates/nostr_connect/tests/coverage.rs b/crates/nostr_connect/tests/coverage.rs
@@ -1,14 +1,16 @@
#[path = "../src/test_fixtures.rs"]
mod test_fixtures;
-use nostr::{Event, EventBuilder, Keys, PublicKey, RelayUrl, SecretKey, Timestamp, UnsignedEvent};
+use nostr::{Event, EventBuilder, JsonUtil, Keys, SecretKey, Timestamp, UnsignedEvent};
use radroots_nostr_connect::prelude::{
CLIENT_URL_MAX_BYTES, ClientMetadata, Method, Permission, Permissions,
RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR, RadrootsNostrConnectError,
RadrootsNostrConnectPendingConnectionPollOutcome, RadrootsNostrConnectRequest,
RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
- RadrootsNostrConnectResponseEnvelope, Uri,
+ RadrootsNostrConnectResponseEnvelope, SignedEvent as ConnectSignedEvent,
+ UnsignedEvent as ConnectUnsignedEvent, Uri,
};
+use radroots_nostr_connect::uri::RelayUrl;
use serde_json::{Value, json};
use std::str::FromStr;
use test_fixtures::{
@@ -16,8 +18,8 @@ use test_fixtures::{
RELAY_TERTIARY_WSS,
};
-fn test_public_key() -> PublicKey {
- PublicKey::parse(FIXTURE_ALICE.public_key_hex).expect("public key")
+fn test_public_key() -> radroots_identity::PublicKey {
+ radroots_identity::PublicKey::from_hex(FIXTURE_ALICE.public_key_hex).expect("public key")
}
fn test_keys() -> Keys {
@@ -433,7 +435,10 @@ fn request_surface_covers_variant_methods_serialization_and_validation() {
Vec::new(),
),
(
- RadrootsNostrConnectRequest::SignEvent(unsigned_event()),
+ RadrootsNostrConnectRequest::SignEvent(
+ ConnectUnsignedEvent::from_json(&unsigned_event().as_json())
+ .expect("unsigned event payload"),
+ ),
Method::SignEvent,
vec![serde_json::to_string(&unsigned_event()).expect("serialize unsigned event")],
),
@@ -751,9 +756,13 @@ fn response_surface_covers_success_and_error_paths() {
),
),
(
- RadrootsNostrConnectResponse::SignedEvent(event.clone()),
+ RadrootsNostrConnectResponse::SignedEvent(
+ ConnectSignedEvent::from_json(&event.as_json()).expect("signed event payload"),
+ ),
Method::SignEvent,
- RadrootsNostrConnectResponse::SignedEvent(event.clone()),
+ RadrootsNostrConnectResponse::SignedEvent(
+ ConnectSignedEvent::from_json(&event.as_json()).expect("signed event payload"),
+ ),
),
(
RadrootsNostrConnectResponse::Pong,
@@ -978,7 +987,9 @@ fn response_surface_covers_success_and_error_paths() {
},
)
.expect("parse object event"),
- RadrootsNostrConnectResponse::SignedEvent(event)
+ RadrootsNostrConnectResponse::SignedEvent(
+ ConnectSignedEvent::from_json(&event.as_json()).expect("signed event payload")
+ )
);
assert_eq!(
RadrootsNostrConnectResponse::from_envelope(
@@ -1007,7 +1018,7 @@ fn response_surface_covers_success_and_error_paths() {
assert!(matches!(
RadrootsNostrConnectResponse::AuthUrl("not-a-url".to_owned()).into_envelope("req"),
- Err(RadrootsNostrConnectError::InvalidUrl { value, .. }) if value == "not-a-url"
+ Err(RadrootsNostrConnectError::InvalidUrl { value, .. }) if value == "[redacted auth URL]"
));
assert!(matches!(
RadrootsNostrConnectResponse::from_envelope(
@@ -1018,7 +1029,7 @@ fn response_surface_covers_success_and_error_paths() {
error: Some("not-a-url".to_owned()),
},
),
- Err(RadrootsNostrConnectError::InvalidUrl { value, .. }) if value == "not-a-url"
+ Err(RadrootsNostrConnectError::InvalidUrl { value, .. }) if value == "[redacted auth URL]"
));
assert!(matches!(
RadrootsNostrConnectResponse::from_envelope(
@@ -1281,6 +1292,6 @@ fn pending_connection_poll_outcome_uses_typed_variants() {
assert!(matches!(
RadrootsNostrConnectResponse::Pong.into_pending_connection_poll_outcome(),
RadrootsNostrConnectPendingConnectionPollOutcome::UnexpectedResponse { response }
- if response == "Pong"
+ if response == "pong"
));
}
diff --git a/crates/nostr_connect/tests/message_contract.rs b/crates/nostr_connect/tests/message_contract.rs
@@ -0,0 +1,190 @@
+use nostr::{Keys, SecretKey};
+use radroots_identity::PublicKey as IdentityPublicKey;
+use radroots_nostr_connect::message::{
+ PendingConnectionOutcome, REMOTE_CAPABILITY_RELAY_COUNT_MAX, REQUEST_ID_MAX_BYTES,
+ RESPONSE_ERROR_MAX_BYTES, RemoteSessionCapability, RequestId, RequestMessage, ResponseEnvelope,
+ ResponseValidator,
+};
+use radroots_nostr_connect::uri::RelayUrl;
+use radroots_nostr_connect::{Error, Method, Request, Response};
+use serde_json::{Value, json};
+use std::str::FromStr;
+
+fn keys(secret_hex: &str) -> Keys {
+ Keys::new(SecretKey::from_hex(secret_hex).expect("secret key"))
+}
+
+fn identity_key(secret_hex: &str) -> IdentityPublicKey {
+ radroots_nostr::key::public_key_from_nostr(keys(secret_hex).public_key())
+ .expect("identity public key")
+}
+
+#[test]
+fn request_and_response_round_trip_with_bounded_correlation() {
+ let request = RequestMessage::try_new("request-1", Request::Ping).expect("valid request");
+ let encoded = serde_json::to_string(&request).expect("serialize request");
+ assert_eq!(
+ serde_json::from_str::<RequestMessage>(&encoded).expect("deserialize request"),
+ request
+ );
+
+ let response = Response::Pong
+ .into_envelope("request-1")
+ .expect("valid response");
+ assert_eq!(
+ request.correlate(response).expect("correlated response"),
+ Response::Pong
+ );
+
+ assert!(matches!(
+ RequestId::parse(""),
+ Err(Error::InvalidRequestId { .. })
+ ));
+ assert!(matches!(
+ RequestId::parse(" request-1"),
+ Err(Error::InvalidRequestId { .. })
+ ));
+ assert!(matches!(
+ RequestId::parse("x".repeat(REQUEST_ID_MAX_BYTES + 1)),
+ Err(Error::InvalidRequestId { .. })
+ ));
+}
+
+#[test]
+fn correlation_rejects_wrong_id_wrong_signer_and_replay() {
+ const SIGNER: &str = "0000000000000000000000000000000000000000000000000000000000000001";
+ const OTHER: &str = "0000000000000000000000000000000000000000000000000000000000000002";
+ let request = RequestMessage::try_new("request-2", Request::Ping).expect("request");
+ let wrong_id = Response::Pong.into_envelope("request-3").expect("response");
+ assert_eq!(
+ request.correlate(wrong_id).expect_err("wrong id"),
+ Error::WrongRequestId
+ );
+
+ let envelope = Response::Pong.into_envelope("request-2").expect("response");
+ let mut validator = ResponseValidator::new(
+ RequestId::parse("request-2").expect("request id"),
+ identity_key(SIGNER),
+ );
+ assert_eq!(
+ validator
+ .validate(identity_key(OTHER), "event-1", &envelope)
+ .expect_err("wrong signer"),
+ Error::WrongResponseSigner
+ );
+ validator
+ .validate(identity_key(SIGNER), "event-1", &envelope)
+ .expect("first response");
+ assert_eq!(
+ validator
+ .validate(identity_key(SIGNER), "event-1", &envelope)
+ .expect_err("replay"),
+ Error::ReplayedResponse
+ );
+}
+
+#[test]
+fn malformed_envelopes_and_unsafe_auth_challenges_fail_closed() {
+ assert!(matches!(
+ ResponseEnvelope::try_new("request-3", None, Some(String::new())),
+ Err(Error::InvalidResponseEnvelope { .. })
+ ));
+ assert!(matches!(
+ ResponseEnvelope::try_new(
+ "request-3",
+ None,
+ Some("x".repeat(RESPONSE_ERROR_MAX_BYTES + 1)),
+ ),
+ Err(Error::InvalidResponseEnvelope { .. })
+ ));
+ assert!(
+ serde_json::from_value::<ResponseEnvelope>(json!({
+ "id": "request-3",
+ "result": "pong",
+ "unexpected": true,
+ }))
+ .is_err()
+ );
+ assert!(matches!(
+ Response::AuthUrl("file:///tmp/approval".to_owned()).into_envelope("request-3"),
+ Err(Error::InvalidUrl { value, .. }) if value == "[redacted auth URL]"
+ ));
+}
+
+#[test]
+fn unknown_methods_round_trip_only_when_canonical() {
+ let method = Method::from_str("vendor_action").expect("canonical extension method");
+ let request = RequestMessage::try_new(
+ "request-custom",
+ Request::Custom {
+ method: method.clone(),
+ params: vec!["alpha".to_owned()],
+ },
+ )
+ .expect("custom request");
+ let encoded = serde_json::to_value(&request).expect("serialize custom request");
+ assert_eq!(encoded["method"], Value::String("vendor_action".to_owned()));
+ assert_eq!(
+ serde_json::from_value::<RequestMessage>(encoded)
+ .expect("deserialize custom request")
+ .payload()
+ .method(),
+ method
+ );
+ assert!(Method::from_str("Vendor-Action").is_err());
+}
+
+#[test]
+fn remote_capabilities_and_pending_outcomes_are_bounded_and_typed() {
+ let relay = RelayUrl::parse("wss://relay.example.test").expect("relay");
+ let capability = RemoteSessionCapability::try_new(
+ identity_key("0000000000000000000000000000000000000000000000000000000000000003"),
+ vec![relay.clone()],
+ Default::default(),
+ )
+ .expect("capability");
+ let response = Response::RemoteSessionCapability(capability.clone());
+ assert_eq!(
+ response.into_pending_connection_poll_outcome(),
+ PendingConnectionOutcome::ApprovedCapability(capability)
+ );
+ assert!(matches!(
+ RemoteSessionCapability::try_new(
+ identity_key("0000000000000000000000000000000000000000000000000000000000000003"),
+ vec![relay; REMOTE_CAPABILITY_RELAY_COUNT_MAX + 1],
+ Default::default(),
+ ),
+ Err(Error::InvalidResponsePayload { .. })
+ ));
+}
+
+#[test]
+fn diagnostics_redact_protocol_payloads() {
+ const SECRET: &str = "do-not-log-connect-secret";
+ let request = Request::Connect {
+ remote_signer_public_key: identity_key(
+ "0000000000000000000000000000000000000000000000000000000000000004",
+ ),
+ secret: Some(SECRET.to_owned()),
+ requested_permissions: Default::default(),
+ client_metadata: None,
+ };
+ assert!(!format!("{request:?}").contains(SECRET));
+
+ let envelope = ResponseEnvelope::try_new(
+ "request-redacted",
+ Some(Value::String(SECRET.to_owned())),
+ None,
+ )
+ .expect("response envelope");
+ assert!(!format!("{envelope:?}").contains(SECRET));
+
+ let response = Response::ConnectSecretEcho(SECRET.to_owned());
+ assert!(!format!("{response:?}").contains(SECRET));
+ assert_eq!(
+ response.into_pending_connection_poll_outcome(),
+ PendingConnectionOutcome::UnexpectedResponse {
+ response: "connect_secret_echo".to_owned(),
+ }
+ );
+}
diff --git a/crates/nostr_connect/tests/protocol.rs b/crates/nostr_connect/tests/protocol.rs
@@ -1,13 +1,15 @@
#[path = "../src/test_fixtures.rs"]
mod test_fixtures;
-use nostr::{EventBuilder, Keys, PublicKey, RelayUrl, SecretKey, Timestamp, UnsignedEvent};
+use nostr::{EventBuilder, JsonUtil, Keys, PublicKey, SecretKey, Timestamp, UnsignedEvent};
use radroots_nostr_connect::prelude::{
CLIENT_METADATA_JSON_MAX_BYTES, CLIENT_NAME_MAX_BYTES, ClientMetadata, Method, Permission,
Permissions, RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR, RadrootsNostrConnectError,
RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
- RadrootsNostrConnectResponseEnvelope, Uri,
+ RadrootsNostrConnectResponseEnvelope, SignedEvent as ConnectSignedEvent,
+ UnsignedEvent as ConnectUnsignedEvent, Uri,
};
+use radroots_nostr_connect::uri::RelayUrl as ConnectRelayUrl;
use serde_json::{Value, json};
use test_fixtures::{
APP_PRIMARY_HTTPS, CDN_PRIMARY_HTTPS, FIXTURE_ALICE, RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS,
@@ -39,10 +41,10 @@ fn logo_url() -> String {
fn remote_session_capability()
-> radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability {
radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability {
- user_public_key: test_public_key(),
+ user_public_key: test_identity_public_key(),
relays: vec![
- RelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay 1"),
- RelayUrl::parse(RELAY_SECONDARY_WSS).expect("relay 2"),
+ ConnectRelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay 1"),
+ ConnectRelayUrl::parse(RELAY_SECONDARY_WSS).expect("relay 2"),
],
permissions: Permissions::from(vec![
Permission::new(Method::Ping),
@@ -128,7 +130,7 @@ fn requested_permissions_roundtrip_as_csv() {
#[test]
fn connect_request_roundtrips_requested_permissions() {
let request = RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: test_public_key(),
+ remote_signer_public_key: test_identity_public_key(),
secret: Some("abcd".to_owned()),
requested_permissions: Permissions::from(vec![
Permission::new(Method::Nip44Encrypt),
@@ -159,7 +161,7 @@ fn connect_request_roundtrips_requested_permissions() {
#[test]
fn connect_request_roundtrips_client_metadata_in_fourth_parameter() {
let request = RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: test_public_key(),
+ remote_signer_public_key: test_identity_public_key(),
secret: None,
requested_permissions: Permissions::default(),
client_metadata: Some(ClientMetadata {
@@ -295,7 +297,10 @@ fn sign_event_request_roundtrips_unsigned_event_payload() {
let message = RadrootsNostrConnectRequestMessage::new(
"req-sign",
- RadrootsNostrConnectRequest::SignEvent(unsigned_event.clone()),
+ RadrootsNostrConnectRequest::SignEvent(
+ ConnectUnsignedEvent::from_json(&unsigned_event.as_json())
+ .expect("unsigned event payload"),
+ ),
);
let encoded = serde_json::to_value(&message).expect("serialize sign request");
assert_eq!(encoded["method"], "sign_event");
@@ -305,7 +310,10 @@ fn sign_event_request_roundtrips_unsigned_event_payload() {
assert_eq!(decoded, message);
assert_eq!(
decoded.request,
- RadrootsNostrConnectRequest::SignEvent(unsigned_event)
+ RadrootsNostrConnectRequest::SignEvent(
+ ConnectUnsignedEvent::from_json(&unsigned_event.as_json())
+ .expect("unsigned event payload"),
+ )
);
}
@@ -322,8 +330,8 @@ fn switch_relays_response_accepts_array_or_null() {
assert_eq!(
parsed,
RadrootsNostrConnectResponse::RelayList(vec![
- RelayUrl::parse(RELAY_SECONDARY_WSS).expect("relay 1"),
- RelayUrl::parse(RELAY_TERTIARY_WSS).expect("relay 2"),
+ ConnectRelayUrl::parse(RELAY_SECONDARY_WSS).expect("relay 1"),
+ ConnectRelayUrl::parse(RELAY_TERTIARY_WSS).expect("relay 2"),
])
);
@@ -407,13 +415,20 @@ fn sign_event_response_roundtrips_signed_event_json_string() {
.sign_with_keys(&keys)
.expect("sign event");
- let envelope = RadrootsNostrConnectResponse::SignedEvent(event.clone())
- .into_envelope("req-sign")
- .expect("serialize response");
+ let envelope = RadrootsNostrConnectResponse::SignedEvent(
+ ConnectSignedEvent::from_json(&event.as_json()).expect("signed event payload"),
+ )
+ .into_envelope("req-sign")
+ .expect("serialize response");
let parsed = RadrootsNostrConnectResponse::from_envelope(&Method::SignEvent, envelope)
.expect("parse signed event response");
- assert_eq!(parsed, RadrootsNostrConnectResponse::SignedEvent(event));
+ assert_eq!(
+ parsed,
+ RadrootsNostrConnectResponse::SignedEvent(
+ ConnectSignedEvent::from_json(&event.as_json()).expect("signed event payload")
+ )
+ );
}
#[test]
diff --git a/crates/nostr_signer/src/backend.rs b/crates/nostr_signer/src/backend.rs
@@ -1303,7 +1303,7 @@ mod tests {
.evaluate_connect_request(
synthetic_public_key(0x93),
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: identity.public_key(),
+ remote_signer_public_key: embedded_public_identity(&identity).public_key(),
secret: Some("connect-secret".into()),
requested_permissions: vec![RadrootsNostrConnectPermission::new(
RadrootsNostrConnectMethod::Ping,
@@ -1372,7 +1372,7 @@ mod tests {
.evaluate_connect_request(
synthetic_public_key(0xa1),
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: identity.public_key(),
+ remote_signer_public_key: embedded_public_identity(&identity).public_key(),
secret: Some("connect-secret-2".into()),
requested_permissions: vec![RadrootsNostrConnectPermission::new(
RadrootsNostrConnectMethod::Ping,
diff --git a/crates/nostr_signer/src/evaluation.rs b/crates/nostr_signer/src/evaluation.rs
@@ -4,13 +4,14 @@ use crate::model::{
RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerPendingRequest,
RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerRequestId,
};
-use nostr::{PublicKey, RelayUrl};
+use nostr::PublicKey;
use radroots_identity::PublicIdentity;
use radroots_nostr_connect::prelude::{
RadrootsNostrConnectClientMetadata, RadrootsNostrConnectMethod, RadrootsNostrConnectPermission,
RadrootsNostrConnectPermissions, RadrootsNostrConnectRemoteSessionCapability,
RadrootsNostrConnectRequest,
};
+use radroots_nostr_connect::uri::RelayUrl as ConnectRelayUrl;
#[derive(Debug, Clone)]
pub enum RadrootsNostrSignerSessionLookup {
@@ -37,9 +38,9 @@ pub enum RadrootsNostrSignerConnectEvaluation {
pub enum RadrootsNostrSignerRequestResponseHint {
None,
Pong,
- UserPublicKey(PublicKey),
+ UserPublicKey(radroots_identity::PublicKey),
RemoteSessionCapability(RadrootsNostrConnectRemoteSessionCapability),
- RelayList(Vec<RelayUrl>),
+ RelayList(Vec<ConnectRelayUrl>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -115,7 +116,7 @@ pub(crate) fn required_permission_for_request(
RadrootsNostrConnectRequest::SignEvent(unsigned_event) => {
Some(RadrootsNostrConnectPermission::with_parameter(
RadrootsNostrConnectMethod::SignEvent,
- format!("kind:{}", unsigned_event.kind.as_u16()),
+ format!("kind:{}", unsigned_event.kind()),
))
}
RadrootsNostrConnectRequest::Nip04Encrypt { .. } => Some(
@@ -167,15 +168,25 @@ pub(crate) fn response_hint_for_request(
RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability(
RadrootsNostrConnectRemoteSessionCapability {
user_public_key: identity_public_key(&connection.user_identity)?,
- relays: connection.relays.clone(),
+ relays: connection
+ .relays
+ .iter()
+ .map(|relay| ConnectRelayUrl::parse(&relay.to_string()))
+ .collect::<Result<Vec<_>, _>>()?,
permissions: connection.effective_permissions(),
},
),
),
RadrootsNostrConnectRequest::Ping => Ok(RadrootsNostrSignerRequestResponseHint::Pong),
- RadrootsNostrConnectRequest::SwitchRelays => Ok(
- RadrootsNostrSignerRequestResponseHint::RelayList(connection.relays.clone()),
- ),
+ RadrootsNostrConnectRequest::SwitchRelays => {
+ Ok(RadrootsNostrSignerRequestResponseHint::RelayList(
+ connection
+ .relays
+ .iter()
+ .map(|relay| ConnectRelayUrl::parse(&relay.to_string()))
+ .collect::<Result<Vec<_>, _>>()?,
+ ))
+ }
_ => Ok(RadrootsNostrSignerRequestResponseHint::None),
}
}
@@ -207,10 +218,10 @@ fn sign_event_kind_suffix(value: &str) -> &str {
value.strip_prefix("kind:").unwrap_or(value)
}
-fn identity_public_key(identity: &PublicIdentity) -> Result<PublicKey, RadrootsNostrSignerError> {
- PublicKey::from_hex(&identity.public_key().to_hex()).map_err(|_| {
- RadrootsNostrSignerError::InvalidState("user identity public key is invalid".into())
- })
+fn identity_public_key(
+ identity: &PublicIdentity,
+) -> Result<radroots_identity::PublicKey, RadrootsNostrSignerError> {
+ Ok(identity.public_key())
}
#[cfg(test)]
@@ -221,21 +232,33 @@ mod tests {
api_primary_https, fixture_alice_identity, fixture_alice_public_key, fixture_bob_identity,
fixture_carol_public_key, fixture_diego_identity, primary_relay, synthetic_public_key,
};
- use nostr::{PublicKey, Timestamp, UnsignedEvent};
+ use nostr::{PublicKey, Timestamp};
+ use radroots_nostr_connect::prelude::UnsignedEvent as ConnectUnsignedEvent;
use serde_json::json;
fn public_key(index: u32) -> PublicKey {
synthetic_public_key(index)
}
- fn unsigned_event(kind: u16) -> UnsignedEvent {
- serde_json::from_value(json!({
- "pubkey": fixture_alice_public_key().to_hex(),
- "created_at": Timestamp::from(1).as_secs(),
- "kind": kind,
- "tags": [],
- "content": "hello"
- }))
+ fn connect_public_key(public_key: PublicKey) -> radroots_identity::PublicKey {
+ radroots_nostr::key::public_key_from_nostr(public_key).expect("identity public key")
+ }
+
+ fn connect_relay(relay: nostr::RelayUrl) -> ConnectRelayUrl {
+ ConnectRelayUrl::parse(&relay.to_string()).expect("connect relay")
+ }
+
+ fn unsigned_event(kind: u16) -> ConnectUnsignedEvent {
+ ConnectUnsignedEvent::from_json(
+ &json!({
+ "pubkey": fixture_alice_public_key().to_hex(),
+ "created_at": Timestamp::from(1).as_secs(),
+ "kind": kind,
+ "tags": [],
+ "content": "hello"
+ })
+ .to_string(),
+ )
.expect("unsigned event")
}
@@ -288,14 +311,9 @@ mod tests {
) {
match hint {
RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability(capability) => {
- let expected_public_key =
- PublicKey::from_hex(&fixture_diego_identity().public_key().to_hex())
- .expect("user public key");
- assert_eq!(
- capability.user_public_key.to_hex(),
- expected_public_key.to_hex()
- );
- assert_eq!(capability.relays, vec![primary_relay()]);
+ let expected_public_key = fixture_diego_identity().public_key();
+ assert_eq!(capability.user_public_key, expected_public_key);
+ assert_eq!(capability.relays, vec![connect_relay(primary_relay())]);
assert_eq!(capability.permissions, expected_permissions);
}
other => panic!("unexpected response hint: {other:?}"),
@@ -420,7 +438,7 @@ mod tests {
assert!(!request_allowed_by_permissions(
&vec![sign_kind, nip44].into(),
&RadrootsNostrConnectRequest::Nip04Encrypt {
- public_key: public_key(7),
+ public_key: connect_public_key(public_key(7)),
plaintext: "hello".into(),
},
));
@@ -461,7 +479,7 @@ mod tests {
let connection = connection();
let public_key = public_key(8);
let connect = RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: public_key,
+ remote_signer_public_key: connect_public_key(public_key),
secret: Some("secret".into()),
requested_permissions: RadrootsNostrConnectPermissions::default(),
client_metadata: None,
@@ -483,7 +501,7 @@ mod tests {
assert!(required_permission_for_request(&get_session_capability).is_none());
assert_eq!(
required_permission_for_request(&RadrootsNostrConnectRequest::Nip04Decrypt {
- public_key,
+ public_key: connect_public_key(public_key),
ciphertext: "cipher".into(),
})
.expect("nip04 decrypt permission")
@@ -492,7 +510,7 @@ mod tests {
);
assert_eq!(
required_permission_for_request(&RadrootsNostrConnectRequest::Nip44Encrypt {
- public_key,
+ public_key: connect_public_key(public_key),
plaintext: "hello".into(),
})
.expect("nip44 encrypt permission")
@@ -501,7 +519,7 @@ mod tests {
);
assert_eq!(
required_permission_for_request(&RadrootsNostrConnectRequest::Nip44Decrypt {
- public_key,
+ public_key: connect_public_key(public_key),
ciphertext: "cipher".into(),
})
.expect("nip44 decrypt permission")
@@ -531,7 +549,7 @@ mod tests {
response_hint_for_request(
&connection,
&RadrootsNostrConnectRequest::Nip04Decrypt {
- public_key,
+ public_key: connect_public_key(public_key),
ciphertext: "cipher".into(),
},
)
@@ -550,7 +568,7 @@ mod tests {
);
assert_eq!(
response_hint_for_request(&connection, &switch_relays).expect("relay hint"),
- RadrootsNostrSignerRequestResponseHint::RelayList(vec![primary_relay()])
+ RadrootsNostrSignerRequestResponseHint::RelayList(vec![connect_relay(primary_relay())])
);
}
}
diff --git a/crates/nostr_signer/src/manager.rs b/crates/nostr_signer/src/manager.rs
@@ -210,6 +210,8 @@ impl RadrootsNostrSignerManager {
));
};
+ let remote_signer_public_key =
+ radroots_nostr::key::public_key_to_nostr(remote_signer_public_key)?;
let (connect_secret, existing_connection) =
self.resolve_connect_request_context(remote_signer_public_key, secret)?;
if let Some(connection) = existing_connection {
@@ -1214,9 +1216,11 @@ mod tests {
api_primary_https, fixture_alice_identity, primary_relay, secondary_relay,
synthetic_public_identity, synthetic_public_key, tertiary_relay,
};
- use nostr::{PublicKey, Timestamp, UnsignedEvent};
+ use nostr::{PublicKey, Timestamp};
use radroots_identity::PublicIdentity;
- use radroots_nostr_connect::prelude::RadrootsNostrConnectPermission;
+ use radroots_nostr_connect::prelude::{
+ RadrootsNostrConnectPermission, UnsignedEvent as ConnectUnsignedEvent,
+ };
use serde_json::json;
use std::sync::Arc;
use std::thread;
@@ -1229,6 +1233,10 @@ mod tests {
synthetic_public_key(index)
}
+ fn connect_public_key(public_key: PublicKey) -> radroots_identity::PublicKey {
+ radroots_nostr::key::public_key_from_nostr(public_key).expect("identity public key")
+ }
+
fn permission(
method: RadrootsNostrConnectMethod,
parameter: Option<&str>,
@@ -1253,14 +1261,17 @@ mod tests {
RadrootsNostrConnectRequestMessage::new(id, request)
}
- fn unsigned_event(kind: u16) -> UnsignedEvent {
- serde_json::from_value(json!({
- "pubkey": public_key(0xa1).to_hex(),
- "created_at": Timestamp::from(1).as_secs(),
- "kind": kind,
- "tags": [],
- "content": "hello"
- }))
+ fn unsigned_event(kind: u16) -> ConnectUnsignedEvent {
+ ConnectUnsignedEvent::from_json(
+ &json!({
+ "pubkey": public_key(0xa1).to_hex(),
+ "created_at": Timestamp::from(1).as_secs(),
+ "kind": kind,
+ "tags": [],
+ "content": "hello"
+ })
+ .to_string(),
+ )
.expect("unsigned event")
}
@@ -1564,7 +1575,7 @@ mod tests {
RadrootsNostrSignerApprovalState::NotRequired
);
assert_eq!(record.auth_state, RadrootsNostrSignerAuthState::NotRequired);
- assert_eq!(record.requested_permissions.as_slice(), &[sign_event, ping]);
+ assert_eq!(record.requested_permissions.as_slice(), &[ping, sign_event]);
assert_eq!(record.relays, vec![secondary_relay(), primary_relay()]);
}
@@ -3458,10 +3469,7 @@ mod tests {
.evaluate_connect_request(
public_key(0x58),
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: PublicKey::from_hex(
- &signer_identity.public_key().to_hex(),
- )
- .expect("signer public key"),
+ remote_signer_public_key: signer_identity.public_key(),
secret: Some("secret".into()),
requested_permissions: RadrootsNostrConnectPermissions::default(),
client_metadata: None,
@@ -3734,7 +3742,7 @@ mod tests {
.evaluate_connect_request(
client_public_key,
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: signer_public_key,
+ remote_signer_public_key: connect_public_key(signer_public_key),
secret: None,
requested_permissions: RadrootsNostrConnectPermissions::default(),
client_metadata: None,
@@ -3747,7 +3755,7 @@ mod tests {
.evaluate_connect_request(
client_public_key,
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: public_key(0x66),
+ remote_signer_public_key: connect_public_key(public_key(0x66)),
secret: None,
requested_permissions: RadrootsNostrConnectPermissions::default(),
client_metadata: None,
@@ -3764,7 +3772,7 @@ mod tests {
.evaluate_connect_request(
client_public_key,
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: signer_public_key,
+ remote_signer_public_key: connect_public_key(signer_public_key),
secret: Some(" connect-secret ".into()),
requested_permissions: vec![
permission(RadrootsNostrConnectMethod::Ping, None),
@@ -3781,7 +3789,7 @@ mod tests {
.evaluate_connect_request(
public_key(0x67),
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: signer_public_key,
+ remote_signer_public_key: connect_public_key(signer_public_key),
secret: Some(" fresh-secret ".into()),
requested_permissions: vec![
permission(RadrootsNostrConnectMethod::Ping, None),
@@ -3812,8 +3820,8 @@ mod tests {
assert_eq!(
proposal.requested_permissions.as_slice(),
&[
- permission(RadrootsNostrConnectMethod::SignEvent, Some("kind:1")),
permission(RadrootsNostrConnectMethod::Ping, None),
+ permission(RadrootsNostrConnectMethod::SignEvent, Some("kind:1")),
]
);
@@ -3821,7 +3829,7 @@ mod tests {
.evaluate_connect_request(
public_key(0x68),
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: signer_public_key,
+ remote_signer_public_key: connect_public_key(signer_public_key),
secret: Some("connect-secret".into()),
requested_permissions: RadrootsNostrConnectPermissions::default(),
client_metadata: None,
@@ -3952,7 +3960,7 @@ mod tests {
request_message_with_request(
"req-connect",
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: active.client_public_key,
+ remote_signer_public_key: connect_public_key(active.client_public_key),
secret: None,
requested_permissions: RadrootsNostrConnectPermissions::default(),
client_metadata: None,
diff --git a/crates/nostr_signer/src/nip46.rs b/crates/nostr_signer/src/nip46.rs
@@ -1,8 +1,8 @@
-use nostr::{PublicKey as RadrootsNostrPublicKey, RelayUrl as RadrootsNostrRelayUrl};
use nostr::{
- UnsignedEvent,
+ JsonUtil, UnsignedEvent,
filter::{Alphabet, SingleLetterTag},
};
+use nostr::{PublicKey as RadrootsNostrPublicKey, RelayUrl as RadrootsNostrRelayUrl};
use radroots_identity::PublicIdentity;
use radroots_nostr::event::Event as RadrootsNostrEvent;
use radroots_nostr::event::GenericBuilder;
@@ -13,6 +13,7 @@ use radroots_nostr::tag::Tag as RadrootsNostrTag;
use radroots_nostr_connect::prelude::{
RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectError, RadrootsNostrConnectPermissions,
RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
+ SignedEvent as ConnectSignedEvent, UnsignedEvent as ConnectUnsignedEvent,
};
use crate::backend::RadrootsNostrSignerBackend;
@@ -202,13 +203,24 @@ impl<S: RadrootsNostrSignerNip46Signer> RadrootsNostrSignerNip46Codec<S> {
&self,
unsigned_event: UnsignedEvent,
) -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError> {
+ let unsigned_event = ConnectUnsignedEvent::from_json(&unsigned_event.as_json())?;
Ok(self.sign_event_response_value(unsigned_event))
}
fn sign_event_response_value(
&self,
- unsigned_event: UnsignedEvent,
+ unsigned_event: ConnectUnsignedEvent,
) -> RadrootsNostrConnectResponse {
+ let unsigned_event = match serde_json::from_str::<UnsignedEvent>(&unsigned_event.as_json())
+ {
+ Ok(unsigned_event) => unsigned_event,
+ Err(error) => {
+ return RadrootsNostrConnectResponse::Error {
+ result: None,
+ error: format!("invalid sign_event payload: {error}"),
+ };
+ }
+ };
let user_public_key = self.signer.user_identity().public_key().to_hex();
if unsigned_event.pubkey.to_hex() != user_public_key {
return RadrootsNostrConnectResponse::Error {
@@ -218,7 +230,13 @@ impl<S: RadrootsNostrSignerNip46Signer> RadrootsNostrSignerNip46Codec<S> {
}
match self.signer.sign_user_event(unsigned_event) {
- Ok(event) => RadrootsNostrConnectResponse::SignedEvent(event),
+ Ok(event) => match ConnectSignedEvent::from_json(&event.as_json()) {
+ Ok(event) => RadrootsNostrConnectResponse::SignedEvent(event),
+ Err(error) => RadrootsNostrConnectResponse::Error {
+ result: None,
+ error: format!("failed to encode signed event: {error}"),
+ },
+ },
Err(error) => RadrootsNostrConnectResponse::Error {
result: None,
error: format!("failed to sign event: {error}"),
@@ -241,7 +259,9 @@ impl<S: RadrootsNostrSignerNip46Signer> RadrootsNostrSignerNip46Codec<S> {
RadrootsNostrConnectRequest::Nip04Encrypt {
public_key,
plaintext,
- } => match self.signer.nip04_encrypt(&public_key, &plaintext) {
+ } => match nostr_public_key(public_key)
+ .and_then(|public_key| self.signer.nip04_encrypt(&public_key, &plaintext))
+ {
Ok(ciphertext) => RadrootsNostrConnectResponse::Nip04Encrypt(ciphertext),
Err(error) => RadrootsNostrConnectResponse::Error {
result: None,
@@ -251,7 +271,9 @@ impl<S: RadrootsNostrSignerNip46Signer> RadrootsNostrSignerNip46Codec<S> {
RadrootsNostrConnectRequest::Nip04Decrypt {
public_key,
ciphertext,
- } => match self.signer.nip04_decrypt(&public_key, &ciphertext) {
+ } => match nostr_public_key(public_key)
+ .and_then(|public_key| self.signer.nip04_decrypt(&public_key, &ciphertext))
+ {
Ok(plaintext) => RadrootsNostrConnectResponse::Nip04Decrypt(plaintext),
Err(error) => RadrootsNostrConnectResponse::Error {
result: None,
@@ -261,7 +283,9 @@ impl<S: RadrootsNostrSignerNip46Signer> RadrootsNostrSignerNip46Codec<S> {
RadrootsNostrConnectRequest::Nip44Encrypt {
public_key,
plaintext,
- } => match self.signer.nip44_encrypt(&public_key, &plaintext) {
+ } => match nostr_public_key(public_key)
+ .and_then(|public_key| self.signer.nip44_encrypt(&public_key, &plaintext))
+ {
Ok(ciphertext) => RadrootsNostrConnectResponse::Nip44Encrypt(ciphertext),
Err(error) => RadrootsNostrConnectResponse::Error {
result: None,
@@ -271,7 +295,9 @@ impl<S: RadrootsNostrSignerNip46Signer> RadrootsNostrSignerNip46Codec<S> {
RadrootsNostrConnectRequest::Nip44Decrypt {
public_key,
ciphertext,
- } => match self.signer.nip44_decrypt(&public_key, &ciphertext) {
+ } => match nostr_public_key(public_key)
+ .and_then(|public_key| self.signer.nip44_decrypt(&public_key, &ciphertext))
+ {
Ok(plaintext) => RadrootsNostrConnectResponse::Nip44Decrypt(plaintext),
Err(error) => RadrootsNostrConnectResponse::Error {
result: None,
@@ -512,7 +538,7 @@ where
&self,
client_public_key: RadrootsNostrPublicKey,
request_message: RadrootsNostrConnectRequestMessage,
- unsigned_event: UnsignedEvent,
+ unsigned_event: ConnectUnsignedEvent,
) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> {
let connection = match self.lookup_connection(client_public_key)? {
Ok(connection) => connection,
@@ -787,13 +813,21 @@ pub fn response_from_hint(
RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability(capability) => {
RadrootsNostrConnectResponse::RemoteSessionCapability(capability)
}
- RadrootsNostrSignerRequestResponseHint::RelayList(relays) => {
- if relays == connection.relays {
+ RadrootsNostrSignerRequestResponseHint::RelayList(relays) => match connection
+ .relays
+ .iter()
+ .map(|relay| radroots_nostr_connect::uri::RelayUrl::parse(&relay.to_string()))
+ .collect::<Result<Vec<_>, _>>()
+ {
+ Ok(connection_relays) if relays == connection_relays => {
RadrootsNostrConnectResponse::RelayList(relays)
- } else {
- RadrootsNostrConnectResponse::RelayList(connection.relays.clone())
}
- }
+ Ok(connection_relays) => RadrootsNostrConnectResponse::RelayList(connection_relays),
+ Err(error) => RadrootsNostrConnectResponse::Error {
+ result: None,
+ error: format!("invalid connection relay state: {error}"),
+ },
+ },
RadrootsNostrSignerRequestResponseHint::None => RadrootsNostrConnectResponse::Error {
result: None,
error: "request evaluation did not provide a response hint".to_owned(),
@@ -801,6 +835,12 @@ pub fn response_from_hint(
}
}
+fn nostr_public_key(
+ public_key: radroots_identity::PublicKey,
+) -> Result<RadrootsNostrPublicKey, RadrootsNostrSignerError> {
+ radroots_nostr::key::public_key_to_nostr(public_key).map_err(Into::into)
+}
+
pub fn handled_request_for_action<F>(
connection: &RadrootsNostrSignerConnectionRecord,
action: RadrootsNostrSignerRequestAction,
@@ -857,7 +897,7 @@ mod tests {
use crate::store::RadrootsNostrSignerStore;
use crate::test_support::{fixture_alice_identity, fixture_carol_public_key, primary_relay};
use nostr::PublicKey as RadrootsNostrPublicKey;
- use nostr::{Keys, SecretKey, Timestamp, UnsignedEvent};
+ use nostr::{JsonUtil, Keys, SecretKey, Timestamp, UnsignedEvent};
use radroots_identity::{PublicIdentity, PublicKey as IdentityPublicKey};
use radroots_nostr::event::Event as RadrootsNostrEvent;
use radroots_nostr::event::GenericBuilder;
@@ -868,7 +908,9 @@ mod tests {
RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions,
RadrootsNostrConnectRemoteSessionCapability, RadrootsNostrConnectRequest,
RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
+ UnsignedEvent as ConnectUnsignedEvent,
};
+ use radroots_nostr_connect::uri::RelayUrl as ConnectRelayUrl;
use std::sync::{
Arc, RwLock,
atomic::{AtomicBool, Ordering},
@@ -1083,6 +1125,14 @@ mod tests {
)
}
+ fn connect_public_key(public_key: RadrootsNostrPublicKey) -> IdentityPublicKey {
+ radroots_nostr::key::public_key_from_nostr(public_key).expect("identity public key")
+ }
+
+ fn connect_relay(relay: nostr::RelayUrl) -> ConnectRelayUrl {
+ ConnectRelayUrl::parse(&relay.to_string()).expect("connect relay")
+ }
+
fn test_signer_with_options(sign_events: bool, fail_crypto: bool) -> TestSigner {
TestSigner {
signer_identity: keys_from_secret(
@@ -1136,7 +1186,7 @@ mod tests {
RadrootsNostrConnectRequestMessage::new(
"req-connect",
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: signer_public_key,
+ remote_signer_public_key: connect_public_key(signer_public_key),
secret: secret.map(ToOwned::to_owned),
requested_permissions: permissions.into(),
client_metadata: None,
@@ -1173,6 +1223,11 @@ mod tests {
.expect("unsigned event")
}
+ fn connect_unsigned_event(kind: u16) -> ConnectUnsignedEvent {
+ let event = unsigned_user_event(kind);
+ ConnectUnsignedEvent::from_json(&event.as_json()).expect("connect unsigned event")
+ }
+
fn registered_connection(
backend: &RadrootsNostrEmbeddedSignerBackend,
client_public_key: &RadrootsNostrPublicKey,
@@ -1280,7 +1335,7 @@ mod tests {
assert_eq!(
codec
.crypto_response(RadrootsNostrConnectRequest::Nip04Encrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
plaintext: "plain".to_owned(),
})
.expect("nip04 encrypt"),
@@ -1289,7 +1344,7 @@ mod tests {
assert_eq!(
codec
.crypto_response(RadrootsNostrConnectRequest::Nip04Decrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
ciphertext: "cipher".to_owned(),
})
.expect("nip04 decrypt"),
@@ -1298,7 +1353,7 @@ mod tests {
assert_eq!(
codec
.crypto_response(RadrootsNostrConnectRequest::Nip44Encrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
plaintext: "plain44".to_owned(),
})
.expect("nip44 encrypt"),
@@ -1307,7 +1362,7 @@ mod tests {
assert_eq!(
codec
.crypto_response(RadrootsNostrConnectRequest::Nip44Decrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
ciphertext: "cipher44".to_owned(),
})
.expect("nip44 decrypt"),
@@ -1326,19 +1381,19 @@ mod tests {
super::RadrootsNostrSignerNip46Codec::new(test_signer_with_options(false, true));
for request in [
RadrootsNostrConnectRequest::Nip04Encrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
plaintext: "plain".to_owned(),
},
RadrootsNostrConnectRequest::Nip04Decrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
ciphertext: "cipher".to_owned(),
},
RadrootsNostrConnectRequest::Nip44Encrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
plaintext: "plain44".to_owned(),
},
RadrootsNostrConnectRequest::Nip44Decrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
ciphertext: "cipher44".to_owned(),
},
] {
@@ -1513,7 +1568,7 @@ mod tests {
)
.expect("relays")
),
- RadrootsNostrConnectResponse::RelayList(vec![primary_relay()])
+ RadrootsNostrConnectResponse::RelayList(vec![connect_relay(primary_relay())])
);
assert!(matches!(
response_from_outcome(
@@ -1522,7 +1577,7 @@ mod tests {
client_public_key,
request_message(
"req-sign",
- RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)),
+ RadrootsNostrConnectRequest::SignEvent(connect_unsigned_event(1)),
),
)
.expect("sign")
@@ -1537,7 +1592,7 @@ mod tests {
request_message(
"req-nip04-decrypt",
RadrootsNostrConnectRequest::Nip04Decrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
ciphertext: "cipher".to_owned(),
},
),
@@ -1554,7 +1609,7 @@ mod tests {
request_message(
"req-nip44-encrypt",
RadrootsNostrConnectRequest::Nip44Encrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
plaintext: "plain".to_owned(),
},
),
@@ -1597,7 +1652,7 @@ mod tests {
request_message(
"req-denied",
RadrootsNostrConnectRequest::Nip04Decrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
ciphertext: "cipher".to_owned(),
},
),
@@ -1633,7 +1688,7 @@ mod tests {
client_public_key,
request_message(
"req-policy-denied-sign",
- RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)),
+ RadrootsNostrConnectRequest::SignEvent(connect_unsigned_event(1)),
),
)
.expect("policy denied sign");
@@ -1647,7 +1702,7 @@ mod tests {
request_message(
"req-policy-denied-crypto",
RadrootsNostrConnectRequest::Nip44Encrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
plaintext: "plain".to_owned(),
},
),
@@ -1721,9 +1776,9 @@ mod tests {
for request in [
RadrootsNostrConnectRequest::Ping,
- RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)),
+ RadrootsNostrConnectRequest::SignEvent(connect_unsigned_event(1)),
RadrootsNostrConnectRequest::Nip04Decrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
ciphertext: "cipher".to_owned(),
},
] {
@@ -1772,7 +1827,7 @@ mod tests {
client_public_key,
request_message(
"req-allowed-sign",
- RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)),
+ RadrootsNostrConnectRequest::SignEvent(connect_unsigned_event(1)),
),
)
.expect("allowed sign")
@@ -1787,7 +1842,7 @@ mod tests {
request_message(
"req-allowed-nip04-encrypt",
RadrootsNostrConnectRequest::Nip04Encrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
plaintext: "plain".to_owned(),
},
),
@@ -1842,7 +1897,7 @@ mod tests {
let crypto = request_message(
"req-eval-crypto",
RadrootsNostrConnectRequest::Nip44Decrypt {
- public_key: client_public_key,
+ public_key: connect_public_key(client_public_key),
ciphertext: "sealed".to_owned(),
},
);
@@ -1860,7 +1915,7 @@ mod tests {
let sign = request_message(
"req-eval-sign",
- RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)),
+ RadrootsNostrConnectRequest::SignEvent(connect_unsigned_event(1)),
);
let sign_eval = backend
.evaluate_request(&connection.connection_id, sign.clone())
@@ -1932,13 +1987,15 @@ mod tests {
assert_eq!(
super::response_from_hint(
&connection,
- RadrootsNostrSignerRequestResponseHint::UserPublicKey(client_public_key),
+ RadrootsNostrSignerRequestResponseHint::UserPublicKey(connect_public_key(
+ client_public_key,
+ )),
),
- RadrootsNostrConnectResponse::UserPublicKey(client_public_key)
+ RadrootsNostrConnectResponse::UserPublicKey(connect_public_key(client_public_key))
);
let capability = RadrootsNostrConnectRemoteSessionCapability {
- user_public_key: client_public_key,
- relays: vec![primary_relay()],
+ user_public_key: connect_public_key(client_public_key),
+ relays: vec![connect_relay(primary_relay())],
permissions: all_runtime_permissions().into(),
};
assert_eq!(
@@ -1951,16 +2008,18 @@ mod tests {
assert_eq!(
super::response_from_hint(
&connection,
- RadrootsNostrSignerRequestResponseHint::RelayList(vec![primary_relay()]),
+ RadrootsNostrSignerRequestResponseHint::RelayList(vec![connect_relay(
+ primary_relay(),
+ )]),
),
- RadrootsNostrConnectResponse::RelayList(vec![primary_relay()])
+ RadrootsNostrConnectResponse::RelayList(vec![connect_relay(primary_relay())])
);
assert_eq!(
super::response_from_hint(
&connection,
RadrootsNostrSignerRequestResponseHint::RelayList(Vec::new()),
),
- RadrootsNostrConnectResponse::RelayList(vec![primary_relay()])
+ RadrootsNostrConnectResponse::RelayList(vec![connect_relay(primary_relay())])
);
assert!(matches!(
super::response_from_hint(&connection, RadrootsNostrSignerRequestResponseHint::None),
@@ -2169,7 +2228,9 @@ mod tests {
assert_eq!(
request.request,
RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: test_signer().signer_identity.public_key(),
+ remote_signer_public_key: connect_public_key(
+ test_signer().signer_identity.public_key(),
+ ),
secret: None,
requested_permissions: vec![RadrootsNostrConnectPermission::new(
RadrootsNostrConnectMethod::Nip04Encrypt,