💾 Archived View for alchemi.dev › en › projects › kochab › files › src › gencert.rs captured on 2023-11-04 at 11:24:26.

View Raw

More Information

⬅️ Previous capture (2022-07-16)

-=-=-=-=-=-=-

//! Tools for automatically generating certificates
//!
//! Really, the only thing you will probably ever need from this module if you aren't
//! developing the project is the [`CertGenMode`] enum, which can be passed to
//! [`Server::set_certificate_generation_mode()`].  You won't even need to call any
//! methods on it or anything.
//!
//! [`Server::set_certificate_generation_mode()`]: crate::Server::set_certificate_generation_mode()
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.
    ///
    /// This is the default (and only) mode when the `certgen` feature is disabled
    None,

    #[cfg(feature="certgen")]
    /// Use a provided set of domains
    Preset(Vec<String>),

    #[cfg(feature="certgen")]
    /// 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();
        }
    }
}