From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id D9AEC1FF0E5 for ; Wed, 29 Jul 2026 11:05:45 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 3DC42213E7; Wed, 29 Jul 2026 11:05:45 +0200 (CEST) Message-ID: <71dc5aa3-94bf-4354-b42c-af7471a45877@proxmox.com> Date: Wed, 29 Jul 2026 11:04:53 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: [PATCH proxmox 1/7] acme: client: add methods to fetch renewal information. To: Shan Shaji , pbs-devel@lists.proxmox.com, pdm-devel@lists.proxmox.com References: <20260625141337.181684-1-m.federanko@proxmox.com> <20260625141337.181684-2-m.federanko@proxmox.com> Content-Language: en-US From: Manuel Federanko In-Reply-To: Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1785315856006 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.175 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_LOW -0.7 Sender listed at https://www.dnswl.org/, low trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 7WRMHEZ6QWMX4NJBPITHDHQOOI5PHSPM X-Message-ID-Hash: 7WRMHEZ6QWMX4NJBPITHDHQOOI5PHSPM X-MailFrom: m.federanko@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: On 2026-07-24 1:16 PM, Shan Shaji wrote: > On Thu Jun 25, 2026 at 4:13 PM CEST, Manuel Federanko wrote: >> Add new structs representing the renewal information returned by the >> server. >> >> Introduce a method on the client that fetches the renewal information >> from the server. A helper method computes the certificate ID passed to >> this method. >> >> Signed-off-by: Manuel Federanko >> --- >> proxmox-acme-api/src/certificate_helpers.rs | 30 +++++++++++++++++++ >> proxmox-acme-api/src/lib.rs | 5 +++- >> proxmox-acme/src/async_client.rs | 31 +++++++++++++++++++ >> proxmox-acme/src/directory.rs | 8 +++++ >> proxmox-acme/src/lib.rs | 3 ++ >> proxmox-acme/src/renewal.rs | 33 +++++++++++++++++++++ >> 6 files changed, 109 insertions(+), 1 deletion(-) >> create mode 100644 proxmox-acme/src/renewal.rs >> >> diff --git a/proxmox-acme-api/src/certificate_helpers.rs b/proxmox-acme-api/src/certificate_helpers.rs >> index 3921b18e..7dc06c2d 100644 >> --- a/proxmox-acme-api/src/certificate_helpers.rs >> +++ b/proxmox-acme-api/src/certificate_helpers.rs >> @@ -31,6 +31,36 @@ pub struct OrderedCertificate { >> pub private_key_pem: Vec, >> } >> >> +pub fn compute_ari_certificate_id(cert: &openssl::x509::X509) -> Option { >> + let authority_key_identifier = match cert.authority_key_id() { >> + Some(v) => v.as_slice(), >> + None => return None, >> + }; >> + let mut serial_number = (match cert.serial_number().to_bn() { >> + Ok(v) => v, >> + Err(_) => return None, >> + }) >> + .to_vec(); >> + if !serial_number.is_empty() && (serial_number[0] & 0x80) > 0 { >> + // check for negative numbers and prepend leading 0 >> + serial_number.insert(0, 0); >> + } >> + >> + let authority_key_identifier = proxmox_base64::url::encode_no_pad(authority_key_identifier); >> + let serial_number = proxmox_base64::url::encode_no_pad(serial_number); >> + Some(format!("{authority_key_identifier}.{serial_number}")) >> +} >> + >> +pub async fn get_renewal_info( >> + acme_config: &AcmeConfig, >> + certificate_id: &str, >> +) -> Result, Error> { >> + let mut acme = super::account_config::load_account_config(&acme_config.account) >> + .await? >> + .client(); >> + acme.get_renewal_info(certificate_id).await >> +} >> + >> pub async fn order_certificate( >> worker: Arc, >> acme_config: &AcmeConfig, >> diff --git a/proxmox-acme-api/src/lib.rs b/proxmox-acme-api/src/lib.rs >> index c315d137..89a5e9a2 100644 >> --- a/proxmox-acme-api/src/lib.rs >> +++ b/proxmox-acme-api/src/lib.rs >> @@ -45,7 +45,10 @@ pub(crate) mod acme_plugin; >> #[cfg(feature = "impl")] >> mod certificate_helpers; >> #[cfg(feature = "impl")] >> -pub use certificate_helpers::{create_self_signed_cert, order_certificate, revoke_certificate}; >> +pub use certificate_helpers::{ >> + compute_ari_certificate_id, create_self_signed_cert, get_renewal_info, order_certificate, >> + revoke_certificate, >> +}; >> >> #[cfg(feature = "impl")] >> pub mod completion { >> diff --git a/proxmox-acme/src/async_client.rs b/proxmox-acme/src/async_client.rs >> index bba92023..6670bc24 100644 >> --- a/proxmox-acme/src/async_client.rs >> +++ b/proxmox-acme/src/async_client.rs >> @@ -335,6 +335,37 @@ impl AcmeClient { >> } >> } >> >> + /// Get the renewal information for a certificate >> + /// Returns None if the server does not support ARI >> + pub async fn get_renewal_info( >> + &mut self, >> + certificate_id: &str, // computed according to rfc9773 section 4.1 > > nit: Would it be better to add this inline comment over `compute_ari_certificate_id` helper? > ack, though I would also leave a reference how this should be generated. The nicer solution would be it's own type i guess. >> + ) -> Result, anyhow::Error> { >> + let directory = self.directory().await?; >> + if directory.renewal_info_url().is_none() { >> + return Ok(None); >> + } >> + let url = format!( >> + "{}/{}", >> + directory.renewal_info_url().unwrap(), >> + certificate_id, >> + ); > > tiny nit: May be we could rewrite this to avoid calling is_none then calling unwrap? > > let Some(renewal_info_url) = directory.renewal_info_url() else { > return Ok(None) > }; > > let url = format!("{renewal_info_url}/{certificate_id}") ack, that would be cleaner. >> + let request = crate::request::Request { >> + url, >> + method: "GET", >> + content_type: crate::request::JSON_CONTENT_TYPE, >> + body: "".into(), >> + expected: &[crate::request::http_status::OK], >> + }; >> + match Self::execute(&mut self.http_client, request, &mut self.nonce).await { >> + Ok(response) => { >> + let data: crate::renewal::RenewalInformationData = response.json()?; >> + Ok(Some(crate::renewal::RenewalInformation { data })) >> + } >> + Err(err) => Err(err.into()), >> + } >> + } >> + >> fn need_account(account: &Option) -> Result<&Account, anyhow::Error> { >> account >> .as_ref() >> diff --git a/proxmox-acme/src/directory.rs b/proxmox-acme/src/directory.rs >> index b940901a..bbce4da5 100644 >> --- a/proxmox-acme/src/directory.rs >> +++ b/proxmox-acme/src/directory.rs >> @@ -38,6 +38,10 @@ pub struct DirectoryData { >> #[serde(skip_serializing_if = "Option::is_none")] >> pub key_change: Option, >> >> + /// URL to get renewal information >> + #[serde(skip_serializing_if = "Option::is_none")] >> + pub renewal_info: Option, >> + >> /// Metadata object, for additional information which aren't directly part of the API >> /// itself, such as the terms of service. >> #[serde(skip_serializing_if = "Option::is_none")] >> @@ -104,6 +108,10 @@ impl Directory { >> self.data.new_order.as_deref() >> } >> >> + pub(crate) fn renewal_info_url(&self) -> Option<&str> { >> + self.data.renewal_info.as_deref() >> + } >> + >> /// Access to the in the Acme spec defined metadata structure. >> pub fn meta(&self) -> Option<&Meta> { >> self.data.meta.as_ref() >> diff --git a/proxmox-acme/src/lib.rs b/proxmox-acme/src/lib.rs >> index 6b774746..370d5ec0 100644 >> --- a/proxmox-acme/src/lib.rs >> +++ b/proxmox-acme/src/lib.rs >> @@ -43,6 +43,9 @@ pub mod error; >> #[cfg(feature = "impl")] >> pub mod order; >> >> +#[cfg(feature = "impl")] >> +pub mod renewal; >> + >> #[cfg(feature = "impl")] >> pub mod util; >> >> diff --git a/proxmox-acme/src/renewal.rs b/proxmox-acme/src/renewal.rs >> new file mode 100644 >> index 00000000..eb4ff96a >> --- /dev/null >> +++ b/proxmox-acme/src/renewal.rs >> @@ -0,0 +1,33 @@ >> +//! Acme renewal information >> +use serde::{Deserialize, Serialize}; >> + >> +/// The suggested renewal time window >> +#[derive(Clone, Debug, Default, Deserialize, Serialize)] >> +#[serde(rename_all = "camelCase")] >> +pub struct SuggestedWindowData { >> + /// RFC3339 encoded time strings >> + pub start: String, >> + /// RFC3339 encoded time strings >> + pub end: String, >> +} >> + >> +/// This contains the renewal information data returned by the ACME server >> +#[derive(Clone, Debug, Default, Deserialize, Serialize)] >> +#[serde(rename_all = "camelCase")] >> +pub struct RenewalInformationData { >> + /// the suggested time window to renew the certificate >> + pub suggested_window: SuggestedWindowData, >> + >> + /// explanatory URL why the windows is suggested >> + #[serde(skip_serializing_if = "Option::is_none")] >> + #[serde(rename = "explanatoryURL")] >> + pub explanation_url: Option, >> +} >> + >> +/// Renewal and retry information >> +#[derive(Clone, Debug, Default, Deserialize, Serialize)] >> +#[serde(rename_all = "kebab-case")] >> +pub struct RenewalInformation { >> + /// the actual response of the acme server >> + pub data: RenewalInformationData, >> +} >