From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 2DFB31FF0E6 for ; Fri, 24 Jul 2026 13:17:20 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id EE676214A1; Fri, 24 Jul 2026 13:17:19 +0200 (CEST) Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 Date: Fri, 24 Jul 2026 13:16:45 +0200 Message-Id: Subject: Re: [PATCH proxmox 1/7] acme: client: add methods to fetch renewal information. From: "Shan Shaji" To: "Manuel Federanko" , , X-Mailer: aerc 0.20.0 References: <20260625141337.181684-1-m.federanko@proxmox.com> <20260625141337.181684-2-m.federanko@proxmox.com> In-Reply-To: <20260625141337.181684-2-m.federanko@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1784891774382 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.144 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: ULSKOSXPVWRSTHOZ323ZJAKRDUEEGYSZ X-Message-ID-Hash: ULSKOSXPVWRSTHOZ323ZJAKRDUEEGYSZ X-MailFrom: s.shaji@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 Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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-a= pi/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, > } > =20 > +pub fn compute_ari_certificate_id(cert: &openssl::x509::X509) -> Option<= String> { > + let authority_key_identifier =3D match cert.authority_key_id() { > + Some(v) =3D> v.as_slice(), > + None =3D> return None, > + }; > + let mut serial_number =3D (match cert.serial_number().to_bn() { > + Ok(v) =3D> v, > + Err(_) =3D> 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 =3D proxmox_base64::url::encode_no_pad(= authority_key_identifier); > + let serial_number =3D proxmox_base64::url::encode_no_pad(serial_numb= er); > + Some(format!("{authority_key_identifier}.{serial_number}")) > +} > + > +pub async fn get_renewal_info( > + acme_config: &AcmeConfig, > + certificate_id: &str, > +) -> Result, Error> { > + let mut acme =3D super::account_config::load_account_config(&acme_co= nfig.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 =3D "impl")] > mod certificate_helpers; > #[cfg(feature =3D "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_inf= o, order_certificate, > + revoke_certificate, > +}; > =20 > #[cfg(feature =3D "impl")] > pub mod completion { > diff --git a/proxmox-acme/src/async_client.rs b/proxmox-acme/src/async_cl= ient.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 { > } > } > =20 > + /// 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_certif= icate_id` helper? =20 > + ) -> Result, anyhow::Erro= r> { > + let directory =3D self.directory().await?; > + if directory.renewal_info_url().is_none() { > + return Ok(None); > + } > + let url =3D format!( > + "{}/{}", > + directory.renewal_info_url().unwrap(), > + certificate_id, > + ); tiny nit: May be we could rewrite this to avoid calling is_none then callin= g unwrap? let Some(renewal_info_url) =3D directory.renewal_info_url() else = { return Ok(None) }; let url =3D format!("{renewal_info_url}/{certificate_id}") > + let request =3D 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.no= nce).await { > + Ok(response) =3D> { > + let data: crate::renewal::RenewalInformationData =3D res= ponse.json()?; > + Ok(Some(crate::renewal::RenewalInformation { data })) > + } > + Err(err) =3D> Err(err.into()), > + } > + } > + > fn need_account(account: &Option) -> Result<&Account, anyho= w::Error> { > account > .as_ref() > diff --git a/proxmox-acme/src/directory.rs b/proxmox-acme/src/directory.r= s > 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 =3D "Option::is_none")] > pub key_change: Option, > =20 > + /// URL to get renewal information > + #[serde(skip_serializing_if =3D "Option::is_none")] > + pub renewal_info: Option, > + > /// Metadata object, for additional information which aren't directl= y part of the API > /// itself, such as the terms of service. > #[serde(skip_serializing_if =3D "Option::is_none")] > @@ -104,6 +108,10 @@ impl Directory { > self.data.new_order.as_deref() > } > =20 > + 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 =3D "impl")] > pub mod order; > =20 > +#[cfg(feature =3D "impl")] > +pub mod renewal; > + > #[cfg(feature =3D "impl")] > pub mod util; > =20 > 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 =3D "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 serv= er > +#[derive(Clone, Debug, Default, Deserialize, Serialize)] > +#[serde(rename_all =3D "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 =3D "Option::is_none")] > + #[serde(rename =3D "explanatoryURL")] > + pub explanation_url: Option, > +} > + > +/// Renewal and retry information > +#[derive(Clone, Debug, Default, Deserialize, Serialize)] > +#[serde(rename_all =3D "kebab-case")] > +pub struct RenewalInformation { > + /// the actual response of the acme server > + pub data: RenewalInformationData, > +}