[pbs-devel] [PATCH proxmox-backup 4/4] acme: certificate ordering through proxmox-acme-api

Samuel Rufinatscha s.rufinatscha at proxmox.com
Tue Dec 2 16:56:55 CET 2025


PBS currently uses its own ACME client and API logic, while PDM uses the
factored out proxmox-acme and proxmox-acme-api crates. This duplication
risks differences in behaviour and requires ACME maintenance in two
places. This patch is part of a series to move PBS over to the shared
ACME stack.

Changes:
- Replace the custom ACME order/authorization loop in node certificates
with a call to proxmox_acme_api::order_certificate.
- Build domain + config data as proxmox-acme-api types
- Remove obsolete local ACME ordering and plugin glue code.

Signed-off-by: Samuel Rufinatscha <s.rufinatscha at proxmox.com>
---
 src/acme/mod.rs               |   2 -
 src/acme/plugin.rs            | 336 ----------------------------------
 src/api2/node/certificates.rs | 240 ++++--------------------
 src/api2/types/acme.rs        |  74 --------
 src/api2/types/mod.rs         |   3 -
 src/config/acme/mod.rs        |   7 +-
 src/config/acme/plugin.rs     |  99 +---------
 src/config/node.rs            |  22 +--
 src/lib.rs                    |   2 -
 9 files changed, 46 insertions(+), 739 deletions(-)
 delete mode 100644 src/acme/mod.rs
 delete mode 100644 src/acme/plugin.rs
 delete mode 100644 src/api2/types/acme.rs

diff --git a/src/acme/mod.rs b/src/acme/mod.rs
deleted file mode 100644
index cc561f9a..00000000
--- a/src/acme/mod.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub(crate) mod plugin;
-pub(crate) use plugin::get_acme_plugin;
diff --git a/src/acme/plugin.rs b/src/acme/plugin.rs
deleted file mode 100644
index 5bc09e1f..00000000
--- a/src/acme/plugin.rs
+++ /dev/null
@@ -1,336 +0,0 @@
-use std::future::Future;
-use std::net::{IpAddr, SocketAddr};
-use std::pin::Pin;
-use std::process::Stdio;
-use std::sync::Arc;
-use std::time::Duration;
-
-use anyhow::{bail, format_err, Error};
-use bytes::Bytes;
-use futures::TryFutureExt;
-use http_body_util::Full;
-use hyper::body::Incoming;
-use hyper::server::conn::http1;
-use hyper::service::service_fn;
-use hyper::{Request, Response};
-use hyper_util::rt::TokioIo;
-use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWriteExt, BufReader};
-use tokio::net::TcpListener;
-use tokio::process::Command;
-
-use proxmox_acme::{Authorization, Challenge};
-
-use crate::api2::types::AcmeDomain;
-use proxmox_acme::async_client::AcmeClient;
-use proxmox_rest_server::WorkerTask;
-
-use crate::config::acme::plugin::{DnsPlugin, PluginData};
-
-const PROXMOX_ACME_SH_PATH: &str = "/usr/share/proxmox-acme/proxmox-acme";
-
-pub(crate) fn get_acme_plugin(
-    plugin_data: &PluginData,
-    name: &str,
-) -> Result<Option<Box<dyn AcmePlugin + Send + Sync + 'static>>, Error> {
-    let (ty, data) = match plugin_data.get(name) {
-        Some(plugin) => plugin,
-        None => return Ok(None),
-    };
-
-    Ok(Some(match ty.as_str() {
-        "dns" => {
-            let plugin: DnsPlugin = serde::Deserialize::deserialize(data)?;
-            Box::new(plugin)
-        }
-        "standalone" => {
-            // this one has no config
-            Box::<StandaloneServer>::default()
-        }
-        other => bail!("missing implementation for plugin type '{}'", other),
-    }))
-}
-
-pub(crate) trait AcmePlugin {
-    /// Setup everything required to trigger the validation and return the corresponding validation
-    /// URL.
-    fn setup<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
-        &'a mut self,
-        client: &'b mut AcmeClient,
-        authorization: &'c Authorization,
-        domain: &'d AcmeDomain,
-        task: Arc<WorkerTask>,
-    ) -> Pin<Box<dyn Future<Output = Result<&'c str, Error>> + Send + 'fut>>;
-
-    fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
-        &'a mut self,
-        client: &'b mut AcmeClient,
-        authorization: &'c Authorization,
-        domain: &'d AcmeDomain,
-        task: Arc<WorkerTask>,
-    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'fut>>;
-}
-
-fn extract_challenge<'a>(
-    authorization: &'a Authorization,
-    ty: &str,
-) -> Result<&'a Challenge, Error> {
-    authorization
-        .challenges
-        .iter()
-        .find(|ch| ch.ty == ty)
-        .ok_or_else(|| format_err!("no supported challenge type ({}) found", ty))
-}
-
-async fn pipe_to_tasklog<T: AsyncRead + Unpin>(
-    pipe: T,
-    task: Arc<WorkerTask>,
-) -> Result<(), std::io::Error> {
-    let mut pipe = BufReader::new(pipe);
-    let mut line = String::new();
-    loop {
-        line.clear();
-        match pipe.read_line(&mut line).await {
-            Ok(0) => return Ok(()),
-            Ok(_) => task.log_message(line.as_str()),
-            Err(err) => return Err(err),
-        }
-    }
-}
-
-impl DnsPlugin {
-    async fn action<'a>(
-        &self,
-        client: &mut AcmeClient,
-        authorization: &'a Authorization,
-        domain: &AcmeDomain,
-        task: Arc<WorkerTask>,
-        action: &str,
-    ) -> Result<&'a str, Error> {
-        let challenge = extract_challenge(authorization, "dns-01")?;
-        let mut stdin_data = client
-            .dns_01_txt_value(
-                challenge
-                    .token()
-                    .ok_or_else(|| format_err!("missing token in challenge"))?,
-            )?
-            .into_bytes();
-        stdin_data.push(b'\n');
-        stdin_data.extend(self.data.as_bytes());
-        if stdin_data.last() != Some(&b'\n') {
-            stdin_data.push(b'\n');
-        }
-
-        let mut command = Command::new("/usr/bin/setpriv");
-
-        #[rustfmt::skip]
-        command.args([
-            "--reuid", "nobody",
-            "--regid", "nogroup",
-            "--clear-groups",
-            "--reset-env",
-            "--",
-            "/bin/bash",
-                PROXMOX_ACME_SH_PATH,
-                action,
-                &self.core.api,
-                domain.alias.as_deref().unwrap_or(&domain.domain),
-        ]);
-
-        // We could use 1 socketpair, but tokio wraps them all in `File` internally causing `close`
-        // to be called separately on all of them without exception, so we need 3 pipes :-(
-
-        let mut child = command
-            .stdin(Stdio::piped())
-            .stdout(Stdio::piped())
-            .stderr(Stdio::piped())
-            .spawn()?;
-
-        let mut stdin = child.stdin.take().expect("Stdio::piped()");
-        let stdout = child.stdout.take().expect("Stdio::piped() failed?");
-        let stdout = pipe_to_tasklog(stdout, Arc::clone(&task));
-        let stderr = child.stderr.take().expect("Stdio::piped() failed?");
-        let stderr = pipe_to_tasklog(stderr, Arc::clone(&task));
-        let stdin = async move {
-            stdin.write_all(&stdin_data).await?;
-            stdin.flush().await?;
-            Ok::<_, std::io::Error>(())
-        };
-        match futures::try_join!(stdin, stdout, stderr) {
-            Ok(((), (), ())) => (),
-            Err(err) => {
-                if let Err(err) = child.kill().await {
-                    task.log_message(format!(
-                        "failed to kill '{PROXMOX_ACME_SH_PATH} {action}' command: {err}"
-                    ));
-                }
-                bail!("'{}' failed: {}", PROXMOX_ACME_SH_PATH, err);
-            }
-        }
-
-        let status = child.wait().await?;
-        if !status.success() {
-            bail!(
-                "'{} {}' exited with error ({})",
-                PROXMOX_ACME_SH_PATH,
-                action,
-                status.code().unwrap_or(-1)
-            );
-        }
-
-        Ok(&challenge.url)
-    }
-}
-
-impl AcmePlugin for DnsPlugin {
-    fn setup<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
-        &'a mut self,
-        client: &'b mut AcmeClient,
-        authorization: &'c Authorization,
-        domain: &'d AcmeDomain,
-        task: Arc<WorkerTask>,
-    ) -> Pin<Box<dyn Future<Output = Result<&'c str, Error>> + Send + 'fut>> {
-        Box::pin(async move {
-            let result = self
-                .action(client, authorization, domain, task.clone(), "setup")
-                .await;
-
-            let validation_delay = self.core.validation_delay.unwrap_or(30) as u64;
-            if validation_delay > 0 {
-                task.log_message(format!(
-                    "Sleeping {validation_delay} seconds to wait for TXT record propagation"
-                ));
-                tokio::time::sleep(Duration::from_secs(validation_delay)).await;
-            }
-            result
-        })
-    }
-
-    fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
-        &'a mut self,
-        client: &'b mut AcmeClient,
-        authorization: &'c Authorization,
-        domain: &'d AcmeDomain,
-        task: Arc<WorkerTask>,
-    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'fut>> {
-        Box::pin(async move {
-            self.action(client, authorization, domain, task, "teardown")
-                .await
-                .map(drop)
-        })
-    }
-}
-
-#[derive(Default)]
-struct StandaloneServer {
-    abort_handle: Option<futures::future::AbortHandle>,
-}
-
-// In case the "order_certificates" future gets dropped between setup & teardown, let's also cancel
-// the HTTP listener on Drop:
-impl Drop for StandaloneServer {
-    fn drop(&mut self) {
-        self.stop();
-    }
-}
-
-impl StandaloneServer {
-    fn stop(&mut self) {
-        if let Some(abort) = self.abort_handle.take() {
-            abort.abort();
-        }
-    }
-}
-
-async fn standalone_respond(
-    req: Request<Incoming>,
-    path: Arc<String>,
-    key_auth: Arc<String>,
-) -> Result<Response<Full<Bytes>>, hyper::Error> {
-    if req.method() == hyper::Method::GET && req.uri().path() == path.as_str() {
-        Ok(Response::builder()
-            .status(hyper::http::StatusCode::OK)
-            .body(key_auth.as_bytes().to_vec().into())
-            .unwrap())
-    } else {
-        Ok(Response::builder()
-            .status(hyper::http::StatusCode::NOT_FOUND)
-            .body("Not found.".into())
-            .unwrap())
-    }
-}
-
-impl AcmePlugin for StandaloneServer {
-    fn setup<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
-        &'a mut self,
-        client: &'b mut AcmeClient,
-        authorization: &'c Authorization,
-        _domain: &'d AcmeDomain,
-        _task: Arc<WorkerTask>,
-    ) -> Pin<Box<dyn Future<Output = Result<&'c str, Error>> + Send + 'fut>> {
-        Box::pin(async move {
-            self.stop();
-
-            let challenge = extract_challenge(authorization, "http-01")?;
-            let token = challenge
-                .token()
-                .ok_or_else(|| format_err!("missing token in challenge"))?;
-            let key_auth = Arc::new(client.key_authorization(token)?);
-            let path = Arc::new(format!("/.well-known/acme-challenge/{token}"));
-
-            // `[::]:80` first, then `*:80`
-            let dual = SocketAddr::new(IpAddr::from([0u16; 8]), 80);
-            let ipv4 = SocketAddr::new(IpAddr::from([0u8; 4]), 80);
-            let incoming = TcpListener::bind(dual)
-                .or_else(|_| TcpListener::bind(ipv4))
-                .await?;
-
-            let server = async move {
-                loop {
-                    let key_auth = Arc::clone(&key_auth);
-                    let path = Arc::clone(&path);
-                    match incoming.accept().await {
-                        Ok((tcp, _)) => {
-                            let io = TokioIo::new(tcp);
-                            let service = service_fn(move |request| {
-                                standalone_respond(
-                                    request,
-                                    Arc::clone(&path),
-                                    Arc::clone(&key_auth),
-                                )
-                            });
-
-                            tokio::task::spawn(async move {
-                                if let Err(err) =
-                                    http1::Builder::new().serve_connection(io, service).await
-                                {
-                                    println!("Error serving connection: {err:?}");
-                                }
-                            });
-                        }
-                        Err(err) => println!("Error accepting connection: {err:?}"),
-                    }
-                }
-            };
-            let (future, abort) = futures::future::abortable(server);
-            self.abort_handle = Some(abort);
-            tokio::spawn(future);
-
-            Ok(challenge.url.as_str())
-        })
-    }
-
-    fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
-        &'a mut self,
-        _client: &'b mut AcmeClient,
-        _authorization: &'c Authorization,
-        _domain: &'d AcmeDomain,
-        _task: Arc<WorkerTask>,
-    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'fut>> {
-        Box::pin(async move {
-            if let Some(abort) = self.abort_handle.take() {
-                abort.abort();
-            }
-            Ok(())
-        })
-    }
-}
diff --git a/src/api2/node/certificates.rs b/src/api2/node/certificates.rs
index 31196715..2a645b4a 100644
--- a/src/api2/node/certificates.rs
+++ b/src/api2/node/certificates.rs
@@ -1,27 +1,19 @@
-use std::sync::Arc;
-use std::time::Duration;
-
 use anyhow::{bail, format_err, Error};
 use openssl::pkey::PKey;
 use openssl::x509::X509;
 use serde::{Deserialize, Serialize};
 use tracing::info;
 
-use proxmox_router::list_subdirs_api_method;
-use proxmox_router::SubdirMap;
-use proxmox_router::{Permission, Router, RpcEnvironment};
-use proxmox_schema::api;
-
+use crate::server::send_certificate_renewal_mail;
 use pbs_api_types::{NODE_SCHEMA, PRIV_SYS_MODIFY};
 use pbs_buildcfg::configdir;
 use pbs_tools::cert;
-use tracing::warn;
-
-use crate::api2::types::AcmeDomain;
-use crate::config::node::NodeConfig;
-use crate::server::send_certificate_renewal_mail;
-use proxmox_acme::async_client::AcmeClient;
+use proxmox_acme_api::AcmeDomain;
 use proxmox_rest_server::WorkerTask;
+use proxmox_router::list_subdirs_api_method;
+use proxmox_router::SubdirMap;
+use proxmox_router::{Permission, Router, RpcEnvironment};
+use proxmox_schema::api;
 
 pub const ROUTER: Router = Router::new()
     .get(&list_subdirs_api_method!(SUBDIRS))
@@ -269,193 +261,6 @@ pub async fn delete_custom_certificate() -> Result<(), Error> {
     Ok(())
 }
 
-struct OrderedCertificate {
-    certificate: hyper::body::Bytes,
-    private_key_pem: Vec<u8>,
-}
-
-async fn order_certificate(
-    worker: Arc<WorkerTask>,
-    node_config: &NodeConfig,
-) -> Result<Option<OrderedCertificate>, Error> {
-    use proxmox_acme::authorization::Status;
-    use proxmox_acme::order::Identifier;
-
-    let domains = node_config.acme_domains().try_fold(
-        Vec::<AcmeDomain>::new(),
-        |mut acc, domain| -> Result<_, Error> {
-            let mut domain = domain?;
-            domain.domain.make_ascii_lowercase();
-            if let Some(alias) = &mut domain.alias {
-                alias.make_ascii_lowercase();
-            }
-            acc.push(domain);
-            Ok(acc)
-        },
-    )?;
-
-    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 (plugins, _) = crate::config::acme::plugin::config()?;
-
-    let mut acme = node_config.acme_client().await?;
-
-    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<String> = 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!("{domain} is already validated!");
-            continue;
-        }
-
-        info!("The validation for {domain} is pending");
-        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::get_acme_plugin(&plugins, plugin_id)?
-            .ok_or_else(|| format_err!("plugin '{plugin_id}' for domain '{domain}' not found!"))?;
-
-        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 '{plugin_id}' for domain '{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,
-        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
-            ),
-        }
-    }
-}
-
 #[api(
     input: {
         properties: {
@@ -525,9 +330,30 @@ fn spawn_certificate_worker(
 
     let auth_id = rpcenv.get_auth_id().unwrap();
 
+    let acme_config = if let Some(cfg) = node_config.acme_config().transpose()? {
+        cfg
+    } else {
+        proxmox_acme_api::parse_acme_config_string("account=default")?
+    };
+
+    let domains = node_config.acme_domains().try_fold(
+        Vec::<AcmeDomain>::new(),
+        |mut acc, domain| -> Result<_, Error> {
+            let mut domain = domain?;
+            domain.domain.make_ascii_lowercase();
+            if let Some(alias) = &mut domain.alias {
+                alias.make_ascii_lowercase();
+            }
+            acc.push(domain);
+            Ok(acc)
+        },
+    )?;
+
     WorkerTask::spawn(name, None, auth_id, true, move |worker| async move {
         let work = || async {
-            if let Some(cert) = order_certificate(worker, &node_config).await? {
+            if let Some(cert) =
+                proxmox_acme_api::order_certificate(worker, &acme_config, &domains).await?
+            {
                 crate::config::set_proxy_certificate(&cert.certificate, &cert.private_key_pem)?;
                 crate::server::reload_proxy_certificate().await?;
             }
@@ -563,16 +389,20 @@ pub fn revoke_acme_cert(rpcenv: &mut dyn RpcEnvironment) -> Result<String, Error
 
     let auth_id = rpcenv.get_auth_id().unwrap();
 
+    let acme_config = if let Some(cfg) = node_config.acme_config().transpose()? {
+        cfg
+    } else {
+        proxmox_acme_api::parse_acme_config_string("account=default")?
+    };
+
     WorkerTask::spawn(
         "acme-revoke-cert",
         None,
         auth_id,
         true,
         move |_worker| async move {
-            info!("Loading ACME account");
-            let mut acme = node_config.acme_client().await?;
             info!("Revoking old certificate");
-            acme.revoke_certificate(cert_pem.as_bytes(), None).await?;
+            proxmox_acme_api::revoke_certificate(&acme_config, &cert_pem.as_bytes()).await?;
             info!("Deleting certificate and regenerating a self-signed one");
             delete_custom_certificate().await?;
             Ok(())
diff --git a/src/api2/types/acme.rs b/src/api2/types/acme.rs
deleted file mode 100644
index 2905b41b..00000000
--- a/src/api2/types/acme.rs
+++ /dev/null
@@ -1,74 +0,0 @@
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
-
-use proxmox_schema::{api, ApiStringFormat, ApiType, Schema, StringSchema};
-
-use pbs_api_types::{DNS_ALIAS_FORMAT, DNS_NAME_FORMAT, PROXMOX_SAFE_ID_FORMAT};
-
-#[api(
-    properties: {
-        "domain": { format: &DNS_NAME_FORMAT },
-        "alias": {
-            optional: true,
-            format: &DNS_ALIAS_FORMAT,
-        },
-        "plugin": {
-            optional: true,
-            format: &PROXMOX_SAFE_ID_FORMAT,
-        },
-    },
-    default_key: "domain",
-)]
-#[derive(Deserialize, Serialize)]
-/// A domain entry for an ACME certificate.
-pub struct AcmeDomain {
-    /// The domain to certify for.
-    pub domain: String,
-
-    /// The domain to use for challenges instead of the default acme challenge domain.
-    ///
-    /// This is useful if you use CNAME entries to redirect `_acme-challenge.*` domains to a
-    /// different DNS server.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub alias: Option<String>,
-
-    /// The plugin to use to validate this domain.
-    ///
-    /// Empty means standalone HTTP validation is used.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub plugin: Option<String>,
-}
-
-pub const ACME_DOMAIN_PROPERTY_SCHEMA: Schema =
-    StringSchema::new("ACME domain configuration string")
-        .format(&ApiStringFormat::PropertyString(&AcmeDomain::API_SCHEMA))
-        .schema();
-
-#[api(
-    properties: {
-        schema: {
-            type: Object,
-            additional_properties: true,
-            properties: {},
-        },
-        type: {
-            type: String,
-        },
-    },
-)]
-#[derive(Serialize)]
-/// Schema for an ACME challenge plugin.
-pub struct AcmeChallengeSchema {
-    /// Plugin ID.
-    pub id: String,
-
-    /// Human readable name, falls back to id.
-    pub name: String,
-
-    /// Plugin Type.
-    #[serde(rename = "type")]
-    pub ty: &'static str,
-
-    /// The plugin's parameter schema.
-    pub schema: Value,
-}
diff --git a/src/api2/types/mod.rs b/src/api2/types/mod.rs
index afc34b30..34193685 100644
--- a/src/api2/types/mod.rs
+++ b/src/api2/types/mod.rs
@@ -4,9 +4,6 @@ use anyhow::bail;
 
 use proxmox_schema::*;
 
-mod acme;
-pub use acme::*;
-
 // File names: may not contain slashes, may not start with "."
 pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| {
     if name.starts_with('.') {
diff --git a/src/config/acme/mod.rs b/src/config/acme/mod.rs
index 35cda50b..afd7abf8 100644
--- a/src/config/acme/mod.rs
+++ b/src/config/acme/mod.rs
@@ -9,8 +9,7 @@ use proxmox_sys::fs::{file_read_string, CreateOptions};
 
 use pbs_api_types::PROXMOX_SAFE_ID_REGEX;
 
-use crate::api2::types::AcmeChallengeSchema;
-use proxmox_acme_api::{AcmeAccountName, KnownAcmeDirectory, KNOWN_ACME_DIRECTORIES};
+use proxmox_acme_api::{AcmeAccountName, AcmeChallengeSchema};
 
 pub(crate) const ACME_DIR: &str = pbs_buildcfg::configdir!("/acme");
 pub(crate) const ACME_ACCOUNT_DIR: &str = pbs_buildcfg::configdir!("/acme/accounts");
@@ -35,8 +34,6 @@ pub(crate) fn make_acme_dir() -> Result<(), Error> {
     create_acme_subdir(ACME_DIR)
 }
 
-pub const DEFAULT_ACME_DIRECTORY_ENTRY: &KnownAcmeDirectory = &KNOWN_ACME_DIRECTORIES[0];
-
 pub fn foreach_acme_account<F>(mut func: F) -> Result<(), Error>
 where
     F: FnMut(AcmeAccountName) -> ControlFlow<Result<(), Error>>,
@@ -80,7 +77,7 @@ pub fn load_dns_challenge_schema() -> Result<Vec<AcmeChallengeSchema>, Error> {
                 .and_then(Value::as_str)
                 .unwrap_or(id)
                 .to_owned(),
-            ty: "dns",
+            ty: "dns".into(),
             schema: schema.to_owned(),
         })
         .collect())
diff --git a/src/config/acme/plugin.rs b/src/config/acme/plugin.rs
index 18e71199..2e979ffe 100644
--- a/src/config/acme/plugin.rs
+++ b/src/config/acme/plugin.rs
@@ -1,104 +1,15 @@
 use std::sync::LazyLock;
 
 use anyhow::Error;
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
-
-use proxmox_schema::{api, ApiType, Schema, StringSchema, Updater};
-use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
-
-use pbs_api_types::PROXMOX_SAFE_ID_FORMAT;
 use pbs_config::{open_backup_lockfile, BackupLockGuard};
-
-pub const PLUGIN_ID_SCHEMA: Schema = StringSchema::new("ACME Challenge Plugin ID.")
-    .format(&PROXMOX_SAFE_ID_FORMAT)
-    .min_length(1)
-    .max_length(32)
-    .schema();
+use proxmox_acme_api::PLUGIN_ID_SCHEMA;
+use proxmox_acme_api::{DnsPlugin, StandalonePlugin};
+use proxmox_schema::{ApiType, Schema};
+use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
+use serde_json::Value;
 
 pub static CONFIG: LazyLock<SectionConfig> = LazyLock::new(init);
 
-#[api(
-    properties: {
-        id: { schema: PLUGIN_ID_SCHEMA },
-    },
-)]
-#[derive(Deserialize, Serialize)]
-/// Standalone ACME Plugin for the http-1 challenge.
-pub struct StandalonePlugin {
-    /// Plugin ID.
-    id: String,
-}
-
-impl Default for StandalonePlugin {
-    fn default() -> Self {
-        Self {
-            id: "standalone".to_string(),
-        }
-    }
-}
-
-#[api(
-    properties: {
-        id: { schema: PLUGIN_ID_SCHEMA },
-        disable: {
-            optional: true,
-            default: false,
-        },
-        "validation-delay": {
-            default: 30,
-            optional: true,
-            minimum: 0,
-            maximum: 2 * 24 * 60 * 60,
-        },
-    },
-)]
-/// DNS ACME Challenge Plugin core data.
-#[derive(Deserialize, Serialize, Updater)]
-#[serde(rename_all = "kebab-case")]
-pub struct DnsPluginCore {
-    /// Plugin ID.
-    #[updater(skip)]
-    pub id: String,
-
-    /// DNS API Plugin Id.
-    pub api: String,
-
-    /// Extra delay in seconds to wait before requesting validation.
-    ///
-    /// Allows to cope with long TTL of DNS records.
-    #[serde(skip_serializing_if = "Option::is_none", default)]
-    pub validation_delay: Option<u32>,
-
-    /// Flag to disable the config.
-    #[serde(skip_serializing_if = "Option::is_none", default)]
-    pub disable: Option<bool>,
-}
-
-#[api(
-    properties: {
-        core: { type: DnsPluginCore },
-    },
-)]
-/// DNS ACME Challenge Plugin.
-#[derive(Deserialize, Serialize)]
-#[serde(rename_all = "kebab-case")]
-pub struct DnsPlugin {
-    #[serde(flatten)]
-    pub core: DnsPluginCore,
-
-    // We handle this property separately in the API calls.
-    /// DNS plugin data (base64url encoded without padding).
-    #[serde(with = "proxmox_serde::string_as_base64url_nopad")]
-    pub data: String,
-}
-
-impl DnsPlugin {
-    pub fn decode_data(&self, output: &mut Vec<u8>) -> Result<(), Error> {
-        Ok(proxmox_base64::url::decode_to_vec(&self.data, output)?)
-    }
-}
-
 fn init() -> SectionConfig {
     let mut config = SectionConfig::new(&PLUGIN_ID_SCHEMA);
 
diff --git a/src/config/node.rs b/src/config/node.rs
index d2a17a49..b9257adf 100644
--- a/src/config/node.rs
+++ b/src/config/node.rs
@@ -6,17 +6,17 @@ use serde::{Deserialize, Serialize};
 
 use proxmox_schema::{api, ApiStringFormat, ApiType, Updater};
 
-use proxmox_http::ProxyConfig;
-
 use pbs_api_types::{
     EMAIL_SCHEMA, MULTI_LINE_COMMENT_SCHEMA, OPENSSL_CIPHERS_TLS_1_2_SCHEMA,
     OPENSSL_CIPHERS_TLS_1_3_SCHEMA,
 };
+use proxmox_acme_api::{AcmeConfig, AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA};
+use proxmox_http::ProxyConfig;
 
 use pbs_buildcfg::configdir;
 use pbs_config::{open_backup_lockfile, BackupLockGuard};
 
-use crate::api2::types::{AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA, HTTP_PROXY_SCHEMA};
+use crate::api2::types::HTTP_PROXY_SCHEMA;
 use proxmox_acme::async_client::AcmeClient;
 use proxmox_acme_api::AcmeAccountName;
 
@@ -45,20 +45,6 @@ pub fn save_config(config: &NodeConfig) -> Result<(), Error> {
     pbs_config::replace_backup_config(CONF_FILE, &raw)
 }
 
-#[api(
-    properties: {
-        account: { type: AcmeAccountName },
-    }
-)]
-#[derive(Deserialize, Serialize)]
-/// The ACME configuration.
-///
-/// Currently only contains the name of the account use.
-pub struct AcmeConfig {
-    /// Account to use to acquire ACME certificates.
-    account: AcmeAccountName,
-}
-
 /// All available languages in Proxmox. Taken from proxmox-i18n repository.
 /// pt_BR, zh_CN, and zh_TW use the same case in the translation files.
 // TODO: auto-generate from available translations
@@ -244,7 +230,7 @@ impl NodeConfig {
 
     pub async fn acme_client(&self) -> Result<AcmeClient, Error> {
         let account = if let Some(cfg) = self.acme_config().transpose()? {
-            cfg.account
+            AcmeAccountName::from_string(cfg.account)?
         } else {
             AcmeAccountName::from_string("default".to_string())? // should really not happen
         };
diff --git a/src/lib.rs b/src/lib.rs
index 8633378c..828f5842 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -27,8 +27,6 @@ pub(crate) mod auth;
 
 pub mod tape;
 
-pub mod acme;
-
 pub mod client_helpers;
 
 pub mod traffic_control_cache;
-- 
2.47.3





More information about the pbs-devel mailing list