diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index 9fc8a61bc..54d541ec3 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -1,4 +1,4 @@ -use std::{fs::read_to_string, path::PathBuf, sync::OnceLock}; +use std::{path::PathBuf, sync::OnceLock}; use anyhow::Context; use colored::Colorize; @@ -16,24 +16,17 @@ use komodo_client::entities::{ }, logger::LogConfig, }; -use noise::key::{EncodedKeyPair, SpkiPublicKey}; +use noise::key::{SpkiPublicKey, load_maybe_generate_private_key}; /// Should call in startup to ensure Core errors without valid private key. pub fn core_private_key() -> &'static String { static CORE_PRIVATE_KEY: OnceLock = OnceLock::new(); CORE_PRIVATE_KEY.get_or_init(|| { let config = core_config(); - let Some(private_key) = config.private_key.as_ref() else { - return EncodedKeyPair::generate().unwrap().private; - }; - if let Some(path) = private_key.strip_prefix("file:") { - read_to_string(path) - .with_context(|| { - format!("Failed to read private key at {path:?}") - }) - .unwrap() + if let Some(path) = config.private_key.strip_prefix("file:") { + load_maybe_generate_private_key(path).unwrap() } else { - private_key.clone() + config.private_key.clone() } }) } @@ -62,7 +55,7 @@ pub fn periphery_public_keys() -> Option<&'static [SpkiPublicKey]> { let maybe_pem = if let Some(path) = public_key.strip_prefix("file:") { - read_to_string(path) + std::fs::read_to_string(path) .with_context(|| { format!("Failed to read public key at {path:?}") }) @@ -167,7 +160,7 @@ pub fn core_config() -> &'static CoreConfig { CoreConfig { // Secret things overridden with file private_key: maybe_read_item_from_file(env.komodo_private_key_file, env.komodo_private_key) - .or(config.private_key), + .unwrap_or(config.private_key), passkey: maybe_read_item_from_file(env.komodo_passkey_file, env.komodo_passkey) .or(config.passkey), jwt_secret: maybe_read_item_from_file(env.komodo_jwt_secret_file, env.komodo_jwt_secret).unwrap_or(config.jwt_secret), diff --git a/bin/periphery/src/config.rs b/bin/periphery/src/config.rs index d5389463b..b1d72cc08 100644 --- a/bin/periphery/src/config.rs +++ b/bin/periphery/src/config.rs @@ -11,22 +11,19 @@ use komodo_client::entities::{ config::periphery::{CliArgs, Env, PeripheryConfig}, logger::{LogConfig, LogLevel}, }; -use noise::key::{EncodedKeyPair, SpkiPublicKey}; +use noise::key::{SpkiPublicKey, load_maybe_generate_private_key}; /// Should call in startup to ensure Periphery errors without valid private key. pub fn periphery_private_key() -> &'static String { static PERIPHERY_PRIVATE_KEY: OnceLock = OnceLock::new(); PERIPHERY_PRIVATE_KEY.get_or_init(|| { let config = periphery_config(); - let Some(private_key) = config.private_key.as_ref() else { - return EncodedKeyPair::generate().unwrap().private; - }; + let private_key = config.private_key.clone().unwrap_or(format!( + "file:{}/keys/periphery.key", + config.root_directory.display() + )); if let Some(path) = private_key.strip_prefix("file:") { - read_to_string(path) - .with_context(|| { - format!("Failed to read private key at {path:?}") - }) - .unwrap() + load_maybe_generate_private_key(path).unwrap() } else { private_key.clone() } diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index da1087c6b..0f95e2f06 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -327,15 +327,19 @@ pub struct CoreConfig { #[serde(default)] pub internet_interface: String, - /// Default private key to use with Noise handshake to authenticate with Periphery agents. - /// If not provided, will use randomly generated one on startup. + /// Private key to use with Noise handshake to authenticate with Periphery agents. /// /// Supports openssl generated pem file, `openssl genpkey -algorithm X25519 -out private.key`. - /// To load from file, use `private_key = "file:/path/to/private.key"` + /// To load from file, use `private_key = "file:/path/to/private.key"`. /// - /// Note. The private key used can be overridden for individual Servers / Builders - #[serde(skip_serializing_if = "Option::is_none")] - pub private_key: Option, + /// If a file is specified and does not exist, will try to generate one at the path + /// and use it going forward. + /// + /// Note. The private key used can be overridden for individual Servers / Builders. + /// + /// Default: file:/config/keys/core.key + #[serde(default = "default_private_key")] + pub private_key: String, /// Default accepted public keys to allow Periphery to connect. /// Core gains knowledge of the Periphery public key through the noise handshake. @@ -679,6 +683,10 @@ fn default_core_bind_ip() -> String { "[::]".to_string() } +fn default_private_key() -> String { + String::from("file:/config/keys/core.key") +} + fn default_frontend_path() -> String { "/app/frontend".to_string() } @@ -798,13 +806,11 @@ impl CoreConfig { host: config.host, port: config.port, bind_ip: config.bind_ip, - private_key: self.private_key.as_ref().map(|private_key| { - if private_key.starts_with("file:") { - private_key.clone() - } else { - empty_or_redacted(private_key) - } - }), + private_key: if self.private_key.starts_with("file:") { + self.private_key.clone() + } else { + empty_or_redacted(&self.private_key) + }, periphery_public_keys: config.periphery_public_keys, passkey: config.passkey.as_deref().map(empty_or_redacted), timezone: config.timezone, diff --git a/client/core/rs/src/entities/config/periphery.rs b/client/core/rs/src/entities/config/periphery.rs index 0fa15b203..189257500 100644 --- a/client/core/rs/src/entities/config/periphery.rs +++ b/client/core/rs/src/entities/config/periphery.rs @@ -212,10 +212,14 @@ pub struct Env { #[derive(Debug, Clone, Deserialize)] pub struct PeripheryConfig { /// The private key used with noise handshake. - /// If not provided, uses randomly generated one on startup. /// /// Supports openssl generated pem file, `openssl genpkey -algorithm X25519 -out private.key`. /// To load from file, use `private_key = "file:/path/to/private.key"` + /// + /// If a file is specified and does not exist, will try to generate one at the path + /// and use it going forward. + /// + /// Default: ${root_directory}/keys/periphery.key #[serde(skip_serializing_if = "Option::is_none")] pub private_key: Option, /// Optionally pin a specific Core public key for additional trust. diff --git a/lib/noise/src/key/command.rs b/lib/noise/src/key/command.rs index 1e5bc4e43..44bd00650 100644 --- a/lib/noise/src/key/command.rs +++ b/lib/noise/src/key/command.rs @@ -13,15 +13,19 @@ pub async fn handle(command: &KeyCommand) -> anyhow::Result<()> { .context("Failed to generate key pair")?; match format { KeyOutputFormat::Standard => { - println!("\nPrivate Key: {}", keys.private.red().bold()); - println!("Public Key: {}", keys.public.bold()); + println!( + "\nPrivate Key: {}", + keys.private.as_str().red().bold() + ); + println!("Public Key: {}", keys.public.as_str().bold()); } KeyOutputFormat::Json => { - print_json(&keys.private, &keys.public)? - } - KeyOutputFormat::JsonPretty => { - print_json_pretty(&keys.private, &keys.public)? + print_json(keys.private.as_str(), keys.public.as_str())? } + KeyOutputFormat::JsonPretty => print_json_pretty( + keys.private.as_str(), + keys.public.as_str(), + )?, } Ok(()) diff --git a/lib/noise/src/key/mod.rs b/lib/noise/src/key/mod.rs index 64a9d8935..c7ac71c82 100644 --- a/lib/noise/src/key/mod.rs +++ b/lib/noise/src/key/mod.rs @@ -1,3 +1,5 @@ +use std::{path::PathBuf, str::FromStr}; + use anyhow::Context; use der::AnyRef; @@ -21,9 +23,9 @@ fn algorithm() -> spki::AlgorithmIdentifier> { pub struct EncodedKeyPair { /// pkcs8 encoded private key - pub private: String, + pub private: Pkcs8PrivateKey, /// spki encoded public key - pub public: String, + pub public: SpkiPublicKey, } impl EncodedKeyPair { @@ -32,10 +34,31 @@ impl EncodedKeyPair { let keypair = builder .generate_keypair() .context("Failed to generate keypair")?; - let private = - Pkcs8PrivateKey::from_raw_bytes(&keypair.private)?.into_inner(); - let public = - SpkiPublicKey::from_raw_bytes(&keypair.public)?.into_inner(); + let private = Pkcs8PrivateKey::from_raw_bytes(&keypair.private)?; + let public = SpkiPublicKey::from_raw_bytes(&keypair.public)?; Ok(EncodedKeyPair { private, public }) } } + +pub fn load_maybe_generate_private_key( + path: &str, +) -> anyhow::Result { + let path = path.as_ref(); + let path = PathBuf::from_str(path) + .with_context(|| format!("Invalid private key path: {path:?}"))?; + if path + .try_exists() + .with_context(|| format!("Invalid private key path: {path:?}"))? + { + // Already exists, load it + std::fs::read_to_string(&path).with_context(|| { + format!("Failed to read private key at {path:?}") + }) + } else { + // Generate and write pems to path + let keys = EncodedKeyPair::generate()?; + keys.private.write_pem(&path)?; + keys.public.write_pem(path.with_extension("pub"))?; + Ok(keys.private.into_inner()) + } +} diff --git a/lib/noise/src/key/private.rs b/lib/noise/src/key/private.rs index b997e962d..a6dea89d5 100644 --- a/lib/noise/src/key/private.rs +++ b/lib/noise/src/key/private.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use anyhow::{Context, anyhow}; use base64::{Engine as _, prelude::BASE64_STANDARD}; use der::{Decode as _, Encode as _, asn1::OctetStringRef}; @@ -26,6 +28,26 @@ impl Pkcs8PrivateKey { self.0 } + pub fn as_pem(&self) -> String { + let private_key = &self.0; + format!( + r#"-----BEGIN PRIVATE KEY----- +{private_key} +-----END PRIVATE KEY----- +"# + ) + } + + pub fn write_pem>( + &self, + path: P, + ) -> anyhow::Result<()> { + let path = path.as_ref(); + std::fs::write(path, self.as_pem()).with_context(|| { + format!("Failed to write private key pem to {path:?}") + }) + } + pub fn from_raw_bytes(private_key: &[u8]) -> anyhow::Result { if private_key.len() > 32 { return Err(anyhow!( @@ -65,6 +87,8 @@ impl Pkcs8PrivateKey { Self::raw_bytes(self.0.as_bytes()) } + /// Converts pkcs8 base64 bytes + /// to raw private key pub fn raw_bytes( pkcs8_private_key: &[u8], ) -> anyhow::Result> { @@ -74,21 +98,9 @@ impl Pkcs8PrivateKey { Self::raw_bytes_after_decode(&decoded) } - fn raw_bytes_after_decode( - decoded: &[u8], - ) -> anyhow::Result> { - let pki = pkcs8::PrivateKeyInfo::from_der(decoded) - .map_err(anyhow::Error::msg) - .context("Failed to parse pki from der")?; - if pki.algorithm.oid != super::OID_X25519 { - return Err(anyhow!("Private key is not X25519")); - } - let octet = OctetStringRef::from_der(pki.private_key) - .map_err(anyhow::Error::msg) - .context("Failed to get octet string ref from private key")?; - Ok(octet.as_bytes().to_vec()) - } - + /// - For raw bytes: clones and returns + /// - For pkcs8 base64: converts and returns + /// - For pkcs8 base64 pem: unwraps and returns pub fn maybe_raw_bytes( maybe_pkcs8_private_key: &str, ) -> anyhow::Result> { @@ -113,6 +125,21 @@ impl Pkcs8PrivateKey { } } + fn raw_bytes_after_decode( + decoded: &[u8], + ) -> anyhow::Result> { + let pki = pkcs8::PrivateKeyInfo::from_der(decoded) + .map_err(anyhow::Error::msg) + .context("Failed to parse pki from der")?; + if pki.algorithm.oid != super::OID_X25519 { + return Err(anyhow!("Private key is not X25519")); + } + let octet = OctetStringRef::from_der(pki.private_key) + .map_err(anyhow::Error::msg) + .context("Failed to get octet string ref from private key")?; + Ok(octet.as_bytes().to_vec()) + } + pub fn compute_public_key( &self, ) -> anyhow::Result { diff --git a/lib/noise/src/key/public.rs b/lib/noise/src/key/public.rs index a47b6d7bd..0a86a717e 100644 --- a/lib/noise/src/key/public.rs +++ b/lib/noise/src/key/public.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use anyhow::{Context, anyhow}; use base64::{Engine as _, prelude::BASE64_STANDARD}; use der::{Decode as _, Encode as _, asn1::BitStringRef}; @@ -27,6 +29,26 @@ impl SpkiPublicKey { self.0 } + pub fn as_pem(&self) -> String { + let public_key = &self.0; + format!( + r#"-----BEGIN PUBLIC KEY----- +{public_key} +-----END PUBLIC KEY----- +"# + ) + } + + pub fn write_pem>( + &self, + path: P, + ) -> anyhow::Result<()> { + let path = path.as_ref(); + std::fs::write(path, self.as_pem()).with_context(|| { + format!("Failed to write private key pem to {path:?}") + }) + } + /// Accepts pem rfc7468 (openssl) or base64 der (second line of rfc7468 pem). pub fn from_maybe_pem( public_key_maybe_pem: &str,