feat: cert config
This commit is contained in:
118
src/config.rs
118
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<u64>,
|
||||
pub csr_timeout: Option<u64>,
|
||||
pub concurrent: Option<u32>, // ?
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CertConfigItem {
|
||||
pub path: String,
|
||||
pub algo: Option<String>,
|
||||
pub common_name: Option<String>,
|
||||
pub dns_names: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CertConfig {
|
||||
pub cert_items: Vec<CertConfigItem>,
|
||||
pub concurrent: Option<u32>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
pub public_key_algo: Option<X509PublicKeyAlgo>,
|
||||
pub common_name: Option<String>,
|
||||
pub dns_names: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CertConfig {
|
||||
pub cert_items: Vec<CertConfigItem>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
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<Option<X509Certificate>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
103
src/main.rs
103
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<String>,
|
||||
cert_file: Option<String>,
|
||||
@@ -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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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(())
|
||||
}
|
||||
|
||||
51
src/x509.rs
51
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<i32> {
|
||||
let get_rsa_bit_length = || -> Option<i32> {
|
||||
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<String>,
|
||||
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<X509Certificate> {
|
||||
pub fn parse_x509(pem_id: &str, pem: &str) -> XResult<X509Certificate> {
|
||||
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<X509Certificate> {
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user