commit 053a2aeffc2546aaf8ab504ea2c3f2214f48fbae
parent 10577cbff24e0d97965611a99c0b1b49e206f25d
Author: triesap <tyson@radroots.org>
Date: Thu, 2 Apr 2026 00:49:37 +0000
remote-signer: request explicit review permissions
- add a shared requested permission contract for bunker and discovery remote signer targets
- send explicit sign_event:kind:1 permissions in the nip46 connect request instead of an empty default set
- surface the same requested permissions in desktop ios and android preview review flows
- cover the shared request and preview mapping with remote signer core and desktop validation
Diffstat:
7 files changed, 85 insertions(+), 14 deletions(-)
diff --git a/crates/android/src/remote_signer.rs b/crates/android/src/remote_signer.rs
@@ -147,11 +147,12 @@ impl AndroidRemoteSigner {
pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> {
let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?;
+ let requested_permissions = preview.requested_permission_labels();
Ok(RadrootsRemoteSignerPreview {
source_label: preview.source_label().to_owned(),
signer_npub: preview.signer_identity.public_key_npub,
relays: preview.relays,
- requested_permissions: Vec::new(),
+ requested_permissions,
})
}
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
@@ -1743,7 +1743,7 @@ mod tests {
source_label: "discovery url".into(),
signer_npub: FIXTURE_BOB.npub.into(),
relays: vec!["ws://localhost:8080".into()],
- requested_permissions: Vec::new(),
+ requested_permissions: vec!["sign_event:kind:1".into()],
}
}
diff --git a/crates/desktop/src/remote_signer.rs b/crates/desktop/src/remote_signer.rs
@@ -146,11 +146,12 @@ impl DesktopRemoteSigner {
pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> {
let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?;
+ let requested_permissions = preview.requested_permission_labels();
Ok(RadrootsRemoteSignerPreview {
source_label: preview.source_label().to_owned(),
signer_npub: preview.signer_identity.public_key_npub,
relays: preview.relays,
- requested_permissions: Vec::new(),
+ requested_permissions,
})
}
@@ -358,5 +359,9 @@ mod tests {
assert_eq!(preview.source_label, "discovery url");
assert_eq!(preview.signer_npub, FIXTURE_BOB.npub);
assert_eq!(preview.relays, vec!["ws://localhost:8080".to_owned()]);
+ assert_eq!(
+ preview.requested_permissions,
+ vec!["sign_event:kind:1".to_owned()]
+ );
}
}
diff --git a/crates/ios/src/remote_signer.rs b/crates/ios/src/remote_signer.rs
@@ -147,11 +147,12 @@ impl IosRemoteSigner {
pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> {
let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?;
+ let requested_permissions = preview.requested_permission_labels();
Ok(RadrootsRemoteSignerPreview {
source_label: preview.source_label().to_owned(),
signer_npub: preview.signer_identity.public_key_npub,
relays: preview.relays,
- requested_permissions: Vec::new(),
+ requested_permissions,
})
}
diff --git a/crates/remote-signer/src/input.rs b/crates/remote-signer/src/input.rs
@@ -1,6 +1,9 @@
use crate::error::RadrootsAppRemoteSignerError;
use radroots_identity::RadrootsIdentityPublic;
-use radroots_nostr_connect::prelude::RadrootsNostrConnectUri;
+use radroots_nostr_connect::prelude::{
+ RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions,
+ RadrootsNostrConnectUri,
+};
use radroots_nostr_connect::uri::RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME;
use url::Url;
@@ -16,6 +19,7 @@ pub struct RadrootsAppRemoteSignerTarget {
pub signer_identity: RadrootsIdentityPublic,
pub relays: Vec<String>,
pub connect_secret: Option<String>,
+ pub requested_permissions: RadrootsNostrConnectPermissions,
}
impl RadrootsAppRemoteSignerTarget {
@@ -25,6 +29,22 @@ impl RadrootsAppRemoteSignerTarget {
RadrootsAppRemoteSignerSource::DiscoveryUrl => "discovery url",
}
}
+
+ pub fn requested_permission_labels(&self) -> Vec<String> {
+ self.requested_permissions
+ .as_slice()
+ .iter()
+ .map(ToString::to_string)
+ .collect()
+ }
+}
+
+pub fn radroots_app_remote_signer_requested_permissions() -> RadrootsNostrConnectPermissions {
+ vec![RadrootsNostrConnectPermission::with_parameter(
+ RadrootsNostrConnectMethod::SignEvent,
+ "kind:1",
+ )]
+ .into()
}
pub fn radroots_app_remote_signer_preview(
@@ -77,6 +97,7 @@ fn parse_bunker_uri(
.map(|relay| relay.to_string())
.collect(),
connect_secret: bunker_uri.secret,
+ requested_permissions: radroots_app_remote_signer_requested_permissions(),
})
}
@@ -112,6 +133,10 @@ mod tests {
assert_eq!(preview.signer_identity.public_key_npub, FIXTURE_ALICE.npub);
assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]);
assert_eq!(preview.connect_secret, None);
+ assert_eq!(
+ preview.requested_permission_labels(),
+ vec!["sign_event:kind:1".to_owned()]
+ );
}
#[test]
@@ -122,6 +147,10 @@ mod tests {
assert_eq!(preview.source, RadrootsAppRemoteSignerSource::DiscoveryUrl);
assert_eq!(preview.signer_identity.public_key_npub, FIXTURE_ALICE.npub);
assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]);
+ assert_eq!(
+ preview.requested_permission_labels(),
+ vec!["sign_event:kind:1".to_owned()]
+ );
}
#[test]
diff --git a/crates/remote-signer/src/lib.rs b/crates/remote-signer/src/lib.rs
@@ -22,7 +22,7 @@ pub use custody::{
pub use error::RadrootsAppRemoteSignerError;
pub use input::{
RadrootsAppRemoteSignerSource, RadrootsAppRemoteSignerTarget,
- radroots_app_remote_signer_preview,
+ radroots_app_remote_signer_preview, radroots_app_remote_signer_requested_permissions,
};
pub use protocol::{
RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerPendingSession,
diff --git a/crates/remote-signer/src/protocol.rs b/crates/remote-signer/src/protocol.rs
@@ -66,17 +66,12 @@ async fn connect_pending_session(
target: RadrootsAppRemoteSignerTarget,
) -> Result<RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerError> {
let client_identity = RadrootsIdentity::generate();
+ let connect_request = connect_request_for_target(&target)?;
let response = execute_request(
&client_identity,
&target,
RadrootsNostrConnectMethod::Connect,
- RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: parse_public_key_hex(
- target.signer_identity.public_key_hex.as_str(),
- )?,
- secret: target.connect_secret.clone(),
- requested_permissions: Default::default(),
- },
+ connect_request,
CONNECT_TIMEOUT,
)
.await?;
@@ -100,6 +95,18 @@ async fn connect_pending_session(
}
}
+fn connect_request_for_target(
+ target: &RadrootsAppRemoteSignerTarget,
+) -> Result<RadrootsNostrConnectRequest, RadrootsAppRemoteSignerError> {
+ Ok(RadrootsNostrConnectRequest::Connect {
+ remote_signer_public_key: parse_public_key_hex(
+ target.signer_identity.public_key_hex.as_str(),
+ )?,
+ secret: target.connect_secret.clone(),
+ requested_permissions: target.requested_permissions.clone(),
+ })
+}
+
async fn poll_pending_session(
record: &RadrootsAppRemoteSignerSessionRecord,
client_secret_key_hex: &str,
@@ -111,6 +118,7 @@ async fn poll_pending_session(
signer_identity: record.signer_identity.clone(),
relays: record.relays.clone(),
connect_secret: None,
+ requested_permissions: crate::radroots_app_remote_signer_requested_permissions(),
};
match execute_request(
@@ -338,8 +346,9 @@ fn parse_public_key_hex(value: &str) -> Result<nostr::PublicKey, RadrootsAppRemo
#[cfg(test)]
mod tests {
use super::*;
+ use crate::radroots_app_remote_signer_preview;
use nostr::PublicKey;
- use radroots_app_test_support::{FIXTURE_ALICE, fixture_identity};
+ use radroots_app_test_support::{FIXTURE_ALICE, RELAY_PRIMARY_WSS, fixture_identity};
fn fixture_public_key() -> PublicKey {
fixture_identity(&FIXTURE_ALICE)
@@ -347,6 +356,16 @@ mod tests {
.public_key()
}
+ fn fixture_discovery_url() -> String {
+ format!(
+ "http://localhost/connect?uri={}",
+ url::form_urlencoded::byte_serialize(
+ format!("bunker://{}?relay={RELAY_PRIMARY_WSS}", FIXTURE_ALICE.npub).as_bytes()
+ )
+ .collect::<String>()
+ )
+ }
+
#[test]
fn pending_connection_response_is_classified_as_pending_approval() {
let outcome =
@@ -412,4 +431,20 @@ mod tests {
if message.contains("unexpected `get_public_key` response")
));
}
+
+ #[test]
+ fn connect_request_uses_explicit_requested_permissions() {
+ let target =
+ radroots_app_remote_signer_preview(fixture_discovery_url().as_str()).expect("preview");
+
+ let request = connect_request_for_target(&target).expect("request");
+
+ match request {
+ RadrootsNostrConnectRequest::Connect {
+ requested_permissions,
+ ..
+ } => assert_eq!(requested_permissions.to_string(), "sign_event:kind:1"),
+ other => panic!("unexpected request: {other:?}"),
+ }
+ }
}