From a4f1017c5ffd1aae9076f22cd158bff618a72124 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 15:39:00 -0500 Subject: [PATCH] Added automatic certificate generation (Closes #4) All my roommates left and now my apartment is lonely, but at least I can still pour unhealthy amounts of time into software projects nobody will ever use --- Cargo.toml | 4 +- src/gencert.rs | 125 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 52 ++++++++++++++++---- 3 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 src/gencert.rs diff --git a/Cargo.toml b/Cargo.toml index 25a1338..f6abbf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,13 @@ readme = "README.md" include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"] [features] +default = ["serve_dir", "certgen"] user_management = ["sled", "bincode", "serde/derive", "crc32fast"] user_management_advanced = ["rust-argon2", "ring", "user_management"] user_management_routes = ["user_management"] -default = ["serve_dir"] serve_dir = ["mime_guess", "tokio/fs"] ratelimiting = ["dashmap"] +certgen = ["rcgen"] [dependencies] anyhow = "1.0.33" @@ -38,6 +39,7 @@ serde = { version = "1.0", optional = true } rust-argon2 = { version = "0.8.2", optional = true } crc32fast = { version = "1.2.1", optional = true } ring = { version = "0.16.15", optional = true } +rcgen = { version = "0.8.5", optional = true } [dev-dependencies] env_logger = "0.8.1" diff --git a/src/gencert.rs b/src/gencert.rs new file mode 100644 index 0000000..224c65b --- /dev/null +++ b/src/gencert.rs @@ -0,0 +1,125 @@ +use anyhow::{bail, Context, Result}; +use rustls::ServerConfig; + +use std::fs; +use std::io::{stdin, stdout, Write}; +use std::path::Path; + +#[derive(Clone, Debug)] +/// The mode to use for determining the domains to use for a new certificate. Only +/// applies to [`CertGenMode::gencert()`]. +pub enum CertGenMode { + + /// Do not generate any certificates. Error if not available. + None, + + /// Use a provided set of domains + Preset(Vec), + + /// Prompt the user using stdin/stdout to enter domains. + Interactive, +} + +impl CertGenMode { + /// Generate a new self-signed certificate + /// + /// Assumes that certificates do not already exist, and will overwrite anything at the + /// provided paths. The paths provided should be paths to non-existant files which + /// the program has access to write to. + /// + /// ## Errors + /// + /// Returns an error if [`CertGenMode::None`], or if there is an error generating the + /// certificate, or writing to either of the provided files. + pub fn gencert(self, cert: impl AsRef, key: impl AsRef) -> Result { + let (domains, interactive) = match self { + Self::None => bail!("Automatic certificate generation disabled"), + Self::Preset(domains) => (domains, false), + Self::Interactive => (prompt_domains(), true), + }; + + let certificate = rcgen::generate_simple_self_signed(domains) + .context("Could not generate a certificate with the given domains")?; + + fs::create_dir_all( + cert.as_ref() + .parent() + .expect("Received directory as certificate path, should be a file.") + )?; + + fs::create_dir_all( + key.as_ref() + .parent() + .expect("Received directory as key path, should be a file.") + )?; + + fs::write(cert.as_ref(), &certificate.serialize_pem()?.as_bytes()) + .context("Failed to write newly generated certificate to file")?; + fs::write(key.as_ref(), &certificate.serialize_private_key_pem().as_bytes()) + .context("Failed to write newly generated private key to file")?; + + if interactive { + println!("Certificate generated successfully!"); + } + + Ok(certificate) + } + + /// Attempts to load a certificate/key from a file, or generate it if not found + /// + /// The produced certificate & key is immediately fed to a [`rustls::ServerConfig`] + /// + /// See [`CertGenMode::gencert()`] for more info. + /// + /// ## Errors + /// + /// Returns an error if a certificate is not found **and** cannot be generated. + pub fn load_or_generate(self, to: &mut ServerConfig, cert: impl AsRef, key: impl AsRef) -> Result<()> { + match (crate::load_cert_chain(&cert.as_ref().into()), crate::load_key(&key.as_ref().into())) { + (Ok(cert_chain), Ok(key)) => { + to.set_single_cert(cert_chain, key) + .context("Failed to use loaded TLS certificate")?; + }, + (Err(e), _) | (_, Err(e)) => { + warn!("Failed to load certificate from file: {}, now trying automatic generation", e); + let cert = self.gencert(cert, key).context("Could not generate certificate")?; + to.set_single_cert( + vec![rustls::Certificate(cert.serialize_der()?)], + rustls::PrivateKey(cert.serialize_private_key_der()) + )?; + } + } + Ok(()) + } +} + +/// Attempt to get domains by prompting the user +/// +/// Guaranteed to return at least one domain. The user is provided `localhost` as a +/// default. +/// +/// ## Panics +/// Panics if reading from stdin or writing to stdout returns an error. +pub fn prompt_domains() -> Vec { + let mut domains = Vec::with_capacity(1); + let mut input = String::with_capacity(8); + println!("Now generating self-signed certificate..."); + print!("Please enter a domain (CN) for your certificate [localhost]: "); + stdout().flush().unwrap(); + loop { + stdin().read_line(&mut input).unwrap(); + let domain = input.trim(); + if domain.is_empty() { + if domains.is_empty() { + println!("Using `localhost` as domain."); + domains.push("localhost".to_string()); + } + return domains; + } else { + domains.push(domain.to_owned()); + input.clear(); + print!("Add another domain, or finish with a blank input: "); + stdout().flush().unwrap(); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 827b21b..1c8c7d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,9 +37,13 @@ pub mod handling; pub mod ratelimiting; #[cfg(feature = "user_management")] pub mod user_management; +#[cfg(feature = "certgen")] +pub mod gencert; #[cfg(feature="user_management")] use user_management::UserManager; +#[cfg(feature = "certgen")] +use gencert::CertGenMode; pub use mime; pub use uriparse as uri; @@ -246,6 +250,8 @@ pub struct Builder { rate_limits: RoutingNode>, #[cfg(feature="user_management")] data_dir: PathBuf, + #[cfg(feature="certgen")] + certgen_mode: CertGenMode, } impl Builder { @@ -261,6 +267,8 @@ impl Builder { rate_limits: RoutingNode::default(), #[cfg(feature="user_management")] data_dir: "data".into(), + #[cfg(feature="certgen")] + certgen_mode: CertGenMode::Interactive, } } @@ -273,6 +281,18 @@ impl Builder { self } + #[cfg(feature="certgen")] + /// Determine where certificate config comes from, if generation is required + /// + /// If a certificate & keyfile are not available on the provided path, they can be + /// generated. If this happens, several modes can be used to generate them, including + /// not generating them and just erroring, interactively prompting the user for + /// information, and using pre-provided information. + pub fn set_certificate_generation_mode(mut self, mode: CertGenMode) -> Self { + self.certgen_mode = mode; + self + } + /// Sets the directory that kochab should look for TLS certs and keys into /// /// Northstar will look for files called `cert.pem` and `key.pem` in the provided @@ -401,8 +421,12 @@ impl Builder { } pub async fn serve(mut self) -> Result<()> { - let config = tls_config(&self.cert_path, &self.key_path) - .context("Failed to create TLS config")?; + let config = tls_config( + &self.cert_path, + &self.key_path, + #[cfg(feature="certgen")] + self.certgen_mode + ).context("Failed to create TLS config")?; let listener = TcpListener::bind(self.addr).await .context("Failed to create socket")?; @@ -467,15 +491,25 @@ async fn prune_ratelimit_log(rate_limits: Arc>>) } } -fn tls_config(cert_path: &PathBuf, key_path: &PathBuf) -> Result> { +fn tls_config( + cert_path: &PathBuf, + key_path: &PathBuf, + #[cfg(feature = "certgen")] + mode: CertGenMode, +) -> Result> { let mut config = ServerConfig::new(AllowAnonOrSelfsignedClient::new()); - let cert_chain = load_cert_chain(cert_path) - .context("Failed to load TLS certificate")?; - let key = load_key(key_path) - .context("Failed to load TLS key")?; - config.set_single_cert(cert_chain, key) - .context("Failed to use loaded TLS certificate")?; + #[cfg(feature = "certgen")] + mode.load_or_generate(&mut config, cert_path, key_path)?; + + #[cfg(not(feature = "certgen"))] { + let cert_chain = load_cert_chain(cert_path) + .context("Failed to load TLS certificate")?; + let key = load_key(key_path) + .context("Failed to load TLS key")?; + config.set_single_cert(cert_chain, key) + .context("Failed to use loaded TLS certificate")?; + } Ok(config.into()) }