diff --git a/Cargo.lock b/Cargo.lock index ba02fb5..69dbd04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1717,6 +1717,7 @@ name = "ldk-server" version = "0.1.0" dependencies = [ "async-trait", + "base64 0.21.7", "bytes", "chrono", "futures-util", @@ -1751,6 +1752,7 @@ dependencies = [ name = "ldk-server-client" version = "0.1.0" dependencies = [ + "bitcoin_hashes 0.14.0", "ldk-server-protos", "prost", "reqwest 0.11.27", diff --git a/README.md b/README.md index 8ea7071..e3358c4 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,6 @@ cargo run --bin ldk-server ./ldk-server/ldk-server-config.toml Interact with the node using CLI: ``` -./target/debug/ldk-server-cli -b localhost:3002 onchain-receive # To generate onchain-receive address. -./target/debug/ldk-server-cli -b localhost:3002 help # To print help/available commands. +./target/debug/ldk-server-cli -b localhost:3002 --api-key your-secret-api-key onchain-receive # To generate onchain-receive address. +./target/debug/ldk-server-cli -b localhost:3002 --api-key your-secret-api-key help # To print help/available commands. ``` diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 6cbfcd8..bab6408 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -46,6 +46,9 @@ struct Cli { #[arg(short, long, default_value = "localhost:3000")] base_url: String, + #[arg(short, long)] + api_key: String, + #[command(subcommand)] command: Commands, } @@ -214,7 +217,7 @@ enum Commands { #[tokio::main] async fn main() { let cli = Cli::parse(); - let client = LdkServerClient::new(cli.base_url); + let client = LdkServerClient::new(cli.base_url, cli.api_key); match cli.command { Commands::GetNodeInfo => { diff --git a/ldk-server-client/Cargo.toml b/ldk-server-client/Cargo.toml index ca0ffad..13916fa 100644 --- a/ldk-server-client/Cargo.toml +++ b/ldk-server-client/Cargo.toml @@ -11,3 +11,4 @@ serde = ["ldk-server-protos/serde"] ldk-server-protos = { path = "../ldk-server-protos" } reqwest = { version = "0.11.13", default-features = false, features = ["rustls-tls"] } prost = { version = "0.11.6", default-features = false, features = ["std", "prost-derive"] } +bitcoin_hashes = "0.14" diff --git a/ldk-server-client/src/client.rs b/ldk-server-client/src/client.rs index 9983151..3c76060 100644 --- a/ldk-server-client/src/client.rs +++ b/ldk-server-client/src/client.rs @@ -13,6 +13,8 @@ use crate::error::LdkServerError; use crate::error::LdkServerErrorCode::{ AuthError, InternalError, InternalServerError, InvalidRequestError, LightningError, }; +use bitcoin_hashes::hmac::{Hmac, HmacEngine}; +use bitcoin_hashes::{sha256, Hash, HashEngine}; use ldk_server_protos::api::{ Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse, @@ -32,6 +34,7 @@ use ldk_server_protos::endpoints::{ use ldk_server_protos::error::{ErrorCode, ErrorResponse}; use reqwest::header::CONTENT_TYPE; use reqwest::Client; +use std::time::{SystemTime, UNIX_EPOCH}; const APPLICATION_OCTET_STREAM: &str = "application/octet-stream"; @@ -40,12 +43,31 @@ const APPLICATION_OCTET_STREAM: &str = "application/octet-stream"; pub struct LdkServerClient { base_url: String, client: Client, + api_key: String, } impl LdkServerClient { /// Constructs a [`LdkServerClient`] using `base_url` as the ldk-server endpoint. - pub fn new(base_url: String) -> Self { - Self { base_url, client: Client::new() } + /// `api_key` is used for HMAC-based authentication. + pub fn new(base_url: String, api_key: String) -> Self { + Self { base_url, client: Client::new(), api_key } + } + + /// Computes the HMAC-SHA256 authentication header value. + /// Format: "HMAC :" + fn compute_auth_header(&self, body: &[u8]) -> String { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("System time should be after Unix epoch") + .as_secs(); + + // Compute HMAC-SHA256(api_key, timestamp_bytes || body) + let mut hmac_engine: HmacEngine = HmacEngine::new(self.api_key.as_bytes()); + hmac_engine.input(×tamp.to_be_bytes()); + hmac_engine.input(body); + let hmac_result = Hmac::::from_engine(hmac_engine); + + format!("HMAC {}:{}", timestamp, hmac_result) } /// Retrieve the latest node info like `node_id`, `current_best_block` etc. @@ -196,10 +218,12 @@ impl LdkServerClient { &self, request: &Rq, url: &str, ) -> Result { let request_body = request.encode_to_vec(); + let auth_header = self.compute_auth_header(&request_body); let response_raw = self .client .post(url) .header(CONTENT_TYPE, APPLICATION_OCTET_STREAM) + .header("X-Auth", auth_header) .body(request_body) .send() .await diff --git a/ldk-server/Cargo.toml b/ldk-server/Cargo.toml index e1053f7..62f82d3 100644 --- a/ldk-server/Cargo.toml +++ b/ldk-server/Cargo.toml @@ -20,6 +20,7 @@ async-trait = { version = "0.1.85", default-features = false } toml = { version = "0.8.9", default-features = false, features = ["parse"] } chrono = { version = "0.4", default-features = false, features = ["clock"] } log = "0.4.28" +base64 = { version = "0.21", default-features = false, features = ["std"] } # Required for RabittMQ based EventPublisher. Only enabled for `events-rabbitmq` feature. lapin = { version = "2.4.0", features = ["rustls"], default-features = false, optional = true } diff --git a/ldk-server/ldk-server-config.toml b/ldk-server/ldk-server-config.toml index e4343b6..1f3d651 100644 --- a/ldk-server/ldk-server-config.toml +++ b/ldk-server/ldk-server-config.toml @@ -12,6 +12,10 @@ dir_path = "/tmp/ldk-server/" # Path for LDK and BDK data persis level = "Debug" # Log level (Error, Warn, Info, Debug, Trace) file_path = "/tmp/ldk-server/ldk-server.log" # Log file path +# HMAC Authentication (REQUIRED) +[auth] +api_key = "your-secret-api-key" + # Must set either bitcoind or esplora settings, but not both # Bitcoin Core settings diff --git a/ldk-server/src/api/error.rs b/ldk-server/src/api/error.rs index cacb0f0..15a5bca 100644 --- a/ldk-server/src/api/error.rs +++ b/ldk-server/src/api/error.rs @@ -43,6 +43,9 @@ pub(crate) enum LdkServerErrorCode { /// Please refer to [`protos::error::ErrorCode::InvalidRequestError`]. InvalidRequestError, + /// Please refer to [`protos::error::ErrorCode::AuthError`]. + AuthError, + /// Please refer to [`protos::error::ErrorCode::LightningError`]. LightningError, @@ -54,6 +57,7 @@ impl fmt::Display for LdkServerErrorCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { LdkServerErrorCode::InvalidRequestError => write!(f, "InvalidRequestError"), + LdkServerErrorCode::AuthError => write!(f, "AuthError"), LdkServerErrorCode::LightningError => write!(f, "LightningError"), LdkServerErrorCode::InternalServerError => write!(f, "InternalServerError"), } diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index f5c86ae..f635994 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -356,7 +356,7 @@ fn main() { match res { Ok((stream, _)) => { let io_stream = TokioIo::new(stream); - let node_service = NodeService::new(Arc::clone(&node), Arc::clone(&paginated_store)); + let node_service = NodeService::new(Arc::clone(&node), Arc::clone(&paginated_store), config_file.auth_config.clone()); runtime.spawn(async move { if let Err(err) = http1::Builder::new().serve_connection(io_stream, node_service).await { error!("Failed to serve connection: {}", err); diff --git a/ldk-server/src/service.rs b/ldk-server/src/service.rs index 6048a10..3c089a6 100644 --- a/ldk-server/src/service.rs +++ b/ldk-server/src/service.rs @@ -7,6 +7,8 @@ // You may not use this file except in accordance with one or both of these // licenses. +use ldk_node::bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use ldk_node::bitcoin::hashes::{sha256, Hash, HashEngine}; use ldk_node::Node; use http_body_util::{BodyExt, Full, Limited}; @@ -30,7 +32,7 @@ use crate::api::bolt12_receive::handle_bolt12_receive_request; use crate::api::bolt12_send::handle_bolt12_send_request; use crate::api::close_channel::{handle_close_channel_request, handle_force_close_channel_request}; use crate::api::error::LdkServerError; -use crate::api::error::LdkServerErrorCode::InvalidRequestError; +use crate::api::error::LdkServerErrorCode::{AuthError, InvalidRequestError}; use crate::api::get_balances::handle_get_balances_request; use crate::api::get_node_info::handle_get_node_info_request; use crate::api::get_payment_details::handle_get_payment_details_request; @@ -43,6 +45,7 @@ use crate::api::open_channel::handle_open_channel; use crate::api::splice_channel::{handle_splice_in_request, handle_splice_out_request}; use crate::api::update_channel_config::handle_update_channel_config_request; use crate::io::persist::paginated_kv_store::PaginatedKVStore; +use crate::util::config::AuthConfig; use crate::util::proto_adapter::to_error_response; use std::future::Future; use std::pin::Pin; @@ -56,14 +59,80 @@ const MAX_BODY_SIZE: usize = 10 * 1024 * 1024; pub struct NodeService { node: Arc, paginated_kv_store: Arc, + auth_config: AuthConfig, } impl NodeService { - pub(crate) fn new(node: Arc, paginated_kv_store: Arc) -> Self { - Self { node, paginated_kv_store } + pub(crate) fn new( + node: Arc, paginated_kv_store: Arc, auth_config: AuthConfig, + ) -> Self { + Self { node, paginated_kv_store, auth_config } } } +// Maximum allowed time difference between client timestamp and server time (1 minute) +const AUTH_TIMESTAMP_TOLERANCE_SECS: u64 = 60; + +/// Extracts authentication parameters from request headers. +/// Returns (timestamp, hmac_hex) if valid format, or error. +fn extract_auth_params(req: &Request) -> Result<(u64, String), LdkServerError> { + let auth_header = req + .headers() + .get("X-Auth") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| LdkServerError::new(AuthError, "Missing X-Auth header"))?; + + // Format: "HMAC :" + let auth_data = auth_header + .strip_prefix("HMAC ") + .ok_or_else(|| LdkServerError::new(AuthError, "Invalid X-Auth header format"))?; + + let (timestamp_str, hmac_hex) = auth_data + .split_once(':') + .ok_or_else(|| LdkServerError::new(AuthError, "Invalid X-Auth header format"))?; + + let timestamp = timestamp_str + .parse::() + .map_err(|_| LdkServerError::new(AuthError, "Invalid timestamp in X-Auth header"))?; + + // validate hmac_hex is valid hex + if hmac_hex.len() != 64 || !hmac_hex.chars().all(|c| c.is_ascii_hexdigit()) { + return Err(LdkServerError::new(AuthError, "Invalid HMAC in X-Auth header")); + } + + Ok((timestamp, hmac_hex.to_string())) +} + +/// Validates the HMAC authentication after the request body has been read. +fn validate_hmac_auth( + timestamp: u64, provided_hmac_hex: &str, body: &[u8], auth_config: &AuthConfig, +) -> Result<(), LdkServerError> { + // Validate timestamp is within acceptable window + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| LdkServerError::new(AuthError, "System time error"))? + .as_secs(); + + let time_diff = now.abs_diff(timestamp); + if time_diff > AUTH_TIMESTAMP_TOLERANCE_SECS { + return Err(LdkServerError::new(AuthError, "Request timestamp expired")); + } + + // Compute expected HMAC: HMAC-SHA256(api_key, timestamp_bytes || body) + let mut hmac_engine: HmacEngine = HmacEngine::new(auth_config.api_key.as_bytes()); + hmac_engine.input(×tamp.to_be_bytes()); + hmac_engine.input(body); + let expected_hmac = Hmac::::from_engine(hmac_engine); + + // Compare HMACs (constant-time comparison via Hash equality) + let expected_hex = expected_hmac.to_string(); + if expected_hex != provided_hmac_hex { + return Err(LdkServerError::new(AuthError, "Invalid credentials")); + } + + Ok(()) +} + pub(crate) struct Context { pub(crate) node: Arc, pub(crate) paginated_kv_store: Arc, @@ -75,56 +144,155 @@ impl Service> for NodeService { type Future = Pin> + Send>>; fn call(&self, req: Request) -> Self::Future { + // Extract auth params from headers (validation happens after body is read) + let auth_params = match extract_auth_params(&req) { + Ok(params) => params, + Err(e) => { + let (error_response, status_code) = to_error_response(e); + return Box::pin(async move { + Ok(Response::builder() + .status(status_code) + .body(Full::new(Bytes::from(error_response.encode_to_vec()))) + // unwrap safety: body only errors when previous chained calls failed. + .unwrap()) + }); + }, + }; + let context = Context { node: Arc::clone(&self.node), paginated_kv_store: Arc::clone(&self.paginated_kv_store), }; + let auth_config = self.auth_config.clone(); + // Exclude '/' from path pattern matching. match &req.uri().path()[1..] { - GET_NODE_INFO_PATH => { - Box::pin(handle_request(context, req, handle_get_node_info_request)) - }, - GET_BALANCES_PATH => { - Box::pin(handle_request(context, req, handle_get_balances_request)) - }, - ONCHAIN_RECEIVE_PATH => { - Box::pin(handle_request(context, req, handle_onchain_receive_request)) - }, - ONCHAIN_SEND_PATH => { - Box::pin(handle_request(context, req, handle_onchain_send_request)) - }, - BOLT11_RECEIVE_PATH => { - Box::pin(handle_request(context, req, handle_bolt11_receive_request)) - }, - BOLT11_SEND_PATH => Box::pin(handle_request(context, req, handle_bolt11_send_request)), - BOLT12_RECEIVE_PATH => { - Box::pin(handle_request(context, req, handle_bolt12_receive_request)) - }, - BOLT12_SEND_PATH => Box::pin(handle_request(context, req, handle_bolt12_send_request)), - OPEN_CHANNEL_PATH => Box::pin(handle_request(context, req, handle_open_channel)), - SPLICE_IN_PATH => Box::pin(handle_request(context, req, handle_splice_in_request)), - SPLICE_OUT_PATH => Box::pin(handle_request(context, req, handle_splice_out_request)), - CLOSE_CHANNEL_PATH => { - Box::pin(handle_request(context, req, handle_close_channel_request)) - }, - FORCE_CLOSE_CHANNEL_PATH => { - Box::pin(handle_request(context, req, handle_force_close_channel_request)) - }, - LIST_CHANNELS_PATH => { - Box::pin(handle_request(context, req, handle_list_channels_request)) - }, - UPDATE_CHANNEL_CONFIG_PATH => { - Box::pin(handle_request(context, req, handle_update_channel_config_request)) - }, - GET_PAYMENT_DETAILS_PATH => { - Box::pin(handle_request(context, req, handle_get_payment_details_request)) - }, - LIST_PAYMENTS_PATH => { - Box::pin(handle_request(context, req, handle_list_payments_request)) - }, - LIST_FORWARDED_PAYMENTS_PATH => { - Box::pin(handle_request(context, req, handle_list_forwarded_payments_request)) - }, + GET_NODE_INFO_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_get_node_info_request, + )), + GET_BALANCES_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_get_balances_request, + )), + ONCHAIN_RECEIVE_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_onchain_receive_request, + )), + ONCHAIN_SEND_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_onchain_send_request, + )), + BOLT11_RECEIVE_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_bolt11_receive_request, + )), + BOLT11_SEND_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_bolt11_send_request, + )), + BOLT12_RECEIVE_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_bolt12_receive_request, + )), + BOLT12_SEND_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_bolt12_send_request, + )), + OPEN_CHANNEL_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_open_channel, + )), + SPLICE_IN_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_splice_in_request, + )), + SPLICE_OUT_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_splice_out_request, + )), + CLOSE_CHANNEL_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_close_channel_request, + )), + FORCE_CLOSE_CHANNEL_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_force_close_channel_request, + )), + LIST_CHANNELS_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_list_channels_request, + )), + UPDATE_CHANNEL_CONFIG_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_update_channel_config_request, + )), + GET_PAYMENT_DETAILS_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_get_payment_details_request, + )), + LIST_PAYMENTS_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_list_payments_request, + )), + LIST_FORWARDED_PAYMENTS_PATH => Box::pin(handle_request( + context, + req, + auth_params, + auth_config, + handle_list_forwarded_payments_request, + )), path => { let error = format!("Unknown request: {}", path).into_bytes(); Box::pin(async { @@ -144,7 +312,8 @@ async fn handle_request< R: Message, F: Fn(Context, T) -> Result, >( - context: Context, request: Request, handler: F, + context: Context, request: Request, auth_params: (u64, String), + auth_config: AuthConfig, handler: F, ) -> Result<>>::Response, hyper::Error> { // Limit the size of the request body to prevent abuse let limited_body = Limited::new(request.into_body(), MAX_BODY_SIZE); @@ -163,6 +332,17 @@ async fn handle_request< }, }; + // Validate HMAC authentication with the request body + let (timestamp, provided_hmac) = auth_params; + if let Err(e) = validate_hmac_auth(timestamp, &provided_hmac, &bytes, &auth_config) { + let (error_response, status_code) = to_error_response(e); + return Ok(Response::builder() + .status(status_code) + .body(Full::new(Bytes::from(error_response.encode_to_vec()))) + // unwrap safety: body only errors when previous chained calls failed. + .unwrap()); + } + match T::decode(bytes) { Ok(request) => match handler(context, request) { Ok(response) => Ok(Response::builder() @@ -189,3 +369,115 @@ async fn handle_request< }, } } + +#[cfg(test)] +mod tests { + use super::*; + + fn compute_hmac(api_key: &str, timestamp: u64, body: &[u8]) -> String { + let mut hmac_engine: HmacEngine = HmacEngine::new(api_key.as_bytes()); + hmac_engine.input(×tamp.to_be_bytes()); + hmac_engine.input(body); + Hmac::::from_engine(hmac_engine).to_string() + } + + fn create_test_request(auth_header: Option) -> Request<()> { + let mut builder = Request::builder(); + if let Some(header) = auth_header { + builder = builder.header("X-Auth", header); + } + builder.body(()).unwrap() + } + + #[test] + fn test_extract_auth_params_success() { + let timestamp = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + let hmac = "8f5a33c2c68fb253899a588308fd13dcaf162d2788966a1fb6cc3aa2e0c51a93"; + let auth_header = format!("HMAC {timestamp}:{hmac}"); + + let req = create_test_request(Some(auth_header)); + + let result = extract_auth_params(&req); + assert!(result.is_ok()); + let (ts, hmac_hex) = result.unwrap(); + assert_eq!(ts, timestamp); + assert_eq!(hmac_hex, hmac); + } + + #[test] + fn test_extract_auth_params_missing_header() { + let req = create_test_request(None); + + let result = extract_auth_params(&req); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().error_code, AuthError); + } + + #[test] + fn test_extract_auth_params_invalid_format() { + // Missing "HMAC " prefix + let req = create_test_request(Some("12345:deadbeef".to_string())); + + let result = extract_auth_params(&req); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().error_code, AuthError); + } + + #[test] + fn test_validate_hmac_auth_success() { + let auth_config = AuthConfig { api_key: "test_api_key".to_string() }; + let body = b"test request body"; + let timestamp = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + let hmac = compute_hmac(&auth_config.api_key, timestamp, body); + + let result = validate_hmac_auth(timestamp, &hmac, body, &auth_config); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_hmac_auth_wrong_key() { + let auth_config = AuthConfig { api_key: "test_api_key".to_string() }; + let body = b"test request body"; + let timestamp = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + // Compute HMAC with wrong key + let hmac = compute_hmac("wrong_key", timestamp, body); + + let result = validate_hmac_auth(timestamp, &hmac, body, &auth_config); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().error_code, AuthError); + } + + #[test] + fn test_validate_hmac_auth_expired_timestamp() { + let auth_config = AuthConfig { api_key: "test_api_key".to_string() }; + let body = b"test request body"; + // Use a timestamp from 10 minutes ago + let timestamp = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() + - 600; + let hmac = compute_hmac(&auth_config.api_key, timestamp, body); + + let result = validate_hmac_auth(timestamp, &hmac, body, &auth_config); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().error_code, AuthError); + } + + #[test] + fn test_validate_hmac_auth_tampered_body() { + let auth_config = AuthConfig { api_key: "test_api_key".to_string() }; + let original_body = b"test request body"; + let tampered_body = b"tampered body"; + let timestamp = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + // Compute HMAC with original body + let hmac = compute_hmac(&auth_config.api_key, timestamp, original_body); + + // Try to validate with tampered body + let result = validate_hmac_auth(timestamp, &hmac, tampered_body, &auth_config); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().error_code, AuthError); + } +} diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index f09ca86..bb77619 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -24,6 +24,7 @@ pub struct Config { pub listening_addr: SocketAddress, pub alias: Option, pub network: Network, + pub auth_config: AuthConfig, pub rest_service_addr: SocketAddr, pub storage_dir_path: String, pub chain_source: ChainSource, @@ -34,6 +35,11 @@ pub struct Config { pub log_file_path: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AuthConfig { + pub api_key: String, +} + #[derive(Debug)] pub enum ChainSource { Rpc { rpc_address: SocketAddr, rpc_user: String, rpc_password: String }, @@ -144,12 +150,23 @@ impl TryFrom for Config { ))? .into()); + let auth_config = toml_config + .auth + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "`auth` section with `api_key` is required in config file", + ) + }) + .map(|auth| AuthConfig { api_key: auth.api_key })?; + Ok(Config { listening_addr, network: toml_config.node.network, alias, rest_service_addr, storage_dir_path: toml_config.storage.disk.dir_path, + auth_config, chain_source, rabbitmq_connection_string, rabbitmq_exchange_name, @@ -171,6 +188,7 @@ pub struct TomlConfig { rabbitmq: Option, liquidity: Option, log: Option, + auth: Option, } #[derive(Deserialize, Serialize)] @@ -220,6 +238,11 @@ struct RabbitmqConfig { exchange_name: String, } +#[derive(Deserialize, Serialize)] +struct TomlAuthConfig { + api_key: String, +} + #[derive(Deserialize, Serialize)] struct LiquidityConfig { lsps2_service: Option, @@ -304,17 +327,20 @@ mod tests { listening_address = "localhost:3001" rest_service_address = "127.0.0.1:3002" alias = "LDK Server" - + [storage.disk] dir_path = "/tmp" [log] level = "Trace" file = "/var/log/ldk-server.log" - + + [auth] + api_key = "test_api_key" + [esplora] server_url = "https://mempool.space/api" - + [rabbitmq] connection_string = "rabbitmq_connection_string" exchange_name = "rabbitmq_exchange_name" @@ -344,6 +370,7 @@ mod tests { network: Network::Regtest, rest_service_addr: SocketAddr::from_str("127.0.0.1:3002").unwrap(), storage_dir_path: "/tmp".to_string(), + auth_config: AuthConfig { api_key: "test_api_key".to_string() }, chain_source: ChainSource::Esplora { server_url: String::from("https://mempool.space/api"), }, @@ -369,6 +396,7 @@ mod tests { assert_eq!(config.network, expected.network); assert_eq!(config.rest_service_addr, expected.rest_service_addr); assert_eq!(config.storage_dir_path, expected.storage_dir_path); + assert_eq!(config.auth_config, expected.auth_config); let ChainSource::Esplora { server_url } = config.chain_source else { panic!("unexpected config chain source"); }; @@ -389,21 +417,24 @@ mod tests { listening_address = "localhost:3001" rest_service_address = "127.0.0.1:3002" alias = "LDK Server" - + [storage.disk] dir_path = "/tmp" [log] level = "Trace" file = "/var/log/ldk-server.log" - + + [auth] + api_key = "test_api_key" + [electrum] server_url = "ssl://electrum.blockstream.info:50002" - + [rabbitmq] connection_string = "rabbitmq_connection_string" exchange_name = "rabbitmq_exchange_name" - + [liquidity.lsps2_service] advertise_service = false channel_opening_fee_ppm = 1000 # 0.1% fee @@ -433,19 +464,22 @@ mod tests { listening_address = "localhost:3001" rest_service_address = "127.0.0.1:3002" alias = "LDK Server" - + [storage.disk] dir_path = "/tmp" [log] level = "Trace" file = "/var/log/ldk-server.log" - + + [auth] + api_key = "test_api_key" + [bitcoind] rpc_address = "127.0.0.1:8332" # RPC endpoint rpc_user = "bitcoind-testuser" rpc_password = "bitcoind-testpassword" - + [rabbitmq] connection_string = "rabbitmq_connection_string" exchange_name = "rabbitmq_exchange_name" @@ -481,22 +515,25 @@ mod tests { listening_address = "localhost:3001" rest_service_address = "127.0.0.1:3002" alias = "LDK Server" - + [storage.disk] dir_path = "/tmp" [log] level = "Trace" file = "/var/log/ldk-server.log" - + + [auth] + api_key = "test_api_key" + [bitcoind] rpc_address = "127.0.0.1:8332" # RPC endpoint rpc_user = "bitcoind-testuser" rpc_password = "bitcoind-testpassword" - + [esplora] server_url = "https://mempool.space/api" - + [rabbitmq] connection_string = "rabbitmq_connection_string" exchange_name = "rabbitmq_exchange_name" diff --git a/ldk-server/src/util/proto_adapter.rs b/ldk-server/src/util/proto_adapter.rs index 7fb7255..6e42a88 100644 --- a/ldk-server/src/util/proto_adapter.rs +++ b/ldk-server/src/util/proto_adapter.rs @@ -9,7 +9,7 @@ use crate::api::error::LdkServerError; use crate::api::error::LdkServerErrorCode::{ - InternalServerError, InvalidRequestError, LightningError, + AuthError, InternalServerError, InvalidRequestError, LightningError, }; use bytes::Bytes; use hex::prelude::*; @@ -443,12 +443,14 @@ pub(crate) fn proto_to_bolt11_description( pub(crate) fn to_error_response(ldk_error: LdkServerError) -> (ErrorResponse, StatusCode) { let error_code = match ldk_error.error_code { InvalidRequestError => ErrorCode::InvalidRequestError, + AuthError => ErrorCode::AuthError, LightningError => ErrorCode::LightningError, InternalServerError => ErrorCode::InternalServerError, } as i32; let status = match ldk_error.error_code { InvalidRequestError => StatusCode::BAD_REQUEST, + AuthError => StatusCode::UNAUTHORIZED, LightningError => StatusCode::INTERNAL_SERVER_ERROR, InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, };