From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id A8CEE1FF17C for ; Tue, 2 Dec 2025 16:57:28 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id CD95016C61; Tue, 2 Dec 2025 16:57:46 +0100 (CET) From: Samuel Rufinatscha To: pbs-devel@lists.proxmox.com Date: Tue, 2 Dec 2025 16:56:53 +0100 Message-ID: <20251202155659.379848-3-s.rufinatscha@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251202155659.379848-1-s.rufinatscha@proxmox.com> References: <20251202155659.379848-1-s.rufinatscha@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1764690986412 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.310 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record TVD_PH_BODY_ACCOUNTS_PRE 0.001 The body matches phrases such as "accounts suspended", "account credited", "account verification" Subject: [pbs-devel] [PATCH proxmox-backup 2/4] acme: drop local AcmeClient X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Backup Server development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pbs-devel-bounces@lists.proxmox.com Sender: "pbs-devel" 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: - Remove the local src/acme/client.rs and switch to proxmox_acme::async_client::AcmeClient where needed. - Use proxmox_acme_api::load_client_with_account to the custom AcmeClient::load() function - Replace the local do_register() logic with proxmox_acme_api::register_account, to further ensure accounts are persisted - Replace the local AcmeAccountName type, required for proxmox_acme_api::register_account Signed-off-by: Samuel Rufinatscha --- src/acme/client.rs | 691 ------------------------- src/acme/mod.rs | 3 - src/acme/plugin.rs | 2 +- src/api2/config/acme.rs | 50 +- src/api2/node/certificates.rs | 2 +- src/api2/types/acme.rs | 8 - src/bin/proxmox_backup_manager/acme.rs | 17 +- src/config/acme/mod.rs | 8 +- src/config/node.rs | 9 +- 9 files changed, 36 insertions(+), 754 deletions(-) delete mode 100644 src/acme/client.rs diff --git a/src/acme/client.rs b/src/acme/client.rs deleted file mode 100644 index 9fb6ad55..00000000 --- a/src/acme/client.rs +++ /dev/null @@ -1,691 +0,0 @@ -//! HTTP Client for the ACME protocol. - -use std::fs::OpenOptions; -use std::io; -use std::os::unix::fs::OpenOptionsExt; - -use anyhow::{bail, format_err}; -use bytes::Bytes; -use http_body_util::BodyExt; -use hyper::Request; -use nix::sys::stat::Mode; -use proxmox_http::Body; -use serde::{Deserialize, Serialize}; - -use proxmox_acme::account::AccountCreator; -use proxmox_acme::order::{Order, OrderData}; -use proxmox_acme::types::AccountData as AcmeAccountData; -use proxmox_acme::Request as AcmeRequest; -use proxmox_acme::{Account, Authorization, Challenge, Directory, Error, ErrorResponse}; -use proxmox_http::client::Client; -use proxmox_sys::fs::{replace_file, CreateOptions}; - -use crate::api2::types::AcmeAccountName; -use crate::config::acme::account_path; -use crate::tools::pbs_simple_http; - -/// Our on-disk format inherited from PVE's proxmox-acme code. -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AccountData { - /// The account's location URL. - location: String, - - /// The account data. - account: AcmeAccountData, - - /// The private key as PEM formatted string. - key: String, - - /// ToS URL the user agreed to. - #[serde(skip_serializing_if = "Option::is_none")] - tos: Option, - - #[serde(skip_serializing_if = "is_false", default)] - debug: bool, - - /// The directory's URL. - directory_url: String, -} - -#[inline] -fn is_false(b: &bool) -> bool { - !*b -} - -pub struct AcmeClient { - directory_url: String, - debug: bool, - account_path: Option, - tos: Option, - account: Option, - directory: Option, - nonce: Option, - http_client: Client, -} - -impl AcmeClient { - /// Create a new ACME client for a given ACME directory URL. - pub fn new(directory_url: String) -> Self { - Self { - directory_url, - debug: false, - account_path: None, - tos: None, - account: None, - directory: None, - nonce: None, - http_client: pbs_simple_http(None), - } - } - - /// Load an existing ACME account by name. - pub async fn load(account_name: &AcmeAccountName) -> Result { - let account_path = account_path(account_name.as_ref()); - let data = match tokio::fs::read(&account_path).await { - Ok(data) => data, - Err(err) if err.kind() == io::ErrorKind::NotFound => { - bail!("acme account '{}' does not exist", account_name) - } - Err(err) => bail!( - "failed to load acme account from '{}' - {}", - account_path, - err - ), - }; - let data: AccountData = serde_json::from_slice(&data).map_err(|err| { - format_err!( - "failed to parse acme account from '{}' - {}", - account_path, - err - ) - })?; - - let account = Account::from_parts(data.location, data.key, data.account); - - let mut me = Self::new(data.directory_url); - me.debug = data.debug; - me.account_path = Some(account_path); - me.tos = data.tos; - me.account = Some(account); - - Ok(me) - } - - pub async fn new_account<'a>( - &'a mut self, - account_name: &AcmeAccountName, - tos_agreed: bool, - contact: Vec, - rsa_bits: Option, - eab_creds: Option<(String, String)>, - ) -> Result<&'a Account, anyhow::Error> { - self.tos = if tos_agreed { - self.terms_of_service_url().await?.map(str::to_owned) - } else { - None - }; - - let mut account = Account::creator() - .set_contacts(contact) - .agree_to_tos(tos_agreed); - - if let Some((eab_kid, eab_hmac_key)) = eab_creds { - account = account.set_eab_credentials(eab_kid, eab_hmac_key)?; - } - - let account = if let Some(bits) = rsa_bits { - account.generate_rsa_key(bits)? - } else { - account.generate_ec_key()? - }; - - let _ = self.register_account(account).await?; - - crate::config::acme::make_acme_account_dir()?; - let account_path = account_path(account_name.as_ref()); - let file = OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(&account_path) - .map_err(|err| format_err!("failed to open {:?} for writing: {}", account_path, err))?; - self.write_to(file).map_err(|err| { - format_err!( - "failed to write acme account to {:?}: {}", - account_path, - err - ) - })?; - self.account_path = Some(account_path); - - // unwrap: Setting `self.account` is literally this function's job, we just can't keep - // the borrow from from `self.register_account()` active due to clashes. - Ok(self.account.as_ref().unwrap()) - } - - fn save(&self) -> Result<(), anyhow::Error> { - let mut data = Vec::::new(); - self.write_to(&mut data)?; - let account_path = self.account_path.as_ref().ok_or_else(|| { - format_err!("no account path set, cannot save updated account information") - })?; - crate::config::acme::make_acme_account_dir()?; - replace_file( - account_path, - &data, - CreateOptions::new() - .perm(Mode::from_bits_truncate(0o600)) - .owner(nix::unistd::ROOT) - .group(nix::unistd::Gid::from_raw(0)), - true, - ) - } - - /// Shortcut to `account().ok_or_else(...).key_authorization()`. - pub fn key_authorization(&self, token: &str) -> Result { - Ok(Self::need_account(&self.account)?.key_authorization(token)?) - } - - /// Shortcut to `account().ok_or_else(...).dns_01_txt_value()`. - /// the key authorization value. - pub fn dns_01_txt_value(&self, token: &str) -> Result { - Ok(Self::need_account(&self.account)?.dns_01_txt_value(token)?) - } - - async fn register_account( - &mut self, - account: AccountCreator, - ) -> Result<&Account, anyhow::Error> { - let mut retry = retry(); - let mut response = loop { - retry.tick()?; - - let (directory, nonce) = Self::get_dir_nonce( - &mut self.http_client, - &self.directory_url, - &mut self.directory, - &mut self.nonce, - ) - .await?; - let request = account.request(directory, nonce)?; - match self.run_request(request).await { - Ok(response) => break response, - Err(err) if err.is_bad_nonce() => continue, - Err(err) => return Err(err.into()), - } - }; - - let account = account.response(response.location_required()?, &response.body)?; - - self.account = Some(account); - Ok(self.account.as_ref().unwrap()) - } - - pub async fn update_account( - &mut self, - data: &T, - ) -> Result<&Account, anyhow::Error> { - let account = Self::need_account(&self.account)?; - - let mut retry = retry(); - let response = loop { - retry.tick()?; - - let (_directory, nonce) = Self::get_dir_nonce( - &mut self.http_client, - &self.directory_url, - &mut self.directory, - &mut self.nonce, - ) - .await?; - - let request = account.post_request(&account.location, nonce, data)?; - match Self::execute(&mut self.http_client, request, &mut self.nonce).await { - Ok(response) => break response, - Err(err) if err.is_bad_nonce() => continue, - Err(err) => return Err(err.into()), - } - }; - - // unwrap: we've been keeping an immutable reference to it from the top of the method - let _ = account; - self.account.as_mut().unwrap().data = response.json()?; - self.save()?; - Ok(self.account.as_ref().unwrap()) - } - - pub async fn new_order(&mut self, domains: I) -> Result - where - I: IntoIterator, - { - let account = Self::need_account(&self.account)?; - - let order = domains - .into_iter() - .fold(OrderData::new(), |order, domain| order.domain(domain)); - - let mut retry = retry(); - loop { - retry.tick()?; - - let (directory, nonce) = Self::get_dir_nonce( - &mut self.http_client, - &self.directory_url, - &mut self.directory, - &mut self.nonce, - ) - .await?; - - let mut new_order = account.new_order(&order, directory, nonce)?; - let mut response = match Self::execute( - &mut self.http_client, - new_order.request.take().unwrap(), - &mut self.nonce, - ) - .await - { - Ok(response) => response, - Err(err) if err.is_bad_nonce() => continue, - Err(err) => return Err(err.into()), - }; - - return Ok( - new_order.response(response.location_required()?, response.bytes().as_ref())? - ); - } - } - - /// Low level "POST-as-GET" request. - async fn post_as_get(&mut self, url: &str) -> Result { - let account = Self::need_account(&self.account)?; - - let mut retry = retry(); - loop { - retry.tick()?; - - let (_directory, nonce) = Self::get_dir_nonce( - &mut self.http_client, - &self.directory_url, - &mut self.directory, - &mut self.nonce, - ) - .await?; - - let request = account.get_request(url, nonce)?; - match Self::execute(&mut self.http_client, request, &mut self.nonce).await { - Ok(response) => return Ok(response), - Err(err) if err.is_bad_nonce() => continue, - Err(err) => return Err(err.into()), - } - } - } - - /// Low level POST request. - async fn post( - &mut self, - url: &str, - data: &T, - ) -> Result { - let account = Self::need_account(&self.account)?; - - let mut retry = retry(); - loop { - retry.tick()?; - - let (_directory, nonce) = Self::get_dir_nonce( - &mut self.http_client, - &self.directory_url, - &mut self.directory, - &mut self.nonce, - ) - .await?; - - let request = account.post_request(url, nonce, data)?; - match Self::execute(&mut self.http_client, request, &mut self.nonce).await { - Ok(response) => return Ok(response), - Err(err) if err.is_bad_nonce() => continue, - Err(err) => return Err(err.into()), - } - } - } - - /// Request challenge validation. Afterwards, the challenge should be polled. - pub async fn request_challenge_validation( - &mut self, - url: &str, - ) -> Result { - Ok(self - .post(url, &serde_json::Value::Object(Default::default())) - .await? - .json()?) - } - - /// Assuming the provided URL is an 'Authorization' URL, get and deserialize it. - pub async fn get_authorization(&mut self, url: &str) -> Result { - Ok(self.post_as_get(url).await?.json()?) - } - - /// Assuming the provided URL is an 'Order' URL, get and deserialize it. - pub async fn get_order(&mut self, url: &str) -> Result { - Ok(self.post_as_get(url).await?.json()?) - } - - /// Finalize an Order via its `finalize` URL property and the DER encoded CSR. - pub async fn finalize(&mut self, url: &str, csr: &[u8]) -> Result<(), anyhow::Error> { - let csr = proxmox_base64::url::encode_no_pad(csr); - let data = serde_json::json!({ "csr": csr }); - self.post(url, &data).await?; - Ok(()) - } - - /// Download a certificate via its 'certificate' URL property. - /// - /// The certificate will be a PEM certificate chain. - pub async fn get_certificate(&mut self, url: &str) -> Result { - Ok(self.post_as_get(url).await?.body) - } - - /// Revoke an existing certificate (PEM or DER formatted). - pub async fn revoke_certificate( - &mut self, - certificate: &[u8], - reason: Option, - ) -> Result<(), anyhow::Error> { - // TODO: This can also work without an account. - let account = Self::need_account(&self.account)?; - - let revocation = account.revoke_certificate(certificate, reason)?; - - let mut retry = retry(); - loop { - retry.tick()?; - - let (directory, nonce) = Self::get_dir_nonce( - &mut self.http_client, - &self.directory_url, - &mut self.directory, - &mut self.nonce, - ) - .await?; - - let request = revocation.request(directory, nonce)?; - match Self::execute(&mut self.http_client, request, &mut self.nonce).await { - Ok(_response) => return Ok(()), - Err(err) if err.is_bad_nonce() => continue, - Err(err) => return Err(err.into()), - } - } - } - - fn need_account(account: &Option) -> Result<&Account, anyhow::Error> { - account - .as_ref() - .ok_or_else(|| format_err!("cannot use client without an account")) - } - - pub(crate) fn account(&self) -> Result<&Account, anyhow::Error> { - Self::need_account(&self.account) - } - - pub fn tos(&self) -> Option<&str> { - self.tos.as_deref() - } - - pub fn directory_url(&self) -> &str { - &self.directory_url - } - - fn to_account_data(&self) -> Result { - let account = self.account()?; - - Ok(AccountData { - location: account.location.clone(), - key: account.private_key.clone(), - account: AcmeAccountData { - only_return_existing: false, // don't actually write this out in case it's set - ..account.data.clone() - }, - tos: self.tos.clone(), - debug: self.debug, - directory_url: self.directory_url.clone(), - }) - } - - fn write_to(&self, out: T) -> Result<(), anyhow::Error> { - let data = self.to_account_data()?; - - Ok(serde_json::to_writer_pretty(out, &data)?) - } -} - -struct AcmeResponse { - body: Bytes, - location: Option, - got_nonce: bool, -} - -impl AcmeResponse { - /// Convenience helper to assert that a location header was part of the response. - fn location_required(&mut self) -> Result { - self.location - .take() - .ok_or_else(|| format_err!("missing Location header")) - } - - /// Convenience shortcut to perform json deserialization of the returned body. - fn json Deserialize<'a>>(&self) -> Result { - Ok(serde_json::from_slice(&self.body)?) - } - - /// Convenience shortcut to get the body as bytes. - fn bytes(&self) -> &[u8] { - &self.body - } -} - -impl AcmeClient { - /// Non-self-borrowing run_request version for borrow workarounds. - async fn execute( - http_client: &mut Client, - request: AcmeRequest, - nonce: &mut Option, - ) -> Result { - let req_builder = Request::builder().method(request.method).uri(&request.url); - - let http_request = if !request.content_type.is_empty() { - req_builder - .header("Content-Type", request.content_type) - .header("Content-Length", request.body.len()) - .body(request.body.into()) - } else { - req_builder.body(Body::empty()) - } - .map_err(|err| Error::Custom(format!("failed to create http request: {err}")))?; - - let response = http_client - .request(http_request) - .await - .map_err(|err| Error::Custom(err.to_string()))?; - let (parts, body) = response.into_parts(); - - let status = parts.status.as_u16(); - let body = body - .collect() - .await - .map_err(|err| Error::Custom(format!("failed to retrieve response body: {err}")))? - .to_bytes(); - - let got_nonce = if let Some(new_nonce) = parts.headers.get(proxmox_acme::REPLAY_NONCE) { - let new_nonce = new_nonce.to_str().map_err(|err| { - Error::Client(format!( - "received invalid replay-nonce header from ACME server: {err}" - )) - })?; - *nonce = Some(new_nonce.to_owned()); - true - } else { - false - }; - - if parts.status.is_success() { - if status != request.expected { - return Err(Error::InvalidApi(format!( - "ACME server responded with unexpected status code: {:?}", - parts.status - ))); - } - - let location = parts - .headers - .get("Location") - .map(|header| { - header.to_str().map(str::to_owned).map_err(|err| { - Error::Client(format!( - "received invalid location header from ACME server: {err}" - )) - }) - }) - .transpose()?; - - return Ok(AcmeResponse { - body, - location, - got_nonce, - }); - } - - let error: ErrorResponse = serde_json::from_slice(&body).map_err(|err| { - Error::Client(format!( - "error status with improper error ACME response: {err}" - )) - })?; - - if error.ty == proxmox_acme::error::BAD_NONCE { - if !got_nonce { - return Err(Error::InvalidApi( - "badNonce without a new Replay-Nonce header".to_string(), - )); - } - return Err(Error::BadNonce); - } - - Err(Error::Api(error)) - } - - /// Low-level API to run an n API request. This automatically updates the current nonce! - async fn run_request(&mut self, request: AcmeRequest) -> Result { - Self::execute(&mut self.http_client, request, &mut self.nonce).await - } - - pub async fn directory(&mut self) -> Result<&Directory, Error> { - Ok(Self::get_directory( - &mut self.http_client, - &self.directory_url, - &mut self.directory, - &mut self.nonce, - ) - .await? - .0) - } - - async fn get_directory<'a, 'b>( - http_client: &mut Client, - directory_url: &str, - directory: &'a mut Option, - nonce: &'b mut Option, - ) -> Result<(&'a Directory, Option<&'b str>), Error> { - if let Some(d) = directory { - return Ok((d, nonce.as_deref())); - } - - let response = Self::execute( - http_client, - AcmeRequest { - url: directory_url.to_string(), - method: "GET", - content_type: "", - body: String::new(), - expected: 200, - }, - nonce, - ) - .await?; - - *directory = Some(Directory::from_parts( - directory_url.to_string(), - response.json()?, - )); - - Ok((directory.as_mut().unwrap(), nonce.as_deref())) - } - - /// Like `get_directory`, but if the directory provides no nonce, also performs a `HEAD` - /// request on the new nonce URL. - async fn get_dir_nonce<'a, 'b>( - http_client: &mut Client, - directory_url: &str, - directory: &'a mut Option, - nonce: &'b mut Option, - ) -> Result<(&'a Directory, &'b str), Error> { - // this let construct is a lifetime workaround: - let _ = Self::get_directory(http_client, directory_url, directory, nonce).await?; - let dir = directory.as_ref().unwrap(); // the above fails if it couldn't fill this option - if nonce.is_none() { - // this is also a lifetime issue... - let _ = Self::get_nonce(http_client, nonce, dir.new_nonce_url()).await?; - }; - Ok((dir, nonce.as_deref().unwrap())) - } - - pub async fn terms_of_service_url(&mut self) -> Result, Error> { - Ok(self.directory().await?.terms_of_service_url()) - } - - async fn get_nonce<'a>( - http_client: &mut Client, - nonce: &'a mut Option, - new_nonce_url: &str, - ) -> Result<&'a str, Error> { - let response = Self::execute( - http_client, - AcmeRequest { - url: new_nonce_url.to_owned(), - method: "HEAD", - content_type: "", - body: String::new(), - expected: 200, - }, - nonce, - ) - .await?; - - if !response.got_nonce { - return Err(Error::InvalidApi( - "no new nonce received from new nonce URL".to_string(), - )); - } - - nonce - .as_deref() - .ok_or_else(|| Error::Client("failed to update nonce".to_string())) - } -} - -/// bad nonce retry count helper -struct Retry(usize); - -const fn retry() -> Retry { - Retry(0) -} - -impl Retry { - fn tick(&mut self) -> Result<(), Error> { - if self.0 >= 3 { - Err(Error::Client("kept getting a badNonce error!".to_string())) - } else { - self.0 += 1; - Ok(()) - } - } -} diff --git a/src/acme/mod.rs b/src/acme/mod.rs index bf61811c..cc561f9a 100644 --- a/src/acme/mod.rs +++ b/src/acme/mod.rs @@ -1,5 +1,2 @@ -mod client; -pub use client::AcmeClient; - pub(crate) mod plugin; pub(crate) use plugin::get_acme_plugin; diff --git a/src/acme/plugin.rs b/src/acme/plugin.rs index f756e9b5..5bc09e1f 100644 --- a/src/acme/plugin.rs +++ b/src/acme/plugin.rs @@ -20,8 +20,8 @@ use tokio::process::Command; use proxmox_acme::{Authorization, Challenge}; -use crate::acme::AcmeClient; use crate::api2::types::AcmeDomain; +use proxmox_acme::async_client::AcmeClient; use proxmox_rest_server::WorkerTask; use crate::config::acme::plugin::{DnsPlugin, PluginData}; diff --git a/src/api2/config/acme.rs b/src/api2/config/acme.rs index 35c3fb77..02f88e2e 100644 --- a/src/api2/config/acme.rs +++ b/src/api2/config/acme.rs @@ -16,15 +16,15 @@ use proxmox_router::{ use proxmox_schema::{api, param_bail}; use proxmox_acme::types::AccountData as AcmeAccountData; -use proxmox_acme::Account; use pbs_api_types::{Authid, PRIV_SYS_MODIFY}; -use crate::acme::AcmeClient; -use crate::api2::types::{AcmeAccountName, AcmeChallengeSchema, KnownAcmeDirectory}; +use crate::api2::types::{AcmeChallengeSchema, KnownAcmeDirectory}; use crate::config::acme::plugin::{ self, DnsPlugin, DnsPluginCore, DnsPluginCoreUpdater, PLUGIN_ID_SCHEMA, }; +use proxmox_acme::async_client::AcmeClient; +use proxmox_acme_api::AcmeAccountName; use proxmox_rest_server::WorkerTask; pub(crate) const ROUTER: Router = Router::new() @@ -143,15 +143,15 @@ pub struct AccountInfo { )] /// Return existing ACME account information. pub async fn get_account(name: AcmeAccountName) -> Result { - let client = AcmeClient::load(&name).await?; - let account = client.account()?; + let account_info = proxmox_acme_api::get_account(name).await?; + Ok(AccountInfo { - location: account.location.clone(), - tos: client.tos().map(str::to_owned), - directory: client.directory_url().to_owned(), + location: account_info.location, + tos: account_info.tos, + directory: account_info.directory, account: AcmeAccountData { only_return_existing: false, // don't actually write this out in case it's set - ..account.data.clone() + ..account_info.account }, }) } @@ -240,41 +240,24 @@ fn register_account( auth_id.to_string(), true, move |_worker| async move { - let mut client = AcmeClient::new(directory); - info!("Registering ACME account '{}'...", &name); - let account = do_register_account( - &mut client, + let location = proxmox_acme_api::register_account( &name, - tos_url.is_some(), contact, - None, + tos_url, + Some(directory), eab_kid.zip(eab_hmac_key), ) .await?; - info!("Registration successful, account URL: {}", account.location); + info!("Registration successful, account URL: {}", location); Ok(()) }, ) } -pub async fn do_register_account<'a>( - client: &'a mut AcmeClient, - name: &AcmeAccountName, - agree_to_tos: bool, - contact: String, - rsa_bits: Option, - eab_creds: Option<(String, String)>, -) -> Result<&'a Account, Error> { - let contact = account_contact_from_string(&contact); - client - .new_account(name, agree_to_tos, contact, rsa_bits, eab_creds) - .await -} - #[api( input: { properties: { @@ -312,7 +295,10 @@ pub fn update_account( None => json!({}), }; - AcmeClient::load(&name).await?.update_account(&data).await?; + proxmox_acme_api::load_client_with_account(&name) + .await? + .update_account(&data) + .await?; Ok(()) }, @@ -350,7 +336,7 @@ pub fn deactivate_account( auth_id.to_string(), true, move |_worker| async move { - match AcmeClient::load(&name) + match proxmox_acme_api::load_client_with_account(&name) .await? .update_account(&json!({"status": "deactivated"})) .await diff --git a/src/api2/node/certificates.rs b/src/api2/node/certificates.rs index 61ef910e..31196715 100644 --- a/src/api2/node/certificates.rs +++ b/src/api2/node/certificates.rs @@ -17,10 +17,10 @@ use pbs_buildcfg::configdir; use pbs_tools::cert; use tracing::warn; -use crate::acme::AcmeClient; 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_rest_server::WorkerTask; pub const ROUTER: Router = Router::new() diff --git a/src/api2/types/acme.rs b/src/api2/types/acme.rs index 210ebdbc..7c9063c0 100644 --- a/src/api2/types/acme.rs +++ b/src/api2/types/acme.rs @@ -60,14 +60,6 @@ pub struct KnownAcmeDirectory { pub url: &'static str, } -proxmox_schema::api_string_type! { - #[api(format: &PROXMOX_SAFE_ID_FORMAT)] - /// ACME account name. - #[derive(Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] - #[serde(transparent)] - pub struct AcmeAccountName(String); -} - #[api( properties: { schema: { diff --git a/src/bin/proxmox_backup_manager/acme.rs b/src/bin/proxmox_backup_manager/acme.rs index 0f0eafea..bb987b26 100644 --- a/src/bin/proxmox_backup_manager/acme.rs +++ b/src/bin/proxmox_backup_manager/acme.rs @@ -7,9 +7,9 @@ use proxmox_router::{cli::*, ApiHandler, RpcEnvironment}; use proxmox_schema::api; use proxmox_sys::fs::file_get_contents; -use proxmox_backup::acme::AcmeClient; +use proxmox_acme::async_client::AcmeClient; +use proxmox_acme_api::AcmeAccountName; use proxmox_backup::api2; -use proxmox_backup::api2::types::AcmeAccountName; use proxmox_backup::config::acme::plugin::DnsPluginCore; use proxmox_backup::config::acme::KNOWN_ACME_DIRECTORIES; @@ -188,17 +188,20 @@ async fn register_account( println!("Attempting to register account with {directory_url:?}..."); - let account = api2::config::acme::do_register_account( - &mut client, + let tos_agreed = tos_agreed + .then(|| directory.terms_of_service_url().map(str::to_owned)) + .flatten(); + + let location = proxmox_acme_api::register_account( &name, - tos_agreed, contact, - None, + tos_agreed, + Some(directory_url), eab_creds, ) .await?; - println!("Registration successful, account URL: {}", account.location); + println!("Registration successful, account URL: {}", location); Ok(()) } diff --git a/src/config/acme/mod.rs b/src/config/acme/mod.rs index 274a23fd..d31b2bc9 100644 --- a/src/config/acme/mod.rs +++ b/src/config/acme/mod.rs @@ -10,7 +10,8 @@ use proxmox_sys::fs::{file_read_string, CreateOptions}; use pbs_api_types::PROXMOX_SAFE_ID_REGEX; -use crate::api2::types::{AcmeAccountName, AcmeChallengeSchema, KnownAcmeDirectory}; +use crate::api2::types::{AcmeChallengeSchema, KnownAcmeDirectory}; +use proxmox_acme_api::AcmeAccountName; pub(crate) const ACME_DIR: &str = pbs_buildcfg::configdir!("/acme"); pub(crate) const ACME_ACCOUNT_DIR: &str = pbs_buildcfg::configdir!("/acme/accounts"); @@ -35,11 +36,6 @@ pub(crate) fn make_acme_dir() -> Result<(), Error> { create_acme_subdir(ACME_DIR) } -pub(crate) fn make_acme_account_dir() -> Result<(), Error> { - make_acme_dir()?; - create_acme_subdir(ACME_ACCOUNT_DIR) -} - pub const KNOWN_ACME_DIRECTORIES: &[KnownAcmeDirectory] = &[ KnownAcmeDirectory { name: "Let's Encrypt V2", diff --git a/src/config/node.rs b/src/config/node.rs index d2d6e383..d2a17a49 100644 --- a/src/config/node.rs +++ b/src/config/node.rs @@ -16,10 +16,9 @@ use pbs_api_types::{ use pbs_buildcfg::configdir; use pbs_config::{open_backup_lockfile, BackupLockGuard}; -use crate::acme::AcmeClient; -use crate::api2::types::{ - AcmeAccountName, AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA, HTTP_PROXY_SCHEMA, -}; +use crate::api2::types::{AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA, HTTP_PROXY_SCHEMA}; +use proxmox_acme::async_client::AcmeClient; +use proxmox_acme_api::AcmeAccountName; const CONF_FILE: &str = configdir!("/node.cfg"); const LOCK_FILE: &str = configdir!("/.node.lck"); @@ -249,7 +248,7 @@ impl NodeConfig { } else { AcmeAccountName::from_string("default".to_string())? // should really not happen }; - AcmeClient::load(&account).await + proxmox_acme_api::load_client_with_account(&account).await } pub fn acme_domains(&'_ self) -> AcmeDomainIter<'_> { -- 2.47.3 _______________________________________________ pbs-devel mailing list pbs-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel