mirror of
https://github.com/YGGverse/agate.git
synced 2026-04-08 20:45:29 +00:00
add automatic certificate generation
This commit is contained in:
parent
d24db63583
commit
2213b055dc
22 changed files with 251 additions and 422 deletions
|
|
@ -1,14 +1,11 @@
|
|||
use {
|
||||
rustls::{
|
||||
internal::pemfile::{certs, pkcs8_private_keys},
|
||||
sign::{CertifiedKey, RSASigningKey},
|
||||
sign::{any_supported_type, CertifiedKey},
|
||||
ResolvesServerCert,
|
||||
},
|
||||
std::{
|
||||
ffi::OsStr,
|
||||
fmt::{Display, Formatter},
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
},
|
||||
|
|
@ -23,17 +20,17 @@ pub(crate) struct CertStore {
|
|||
certs: Vec<(String, CertifiedKey)>,
|
||||
}
|
||||
|
||||
static CERT_FILE_NAME: &str = "cert.pem";
|
||||
static KEY_FILE_NAME: &str = "key.rsa";
|
||||
pub static CERT_FILE_NAME: &str = "cert.der";
|
||||
pub static KEY_FILE_NAME: &str = "key.der";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CertLoadError {
|
||||
/// could not access the certificate root directory
|
||||
NoReadCertDir,
|
||||
/// no certificates or keys were found
|
||||
Empty,
|
||||
/// 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),
|
||||
|
|
@ -55,17 +52,13 @@ 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::Empty => write!(f, "No keys or certificates were found in the given directory.\nSpecify the --hostname option to generate these automatically."),
|
||||
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::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, e) => {
|
||||
write!(f, "The certificate file for {} is malformed: {}", domain, e)
|
||||
|
|
@ -97,29 +90,25 @@ fn load_domain(certs_dir: &Path, domain: String) -> Result<CertifiedKey, CertLoa
|
|||
CertLoadError::MissingCert(domain)
|
||||
});
|
||||
}
|
||||
|
||||
let cert_chain = match certs(&mut BufReader::new(File::open(&path).unwrap())) {
|
||||
Ok(cert) => cert,
|
||||
Err(()) => return Err(CertLoadError::BadCert(domain, String::new())),
|
||||
};
|
||||
let cert = rustls::Certificate(
|
||||
std::fs::read(&path).map_err(|_| CertLoadError::MissingCert(domain.clone()))?,
|
||||
);
|
||||
|
||||
// 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),
|
||||
Ok(_) => return Err(CertLoadError::NoKeys(domain)),
|
||||
Err(()) => return Err(CertLoadError::BadKey(domain)),
|
||||
};
|
||||
let key = rustls::PrivateKey(
|
||||
std::fs::read(&path).map_err(|_| CertLoadError::MissingKey(domain.clone()))?,
|
||||
);
|
||||
|
||||
// transform key to correct format
|
||||
let key = match RSASigningKey::new(&key) {
|
||||
let key = match any_supported_type(&key) {
|
||||
Ok(key) => key,
|
||||
Err(()) => return Err(CertLoadError::BadKey(domain)),
|
||||
};
|
||||
Ok(CertifiedKey::new(cert_chain, Arc::new(Box::new(key))))
|
||||
Ok(CertifiedKey::new(vec![cert], Arc::new(key)))
|
||||
}
|
||||
|
||||
impl CertStore {
|
||||
|
|
@ -135,14 +124,12 @@ impl CertStore {
|
|||
let mut certs = vec![];
|
||||
|
||||
// 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()) {
|
||||
// certificate directory.
|
||||
match load_domain(certs_dir, String::new()) {
|
||||
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::Empty)
|
||||
| Err(CertLoadError::NoReadCertDir)
|
||||
| Err(CertLoadError::BadDomain(_)) => unreachable!(),
|
||||
Err(CertLoadError::BadKey(_)) => {
|
||||
return Err(CertLoadError::BadKey("fallback".to_string()))
|
||||
}
|
||||
|
|
@ -188,6 +175,10 @@ impl CertStore {
|
|||
certs.push((filename, key));
|
||||
}
|
||||
|
||||
if certs.is_empty() {
|
||||
return Err(CertLoadError::Empty);
|
||||
}
|
||||
|
||||
certs.sort_unstable_by(|(a, _), (b, _)| {
|
||||
// Try to match as many domain segments as possible. If one is a
|
||||
// substring of the other, the `zip` will only compare the smaller
|
||||
|
|
|
|||
79
src/main.rs
79
src/main.rs
|
|
@ -7,12 +7,15 @@ use metadata::{FileOptions, PresetMeta};
|
|||
use {
|
||||
once_cell::sync::Lazy,
|
||||
percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS},
|
||||
rcgen::{Certificate, CertificateParams, DnType},
|
||||
rustls::{NoClientAuth, ServerConfig},
|
||||
std::{
|
||||
borrow::Cow,
|
||||
error::Error,
|
||||
ffi::OsStr,
|
||||
fmt::Write,
|
||||
fs::{self, File},
|
||||
io::Write as _,
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
|
|
@ -132,20 +135,88 @@ fn args() -> Result<Args> {
|
|||
"central-conf",
|
||||
"Use a central .meta file in the content root directory. Decentral config files will be ignored.",
|
||||
);
|
||||
opts.optflag(
|
||||
"",
|
||||
"ecdsa",
|
||||
"Generate keys using the ecdsa signature algorithm instead of the default ed25519.",
|
||||
);
|
||||
|
||||
let matches = opts.parse(&args[1..]).map_err(|f| f.to_string())?;
|
||||
|
||||
if matches.opt_present("h") {
|
||||
eprintln!("{}", opts.usage(&format!("Usage: {} [options]", &args[0])));
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
if matches.opt_present("V") {
|
||||
eprintln!("agate {}", env!("CARGO_PKG_VERSION"));
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let certs_path = check_path(matches.opt_get_default("certs", ".certificates".into())?)?;
|
||||
let certs = match certificates::CertStore::load_from(&certs_path) {
|
||||
Ok(certs) => Some(certs),
|
||||
Err(certificates::CertLoadError::Empty) if matches.opt_present("hostname") => {
|
||||
// we will generate certificates in the next step
|
||||
None
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
let mut reload_certs = false;
|
||||
let mut hostnames = vec![];
|
||||
for s in matches.opt_strs("hostname") {
|
||||
hostnames.push(Host::parse(&s)?);
|
||||
let hostname = Host::parse(&s)?;
|
||||
|
||||
// check if we have a certificate for that domain
|
||||
if let Host::Domain(ref domain) = hostname {
|
||||
if !matches!(certs, Some(ref certs) if certs.has_domain(domain)) {
|
||||
eprintln!("no certificate or key found for {:?}, generating...", s);
|
||||
|
||||
let mut cert_params = CertificateParams::new(vec![domain.clone()]);
|
||||
cert_params
|
||||
.distinguished_name
|
||||
.push(DnType::CommonName, domain);
|
||||
|
||||
// <CertificateParams as Default>::default() already implements a
|
||||
// date in the far future from the time of writing: 4096-01-01
|
||||
|
||||
if !matches.opt_present("ecdsa") {
|
||||
cert_params.alg = &rcgen::PKCS_ED25519;
|
||||
}
|
||||
|
||||
// generate the certificate with the configuration
|
||||
let cert = Certificate::from_params(cert_params)?;
|
||||
|
||||
fs::create_dir(certs_path.join(domain))?;
|
||||
// write certificate data to disk
|
||||
let mut cert_file = File::create(certs_path.join(format!(
|
||||
"{}/{}",
|
||||
domain,
|
||||
certificates::CERT_FILE_NAME
|
||||
)))?;
|
||||
cert_file.write_all(&cert.serialize_der()?)?;
|
||||
let mut key_file = File::create(certs_path.join(format!(
|
||||
"{}/{}",
|
||||
domain,
|
||||
certificates::KEY_FILE_NAME
|
||||
)))?;
|
||||
key_file.write_all(&cert.serialize_private_key_der())?;
|
||||
|
||||
reload_certs = true;
|
||||
}
|
||||
}
|
||||
|
||||
hostnames.push(hostname);
|
||||
}
|
||||
|
||||
// if new certificates were generated, reload the certificate store
|
||||
let certs = if reload_certs {
|
||||
certificates::CertStore::load_from(&certs_path)?
|
||||
} else {
|
||||
certs.unwrap()
|
||||
};
|
||||
|
||||
let mut addrs = vec![];
|
||||
for i in matches.opt_strs("addr") {
|
||||
addrs.push(i.parse()?);
|
||||
|
|
@ -157,14 +228,10 @@ fn args() -> Result<Args> {
|
|||
];
|
||||
}
|
||||
|
||||
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())?)?,
|
||||
certs,
|
||||
certs: Arc::new(certs),
|
||||
hostnames,
|
||||
language: matches.opt_str("lang"),
|
||||
serve_secret: matches.opt_present("serve-secret"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue