1313//! locating the server's TLS certificate and API key on disk, so multiple clients (CLI, MCP
1414//! bridge, etc.) can resolve connection credentials in a consistent way.
1515
16- use std:: path:: PathBuf ;
16+ use std:: io:: { self , ErrorKind , Read } ;
17+ use std:: path:: { Path , PathBuf } ;
1718
1819use hex_conservative:: DisplayHex ;
1920use serde:: { Deserialize , Serialize } ;
2021
2122const DEFAULT_CONFIG_FILE : & str = "config.toml" ;
2223const DEFAULT_CERT_FILE : & str = "tls.crt" ;
2324const API_KEY_FILE : & str = "api_key" ;
25+ const API_KEY_LEN : usize = 32 ;
26+ const CONFIG_FILE_SIZE_LIMIT : usize = 1024 * 1024 ;
27+ const TLS_CERT_FILE_SIZE_LIMIT : usize = 1024 * 1024 ;
2428
2529/// Default address of the `ldk-server` gRPC endpoint when no explicit value is configured.
2630pub const DEFAULT_GRPC_SERVICE_ADDRESS : & str = "127.0.0.1:3536" ;
@@ -124,13 +128,21 @@ impl Config {
124128}
125129
126130/// Reads and parses the `ldk-server` configuration file at `path`.
127- pub fn load_config ( path : & PathBuf ) -> Result < Config , String > {
128- let contents = std :: fs :: read_to_string ( path)
131+ pub fn load_config ( path : & Path ) -> Result < Config , String > {
132+ let contents = read_to_string_with_limit ( path, CONFIG_FILE_SIZE_LIMIT )
129133 . map_err ( |e| format ! ( "Failed to read config file '{}': {}" , path. display( ) , e) ) ?;
130134 toml:: from_str ( & contents)
131135 . map_err ( |e| format ! ( "Failed to parse config file '{}': {}" , path. display( ) , e) )
132136}
133137
138+ /// Reads the server TLS certificate at `path`.
139+ ///
140+ /// Returns an error if the file exceeds 1 MiB.
141+ pub fn read_tls_certificate ( path : & Path ) -> Result < Vec < u8 > , String > {
142+ read_with_limit ( path, TLS_CERT_FILE_SIZE_LIMIT )
143+ . map_err ( |e| format ! ( "Failed to read server certificate file '{}': {e}" , path. display( ) ) )
144+ }
145+
134146/// Resolves the base URL of the `ldk-server` gRPC endpoint.
135147///
136148/// Prefers `override_url`, falls back to the configuration file, and finally to
@@ -146,18 +158,65 @@ pub fn resolve_base_url(override_url: Option<String>, config: Option<&Config>) -
146158/// Prefers `override_key`, falls back to reading the API key file from the configured storage
147159/// directory, and finally from the OS-specific default data directory. The raw bytes read from
148160/// disk are lower-hex encoded before being returned.
149- pub fn resolve_api_key ( override_key : Option < String > , config : Option < & Config > ) -> Option < String > {
150- override_key. or_else ( || {
151- let network =
152- config. and_then ( |c| c. network ( ) . ok ( ) ) . unwrap_or_else ( || "bitcoin" . to_string ( ) ) ;
153- storage_dir ( config)
154- . map ( |dir| api_key_path_for_storage_dir ( dir, & network) )
155- . and_then ( |path| std:: fs:: read ( & path) . ok ( ) )
156- . or_else ( || {
157- get_default_api_key_path ( & network) . and_then ( |path| std:: fs:: read ( & path) . ok ( ) )
158- } )
159- . map ( |bytes| bytes. to_lower_hex_string ( ) )
160- } )
161+ ///
162+ /// Returns an error if a candidate API key file exists but cannot be read or does not contain
163+ /// exactly 32 bytes.
164+ pub fn resolve_api_key (
165+ override_key : Option < String > , config : Option < & Config > ,
166+ ) -> Result < Option < String > , String > {
167+ if override_key. is_some ( ) {
168+ return Ok ( override_key) ;
169+ }
170+
171+ let network = config. and_then ( |c| c. network ( ) . ok ( ) ) . unwrap_or_else ( || "bitcoin" . to_string ( ) ) ;
172+ if let Some ( dir) = storage_dir ( config) {
173+ let path = api_key_path_for_storage_dir ( dir, & network) ;
174+ if let Some ( api_key) = read_api_key ( & path) ? {
175+ return Ok ( Some ( api_key) ) ;
176+ }
177+ }
178+
179+ match get_default_api_key_path ( & network) {
180+ Some ( path) => read_api_key ( & path) ,
181+ None => Ok ( None ) ,
182+ }
183+ }
184+
185+ fn read_api_key ( path : & Path ) -> Result < Option < String > , String > {
186+ let file = match std:: fs:: File :: open ( path) {
187+ Ok ( file) => file,
188+ Err ( e) if e. kind ( ) == ErrorKind :: NotFound => return Ok ( None ) ,
189+ Err ( e) => return Err ( format ! ( "Failed to read API key file '{}': {e}" , path. display( ) ) ) ,
190+ } ;
191+ let mut bytes = Vec :: with_capacity ( API_KEY_LEN + 1 ) ;
192+ file. take ( ( API_KEY_LEN + 1 ) as u64 )
193+ . read_to_end ( & mut bytes)
194+ . map_err ( |e| format ! ( "Failed to read API key file '{}': {e}" , path. display( ) ) ) ?;
195+ if bytes. len ( ) != API_KEY_LEN {
196+ return Err ( format ! (
197+ "API key file '{}' must contain exactly {API_KEY_LEN} bytes" ,
198+ path. display( )
199+ ) ) ;
200+ }
201+ Ok ( Some ( bytes. to_lower_hex_string ( ) ) )
202+ }
203+
204+ fn read_with_limit ( path : & Path , limit : usize ) -> io:: Result < Vec < u8 > > {
205+ let file = std:: fs:: File :: open ( path) ?;
206+ let mut contents = Vec :: new ( ) ;
207+ file. take ( limit. saturating_add ( 1 ) as u64 ) . read_to_end ( & mut contents) ?;
208+ if contents. len ( ) > limit {
209+ return Err ( io:: Error :: new (
210+ io:: ErrorKind :: InvalidData ,
211+ format ! ( "File '{}' exceeds the {limit} byte limit" , path. display( ) ) ,
212+ ) ) ;
213+ }
214+ Ok ( contents)
215+ }
216+
217+ fn read_to_string_with_limit ( path : & Path , limit : usize ) -> io:: Result < String > {
218+ String :: from_utf8 ( read_with_limit ( path, limit) ?)
219+ . map_err ( |e| io:: Error :: new ( io:: ErrorKind :: InvalidData , e) )
161220}
162221
163222/// Resolves the path to the server's TLS certificate (PEM).
@@ -187,7 +246,10 @@ fn default_grpc_service_address() -> String {
187246
188247#[ cfg( test) ]
189248mod tests {
190- use super :: { resolve_base_url, Config , DEFAULT_GRPC_SERVICE_ADDRESS } ;
249+ use super :: {
250+ load_config, read_tls_certificate, resolve_base_url, Config , CONFIG_FILE_SIZE_LIMIT ,
251+ DEFAULT_GRPC_SERVICE_ADDRESS , TLS_CERT_FILE_SIZE_LIMIT ,
252+ } ;
191253
192254 #[ test]
193255 fn config_defaults_grpc_service_address ( ) {
@@ -282,4 +344,28 @@ mod tests {
282344 fn resolve_base_url_falls_back_to_default ( ) {
283345 assert_eq ! ( resolve_base_url( None , None ) , DEFAULT_GRPC_SERVICE_ADDRESS ) ;
284346 }
347+
348+ #[ test]
349+ fn read_tls_certificate_rejects_oversized_file ( ) {
350+ let path = std:: env:: temp_dir ( )
351+ . join ( format ! ( "ldk-server-client-oversized-cert-{}" , std:: process:: id( ) ) ) ;
352+ std:: fs:: write ( & path, vec ! [ 0 ; TLS_CERT_FILE_SIZE_LIMIT + 1 ] ) . unwrap ( ) ;
353+
354+ let error = read_tls_certificate ( & path) . unwrap_err ( ) ;
355+ assert ! ( error. contains( "exceeds" ) ) ;
356+
357+ std:: fs:: remove_file ( path) . unwrap ( ) ;
358+ }
359+
360+ #[ test]
361+ fn load_config_rejects_oversized_file ( ) {
362+ let path = std:: env:: temp_dir ( )
363+ . join ( format ! ( "ldk-server-client-oversized-config-{}" , std:: process:: id( ) ) ) ;
364+ std:: fs:: write ( & path, vec ! [ b'a' ; CONFIG_FILE_SIZE_LIMIT + 1 ] ) . unwrap ( ) ;
365+
366+ let error = load_config ( & path) . unwrap_err ( ) ;
367+ assert ! ( error. contains( "exceeds" ) ) ;
368+
369+ std:: fs:: remove_file ( path) . unwrap ( ) ;
370+ }
285371}
0 commit comments