diff --git a/acme_common/Cargo.toml b/acme_common/Cargo.toml index ca5b361..fa6aec9 100644 --- a/acme_common/Cargo.toml +++ b/acme_common/Cargo.toml @@ -22,6 +22,7 @@ openssl = "0.10" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" syslog = "4.0" +time = "0.1" toml = "0.5" [target.'cfg(unix)'.dependencies] diff --git a/acme_common/src/crypto.rs b/acme_common/src/crypto.rs index 6833708..2943f29 100644 --- a/acme_common/src/crypto.rs +++ b/acme_common/src/crypto.rs @@ -1,3 +1,6 @@ mod openssl_keys; pub use openssl_keys::{gen_keypair, KeyType, PrivateKey, PublicKey}; pub const DEFAULT_ALGO: &str = "rsa2048"; + +mod openssl_certificate; +pub use openssl_certificate::{Csr, X509Certificate}; diff --git a/acme_common/src/crypto/openssl_certificate.rs b/acme_common/src/crypto/openssl_certificate.rs new file mode 100644 index 0000000..9fbe858 --- /dev/null +++ b/acme_common/src/crypto/openssl_certificate.rs @@ -0,0 +1,163 @@ +use super::{gen_keypair, KeyType, PrivateKey, PublicKey}; +use crate::b64_encode; +use crate::error::Error; +use openssl::asn1::Asn1Time; +use openssl::bn::{BigNum, MsbOption}; +use openssl::hash::MessageDigest; +use openssl::stack::Stack; +use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName}; +use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509Req, X509ReqBuilder, X509}; +use std::collections::HashSet; +use time::strptime; + +const APP_ORG: &str = "ACMEd"; +const APP_NAME: &str = "ACMEd"; +const X509_VERSION: i32 = 0x02; +const CRT_SERIAL_NB_BITS: i32 = 32; +const CRT_NB_DAYS_VALIDITY: u32 = 7; +const INVALID_EXT_MSG: &str = "Invalid acmeIdentifier extension."; + +pub struct Csr { + inner_csr: X509Req, +} + +impl Csr { + pub fn new( + pub_key: &PublicKey, + priv_key: &PrivateKey, + domains: &[String], + ) -> Result { + let mut builder = X509ReqBuilder::new()?; + builder.set_pubkey(&pub_key.inner_key)?; + let ctx = builder.x509v3_context(None); + let mut san = SubjectAlternativeName::new(); + for dns in domains.iter() { + san.dns(&dns); + } + let san = san.build(&ctx)?; + let mut ext_stack = Stack::new()?; + ext_stack.push(san)?; + builder.add_extensions(&ext_stack)?; + builder.sign(&priv_key.inner_key, MessageDigest::sha256())?; + Ok(Csr { + inner_csr: builder.build(), + }) + } + + pub fn to_der_base64(&self) -> Result { + let csr = self.inner_csr.to_der()?; + let csr = b64_encode(&csr); + Ok(csr) + } +} + +pub struct X509Certificate { + pub inner_cert: X509, +} + +impl X509Certificate { + pub fn from_pem(pem_data: &[u8]) -> Result { + Ok(X509Certificate { + inner_cert: X509::from_pem(pem_data)?, + }) + } + + pub fn from_acme_ext(domain: &str, acme_ext: &str) -> Result<(PrivateKey, Self), Error> { + let (pub_key, priv_key) = gen_keypair(KeyType::EcdsaP256)?; + let inner_cert = gen_certificate(domain, &pub_key, &priv_key, acme_ext)?; + let cert = X509Certificate { inner_cert }; + Ok((priv_key, cert)) + } + + pub fn public_key(&self) -> Result { + let raw_key = self.inner_cert.public_key()?.public_key_to_pem()?; + let pub_key = PublicKey::from_pem(&raw_key)?; + Ok(pub_key) + } + + // OpenSSL ASN1_TIME_print madness + // The real fix would be to add Asn1TimeRef access in the openssl crate. + // + // https://github.com/sfackler/rust-openssl/issues/687 + // https://github.com/sfackler/rust-openssl/pull/673 + pub fn not_after(&self) -> Result { + let formats = [ + "%b %d %T %Y %Z", + "%b %d %T %Y %Z", + "%b %d %T %Y", + "%b %d %T %Y", + "%b %d %T.%f %Y %Z", + "%b %d %T.%f %Y %Z", + "%b %d %T.%f %Y", + "%b %d %T.%f %Y", + ]; + let not_after = self.inner_cert.not_after().to_string(); + for fmt in formats.iter() { + if let Ok(t) = strptime(¬_after, fmt) { + return Ok(t); + } + } + Err(format!("invalid time string: {}", ¬_after).into()) + } + + pub fn subject_alt_names(&self) -> HashSet { + match self.inner_cert.subject_alt_names() { + Some(s) => s + .iter() + .filter(|v| v.dnsname().is_some()) + .map(|v| v.dnsname().unwrap().to_string()) + .collect(), + None => HashSet::new(), + } + } +} + +fn gen_certificate( + domain: &str, + public_key: &PublicKey, + private_key: &PrivateKey, + acme_ext: &str, +) -> Result { + let mut x509_name = X509NameBuilder::new()?; + x509_name.append_entry_by_text("O", APP_ORG)?; + let ca_name = format!("{} TLS-ALPN-01 Authority", APP_NAME); + x509_name.append_entry_by_text("CN", &ca_name)?; + let x509_name = x509_name.build(); + + let mut builder = X509Builder::new()?; + builder.set_version(X509_VERSION)?; + let serial_number = { + let mut serial = BigNum::new()?; + serial.rand(CRT_SERIAL_NB_BITS - 1, MsbOption::MAYBE_ZERO, false)?; + serial.to_asn1_integer()? + }; + builder.set_serial_number(&serial_number)?; + builder.set_subject_name(&x509_name)?; + builder.set_issuer_name(&x509_name)?; + builder.set_pubkey(&public_key.inner_key)?; + let not_before = Asn1Time::days_from_now(0)?; + builder.set_not_before(¬_before)?; + let not_after = Asn1Time::days_from_now(CRT_NB_DAYS_VALIDITY)?; + builder.set_not_after(¬_after)?; + + builder.append_extension(BasicConstraints::new().build()?)?; + let ctx = builder.x509v3_context(None, None); + let san_ext = SubjectAlternativeName::new().dns(domain).build(&ctx)?; + builder.append_extension(san_ext)?; + + let ctx = builder.x509v3_context(None, None); + let mut v: Vec<&str> = acme_ext.split('=').collect(); + let value = v.pop().ok_or_else(|| Error::from(INVALID_EXT_MSG))?; + let acme_ext_name = v.pop().ok_or_else(|| Error::from(INVALID_EXT_MSG))?; + if !v.is_empty() { + return Err(Error::from(INVALID_EXT_MSG)); + } + let acme_ext = X509Extension::new(None, Some(&ctx), &acme_ext_name, &value) + .map_err(|_| Error::from(INVALID_EXT_MSG))?; + builder + .append_extension(acme_ext) + .map_err(|_| Error::from(INVALID_EXT_MSG))?; + builder.sign(&private_key.inner_key, MessageDigest::sha256())?; + let cert = builder.build(); + Ok(cert) +} diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index a925353..4d9caa1 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -5,6 +5,7 @@ use crate::acme_proto::structs::{ }; use crate::certificate::Certificate; use crate::storage; +use acme_common::crypto::Csr; use acme_common::error::Error; use std::fmt; @@ -176,8 +177,9 @@ pub fn request_certificate(cert: &Certificate, root_certs: &[String]) -> Result< )?; // 11. Finalize the order by sending the CSR - let (priv_key, pub_key) = certificate::get_key_pair(cert)?; - let csr = certificate::generate_csr(cert, &priv_key, &pub_key)?; + let (pub_key, priv_key) = certificate::get_key_pair(cert)?; + let domains: Vec = cert.domains.iter().map(|e| e.dns.to_owned()).collect(); + let csr = Csr::new(&pub_key, &priv_key, domains.as_slice())?.to_der_base64()?; let data_builder = set_data_builder!(account, csr.as_bytes(), order.finalize); let (order, nonce): (Order, String) = http::get_obj(cert, root_certs, &order.finalize, &data_builder, &nonce)?; diff --git a/acmed/src/acme_proto/certificate.rs b/acmed/src/acme_proto/certificate.rs index fd874fa..fdb358d 100644 --- a/acmed/src/acme_proto/certificate.rs +++ b/acmed/src/acme_proto/certificate.rs @@ -1,13 +1,7 @@ use crate::certificate::{Algorithm, Certificate}; use crate::storage; -use acme_common::b64_encode; use acme_common::crypto::{gen_keypair, KeyType, PrivateKey, PublicKey}; use acme_common::error::Error; -use openssl::hash::MessageDigest; -use openssl::stack::Stack; -use openssl::x509::extension::SubjectAlternativeName; -use openssl::x509::X509ReqBuilder; -use serde_json::json; fn gen_key_pair(cert: &Certificate) -> Result<(PublicKey, PrivateKey), Error> { let key_type = match cert.algo { @@ -37,27 +31,3 @@ pub fn get_key_pair(cert: &Certificate) -> Result<(PublicKey, PrivateKey), Error gen_key_pair(cert) } } - -pub fn generate_csr( - cert: &Certificate, - pub_key: &PublicKey, - priv_key: &PrivateKey, -) -> Result { - let mut builder = X509ReqBuilder::new()?; - builder.set_pubkey(&pub_key.inner_key)?; - let ctx = builder.x509v3_context(None); - let mut san = SubjectAlternativeName::new(); - for c in cert.domains.iter() { - san.dns(&c.dns); - } - let san = san.build(&ctx)?; - let mut ext_stack = Stack::new()?; - ext_stack.push(san)?; - builder.add_extensions(&ext_stack)?; - builder.sign(&priv_key.inner_key, MessageDigest::sha256())?; - let csr = builder.build(); - let csr = csr.to_der()?; - let csr = b64_encode(&csr); - let csr = json!({ "csr": csr }); - Ok(csr.to_string()) -} diff --git a/acmed/src/certificate.rs b/acmed/src/certificate.rs index 76104df..1e86921 100644 --- a/acmed/src/certificate.rs +++ b/acmed/src/certificate.rs @@ -2,13 +2,13 @@ use crate::acme_proto::Challenge; use crate::config::{Account, Domain, HookType}; use crate::hooks::{self, ChallengeHookData, Hook, HookEnvData, PostOperationHookData}; use crate::storage::{certificate_files_exists, get_certificate}; +use acme_common::crypto::X509Certificate; use acme_common::error::Error; use log::{debug, info, trace, warn}; -use openssl::x509::X509; use std::collections::{HashMap, HashSet}; use std::fmt; use std::sync::mpsc::SyncSender; -use time::{strptime, Duration}; +use time::Duration; #[derive(Clone, Debug)] pub enum Algorithm { @@ -90,33 +90,6 @@ impl Certificate { trace!("{}: {}", &self, msg); } - // OpenSSL ASN1_TIME_print madness - // The real fix would be to add Asn1TimeRef access in the openssl crate. - // - // https://github.com/sfackler/rust-openssl/issues/687 - // https://github.com/sfackler/rust-openssl/pull/673 - fn parse_openssl_time_string(&self, time: &str) -> Result { - self.debug(&format!("Parsing OpenSSL time: \"{}\"", time)); - let formats = [ - "%b %d %T %Y %Z", - "%b %d %T %Y %Z", - "%b %d %T %Y", - "%b %d %T %Y", - "%b %d %T.%f %Y %Z", - "%b %d %T.%f %Y %Z", - "%b %d %T.%f %Y", - "%b %d %T.%f %Y", - ]; - for fmt in formats.iter() { - if let Ok(t) = strptime(time, fmt) { - self.trace(&format!("Format \"{}\" matches", fmt)); - return Ok(t); - } - self.trace(&format!("Format \"{}\" does not match", fmt)); - } - Err(format!("invalid time string: {}", time).into()) - } - pub fn get_domain_challenge(&self, domain_name: &str) -> Result { let domain_name = domain_name.to_string(); for d in self.domains.iter() { @@ -128,9 +101,8 @@ impl Certificate { Err(format!("{}: domain name not found", domain_name).into()) } - fn is_expiring(&self, cert: &X509) -> Result { - let not_after = cert.not_after().to_string(); - let not_after = self.parse_openssl_time_string(¬_after)?; + fn is_expiring(&self, cert: &X509Certificate) -> Result { + let not_after = cert.not_after()?; self.debug(&format!("not after: {}", not_after.asctime())); // TODO: allow a custom duration (using time-parse ?) let renewal_time = not_after - Duration::weeks(3); @@ -138,15 +110,8 @@ impl Certificate { Ok(time::now_utc() > renewal_time) } - fn has_missing_domains(&self, cert: &X509) -> bool { - let cert_names = match cert.subject_alt_names() { - Some(s) => s - .iter() - .filter(|v| v.dnsname().is_some()) - .map(|v| v.dnsname().unwrap().to_string()) - .collect(), - None => HashSet::new(), - }; + fn has_missing_domains(&self, cert: &X509Certificate) -> bool { + let cert_names = cert.subject_alt_names(); let req_names = self .domains .iter() @@ -242,58 +207,3 @@ impl Certificate { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::{Algorithm, Certificate}; - use std::collections::HashMap; - use std::sync::mpsc::sync_channel; - - fn get_dummy_certificate() -> Certificate { - let (https_throttle, _) = sync_channel(0); - Certificate { - account: crate::config::Account { - name: String::new(), - email: String::new(), - }, - domains: Vec::new(), - algo: Algorithm::Rsa2048, - kp_reuse: false, - remote_url: String::new(), - tos_agreed: false, - https_throttle, - hooks: Vec::new(), - account_directory: String::new(), - crt_directory: String::new(), - crt_name: String::new(), - crt_name_format: String::new(), - cert_file_mode: 0, - cert_file_owner: None, - cert_file_group: None, - pk_file_mode: 0, - pk_file_owner: None, - pk_file_group: None, - env: HashMap::new(), - id: 0, - } - } - - #[test] - fn test_parse_openssl_time() { - let time_str_lst = [ - "May 7 18:34:07 2024", - "May 7 18:34:07 2024 GMT", - "May 17 18:34:07 2024", - "May 17 18:34:07 2024 GMT", - "May 7 18:34:07.922661874 2024", - "May 7 18:34:07.922661874 2024 GMT", - "May 17 18:34:07.922661874 2024", - "May 17 18:34:07.922661874 2024 GMT", - ]; - let crt = get_dummy_certificate(); - for time_str in time_str_lst.iter() { - let time_res = crt.parse_openssl_time_string(time_str); - assert!(time_res.is_ok()); - } - } -} diff --git a/acmed/src/storage.rs b/acmed/src/storage.rs index 09787dc..00dbc27 100644 --- a/acmed/src/storage.rs +++ b/acmed/src/storage.rs @@ -2,9 +2,8 @@ use crate::certificate::Certificate; use crate::config::HookType; use crate::hooks::{self, FileStorageHookData, HookEnvData}; use acme_common::b64_encode; -use acme_common::crypto::{PrivateKey, PublicKey}; +use acme_common::crypto::{PrivateKey, PublicKey, X509Certificate}; use acme_common::error::Error; -use openssl::x509::X509; use std::collections::HashMap; use std::fmt; use std::fs::{File, OpenOptions}; @@ -212,15 +211,13 @@ pub fn set_priv_key(cert: &Certificate, key: &PrivateKey) -> Result<(), Error> { } pub fn get_pub_key(cert: &Certificate) -> Result { - let raw_key = get_certificate(cert)?.public_key()?.public_key_to_pem()?; - let pub_key = PublicKey::from_pem(&raw_key)?; - Ok(pub_key) + get_certificate(cert)?.public_key() } -pub fn get_certificate(cert: &Certificate) -> Result { +pub fn get_certificate(cert: &Certificate) -> Result { let path = get_file_path(cert, FileType::Certificate)?; let raw_crt = read_file(cert, &path)?; - let crt = X509::from_pem(&raw_crt)?; + let crt = X509Certificate::from_pem(&raw_crt)?; Ok(crt) } diff --git a/tacd/src/certificate.rs b/tacd/src/certificate.rs deleted file mode 100644 index b74b36e..0000000 --- a/tacd/src/certificate.rs +++ /dev/null @@ -1,68 +0,0 @@ -use acme_common::crypto::{gen_keypair, KeyType, PrivateKey, PublicKey}; -use acme_common::error::Error; -use openssl::asn1::Asn1Time; -use openssl::bn::{BigNum, MsbOption}; -use openssl::hash::MessageDigest; -use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName}; -use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509}; - -const X509_VERSION: i32 = 0x02; -const CRT_SERIAL_NB_BITS: i32 = 32; -const CRT_NB_DAYS_VALIDITY: u32 = 7; -const INVALID_EXT_MSG: &str = "Invalid acmeIdentifier extension."; - -fn get_certificate( - domain: &str, - public_key: &PublicKey, - private_key: &PrivateKey, - acme_ext: &str, -) -> Result { - let mut x509_name = X509NameBuilder::new()?; - x509_name.append_entry_by_text("O", crate::APP_ORG)?; - let ca_name = format!("{} TLS-ALPN-01 Authority", crate::APP_NAME); - x509_name.append_entry_by_text("CN", &ca_name)?; - let x509_name = x509_name.build(); - - let mut builder = X509Builder::new()?; - builder.set_version(X509_VERSION)?; - let serial_number = { - let mut serial = BigNum::new()?; - serial.rand(CRT_SERIAL_NB_BITS - 1, MsbOption::MAYBE_ZERO, false)?; - serial.to_asn1_integer()? - }; - builder.set_serial_number(&serial_number)?; - builder.set_subject_name(&x509_name)?; - builder.set_issuer_name(&x509_name)?; - builder.set_pubkey(&public_key.inner_key)?; - let not_before = Asn1Time::days_from_now(0)?; - builder.set_not_before(¬_before)?; - let not_after = Asn1Time::days_from_now(CRT_NB_DAYS_VALIDITY)?; - builder.set_not_after(¬_after)?; - - builder.append_extension(BasicConstraints::new().build()?)?; - let ctx = builder.x509v3_context(None, None); - let san_ext = SubjectAlternativeName::new().dns(domain).build(&ctx)?; - builder.append_extension(san_ext)?; - - let ctx = builder.x509v3_context(None, None); - let mut v: Vec<&str> = acme_ext.split('=').collect(); - let value = v.pop().ok_or_else(|| Error::from(INVALID_EXT_MSG))?; - let acme_ext_name = v.pop().ok_or_else(|| Error::from(INVALID_EXT_MSG))?; - if !v.is_empty() { - return Err(Error::from(INVALID_EXT_MSG)); - } - let acme_ext = X509Extension::new(None, Some(&ctx), &acme_ext_name, &value) - .map_err(|_| Error::from(INVALID_EXT_MSG))?; - builder - .append_extension(acme_ext) - .map_err(|_| Error::from(INVALID_EXT_MSG))?; - builder.sign(&private_key.inner_key, MessageDigest::sha256())?; - let cert = builder.build(); - Ok(cert) -} - -pub fn gen_certificate(domain: &str, acme_ext: &str) -> Result<(PrivateKey, X509), Error> { - let (pub_key, priv_key) = gen_keypair(KeyType::EcdsaP256)?; - let cert = get_certificate(domain, &pub_key, &priv_key, acme_ext)?; - Ok((priv_key, cert)) -} diff --git a/tacd/src/main.rs b/tacd/src/main.rs index faa41be..b06ccb2 100644 --- a/tacd/src/main.rs +++ b/tacd/src/main.rs @@ -1,15 +1,14 @@ +use acme_common::crypto::X509Certificate; use acme_common::error::Error; use clap::{App, Arg, ArgMatches}; use log::{debug, error, info}; use std::fs::File; use std::io::{self, Read}; -mod certificate; mod server; const APP_NAME: &str = env!("CARGO_PKG_NAME"); const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); -const APP_ORG: &str = "ACMEd"; const DEFAULT_PID_FILE: &str = "/var/run/admed.pid"; const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:5001"; const ALPN_ACME_PROTO_NAME: &[u8] = b"\x0aacme-tls/1"; @@ -46,7 +45,7 @@ fn init(cnf: &ArgMatches) -> Result<(), Error> { let domain = get_acme_value(cnf, "domain", "domain-file")?; let ext = get_acme_value(cnf, "acme-ext", "acme-ext-file")?; let listen_addr = cnf.value_of("listen").unwrap_or(DEFAULT_LISTEN_ADDR); - let (pk, cert) = certificate::gen_certificate(&domain, &ext)?; + let (pk, cert) = X509Certificate::from_acme_ext(&domain, &ext)?; info!("Starting {} on {} for {}", APP_NAME, listen_addr, domain); server::start(listen_addr, &cert, &pk)?; Ok(()) diff --git a/tacd/src/server.rs b/tacd/src/server.rs index 174cebe..33d945b 100644 --- a/tacd/src/server.rs +++ b/tacd/src/server.rs @@ -1,8 +1,7 @@ -use acme_common::crypto::PrivateKey; +use acme_common::crypto::{PrivateKey, X509Certificate}; use acme_common::error::Error; use log::debug; use openssl::ssl::{self, AlpnError, SslAcceptor, SslMethod}; -use openssl::x509::X509; use std::net::TcpListener; use std::sync::Arc; use std::thread; @@ -30,14 +29,18 @@ macro_rules! listen_and_accept { }; } -pub fn start(listen_addr: &str, certificate: &X509, private_key: &PrivateKey) -> Result<(), Error> { +pub fn start( + listen_addr: &str, + certificate: &X509Certificate, + private_key: &PrivateKey, +) -> Result<(), Error> { let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; acceptor.set_alpn_select_callback(|_, client| { debug!("ALPN negociation"); ssl::select_next_proto(crate::ALPN_ACME_PROTO_NAME, client).ok_or(ALPN_ERROR) }); acceptor.set_private_key(&private_key.inner_key)?; - acceptor.set_certificate(certificate)?; + acceptor.set_certificate(&certificate.inner_cert)?; acceptor.check_private_key()?; let acceptor = Arc::new(acceptor.build()); if cfg!(unix) && listen_addr.starts_with("unix:") {