Skip to content

Commit 709162e

Browse files
committed
feat(security): add HMAC authentication for gRPC server
1 parent e5f02e7 commit 709162e

8 files changed

Lines changed: 442 additions & 538 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ddk-node/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ bitcoin = { version = "0.32.6", features = ["rand", "serde"] }
1919
anyhow = "1.0.86"
2020
clap = { version = "4.5.9", features = ["derive"] }
2121
hex = "0.4.3"
22+
hmac = "0.12"
23+
sha2 = "0.10"
2224
homedir = "0.3.3"
2325
inquire = "0.7.5"
2426
prost = "0.12.1"

ddk-node/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,17 @@ Options:
3131
-n, --network <NETWORK> Set the Bitcoin network [default: signet]
3232
-s, --storage-dir <STORAGE_DIR> Data storage path [default: ~/.ddk]
3333
-p, --port <PORT> Transport listening port [default: 1776]
34-
--grpc <GRPC_HOST> gRPC server host:port [default: 0.0.0.0:3030]
34+
--grpc <GRPC_HOST> gRPC server host:port [default: 127.0.0.1:3030]
35+
--api-secret <API_SECRET> HMAC secret for gRPC authentication
3536
--esplora <ESPLORA_HOST> Esplora server URL [default: https://mutinynet.com/api]
3637
--oracle <ORACLE_HOST> Kormir oracle URL [default: https://kormir.dlcdevkit.com]
3738
--seed <SEED> Seed strategy: 'file' or 'bytes' [default: file]
3839
--postgres-url <URL> PostgreSQL connection URL
3940
-h, --help Print help
4041
```
4142

43+
When binding to non-localhost addresses (e.g., `--grpc 0.0.0.0:3030`), an API secret is required via `--api-secret`.
44+
4245
## CLI Usage
4346

4447
```

ddk-node/src/bin/cli.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
use clap::Parser;
22
use ddk_node::cli_opts::CliCommand;
33
use ddk_node::ddkrpc::ddk_rpc_client::DdkRpcClient;
4+
use hmac::{Hmac, Mac};
5+
use sha2::Sha256;
6+
use tonic::metadata::MetadataValue;
7+
use tonic::transport::Channel;
8+
9+
type HmacSha256 = Hmac<Sha256>;
10+
11+
fn compute_signature(timestamp: &str, secret: &[u8]) -> String {
12+
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
13+
mac.update(timestamp.as_bytes());
14+
hex::encode(mac.finalize().into_bytes())
15+
}
416

517
#[derive(Debug, Clone, Parser)]
618
#[clap(name = "ddk-cli")]
@@ -14,6 +26,9 @@ struct DdkCliArgs {
1426
#[arg(help = "ddk-node gRPC server to connect to.")]
1527
#[arg(default_value = "http://127.0.0.1:3030")]
1628
pub server: String,
29+
#[arg(long)]
30+
#[arg(help = "HMAC secret for authentication")]
31+
pub api_secret: Option<String>,
1732
#[clap(subcommand)]
1833
pub command: CliCommand,
1934
}
@@ -22,9 +37,33 @@ struct DdkCliArgs {
2237
async fn main() -> anyhow::Result<()> {
2338
let opts = DdkCliArgs::parse();
2439

25-
let mut client = DdkRpcClient::connect(opts.server).await?;
40+
if let Some(secret) = opts.api_secret {
41+
let channel = Channel::from_shared(opts.server)?.connect().await?;
42+
let secret_bytes = secret.into_bytes();
43+
44+
let mut client =
45+
DdkRpcClient::with_interceptor(channel, move |mut req: tonic::Request<()>| {
46+
let timestamp = std::time::SystemTime::now()
47+
.duration_since(std::time::UNIX_EPOCH)
48+
.expect("Time went backwards")
49+
.as_secs()
50+
.to_string();
51+
52+
let signature = compute_signature(&timestamp, &secret_bytes);
53+
54+
let ts_value: MetadataValue<_> = timestamp.parse().unwrap();
55+
let sig_value: MetadataValue<_> = signature.parse().unwrap();
56+
57+
req.metadata_mut().insert("x-timestamp", ts_value);
58+
req.metadata_mut().insert("x-signature", sig_value);
59+
Ok(req)
60+
});
2661

27-
ddk_node::command::cli_command(opts.command, &mut client).await?;
62+
ddk_node::command::cli_command(opts.command, &mut client).await?;
63+
} else {
64+
let mut client = DdkRpcClient::connect(opts.server).await?;
65+
ddk_node::command::cli_command(opts.command, &mut client).await?;
66+
}
2867

2968
Ok(())
3069
}

ddk-node/src/command.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ use ddk_messages::oracle_msgs::{EventDescriptor, OracleAnnouncement};
2727
use ddk_messages::{AcceptDlc, OfferDlc};
2828
use inquire::{Select, Text};
2929
use serde_json::Value;
30-
use tonic::transport::Channel;
3130

32-
pub async fn cli_command(
33-
arg: CliCommand,
34-
client: &mut DdkRpcClient<Channel>,
35-
) -> anyhow::Result<()> {
31+
pub async fn cli_command<T>(arg: CliCommand, client: &mut DdkRpcClient<T>) -> anyhow::Result<()>
32+
where
33+
T: tonic::client::GrpcService<tonic::body::BoxBody> + Send + 'static,
34+
T::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
35+
T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + 'static,
36+
<T::ResponseBody as tonic::codegen::Body>::Error:
37+
Into<Box<dyn std::error::Error + Send + Sync>> + Send,
38+
{
3639
match arg {
3740
CliCommand::Info => {
3841
let info = client.info(InfoRequest::default()).await?.into_inner();
@@ -306,9 +309,16 @@ async fn generate_contract_input() -> anyhow::Result<ContractInput> {
306309
})
307310
}
308311

309-
async fn interactive_contract_input(
310-
client: &mut DdkRpcClient<Channel>,
311-
) -> anyhow::Result<ContractInput> {
312+
async fn interactive_contract_input<T>(
313+
client: &mut DdkRpcClient<T>,
314+
) -> anyhow::Result<ContractInput>
315+
where
316+
T: tonic::client::GrpcService<tonic::body::BoxBody> + Send + 'static,
317+
T::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
318+
T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + 'static,
319+
<T::ResponseBody as tonic::codegen::Body>::Error:
320+
Into<Box<dyn std::error::Error + Send + Sync>> + Send,
321+
{
312322
let contract_type =
313323
Select::new("Select type of contract.", vec!["enum", "numerical"]).prompt()?;
314324

0 commit comments

Comments
 (0)