From f374598fd3df2a9eec4f947ea8d105bf0780f325 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sat, 27 Feb 2021 19:32:55 +0100 Subject: [PATCH 01/13] add module to store multiple certificates --- Cargo.toml | 2 +- src/certificates.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/certificates.rs diff --git a/Cargo.toml b/Cargo.toml index 9ad6b51..e466cd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,11 +24,11 @@ rustls = "0.19.0" url = "2.2.1" glob = "0.3" configparser = "2.0" +webpki = "0.21.4" [dev-dependencies] gemini-fetch = "0.2.1" anyhow = "1.0" -webpki = "0.21.4" [profile.release] lto = true diff --git a/src/certificates.rs b/src/certificates.rs new file mode 100644 index 0000000..e378b1e --- /dev/null +++ b/src/certificates.rs @@ -0,0 +1,112 @@ +use { + rustls::{ + internal::pemfile::{certs, pkcs8_private_keys}, + sign::{CertifiedKey, RSASigningKey}, + ResolvesServerCert, + }, + std::{fs::File, io::BufReader, path::PathBuf, sync::Arc}, + webpki::DNSNameRef, +}; + +/// A struct that holds all loaded certificates and the respective domain +/// names. +pub(crate) struct CertStore { + // use a Vec of pairs instead of a HashMap because order matters + certs: Vec<(String, CertifiedKey)>, +} + +static CERT_FILE_NAME: &str = "cert.pem"; +static KEY_FILE_NAME: &str = "key.rsa"; + +impl CertStore { + /// Load certificates from a certificate directory. + /// Certificates should be stored in a folder for each hostname, for example + /// the certificate and key for `example.com` should be in the files + /// `certs_dir/example.com/{cert.pem,key.rsa}` respectively. + pub fn load_from(certs_dir: PathBuf) -> Result { + // load all certificates from directories + let mut certs = certs_dir + .read_dir() + .expect("could not read from certificate directory") + .filter_map(Result::ok) + .filter_map(|entry| { + if !entry.metadata().map_or(false, |data| data.is_dir()) { + None + } else if !entry.file_name().to_str().map_or(false, |s| s.is_ascii()) { + Some(Err( + "domain for certificate is not US-ASCII, must be punycoded".to_string(), + )) + } else { + let filename = entry.file_name(); + let dns_name = match DNSNameRef::try_from_ascii_str(filename.to_str().unwrap()) + { + Ok(name) => name, + Err(e) => return Some(Err(e.to_string())), + }; + + // load certificate from file + let mut path = entry.path(); + path.push(CERT_FILE_NAME); + if !path.is_file() { + return Some(Err(format!("expected certificate {:?}", path))); + } + let cert_chain = match certs(&mut BufReader::new(File::open(&path).unwrap())) { + Ok(cert) => cert, + Err(_) => return Some(Err("bad cert file".to_string())), + }; + + // load key from file + path.set_file_name(KEY_FILE_NAME); + if !path.is_file() { + return Some(Err(format!("expected key {:?}", path))); + } + let key = + match pkcs8_private_keys(&mut BufReader::new(File::open(&path).unwrap())) { + Ok(mut keys) if !keys.is_empty() => keys.remove(0), + Ok(_) => return Some(Err(format!("key file empty {:?}", path))), + Err(_) => return Some(Err("bad key file".to_string())), + }; + + // transform key to correct format + let key = match RSASigningKey::new(&key) { + Ok(key) => key, + Err(_) => return Some(Err("bad key".to_string())), + }; + let key = CertifiedKey::new(cert_chain, Arc::new(Box::new(key))); + if let Err(e) = key.cross_check_end_entity_cert(Some(dns_name)) { + return Some(Err(e.to_string())); + } + Some(Ok((entry.file_name().to_str().unwrap().to_string(), key))) + } + }) + .collect::, _>>()?; + certs.sort_unstable_by(|(a, _), (b, _)| { + // try to match as many as possible. If one is a substring of the other, + // the `zip` will make them look equal and make the length decide. + for (a_part, b_part) in a.split('.').rev().zip(b.split('.').rev()) { + if a_part != b_part { + return a_part.cmp(b_part); + } + } + // longer domains first + a.len().cmp(&b.len()).reverse() + }); + Ok(Self { certs }) + } +} + +impl ResolvesServerCert for CertStore { + fn resolve(&self, client_hello: rustls::ClientHello<'_>) -> Option { + if let Some(name) = client_hello.server_name() { + let name: &str = name.into(); + self.certs + .iter() + .find(|(s, _)| name.ends_with(s)) + .map(|(_, k)| k) + .cloned() + } else { + // This kind of resolver requires SNI + None + } + } +} diff --git a/src/main.rs b/src/main.rs index b54416b..b22f003 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![forbid(unsafe_code)] +mod certificates; mod metadata; use metadata::{FileOptions, PresetMeta}; From 5a4907292fee6591a7c2b14912fc4fee435df6cc Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sat, 27 Feb 2021 20:02:19 +0100 Subject: [PATCH 02/13] use certificate store --- src/main.rs | 43 ++++++++++--------------------------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/src/main.rs b/src/main.rs index b22f003..8beb1bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,17 +7,12 @@ use metadata::{FileOptions, PresetMeta}; use { once_cell::sync::Lazy, percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS}, - rustls::{ - internal::pemfile::{certs, pkcs8_private_keys}, - Certificate, NoClientAuth, PrivateKey, ServerConfig, - }, + rustls::{NoClientAuth, ServerConfig}, std::{ borrow::Cow, error::Error, ffi::OsStr, fmt::Write, - fs::File, - io::BufReader, net::SocketAddr, path::{Path, PathBuf}, sync::Arc, @@ -78,8 +73,7 @@ static ARGS: Lazy = Lazy::new(|| { struct Args { addrs: Vec, content_dir: PathBuf, - cert_chain: Vec, - key: PrivateKey, + certs: Arc, hostnames: Vec, language: Option, silent: bool, @@ -100,15 +94,9 @@ fn args() -> Result { ); opts.optopt( "", - "cert", - "TLS certificate PEM file (default ./cert.pem)", - "FILE", - ); - opts.optopt( - "", - "key", - "PKCS8 private key file (default ./key.rsa)", - "FILE", + "certs", + "folder for certificate files (default ./.certificates/)", + "FOLDER", ); opts.optmulti( "", @@ -172,25 +160,14 @@ fn args() -> Result { ]; } - let cert_file = File::open(check_path( - matches.opt_get_default("cert", "cert.pem".into())?, - )?)?; - let cert_chain = certs(&mut BufReader::new(cert_file)).or(Err("bad cert"))?; - - let key_file = File::open(check_path( - matches.opt_get_default("key", "key.rsa".into())?, - )?)?; - let key = pkcs8_private_keys(&mut BufReader::new(key_file)) - .or(Err("bad key file"))? - .drain(..) - .next() - .ok_or("no keys found")?; + let certs = Arc::new(certificates::CertStore::load_from(check_path( + matches.opt_get_default("certs", ".certificates".into())?, + )?)?); Ok(Args { addrs, content_dir: check_path(matches.opt_get_default("content", "content".into())?)?, - cert_chain, - key, + certs, hostnames, language: matches.opt_str("lang"), silent: matches.opt_present("s"), @@ -218,7 +195,7 @@ fn acceptor() -> Result { if ARGS.only_tls13 { config.versions = vec![rustls::ProtocolVersion::TLSv1_3]; } - config.set_single_cert(ARGS.cert_chain.clone(), ARGS.key.clone())?; + config.cert_resolver = ARGS.certs.clone(); Ok(TlsAcceptor::from(Arc::new(config))) } From 06819eeabd972bef0d3c513749b3a7a64722fecd Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 28 Feb 2021 15:12:20 +0100 Subject: [PATCH 03/13] add loading fallback certificates --- src/certificates.rs | 195 +++++++++++++++++------- src/main.rs | 8 +- tests/data/{ => .certificates}/cert.pem | 0 tests/data/{ => .certificates}/key.rsa | 0 4 files changed, 147 insertions(+), 56 deletions(-) rename tests/data/{ => .certificates}/cert.pem (100%) rename tests/data/{ => .certificates}/key.rsa (100%) diff --git a/src/certificates.rs b/src/certificates.rs index e378b1e..c649622 100644 --- a/src/certificates.rs +++ b/src/certificates.rs @@ -4,7 +4,14 @@ use { sign::{CertifiedKey, RSASigningKey}, ResolvesServerCert, }, - std::{fs::File, io::BufReader, path::PathBuf, sync::Arc}, + std::{ + ffi::OsStr, + fmt::{Display, Formatter}, + fs::File, + io::BufReader, + path::Path, + sync::Arc, + }, webpki::DNSNameRef, }; @@ -18,68 +25,151 @@ pub(crate) struct CertStore { static CERT_FILE_NAME: &str = "cert.pem"; static KEY_FILE_NAME: &str = "key.rsa"; +#[derive(Debug)] +pub enum CertLoadError { + /// could not access the certificate root directory + NoReadCertDir, + /// the specified domain name cannot be processed correctly + BadDomain(String), + /// the key file for the specified domain is bad (e.g. does not contain a + /// key or is invalid) + BadKey(String), + /// the certificate file for the specified domain is bad (e.g. invalid) + BadCert(String), + /// the key file for the specified domain is missing (but a certificate + /// file was present) + MissingKey(String), + /// the certificate file for the specified domain is missing (but a key + /// file was present) + MissingCert(String), + /// neither a key file nor a certificate file were present for the given + /// domain (but a folder was present) + EmptyDomain(String), +} + +impl Display for CertLoadError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::NoReadCertDir => write!(f, "Could not read from certificate directory."), + Self::BadDomain(domain) if !domain.is_ascii() => write!( + f, + "The domain name {} cannot be processed, it must be punycoded.", + domain + ), + Self::BadDomain(domain) => write!(f, "The domain name {} cannot be processed.", domain), + Self::BadKey(domain) => write!(f, "The key file for {} is malformed.", domain), + Self::BadCert(domain) => write!(f, "The certificate file for {} is malformed.", domain), + Self::MissingKey(domain) => write!(f, "The key file for {} is missing.", domain), + Self::MissingCert(domain) => { + write!(f, "The certificate file for {} is missing.", domain) + } + Self::EmptyDomain(domain) => write!( + f, + "A folder for {} exists, but there is no certificate or key file.", + domain + ), + } + } +} + +impl std::error::Error for CertLoadError {} + +fn load_domain(certs_dir: &Path, domain: String) -> Result { + let mut path = certs_dir.to_path_buf(); + path.push(&domain); + // load certificate from file + path.push(CERT_FILE_NAME); + if !path.is_file() { + return Err(if !path.with_file_name(KEY_FILE_NAME).is_file() { + CertLoadError::EmptyDomain(domain) + } else { + CertLoadError::MissingCert(domain) + }); + } + + let cert_chain = match certs(&mut BufReader::new(File::open(&path).unwrap())) { + Ok(cert) => cert, + Err(_) => return Err(CertLoadError::BadCert(domain)), + }; + + // load key from file + path.set_file_name(KEY_FILE_NAME); + if !path.is_file() { + return Err(CertLoadError::MissingKey(domain)); + } + let key = match pkcs8_private_keys(&mut BufReader::new(File::open(&path).unwrap())) { + Ok(mut keys) if !keys.is_empty() => keys.remove(0), + _ => return Err(CertLoadError::BadKey(domain)), + }; + + // transform key to correct format + let key = match RSASigningKey::new(&key) { + Ok(key) => key, + Err(_) => return Err(CertLoadError::BadKey(domain)), + }; + Ok(CertifiedKey::new(cert_chain, Arc::new(Box::new(key)))) +} + impl CertStore { /// Load certificates from a certificate directory. /// Certificates should be stored in a folder for each hostname, for example /// the certificate and key for `example.com` should be in the files /// `certs_dir/example.com/{cert.pem,key.rsa}` respectively. - pub fn load_from(certs_dir: PathBuf) -> Result { + /// + /// If there are `cert.pem` and `key.rsa` directly in certs_dir, these will be + /// loaded as default certificates. + pub fn load_from(certs_dir: &Path) -> Result { // load all certificates from directories - let mut certs = certs_dir + let mut certs = vec![]; + + // try to load fallback certificate and key + match load_domain(certs_dir, ".".to_string()) { + Err(CertLoadError::EmptyDomain(_)) => { /* there are no fallback keys */ } + Err(CertLoadError::NoReadCertDir) => unreachable!(), + Err(CertLoadError::BadDomain(_)) => unreachable!(), + Err(CertLoadError::BadKey(_)) => { + return Err(CertLoadError::BadKey("fallback".to_string())) + } + Err(CertLoadError::BadCert(_)) => { + return Err(CertLoadError::BadCert("fallback".to_string())) + } + Err(CertLoadError::MissingKey(_)) => { + return Err(CertLoadError::MissingKey("fallback".to_string())) + } + Err(CertLoadError::MissingCert(_)) => { + return Err(CertLoadError::MissingCert("fallback".to_string())) + } + // if there are files, just push them because there is no domain + // name to check against + Ok(key) => certs.push((String::new(), key)), + } + + for file in certs_dir .read_dir() - .expect("could not read from certificate directory") + .or(Err(CertLoadError::NoReadCertDir))? .filter_map(Result::ok) - .filter_map(|entry| { - if !entry.metadata().map_or(false, |data| data.is_dir()) { - None - } else if !entry.file_name().to_str().map_or(false, |s| s.is_ascii()) { - Some(Err( - "domain for certificate is not US-ASCII, must be punycoded".to_string(), - )) - } else { - let filename = entry.file_name(); - let dns_name = match DNSNameRef::try_from_ascii_str(filename.to_str().unwrap()) - { - Ok(name) => name, - Err(e) => return Some(Err(e.to_string())), - }; + .filter(|x| x.path().is_dir()) + { + let path = file.path(); + let filename = path + .file_name() + .and_then(OsStr::to_str) + .unwrap() + .to_string(); - // load certificate from file - let mut path = entry.path(); - path.push(CERT_FILE_NAME); - if !path.is_file() { - return Some(Err(format!("expected certificate {:?}", path))); - } - let cert_chain = match certs(&mut BufReader::new(File::open(&path).unwrap())) { - Ok(cert) => cert, - Err(_) => return Some(Err("bad cert file".to_string())), - }; + let dns_name = match DNSNameRef::try_from_ascii_str(&filename) { + Ok(name) => name, + Err(_) => return Err(CertLoadError::BadDomain(filename)), + }; - // load key from file - path.set_file_name(KEY_FILE_NAME); - if !path.is_file() { - return Some(Err(format!("expected key {:?}", path))); - } - let key = - match pkcs8_private_keys(&mut BufReader::new(File::open(&path).unwrap())) { - Ok(mut keys) if !keys.is_empty() => keys.remove(0), - Ok(_) => return Some(Err(format!("key file empty {:?}", path))), - Err(_) => return Some(Err("bad key file".to_string())), - }; + let key = load_domain(certs_dir, filename.clone())?; + if key.cross_check_end_entity_cert(Some(dns_name)).is_err() { + return Err(CertLoadError::BadCert(filename)); + } + + certs.push((filename, key)); + } - // transform key to correct format - let key = match RSASigningKey::new(&key) { - Ok(key) => key, - Err(_) => return Some(Err("bad key".to_string())), - }; - let key = CertifiedKey::new(cert_chain, Arc::new(Box::new(key))); - if let Err(e) = key.cross_check_end_entity_cert(Some(dns_name)) { - return Some(Err(e.to_string())); - } - Some(Ok((entry.file_name().to_str().unwrap().to_string(), key))) - } - }) - .collect::, _>>()?; certs.sort_unstable_by(|(a, _), (b, _)| { // try to match as many as possible. If one is a substring of the other, // the `zip` will make them look equal and make the length decide. @@ -91,6 +181,7 @@ impl CertStore { // longer domains first a.len().cmp(&b.len()).reverse() }); + Ok(Self { certs }) } } diff --git a/src/main.rs b/src/main.rs index 8beb1bc..4155f9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -160,7 +160,7 @@ fn args() -> Result { ]; } - let certs = Arc::new(certificates::CertStore::load_from(check_path( + let certs = Arc::new(certificates::CertStore::load_from(&check_path( matches.opt_get_default("certs", ".certificates".into())?, )?)?); @@ -188,15 +188,15 @@ fn check_path(s: String) -> Result { } /// TLS configuration. -static TLS: Lazy = Lazy::new(|| acceptor().unwrap()); +static TLS: Lazy = Lazy::new(acceptor); -fn acceptor() -> Result { +fn acceptor() -> TlsAcceptor { let mut config = ServerConfig::new(NoClientAuth::new()); if ARGS.only_tls13 { config.versions = vec![rustls::ProtocolVersion::TLSv1_3]; } config.cert_resolver = ARGS.certs.clone(); - Ok(TlsAcceptor::from(Arc::new(config))) + TlsAcceptor::from(Arc::new(config)) } struct RequestHandle { diff --git a/tests/data/cert.pem b/tests/data/.certificates/cert.pem similarity index 100% rename from tests/data/cert.pem rename to tests/data/.certificates/cert.pem diff --git a/tests/data/key.rsa b/tests/data/.certificates/key.rsa similarity index 100% rename from tests/data/key.rsa rename to tests/data/.certificates/key.rsa From 635f7bc1e7c9164c20e6410e79a60b8169954697 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 3 Mar 2021 18:46:41 +0100 Subject: [PATCH 04/13] better error messages --- src/certificates.rs | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/certificates.rs b/src/certificates.rs index c649622..af52ac3 100644 --- a/src/certificates.rs +++ b/src/certificates.rs @@ -31,11 +31,14 @@ pub enum CertLoadError { NoReadCertDir, /// the specified domain name cannot be processed correctly BadDomain(String), + /// The key file for the given domain does not contain any suitable keys. + NoKeys(String), /// the key file for the specified domain is bad (e.g. does not contain a /// key or is invalid) BadKey(String), - /// the certificate file for the specified domain is bad (e.g. invalid) - BadCert(String), + /// The certificate file for the specified domain is bad (e.g. invalid) + /// The second parameter is the error message. + BadCert(String, String), /// the key file for the specified domain is missing (but a certificate /// file was present) MissingKey(String), @@ -57,8 +60,15 @@ impl Display for CertLoadError { domain ), Self::BadDomain(domain) => write!(f, "The domain name {} cannot be processed.", domain), + Self::NoKeys(domain) => write!( + f, + "The key file for {} does not contain any suitable key.", + domain + ), Self::BadKey(domain) => write!(f, "The key file for {} is malformed.", domain), - Self::BadCert(domain) => write!(f, "The certificate file for {} is malformed.", domain), + Self::BadCert(domain, e) => { + write!(f, "The certificate file for {} is malformed: {}", domain, e) + } Self::MissingKey(domain) => write!(f, "The key file for {} is missing.", domain), Self::MissingCert(domain) => { write!(f, "The certificate file for {} is missing.", domain) @@ -89,7 +99,7 @@ fn load_domain(certs_dir: &Path, domain: String) -> Result cert, - Err(_) => return Err(CertLoadError::BadCert(domain)), + Err(()) => return Err(CertLoadError::BadCert(domain, String::new())), }; // load key from file @@ -99,13 +109,14 @@ fn load_domain(certs_dir: &Path, domain: String) -> Result keys.remove(0), - _ => return Err(CertLoadError::BadKey(domain)), + Ok(_) => return Err(CertLoadError::NoKeys(domain)), + Err(()) => return Err(CertLoadError::BadKey(domain)), }; // transform key to correct format let key = match RSASigningKey::new(&key) { Ok(key) => key, - Err(_) => return Err(CertLoadError::BadKey(domain)), + Err(()) => return Err(CertLoadError::BadKey(domain)), }; Ok(CertifiedKey::new(cert_chain, Arc::new(Box::new(key)))) } @@ -127,11 +138,14 @@ impl CertStore { Err(CertLoadError::EmptyDomain(_)) => { /* there are no fallback keys */ } Err(CertLoadError::NoReadCertDir) => unreachable!(), Err(CertLoadError::BadDomain(_)) => unreachable!(), + Err(CertLoadError::NoKeys(_)) => { + return Err(CertLoadError::NoKeys("fallback".to_string())) + } Err(CertLoadError::BadKey(_)) => { return Err(CertLoadError::BadKey("fallback".to_string())) } - Err(CertLoadError::BadCert(_)) => { - return Err(CertLoadError::BadCert("fallback".to_string())) + Err(CertLoadError::BadCert(_, e)) => { + return Err(CertLoadError::BadCert("fallback".to_string(), e)) } Err(CertLoadError::MissingKey(_)) => { return Err(CertLoadError::MissingKey("fallback".to_string())) @@ -163,9 +177,8 @@ impl CertStore { }; let key = load_domain(certs_dir, filename.clone())?; - if key.cross_check_end_entity_cert(Some(dns_name)).is_err() { - return Err(CertLoadError::BadCert(filename)); - } + key.cross_check_end_entity_cert(Some(dns_name)) + .or_else(|e| Err(CertLoadError::BadCert(filename.clone(), e.to_string())))?; certs.push((filename, key)); } From f03e8e8596302c8bedbf20c36a7ef4d86455a3ea Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 3 Mar 2021 18:57:41 +0100 Subject: [PATCH 05/13] add test for multicert --- tests/data/cert_missing/key.rsa | 52 +++++ tests/data/key_missing/cert.pem | 29 +++ tests/data/multicert/ca_cert.pem | 29 +++ tests/data/multicert/ca_key.rsa | 52 +++++ tests/data/multicert/create_certs.sh | 38 ++++ tests/data/multicert/example.com/cert.pem | 30 +++ tests/data/multicert/example.com/key.rsa | 52 +++++ tests/data/multicert/example.org/cert.pem | 30 +++ tests/data/multicert/example.org/key.rsa | 52 +++++ tests/tests.rs | 248 ++++++++++++++++------ 10 files changed, 546 insertions(+), 66 deletions(-) create mode 100644 tests/data/cert_missing/key.rsa create mode 100644 tests/data/key_missing/cert.pem create mode 100644 tests/data/multicert/ca_cert.pem create mode 100644 tests/data/multicert/ca_key.rsa create mode 100755 tests/data/multicert/create_certs.sh create mode 100644 tests/data/multicert/example.com/cert.pem create mode 100644 tests/data/multicert/example.com/key.rsa create mode 100644 tests/data/multicert/example.org/cert.pem create mode 100644 tests/data/multicert/example.org/key.rsa diff --git a/tests/data/cert_missing/key.rsa b/tests/data/cert_missing/key.rsa new file mode 100644 index 0000000..a713c8e --- /dev/null +++ b/tests/data/cert_missing/key.rsa @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +tPwNvAOh6tUMmDlbRt+xdpFmU238jA== +-----END PRIVATE KEY----- diff --git a/tests/data/key_missing/cert.pem b/tests/data/key_missing/cert.pem new file mode 100644 index 0000000..df3fbd8 --- /dev/null +++ b/tests/data/key_missing/cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUP60wPmdZzVFdc9c1ReFbzcWk9CwwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIwMTEwODE0MzE1NloXDTMwMTEw +NjE0MzE1NlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA+3+qGCFlrKMqyCl7uO7QJ+ILZx94swlcxaO1Vo2plau/ +rLFksNTm9EeT5kfovgE8UIXyuwQWZOoxOouaX30t3SUt1qhM2tJw+bkgN+M02LSX +i67AbjqJXWeJs+yQL64RVdM3c13Qow6EBqkOPSViISNtp09GKufKgePW6ptb8fk2 +o1yA7inQicCX7YRf/bF4q5qjOBrDjswDH8W1N5Vn8XVNa1ydLQZWUh/299WZ7jQg +LgIFiJ9RL9qhtl9FlSvN3yeRnny3AX4bk+I666vmleqAB8579QCT7wcGIVatPX71 +b0xZxayCZHNzcWTI4nZI2CO5b4T88F9qztM8NzBC/OvNKVPAiTrFwMnlxg3oqbe9 +Evqep2Ut/n5aKjkgeWDHUI57VoAWJEfPWPKuJhIPnmokf5RRzjqGa3invivnYzD2 +Jna0RMUNfA3CjVpdt/c4/59kKU3Z848uzZLrw2cmX+KZpv9LuCnfUqOnEsmwhoxr +ZaZqb8wuF7nm7aPwO85SXiIIZdppm6N1TIZTOOJ0HpDutZiKs9nZKoBoH+5KkCkr +76P0Y+bvA+DdxZ0V0Jvyar5QskJac3hbpO7xSc9cgNXytwsX7iusbnJTIY6Nr4DQ +sGogRY2/3gJ6zPv27VHykxlyMeJ3nRK5bE7k6rpCUQjrpVXb9GVbFZ1uv9LeIfkC +AwEAAaNTMFEwHQYDVR0OBBYEFI7IjwaA0gwpeZXl5x7Knw8i4STDMB8GA1UdIwQY +MBaAFI7IjwaA0gwpeZXl5x7Knw8i4STDMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBANQJ+WuQ8UZsk86t7jTS9K2Igit22ImXhdddDHHbBrbdLXoN +g4IXij8mEJqfPyqZhT49S23booihvJOyhT/RgRB6LI/hBuV1vTwARmIRU4WhZAh0 +xIwVCqniDp6Rgf6OEOYgLGk11CEv7vMfPZYbLddDY1HvmSl8l087CLEQ37WRb51M +5Quhsrny441s6aTn8I7c9WY2H/CUmlF8byoLuIl2MpR5bQN17binfsJZPYkKaMyJ +5URM0nCVUEr+o/1znYsQJYa+GSVEsJJ6OyS7TMSoFJlVFslOxbdPzNkdcL6jxpXN +B0rCrC1gTaQynxTOoQ56N8Z74V9xoXNd0ZwaSgEWfeM++YOyk3qgfvhobd0F8rTD +8+dvMN7eI+N8P+S+VCnX9YzrQIZyTwEhHK9fXLlcqoiAhpizgGhlGctiZ1MmpzT2 +aqFmLOCKpcQLyofsSLSFbhV2/w8rJbS1kTlrzwQLzaJvtLVy+ZZdQFP49Vj8Lb7n +3Oos/YeNoGhJoTWX7S2nQBChYMsSUA15+IS7RN0b+cJroHESsqCkbp07M20zhztz +fDWdYFh+o4V2lF9ecnqV7MwTvz9WxpchcUfQgENrJ3dgTn35hsZOMM2anwFWrmG+ +KVyFMhWNnZB1E530Nsu9cNHntqc3sFBdJebrFED9gOFErt5Vou8btjIqPm/Y +-----END CERTIFICATE----- diff --git a/tests/data/multicert/ca_cert.pem b/tests/data/multicert/ca_cert.pem new file mode 100644 index 0000000..7cca683 --- /dev/null +++ b/tests/data/multicert/ca_cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIUIBGuqsV2UiwpjgNTBhsCWSEofMEwDQYJKoZIhvcNAQEL +BQAwFTETMBEGA1UEAwwKZXhhbXBsZSBDQTAeFw0yMTAzMDMxNzI1MjFaFw0zMTAz +MDExNzI1MjFaMBUxEzARBgNVBAMMCmV4YW1wbGUgQ0EwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDAYjrXvFY9MpJ0Cy9Jte42u9peeUQ+4isGIDmnSAmA +hngVqoDlqvr6mXXU+N0c7jDkLEcs7EySORP/k1rRrAtQPSZeMSVQ47aJirmIlcnb +P9/ivyYv8lyndZCZikOAOz7D1beRV66MX2q1/plnW1QuWHbk83IqNniZcL8QRTWw +EYy8CxNd9UO9TlzF/kuIadNlFEywiQ6pbZiIhPChCtg2vKx/abQukYSGDoQxTIEp +T6uqoHevAMZFMhwhURG5kUX65xQcIDSobvxyziSc3fKW/fxzu/ORkLHBB5T3jcQJ ++BL4pb+hZl7Xe/r4TvfhSpdzu5hoLFAqLWhQUHymMG8kmqJ6Q9lfuQO4A/HuqCQN +cYtEce88uOaF/TPt99qhvg/7V1LXxZ9UNnyc2cQUroT8jx+5NtWBPQvekKSoufxX +4Kv4kdtaPyGRVZcshLQKog6kD49nzlCJhYV9UUiGQMUrzb7H5n+Y2puN87B5oUVY +djNJ65h7y27fKeXfrpvTJ+kqi9A6hrwNR6INTkRJ+l6mnpX0tnTmGmSiChsW+xCF +R2bIl04y4efqYToG3fQk0vzn+rGx57DqnpgDavIYJxsNWzJ4qWWxdN08QV8OsmEo +0u6Ks9+EVv6sHmY+WWsOB+8Jgwa0p2HcQ2nu0KrIDNVxUS29jPA/Iw7g/w95az/X +2wIDAQABo1MwUTAdBgNVHQ4EFgQUtyhzhR/wxqDZlRxR4odCXFhSwtEwHwYDVR0j +BBgwFoAUtyhzhR/wxqDZlRxR4odCXFhSwtEwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAZYiHTpAsWvOdqsMa9KhiWgOYAtJ/vERaTkWowxfqpBrx +nC9k5PAU4r5mAa38RX9hbRWEpXA2oEET/txg4a9oHkYPCk8+Nq/K7JwoY32gdsuC +EeAFIFhsfV5aDnoMxEfjOo7erFa89UBLup9bw3VupQ6+9B7UJP5Q1g8aapdcw2Io +nFiof56TNBUNSSJc0ErfBInCQ+T2yIbbyXvsNhbENidlP8nDv9cwHEQewUPbH9/y +k4MqsZVsdrm00m0ZWgdQfZnTgM5/TBp+tTyHJOQqfekPiqob7lPGgakMhkpGHJvu +EnkQJecgHA1/k2ETM4Ja162kbshN8LjLLrXi9aEwDYTW1xFbvK9MrHKcSOTq+FJs +WV3RK1J56pqq3iNJLXkXjSuo6bNIA4fjxJk8scRdsANAYfV9I3pJUY1EB/LvycSo +zCUgpp+tnqT+lgvCZ3aFi/Iajb0TgoNb/xgHo2MJmRNLj6RQlJkLDEYBQTE1iiru +bWZW2jf7LEBM9MwT2+I2AbmCLyPoA04ZT7GH2yeugU1YrxO5Erj6m3JBdwuKIU7g +DJH2DttPIm1ay00tFBapYoODfXwqqIPtYRSAhSxxuWRV1fl5kVgyT2TjEvv9b/8j +SMRrGvo4Ws2H8W8Fcf0EVIywkVxpE2YlzztEWhVJEmltM74slX82QZ2ppWCmepg= +-----END CERTIFICATE----- diff --git a/tests/data/multicert/ca_key.rsa b/tests/data/multicert/ca_key.rsa new file mode 100644 index 0000000..b5fb859 --- /dev/null +++ b/tests/data/multicert/ca_key.rsa @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +uabNtK2No3EmpBOK31TRSIKWx7bFnw== +-----END PRIVATE KEY----- diff --git a/tests/data/multicert/create_certs.sh b/tests/data/multicert/create_certs.sh new file mode 100755 index 0000000..8b5e0e7 --- /dev/null +++ b/tests/data/multicert/create_certs.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +mkdir -p example.com example.org + +# create our own CA so we can use rustls without it complaining about using a +# CA cert as end cert +openssl req -x509 -newkey rsa:4096 -keyout ca_key.rsa -out ca_cert.pem -days 3650 -nodes -subj "/CN=example CA" + +for domain in "example.com" "example.org" +do +openssl genpkey -out $domain/key.rsa -algorithm RSA -pkeyopt rsa_keygen_bits:4096 + +cat >openssl.conf < SocketAddr { struct Server { server: std::process::Child, buf: u8, + // is set when output is collected by stop() + output: Option>, } impl Server { @@ -44,17 +47,20 @@ impl Server { Self { server, buf: buffer[0], + output: None, } } -} -impl Drop for Server { - fn drop(&mut self) { - // try to stop the server again - match self.server.try_wait() { - Err(e) => panic!("cannot access orchestrated program: {:?}", e), + pub fn stop(&mut self) -> Result<(), String> { + // try to stop the server + if let Some(output) = self.output.clone() { + return output; + } + + self.output = Some(match self.server.try_wait() { + Err(e) => Err(format!("cannot access orchestrated program: {:?}", e)), // everything fine, still running as expected, kill it now - Ok(None) => self.server.kill().unwrap(), + Ok(None) => Ok(self.server.kill().unwrap()), Ok(Some(_)) => { // forward stderr so we have a chance to understand the problem let buffer = std::iter::once(Ok(self.buf)) @@ -62,23 +68,37 @@ impl Drop for Server { .collect::, _>>() .unwrap(); - eprintln!("{}", String::from_utf8_lossy(&buffer)); - // make the test fail - panic!("program had crashed"); + Err(String::from_utf8_lossy(&buffer).into_owned()) } + }); + return self.output.clone().unwrap(); + } +} + +impl Drop for Server { + fn drop(&mut self) { + if self.output.is_none() && !std::thread::panicking() { + // a potential error message was not yet handled + self.stop().unwrap(); + } else if self.output.is_some() { + // error was already handled, ignore it + self.stop().unwrap_or(()); + } else { + // we are panicking and a potential error was not handled + self.stop().unwrap_or_else(|e| eprintln!("{:?}", e)); } } } fn get(args: &[&str], addr: SocketAddr, url: &str) -> Result { - let _server = Server::new(args); + let mut server = Server::new(args); // actually perform the request let page = tokio::runtime::Runtime::new() .unwrap() .block_on(async { Page::fetch_from(&Url::parse(url).unwrap(), addr, None).await }); - page + server.stop().map_err(|e| anyhow!(e)).and(page) } #[test] @@ -298,65 +318,161 @@ fn explicit_tls_version() { tls.read(&mut buf).unwrap(); } -#[test] -/// - simple vhosts are enabled when multiple hostnames are supplied -/// - the vhosts access the correct files -fn vhosts_example_com() { - let page = get( - &[ - "--addr", - "[::]:1977", - "--hostname", - "example.com", - "--hostname", - "example.org", - ], - addr(1977), - "gemini://example.com/", - ) - .expect("could not get page"); +mod vhosts { + use super::*; - assert_eq!(page.header.status, Status::Success); - - assert_eq!( - page.body, - Some( - std::fs::read_to_string(concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/data/content/example.com/index.gmi" - )) - .unwrap() + #[test] + /// - simple vhosts are enabled when multiple hostnames are supplied + /// - the vhosts access the correct files + fn example_com() { + let page = get( + &[ + "--addr", + "[::]:1977", + "--hostname", + "example.com", + "--hostname", + "example.org", + ], + addr(1977), + "gemini://example.com/", ) - ); + .expect("could not get page"); + + assert_eq!(page.header.status, Status::Success); + + assert_eq!( + page.body, + Some( + std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/content/example.com/index.gmi" + )) + .unwrap() + ) + ); + } + + #[test] + /// - the vhosts access the correct files + fn example_org() { + let page = get( + &[ + "--addr", + "[::]:1978", + "--hostname", + "example.com", + "--hostname", + "example.org", + ], + addr(1978), + "gemini://example.org/", + ) + .expect("could not get page"); + + assert_eq!(page.header.status, Status::Success); + + assert_eq!( + page.body, + Some( + std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/content/example.org/index.gmi" + )) + .unwrap() + ) + ); + } } -#[test] -/// - the vhosts access the correct files -fn vhosts_example_org() { - let page = get( - &[ - "--addr", - "[::]:1978", - "--hostname", - "example.com", - "--hostname", - "example.org", - ], - addr(1978), - "gemini://example.org/", - ) - .expect("could not get page"); +mod multicert { + use super::*; - assert_eq!(page.header.status, Status::Success); + #[test] + fn cert_missing() { + let mut server = Server::new(&["--addr", "[::]:1979", "--certs", "cert_missing"]); - assert_eq!( - page.body, - Some( - std::fs::read_to_string(concat!( + // wait for the server to stop, it should crash + let _ = server.server.wait(); + + assert!(server + .stop() + .unwrap_err() + .to_string() + .contains("certificate file for fallback is missing")); + } + + #[test] + fn key_missing() { + let mut server = Server::new(&["--addr", "[::]:1980", "--certs", "key_missing"]); + + // wait for the server to stop, it should crash + let _ = server.server.wait(); + + assert!(server + .stop() + .unwrap_err() + .to_string() + .contains("key file for fallback is missing")); + } + + #[test] + fn example_com() { + use rustls::ClientSession; + use std::io::{Cursor, Write}; + use std::net::TcpStream; + + let mut server = Server::new(&["--addr", "[::]:1981", "--certs", "multicert"]); + + let mut config = rustls::ClientConfig::new(); + config + .root_store + .add_pem_file(&mut Cursor::new(include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), - "/tests/data/content/example.org/index.gmi" - )) - .unwrap() - ) - ); + "/tests/data/multicert/ca_cert.pem" + )))) + .unwrap(); + + let dns_name = webpki::DNSNameRef::try_from_ascii_str("example.com").unwrap(); + let mut session = ClientSession::new(&std::sync::Arc::new(config), dns_name); + let mut tcp = TcpStream::connect(addr(1981)).unwrap(); + let mut tls = rustls::Stream::new(&mut session, &mut tcp); + + write!(tls, "gemini://example.com/\r\n").unwrap(); + + let mut buf = [0; 10]; + tls.read(&mut buf).unwrap(); + + server.stop().unwrap() + } + + #[test] + fn example_org() { + use rustls::ClientSession; + use std::io::{Cursor, Write}; + use std::net::TcpStream; + + let mut server = Server::new(&["--addr", "[::]:1982", "--certs", "multicert"]); + + let mut config = rustls::ClientConfig::new(); + config + .root_store + .add_pem_file(&mut Cursor::new(include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/multicert/ca_cert.pem" + )))) + .unwrap(); + + let dns_name = webpki::DNSNameRef::try_from_ascii_str("example.org").unwrap(); + let mut session = ClientSession::new(&std::sync::Arc::new(config), dns_name); + let mut tcp = TcpStream::connect(addr(1982)).unwrap(); + let mut tls = rustls::Stream::new(&mut session, &mut tcp); + + write!(tls, "gemini://example.org/\r\n").unwrap(); + + let mut buf = [0; 10]; + tls.read(&mut buf).unwrap(); + + server.stop().unwrap() + } } From 424bed7861c44150795e5d96366718c5d55f1ca4 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 3 Mar 2021 19:34:56 +0100 Subject: [PATCH 06/13] add instructions for multiple certificates also adjusted the certificate creation example to contain a subject alt name with a DNS entry. This is strictly speaking not required for the top level certificate, but it doesn't hurt to include it and makes the example reusable for all certificates. --- README.md | 40 ++++++++++++++++++++++++++++++++++------ src/main.rs | 2 +- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 51f8758..22bdeb1 100644 --- a/README.md +++ b/README.md @@ -39,19 +39,20 @@ You can use the install script in the `tools` directory for the remaining steps If there is none, please consider contributing one to make it easier for less tech-savvy users! *** -2. Generate a self-signed TLS certificate and private key. For example, if you have OpenSSL 1.1 installed, you can use a command like the following. (Replace the hostname `example.com` with the address of your Gemini server.) +2. Generate a self-signed TLS certificate and private key in the `.certificates` directory. For example, if you have OpenSSL 1.1 installed, you can use a command like the following. (Replace the *two* occurences of `example.com` in the last line with the domain of your Gemini server.) ``` -openssl req -x509 -newkey rsa:4096 -keyout key.rsa -out cert.pem \ - -days 3650 -nodes -subj "/CN=example.com" +mkdir -p .certificates + +openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \ + -keyout .certificates/key.rsa -out .certificates/cert.pem \ + -subj "/CN=example.com" -addext "subjectAltName = DNS:example.com" ``` 3. Run the server. You can use the following arguments to specify the locations of the content directory, certificate and key files, IP address and port to listen on, host name to expect in request URLs, and default language code(s) to include in the MIME type for for text/gemini files: (Again replace the hostname `example.com` with the address of your Gemini server.) ``` agate --content path/to/content/ \ - --key key.rsa \ - --cert cert.pem \ --addr [::]:1965 \ --addr 0.0.0.0:1965 \ --hostname example.com \ @@ -107,7 +108,7 @@ Rules can overwrite other rules, so if a file is matched by multiple rules, the If a line violates the format or looks like case 3, but is incorrect, it might be ignored. You should check your logs. Please know that this configuration file is first read when a file from the respective directory is accessed. So no log messages after startup does not mean the `.meta` file is okay. Such a configuration file might look like this: -```text +``` # This line will be ignored. **/*.de.gmi: ;lang=de nl/**/*.gmi: ;lang=nl @@ -142,6 +143,33 @@ Agate does not support different certificates for different hostnames, you will If you want to serve the same content for multiple domains, you can instead disable the hostname check by not specifying `--hostname`. In this case Agate will disregard a request's hostname apart from checking that there is one. +### Multiple certificates + +Agate has support for using multiple certificates with the `--certs` option. Agate will thus always require that a client uses SNI, which should not be a problem since the Gemini specification also requires SNI to be used. + +Certificates are by default stored in the `.certificates` directory. This is a hidden directory for the purpose that uncautious people may set the content root directory to the currrent director which may also contain the certificates directory. In this case, the certificates and private keys would still be hidden. The certificates are only loaded when Agate is started and are not reloaded while running. The certificates directory may directly contain a key and certificate pair, this is the default pair used if no other matching keys are present. The certificates directory may also contain subdirectories for specific domains, for example a folder for `example.org` and `portal.example.org`. Note that the subfolders for subdomains (like `portal.example.org`) should not be inside other subfolders but directly in the certificates directory. Agate will select the certificate/key pair whose name matches most closely. For example take the following directory structure: + +``` +.certificates +|-- cert.pem (1) +|-- key.rsa (1) +|-- example.org +| |-- cert.pem (2) +| `-- key.rsa (2) +`-- portal.example.org + |-- cert.pem (3) + `-- key.rsa (3) +``` + +This would be understood like this: +* The certificate/key pair (1) would be used for the entire domain tree (exceptions below). +* The certificate/key pair (2) would be used for the entire domain tree of `example.org`, so also including subdomains like `secret.example.org`. It overrides the pair (1) for this subtree (exceptions below). +* The certificate/key pair (3) would be used for the entire domain tree of `portal.example.org`, so also inclduding subdomains like `test.portal.example.org`. It overrides the pairs (1) and (2) for this subtree. + +Using a directory named just `.` causes undefined behaviour as this would have the same meaning as the top level certificate/key pair (pair (1) in the example above). + +The files for a certificate/key pair have to be named `cert.pem` and `key.rsa` respectively. The certificate has to be a X.509 certificate in a PEM file and has to include a subject alt name of the domain name. The private key has to be in PKCS#8 format. For an example of how to create such certificates see Installation and Setup, step 2. + [Gemini]: https://gemini.circumlunar.space/ [Rust]: https://www.rust-lang.org/ [home]: gemini://qwertqwefsday.eu/agate.gmi diff --git a/src/main.rs b/src/main.rs index 4155f9c..f60ac80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,7 +96,7 @@ fn args() -> Result { "", "certs", "folder for certificate files (default ./.certificates/)", - "FOLDER", + "DIR", ); opts.optmulti( "", From 564424702a456b8cc080656c8c3702115059cb0b Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 3 Mar 2021 19:51:16 +0100 Subject: [PATCH 07/13] update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d864131..00e85d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +* The ability to specify a certificate and key with `--cert` and `--key` respectively has been replaced with the `--certs` option. + Certificates are now stored in a special directory. To migrate to this version, the keys should be stored in the `.certificates` directory (or any other directory you specify). + This enables us to use multiple certificates for multiple domains. + +### Fixed +* Agate now requires the use of SNI by any connecting client. + ## [2.5.3] - 2021-02-27 Thank you to @littleli and @06kellyjac for contributing to this release. From 5dbb4be864ae2d29dc98e4e20f703ef172a704d9 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 3 Mar 2021 23:09:29 +0100 Subject: [PATCH 08/13] improve comments --- src/certificates.rs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/certificates.rs b/src/certificates.rs index af52ac3..078156b 100644 --- a/src/certificates.rs +++ b/src/certificates.rs @@ -18,7 +18,8 @@ use { /// A struct that holds all loaded certificates and the respective domain /// names. pub(crate) struct CertStore { - // use a Vec of pairs instead of a HashMap because order matters + /// Stores the certificates and the domains they apply to, sorted by domain + /// names, longest matches first certs: Vec<(String, CertifiedKey)>, } @@ -133,7 +134,8 @@ impl CertStore { // load all certificates from directories let mut certs = vec![]; - // try to load fallback certificate and key + // Try to load fallback certificate and key directly from the top level + // certificate directory. It will be loaded as the `.` domain. match load_domain(certs_dir, ".".to_string()) { Err(CertLoadError::EmptyDomain(_)) => { /* there are no fallback keys */ } Err(CertLoadError::NoReadCertDir) => unreachable!(), @@ -153,8 +155,9 @@ impl CertStore { Err(CertLoadError::MissingCert(_)) => { return Err(CertLoadError::MissingCert("fallback".to_string())) } - // if there are files, just push them because there is no domain - // name to check against + // For the fallback keys there is no domain name to verify them + // against, so we can skip that step and only have to do it for the + // other keys below. Ok(key) => certs.push((String::new(), key)), } @@ -165,6 +168,8 @@ impl CertStore { .filter(|x| x.path().is_dir()) { let path = file.path(); + + // the filename should be the domain name let filename = path .file_name() .and_then(OsStr::to_str) @@ -184,14 +189,17 @@ impl CertStore { } certs.sort_unstable_by(|(a, _), (b, _)| { - // try to match as many as possible. If one is a substring of the other, - // the `zip` will make them look equal and make the length decide. + // Try to match as many domain segments as possible. If one is a + // substring of the other, the `zip` will only compare the smaller + // length of either a or b and the for loop will not decide. for (a_part, b_part) in a.split('.').rev().zip(b.split('.').rev()) { if a_part != b_part { + // What we sort by here is not really important, but `str` + // already implements Ord, making it easier for us. return a_part.cmp(b_part); } } - // longer domains first + // Sort longer domains first. a.len().cmp(&b.len()).reverse() }); @@ -203,13 +211,17 @@ impl ResolvesServerCert for CertStore { fn resolve(&self, client_hello: rustls::ClientHello<'_>) -> Option { if let Some(name) = client_hello.server_name() { let name: &str = name.into(); + // The certificate list is sorted so the longest match will always + // appear first. We have to find the first that is either this + // domain or a parent domain of the current one. self.certs .iter() .find(|(s, _)| name.ends_with(s)) + // only the key is interesting .map(|(_, k)| k) .cloned() } else { - // This kind of resolver requires SNI + // This kind of resolver requires SNI. None } } From afd30c386c1b8762e21c6880e32563a518047c3d Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 3 Mar 2021 23:17:35 +0100 Subject: [PATCH 09/13] implement clippy reccomendation --- src/certificates.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/certificates.rs b/src/certificates.rs index 078156b..9e32c55 100644 --- a/src/certificates.rs +++ b/src/certificates.rs @@ -183,7 +183,7 @@ impl CertStore { let key = load_domain(certs_dir, filename.clone())?; key.cross_check_end_entity_cert(Some(dns_name)) - .or_else(|e| Err(CertLoadError::BadCert(filename.clone(), e.to_string())))?; + .map_err(|e| CertLoadError::BadCert(filename.clone(), e.to_string()))?; certs.push((filename, key)); } From ddc1f4ddb2f2a0434a411c3b81ee867d8e8dd9c7 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 10 Mar 2021 22:55:50 +0100 Subject: [PATCH 10/13] update Debian install script --- tools/debian/install.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/debian/install.sh b/tools/debian/install.sh index e56fc56..902b9ab 100755 --- a/tools/debian/install.sh +++ b/tools/debian/install.sh @@ -66,8 +66,10 @@ cp geminilogs /etc/logrotate.d/ echo "setting up content files..." mkdir -p /srv/gemini/content -openssl req -x509 -newkey rsa:4096 -keyout /srv/gemini/key.rsa -out /srv/gemini/cert.pem \ - -days 3650 -nodes -subj "/CN=$(uname -n)" +mkdir -p /srv/gemini/.certificates +openssl req -x509 -newkey rsa:4096 -keyout /srv/gemini/.certificates/key.rsa \ + -out /srv/gemini/.certificates/cert.pem -days 3650 -nodes \ + -subj "/CN=$(uname -n)" -addext "subjectAltName = DNS:$(uname -n)" echo "starting service..." systemctl daemon-reload From b96cf3940b75b54a6f752579efe5255e57e29f57 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 10 Mar 2021 23:05:38 +0100 Subject: [PATCH 11/13] add uninstall script --- tools/debian/uninstall.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100755 tools/debian/uninstall.sh diff --git a/tools/debian/uninstall.sh b/tools/debian/uninstall.sh new file mode 100755 index 0000000..93de8fb --- /dev/null +++ b/tools/debian/uninstall.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# This file is part of the Agate software and licensed under either the +# MIT license or Apache license at your option. +# +# Please keep in mind that there is not warranty whatsoever provided for this +# software as specified in the disclaimer in the MIT license or section 7 of +# the Apache license respectively. + +echo "stopping and disabling service..." +systemctl stop gemini +systemctl disable gemini + +echo "removing config files..." +rm -f /etc/systemd/system/gemini.service /etc/rsyslog.d/gemini.conf /etc/logrotate.d/geminilogs + +echo "deleting certificates..." +rm -rf /srv/gemini/.certificates +# do not delete content files, user might want to use them still or can delete them manually +echo "NOTE: content files at /srv/gemini/content not deleted" +# cannot uninstall executable since we did not install it +echo "NOTE: agate executable at $(which agate) not uninstalled" From 782e0430832cde12339edb732ebe8989f376e8e9 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Thu, 11 Mar 2021 22:24:10 +0100 Subject: [PATCH 12/13] fix tests for multiple certificates The tests now actually check that a specific certificate is being used by only loading the correct one into the trust chain while running the test. The problem before was that openssl-req by default generates CA-capable certs which are not accepted by rustls. --- tests/data/multicert/ca_cert.pem | 29 ------- tests/data/multicert/ca_key.rsa | 52 ----------- tests/data/multicert/create_certs.sh | 28 +++--- tests/data/multicert/example.com/cert.pem | 54 ++++++------ tests/data/multicert/example.com/key.rsa | 100 +++++++++++----------- tests/data/multicert/example.org/cert.pem | 54 ++++++------ tests/data/multicert/example.org/key.rsa | 100 +++++++++++----------- tests/tests.rs | 4 +- 8 files changed, 167 insertions(+), 254 deletions(-) delete mode 100644 tests/data/multicert/ca_cert.pem delete mode 100644 tests/data/multicert/ca_key.rsa diff --git a/tests/data/multicert/ca_cert.pem b/tests/data/multicert/ca_cert.pem deleted file mode 100644 index 7cca683..0000000 --- a/tests/data/multicert/ca_cert.pem +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFCzCCAvOgAwIBAgIUIBGuqsV2UiwpjgNTBhsCWSEofMEwDQYJKoZIhvcNAQEL -BQAwFTETMBEGA1UEAwwKZXhhbXBsZSBDQTAeFw0yMTAzMDMxNzI1MjFaFw0zMTAz -MDExNzI1MjFaMBUxEzARBgNVBAMMCmV4YW1wbGUgQ0EwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQDAYjrXvFY9MpJ0Cy9Jte42u9peeUQ+4isGIDmnSAmA -hngVqoDlqvr6mXXU+N0c7jDkLEcs7EySORP/k1rRrAtQPSZeMSVQ47aJirmIlcnb -P9/ivyYv8lyndZCZikOAOz7D1beRV66MX2q1/plnW1QuWHbk83IqNniZcL8QRTWw -EYy8CxNd9UO9TlzF/kuIadNlFEywiQ6pbZiIhPChCtg2vKx/abQukYSGDoQxTIEp -T6uqoHevAMZFMhwhURG5kUX65xQcIDSobvxyziSc3fKW/fxzu/ORkLHBB5T3jcQJ -+BL4pb+hZl7Xe/r4TvfhSpdzu5hoLFAqLWhQUHymMG8kmqJ6Q9lfuQO4A/HuqCQN -cYtEce88uOaF/TPt99qhvg/7V1LXxZ9UNnyc2cQUroT8jx+5NtWBPQvekKSoufxX -4Kv4kdtaPyGRVZcshLQKog6kD49nzlCJhYV9UUiGQMUrzb7H5n+Y2puN87B5oUVY -djNJ65h7y27fKeXfrpvTJ+kqi9A6hrwNR6INTkRJ+l6mnpX0tnTmGmSiChsW+xCF -R2bIl04y4efqYToG3fQk0vzn+rGx57DqnpgDavIYJxsNWzJ4qWWxdN08QV8OsmEo -0u6Ks9+EVv6sHmY+WWsOB+8Jgwa0p2HcQ2nu0KrIDNVxUS29jPA/Iw7g/w95az/X -2wIDAQABo1MwUTAdBgNVHQ4EFgQUtyhzhR/wxqDZlRxR4odCXFhSwtEwHwYDVR0j -BBgwFoAUtyhzhR/wxqDZlRxR4odCXFhSwtEwDwYDVR0TAQH/BAUwAwEB/zANBgkq -hkiG9w0BAQsFAAOCAgEAZYiHTpAsWvOdqsMa9KhiWgOYAtJ/vERaTkWowxfqpBrx -nC9k5PAU4r5mAa38RX9hbRWEpXA2oEET/txg4a9oHkYPCk8+Nq/K7JwoY32gdsuC -EeAFIFhsfV5aDnoMxEfjOo7erFa89UBLup9bw3VupQ6+9B7UJP5Q1g8aapdcw2Io -nFiof56TNBUNSSJc0ErfBInCQ+T2yIbbyXvsNhbENidlP8nDv9cwHEQewUPbH9/y -k4MqsZVsdrm00m0ZWgdQfZnTgM5/TBp+tTyHJOQqfekPiqob7lPGgakMhkpGHJvu -EnkQJecgHA1/k2ETM4Ja162kbshN8LjLLrXi9aEwDYTW1xFbvK9MrHKcSOTq+FJs -WV3RK1J56pqq3iNJLXkXjSuo6bNIA4fjxJk8scRdsANAYfV9I3pJUY1EB/LvycSo -zCUgpp+tnqT+lgvCZ3aFi/Iajb0TgoNb/xgHo2MJmRNLj6RQlJkLDEYBQTE1iiru -bWZW2jf7LEBM9MwT2+I2AbmCLyPoA04ZT7GH2yeugU1YrxO5Erj6m3JBdwuKIU7g -DJH2DttPIm1ay00tFBapYoODfXwqqIPtYRSAhSxxuWRV1fl5kVgyT2TjEvv9b/8j -SMRrGvo4Ws2H8W8Fcf0EVIywkVxpE2YlzztEWhVJEmltM74slX82QZ2ppWCmepg= ------END CERTIFICATE----- diff --git a/tests/data/multicert/ca_key.rsa b/tests/data/multicert/ca_key.rsa deleted file mode 100644 index b5fb859..0000000 --- a/tests/data/multicert/ca_key.rsa +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -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 -uabNtK2No3EmpBOK31TRSIKWx7bFnw== ------END PRIVATE KEY----- diff --git a/tests/data/multicert/create_certs.sh b/tests/data/multicert/create_certs.sh index 8b5e0e7..02f58b4 100755 --- a/tests/data/multicert/create_certs.sh +++ b/tests/data/multicert/create_certs.sh @@ -2,27 +2,25 @@ mkdir -p example.com example.org -# create our own CA so we can use rustls without it complaining about using a -# CA cert as end cert -openssl req -x509 -newkey rsa:4096 -keyout ca_key.rsa -out ca_cert.pem -days 3650 -nodes -subj "/CN=example CA" - for domain in "example.com" "example.org" do +# create private key openssl genpkey -out $domain/key.rsa -algorithm RSA -pkeyopt rsa_keygen_bits:4096 +# create config file: +# the generated certificates must not be CA-capable, otherwise rustls complains cat >openssl.conf < Date: Tue, 23 Mar 2021 23:25:04 +0100 Subject: [PATCH 13/13] finish up for merge --- README.md | 4 ++++ src/certificates.rs | 7 ++++--- tests/tests.rs | 3 +-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7fb6e7a..e32215e 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,10 @@ The "error:" part will only be logged if an error occurred. This should only be There are some lines apart from these that might occur in logs depending on the selected log level. For example the initial "Listening on..." line or information about listing a particular directory. +## Security considerations + +If you want to run agate on a multi-user system, you should be aware that all certificate and key data is loaded into memory and stored there until the server stops. Since the memory is also not explicitly overwritten or zeroed after use, the sensitive data might stay in memory after the server has terminated. + [Gemini]: https://gemini.circumlunar.space/ [Rust]: https://www.rust-lang.org/ [home]: gemini://qwertqwefsday.eu/agate.gmi diff --git a/src/certificates.rs b/src/certificates.rs index 9e32c55..766de30 100644 --- a/src/certificates.rs +++ b/src/certificates.rs @@ -194,9 +194,10 @@ impl CertStore { // length of either a or b and the for loop will not decide. for (a_part, b_part) in a.split('.').rev().zip(b.split('.').rev()) { if a_part != b_part { - // What we sort by here is not really important, but `str` - // already implements Ord, making it easier for us. - return a_part.cmp(b_part); + // Here we have to make sure that the empty string will + // always be sorted to the end, so we reverse the usual + // ordering of str. + return a_part.cmp(b_part).reverse(); } } // Sort longer domains first. diff --git a/tests/tests.rs b/tests/tests.rs index e2cd390..9d9752e 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -81,8 +81,7 @@ impl Drop for Server { // a potential error message was not yet handled self.stop().unwrap(); } else if self.output.is_some() { - // error was already handled, ignore it - self.stop().unwrap_or(()); + // server was already stopped } else { // we are panicking and a potential error was not handled self.stop().unwrap_or_else(|e| eprintln!("{:?}", e));