~zethra/stargazer

dcad5ff55ee5e899bd5fa44ca328fc75bd48c821 — Ben Aaron Goldberg a month ago 9f8ede6 f/cert-conf
tls: add custom route certs

Users can now set custom certs for routes that are not managed by
stargazer.

Signed-off-by: Ben Aaron Goldberg <ben@benaaron.dev>
M doc/stargazer-ini.scd => doc/stargazer-ini.scd +10 -0
@@ 200,6 200,16 @@ Within each routing section, the following keys are used to configure how
	whole number of seconds. By default CGI processes have no timeout and
	stargazer will wait indefinitely for them to finish.

*cert-path*
	Path to this certificate for this route, overrides the default store path.
	This is intended to be used for certs not managed by stargazer. As such
	stargazer will not attempt to generate or regenerate these certs.
	If this is set, *key-path* must also be set.

*key-path*
	Path to the key that goes with the cert set by the *cert-path*. If this is
	set, *cert-path* must also be set.

# CGI Support

*stargazer* supports a limited version of CGI, compatible with the Jetforce

M doc/stargazer.1.txt => doc/stargazer.1.txt +1 -1
@@ 34,4 34,4 @@ AUTHORS
       bugs/patches can be submitted by email to
       ~zethra/stargazer@lists.sr.ht.

			      2021-06-09		  stargazer(1)
			      2021-08-09		  stargazer(1)

M doc/stargazer.ini.5.txt => doc/stargazer.ini.5.txt +21 -3
@@ 60,6 60,13 @@ CONFIGURATION KEYS
	   this in with the name of the organization responsible for
	   the host and it will be filled in as the X.509 /O name.

       gen-certs
	   Set to false to turn off automatic certificate generation.

       regen-certs
	   Set to false to turn off automatic regeneration of expired
	   certificates.

   ROUTING KEYS
       To configure stargazer to service requests, routing keys must
       be defined. The name of the configuration section is used to


@@ 134,8 141,8 @@ CONFIGURATION KEYS
       auto-index
	   "on" to enable the auto-index feature, which presents
	   clients with a list of files in the requested directory
	   when an index file cannot be found. Off by default. Mutu‐
	   ally exclusive with cgi.
	   when an index file cannot be found. Off by default. Only
	   availible for static file routes.

       lang
	   Set this value as the lang parameter for gemini files


@@ 202,6 209,17 @@ CONFIGURATION KEYS
	   number of seconds. By default CGI processes have no timeout
	   and stargazer will wait indefinitely for them to finish.

       cert-path
	   Path to this certificate for this route, overrides the de‐
	   fault store path. This is intended to be used for certs not
	   managed by stargazer. As such stargazer will not attempt to
	   generate or regenerate these certs. If this is set, key-
	   path must also be set.

       key-path
	   Path to the key that goes with the cert set by the cert-
	   path. If this is set, cert-path must also be set.

CGI Support
       stargazer supports a limited version of CGI, compatible with
       the Jetforce server. It is not a faithful implementation of RFC


@@ 331,4 349,4 @@ AUTHORS
       bugs/patches can be submitted by email to
       ~zethra/stargazer@lists.sr.ht.

			      2021-06-09	      stargazer.ini(5)
			      2021-08-09	      stargazer.ini(5)

M src/config.rs => src/config.rs +15 -0
@@ 124,6 124,7 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {

        let mut sites = Vec::with_capacity(5);

        // Load router configs
        for (section, props) in conf.iter_mut() {
            let section = match section {
                Some(section) => section,


@@ 171,6 172,18 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {
                None => None,
            };

            let cert_path = props.remove("cert-path");
            let key_path = props.remove("key-path");
            let cert_key_path = match (cert_path, key_path) {
                (Some(cert_path), Some(key_path)) => {
                    Some((cert_path.into(), key_path.into()))
                }
                (None, None) => None,
                _ => {
                    bail!("If either `cert-path` or `key-path` are set, both must be set");
                }
            };

            let route_type = if scgi {
                let scgi_addr = props.remove("scgi-address").context(
                    "Routes with `scgi` on must include an `scg-address`",


@@ 251,6 264,7 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {
                route_type,
                lang: props.remove("lang"),
                charset: props.remove("charset"),
                cert_key_path,
            });
            check_section_empty(section, props)?;
        }


@@ 302,6 316,7 @@ pub fn dev_config() -> Result<Config> {
                auto_index: true,
            }
            .into(),
            cert_key_path: None,
        }],
        worker_threads: num_cpus::get(),
        request_timeout: 5,

M src/main.rs => src/main.rs +1 -6
@@ 113,12 113,7 @@ fn main() {
    }

    // Configure tls acceptor for all domain names in config
    let domains: Vec<_> = CONF
        .routes
        .iter()
        .map(|site| site.domain.to_owned())
        .collect();
    let acceptor = TlsAcceptor::from(match tls::load_config(&domains) {
    let acceptor = TlsAcceptor::from(match tls::load_config(&CONF.routes) {
        Ok(conf) => Arc::new(conf),
        Err(e) => {
            error!("{:#}", e);

M src/router.rs => src/router.rs +1 -0
@@ 34,6 34,7 @@ pub struct Route {
    pub lang: Option<String>,
    pub charset: Option<String>,
    pub route_type: RouteType,
    pub cert_key_path: Option<(PathBuf, PathBuf)>,
}

#[derive(Debug, Clone)]

M src/tls.rs => src/tls.rs +32 -17
@@ 14,8 14,9 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use crate::router::Route;
use crate::CONF;
use anyhow::{Context, Result, anyhow, bail};
use anyhow::{anyhow, bail, Context, Result};
use async_rustls::rustls::internal::msgs::handshake::DigitallySignedStruct;
use async_rustls::rustls::{
    internal::pemfile::{certs, pkcs8_private_keys},


@@ 134,21 135,24 @@ fn load_key(path: &Path) -> Result<PrivateKey> {
/// See https://docs.rs/rustls/0.16.0/rustls/struct.ServerConfig.html for details
///
/// A TLS server needs a certificate and a fitting private key
pub fn load_config(domains: &[String]) -> Result<ServerConfig> {
pub fn load_config(routes: &[Route]) -> Result<ServerConfig> {
    let mut resolver = ResolvesServerCertUsingSNI::new();
    for domain in domains {
        let (cert_chain, key) = get_cert_and_key(domain)?;
    for route in routes {
        let (cert_chain, key) = get_cert_and_key(route)?;

        let signing_key = sign::any_supported_type(&key)
            .map_err(|_| anyhow!("Invalid key for domain {}", domain))?;
            .map_err(|_| anyhow!("Invalid key for domain {}", route.domain))?;

        log::debug!("Loaded cert+key for domain {}", domain);
        log::debug!("Loaded cert+key for domain {}", route.domain);
        resolver
            .add(domain, CertifiedKey::new(cert_chain, Arc::new(signing_key)))
            .add(
                &route.domain,
                CertifiedKey::new(cert_chain, Arc::new(signing_key)),
            )
            .with_context(|| {
                format!(
                    "Error adding cert/key for domain `{}` to tls config",
                    domain
                    route.domain
                )
            })?;
    }


@@ 161,19 165,27 @@ pub fn load_config(domains: &[String]) -> Result<ServerConfig> {
    Ok(config)
}

fn get_cert_and_key(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
    let cert_path = CONF.store.join(format!("{}.crt", domain));
    let key_path = CONF.store.join(format!("{}.key", domain));
    if !cert_path.exists() || !key_path.exists() {
        let (cert_path, key_path) = gen_cert_and_key(domain)?;
fn get_cert_and_key(route: &Route) -> Result<(Vec<Certificate>, PrivateKey)> {
    let (cert_path, key_path, default_paths) = match route.cert_key_path.clone() {
        Some((cert_path, key_path)) => (cert_path, key_path, false),
        None => (
            CONF.store.join(format!("{}.crt", route.domain)),
            CONF.store.join(format!("{}.key", route.domain)),
            true,
        ),
    };
    if default_paths && (!cert_path.exists() || !key_path.exists()) {
        let (cert_path, key_path) = gen_cert_and_key(&route.domain)?;
        return Ok((load_cert(&cert_path)?, load_key(&key_path)?));
    }
    let cert_chain = load_cert(&cert_path)?;
    for cert in &cert_chain {
        let (_, cert) = parse_x509_certificate(cert.as_ref())
            .with_context(|| format!("Error parsing cert for {}", domain))?;
        let (_, cert) =
            parse_x509_certificate(cert.as_ref()).with_context(|| {
                format!("Error parsing cert for {}", route.domain)
            })?;
        if CONF.regen_certs && !cert.validity().is_valid() {
            let (cert_path, key_path) = gen_cert_and_key(domain)?;
            let (cert_path, key_path) = gen_cert_and_key(&route.domain)?;
            return Ok((load_cert(&cert_path)?, load_key(&key_path)?));
        }
    }


@@ 182,7 194,10 @@ fn get_cert_and_key(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {

fn gen_cert_and_key(domain: &str) -> Result<(PathBuf, PathBuf)> {
    if !CONF.generate_certs {
        bail!("Cert not found for domain {} and cert generation is disabled", domain);
        bail!(
            "Cert not found for domain {} and cert generation is disabled",
            domain
        );
    }
    debug!("Generating cert+key for {}", domain);
    let mut params = CertificateParams::new(vec![domain.to_owned()]);

M test_data/testing.ini => test_data/testing.ini +2 -0
@@ 16,6 16,8 @@ charset = ascii
root = ./test_data/test_site
lang = en
charset = ascii
cert-path = ./test_data/store/localhost.crt
key-path = ./test_data/store/localhost.key

[localhost:/cgi-bin]
root = ./test_data