diff --git a/src/config.rs b/src/config.rs index acab166..07cd16e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,14 +5,20 @@ use std::fs; use std::fs::File; use std::io::Read; use acme_lib::DirectoryUrl; +use std::path::PathBuf; +use std::str::FromStr; +use crate::x509; +use crate::x509::{X509PublicKeyAlgo, X509EcPublicKeyAlgo, X509Certificate}; +use std::time::{SystemTime, Duration}; -const CERT_NAME: &str = "cert.pem"; -const KEY_NAME: &str = "key.pem"; +pub const CERT_NAME: &str = "cert.pem"; +pub const KEY_NAME: &str = "key.pem"; #[derive(Debug, Clone, Copy)] pub enum AcmeAlgo { Ec256, Ec384, + Ec521, Rsa(u32), } @@ -31,6 +37,7 @@ impl AcmeAlgo { match s { "ec256" => Ok(AcmeAlgo::Ec256), "ec384" => Ok(AcmeAlgo::Ec384), + "ec521" => Ok(AcmeAlgo::Ec521), "rsa2048" => Ok(AcmeAlgo::Rsa(2048)), "rsa3072" => Ok(AcmeAlgo::Rsa(3072)), "rsa4096" => Ok(AcmeAlgo::Rsa(4096)), @@ -79,22 +86,7 @@ pub struct AcmeConfig { pub dir: String, pub auth_timeout: Option, pub csr_timeout: Option, - pub concurrent: Option, // ? -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CertConfigItem { - pub path: String, - pub algo: Option, - pub common_name: Option, - pub dns_names: Option>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CertConfig { - pub cert_items: Vec, + pub concurrent: Option, } impl AcmeConfig { @@ -111,10 +103,94 @@ impl AcmeConfig { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CertConfigItem { + pub path: String, + pub algo: Option, + pub public_key_algo: Option, + pub common_name: Option, + pub dns_names: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CertConfig { + pub cert_items: Vec, +} + +impl CertConfig { + pub fn filter_cert_config_items(self, valid_days: i32) -> Self { + let mut cert_items = vec![]; + + let valid_days_secs = valid_days as i64 * 24 * 3600; + let secs_from_unix_epoch = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64; + for item in &self.cert_items { + let mut item2 = item.clone(); + match item2.fill_dns_names() { + Ok(Some(x509_certificate)) => { + if x509_certificate.certificate_not_after >= (valid_days_secs + secs_from_unix_epoch) { + information!("Certificate: {} is valid: {} days", item.path, + (x509_certificate.certificate_not_after - secs_from_unix_epoch)/valid_days_secs + ) + } else { + cert_items.push(item2); + } + } + Ok(None) => { + if fs::read_dir(&item.path).is_err() { + information!("Create path: {}", item.path); + fs::create_dir_all(&item.path).ok(); + } + cert_items.push(item2); + } + Err(e) => warning!("Certificate: {}, parse error: {}", item.path, e), + } + } + + Self { cert_items } + } + + pub fn load(config_fn: &str) -> XResult { + let config_content = opt_result!(fs::read_to_string(config_fn), "Load config: {}, failed: {}", config_fn); + let config: CertConfig = opt_result!(deser_hjson::from_str(&config_content), "Parse config: {}, failed: {}", config_fn); + Ok(config) + } +} + impl CertConfigItem { - pub fn fill_dns_names(&mut self) -> XResult<()> { - // TODO - Ok(()) + pub fn fill_dns_names(&mut self) -> XResult> { + if self.common_name.is_none() || self.dns_names.is_none() { + if self.path.is_empty() { + return simple_error!("Cert config item common name and path both empty"); + } + let path_buff = opt_result!(PathBuf::from_str(&self.path), "Path: {}, failed: {}", self.path); + let cert_path_buff = path_buff.join(CERT_NAME); + let pem = opt_result!(fs::read_to_string(cert_path_buff.clone()), "Read file: {:?}, failed: {}", cert_path_buff); + let x509_certificate = opt_result!(x509::parse_x509(&format!("{}/{}", self.path, CERT_NAME), &pem), "Parse x509: {}/{}, faield: {}", self.path, CERT_NAME); + self.common_name = Some(x509_certificate.common_name.clone()); + self.dns_names = Some(x509_certificate.alt_names.clone()); + self.algo = None; + self.public_key_algo = Some(x509_certificate.public_key_algo.clone()); + Ok(Some(x509_certificate)) + } else { + if self.path.is_empty() { + return simple_error!("Cert config item path is empty"); + } + if self.public_key_algo.is_none() { + self.public_key_algo = match &self.algo { + None => Some(X509PublicKeyAlgo::Rsa(2048)), + Some(algo) => match AcmeAlgo::parse(&algo) { + Ok(AcmeAlgo::Rsa(bit_length)) => Some(X509PublicKeyAlgo::Rsa(bit_length)), + Ok(AcmeAlgo::Ec256) => Some(X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp256r1)), + Ok(AcmeAlgo::Ec384) => Some(X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp384r1)), + Ok(AcmeAlgo::Ec521) => Some(X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp521r1)), + Err(_) => return simple_error!("Unknown algo: {}", algo), + }, + }; + } + Ok(None) + } } } diff --git a/src/main.rs b/src/main.rs index 1ac6996..80ea23e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ extern crate rust_util; mod config; mod x509; -mod simple_thread_pool; +// mod simple_thread_pool; use rust_util::XResult; use acme_lib::{DirectoryUrl, Directory}; @@ -23,6 +23,7 @@ use async_std::channel; use async_std::channel::Sender; use config::AcmeAlgo; use config::AcmeMode; +use crate::config::{CertConfig, CERT_NAME, KEY_NAME}; const NAME: &str = env!("CARGO_PKG_NAME"); const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -40,7 +41,7 @@ struct AcmeRequest<'a> { alt_names: &'a [&'a str], algo: AcmeAlgo, mode: AcmeMode, - dir: &'a str, + account_dir: &'a str, timeout: u64, key_file: Option, cert_file: Option, @@ -64,6 +65,8 @@ async fn main() -> tide::Result<()> { .arg(Arg::with_name("timeout").long("timeout").takes_value(true).default_value("5000").help("Timeout (ms)")) .arg(Arg::with_name("mode").short("m").long("mode").takes_value(true).default_value("prod").help("Mode")) .arg(Arg::with_name("dir").long("dir").takes_value(true).default_value("acme_dir").help("Account key dir")) + .arg(Arg::with_name("config").short("c").long("config").takes_value(true).help("Acme config")) + .arg(Arg::with_name("cert-config").long("cert-config").takes_value(true).help("Cert config")) .get_matches(); if matches.is_present("version") { @@ -120,7 +123,7 @@ async fn main() -> tide::Result<()> { exit(1); } }; - let dir = matches.value_of("dir").unwrap_or("acme_dir"); + let account_dir = matches.value_of("dir").unwrap_or("acme_dir"); let domains_val = matches.values_of("domain").unwrap_or_else(|| { failure!("Domains is not assigned."); @@ -132,33 +135,72 @@ async fn main() -> tide::Result<()> { r.recv().await.ok(); task::sleep(Duration::from_millis(500)).await; - let domains: Vec<&str> = domains_val.collect::>(); - let primary_name = domains[0]; - let alt_names: Vec<&str> = domains.into_iter().skip(1).collect(); - information!("Domains, main: {}, alt: {:?}", primary_name, alt_names); - let acme_request = AcmeRequest { - contract_email: email, - primary_name, - alt_names: &alt_names, - algo, - mode, - dir, - timeout, - ..Default::default() - }; - if let Err(e) = request_domains(acme_request) { - failure!("Request certificate by acme failed: {}", e); - exit(1); + // TODO ...... + let config = matches.value_of("config"); + + let cert_config = matches.value_of("cert-config"); + match cert_config { + None => { // cert config is not assigned + let domains: Vec<&str> = domains_val.collect::>(); + let primary_name = domains[0]; + let alt_names: Vec<&str> = domains.into_iter().skip(1).collect(); + information!("Domains, main: {}, alt: {:?}", primary_name, alt_names); + let acme_request = AcmeRequest { + contract_email: email, + primary_name, + alt_names: &alt_names, + algo, + mode, + account_dir, + timeout, + ..Default::default() + }; + if let Err(e) = request_domains(acme_request) { + failure!("Request certificate by acme failed: {}", e); + exit(1); + } + } + Some(cert_config) => { // cert config is assigned + let cert_config = { + CertConfig::load(cert_config).unwrap_or_else(|e| { + failure!("Load cert config: {}, failed: {}", cert_config, e); + exit(1); + }) + }; + let filtered_cert_config = cert_config.filter_cert_config_items(30); + + for item in &filtered_cert_config.cert_items { + if let (Some(common_name), Some(dns_names)) = (&item.common_name, &item.dns_names) { + information!("Domains, main: {}, alt: {:?}", common_name, dns_names); + let alt_names: Vec<&str> = dns_names.iter().map(|n| n.as_str()).collect(); + let acme_request = AcmeRequest { + contract_email: email, + primary_name: common_name, + alt_names: &alt_names, + algo, + mode, + account_dir, + timeout, + cert_file: Some(format!("{}/{}", item.path, CERT_NAME)), + key_file: Some(format!("{}/{}", item.path, KEY_NAME)), + }; + if let Err(e) = request_domains(acme_request) { + failure!("Request certificate: {}, by acme failed: {}", item.path, e); + } + } + } + } } + Ok(()) } fn request_domains(acme_request: AcmeRequest) -> XResult<()> { information!("Acme mode: {:?}", acme_request.mode); let url = acme_request.mode.directory_url(); - information!("Acme dir: {}", acme_request.dir); - fs::create_dir_all(acme_request.dir).ok(); - let persist = FilePersist::new(acme_request.dir); + information!("Acme dir: {}", acme_request.account_dir); + fs::create_dir_all(acme_request.account_dir).ok(); + let persist = FilePersist::new(acme_request.account_dir); let dir = opt_result!(Directory::from_url(persist, url), "Create directory from url failed: {}"); let acc = opt_result!(dir.account(acme_request.contract_email), "Directory set account failed: {}"); let mut ord_new = opt_result!( acc.new_order(acme_request.primary_name, acme_request.alt_names), "Create order failed: {}"); @@ -193,14 +235,25 @@ fn request_domains(acme_request: AcmeRequest) -> XResult<()> { let pkey_pri = match acme_request.algo { AcmeAlgo::Ec256 => create_p256_key(), AcmeAlgo::Ec384 => create_p384_key(), + AcmeAlgo::Ec521 => return simple_error!("Algo ec521 is not supported"), AcmeAlgo::Rsa(bits) => create_rsa_key(bits), }; let ord_cert = opt_result!( ord_csr.finalize_pkey(pkey_pri, acme_request.timeout), "Submit CSR failed: {}"); let cert = opt_result!( ord_cert.download_and_save_cert(), "Download and save certificate failed: {}"); - information!("Certificate key: {}", cert.private_key()); - information!("Certificate pem: {}", cert.certificate()); + + if let (Some(cert_file), Some(key_file)) = (&acme_request.cert_file, &acme_request.key_file) { + if let Err(e) = fs::write(cert_file, cert.certificate()) { + failure!("Write file: {}, failed: {}", cert_file, e); + } + if let Err(e) = fs::write(key_file, cert.private_key()) { + failure!("Write file: {}, failed: {}", key_file, e); + } + } else { + information!("Certificate key: {}", cert.private_key()); + information!("Certificate pem: {}", cert.certificate()); + } Ok(()) } diff --git a/src/x509.rs b/src/x509.rs index 4b24789..40a3321 100644 --- a/src/x509.rs +++ b/src/x509.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use x509_parser::parse_x509_certificate; use x509_parser::pem::parse_x509_pem; use x509_parser::extensions::{ParsedExtension, GeneralName}; @@ -22,23 +23,23 @@ lazy_static! { static ref OID_SECP521R1: Oid<'static> = Oid::from_str("1.3.132.0.35").unwrap(); } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum X509IssuerAlgo { RsaWithSha256, EcdsaWithSha256, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum X509EcPublicKeyAlgo { Secp256r1, Secp384r1, Secp521r1, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum X509PublicKeyAlgo { EcKey(X509EcPublicKeyAlgo), - Rsa, + Rsa(u32), } impl X509PublicKeyAlgo { @@ -62,48 +63,41 @@ impl X509PublicKeyAlgo { }; Ok(Self::EcKey(ec_public_key_algo)) } else if public_key_algo_oid == &*OID_RSA_PUBLIC_KEY { - // TODO .. - println!(":::-> {:?}", public_key_info.subject_public_key); - let d = parse_der(public_key_info.subject_public_key.data); - println!("{:?}", d); - println!("{:?}", d.as_ref().unwrap().1.content); - if let BerObjectContent::Sequence(seq) = &d.as_ref().unwrap().1.content { + let public_key_data = parse_der(public_key_info.subject_public_key.data); + if let BerObjectContent::Sequence(seq) = &public_key_data.as_ref().unwrap().1.content { let mut rsa_n_len = 0; if let BerObjectContent::Integer(n) = seq[0].content { rsa_n_len = n.len() - (if n[0] == 0 { 1 } else { 0 }); } - let rsa_bit_length = || -> Option { + let get_rsa_bit_length = || -> Option { for bit_len in &[1024, 2048, 3072, 4096] { - if i32::abs(bit_len - (rsa_n_len as i32 * 8)) <= 16 { + if i32::abs(bit_len - (rsa_n_len as i32 * 8)) <= 32 { return Some(*bit_len); } } None }; + if let Some(rsa_bit_length) = get_rsa_bit_length() { + return Ok(Self::Rsa(rsa_bit_length as u32)); + } } - Ok(Self::Rsa) + simple_error!("Parse cert: {}, not valid rsa public key", pem_id) } else { simple_error!("Parse cert: {}, unknown public key algo oid: {}", pem_id, public_key_algo_oid) } } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct X509Certificate { pub issuer_algo: X509IssuerAlgo, pub common_name: String, pub alt_names: Vec, pub public_key_algo: X509PublicKeyAlgo, + pub certificate_not_after: i64, } -#[test] -fn test() { - let p = include_str!("sample_cert.pem"); - let c = parse_x500("aa", p); - println!("{:?}", c); -} - -pub fn parse_x500(pem_id: &str, pem: &str) -> XResult { +pub fn parse_x509(pem_id: &str, pem: &str) -> XResult { let (_, der) = opt_result!(parse_x509_pem(pem.as_bytes()), "Parse pem: {} to der failed: {}", pem_id); let (_, cert) = opt_result!(parse_x509_certificate(der.contents.as_slice()), "Parse cert: {} failed: {}", pem_id); @@ -138,10 +132,23 @@ pub fn parse_x500(pem_id: &str, pem: &str) -> XResult { } let public_key_algo = X509PublicKeyAlgo::parse(pem_id, &cert.tbs_certificate.subject_pki)?; + let certificate_not_after = cert.tbs_certificate.validity.not_after.timestamp(); + Ok(X509Certificate { issuer_algo, common_name, alt_names, public_key_algo, + certificate_not_after, }) } + +#[test] +fn test_sample_cert() { + let cert_pem = include_str!("sample_cert.pem"); + let x509_certificate = parse_x509("test", cert_pem).unwrap(); + assert_eq!(X509IssuerAlgo::RsaWithSha256, x509_certificate.issuer_algo); + assert_eq!("zigstack.org", x509_certificate.common_name.as_str()); + assert_eq!(vec!["www.zigstack.org", "zigstack.org"], x509_certificate.alt_names); + assert_eq!(X509PublicKeyAlgo::Rsa(2048), x509_certificate.public_key_algo); +}