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
This commit is contained in:
Emii Tatsuo 2020-11-25 15:39:00 -05:00
parent 81761b69d1
commit a4f1017c5f
Signed by: Emi
GPG Key ID: 68FAB2E2E6DFC98B
3 changed files with 171 additions and 10 deletions

View File

@ -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"

125
src/gencert.rs Normal file
View File

@ -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<String>),
/// 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<Path>, key: impl AsRef<Path>) -> Result<rcgen::Certificate> {
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<Path>, key: impl AsRef<Path>) -> 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<String> {
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();
}
}
}

View File

@ -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<A> {
rate_limits: RoutingNode<RateLimiter<IpAddr>>,
#[cfg(feature="user_management")]
data_dir: PathBuf,
#[cfg(feature="certgen")]
certgen_mode: CertGenMode,
}
impl<A: ToSocketAddrs> Builder<A> {
@ -261,6 +267,8 @@ impl<A: ToSocketAddrs> Builder<A> {
rate_limits: RoutingNode::default(),
#[cfg(feature="user_management")]
data_dir: "data".into(),
#[cfg(feature="certgen")]
certgen_mode: CertGenMode::Interactive,
}
}
@ -273,6 +281,18 @@ impl<A: ToSocketAddrs> Builder<A> {
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<A: ToSocketAddrs> Builder<A> {
}
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<RoutingNode<RateLimiter<IpAddr>>>)
}
}
fn tls_config(cert_path: &PathBuf, key_path: &PathBuf) -> Result<Arc<ServerConfig>> {
fn tls_config(
cert_path: &PathBuf,
key_path: &PathBuf,
#[cfg(feature = "certgen")]
mode: CertGenMode,
) -> Result<Arc<ServerConfig>> {
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())
}