commit dc1453dfeed86140c283b7d7b282ebda0651d8da
parent 4d65e37568b074d8de5ae3af14e3449c68198baa
Author: triesap <triesap@radroots.dev>
Date: Wed, 7 Jan 2026 16:11:22 +0000
nip46: enforce sign_event kind permissions
- allow sign_event:<kind> permissions through filtering
- add kind-scoped sign_event checks for jsonrpc and nostr listener
- add core unit coverage for permission parsing and kind gating
- constrain dev config to sign_event:1 for scoped tests
Diffstat:
4 files changed, 66 insertions(+), 6 deletions(-)
diff --git a/src/core/nip46/session.rs b/src/core/nip46/session.rs
@@ -103,13 +103,29 @@ pub fn filter_perms(requested: &[String], allowed: &[String]) -> Vec<String> {
if allowed.is_empty() {
return Vec::new();
}
+ let allows_sign_event = allowed.iter().any(|entry| entry == "sign_event");
requested
.iter()
- .filter(|perm| allowed.iter().any(|allow| allow == *perm))
- .cloned()
+ .filter_map(|perm| {
+ if allowed.iter().any(|allow| allow == perm) {
+ return Some(perm.clone());
+ }
+ if allows_sign_event && perm.starts_with("sign_event:") {
+ return Some(perm.clone());
+ }
+ None
+ })
.collect()
}
+pub fn sign_event_allowed(perms: &[String], kind: u32) -> bool {
+ if perms.iter().any(|entry| entry == "sign_event") {
+ return true;
+ }
+ let entry = format!("sign_event:{kind}");
+ perms.iter().any(|perm| perm == &entry)
+}
+
pub fn session_expires_at(ttl_secs: u64) -> Option<Instant> {
if ttl_secs == 0 {
None
@@ -187,4 +203,30 @@ mod tests {
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].id, "active");
}
+
+ #[test]
+ fn filter_perms_allows_sign_event_kinds() {
+ let requested = vec![
+ "sign_event:1".to_string(),
+ "sign_event:4".to_string(),
+ "nip04_encrypt".to_string(),
+ ];
+ let allowed = vec!["sign_event".to_string(), "nip04_encrypt".to_string()];
+ let filtered = filter_perms(&requested, &allowed);
+ assert_eq!(
+ filtered,
+ vec![
+ "sign_event:1".to_string(),
+ "sign_event:4".to_string(),
+ "nip04_encrypt".to_string()
+ ]
+ );
+ }
+
+ #[test]
+ fn sign_event_allowed_respects_kinds() {
+ let perms = vec!["sign_event:1".to_string()];
+ assert!(sign_event_allowed(&perms, 1));
+ assert!(!sign_event_allowed(&perms, 3));
+ }
}
diff --git a/src/transport/jsonrpc/methods/nip46/sign_event.rs b/src/transport/jsonrpc/methods/nip46/sign_event.rs
@@ -24,7 +24,10 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res
.parse()
.map_err(|e| RpcError::InvalidParams(e.to_string()))?;
let session = session::get_session(ctx.as_ref(), &session_id).await?;
- session::require_permission(&session, "sign_event")?;
+ session::require_sign_event_permission(
+ &session,
+ u32::from(event.kind.as_u16()),
+ )?;
if event.pubkey != session.remote_signer_pubkey {
return Err(RpcError::InvalidParams(
"event pubkey does not match remote signer".to_string(),
diff --git a/src/transport/jsonrpc/nip46/session.rs b/src/transport/jsonrpc/nip46/session.rs
@@ -1,4 +1,4 @@
-use crate::core::nip46::session::Nip46Session;
+use crate::core::nip46::session::{sign_event_allowed, Nip46Session};
use crate::transport::jsonrpc::{RpcContext, RpcError};
pub async fn get_session(
@@ -19,3 +19,14 @@ pub fn require_permission(session: &Nip46Session, perm: &str) -> Result<(), RpcE
Err(RpcError::Other(format!("unauthorized {perm}")))
}
}
+
+pub fn require_sign_event_permission(
+ session: &Nip46Session,
+ kind: u32,
+) -> Result<(), RpcError> {
+ if sign_event_allowed(&session.perms, kind) {
+ Ok(())
+ } else {
+ Err(RpcError::Other(format!("unauthorized sign_event:{kind}")))
+ }
+}
diff --git a/src/transport/nostr/listener.rs b/src/transport/nostr/listener.rs
@@ -13,7 +13,7 @@ use nostr::JsonUtil;
use tokio::sync::broadcast;
use tracing::{info, warn};
-use crate::core::nip46::session::{session_expires_at, Nip46Session};
+use crate::core::nip46::session::{session_expires_at, sign_event_allowed, Nip46Session};
use crate::core::state::Radrootsd;
use radroots_nostr::prelude::{
radroots_nostr_filter_tag,
@@ -150,7 +150,7 @@ async fn handle_request(
Ok(session) => session,
Err(response) => return response,
};
- if !has_permission(&session, "sign_event") {
+ if !has_sign_event_permission(&session, u32::from(unsigned.kind.as_u16())) {
return NostrConnectResponse::with_error("unauthorized sign_event");
}
if unsigned.pubkey != radrootsd.pubkey {
@@ -240,3 +240,7 @@ async fn session_for_client(
fn has_permission(session: &Nip46Session, perm: &str) -> bool {
session.perms.iter().any(|entry| entry == perm)
}
+
+fn has_sign_event_permission(session: &Nip46Session, kind: u32) -> bool {
+ sign_event_allowed(&session.perms, kind)
+}