diff --git a/Cargo.lock b/Cargo.lock index bb849fd..212a38c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1737,6 +1737,7 @@ dependencies = [ "bytes", "chrono", "futures-util", + "getrandom 0.2.16", "hex-conservative 0.2.1", "http-body-util", "hyper 1.7.0", @@ -1746,8 +1747,7 @@ dependencies = [ "ldk-server-protos", "log", "prost", - "rand 0.8.5", - "rcgen", + "ring", "rusqlite", "serde", "tokio", @@ -2543,18 +2543,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "rcgen" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" -dependencies = [ - "ring", - "rustls-pki-types", - "time", - "yasna", -] - [[package]] name = "reactor-trait" version = "1.1.0" @@ -4069,15 +4057,6 @@ dependencies = [ "time", ] -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "time", -] - [[package]] name = "yoke" version = "0.8.1" diff --git a/ldk-server/Cargo.toml b/ldk-server/Cargo.toml index 965c960..54f4626 100644 --- a/ldk-server/Cargo.toml +++ b/ldk-server/Cargo.toml @@ -11,13 +11,13 @@ http-body-util = { version = "0.1", default-features = false } hyper-util = { version = "0.1", default-features = false, features = ["server-graceful"] } tokio = { version = "1.38.0", default-features = false, features = ["time", "signal", "rt-multi-thread"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } -rcgen = { version = "0.13", default-features = false, features = ["ring"] } +ring = { version = "0.17", default-features = false } +getrandom = { version = "0.2", default-features = false } prost = { version = "0.11.6", default-features = false, features = ["std"] } ldk-server-protos = { path = "../ldk-server-protos" } bytes = { version = "1.4.0", default-features = false } hex = { package = "hex-conservative", version = "0.2.1", default-features = false } rusqlite = { version = "0.31.0", features = ["bundled"] } -rand = { version = "0.8.5", default-features = false } 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"] } diff --git a/ldk-server/src/io/persist/sqlite_store/mod.rs b/ldk-server/src/io/persist/sqlite_store/mod.rs index a52ffa4..45e421e 100644 --- a/ldk-server/src/io/persist/sqlite_store/mod.rs +++ b/ldk-server/src/io/persist/sqlite_store/mod.rs @@ -291,9 +291,8 @@ impl PaginatedKVStore for SqliteStore { mod tests { use std::panic::RefUnwindSafe; + use hex::DisplayHex; use ldk_node::lightning::util::persist::KVSTORE_NAMESPACE_KEY_MAX_LEN; - use rand::distributions::Alphanumeric; - use rand::{thread_rng, Rng}; use super::*; @@ -312,8 +311,9 @@ mod tests { pub(crate) fn random_storage_path() -> PathBuf { let mut temp_path = std::env::temp_dir(); - let mut rng = thread_rng(); - let rand_dir: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); + let mut bytes = [0u8; 8]; + getrandom::getrandom(&mut bytes).expect("Failed to generate random bytes"); + let rand_dir = bytes.to_lower_hex_string(); temp_path.push(rand_dir); temp_path } diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index acc1a9b..bee2712 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -31,7 +31,6 @@ use ldk_server_protos::events::{event_envelope, EventEnvelope}; use ldk_server_protos::types::Payment; use log::{debug, error, info}; use prost::Message; -use rand::Rng; use tokio::net::TcpListener; use tokio::select; use tokio::signal::unix::SignalKind; @@ -408,8 +407,8 @@ fn main() { // We don't expose this payment-id to the user, it is a temporary measure to generate // some unique identifiers until we have forwarded-payment-id available in ldk. // Currently, this is the expected user handling behaviour for forwarded payments. - let mut forwarded_payment_id = [0u8;32]; - rand::thread_rng().fill(&mut forwarded_payment_id); + let mut forwarded_payment_id = [0u8; 32]; + getrandom::getrandom(&mut forwarded_payment_id).expect("Failed to generate random bytes"); let forwarded_payment_creation_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time must be > 1970").as_secs() as i64; @@ -546,9 +545,8 @@ fn load_or_generate_api_key(storage_dir: &Path) -> std::io::Result { Ok(key_bytes.to_lower_hex_string()) } else { // Generate a 32-byte random API key - let mut rng = rand::thread_rng(); let mut key_bytes = [0u8; 32]; - rng.fill(&mut key_bytes); + getrandom::getrandom(&mut key_bytes).map_err(std::io::Error::other)?; // Write the raw bytes to the file fs::write(&api_key_path, key_bytes)?; diff --git a/ldk-server/src/util/tls.rs b/ldk-server/src/util/tls.rs index c0d65fc..aae5e94 100644 --- a/ldk-server/src/util/tls.rs +++ b/ldk-server/src/util/tls.rs @@ -8,20 +8,42 @@ // licenses. use std::fs; +use std::net::IpAddr; use base64::Engine; -use rcgen::{generate_simple_self_signed, CertifiedKey}; +use ring::rand::SystemRandom; +use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING}; use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; use tokio_rustls::rustls::ServerConfig; use crate::util::config::TlsConfig; +// Issuer and Subject common name +const ISSUER_NAME: &str = "localhost"; + // PEM markers const PEM_CERT_BEGIN: &str = "-----BEGIN CERTIFICATE-----"; const PEM_CERT_END: &str = "-----END CERTIFICATE-----"; const PEM_KEY_BEGIN: &str = "-----BEGIN PRIVATE KEY-----"; const PEM_KEY_END: &str = "-----END PRIVATE KEY-----"; +// OIDs +const OID_EC_PUBLIC_KEY: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]; // 1.2.840.10045.2.1 +const OID_PRIME256V1: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; // 1.2.840.10045.3.1.7 +const OID_ECDSA_WITH_SHA256: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02]; // 1.2.840.10045.4.3.2 +const OID_COMMON_NAME: &[u8] = &[0x55, 0x04, 0x03]; // 2.5.4.3 +const OID_SUBJECT_ALT_NAME: &[u8] = &[0x55, 0x1D, 0x11]; // 2.5.29.17 + +// DER tag constants (universal class, bits 7-6 = 00) +const TAG_INTEGER: u8 = 0x02; +const TAG_BIT_STRING: u8 = 0x03; +const TAG_OCTET_STRING: u8 = 0x04; +const TAG_OID: u8 = 0x06; +const TAG_UTF8_STRING: u8 = 0x0C; +const TAG_UTC_TIME: u8 = 0x17; +const TAG_SEQUENCE: u8 = 0x30; +const TAG_SET: u8 = 0x31; + /// Gets or generates TLS configuration. If custom paths are provided, uses those. /// Otherwise, generates a self-signed certificate in the storage directory. pub fn get_or_generate_tls_config( @@ -93,43 +115,283 @@ fn generate_self_signed_cert( let mut hosts = vec!["localhost".to_string(), "127.0.0.1".to_string()]; hosts.extend_from_slice(configure_hosts); - let CertifiedKey { cert, key_pair } = generate_simple_self_signed(hosts) - .map_err(|e| format!("Failed to generate self-signed certificate: {e}"))?; - - // Convert DER to PEM format - let cert_der = cert.der(); - let key_der = key_pair.serialize_der(); - - let cert_pem = format!( - "{PEM_CERT_BEGIN}\n{}\n{PEM_CERT_END}\n", - base64::engine::general_purpose::STANDARD - .encode(cert_der) - .as_bytes() - .chunks(64) - .map(|chunk| std::str::from_utf8(chunk).unwrap()) - .collect::>() - .join("\n") - ); - - let key_pem = format!( - "{PEM_KEY_BEGIN}\n{}\n{PEM_KEY_END}\n", - base64::engine::general_purpose::STANDARD - .encode(&key_der) - .as_bytes() - .chunks(64) - .map(|chunk| std::str::from_utf8(chunk).unwrap()) - .collect::>() - .join("\n") - ); + let rng = SystemRandom::new(); + + // Generate ECDSA P-256 key pair + let pkcs8_doc = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng) + .map_err(|e| format!("Failed to generate key pair: {e}"))?; + + let key_pair = + EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8_doc.as_ref(), &rng) + .map_err(|e| format!("Failed to parse generated key pair: {e}"))?; + + // Build the certificate + let cert_der = build_self_signed_cert(&key_pair, &hosts, &rng)?; + + // Convert to PEM format + let cert_pem = der_to_pem(&cert_der, PEM_CERT_BEGIN, PEM_CERT_END); + let key_pem = der_to_pem(pkcs8_doc.as_ref(), PEM_KEY_BEGIN, PEM_KEY_END); - fs::write(cert_path, &cert_pem) - .map_err(|e| format!("Failed to write TLS certificate to '{cert_path}': {e}"))?; fs::write(key_path, &key_pem) .map_err(|e| format!("Failed to write TLS key to '{key_path}': {e}"))?; + fs::write(cert_path, &cert_pem) + .map_err(|e| format!("Failed to write TLS certificate to '{cert_path}': {e}"))?; Ok(()) } +fn der_to_pem(der: &[u8], begin: &str, end: &str) -> String { + let b64 = base64::engine::general_purpose::STANDARD.encode(der); + let lines: Vec<&str> = + b64.as_bytes().chunks(64).map(|c| std::str::from_utf8(c).unwrap()).collect(); + format!("{begin}\n{}\n{end}\n", lines.join("\n")) +} + +/// Build a self-signed X.509 certificate +fn build_self_signed_cert( + key_pair: &EcdsaKeyPair, hosts: &[String], rng: &SystemRandom, +) -> Result, String> { + // Build TBSCertificate + let tbs_cert = build_tbs_certificate(key_pair, hosts)?; + + // Sign the TBSCertificate + let signature = key_pair.sign(rng, &tbs_cert).map_err(|_| "Failed to sign certificate")?; + + // Build final Certificate structure: the signed certificate data, the algorithm used + // to sign it, and the signature itself. + // Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue } + let sig_alg_oid = der_oid(OID_ECDSA_WITH_SHA256); + let sig_alg = der_sequence(&sig_alg_oid); + let sig_value = der_bit_string(signature.as_ref()); + + let cert_content = [tbs_cert, sig_alg, sig_value].concat(); + let cert = der_sequence(&cert_content); + + Ok(cert) +} + +/// Builds the TBSCertificate (To Be Signed Certificate) structure per RFC 5280. +/// +/// This is the core certificate data that gets signed. The structure contains: +/// - Version (v3) +/// - Serial number (fixed to 1) +/// - Signature algorithm identifier +/// - Issuer and subject distinguished names (both set to CN=localhost) +/// - Validity period (2026-01-01 to 2049-12-31) +/// - Subject public key info +/// - Extensions (Subject Alternative Names for the provided hosts) +fn build_tbs_certificate(key_pair: &EcdsaKeyPair, hosts: &[String]) -> Result, String> { + // version [0] EXPLICIT INTEGER { v3(2) } + let version = der_context_explicit(0, &der_integer(&[2])); + + // serialNumber INTEGER (fixed to 1) + let serial_number = der_integer(&[1]); + + // signature AlgorithmIdentifier (ECDSA with SHA-256) + let sig_alg_oid = der_oid(OID_ECDSA_WITH_SHA256); + let signature_alg = der_sequence(&sig_alg_oid); + + // issuer Name (CN=localhost) + let issuer = build_name(ISSUER_NAME); + + // validity (2026-01-01 00:00:00Z to 2049-12-31 23:59:59Z) + // UTCTime format: YYMMDDHHMMSSZ (years 00-49 = 2000-2049, years 50-99 = 1950-1999) + let validity = + der_sequence(&[der_utc_time("260101000000Z"), der_utc_time("491231235959Z")].concat()); + + // subject Name (same as issuer for self-signed) + let subject = build_name(ISSUER_NAME); + + // subjectPublicKeyInfo + let spki = build_subject_public_key_info(key_pair); + + // extensions [3] EXPLICIT Extensions + let extensions = build_extensions(hosts)?; + let extensions_explicit = der_context_explicit(3, &extensions); + + let tbs_content = [ + version, + serial_number, + signature_alg, + issuer, + validity, + subject, + spki, + extensions_explicit, + ] + .concat(); + + Ok(der_sequence(&tbs_content)) +} + +fn build_name(cn: &str) -> Vec { + // A Name (like "CN=ldk-server") is a list of relative distinguished names (RDNs). + // Each RDN is a set of attribute-value pairs (e.g., commonName = "ldk-server"). + // Name ::= SEQUENCE OF RelativeDistinguishedName + // RDN ::= SET OF AttributeTypeAndValue + // AttributeTypeAndValue ::= SEQUENCE { type OID, value ANY } + let cn_attr = der_sequence(&[der_oid(OID_COMMON_NAME), der_utf8_string(cn)].concat()); + let rdn = der_set(&cn_attr); + der_sequence(&rdn) +} + +fn build_subject_public_key_info(key_pair: &EcdsaKeyPair) -> Vec { + // Contains the public key and identifies what type of key it is (EC using P-256 curve). + // SubjectPublicKeyInfo ::= SEQUENCE { + // algorithm AlgorithmIdentifier, + // subjectPublicKey BIT STRING + // } + let algorithm = der_sequence(&[der_oid(OID_EC_PUBLIC_KEY), der_oid(OID_PRIME256V1)].concat()); + + let public_key = key_pair.public_key().as_ref(); + let public_key_bits = der_bit_string(public_key); + + der_sequence(&[algorithm, public_key_bits].concat()) +} + +fn build_extensions(hosts: &[String]) -> Result, String> { + // Extensions add optional features to the certificate. Each extension has an OID + // identifying what it is, an optional critical flag, and the extension data. + // Extensions ::= SEQUENCE OF Extension + // Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE, extnValue OCTET STRING } + + // Build Subject Alternative Name extension + let san_ext = build_san_extension(hosts)?; + + Ok(der_sequence(&san_ext)) +} + +fn build_san_extension(hosts: &[String]) -> Result, String> { + // Subject Alternative Name (SAN) lists the hostnames/IPs the certificate is valid for. + // Each entry is either a DNS name (like "localhost") or an IP address. + // GeneralNames ::= SEQUENCE OF GeneralName + // GeneralName ::= CHOICE { + // dNSName [2] IA5String, + // iPAddress [7] OCTET STRING + // } + let mut general_names = Vec::new(); + + for host in hosts { + if let Ok(ip) = host.parse::() { + // IP address - tag [7] + let ip_bytes = match ip { + IpAddr::V4(v4) => v4.octets().to_vec(), + IpAddr::V6(v6) => v6.octets().to_vec(), + }; + general_names.extend(der_context_implicit(7, &ip_bytes)); + } else { + // DNS name - tag [2] + general_names.extend(der_context_implicit(2, host.as_bytes())); + } + } + + let san_value = der_sequence(&general_names); + let san_octet = der_octet_string(&san_value); + + // Extension ::= SEQUENCE { extnID, extnValue } + // (critical is omitted since it defaults to FALSE) + Ok(der_sequence(&[der_oid(OID_SUBJECT_ALT_NAME), san_octet].concat())) +} + +// DER encoding helpers + +fn der_length_size(len: usize) -> usize { + if len < 128 { + 1 + } else if len < 256 { + 2 + } else if len < 65536 { + 3 + } else { + 4 + } +} + +fn der_tag_length_value(tag: u8, value: &[u8]) -> Vec { + let len = value.len(); + let len_size = der_length_size(len); + let mut result = Vec::with_capacity(1 + len_size + len); + + result.push(tag); + + // Encode length using DER rules: + // - Short form (len < 128): single byte with the length value + // - Long form (len >= 128): first byte is 0x80 | number_of_length_bytes, + // followed by the length in big-endian + if len < 128 { + result.push(len as u8); + } else if len < 256 { + result.push(0x81); // 1 length byte follows + result.push(len as u8); + } else if len < 65536 { + result.push(0x82); // 2 length bytes follow + result.push((len >> 8) as u8); + result.push(len as u8); + } else { + result.push(0x83); // 3 length bytes follow + result.push((len >> 16) as u8); + result.push((len >> 8) as u8); + result.push(len as u8); + } + + result.extend_from_slice(value); + result +} + +fn der_sequence(content: &[u8]) -> Vec { + der_tag_length_value(TAG_SEQUENCE, content) +} + +fn der_set(content: &[u8]) -> Vec { + der_tag_length_value(TAG_SET, content) +} + +fn der_integer(value: &[u8]) -> Vec { + // If high bit is set, prepend 0x00 to indicate positive + if !value.is_empty() && value[0] & 0x80 != 0 { + let mut padded = Vec::with_capacity(1 + value.len()); + padded.push(0x00); + padded.extend_from_slice(value); + der_tag_length_value(TAG_INTEGER, &padded) + } else { + der_tag_length_value(TAG_INTEGER, value) + } +} + +fn der_bit_string(value: &[u8]) -> Vec { + // BIT STRING: first byte is number of unused bits (0 for us) + let mut content = Vec::with_capacity(1 + value.len()); + content.push(0x00); + content.extend_from_slice(value); + der_tag_length_value(TAG_BIT_STRING, &content) +} + +fn der_octet_string(value: &[u8]) -> Vec { + der_tag_length_value(TAG_OCTET_STRING, value) +} + +fn der_oid(oid: &[u8]) -> Vec { + der_tag_length_value(TAG_OID, oid) +} + +fn der_utf8_string(s: &str) -> Vec { + der_tag_length_value(TAG_UTF8_STRING, s.as_bytes()) +} + +fn der_utc_time(s: &str) -> Vec { + der_tag_length_value(TAG_UTC_TIME, s.as_bytes()) +} + +fn der_context_explicit(tag_num: u8, content: &[u8]) -> Vec { + // 0xA0 = context-specific class, constructed + der_tag_length_value(0xA0 | tag_num, content) +} + +fn der_context_implicit(tag_num: u8, content: &[u8]) -> Vec { + // 0x80 = context-specific class, primitive + der_tag_length_value(0x80 | tag_num, content) +} + /// Loads TLS configuration from provided paths. fn load_tls_config(cert_path: &str, key_path: &str) -> Result { let cert_pem = fs::read_to_string(cert_path) @@ -201,7 +463,9 @@ mod tests { #[test] fn test_generate_and_load_roundtrip() { let temp_dir = std::env::temp_dir(); - let suffix: u64 = rand::random(); + let mut suffix_bytes = [0u8; 8]; + getrandom::getrandom(&mut suffix_bytes).unwrap(); + let suffix = u64::from_ne_bytes(suffix_bytes); let cert_path = temp_dir.join(format!("test_tls_cert_{suffix}.pem")); let key_path = temp_dir.join(format!("test_tls_key_{suffix}.pem"));