diff --git a/Cargo.lock b/Cargo.lock index f6f9e2c..e3e5718 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1043,12 +1043,14 @@ dependencies = [ "ddk-messages", "ddk-payouts", "hex", + "hmac", "homedir", "inquire", "prost", "reqwest", "serde", "serde_json", + "sha2", "tokio", "tonic", "tonic-build", diff --git a/ddk-node/Cargo.toml b/ddk-node/Cargo.toml index dd21b8b..e9017cd 100644 --- a/ddk-node/Cargo.toml +++ b/ddk-node/Cargo.toml @@ -19,6 +19,8 @@ bitcoin = { version = "0.32.6", features = ["rand", "serde"] } anyhow = "1.0.86" clap = { version = "4.5.9", features = ["derive"] } hex = "0.4.3" +hmac = "0.12" +sha2 = "0.10" homedir = "0.3.3" inquire = "0.7.5" prost = "0.12.1" diff --git a/ddk-node/README.md b/ddk-node/README.md index ccbd3ed..1d3e100 100644 --- a/ddk-node/README.md +++ b/ddk-node/README.md @@ -31,7 +31,8 @@ Options: -n, --network Set the Bitcoin network [default: signet] -s, --storage-dir Data storage path [default: ~/.ddk] -p, --port Transport listening port [default: 1776] - --grpc gRPC server host:port [default: 0.0.0.0:3030] + --grpc gRPC server host:port [default: 127.0.0.1:3030] + --api-secret HMAC secret for gRPC authentication --esplora Esplora server URL [default: https://mutinynet.com/api] --oracle Kormir oracle URL [default: https://kormir.dlcdevkit.com] --seed Seed strategy: 'file' or 'bytes' [default: file] @@ -39,6 +40,8 @@ Options: -h, --help Print help ``` +When binding to non-localhost addresses (e.g., `--grpc 0.0.0.0:3030`), an API secret is required via `--api-secret`. + ## CLI Usage ``` diff --git a/ddk-node/src/bin/cli.rs b/ddk-node/src/bin/cli.rs index 14a391d..0d64fb3 100644 --- a/ddk-node/src/bin/cli.rs +++ b/ddk-node/src/bin/cli.rs @@ -1,6 +1,18 @@ use clap::Parser; use ddk_node::cli_opts::CliCommand; use ddk_node::ddkrpc::ddk_rpc_client::DdkRpcClient; +use hmac::{Hmac, Mac}; +use sha2::Sha256; +use tonic::metadata::MetadataValue; +use tonic::transport::Channel; + +type HmacSha256 = Hmac; + +fn compute_signature(timestamp: &str, secret: &[u8]) -> String { + let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size"); + mac.update(timestamp.as_bytes()); + hex::encode(mac.finalize().into_bytes()) +} #[derive(Debug, Clone, Parser)] #[clap(name = "ddk-cli")] @@ -14,6 +26,9 @@ struct DdkCliArgs { #[arg(help = "ddk-node gRPC server to connect to.")] #[arg(default_value = "http://127.0.0.1:3030")] pub server: String, + #[arg(long)] + #[arg(help = "HMAC secret for authentication")] + pub api_secret: Option, #[clap(subcommand)] pub command: CliCommand, } @@ -22,9 +37,33 @@ struct DdkCliArgs { async fn main() -> anyhow::Result<()> { let opts = DdkCliArgs::parse(); - let mut client = DdkRpcClient::connect(opts.server).await?; + if let Some(secret) = opts.api_secret { + let channel = Channel::from_shared(opts.server)?.connect().await?; + let secret_bytes = secret.into_bytes(); + + let mut client = + DdkRpcClient::with_interceptor(channel, move |mut req: tonic::Request<()>| { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + .to_string(); + + let signature = compute_signature(×tamp, &secret_bytes); + + let ts_value: MetadataValue<_> = timestamp.parse().unwrap(); + let sig_value: MetadataValue<_> = signature.parse().unwrap(); + + req.metadata_mut().insert("x-timestamp", ts_value); + req.metadata_mut().insert("x-signature", sig_value); + Ok(req) + }); - ddk_node::command::cli_command(opts.command, &mut client).await?; + ddk_node::command::cli_command(opts.command, &mut client).await?; + } else { + let mut client = DdkRpcClient::connect(opts.server).await?; + ddk_node::command::cli_command(opts.command, &mut client).await?; + } Ok(()) } diff --git a/ddk-node/src/command.rs b/ddk-node/src/command.rs index f2f1dd3..c7b14b1 100644 --- a/ddk-node/src/command.rs +++ b/ddk-node/src/command.rs @@ -27,12 +27,15 @@ use ddk_messages::oracle_msgs::{EventDescriptor, OracleAnnouncement}; use ddk_messages::{AcceptDlc, OfferDlc}; use inquire::{Select, Text}; use serde_json::Value; -use tonic::transport::Channel; -pub async fn cli_command( - arg: CliCommand, - client: &mut DdkRpcClient, -) -> anyhow::Result<()> { +pub async fn cli_command(arg: CliCommand, client: &mut DdkRpcClient) -> anyhow::Result<()> +where + T: tonic::client::GrpcService + Send + 'static, + T::Error: Into>, + T::ResponseBody: tonic::codegen::Body + Send + 'static, + ::Error: + Into> + Send, +{ match arg { CliCommand::Info => { let info = client.info(InfoRequest::default()).await?.into_inner(); @@ -306,9 +309,16 @@ async fn generate_contract_input() -> anyhow::Result { }) } -async fn interactive_contract_input( - client: &mut DdkRpcClient, -) -> anyhow::Result { +async fn interactive_contract_input( + client: &mut DdkRpcClient, +) -> anyhow::Result +where + T: tonic::client::GrpcService + Send + 'static, + T::Error: Into>, + T::ResponseBody: tonic::codegen::Body + Send + 'static, + ::Error: + Into> + Send, +{ let contract_type = Select::new("Select type of contract.", vec!["enum", "numerical"]).prompt()?; diff --git a/ddk-node/src/ddkrpc.rs b/ddk-node/src/ddkrpc.rs index ad25d1d..89ce09b 100644 --- a/ddk-node/src/ddkrpc.rs +++ b/ddk-node/src/ddkrpc.rs @@ -277,8 +277,8 @@ pub struct SignResponse { /// Generated client implementations. pub mod ddk_rpc_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] - use tonic::codegen::*; use tonic::codegen::http::Uri; + use tonic::codegen::*; #[derive(Debug, Clone)] pub struct DdkRpcClient { inner: tonic::client::Grpc, @@ -322,9 +322,8 @@ pub mod ddk_rpc_client { >::ResponseBody, >, >, - , - >>::Error: Into + Send + Sync, + >>::Error: + Into + Send + Sync, { DdkRpcClient::new(InterceptedService::new(inner, interceptor)) } @@ -363,131 +362,103 @@ pub mod ddk_rpc_client { &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/Info"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "Info")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "Info")); self.inner.unary(req, path, codec).await } pub async fn send_offer( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/SendOffer"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "SendOffer")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "SendOffer")); self.inner.unary(req, path, codec).await } pub async fn accept_offer( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/ddkrpc.DdkRpc/AcceptOffer", - ); + let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/AcceptOffer"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "AcceptOffer")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "AcceptOffer")); self.inner.unary(req, path, codec).await } pub async fn list_offers( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/ListOffers"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "ListOffers")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "ListOffers")); self.inner.unary(req, path, codec).await } pub async fn new_address( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/NewAddress"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "NewAddress")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "NewAddress")); self.inner.unary(req, path, codec).await } pub async fn wallet_balance( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/ddkrpc.DdkRpc/WalletBalance", - ); + let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/WalletBalance"); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("ddkrpc.DdkRpc", "WalletBalance")); @@ -496,64 +467,51 @@ pub mod ddk_rpc_client { pub async fn wallet_sync( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/WalletSync"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "WalletSync")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "WalletSync")); self.inner.unary(req, path, codec).await } pub async fn sync( &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/Sync"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "Sync")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "Sync")); self.inner.unary(req, path, codec).await } pub async fn get_wallet_transactions( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/ddkrpc.DdkRpc/GetWalletTransactions", - ); + let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/GetWalletTransactions"); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("ddkrpc.DdkRpc", "GetWalletTransactions")); @@ -562,115 +520,85 @@ pub mod ddk_rpc_client { pub async fn list_utxos( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/ListUtxos"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "ListUtxos")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "ListUtxos")); self.inner.unary(req, path, codec).await } pub async fn list_peers( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/ListPeers"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "ListPeers")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "ListPeers")); self.inner.unary(req, path, codec).await } pub async fn connect_peer( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/ddkrpc.DdkRpc/ConnectPeer", - ); + let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/ConnectPeer"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "ConnectPeer")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "ConnectPeer")); self.inner.unary(req, path, codec).await } pub async fn list_oracles( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/ddkrpc.DdkRpc/ListOracles", - ); + let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/ListOracles"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "ListOracles")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "ListOracles")); self.inner.unary(req, path, codec).await } pub async fn list_contracts( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/ddkrpc.DdkRpc/ListContracts", - ); + let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/ListContracts"); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("ddkrpc.DdkRpc", "ListContracts")); @@ -680,41 +608,32 @@ pub mod ddk_rpc_client { &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/Send"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "Send")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "Send")); self.inner.unary(req, path, codec).await } pub async fn oracle_announcements( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/ddkrpc.DdkRpc/OracleAnnouncements", - ); + let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/OracleAnnouncements"); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("ddkrpc.DdkRpc", "OracleAnnouncements")); @@ -723,45 +642,34 @@ pub mod ddk_rpc_client { pub async fn create_enum( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/CreateEnum"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("ddkrpc.DdkRpc", "CreateEnum")); + req.extensions_mut() + .insert(GrpcMethod::new("ddkrpc.DdkRpc", "CreateEnum")); self.inner.unary(req, path, codec).await } pub async fn create_numeric( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/ddkrpc.DdkRpc/CreateNumeric", - ); + let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/CreateNumeric"); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("ddkrpc.DdkRpc", "CreateNumeric")); @@ -771,19 +679,14 @@ pub mod ddk_rpc_client { &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/ddkrpc.DdkRpc/SignAnnouncement", - ); + let path = http::uri::PathAndQuery::from_static("/ddkrpc.DdkRpc/SignAnnouncement"); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("ddkrpc.DdkRpc", "SignAnnouncement")); @@ -805,45 +708,27 @@ pub mod ddk_rpc_server { async fn send_offer( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn accept_offer( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn list_offers( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn new_address( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn wallet_balance( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn wallet_sync( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn sync( &self, request: tonic::Request, @@ -851,24 +736,15 @@ pub mod ddk_rpc_server { async fn get_wallet_transactions( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn list_utxos( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn list_peers( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn connect_peer( &self, request: tonic::Request, @@ -876,17 +752,11 @@ pub mod ddk_rpc_server { async fn list_oracles( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn list_contracts( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn send( &self, request: tonic::Request, @@ -894,24 +764,15 @@ pub mod ddk_rpc_server { async fn oracle_announcements( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn create_enum( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn create_numeric( &self, request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; + ) -> std::result::Result, tonic::Status>; async fn sign_announcement( &self, request: tonic::Request, @@ -940,10 +801,7 @@ pub mod ddk_rpc_server { max_encoding_message_size: None, } } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService + pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService where F: tonic::service::Interceptor, { @@ -999,21 +857,15 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/Info" => { #[allow(non_camel_case_types)] struct InfoSvc(pub Arc); - impl tonic::server::UnaryService - for InfoSvc { + impl tonic::server::UnaryService for InfoSvc { type Response = super::InfoResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::info(&inner, request).await - }; + let fut = async move { ::info(&inner, request).await }; Box::pin(fut) } } @@ -1043,21 +895,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/SendOffer" => { #[allow(non_camel_case_types)] struct SendOfferSvc(pub Arc); - impl tonic::server::UnaryService - for SendOfferSvc { + impl tonic::server::UnaryService for SendOfferSvc { type Response = super::SendOfferResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::send_offer(&inner, request).await - }; + let fut = + async move { ::send_offer(&inner, request).await }; Box::pin(fut) } } @@ -1087,23 +934,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/AcceptOffer" => { #[allow(non_camel_case_types)] struct AcceptOfferSvc(pub Arc); - impl< - T: DdkRpc, - > tonic::server::UnaryService - for AcceptOfferSvc { + impl tonic::server::UnaryService for AcceptOfferSvc { type Response = super::AcceptOfferResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::accept_offer(&inner, request).await - }; + let fut = + async move { ::accept_offer(&inner, request).await }; Box::pin(fut) } } @@ -1133,21 +973,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/ListOffers" => { #[allow(non_camel_case_types)] struct ListOffersSvc(pub Arc); - impl tonic::server::UnaryService - for ListOffersSvc { + impl tonic::server::UnaryService for ListOffersSvc { type Response = super::ListOffersResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::list_offers(&inner, request).await - }; + let fut = + async move { ::list_offers(&inner, request).await }; Box::pin(fut) } } @@ -1177,21 +1012,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/NewAddress" => { #[allow(non_camel_case_types)] struct NewAddressSvc(pub Arc); - impl tonic::server::UnaryService - for NewAddressSvc { + impl tonic::server::UnaryService for NewAddressSvc { type Response = super::NewAddressResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::new_address(&inner, request).await - }; + let fut = + async move { ::new_address(&inner, request).await }; Box::pin(fut) } } @@ -1221,23 +1051,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/WalletBalance" => { #[allow(non_camel_case_types)] struct WalletBalanceSvc(pub Arc); - impl< - T: DdkRpc, - > tonic::server::UnaryService - for WalletBalanceSvc { + impl tonic::server::UnaryService for WalletBalanceSvc { type Response = super::WalletBalanceResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::wallet_balance(&inner, request).await - }; + let fut = + async move { ::wallet_balance(&inner, request).await }; Box::pin(fut) } } @@ -1267,21 +1090,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/WalletSync" => { #[allow(non_camel_case_types)] struct WalletSyncSvc(pub Arc); - impl tonic::server::UnaryService - for WalletSyncSvc { + impl tonic::server::UnaryService for WalletSyncSvc { type Response = super::WalletSyncResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::wallet_sync(&inner, request).await - }; + let fut = + async move { ::wallet_sync(&inner, request).await }; Box::pin(fut) } } @@ -1311,21 +1129,15 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/Sync" => { #[allow(non_camel_case_types)] struct SyncSvc(pub Arc); - impl tonic::server::UnaryService - for SyncSvc { + impl tonic::server::UnaryService for SyncSvc { type Response = super::SyncResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::sync(&inner, request).await - }; + let fut = async move { ::sync(&inner, request).await }; Box::pin(fut) } } @@ -1355,23 +1167,18 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/GetWalletTransactions" => { #[allow(non_camel_case_types)] struct GetWalletTransactionsSvc(pub Arc); - impl< - T: DdkRpc, - > tonic::server::UnaryService - for GetWalletTransactionsSvc { + impl tonic::server::UnaryService + for GetWalletTransactionsSvc + { type Response = super::GetWalletTransactionsResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_wallet_transactions(&inner, request) - .await + ::get_wallet_transactions(&inner, request).await }; Box::pin(fut) } @@ -1402,21 +1209,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/ListUtxos" => { #[allow(non_camel_case_types)] struct ListUtxosSvc(pub Arc); - impl tonic::server::UnaryService - for ListUtxosSvc { + impl tonic::server::UnaryService for ListUtxosSvc { type Response = super::ListUtxosResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::list_utxos(&inner, request).await - }; + let fut = + async move { ::list_utxos(&inner, request).await }; Box::pin(fut) } } @@ -1446,21 +1248,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/ListPeers" => { #[allow(non_camel_case_types)] struct ListPeersSvc(pub Arc); - impl tonic::server::UnaryService - for ListPeersSvc { + impl tonic::server::UnaryService for ListPeersSvc { type Response = super::ListPeersResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::list_peers(&inner, request).await - }; + let fut = + async move { ::list_peers(&inner, request).await }; Box::pin(fut) } } @@ -1490,21 +1287,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/ConnectPeer" => { #[allow(non_camel_case_types)] struct ConnectPeerSvc(pub Arc); - impl tonic::server::UnaryService - for ConnectPeerSvc { + impl tonic::server::UnaryService for ConnectPeerSvc { type Response = super::ConnectResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::connect_peer(&inner, request).await - }; + let fut = + async move { ::connect_peer(&inner, request).await }; Box::pin(fut) } } @@ -1534,23 +1326,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/ListOracles" => { #[allow(non_camel_case_types)] struct ListOraclesSvc(pub Arc); - impl< - T: DdkRpc, - > tonic::server::UnaryService - for ListOraclesSvc { + impl tonic::server::UnaryService for ListOraclesSvc { type Response = super::ListOraclesResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::list_oracles(&inner, request).await - }; + let fut = + async move { ::list_oracles(&inner, request).await }; Box::pin(fut) } } @@ -1580,23 +1365,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/ListContracts" => { #[allow(non_camel_case_types)] struct ListContractsSvc(pub Arc); - impl< - T: DdkRpc, - > tonic::server::UnaryService - for ListContractsSvc { + impl tonic::server::UnaryService for ListContractsSvc { type Response = super::ListContractsResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::list_contracts(&inner, request).await - }; + let fut = + async move { ::list_contracts(&inner, request).await }; Box::pin(fut) } } @@ -1626,21 +1404,15 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/Send" => { #[allow(non_camel_case_types)] struct SendSvc(pub Arc); - impl tonic::server::UnaryService - for SendSvc { + impl tonic::server::UnaryService for SendSvc { type Response = super::SendResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::send(&inner, request).await - }; + let fut = async move { ::send(&inner, request).await }; Box::pin(fut) } } @@ -1670,15 +1442,11 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/OracleAnnouncements" => { #[allow(non_camel_case_types)] struct OracleAnnouncementsSvc(pub Arc); - impl< - T: DdkRpc, - > tonic::server::UnaryService - for OracleAnnouncementsSvc { + impl tonic::server::UnaryService + for OracleAnnouncementsSvc + { type Response = super::OracleAnnouncementsResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, @@ -1716,21 +1484,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/CreateEnum" => { #[allow(non_camel_case_types)] struct CreateEnumSvc(pub Arc); - impl tonic::server::UnaryService - for CreateEnumSvc { + impl tonic::server::UnaryService for CreateEnumSvc { type Response = super::CreateEnumResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::create_enum(&inner, request).await - }; + let fut = + async move { ::create_enum(&inner, request).await }; Box::pin(fut) } } @@ -1760,23 +1523,16 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/CreateNumeric" => { #[allow(non_camel_case_types)] struct CreateNumericSvc(pub Arc); - impl< - T: DdkRpc, - > tonic::server::UnaryService - for CreateNumericSvc { + impl tonic::server::UnaryService for CreateNumericSvc { type Response = super::CreateNumericResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::create_numeric(&inner, request).await - }; + let fut = + async move { ::create_numeric(&inner, request).await }; Box::pin(fut) } } @@ -1806,13 +1562,9 @@ pub mod ddk_rpc_server { "/ddkrpc.DdkRpc/SignAnnouncement" => { #[allow(non_camel_case_types)] struct SignAnnouncementSvc(pub Arc); - impl tonic::server::UnaryService - for SignAnnouncementSvc { + impl tonic::server::UnaryService for SignAnnouncementSvc { type Response = super::SignResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, @@ -1847,18 +1599,14 @@ pub mod ddk_rpc_server { }; Box::pin(fut) } - _ => { - Box::pin(async move { - Ok( - http::Response::builder() - .status(200) - .header("grpc-status", "12") - .header("content-type", "application/grpc") - .body(empty_body()) - .unwrap(), - ) - }) - } + _ => Box::pin(async move { + Ok(http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap()) + }), } } } diff --git a/ddk-node/src/lib.rs b/ddk-node/src/lib.rs index 44d2563..c1e8f28 100644 --- a/ddk-node/src/lib.rs +++ b/ddk-node/src/lib.rs @@ -32,14 +32,91 @@ use ddkrpc::{InfoRequest, InfoResponse}; use opts::NodeOpts; use std::str::FromStr; use std::sync::Arc; +use tonic::service::Interceptor; use tonic::transport::Server; use tonic::Request; use tonic::Response; use tonic::Status; use tonic::{async_trait, Code}; +use hmac::{Hmac, Mac}; +use sha2::Sha256; + +type HmacSha256 = Hmac; + type Ddk = DlcDevKit; +const TIMESTAMP_TOLERANCE_SECS: i64 = 300; + +/// HMAC authentication interceptor for gRPC requests. +/// +/// This interceptor verifies requests using HMAC-SHA256 signatures. Clients must send: +/// - `x-timestamp`: Unix timestamp (seconds) when the request was made +/// - `x-signature`: HMAC-SHA256(timestamp, secret) as a hex string +/// +/// The server verifies the signature matches and the timestamp is within 5 minutes. +/// If no secret is configured, all requests are allowed (for localhost-only deployments). +#[derive(Clone)] +pub struct HmacAuthInterceptor { + secret: Option>, +} + +impl HmacAuthInterceptor { + pub fn new(secret: Option) -> Self { + Self { + secret: secret.map(|s| s.into_bytes()), + } + } + + fn verify_signature(&self, timestamp: &str, signature: &str, secret: &[u8]) -> bool { + let Ok(mut mac) = HmacSha256::new_from_slice(secret) else { + return false; + }; + mac.update(timestamp.as_bytes()); + let expected = hex::encode(mac.finalize().into_bytes()); + expected == signature + } +} + +impl Interceptor for HmacAuthInterceptor { + fn call(&mut self, req: Request<()>) -> Result, Status> { + let Some(secret) = &self.secret else { + return Ok(req); + }; + + let metadata = req.metadata(); + + let timestamp = metadata + .get("x-timestamp") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| Status::unauthenticated("Missing x-timestamp header"))?; + + let signature = metadata + .get("x-signature") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| Status::unauthenticated("Missing x-signature header"))?; + + let ts: i64 = timestamp + .parse() + .map_err(|_| Status::unauthenticated("Invalid timestamp format"))?; + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| Status::internal("System time error"))? + .as_secs() as i64; + + if (now - ts).abs() > TIMESTAMP_TOLERANCE_SECS { + return Err(Status::unauthenticated("Timestamp expired")); + } + + if !self.verify_signature(timestamp, signature, secret) { + return Err(Status::unauthenticated("Invalid signature")); + } + + Ok(req) + } +} + #[derive(Clone)] pub struct DdkNode { pub node: Arc, @@ -53,6 +130,17 @@ impl DdkNode { } pub async fn serve(opts: NodeOpts) -> anyhow::Result<()> { + let is_localhost = + opts.grpc_host.starts_with("127.0.0.1") || opts.grpc_host.starts_with("localhost"); + + if !is_localhost && opts.api_secret.is_none() { + anyhow::bail!( + "API secret is required when binding to non-localhost address ({}). \ + Use --api-secret to set a secret or bind to 127.0.0.1 for local-only access.", + opts.grpc_host + ); + } + let logger = Arc::new(Logger::console( "console_logger".to_string(), LogLevel::Info, @@ -102,8 +190,11 @@ impl DdkNode { ddk.start()?; let node = DdkNode::new(ddk); let node_stop = node.node.clone(); + + let interceptor = HmacAuthInterceptor::new(opts.api_secret.clone()); + let server = Server::builder() - .add_service(DdkRpcServer::new(node)) + .add_service(DdkRpcServer::with_interceptor(node, interceptor)) .serve_with_shutdown(opts.grpc_host.parse()?, async { tokio::signal::ctrl_c() .await @@ -111,6 +202,12 @@ impl DdkNode { let _ = node_stop.stop(); }); + if opts.api_secret.is_some() { + tracing::info!("gRPC server starting with HMAC authentication enabled"); + } else { + tracing::info!("gRPC server starting without authentication (localhost only)"); + } + server.await?; Ok(()) diff --git a/ddk-node/src/opts.rs b/ddk-node/src/opts.rs index 01df9d0..9387097 100644 --- a/ddk-node/src/opts.rs +++ b/ddk-node/src/opts.rs @@ -30,9 +30,12 @@ pub struct NodeOpts { #[arg(help = "Listening port for the lightning network transport.")] pub listening_port: u16, #[arg(long = "grpc")] - #[arg(default_value = "0.0.0.0:3030")] + #[arg(default_value = "127.0.0.1:3030")] #[arg(help = "Host and port the gRPC server will run on.")] pub grpc_host: String, + #[arg(long = "api-secret")] + #[arg(help = "HMAC secret for gRPC authentication. Required for non-localhost bindings.")] + pub api_secret: Option, #[arg(long = "esplora")] #[arg(default_value = "https://mutinynet.com/api")] #[arg(help = "Esplora server to connect to.")]