Implement gemini cert store
This commit is contained in:
parent
4c1f843048
commit
795817f99d
@ -1,6 +1,7 @@
|
||||
pub mod acme;
|
||||
pub mod checker;
|
||||
mod config;
|
||||
pub mod gemini;
|
||||
pub mod manager;
|
||||
mod name;
|
||||
mod settings;
|
||||
|
@ -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");
|
||||
|
101
src/domain/gemini.rs
Normal file
101
src/domain/gemini.rs
Normal file
@ -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<Self> {
|
||||
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<sync::Arc<rustls::sign::CertifiedKey>> {
|
||||
let domain = client_hello.server_name()?;
|
||||
|
||||
let res: unexpected::Result<Option<sync::Arc<rustls::sign::CertifiedKey>>> = (|| {
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
@ -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<u8>);
|
||||
|
||||
impl Certificate {
|
||||
pub fn new_self_signed(
|
||||
pkey: &PrivateKey,
|
||||
domain: &domain::Name,
|
||||
) -> unexpected::Result<Certificate> {
|
||||
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<Private> = 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;
|
||||
|
||||
|
@ -11,9 +11,9 @@ pub struct PrivateKey(Vec<u8>);
|
||||
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")
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user