proxmox/proxmox-acme-api/src/certificate_helpers.rs
Dietmar Maurer cfc155a06b acme-api: reusable ACME api implementation.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
2024-05-16 12:35:14 +02:00

207 lines
7.0 KiB
Rust

use std::sync::Arc;
use std::time::Duration;
use anyhow::{bail, format_err, Error};
use proxmox_acme::async_client::AcmeClient;
use proxmox_rest_server::WorkerTask;
use proxmox_sys::{task_log, task_warn};
use crate::config::AcmeApiConfig;
use crate::types::{AcmeConfig, AcmeDomain};
pub struct OrderedCertificate {
pub certificate: Vec<u8>,
pub private_key_pem: Vec<u8>,
}
impl AcmeApiConfig {
pub async fn order_certificate(
&self,
worker: Arc<WorkerTask>,
acme_config: AcmeConfig,
domains: Vec<AcmeDomain>,
) -> Result<Option<OrderedCertificate>, 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() {
task_log!(
worker,
"No domains configured to be ordered from an ACME server."
);
return Ok(None);
}
let mut acme = self.load_account_config(&acme_config.account).await?.client();
let (plugins, _) = self.plugin_config()?;
task_log!(worker, "Placing ACME order");
let order = acme
.new_order(domains.iter().map(|d| d.domain.to_ascii_lowercase()))
.await?;
task_log!(worker, "Order URL: {}", order.location);
let identifiers: Vec<String> = order
.data
.identifiers
.iter()
.map(|identifier| match identifier {
Identifier::Dns(domain) => domain.clone(),
})
.collect();
for auth_url in &order.data.authorizations {
task_log!(worker, "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 {
task_log!(worker, "{} is already validated!", domain);
continue;
}
task_log!(worker, "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)
})?;
task_log!(worker, "Setting up validation plugin");
let validation_url = plugin_cfg
.setup(&mut acme, &auth, domain_config, Arc::clone(&worker))
.await?;
let result = Self::request_validation(&worker, &mut acme, auth_url, validation_url).await;
if let Err(err) = plugin_cfg
.teardown(&mut acme, &auth, domain_config, Arc::clone(&worker))
.await
{
task_warn!(
worker,
"Failed to teardown plugin '{}' for domain '{}' - {}",
plugin_id,
domain,
err
);
}
result?;
}
task_log!(worker, "All domains validated");
task_log!(worker, "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 => {
task_log!(worker, "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 => {
task_log!(worker, "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 => {
task_log!(worker, "still processing, trying again in 30 seconds");
tokio::time::sleep(Duration::from_secs(30)).await;
}
Status::Valid => {
task_log!(worker, "valid");
break;
}
other => bail!("order status: {:?}", other),
}
}
task_log!(worker, "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(
worker: &WorkerTask,
acme: &mut AcmeClient,
auth_url: &str,
validation_url: &str,
) -> Result<(), Error> {
task_log!(worker, "Triggering validation");
acme.request_challenge_validation(validation_url).await?;
task_log!(worker, "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 => {
task_log!(
worker,
"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
),
}
}
}
}