From 795817f99dfeca609a382f5a48efc4194f3915a7 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Thu, 27 Jul 2023 16:09:44 +0200 Subject: [PATCH] Implement gemini cert store --- src/domain.rs | 1 + src/domain/acme/manager.rs | 4 +- src/domain/gemini.rs | 101 ++++++++++++++++++++++++++++++++++ src/domain/tls/certificate.rs | 64 +++++++++++++++++++++ src/domain/tls/private_key.rs | 6 +- src/main.rs | 8 ++- 6 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 src/domain/gemini.rs diff --git a/src/domain.rs b/src/domain.rs index 907af98..327aa09 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -1,6 +1,7 @@ pub mod acme; pub mod checker; mod config; +pub mod gemini; pub mod manager; mod name; mod settings; diff --git a/src/domain/acme/manager.rs b/src/domain/acme/manager.rs index 35fc1fb..7eca505 100644 --- a/src/domain/acme/manager.rs +++ b/src/domain/acme/manager.rs @@ -106,8 +106,8 @@ impl Manager for ManagerImpl { ) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>> { Box::pin(async move { // if there's an existing cert, and its expiry (determined by the soonest value of - // not_after amongst its parts) is later than 30 days from now, then we consider it to be - // synced. + // not_after amongst its parts) is later than 30 days from now, then we consider it to + // be synced. if let Ok((_, cert)) = self.store.get_certificate(domain.as_str()) { let thirty_days = openssl::asn1::Asn1Time::days_from_now(30) .expect("parsed thirty days from now as Asn1Time"); diff --git a/src/domain/gemini.rs b/src/domain/gemini.rs new file mode 100644 index 0000000..3dd335d --- /dev/null +++ b/src/domain/gemini.rs @@ -0,0 +1,101 @@ +use crate::domain::tls::{Certificate, PrivateKey}; +use crate::error::unexpected::{self, Mappable}; +use crate::{domain, util}; + +use serde::{Deserialize, Serialize}; + +use std::{fs, path, sync}; + +#[derive(Debug, Serialize, Deserialize)] +struct StoredPKeyCert { + private_key: PrivateKey, + cert: Certificate, +} + +pub struct FSStore { + cert_dir_path: path::PathBuf, +} + +impl FSStore { + pub fn new(dir_path: &path::Path) -> unexpected::Result { + let cert_dir_path = dir_path.join("certificates"); + fs::create_dir_all(&cert_dir_path) + .map_unexpected_while(|| format!("creating dir {}", cert_dir_path.display()))?; + + Ok(Self { cert_dir_path }) + } + + fn pkey_cert_path(&self, domain: &domain::Name) -> path::PathBuf { + let mut domain = domain.as_str().to_string(); + domain.push_str(".json"); + self.cert_dir_path.join(domain) + } + + fn get_certificate( + &self, + domain: &domain::Name, + ) -> unexpected::Result<(PrivateKey, Certificate)> { + let path = self.pkey_cert_path(domain); + + let file = match util::open_file(path.as_path()) + .map_unexpected_while(|| format!("opening file {}", path.display()))? + { + Some(file) => file, + None => { + let pkey = PrivateKey::new(); + let cert = Certificate::new_self_signed(&pkey, domain) + .or_unexpected_while("creating self-signed cert")?; + + let file = fs::File::create(path.as_path()) + .map_unexpected_while(|| format!("creating file {}", path.display()))?; + + let stored = StoredPKeyCert { + private_key: pkey, + cert, + }; + + serde_json::to_writer(file, &stored).or_unexpected_while("writing cert to file")?; + + return Ok((stored.private_key, stored.cert)); + } + }; + + let stored: StoredPKeyCert = + serde_json::from_reader(file).or_unexpected_while("parsing json")?; + + Ok((stored.private_key, stored.cert)) + } +} + +impl rustls::server::ResolvesServerCert for FSStore { + fn resolve( + &self, + client_hello: rustls::server::ClientHello<'_>, + ) -> Option> { + let domain = client_hello.server_name()?; + + let res: unexpected::Result>> = (|| { + let domain: domain::Name = domain + .parse() + .map_unexpected_while(|| format!("parsing domain {domain}"))?; + + let (pkey, cert) = self + .get_certificate(&domain) + .or_unexpected_while("fetching pkey/cert")?; + + let pkey = rustls::sign::any_supported_type(&pkey.into()).or_unexpected()?; + + Ok(Some(sync::Arc::new(rustls::sign::CertifiedKey { + cert: vec![cert.into()], + key: pkey, + ocsp: None, + sct_list: None, + }))) + })(); + + res.unwrap_or_else(|err| { + log::error!("Unexpected error getting cert for domain {domain}: {err}"); + None + }) + } +} diff --git a/src/domain/tls/certificate.rs b/src/domain/tls/certificate.rs index 57d641b..0eadf0e 100644 --- a/src/domain/tls/certificate.rs +++ b/src/domain/tls/certificate.rs @@ -1,3 +1,7 @@ +use crate::domain; +use crate::domain::tls::PrivateKey; +use crate::error::unexpected::{self, Mappable}; + use std::convert::{From, TryFrom}; use std::fmt; use std::str::FromStr; @@ -8,6 +12,66 @@ use serde_with::{DeserializeFromStr, SerializeDisplay}; /// DER-encoded X.509, like rustls::Certificate. pub struct Certificate(Vec); +impl Certificate { + pub fn new_self_signed( + pkey: &PrivateKey, + domain: &domain::Name, + ) -> unexpected::Result { + use openssl::asn1::*; + use openssl::pkey::*; + use openssl::x509::extension::SubjectAlternativeName; + use openssl::x509::*; + + let name = { + let mut builder = X509Name::builder().expect("initializing x509 name builder"); + builder + .append_entry_by_text("CN", domain.as_str()) + .or_unexpected_while("adding CN")?; + builder.build() + }; + + let mut builder = X509::builder().expect("initializing x509 builder"); + builder + .set_subject_name(&name) + .or_unexpected_while("setting subject name")?; + + // 9999/07/23 + let not_after = Asn1Time::from_unix(253388296800).expect("initializing not_after"); + builder + .set_not_after(not_after.as_ref()) + .or_unexpected_while("setting not_after")?; + + // Add domain as SAN + let san_extension = { + let mut san = SubjectAlternativeName::new(); + san.dns(domain.as_str()); + san.build(&builder.x509v3_context(None, None)) + .or_unexpected_while("building SAN")? + }; + + builder + .append_extension(san_extension) + .or_unexpected_while("appending SAN")?; + + let pkey: PKey = pkey.try_into().or_unexpected_while("converting PKey")?; + + builder + .set_pubkey(pkey.as_ref()) + .or_unexpected_while("setting pubkey")?; + + builder + .sign(pkey.as_ref(), openssl::hash::MessageDigest::sha256()) + .or_unexpected_while("signing with private key")?; + + let cert = builder.build(); + + Ok(cert + .as_ref() + .try_into() + .or_unexpected_while("converting to cert")?) + } +} + impl FromStr for Certificate { type Err = pem::PemError; diff --git a/src/domain/tls/private_key.rs b/src/domain/tls/private_key.rs index e7b1104..945fcbb 100644 --- a/src/domain/tls/private_key.rs +++ b/src/domain/tls/private_key.rs @@ -11,9 +11,9 @@ pub struct PrivateKey(Vec); impl PrivateKey { #[allow(clippy::new_without_default)] pub fn new() -> PrivateKey { - acme2::gen_rsa_private_key(4096) - .expect("RSA private key generated") - .as_ref() + let rsa = openssl::rsa::Rsa::generate(4096).expect("generating RSA"); + let key = openssl::pkey::PKey::from_rsa(rsa).expect("creating private key from RSA"); + key.as_ref() .try_into() .expect("RSA private key converted to internal representation") } diff --git a/src/main.rs b/src/main.rs index 888128a..3b950d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use clap::Parser; use futures::stream::StreamExt; use signal_hook_tokio::Signals; -use std::path; +use std::{path, sync}; #[derive(Parser, Debug)] #[command(version)] @@ -128,6 +128,10 @@ async fn main() { None }; + let domain_gemini_store = + domani::domain::gemini::FSStore::new(&config.domain.store_dir_path.join("gemini")) + .unwrap_or_else(|e| panic!("domain gemini store initialization failed: {e}")); + let mut task_stack = domani::task_stack::TaskStack::new(); let domain_manager = domani::domain::manager::ManagerImpl::new( @@ -147,7 +151,7 @@ async fn main() { let _ = domani::service::gemini::Service::new( &mut task_stack, - domain_manager.clone(), + sync::Arc::new(domain_gemini_store), config.service, );