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:
parent
81761b69d1
commit
a4f1017c5f
|
@ -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
125
src/gencert.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
52
src/lib.rs
52
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<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())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue