From 75638c72b7d056d9a184cb140e9acd873e4b3179 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Thu, 27 Aug 2026 09:37:34 -0400 Subject: [PATCH] Renamed nsigner module to signer: unified naming, added native-tls to tokio-tungstenite, libc dep for signer transports --- Cargo.lock | 28 +- Cargo.toml | 2 +- README.md | 4 +- core/src/error.rs | 18 +- signer/Cargo.toml | 1 + signer/src/lib.rs | 4 +- signer/src/{nsigner.rs => signer.rs} | 745 +++++++++++++++++++-------- signer/src/traits.rs | 2 +- 8 files changed, 548 insertions(+), 256 deletions(-) rename signer/src/{nsigner.rs => signer.rs} (60%) diff --git a/Cargo.lock b/Cargo.lock index c43e5f5..b74d877 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,12 +380,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] name = "event-signer" -version = "0.0.2" +version = "0.0.3" dependencies = [ "nostr-core", "nostr-nips", @@ -868,7 +868,7 @@ dependencies = [ [[package]] name = "integration-tests" -version = "0.0.2" +version = "0.0.3" dependencies = [ "nostr-core", "nostr-nips", @@ -910,7 +910,7 @@ dependencies = [ [[package]] name = "keypair-generator" -version = "0.0.2" +version = "0.0.3" dependencies = [ "nostr-core", ] @@ -1041,7 +1041,7 @@ dependencies = [ [[package]] name = "nostr-core" -version = "0.0.2" +version = "0.0.3" dependencies = [ "aes", "base64", @@ -1063,7 +1063,7 @@ dependencies = [ [[package]] name = "nostr-core-umbrella" -version = "0.0.2" +version = "0.0.3" dependencies = [ "nostr-core", "nostr-nips", @@ -1074,7 +1074,7 @@ dependencies = [ [[package]] name = "nostr-nips" -version = "0.0.2" +version = "0.0.3" dependencies = [ "aes", "block-modes", @@ -1096,7 +1096,7 @@ dependencies = [ [[package]] name = "nostr-relay" -version = "0.0.2" +version = "0.0.3" dependencies = [ "futures-util", "nostr-core", @@ -1114,7 +1114,7 @@ dependencies = [ [[package]] name = "nostr-services" -version = "0.0.2" +version = "0.0.3" dependencies = [ "nostr-core", "nostr-relay", @@ -1129,11 +1129,12 @@ dependencies = [ [[package]] name = "nostr-signer" -version = "0.0.2" +version = "0.0.3" dependencies = [ "aes", "cbc", "hex", + "libc", "nostr-core", "rand", "serde", @@ -1448,7 +1449,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1798,7 +1799,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1907,7 +1908,9 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", + "native-tls", "tokio", + "tokio-native-tls", "tungstenite", ] @@ -2019,6 +2022,7 @@ dependencies = [ "http", "httparse", "log", + "native-tls", "rand", "sha1", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index cff682b..17eeb0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ serde_json = "1" # Networking tokio = { version = "1", features = ["full"] } -tokio-tungstenite = "0.24" +tokio-tungstenite = { version = "0.24", features = ["native-tls"] } tungstenite = "0.24" reqwest = { version = "0.12", features = ["json", "native-tls"] } url = "2" diff --git a/README.md b/README.md index b0ba554..48751e4 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ nostr_core_lib_rust/ ├── core/ # Core types, errors, crypto, utilities ├── relay/ # WebSocket, HTTP, relay pool ├── nips/ # All NIP implementations -├── signer/ # Signer trait, local + nsigner remote +├── signer/ # Signer trait, local + signer remote ├── services/ # Request validator, Blossom, Cashu ├── nostr-core/ # Umbrella re-export crate ├── examples/ # Example programs @@ -156,7 +156,7 @@ nostr-core = { git = "ssh://git@laantungir.net:2222/laantungir/nostr_core_lib_ru | `nostr-core` | 34 | Types, errors, crypto, utilities | | `nostr-nips` | 68 | All 14 NIP implementations | | `nostr-relay` | 8 | WebSocket, HTTP, relay pool | -| `nostr-signer` | 23 | Signer trait, local + nsigner | +| `nostr-signer` | 23 | Signer trait, local + signer | | `nostr-services` | 31 | Validator, Blossom, Cashu | | `integration-tests` | 38 | Ported from C test suite | | **Total** | **202** | | diff --git a/core/src/error.rs b/core/src/error.rs index b1bfc14..1c461ca 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -191,11 +191,11 @@ pub enum NostrError { #[error("NIP-03: invalid event id")] Nip03InvalidEventId, - // ── Nsigner ───────────────────────────────────────────────────── - #[error("nsigner: policy denied")] - NsignerPolicyDenied, - #[error("nsigner: index not allowed")] - NsignerIndexNotAllowed, + // ── Signer ────────────────────────────────────────────────────── + #[error("signer: policy denied")] + SignerPolicyDenied, + #[error("signer: index not allowed")] + SignerIndexNotAllowed, // ── Internal / catch-all ──────────────────────────────────────── #[error("unknown error code: {0}")] @@ -283,8 +283,8 @@ impl From for NostrError { -425 => NostrError::CashuProofsSpent, -426 => NostrError::CashuCryptoFailed, -427 => NostrError::CashuInvalidKeyset, - -2001 => NostrError::NsignerPolicyDenied, - -2002 => NostrError::NsignerIndexNotAllowed, + -2001 => NostrError::SignerPolicyDenied, + -2002 => NostrError::SignerIndexNotAllowed, -6 => NostrError::Nip03InvalidOtsFormat, -7 => NostrError::Nip03InvalidEventId, other => NostrError::Unknown(other), @@ -375,8 +375,8 @@ impl From for i32 { NostrError::CashuInvalidKeyset => -427, NostrError::Nip03InvalidOtsFormat => -6, NostrError::Nip03InvalidEventId => -7, - NostrError::NsignerPolicyDenied => -2001, - NostrError::NsignerIndexNotAllowed => -2002, + NostrError::SignerPolicyDenied => -2001, + NostrError::SignerIndexNotAllowed => -2002, NostrError::Unknown(code) => code, } } diff --git a/signer/Cargo.toml b/signer/Cargo.toml index bdfb815..96377d1 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -17,3 +17,4 @@ sha2.workspace = true hex.workspace = true rand.workspace = true serialport.workspace = true +libc = "0.2" diff --git a/signer/src/lib.rs b/signer/src/lib.rs index d435efd..9c4da64 100644 --- a/signer/src/lib.rs +++ b/signer/src/lib.rs @@ -1,8 +1,8 @@ -//! Signer abstraction: local and remote (nsigner) implementations. +//! Signer abstraction: local and remote (signer) implementations. pub mod traits; pub mod local; -pub mod nsigner; +pub mod signer; pub use traits::NostrSigner; pub use local::LocalSigner; diff --git a/signer/src/nsigner.rs b/signer/src/signer.rs similarity index 60% rename from signer/src/nsigner.rs rename to signer/src/signer.rs index 8a3de78..6a61d8c 100644 --- a/signer/src/nsigner.rs +++ b/signer/src/signer.rs @@ -1,7 +1,7 @@ -//! Nsigner remote signer support. +//! Signer remote signer support. //! //! Provides transport layer abstractions and a remote signer implementation -//! that communicates with an n_signer daemon over various transports +//! that communicates with an signer daemon over various transports //! (Unix sockets, TCP, serial, file descriptors, Qubes qrexec). use std::sync::Mutex; @@ -14,15 +14,15 @@ use crate::traits::NostrSigner; // ── Transport Trait ───────────────────────────────────────────────────────── -/// A framed transport for communicating with an n_signer daemon. -pub trait NsignerTransport: Send + Sync { +/// A framed transport for communicating with an signer daemon. +pub trait SignerTransport: Send + Sync { /// Send a framed JSON message. fn send_framed(&self, json: &str) -> NostrResult<()>; /// Receive a framed JSON message. fn recv_framed(&self) -> NostrResult; - /// Re-establish the connection (n_signer closes after each request). + /// Re-establish the connection (signer closes after each request). fn reconnect(&self) -> NostrResult<()>; /// Close the transport. @@ -32,6 +32,10 @@ pub trait NsignerTransport: Send + Sync { // ── Transport Implementations ─────────────────────────────────────────────── /// Unix socket transport. +/// +/// Connects to either a filesystem socket path (`/path/to/sock`) or an +/// abstract-namespace socket (name without a leading `/`), mirroring the +/// reference `signer` daemon which listens on an abstract socket. pub struct UnixTransport { socket_name: String, timeout_ms: u64, @@ -49,8 +53,24 @@ impl UnixTransport { fn connect_inner(&self) -> NostrResult { use std::time::Duration; - let stream = std::os::unix::net::UnixStream::connect(&self.socket_name) - .map_err(|_| NostrError::NetworkFailed)?; + let stream = if self.socket_name.starts_with('/') { + std::os::unix::net::UnixStream::connect(&self.socket_name) + .map_err(|e| { + eprintln!( + "[signer] unix connect to {} failed: {}", + self.socket_name, e + ); + NostrError::NetworkFailed + })? + } else { + connect_abstract_unix(&self.socket_name).map_err(|e| { + eprintln!( + "[signer] unix abstract connect to @{} failed: {}", + self.socket_name, e + ); + NostrError::NetworkFailed + })? + }; stream .set_read_timeout(Some(Duration::from_millis(self.timeout_ms))) .ok(); @@ -61,7 +81,54 @@ impl UnixTransport { } } -impl NsignerTransport for UnixTransport { +/// Connect to a Unix socket in the abstract namespace via libc. +/// +/// Rust's safe `UnixStream::connect` rejects paths containing null bytes, so +/// abstract sockets (sun_path[0] = '\0') must be connected via libc. +#[cfg(unix)] +fn connect_abstract_unix(name: &str) -> std::io::Result { + use std::os::unix::io::FromRawFd; + + if name.len() >= 107 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "socket name too long", + )); + } + + let fd = unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_STREAM, 0) }; + if fd < 0 { + return Err(std::io::Error::last_os_error()); + } + + let mut addr: libc::sockaddr_un = unsafe { std::mem::zeroed() }; + addr.sun_family = libc::AF_UNIX as libc::sa_family_t; + let name_bytes = name.as_bytes(); + for (i, &b) in name_bytes.iter().enumerate() { + addr.sun_path[i + 1] = b as libc::c_char; + } + + let addrlen = (std::mem::size_of::() + 1 + name_bytes.len()) + as libc::socklen_t; + + let rc = unsafe { + libc::connect( + fd, + &addr as *const libc::sockaddr_un as *const libc::sockaddr, + addrlen, + ) + }; + if rc != 0 { + let err = std::io::Error::last_os_error(); + unsafe { libc::close(fd) }; + return Err(err); + } + + let stream = unsafe { std::os::unix::net::UnixStream::from_raw_fd(fd) }; + Ok(stream) +} + +impl SignerTransport for UnixTransport { fn send_framed(&self, json: &str) -> NostrResult<()> { let mut guard = self.stream.lock().unwrap(); let stream = guard.as_mut().ok_or(NostrError::NetworkFailed)?; @@ -137,7 +204,7 @@ impl TcpTransport { } } -impl NsignerTransport for TcpTransport { +impl SignerTransport for TcpTransport { fn send_framed(&self, json: &str) -> NostrResult<()> { let mut guard = self.stream.lock().unwrap(); let stream = guard.as_mut().ok_or(NostrError::NetworkFailed)?; @@ -206,7 +273,7 @@ impl SerialTransport { } } -impl NsignerTransport for SerialTransport { +impl SignerTransport for SerialTransport { fn send_framed(&self, json: &str) -> NostrResult<()> { use std::io::Write; let mut guard = self.port.lock().unwrap(); @@ -278,7 +345,7 @@ impl FdTransport { } #[cfg(unix)] -impl NsignerTransport for FdTransport { +impl SignerTransport for FdTransport { fn send_framed(&self, json: &str) -> NostrResult<()> { use std::io::Write; let mut guard = self.handles.lock().unwrap(); @@ -352,14 +419,25 @@ impl QrexecTransport { fn connect_inner(&self) -> NostrResult { use std::process::Stdio; - let mut child = std::process::Command::new("qrexec-client-vm") + let mut child = match std::process::Command::new("qrexec-client-vm") .arg(&self.domain) .arg(&self.service_name) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::null()) + // Inherit stderr so qrexec-client-vm errors (denied service, + // unknown qube, ...) are visible on the console. + .stderr(Stdio::inherit()) .spawn() - .map_err(|_| NostrError::NetworkFailed)?; + { + Ok(c) => c, + Err(e) => { + eprintln!( + "[signer] failed to spawn qrexec-client-vm {} {}: {}", + self.domain, self.service_name, e + ); + return Err(NostrError::NetworkFailed); + } + }; let stdin = child.stdin.take().ok_or(NostrError::NetworkFailed)?; let stdout = child.stdout.take().ok_or(NostrError::NetworkFailed)?; Ok(QrexecState { child, stdin, stdout }) @@ -367,7 +445,7 @@ impl QrexecTransport { } #[cfg(unix)] -impl NsignerTransport for QrexecTransport { +impl SignerTransport for QrexecTransport { fn send_framed(&self, json: &str) -> NostrResult<()> { use std::io::Write; let mut guard = self.state.lock().unwrap(); @@ -430,24 +508,26 @@ impl Drop for QrexecTransport { } } -// ── Nsigner Client ────────────────────────────────────────────────────────── +// ── Signer Client ────────────────────────────────────────────────────────── -/// Low-level nsigner RPC client. -pub struct NsignerClient { - transport: Box, +/// Low-level signer RPC client. +pub struct SignerClient { + transport: Box, auth_privkey: Mutex>, auth_label: Mutex>, last_error: Mutex>, + next_id: Mutex, } -impl NsignerClient { - /// Create a new nsigner client from a transport. - pub fn new(transport: Box) -> Self { - NsignerClient { +impl SignerClient { + /// Create a new signer client from a transport. + pub fn new(transport: Box) -> Self { + SignerClient { transport, auth_privkey: Mutex::new(None), auth_label: Mutex::new(None), last_error: Mutex::new(None), + next_id: Mutex::new(1), } } @@ -457,10 +537,24 @@ impl NsignerClient { *self.auth_label.lock().unwrap() = Some(label.to_string()); } - /// Make an RPC call to the nsigner daemon. - pub fn call(&self, method: &str, params: serde_json::Value) -> NostrResult { + /// Make an RPC call to the signer daemon. + /// + /// Wire format (JSON-RPC 2.0, matching the reference `signer-client`): + /// request `{"id":"N","method":"...","params":[...]}` — params MUST be an + /// array; the options object rides as the last array element. + /// response `{"id":"...","result":...}` or + /// `{"id":"...","error":{"code":N,"message":"..."}}` + pub fn call(&self, method: &str, params: Vec) -> NostrResult { + let id = { + let mut next = self.next_id.lock().unwrap(); + let id = next.to_string(); + *next += 1; + id + }; + // Build request let request = serde_json::json!({ + "id": id, "method": method, "params": params, }); @@ -488,24 +582,51 @@ impl NsignerClient { request.to_string() }; - // Reconnect (n_signer closes connection after each request) - self.transport.reconnect()?; + // Reconnect (signer closes connection after each request) + if let Err(e) = self.transport.reconnect() { + eprintln!("[signer] {} {}: reconnect failed: {:?}", method, id, e); + return Err(e); + } // Send request - self.transport.send_framed(&request_json)?; + if let Err(e) = self.transport.send_framed(&request_json) { + eprintln!("[signer] {} {}: send failed: {:?}", method, id, e); + return Err(e); + } // Receive response - let response_json = self.transport.recv_framed()?; + let response_json = match self.transport.recv_framed() { + Ok(r) => r, + Err(e) => { + eprintln!("[signer] {} {}: recv failed: {:?}", method, id, e); + return Err(e); + } + }; // Parse response - let response: serde_json::Value = - serde_json::from_str(&response_json).map_err(|_| NostrError::InvalidInput)?; + let response: serde_json::Value = match serde_json::from_str(&response_json) { + Ok(v) => v, + Err(e) => { + eprintln!( + "[signer] {} {}: response parse failed: {} — raw: {}", + method, id, e, response_json + ); + return Err(NostrError::InvalidInput); + } + }; - // Check for error + // Check for error — the server sends {"code":N,"message":"..."} objects. if let Some(error) = response.get("error") { - let err_msg = error.as_str().unwrap_or("unknown error"); - *self.last_error.lock().unwrap() = Some(err_msg.to_string()); - return Err(NostrError::NsignerPolicyDenied); + let code = error.get("code").and_then(|v| v.as_i64()).unwrap_or(0); + let msg = error + .get("message") + .and_then(|v| v.as_str()) + .or_else(|| error.as_str()) + .unwrap_or("unknown error"); + let err_msg = format!("{} (code {})", msg, code); + eprintln!("[signer] {} {}: server error: {}", method, id, err_msg); + *self.last_error.lock().unwrap() = Some(err_msg); + return Err(NostrError::SignerPolicyDenied); } Ok(response) @@ -517,31 +638,29 @@ impl NsignerClient { } } -// ── Nsigner Signer ────────────────────────────────────────────────────────── +// ── Signer Signer ────────────────────────────────────────────────────────── -/// A remote signer that delegates to an n_signer daemon. -pub struct NsignerSigner { - client: NsignerClient, +/// A remote signer that delegates to an signer daemon. +pub struct SignerSigner { + client: SignerClient, role: String, role_path: Mutex>, derive_index: Mutex>, algorithm: Mutex, - last_error: Mutex>, } -impl NsignerSigner { - /// Create a new nsigner signer from a transport. +impl SignerSigner { + /// Create a new signer signer from a transport. pub fn from_transport( - transport: Box, + transport: Box, role: &str, ) -> Self { - NsignerSigner { - client: NsignerClient::new(transport), + SignerSigner { + client: SignerClient::new(transport), role: role.to_string(), role_path: Mutex::new(None), derive_index: Mutex::new(None), algorithm: Mutex::new("secp256k1".to_string()), - last_error: Mutex::new(None), } } @@ -565,28 +684,48 @@ impl NsignerSigner { self.client.set_auth(privkey, label); } - /// Inject `role_path` into a params object if it is set. - fn with_role_path(&self, mut params: serde_json::Value) -> serde_json::Value { + /// Build the selector options object for nostr verbs: + /// `{"role":"...","role_path":"..."}` (role_path only when set). + fn selector_options(&self) -> serde_json::Value { + let mut opts = serde_json::Map::new(); + opts.insert("role".to_string(), serde_json::json!(self.role)); if let Some(path) = self.role_path.lock().unwrap().clone() { - if let Some(obj) = params.as_object_mut() { - obj.insert("role_path".to_string(), serde_json::Value::String(path)); - } + opts.insert("role_path".to_string(), serde_json::json!(path)); } - params + serde_json::Value::Object(opts) + } + + /// Build the algorithm options object for algorithm verbs: + /// `{"algorithm":"...","index":N}` plus any extra fields. + fn algorithm_options( + &self, + algorithm: &str, + index: i32, + extra: &[(&str, serde_json::Value)], + ) -> serde_json::Value { + let mut opts = serde_json::Map::new(); + opts.insert("algorithm".to_string(), serde_json::json!(algorithm)); + opts.insert("index".to_string(), serde_json::json!(index)); + for (k, v) in extra { + opts.insert(k.to_string(), v.clone()); + } + serde_json::Value::Object(opts) } // ── ed25519 ────────────────────────────────────────────────────────────── /// Sign arbitrary data with ed25519. Returns the signature bytes. pub fn ed25519_sign(&self, data: &[u8]) -> NostrResult> { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "algorithm": "ed25519", - "data": hex::encode(data), - })); - let response = self.client.call("sign", params)?; - let sig_hex = response + let index = self.derive_index.lock().unwrap().unwrap_or(0); + let opts = self.algorithm_options("ed25519", index, &[]); + let response = + self.client + .call("sign", vec![serde_json::json!(hex::encode(data)), opts])?; + let result = response .get("result") + .ok_or(NostrError::Nip46InvalidResponse)?; + let sig_hex = result + .get("signature") .and_then(|r| r.as_str()) .ok_or(NostrError::Nip46InvalidResponse)?; hex::decode(sig_hex).map_err(|_| NostrError::InvalidInput) @@ -594,13 +733,14 @@ impl NsignerSigner { /// Get the ed25519 public key (32 bytes). pub fn ed25519_get_public_key(&self) -> NostrResult<[u8; 32]> { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "algorithm": "ed25519", - })); - let response = self.client.call("get_public_key", params)?; - let pk_hex = response + let index = self.derive_index.lock().unwrap().unwrap_or(0); + let opts = self.algorithm_options("ed25519", index, &[]); + let response = self.client.call("get_public_key", vec![opts])?; + let result = response .get("result") + .ok_or(NostrError::Nip46InvalidResponse)?; + let pk_hex = result + .get("public_key") .and_then(|r| r.as_str()) .ok_or(NostrError::Nip46InvalidResponse)?; let bytes = hex::decode(pk_hex).map_err(|_| NostrError::InvalidInput)?; @@ -615,13 +755,14 @@ impl NsignerSigner { /// Get the x25519 public key (32 bytes). pub fn x25519_get_public_key(&self) -> NostrResult<[u8; 32]> { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "algorithm": "x25519", - })); - let response = self.client.call("get_public_key", params)?; - let pk_hex = response + let index = self.derive_index.lock().unwrap().unwrap_or(0); + let opts = self.algorithm_options("x25519", index, &[]); + let response = self.client.call("get_public_key", vec![opts])?; + let result = response .get("result") + .ok_or(NostrError::Nip46InvalidResponse)?; + let pk_hex = result + .get("public_key") .and_then(|r| r.as_str()) .ok_or(NostrError::Nip46InvalidResponse)?; let bytes = hex::decode(pk_hex).map_err(|_| NostrError::InvalidInput)?; @@ -634,14 +775,17 @@ impl NsignerSigner { /// Compute a shared secret via x25519 ECDH with a peer public key. pub fn x25519_ecdh(&self, peer_pubkey: &[u8; 32]) -> NostrResult<[u8; 32]> { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "algorithm": "x25519", - "peer_pubkey": hex::encode(peer_pubkey), - })); - let response = self.client.call("ecdh", params)?; - let ss_hex = response + let index = self.derive_index.lock().unwrap().unwrap_or(0); + let opts = self.algorithm_options("x25519", index, &[]); + let response = self.client.call( + "derive_shared_secret", + vec![serde_json::json!(hex::encode(peer_pubkey)), opts], + )?; + let result = response .get("result") + .ok_or(NostrError::Nip46InvalidResponse)?; + let ss_hex = result + .get("shared_secret") .and_then(|r| r.as_str()) .ok_or(NostrError::Nip46InvalidResponse)?; let bytes = hex::decode(ss_hex).map_err(|_| NostrError::InvalidInput)?; @@ -656,36 +800,47 @@ impl NsignerSigner { /// Sign data with ML-DSA-65. Returns the signature bytes. pub fn ml_dsa_sign(&self, data: &[u8]) -> NostrResult> { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "algorithm": "ml-dsa-65", - "data": hex::encode(data), - })); - let response = self.client.call("sign", params)?; - let sig_hex = response + let index = self.derive_index.lock().unwrap().unwrap_or(0); + let opts = self.algorithm_options("ml-dsa-65", index, &[]); + let response = + self.client + .call("sign", vec![serde_json::json!(hex::encode(data)), opts])?; + let result = response .get("result") + .ok_or(NostrError::Nip46InvalidResponse)?; + let sig_hex = result + .get("signature") .and_then(|r| r.as_str()) .ok_or(NostrError::Nip46InvalidResponse)?; hex::decode(sig_hex).map_err(|_| NostrError::InvalidInput) } - /// Verify an ML-DSA-65 signature. + /// Verify an ML-DSA-65 signature against the signer's own derived key. + /// + /// Note: the server's `verify` verb verifies against the key it derives + /// from its own mnemonic (params are `msg_hex`, `sig_hex`, opts) — the + /// caller-supplied pubkey is not part of the wire protocol. pub fn ml_dsa_verify( &self, - pubkey: &[u8], + _pubkey: &[u8], data: &[u8], signature: &[u8], ) -> NostrResult { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "algorithm": "ml-dsa-65", - "pubkey": hex::encode(pubkey), - "data": hex::encode(data), - "signature": hex::encode(signature), - })); - let response = self.client.call("verify", params)?; - response + let index = self.derive_index.lock().unwrap().unwrap_or(0); + let opts = self.algorithm_options("ml-dsa-65", index, &[]); + let response = self.client.call( + "verify", + vec![ + serde_json::json!(hex::encode(data)), + serde_json::json!(hex::encode(signature)), + opts, + ], + )?; + let result = response .get("result") + .ok_or(NostrError::Nip46InvalidResponse)?; + result + .get("valid") .and_then(|r| r.as_bool()) .ok_or(NostrError::Nip46InvalidResponse) } @@ -695,12 +850,12 @@ impl NsignerSigner { /// Encapsulate a shared secret against an ML-KEM-768 public key. /// Returns `(ciphertext, shared_secret)`. pub fn ml_kem_encapsulate(&self, pubkey: &[u8]) -> NostrResult<(Vec, Vec)> { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "algorithm": "ml-kem-768", - "pubkey": hex::encode(pubkey), - })); - let response = self.client.call("encapsulate", params)?; + let index = self.derive_index.lock().unwrap().unwrap_or(0); + let opts = self.algorithm_options("ml-kem-768", index, &[]); + let response = self.client.call( + "encapsulate", + vec![serde_json::json!(hex::encode(pubkey)), opts], + )?; let result = response .get("result") .ok_or(NostrError::Nip46InvalidResponse)?; @@ -719,14 +874,17 @@ impl NsignerSigner { /// Decapsulate an ML-KEM-768 ciphertext. Returns the shared secret. pub fn ml_kem_decapsulate(&self, ciphertext: &[u8]) -> NostrResult> { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "algorithm": "ml-kem-768", - "ciphertext": hex::encode(ciphertext), - })); - let response = self.client.call("decapsulate", params)?; - let ss_hex = response + let index = self.derive_index.lock().unwrap().unwrap_or(0); + let opts = self.algorithm_options("ml-kem-768", index, &[]); + let response = self.client.call( + "decapsulate", + vec![serde_json::json!(hex::encode(ciphertext)), opts], + )?; + let result = response .get("result") + .ok_or(NostrError::Nip46InvalidResponse)?; + let ss_hex = result + .get("shared_secret") .and_then(|r| r.as_str()) .ok_or(NostrError::Nip46InvalidResponse)?; hex::decode(ss_hex).map_err(|_| NostrError::InvalidInput) @@ -736,11 +894,12 @@ impl NsignerSigner { /// Encrypt data with a one-time-pad backed by the signer's entropy. pub fn otp_encrypt(&self, plaintext: &[u8]) -> NostrResult> { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "data": hex::encode(plaintext), - })); - let response = self.client.call("otp_encrypt", params)?; + let index = self.derive_index.lock().unwrap().unwrap_or(0); + let opts = self.algorithm_options("otp", index, &[]); + let response = self.client.call( + "encrypt", + vec![serde_json::json!(hex::encode(plaintext)), opts], + )?; let ct_hex = response .get("result") .and_then(|r| r.as_str()) @@ -750,11 +909,12 @@ impl NsignerSigner { /// Decrypt data with a one-time-pad backed by the signer's entropy. pub fn otp_decrypt(&self, ciphertext: &[u8]) -> NostrResult> { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "data": hex::encode(ciphertext), - })); - let response = self.client.call("otp_decrypt", params)?; + let index = self.derive_index.lock().unwrap().unwrap_or(0); + let opts = self.algorithm_options("otp", index, &[]); + let response = self.client.call( + "decrypt", + vec![serde_json::json!(hex::encode(ciphertext)), opts], + )?; let pt_hex = response .get("result") .and_then(|r| r.as_str()) @@ -763,12 +923,10 @@ impl NsignerSigner { } } -impl NostrSigner for NsignerSigner { +impl NostrSigner for SignerSigner { fn get_public_key(&self) -> NostrResult { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - })); - let response = self.client.call("get_public_key", params)?; + let opts = self.selector_options(); + let response = self.client.call("nostr_get_public_key", vec![opts])?; let pubkey_hex = response .get("result") .and_then(|r| r.as_str()) @@ -779,11 +937,10 @@ impl NostrSigner for NsignerSigner { fn sign_event(&self, event: &Event) -> NostrResult { let event_json = serde_json::to_string(event) .map_err(|_| NostrError::EventInvalidStructure)?; - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "event": event_json, - })); - let response = self.client.call("sign_event", params)?; + let opts = self.selector_options(); + let response = self + .client + .call("nostr_sign_event", vec![serde_json::json!(event_json), opts])?; let signed_json = response .get("result") .and_then(|r| r.as_str()) @@ -795,27 +952,30 @@ impl NostrSigner for NsignerSigner { fn derive_hmac(&self, data: &str) -> NostrResult { let index = self.derive_index.lock().unwrap().ok_or(NostrError::InvalidInput)?; let algorithm = self.algorithm.lock().unwrap().clone(); - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "algorithm": algorithm, - "index": index, - "data": data, - })); - let response = self.client.call("derive", params)?; - response + let opts = self.algorithm_options(&algorithm, index, &[]); + let response = self + .client + .call("derive", vec![serde_json::json!(data), opts])?; + let result = response .get("result") + .ok_or(NostrError::Nip46InvalidResponse)?; + result + .get("digest") .and_then(|r| r.as_str()) .map(|s| s.to_string()) .ok_or(NostrError::Nip46InvalidResponse) } fn nip04_encrypt(&self, peer_pubkey: &PublicKey, plaintext: &str) -> NostrResult { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "pubkey": peer_pubkey.to_hex(), - "plaintext": plaintext, - })); - let response = self.client.call("nip04_encrypt", params)?; + let opts = self.selector_options(); + let response = self.client.call( + "nostr_nip04_encrypt", + vec![ + serde_json::json!(peer_pubkey.to_hex()), + serde_json::json!(plaintext), + opts, + ], + )?; response .get("result") .and_then(|r| r.as_str()) @@ -824,12 +984,15 @@ impl NostrSigner for NsignerSigner { } fn nip04_decrypt(&self, peer_pubkey: &PublicKey, ciphertext: &str) -> NostrResult { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "pubkey": peer_pubkey.to_hex(), - "ciphertext": ciphertext, - })); - let response = self.client.call("nip04_decrypt", params)?; + let opts = self.selector_options(); + let response = self.client.call( + "nostr_nip04_decrypt", + vec![ + serde_json::json!(peer_pubkey.to_hex()), + serde_json::json!(ciphertext), + opts, + ], + )?; response .get("result") .and_then(|r| r.as_str()) @@ -838,12 +1001,15 @@ impl NostrSigner for NsignerSigner { } fn nip44_encrypt(&self, peer_pubkey: &PublicKey, plaintext: &str) -> NostrResult> { - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "pubkey": peer_pubkey.to_hex(), - "plaintext": plaintext, - })); - let response = self.client.call("nip44_encrypt", params)?; + let opts = self.selector_options(); + let response = self.client.call( + "nostr_nip44_encrypt", + vec![ + serde_json::json!(peer_pubkey.to_hex()), + serde_json::json!(plaintext), + opts, + ], + )?; let result_b64 = response .get("result") .and_then(|r| r.as_str()) @@ -853,12 +1019,15 @@ impl NostrSigner for NsignerSigner { fn nip44_decrypt(&self, peer_pubkey: &PublicKey, ciphertext: &[u8]) -> NostrResult> { let ciphertext_b64 = nostr_core::util::base64_encode(ciphertext); - let params = self.with_role_path(serde_json::json!({ - "role": self.role, - "pubkey": peer_pubkey.to_hex(), - "ciphertext": ciphertext_b64, - })); - let response = self.client.call("nip44_decrypt", params)?; + let opts = self.selector_options(); + let response = self.client.call( + "nostr_nip44_decrypt", + vec![ + serde_json::json!(peer_pubkey.to_hex()), + serde_json::json!(ciphertext_b64), + opts, + ], + )?; let result_b64 = response .get("result") .and_then(|r| r.as_str()) @@ -867,13 +1036,12 @@ impl NostrSigner for NsignerSigner { } fn get_info(&self) -> NostrResult { - let params = serde_json::json!({}); - let response = self.client.call("get_info", params)?; + let response = self.client.call("get_info", vec![])?; Ok(serde_json::to_string(&response).unwrap_or_default()) } fn last_error(&self) -> Option { - self.last_error.lock().unwrap().clone() + self.client.last_error() } } @@ -913,7 +1081,7 @@ impl Default for MockTransport { } } -impl NsignerTransport for MockTransport { +impl SignerTransport for MockTransport { fn send_framed(&self, json: &str) -> NostrResult<()> { self.sent.lock().unwrap().push(json.to_string()); Ok(()) @@ -940,7 +1108,7 @@ mod tests { #[test] fn test_unix_transport_creation() { - let _transport = UnixTransport::new("/tmp/nsigner.sock", 5000); + let _transport = UnixTransport::new("/tmp/signer.sock", 5000); // Just verify creation doesn't panic } @@ -963,24 +1131,24 @@ mod tests { #[cfg(unix)] #[test] fn test_qrexec_transport_creation() { - let _transport = QrexecTransport::new("dom0", "nsigner.Sign", 5000); + let _transport = QrexecTransport::new("dom0", "signer.Sign", 5000); } #[test] - fn test_nsigner_client_creation() { - let transport = Box::new(UnixTransport::new("/tmp/nsigner.sock", 5000)); - let client = NsignerClient::new(transport); + fn test_signer_client_creation() { + let transport = Box::new(UnixTransport::new("/tmp/signer.sock", 5000)); + let client = SignerClient::new(transport); assert!(client.last_error().is_none()); } // ── Mock-based tests ───────────────────────────────────────────────────── - fn signer_with_mock() -> (NsignerSigner, std::sync::Arc) { + fn signer_with_mock() -> (SignerSigner, std::sync::Arc) { let mock = std::sync::Arc::new(MockTransport::new()); // Wrap the Arc in a thin newtype that implements - // NsignerTransport by delegating to the inner MockTransport. + // SignerTransport by delegating to the inner MockTransport. struct MockWrapper(std::sync::Arc); - impl NsignerTransport for MockWrapper { + impl SignerTransport for MockWrapper { fn send_framed(&self, json: &str) -> NostrResult<()> { self.0.send_framed(json) } @@ -994,46 +1162,68 @@ mod tests { self.0.close() } } - let signer = NsignerSigner::from_transport(Box::new(MockWrapper(mock.clone())), "test"); + let signer = SignerSigner::from_transport(Box::new(MockWrapper(mock.clone())), "test"); (signer, mock) } - fn last_sent_params(mock: &MockTransport) -> serde_json::Value { + fn last_sent_request(mock: &MockTransport) -> serde_json::Value { let sent = mock.sent_messages(); let last = sent.last().expect("no message sent"); let outer: serde_json::Value = serde_json::from_str(last).unwrap(); // The outer may be an auth envelope {body: {...}} or a plain request. if let Some(body) = outer.get("body") { - body.get("params").cloned().unwrap_or(serde_json::Value::Null) + body.clone() } else { - outer.get("params").cloned().unwrap_or(serde_json::Value::Null) + outer } } + fn last_sent_params(mock: &MockTransport) -> serde_json::Value { + last_sent_request(mock) + .get("params") + .cloned() + .unwrap_or(serde_json::Value::Null) + } + fn last_sent_method(mock: &MockTransport) -> String { - let sent = mock.sent_messages(); - let last = sent.last().expect("no message sent"); - let outer: serde_json::Value = serde_json::from_str(last).unwrap(); - let req = if let Some(body) = outer.get("body") { - body - } else { - &outer - }; - req.get("method") + last_sent_request(mock) + .get("method") .and_then(|m| m.as_str()) .unwrap_or("") .to_string() } #[test] - fn test_role_path_included_in_get_public_key() { + fn test_request_shape_is_jsonrpc_array_params() { + // The server dispatcher requires: id present, params an ARRAY. + let (signer, mock) = signer_with_mock(); + mock.enqueue_response(serde_json::json!({"result": "0000000000000000000000000000000000000000000000000000000000000001"})); + let _ = signer.get_public_key(); + let req = last_sent_request(&mock); + assert!(req.get("id").is_some(), "request must carry an id"); + assert!( + req.get("params").unwrap().is_array(), + "params must be a JSON array" + ); + } + + #[test] + fn test_get_public_key_uses_nostr_verb_and_selector() { let (signer, mock) = signer_with_mock(); signer.set_role_path("m/44'/1237'/0'/0/0"); mock.enqueue_response(serde_json::json!({"result": "0000000000000000000000000000000000000000000000000000000000000001"})); - let _ = signer.get_public_key(); - let params = last_sent_params(&mock); + let pk = signer.get_public_key().unwrap(); assert_eq!( - params.get("role_path").and_then(|r| r.as_str()), + pk.to_hex(), + "0000000000000000000000000000000000000000000000000000000000000001" + ); + assert_eq!(last_sent_method(&mock), "nostr_get_public_key"); + let params = last_sent_params(&mock); + assert!(params.is_array()); + let opts = params.get(0).unwrap(); + assert_eq!(opts.get("role").and_then(|r| r.as_str()), Some("test")); + assert_eq!( + opts.get("role_path").and_then(|r| r.as_str()), Some("m/44'/1237'/0'/0/0") ); } @@ -1044,11 +1234,42 @@ mod tests { mock.enqueue_response(serde_json::json!({"result": "0000000000000000000000000000000000000000000000000000000000000001"})); let _ = signer.get_public_key(); let params = last_sent_params(&mock); - assert!(params.get("role_path").is_none()); + let opts = params.get(0).unwrap(); + assert!(opts.get("role_path").is_none()); + assert_eq!(opts.get("role").and_then(|r| r.as_str()), Some("test")); } #[test] - fn test_role_path_included_in_nip04_encrypt() { + fn test_sign_event_uses_nostr_verb() { + let (signer, mock) = signer_with_mock(); + signer.set_role_path("m/44'/1237'/0'/0/0"); + let event = Event::new( + "0000000000000000000000000000000000000000000000000000000000000001" + .parse() + .unwrap(), + 1700000000, + nostr_core::types::Kind::Text, + vec![], + "hello", + ); + let signed_json = serde_json::to_string(&event).unwrap(); + mock.enqueue_response(serde_json::json!({"result": signed_json})); + let _ = signer.sign_event(&event); + assert_eq!(last_sent_method(&mock), "nostr_sign_event"); + let params = last_sent_params(&mock); + assert!(params.is_array()); + assert_eq!(params.as_array().unwrap().len(), 2); + assert!(params.get(0).unwrap().is_string(), "event JSON is params[0]"); + let opts = params.get(1).unwrap(); + assert_eq!(opts.get("role").and_then(|r| r.as_str()), Some("test")); + assert_eq!( + opts.get("role_path").and_then(|r| r.as_str()), + Some("m/44'/1237'/0'/0/0") + ); + } + + #[test] + fn test_nip04_encrypt_uses_nostr_verb_and_array_params() { let (signer, mock) = signer_with_mock(); signer.set_role_path("m/44'/1237'/0'/0/0"); let peer: PublicKey = "0000000000000000000000000000000000000000000000000000000000000002" @@ -1056,36 +1277,66 @@ mod tests { .unwrap(); mock.enqueue_response(serde_json::json!({"result": "ciphertext"})); let _ = signer.nip04_encrypt(&peer, "hello"); + assert_eq!(last_sent_method(&mock), "nostr_nip04_encrypt"); let params = last_sent_params(&mock); + assert!(params.is_array()); + assert_eq!(params.as_array().unwrap().len(), 3); assert_eq!( - params.get("role_path").and_then(|r| r.as_str()), + params.get(0).and_then(|r| r.as_str()), + Some("0000000000000000000000000000000000000000000000000000000000000002") + ); + assert_eq!(params.get(1).and_then(|r| r.as_str()), Some("hello")); + let opts = params.get(2).unwrap(); + assert_eq!( + opts.get("role_path").and_then(|r| r.as_str()), Some("m/44'/1237'/0'/0/0") ); } + #[test] + fn test_error_object_parsed_into_last_error() { + let (signer, mock) = signer_with_mock(); + mock.enqueue_response(serde_json::json!({ + "error": {"code": 1002, "message": "path_not_allowed"} + })); + let err = signer.get_public_key().unwrap_err(); + assert!(matches!(err, NostrError::SignerPolicyDenied)); + assert_eq!( + signer.last_error().as_deref(), + Some("path_not_allowed (code 1002)") + ); + } + #[test] fn test_derive_hmac_uses_configured_algorithm() { let (signer, mock) = signer_with_mock(); signer.set_derive_index(0); signer.set_algorithm("ed25519"); - mock.enqueue_response(serde_json::json!({"result": "deadbeef"})); - let _ = signer.derive_hmac("data"); + mock.enqueue_response(serde_json::json!({"result": {"digest": "deadbeef"}})); + let digest = signer.derive_hmac("data").unwrap(); + assert_eq!(digest, "deadbeef"); + assert_eq!(last_sent_method(&mock), "derive"); let params = last_sent_params(&mock); + assert!(params.is_array()); + assert_eq!(params.get(0).and_then(|r| r.as_str()), Some("data")); + let opts = params.get(1).unwrap(); assert_eq!( - params.get("algorithm").and_then(|r| r.as_str()), + opts.get("algorithm").and_then(|r| r.as_str()), Some("ed25519") ); + assert_eq!(opts.get("index").and_then(|r| r.as_i64()), Some(0)); } #[test] fn test_derive_hmac_defaults_to_secp256k1() { let (signer, mock) = signer_with_mock(); signer.set_derive_index(0); - mock.enqueue_response(serde_json::json!({"result": "deadbeef"})); + mock.enqueue_response(serde_json::json!({"result": {"digest": "deadbeef"}})); let _ = signer.derive_hmac("data"); let params = last_sent_params(&mock); + let opts = params.get(1).unwrap(); assert_eq!( - params.get("algorithm").and_then(|r| r.as_str()), + opts.get("algorithm").and_then(|r| r.as_str()), Some("secp256k1") ); } @@ -1093,31 +1344,38 @@ mod tests { #[test] fn test_ed25519_sign_params() { let (signer, mock) = signer_with_mock(); - mock.enqueue_response(serde_json::json!({"result": "deadbeef"})); - let _ = signer.ed25519_sign(b"hello"); + mock.enqueue_response(serde_json::json!({"result": {"signature": "deadbeef"}})); + let sig = signer.ed25519_sign(b"hello").unwrap(); + assert_eq!(sig, hex::decode("deadbeef").unwrap()); assert_eq!(last_sent_method(&mock), "sign"); let params = last_sent_params(&mock); + assert!(params.is_array()); assert_eq!( - params.get("algorithm").and_then(|r| r.as_str()), - Some("ed25519") - ); - assert_eq!( - params.get("data").and_then(|r| r.as_str()), + params.get(0).and_then(|r| r.as_str()), Some(hex::encode(b"hello")).as_deref() ); + let opts = params.get(1).unwrap(); + assert_eq!( + opts.get("algorithm").and_then(|r| r.as_str()), + Some("ed25519") + ); } #[test] fn test_ed25519_get_public_key_params() { let (signer, mock) = signer_with_mock(); - let pk = [0u8; 32]; - mock.enqueue_response(serde_json::json!({"result": hex::encode(pk)})); + let pk = [5u8; 32]; + mock.enqueue_response(serde_json::json!({ + "result": {"algorithm": "ed25519", "public_key": hex::encode(pk)} + })); let result = signer.ed25519_get_public_key().unwrap(); assert_eq!(result, pk); assert_eq!(last_sent_method(&mock), "get_public_key"); let params = last_sent_params(&mock); + assert!(params.is_array()); + let opts = params.get(0).unwrap(); assert_eq!( - params.get("algorithm").and_then(|r| r.as_str()), + opts.get("algorithm").and_then(|r| r.as_str()), Some("ed25519") ); } @@ -1127,30 +1385,35 @@ mod tests { let (signer, mock) = signer_with_mock(); let peer = [1u8; 32]; let ss = [2u8; 32]; - mock.enqueue_response(serde_json::json!({"result": hex::encode(ss)})); + mock.enqueue_response(serde_json::json!({ + "result": {"shared_secret": hex::encode(ss), "algorithm": "x25519"} + })); let result = signer.x25519_ecdh(&peer).unwrap(); assert_eq!(result, ss); - assert_eq!(last_sent_method(&mock), "ecdh"); + assert_eq!(last_sent_method(&mock), "derive_shared_secret"); let params = last_sent_params(&mock); + assert!(params.is_array()); assert_eq!( - params.get("algorithm").and_then(|r| r.as_str()), - Some("x25519") - ); - assert_eq!( - params.get("peer_pubkey").and_then(|r| r.as_str()), + params.get(0).and_then(|r| r.as_str()), Some(hex::encode(peer)).as_deref() ); + let opts = params.get(1).unwrap(); + assert_eq!( + opts.get("algorithm").and_then(|r| r.as_str()), + Some("x25519") + ); } #[test] fn test_ml_dsa_sign_params() { let (signer, mock) = signer_with_mock(); - mock.enqueue_response(serde_json::json!({"result": "deadbeef"})); + mock.enqueue_response(serde_json::json!({"result": {"signature": "deadbeef"}})); let _ = signer.ml_dsa_sign(b"data"); assert_eq!(last_sent_method(&mock), "sign"); let params = last_sent_params(&mock); + let opts = params.get(1).unwrap(); assert_eq!( - params.get("algorithm").and_then(|r| r.as_str()), + opts.get("algorithm").and_then(|r| r.as_str()), Some("ml-dsa-65") ); } @@ -1158,18 +1421,26 @@ mod tests { #[test] fn test_ml_dsa_verify_params() { let (signer, mock) = signer_with_mock(); - mock.enqueue_response(serde_json::json!({"result": true})); + mock.enqueue_response(serde_json::json!({"result": {"valid": true}})); let ok = signer.ml_dsa_verify(b"pk", b"data", b"sig").unwrap(); assert!(ok); assert_eq!(last_sent_method(&mock), "verify"); let params = last_sent_params(&mock); + assert!(params.is_array()); + assert_eq!(params.as_array().unwrap().len(), 3); assert_eq!( - params.get("algorithm").and_then(|r| r.as_str()), + params.get(0).and_then(|r| r.as_str()), + Some(hex::encode(b"data")).as_deref() + ); + assert_eq!( + params.get(1).and_then(|r| r.as_str()), + Some(hex::encode(b"sig")).as_deref() + ); + let opts = params.get(2).unwrap(); + assert_eq!( + opts.get("algorithm").and_then(|r| r.as_str()), Some("ml-dsa-65") ); - assert!(params.get("pubkey").is_some()); - assert!(params.get("data").is_some()); - assert!(params.get("signature").is_some()); } #[test] @@ -1188,8 +1459,14 @@ mod tests { assert_eq!(got_ss, ss); assert_eq!(last_sent_method(&mock), "encapsulate"); let params = last_sent_params(&mock); + assert!(params.is_array()); assert_eq!( - params.get("algorithm").and_then(|r| r.as_str()), + params.get(0).and_then(|r| r.as_str()), + Some(hex::encode(b"pk")).as_deref() + ); + let opts = params.get(1).unwrap(); + assert_eq!( + opts.get("algorithm").and_then(|r| r.as_str()), Some("ml-kem-768") ); } @@ -1198,16 +1475,23 @@ mod tests { fn test_ml_kem_decapsulate_params() { let (signer, mock) = signer_with_mock(); let ss = vec![2u8; 16]; - mock.enqueue_response(serde_json::json!({"result": hex::encode(&ss)})); + mock.enqueue_response(serde_json::json!({ + "result": {"shared_secret": hex::encode(&ss)} + })); let got = signer.ml_kem_decapsulate(b"ct").unwrap(); assert_eq!(got, ss); assert_eq!(last_sent_method(&mock), "decapsulate"); let params = last_sent_params(&mock); + assert!(params.is_array()); assert_eq!( - params.get("algorithm").and_then(|r| r.as_str()), + params.get(0).and_then(|r| r.as_str()), + Some(hex::encode(b"ct")).as_deref() + ); + let opts = params.get(1).unwrap(); + assert_eq!( + opts.get("algorithm").and_then(|r| r.as_str()), Some("ml-kem-768") ); - assert!(params.get("ciphertext").is_some()); } #[test] @@ -1217,17 +1501,20 @@ mod tests { mock.enqueue_response(serde_json::json!({"result": hex::encode(&ct)})); let got_ct = signer.otp_encrypt(b"plain").unwrap(); assert_eq!(got_ct, ct); - assert_eq!(last_sent_method(&mock), "otp_encrypt"); + assert_eq!(last_sent_method(&mock), "encrypt"); let params = last_sent_params(&mock); + assert!(params.is_array()); assert_eq!( - params.get("data").and_then(|r| r.as_str()), + params.get(0).and_then(|r| r.as_str()), Some(hex::encode(b"plain")).as_deref() ); + let opts = params.get(1).unwrap(); + assert_eq!(opts.get("algorithm").and_then(|r| r.as_str()), Some("otp")); let pt = vec![7u8; 8]; mock.enqueue_response(serde_json::json!({"result": hex::encode(&pt)})); let got_pt = signer.otp_decrypt(&ct).unwrap(); assert_eq!(got_pt, pt); - assert_eq!(last_sent_method(&mock), "otp_decrypt"); + assert_eq!(last_sent_method(&mock), "decrypt"); } } diff --git a/signer/src/traits.rs b/signer/src/traits.rs index 21272fc..e668303 100644 --- a/signer/src/traits.rs +++ b/signer/src/traits.rs @@ -1,6 +1,6 @@ //! NostrSigner trait — abstract interface for signing operations. //! -//! Both local (in-memory private key) and remote (nsigner) signers +//! Both local (in-memory private key) and remote (signer) signers //! implement this trait, matching the C `nostr_signer_t` abstraction. use nostr_core::types::{Event, PublicKey};