From 904d14064f51ec9a3f74882a312a70eea3a6784c Mon Sep 17 00:00:00 2001 From: Awiteb Date: Wed, 6 Aug 2025 07:58:06 +0000 Subject: [PATCH] feat: Sign using NIP-07 Signed-off-by: Awiteb --- CHANGELOG.md | 1 + Cargo.lock | 39 +++++++++++++++++++++ Cargo.toml | 25 +++++++------- README.md | 2 +- docs/SUMMARY.md | 1 + docs/commands.md | 3 ++ docs/config/nip07.md | 22 ++++++++++++ docs/index.html | 2 +- src/cli/commands/config/mod.rs | 8 ++++- src/cli/commands/config/nip07.rs | 58 ++++++++++++++++++++++++++++++++ src/cli/commands/mod.rs | 43 ++++++++++++++--------- src/cli/config.rs | 5 ++- src/cli/mod.rs | 31 +++++++++++++++-- src/cli/options_state.rs | 56 ++++++++++++++++++++++++++++++ src/cli/parsers.rs | 4 +-- src/error.rs | 10 ++++-- src/nostr_utils/mod.rs | 2 +- 17 files changed, 272 insertions(+), 40 deletions(-) create mode 100644 docs/config/nip07.md create mode 100644 src/cli/commands/config/nip07.rs create mode 100644 src/cli/options_state.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 01cc909..b4d6bac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support signing using NIP-46 bunker - by Awiteb - Keyring the secret key `n34 config keyring --enable` - by Awiteb - New flag to `patch apply and merge` to mention patches - by Awiteb +- Sign using NIP-07 - by Awiteb ### Dependencies diff --git a/Cargo.lock b/Cargo.lock index 69c85c3..db85029 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -837,6 +837,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.6.0" @@ -850,6 +856,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1235,6 +1242,7 @@ dependencies = [ "either", "futures", "nostr", + "nostr-browser-signer-proxy", "nostr-connect", "nostr-keyring", "nostr-sdk", @@ -1279,6 +1287,25 @@ dependencies = [ "url", ] +[[package]] +name = "nostr-browser-signer-proxy" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36272167bc7aa8b87d0638675d1b0fd7bff04eb6ab49f97303229a06a7bbc849" +dependencies = [ + "atomic-destructor", + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "nostr", + "serde", + "serde_json", + "tokio", + "tracing", + "uuid", +] + [[package]] name = "nostr-connect" version = "0.43.0" @@ -2494,6 +2521,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 479f6a1..b64c30a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,18 +29,19 @@ nostr-sdk = { version = "0.43.0", default-features = false } serde = { version = "1.0.219", features = ["derive"] } tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } -chrono = "0.4.41" -convert_case = "0.8.0" -dirs = "6.0.0" -easy-ext = "1.0.2" -either = "1.15.0" -futures = "0.3.31" -regex = "1.11.1" -tempfile = "3.20.0" -thiserror = "2.0.12" -toml = "0.9.4" -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +chrono = "0.4.41" +convert_case = "0.8.0" +dirs = "6.0.0" +easy-ext = "1.0.2" +either = "1.15.0" +futures = "0.3.31" +nostr-browser-signer-proxy = "0.43.0" +regex = "1.11.1" +tempfile = "3.20.0" +thiserror = "2.0.12" +toml = "0.9.4" +tracing = "0.1.41" +tracing-subscriber = "0.3.19" [profile.release] lto = "fat" diff --git a/README.md b/README.md index bf65c70..40b68c6 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ details, see the following section. - [X] Proof of Work ([NIP-13]) - [X] `nostr:` URI scheme, in the issue/reply content ([NIP-21]) - [X] Signing using bunker ([NIP-46]) -- [ ] Signing using [NIP-07] proxy ([nostr-browser-signer-proxy]) +- [X] Signing using [NIP-07] proxy ([nostr-browser-signer-proxy]) - [ ] Code Snippets ([NIP-C0]) - [X] In device relays and repos bookmark (`sets` command) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index aa52e77..78a8509 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -9,6 +9,7 @@ - [Manage Configuration](config/README.md) - [Default PoW Difficulty](config/pow.md) - [Fallback Relays](config/relays.md) + - [NIP-07 Browser Signer Proxy](config/nip07.md) - [NIP-46 Bunker](config/bunker.md) - [Secret Key Keyring](config/keyring.md) - [Manage Repositories](repo/README.md) diff --git a/docs/commands.md b/docs/commands.md index c7f28f4..1adce82 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -8,6 +8,9 @@ The `n34` command-line tool accepts the following options: signing events. - `-b`, `--bunker-url`: The URL of a NIP-46 bunker service used for remote signing of events. +- `-7`, `--nip07`: Enables signing events using the browser's NIP-07 + extension. Listens on `127.0.0.1:51034`. You can configure the address with `n34 + config nip07` - `-r`, `--relays`: A relay to read from and write to. This option can be specified multiple times to connect to several relays. - `--pow`: Sets the Proof of Work difficulty required when creating events. diff --git a/docs/config/nip07.md b/docs/config/nip07.md new file mode 100644 index 0000000..3e2f9c2 --- /dev/null +++ b/docs/config/nip07.md @@ -0,0 +1,22 @@ +# NIP-07 Browser Signer Proxy + +> `n34 config nip07` + +**Usage:** +``` +Manage the NIP-07 browser signer proxy by enabling or disabling it and configuring the `ip:port` address. + +Usage: n34 config nip07 [OPTIONS] <--enable|--disable> + +Options: + --enable Enable NIP-07 as the default signer + --disable Disable NIP-07 as the default signer + --addr Set the `ip:port` for the browser signer proxy (default: 127.0.0.1:51034) +``` + +Use [NIP-07] (Browser Extension Signer) as your default signer. This is achieved +by running a proxy at the specified `ADDR`, which defaults to `127.0.0.1:51034`. +The proxy forwards `n34` requests to the browser signer and relays the responses +back. + +[NIP-07]: https://github.com/nostr-protocol/nips/blob/master/07.md diff --git a/docs/index.html b/docs/index.html index 4e824b2..c70ebff 100644 --- a/docs/index.html +++ b/docs/index.html @@ -390,7 +390,7 @@ Signing using bunker (NIP-46)
  • - + Signing using NIP-07 proxy (nostr-browser-signer-proxy)
  • diff --git a/src/cli/commands/config/mod.rs b/src/cli/commands/config/mod.rs index 24968be..8b81c31 100644 --- a/src/cli/commands/config/mod.rs +++ b/src/cli/commands/config/mod.rs @@ -18,6 +18,8 @@ mod bunker; /// `config keyring` subcommand mod keyring; +/// `config nip07` subcommand +mod nip07; /// `config pow` subcommand mod pow; /// `config relays` subcommand @@ -27,6 +29,7 @@ use clap::Subcommand; use self::bunker::BunkerArgs; use self::keyring::KeyringArgs; +use self::nip07::Nip07Args; use self::pow::PowArgs; use self::relays::RelaysArgs; use super::CliOptions; @@ -45,10 +48,13 @@ pub enum ConfigSubcommands { /// Managing the secret key keyring, including enabling, disabling, or /// resetting it. Keyring(KeyringArgs), + /// Controls the NIP-07 browser signer proxy, turning it on or off, and + /// configures the `ip:port` address. + Nip07(Nip07Args), } impl CommandRunner for ConfigSubcommands { async fn run(self, options: CliOptions) -> N34Result<()> { - crate::run_command!(self, options, & Pow Relays Bunker Keyring) + crate::run_command!(self, options, & Pow Relays Bunker Keyring Nip07) } } diff --git a/src/cli/commands/config/nip07.rs b/src/cli/commands/config/nip07.rs new file mode 100644 index 0000000..3751224 --- /dev/null +++ b/src/cli/commands/config/nip07.rs @@ -0,0 +1,58 @@ +// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr +// Copyright (C) 2025 Awiteb +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::net::SocketAddr; + +use clap::{ArgGroup, Args}; + +use crate::{ + cli::{CliOptions, options_state::DEFAULT_NIP07_PROXY_ADDR, traits::CommandRunner}, + error::N34Result, +}; + +#[derive(Args, Debug)] +#[clap( + group( + ArgGroup::new("options") + .required(true) + ) +)] +pub struct Nip07Args { + /// Enable NIP-07 as the default signer. + #[arg(long, group = "options")] + enable: bool, + /// Disable NIP-07 as the default signer. + #[arg(long, group = "options", group = "disable_options")] + disable: bool, + /// Set the default `ip:port` for the browser signer proxy. + #[arg(long, group = "disable_options")] + addr: Option, +} + +impl CommandRunner for Nip07Args { + const NEED_SIGNER: bool = false; + + async fn run(self, mut options: CliOptions) -> N34Result<()> { + if self.enable { + let addr = self.addr.unwrap_or(DEFAULT_NIP07_PROXY_ADDR); + options.config.nip07 = Some(addr) + } else { + options.config.nip07 = None + } + + options.config.dump() + } +} diff --git a/src/cli/commands/mod.rs b/src/cli/commands/mod.rs index 5fa3341..ea94134 100644 --- a/src/cli/commands/mod.rs +++ b/src/cli/commands/mod.rs @@ -31,7 +31,7 @@ use std::fmt; use std::sync::Arc; use std::time::Duration; -use clap::{ArgGroup, Args, Parser}; +use clap::{Args, Parser}; use nostr::key::{Keys, PublicKey, SecretKey}; use nostr::nips::nip46::NostrConnectURI; use nostr::signer::{IntoNostrSigner, NostrSigner}; @@ -44,6 +44,7 @@ use self::reply::ReplyArgs; use self::repo::RepoSubcommands; use self::sets::SetsSubcommands; use super::CliConfig; +use super::options_state::OptionsState; use super::types::RelayOrSet; use super::{parsers, traits::CommandRunner}; use crate::cli::Cli; @@ -59,21 +60,18 @@ pub const DEFAULT_FALLBACK_PATH: &str = "I_DO_NOT_KNOW_WHY_CLAP_DO_NOT_SUPPORT_L const BUNKER_TIMEOUT: Duration = Duration::from_secs(60 * 3); /// The command-line interface options -#[derive(Args, Clone)] -#[clap( - group( - ArgGroup::new("signer") - .args(&["secret_key", "bunker_url"]) - .required(false) - ) -)] +#[derive(Args)] pub struct CliOptions { /// Your Nostr secret key - #[arg(short, long)] + #[arg(short, long, group = "signer")] pub secret_key: Option, /// NIP-46 bunker url used for signing events - #[arg(short, long, value_parser=parsers::parse_bunker_url)] + #[arg(short, long, group = "signer", value_parser = parsers::parse_bunker_url)] pub bunker_url: Option, + /// Enables signing events using the browser's NIP-07 extension. Listens on + /// `127.0.0.1:51034`. + #[arg(short = '7', long, group = "signer")] + pub nip07: bool, /// Fallbacks relay to write and read from it. Multiple relays can be /// passed. #[arg(short, long)] @@ -86,6 +84,10 @@ pub struct CliOptions { hide_default_value = true, value_parser = parsers::parse_config_path )] pub config: CliConfig, + /// The state of options. Some values that are used by them but should not + /// be entered via the CLI + #[arg(skip)] + pub state: OptionsState, } /// N34 commands @@ -124,7 +126,7 @@ pub enum Commands { impl CliOptions { /// Gets the public key of the user. pub async fn pubkey(&self) -> N34Result { - let Some(signer) = self.signer()? else { + let Some(signer) = self.signer().await? else { unreachable!( "This method should only be called when a signer is required. If this panic \ occurs, it indicates a bug where the command failed to properly require a signer \ @@ -136,10 +138,21 @@ impl CliOptions { } /// Returns the signer - pub fn signer(&self) -> N34Result>> { - if self.config.keyring_secret_key { + pub async fn signer(&self) -> N34Result>> { + if self.nip07 { + self.state.browser_signer_proxy.start().await?; + + println!( + "Browser signer proxy started at: {}", + self.state.browser_signer_proxy.url() + ); + + // FIXME: Use `BrowserSignerProxy::is_session_active` after it release + // nostr@0.44.0 + tokio::time::sleep(Duration::from_secs(10)).await; + return Ok(Some( - Cli::user_keypair(self.secret_key.clone())?.into_nostr_signer(), + self.state.browser_signer_proxy.clone().into_nostr_signer(), )); } diff --git a/src/cli/config.rs b/src/cli/config.rs index a495202..7180c27 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::{collections::HashSet, fs, path::PathBuf}; +use std::{collections::HashSet, fs, net::SocketAddr, path::PathBuf}; use nostr::{ nips::{nip19::Nip19Coordinate, nip46::NostrConnectURI}, @@ -76,6 +76,9 @@ pub struct CliConfig { /// Whether to use the system keyring to store the secret key. #[serde(default)] pub keyring_secret_key: bool, + /// Signs events using the browser's NIP-07 extension. + #[serde(default)] + pub nip07: Option, } /// A named group of repositories and relays. diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f3e7e94..735915a 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -24,6 +24,8 @@ pub mod config; pub mod defaults; /// Macros for CLI application. pub mod macros; +/// Represents the state used for CLI options. +pub mod options_state; /// CLI arguments parsers pub mod parsers; /// CLI traits @@ -31,10 +33,13 @@ pub mod traits; /// Common helper types used throughout the CLI. pub mod types; + use clap::Parser; use clap_verbosity_flag::Verbosity; use nostr::key::Keys; use nostr::key::SecretKey; +use nostr_browser_signer_proxy::BrowserSignerProxy; +use nostr_browser_signer_proxy::BrowserSignerProxyOptions; use nostr_keyring::KeyringError; use nostr_keyring::NostrKeyring; use types::RelayOrSet; @@ -42,6 +47,7 @@ use types::RelayOrSet; pub use self::commands::*; pub use self::config::*; use self::traits::CommandRunner; +use crate::cli::options_state::BROWSER_SIGNER_PROXY_TIMEOUT; use crate::error::N34Error; use crate::error::N34Result; use crate::nostr_utils::traits::NostrKeyringErrorUtils; @@ -143,10 +149,29 @@ pub fn post_cli(mut cli: Cli) -> N34Result { cli.options.relays = relays.iter().cloned().map(RelayOrSet::Relay).collect(); } - if cli.options.bunker_url.is_none() - && let Some(bunker_url) = &cli.options.config.bunker_url + // Automatically sets the signer based on the configuration if no signer + // is provided. + if !cli.options.nip07 + && cli.options.bunker_url.is_none() + && (cli.options.secret_key.is_none() || cli.options.config.keyring_secret_key) { - cli.options.bunker_url = Some(bunker_url.clone()); + if let Some(addr) = cli.options.config.nip07 { + cli.options.nip07 = true; + cli.options.state.browser_signer_proxy = BrowserSignerProxy::new( + BrowserSignerProxyOptions::default() + .timeout(BROWSER_SIGNER_PROXY_TIMEOUT) + .ip_addr(addr.ip()) + .port(addr.port()), + ); + } else if let Some(bunker_url) = &cli.options.config.bunker_url { + cli.options.bunker_url = Some(bunker_url.clone()); + } else if cli.options.config.keyring_secret_key { + cli.options.secret_key = Some( + Cli::user_keypair(cli.options.secret_key)? + .secret_key() + .clone(), + ); + } } Ok(cli) diff --git a/src/cli/options_state.rs b/src/cli/options_state.rs new file mode 100644 index 0000000..9b5521c --- /dev/null +++ b/src/cli/options_state.rs @@ -0,0 +1,56 @@ +// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr +// Copyright (C) 2025 Awiteb +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::{ + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + time::Duration, +}; + +use nostr_browser_signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions}; + +/// The default socket address used for the NIP-07 signer proxy, set to +/// localhost on port 51034. +pub const DEFAULT_NIP07_PROXY_ADDR: SocketAddr = + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 51034)); + +/// How long to wait for the proxy response (3 minutes). +pub const BROWSER_SIGNER_PROXY_TIMEOUT: Duration = Duration::from_secs(60 * 3); + + +/// Represents the state used for CLI options. +pub struct OptionsState { + /// The browser signer proxy, will be used if `--nip07` is enabled + pub browser_signer_proxy: BrowserSignerProxy, +} + +impl Default for OptionsState { + fn default() -> Self { + Self { + browser_signer_proxy: default_browser_signer_proxy(), + } + } +} + +/// Build the default browser signer proxy +#[inline] +fn default_browser_signer_proxy() -> BrowserSignerProxy { + BrowserSignerProxy::new( + BrowserSignerProxyOptions::default() + .timeout(BROWSER_SIGNER_PROXY_TIMEOUT) + .ip_addr(DEFAULT_NIP07_PROXY_ADDR.ip()) + .port(DEFAULT_NIP07_PROXY_ADDR.port()), + ) +} diff --git a/src/cli/parsers.rs b/src/cli/parsers.rs index dbe27c3..aba0950 100644 --- a/src/cli/parsers.rs +++ b/src/cli/parsers.rs @@ -75,8 +75,8 @@ pub fn parse_config_path(config_path: &str) -> N34Result { CliConfig::load(path) } -// Parses a bunker URL and checks if it's a valid Nostr Connect URI. -// Returns an error if the URL is not a valid bunker URL. +/// Parses a bunker URL and checks if it's a valid Nostr Connect URI. +/// Returns an error if the URL is not a valid bunker URL. pub fn parse_bunker_url(bunker_url: &str) -> N34Result { match NostrConnectURI::parse(bunker_url) { Ok(url) if url.is_bunker() => Ok(url), diff --git a/src/error.rs b/src/error.rs index ddac1c1..61169b6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::process::ExitCode; +use std::{net::AddrParseError, process::ExitCode}; use nostr::{ event::{Kind, builder::Error as EventBuilderError}, @@ -47,6 +47,10 @@ pub enum N34Error { Io(#[from] std::io::Error), #[error("Signer Error: {0}")] SignerError(#[from] SignerError), + #[error("Invalid Browser Signer Proxy Address: {0}")] + Addr(#[from] AddrParseError), + #[error("Browser Signer Proxy Error: {0}")] + BrowserSignerProxy(#[from] nostr_browser_signer_proxy::Error), #[error("Keyring error: {0}")] Keyring(#[from] nostr_keyring::Error), #[error("{0}")] @@ -92,8 +96,8 @@ pub enum N34Error { #[error("One naddr is required for this command")] EmptyNaddrs, #[error( - "This command requires a signer to sign events. Use `--secret-key` or `--bunker-url` to \ - provide a signer" + "This command requires a signer to sign events. Use `--secret-key`, `--nip07` or \ + `--bunker-url` to provide a signer" )] SignerRequired, #[error( diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs index 8c063dc..f870fcc 100644 --- a/src/nostr_utils/mod.rs +++ b/src/nostr_utils/mod.rs @@ -114,7 +114,7 @@ impl NostrClient { let mut client_builder = Client::builder().opts(ClientOptions::new().verify_subscriptions(true)); - if let Ok(Some(signer)) = options.signer() { + if let Ok(Some(signer)) = options.signer().await { client_builder = client_builder.signer(signer); }