Skip to content

Commit fc7ef64

Browse files
benthecarmanclaude
andcommitted
Add TLS support for RPC/CLI
With TLS we can now have encrypted communication with the client and server. We auto generate a self signed cert on startup. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent ee1c96c commit fc7ef64

File tree

9 files changed

+374
-26
lines changed

9 files changed

+374
-26
lines changed

Cargo.lock

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

ldk-server-cli/src/main.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ struct Cli {
4949
#[arg(short, long, required(true))]
5050
api_key: String,
5151

52+
#[arg(
53+
short,
54+
long,
55+
required(true),
56+
help = "Path to the server's TLS certificate file (PEM format). Found at <server_storage_dir>/tls_cert.pem"
57+
)]
58+
tls_cert: String,
59+
5260
#[command(subcommand)]
5361
command: Commands,
5462
}
@@ -217,7 +225,18 @@ enum Commands {
217225
#[tokio::main]
218226
async fn main() {
219227
let cli = Cli::parse();
220-
let client = LdkServerClient::new(cli.base_url, cli.api_key);
228+
229+
// Load server certificate for TLS verification
230+
let server_cert_pem = std::fs::read(&cli.tls_cert).unwrap_or_else(|e| {
231+
eprintln!("Failed to read server certificate file '{}': {}", cli.tls_cert, e);
232+
std::process::exit(1);
233+
});
234+
235+
let client =
236+
LdkServerClient::new(cli.base_url, cli.api_key, &server_cert_pem).unwrap_or_else(|e| {
237+
eprintln!("Failed to create client: {e}");
238+
std::process::exit(1);
239+
});
221240

222241
match cli.command {
223242
Commands::GetNodeInfo => {

ldk-server-client/src/client.rs

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,16 @@ use ldk_server_protos::endpoints::{
3333
};
3434
use ldk_server_protos::error::{ErrorCode, ErrorResponse};
3535
use reqwest::header::CONTENT_TYPE;
36-
use reqwest::Client;
36+
use reqwest::{Certificate, Client};
3737
use std::time::{SystemTime, UNIX_EPOCH};
3838

3939
const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
4040

4141
/// Client to access a hosted instance of LDK Server.
42+
///
43+
/// The client requires the server's TLS certificate to be provided for verification.
44+
/// This certificate can be found at `<server_storage_dir>/tls_cert.pem` after the
45+
/// server generates it on first startup.
4246
#[derive(Clone)]
4347
pub struct LdkServerClient {
4448
base_url: String,
@@ -48,9 +52,21 @@ pub struct LdkServerClient {
4852

4953
impl LdkServerClient {
5054
/// Constructs a [`LdkServerClient`] using `base_url` as the ldk-server endpoint.
55+
///
56+
/// `base_url` should not include the scheme, e.g., `localhost:3000`.
5157
/// `api_key` is used for HMAC-based authentication.
52-
pub fn new(base_url: String, api_key: String) -> Self {
53-
Self { base_url, client: Client::new(), api_key }
58+
/// `server_cert_pem` is the server's TLS certificate in PEM format. This can be
59+
/// found at `<server_storage_dir>/tls_cert.pem` after the server starts.
60+
pub fn new(base_url: String, api_key: String, server_cert_pem: &[u8]) -> Result<Self, String> {
61+
let cert = Certificate::from_pem(server_cert_pem)
62+
.map_err(|e| format!("Failed to parse server certificate: {e}"))?;
63+
64+
let client = Client::builder()
65+
.add_root_certificate(cert)
66+
.build()
67+
.map_err(|e| format!("Failed to build HTTP client: {e}"))?;
68+
69+
Ok(Self { base_url, client, api_key })
5470
}
5571

5672
/// Computes the HMAC-SHA256 authentication header value.
@@ -75,7 +91,7 @@ impl LdkServerClient {
7591
pub async fn get_node_info(
7692
&self, request: GetNodeInfoRequest,
7793
) -> Result<GetNodeInfoResponse, LdkServerError> {
78-
let url = format!("http://{}/{GET_NODE_INFO_PATH}", self.base_url);
94+
let url = format!("https://{}/{GET_NODE_INFO_PATH}", self.base_url);
7995
self.post_request(&request, &url).await
8096
}
8197

@@ -84,7 +100,7 @@ impl LdkServerClient {
84100
pub async fn get_balances(
85101
&self, request: GetBalancesRequest,
86102
) -> Result<GetBalancesResponse, LdkServerError> {
87-
let url = format!("http://{}/{GET_BALANCES_PATH}", self.base_url);
103+
let url = format!("https://{}/{GET_BALANCES_PATH}", self.base_url);
88104
self.post_request(&request, &url).await
89105
}
90106

@@ -93,7 +109,7 @@ impl LdkServerClient {
93109
pub async fn onchain_receive(
94110
&self, request: OnchainReceiveRequest,
95111
) -> Result<OnchainReceiveResponse, LdkServerError> {
96-
let url = format!("http://{}/{ONCHAIN_RECEIVE_PATH}", self.base_url);
112+
let url = format!("https://{}/{ONCHAIN_RECEIVE_PATH}", self.base_url);
97113
self.post_request(&request, &url).await
98114
}
99115

@@ -102,7 +118,7 @@ impl LdkServerClient {
102118
pub async fn onchain_send(
103119
&self, request: OnchainSendRequest,
104120
) -> Result<OnchainSendResponse, LdkServerError> {
105-
let url = format!("http://{}/{ONCHAIN_SEND_PATH}", self.base_url);
121+
let url = format!("https://{}/{ONCHAIN_SEND_PATH}", self.base_url);
106122
self.post_request(&request, &url).await
107123
}
108124

@@ -111,7 +127,7 @@ impl LdkServerClient {
111127
pub async fn bolt11_receive(
112128
&self, request: Bolt11ReceiveRequest,
113129
) -> Result<Bolt11ReceiveResponse, LdkServerError> {
114-
let url = format!("http://{}/{BOLT11_RECEIVE_PATH}", self.base_url);
130+
let url = format!("https://{}/{BOLT11_RECEIVE_PATH}", self.base_url);
115131
self.post_request(&request, &url).await
116132
}
117133

@@ -120,7 +136,7 @@ impl LdkServerClient {
120136
pub async fn bolt11_send(
121137
&self, request: Bolt11SendRequest,
122138
) -> Result<Bolt11SendResponse, LdkServerError> {
123-
let url = format!("http://{}/{BOLT11_SEND_PATH}", self.base_url);
139+
let url = format!("https://{}/{BOLT11_SEND_PATH}", self.base_url);
124140
self.post_request(&request, &url).await
125141
}
126142

@@ -129,7 +145,7 @@ impl LdkServerClient {
129145
pub async fn bolt12_receive(
130146
&self, request: Bolt12ReceiveRequest,
131147
) -> Result<Bolt12ReceiveResponse, LdkServerError> {
132-
let url = format!("http://{}/{BOLT12_RECEIVE_PATH}", self.base_url);
148+
let url = format!("https://{}/{BOLT12_RECEIVE_PATH}", self.base_url);
133149
self.post_request(&request, &url).await
134150
}
135151

@@ -138,7 +154,7 @@ impl LdkServerClient {
138154
pub async fn bolt12_send(
139155
&self, request: Bolt12SendRequest,
140156
) -> Result<Bolt12SendResponse, LdkServerError> {
141-
let url = format!("http://{}/{BOLT12_SEND_PATH}", self.base_url);
157+
let url = format!("https://{}/{BOLT12_SEND_PATH}", self.base_url);
142158
self.post_request(&request, &url).await
143159
}
144160

@@ -147,7 +163,7 @@ impl LdkServerClient {
147163
pub async fn open_channel(
148164
&self, request: OpenChannelRequest,
149165
) -> Result<OpenChannelResponse, LdkServerError> {
150-
let url = format!("http://{}/{OPEN_CHANNEL_PATH}", self.base_url);
166+
let url = format!("https://{}/{OPEN_CHANNEL_PATH}", self.base_url);
151167
self.post_request(&request, &url).await
152168
}
153169

@@ -156,7 +172,7 @@ impl LdkServerClient {
156172
pub async fn splice_in(
157173
&self, request: SpliceInRequest,
158174
) -> Result<SpliceInResponse, LdkServerError> {
159-
let url = format!("http://{}/{SPLICE_IN_PATH}", self.base_url);
175+
let url = format!("https://{}/{SPLICE_IN_PATH}", self.base_url);
160176
self.post_request(&request, &url).await
161177
}
162178

@@ -165,7 +181,7 @@ impl LdkServerClient {
165181
pub async fn splice_out(
166182
&self, request: SpliceOutRequest,
167183
) -> Result<SpliceOutResponse, LdkServerError> {
168-
let url = format!("http://{}/{SPLICE_OUT_PATH}", self.base_url);
184+
let url = format!("https://{}/{SPLICE_OUT_PATH}", self.base_url);
169185
self.post_request(&request, &url).await
170186
}
171187

@@ -174,7 +190,7 @@ impl LdkServerClient {
174190
pub async fn close_channel(
175191
&self, request: CloseChannelRequest,
176192
) -> Result<CloseChannelResponse, LdkServerError> {
177-
let url = format!("http://{}/{CLOSE_CHANNEL_PATH}", self.base_url);
193+
let url = format!("https://{}/{CLOSE_CHANNEL_PATH}", self.base_url);
178194
self.post_request(&request, &url).await
179195
}
180196

@@ -183,7 +199,7 @@ impl LdkServerClient {
183199
pub async fn force_close_channel(
184200
&self, request: ForceCloseChannelRequest,
185201
) -> Result<ForceCloseChannelResponse, LdkServerError> {
186-
let url = format!("http://{}/{FORCE_CLOSE_CHANNEL_PATH}", self.base_url);
202+
let url = format!("https://{}/{FORCE_CLOSE_CHANNEL_PATH}", self.base_url);
187203
self.post_request(&request, &url).await
188204
}
189205

@@ -192,7 +208,7 @@ impl LdkServerClient {
192208
pub async fn list_channels(
193209
&self, request: ListChannelsRequest,
194210
) -> Result<ListChannelsResponse, LdkServerError> {
195-
let url = format!("http://{}/{LIST_CHANNELS_PATH}", self.base_url);
211+
let url = format!("https://{}/{LIST_CHANNELS_PATH}", self.base_url);
196212
self.post_request(&request, &url).await
197213
}
198214

@@ -201,7 +217,7 @@ impl LdkServerClient {
201217
pub async fn list_payments(
202218
&self, request: ListPaymentsRequest,
203219
) -> Result<ListPaymentsResponse, LdkServerError> {
204-
let url = format!("http://{}/{LIST_PAYMENTS_PATH}", self.base_url);
220+
let url = format!("https://{}/{LIST_PAYMENTS_PATH}", self.base_url);
205221
self.post_request(&request, &url).await
206222
}
207223

@@ -210,7 +226,7 @@ impl LdkServerClient {
210226
pub async fn update_channel_config(
211227
&self, request: UpdateChannelConfigRequest,
212228
) -> Result<UpdateChannelConfigResponse, LdkServerError> {
213-
let url = format!("http://{}/{UPDATE_CHANNEL_CONFIG_PATH}", self.base_url);
229+
let url = format!("https://{}/{UPDATE_CHANNEL_CONFIG_PATH}", self.base_url);
214230
self.post_request(&request, &url).await
215231
}
216232

ldk-server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ hyper = { version = "1", default-features = false, features = ["server", "http1"
1010
http-body-util = { version = "0.1", default-features = false }
1111
hyper-util = { version = "0.1", default-features = false, features = ["server-graceful"] }
1212
tokio = { version = "1.38.0", default-features = false, features = ["time", "signal", "rt-multi-thread"] }
13+
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
14+
rcgen = { version = "0.13", default-features = false, features = ["ring"] }
1315
prost = { version = "0.11.6", default-features = false, features = ["std"] }
1416
ldk-server-protos = { path = "../ldk-server-protos" }
1517
bytes = { version = "1.4.0", default-features = false }

ldk-server/ldk-server-config.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ dir_path = "/tmp/ldk-server/" # Path for LDK and BDK data persis
1313
level = "Debug" # Log level (Error, Warn, Info, Debug, Trace)
1414
file_path = "/tmp/ldk-server/ldk-server.log" # Log file path
1515

16+
[tls]
17+
#cert_path = "/path/to/tls.crt" # Path to TLS certificate, by default uses dir_path/tls.crt
18+
#key_path = "/path/to/tls.key" # Path to TLS private key, by default uses dir_path/tls.key
19+
hosts = ["example.com"] # Allowed hosts for TLS, will always include "localhost" and "127.0.0.1"
20+
1621
# Must set either bitcoind or esplora settings, but not both
1722

1823
# Bitcoin Core settings

ldk-server/src/main.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use crate::io::persist::{
3636
use crate::util::config::{load_config, ChainSource};
3737
use crate::util::logger::ServerLogger;
3838
use crate::util::proto_adapter::{forwarded_payment_to_proto, payment_to_proto};
39+
use crate::util::tls::get_or_generate_tls_config;
3940
use hex::DisplayHex;
4041
use ldk_node::config::Config;
4142
use ldk_node::lightning::ln::channelmanager::PaymentId;
@@ -155,14 +156,15 @@ fn main() {
155156
},
156157
};
157158

158-
let paginated_store: Arc<dyn PaginatedKVStore> =
159-
Arc::new(match SqliteStore::new(PathBuf::from(config_file.storage_dir_path), None, None) {
159+
let paginated_store: Arc<dyn PaginatedKVStore> = Arc::new(
160+
match SqliteStore::new(PathBuf::from(&config_file.storage_dir_path), None, None) {
160161
Ok(store) => store,
161162
Err(e) => {
162163
error!("Failed to create SqliteStore: {e:?}");
163164
std::process::exit(-1);
164165
},
165-
});
166+
},
167+
);
166168

167169
#[cfg(not(feature = "events-rabbitmq"))]
168170
let event_publisher: Arc<dyn EventPublisher> =
@@ -213,6 +215,20 @@ fn main() {
213215
let rest_svc_listener = TcpListener::bind(config_file.rest_service_addr)
214216
.await
215217
.expect("Failed to bind listening port");
218+
219+
let server_config = match get_or_generate_tls_config(
220+
config_file.tls_config,
221+
&config_file.storage_dir_path,
222+
) {
223+
Ok(config) => config,
224+
Err(e) => {
225+
error!("Failed to set up TLS: {e}");
226+
std::process::exit(-1);
227+
}
228+
};
229+
let tls_acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_config));
230+
info!("TLS enabled for REST service on {}", config_file.rest_service_addr);
231+
216232
loop {
217233
select! {
218234
event = event_node.next_event_async() => {
@@ -355,11 +371,17 @@ fn main() {
355371
res = rest_svc_listener.accept() => {
356372
match res {
357373
Ok((stream, _)) => {
358-
let io_stream = TokioIo::new(stream);
359374
let node_service = NodeService::new(Arc::clone(&node), Arc::clone(&paginated_store), config_file.api_key.clone());
375+
let acceptor = tls_acceptor.clone();
360376
runtime.spawn(async move {
361-
if let Err(err) = http1::Builder::new().serve_connection(io_stream, node_service).await {
362-
error!("Failed to serve connection: {}", err);
377+
match acceptor.accept(stream).await {
378+
Ok(tls_stream) => {
379+
let io_stream = TokioIo::new(tls_stream);
380+
if let Err(err) = http1::Builder::new().serve_connection(io_stream, node_service).await {
381+
error!("Failed to serve TLS connection: {err}");
382+
}
383+
},
384+
Err(e) => error!("TLS handshake failed: {e}"),
363385
}
364386
});
365387
},

0 commit comments

Comments
 (0)