use std::mem::MaybeUninit; use std::sync::Arc; use std::time::Duration; use foreign_types::ForeignTypeRef; use anyhow::{bail, format_err, Error}; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; use openssl::x509::{X509Builder, X509}; use proxmox_acme::async_client::AcmeClient; use proxmox_log::{info, warn}; use proxmox_rest_server::WorkerTask; use crate::types::{AcmeConfig, AcmeDomain}; use crate::CertificateInfo; pub async fn revoke_certificate(acme_config: &AcmeConfig, certificate: &[u8]) -> Result<(), Error> { let mut acme = super::account_config::load_account_config(&acme_config.account) .await? .client(); acme.revoke_certificate(certificate, None).await?; Ok(()) } pub struct OrderedCertificate { pub certificate: Vec, pub private_key_pem: Vec, } pub async fn order_certificate( worker: Arc, acme_config: &AcmeConfig, domains: &[AcmeDomain], ) -> Result, Error> { use proxmox_acme::authorization::Status; use proxmox_acme::order::Identifier; let get_domain_config = |domain: &str| { domains .iter() .find(|d| d.domain == domain) .ok_or_else(|| format_err!("no config for domain '{}'", domain)) }; if domains.is_empty() { info!("No domains configured to be ordered from an ACME server."); return Ok(None); } let mut acme = super::account_config::load_account_config(&acme_config.account) .await? .client(); let (plugins, _) = super::plugin_config::plugin_config()?; info!("Placing ACME order"); let order = acme .new_order(domains.iter().map(|d| d.domain.to_ascii_lowercase())) .await?; info!("Order URL: {}", order.location); let identifiers: Vec = order .data .identifiers .iter() .map(|identifier| match identifier { Identifier::Dns(domain) => domain.clone(), }) .collect(); for auth_url in &order.data.authorizations { info!("Getting authorization details from '{}'", auth_url); let mut auth = acme.get_authorization(auth_url).await?; let domain = match &mut auth.identifier { Identifier::Dns(domain) => domain.to_ascii_lowercase(), }; if auth.status == Status::Valid { info!("{} is already validated!", domain); continue; } info!("The validation for {} is pending", domain); let domain_config: &AcmeDomain = get_domain_config(&domain)?; let plugin_id = domain_config.plugin.as_deref().unwrap_or("standalone"); let mut plugin_cfg = crate::acme_plugin::get_acme_plugin(&plugins, plugin_id)?.ok_or_else(|| { format_err!("plugin '{}' for domain '{}' not found!", plugin_id, domain) })?; info!("Setting up validation plugin"); let validation_url = plugin_cfg .setup(&mut acme, &auth, domain_config, Arc::clone(&worker)) .await?; let result = request_validation(&mut acme, auth_url, validation_url).await; if let Err(err) = plugin_cfg .teardown(&mut acme, &auth, domain_config, Arc::clone(&worker)) .await { warn!( "Failed to teardown plugin '{}' for domain '{}' - {}", plugin_id, domain, err ); } result?; } info!("All domains validated"); info!("Creating CSR"); let csr = proxmox_acme::util::Csr::generate(&identifiers, &Default::default())?; let mut finalize_error_cnt = 0u8; let order_url = &order.location; let mut order; loop { use proxmox_acme::order::Status; order = acme.get_order(order_url).await?; match order.status { Status::Pending => { info!("still pending, trying to finalize anyway"); let finalize = order .finalize .as_deref() .ok_or_else(|| format_err!("missing 'finalize' URL in order"))?; if let Err(err) = acme.finalize(finalize, &csr.data).await { if finalize_error_cnt >= 5 { return Err(err); } finalize_error_cnt += 1; } tokio::time::sleep(Duration::from_secs(5)).await; } Status::Ready => { info!("order is ready, finalizing"); let finalize = order .finalize .as_deref() .ok_or_else(|| format_err!("missing 'finalize' URL in order"))?; acme.finalize(finalize, &csr.data).await?; tokio::time::sleep(Duration::from_secs(5)).await; } Status::Processing => { info!("still processing, trying again in 30 seconds"); tokio::time::sleep(Duration::from_secs(30)).await; } Status::Valid => { info!("valid"); break; } other => bail!("order status: {:?}", other), } } info!("Downloading certificate"); let certificate = acme .get_certificate( order .certificate .as_deref() .ok_or_else(|| format_err!("missing certificate url in finalized order"))?, ) .await?; Ok(Some(OrderedCertificate { certificate: certificate.to_vec(), private_key_pem: csr.private_key_pem, })) } async fn request_validation( acme: &mut AcmeClient, auth_url: &str, validation_url: &str, ) -> Result<(), Error> { info!("Triggering validation"); acme.request_challenge_validation(validation_url).await?; info!("Sleeping for 5 seconds"); tokio::time::sleep(Duration::from_secs(5)).await; loop { use proxmox_acme::authorization::Status; let auth = acme.get_authorization(auth_url).await?; match auth.status { Status::Pending => { info!("Status is still 'pending', trying again in 10 seconds"); tokio::time::sleep(Duration::from_secs(10)).await; } Status::Valid => return Ok(()), other => bail!( "validating challenge '{}' failed - status: {:?}", validation_url, other ), } } } pub fn create_self_signed_cert( product_name: &str, nodename: &str, domain: Option<&str>, ) -> Result<(PKey, X509), Error> { let rsa = Rsa::generate(4096).unwrap(); let mut x509 = X509Builder::new()?; x509.set_version(2)?; let today = openssl::asn1::Asn1Time::days_from_now(0)?; x509.set_not_before(&today)?; let expire = openssl::asn1::Asn1Time::days_from_now(365 * 1000)?; x509.set_not_after(&expire)?; let mut fqdn = nodename.to_owned(); if let Some(domain) = domain { fqdn.push('.'); fqdn.push_str(domain); } // we try to generate an unique 'subject' to avoid browser problems //(reused serial numbers, ..) let uuid = proxmox_uuid::Uuid::generate(); let mut subject_name = openssl::x509::X509NameBuilder::new()?; subject_name.append_entry_by_text("O", product_name)?; subject_name.append_entry_by_text("OU", &format!("{:X}", uuid))?; subject_name.append_entry_by_text("CN", &fqdn)?; let subject_name = subject_name.build(); x509.set_subject_name(&subject_name)?; x509.set_issuer_name(&subject_name)?; let bc = openssl::x509::extension::BasicConstraints::new(); // CA = false let bc = bc.build()?; x509.append_extension(bc)?; let usage = openssl::x509::extension::ExtendedKeyUsage::new() .server_auth() .build()?; x509.append_extension(usage)?; let context = x509.x509v3_context(None, None); let mut alt_names = openssl::x509::extension::SubjectAlternativeName::new(); alt_names.ip("127.0.0.1"); alt_names.ip("::1"); alt_names.dns("localhost"); if nodename != "localhost" { alt_names.dns(nodename); } if nodename != fqdn { alt_names.dns(&fqdn); } let alt_names = alt_names.build(&context)?; x509.append_extension(alt_names)?; let pub_pem = rsa.public_key_to_pem()?; let pubkey = PKey::public_key_from_pem(&pub_pem)?; x509.set_pubkey(&pubkey)?; let context = x509.x509v3_context(None, None); let ext = openssl::x509::extension::SubjectKeyIdentifier::new().build(&context)?; x509.append_extension(ext)?; let context = x509.x509v3_context(None, None); let ext = openssl::x509::extension::AuthorityKeyIdentifier::new() .keyid(true) .build(&context)?; x509.append_extension(ext)?; let privkey = PKey::from_rsa(rsa)?; x509.sign(&privkey, openssl::hash::MessageDigest::sha256())?; Ok((privkey, x509.build())) } impl CertificateInfo { pub fn from_pem(filename: &str, cert_pem: &[u8]) -> Result { let x509 = openssl::x509::X509::from_pem(cert_pem)?; let cert_pem = String::from_utf8(cert_pem.to_vec()) .map_err(|_| format_err!("certificate in {:?} is not a valid PEM file", filename))?; let pubkey = x509.public_key()?; let subject = x509name_to_string(x509.subject_name())?; let issuer = x509name_to_string(x509.issuer_name())?; let fingerprint = x509.digest(openssl::hash::MessageDigest::sha256())?; let fingerprint = hex::encode(fingerprint) .as_bytes() .chunks(2) .map(|v| std::str::from_utf8(v).unwrap()) .collect::>() .join(":"); let public_key_type = openssl::nid::Nid::from_raw(pubkey.id().as_raw()) .long_name() .unwrap_or("") .to_owned(); let san = x509 .subject_alt_names() .map(|san| { san.into_iter() .filter_map(|name| { // this is not actually a map and we don't want to break the pattern #[allow(clippy::manual_map)] if let Some(name) = name.dnsname() { Some(format!("DNS: {name}")) } else if let Some(ip) = name.ipaddress() { Some(format!("IP: {ip:?}")) } else if let Some(email) = name.email() { Some(format!("EMAIL: {email}")) } else if let Some(uri) = name.uri() { Some(format!("URI: {uri}")) } else { None } }) .collect() }) .unwrap_or_default(); Ok(CertificateInfo { filename: filename.to_string(), pem: Some(cert_pem), subject, issuer, fingerprint: Some(fingerprint), public_key_bits: Some(pubkey.bits()), notbefore: asn1_time_to_unix(x509.not_before()).ok(), notafter: asn1_time_to_unix(x509.not_after()).ok(), public_key_type, san, }) } /// Check if the certificate is expired at or after a specific unix epoch. pub fn is_expired_after_epoch(&self, epoch: i64) -> Result { if let Some(notafter) = self.notafter { Ok(notafter < epoch) } else { Ok(false) } } } fn x509name_to_string(name: &openssl::x509::X509NameRef) -> Result { let mut parts = Vec::new(); for entry in name.entries() { parts.push(format!( "{} = {}", entry.object().nid().short_name()?, entry.data().as_utf8()? )); } Ok(parts.join(", ")) } // C type: #[allow(non_camel_case_types)] type ASN1_TIME = ::CType; unsafe extern "C" { fn ASN1_TIME_to_tm(s: *const ASN1_TIME, tm: *mut libc::tm) -> libc::c_int; } fn asn1_time_to_unix(time: &openssl::asn1::Asn1TimeRef) -> Result { let mut c_tm = MaybeUninit::::uninit(); let rc = unsafe { ASN1_TIME_to_tm(time.as_ptr(), c_tm.as_mut_ptr()) }; if rc != 1 { bail!("failed to parse ASN1 time"); } let mut c_tm = unsafe { c_tm.assume_init() }; proxmox_time::timegm(&mut c_tm) }