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 706121FF0AA for ; Tue, 06 Oct 2026 14:31:44 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id AE2A82139E; Tue, 06 Oct 2026 14:31:41 +0200 (CEST) From: David Riley To: pve-devel@lists.proxmox.com Subject: [PATCH pve-common 1/2] certificate: add helpers for lifetime-based ACME renewal Date: Tue, 6 Oct 2026 14:31:19 +0200 Message-ID: <20261006123120.68750-2-d.riley@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261006123120.68750-1-d.riley@proxmox.com> References: <20261006123120.68750-1-d.riley@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1791289894599 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.775 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_MED -2.3 Sender listed at https://www.dnswl.org/, medium 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: 7WWEAUVWVDDXFNIOAVHEEQS7M5BWDFEK X-Message-ID-Hash: 7WWEAUVWVDDXFNIOAVHEEQS7M5BWDFEK X-MailFrom: d.riley@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 VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Add helpers to calculate a renewal lead time based on the actual lifetime of an X.509 certificate (the difference between its notBefore and notAfter dates [0]). This commit is in preparation for fixing #8006. The scaling mirrors the logic already used in Proxmox Backup Server [1]: certificates living for less than 10 days are renewed at half their lifetime, while longer-lived certificates use a third of their lifetime. Link: https://bugzilla.proxmox.com/show_bug.cgi?id=8006 [0] https://www.cs.auckland.ac.nz/~pgut001/pubs/x509guide.txt [1] https://lore.proxmox.com/pbs-devel/20260423134607.105229-2-m.federanko@proxmox.com/ Signed-off-by: David Riley --- src/PVE/Certificate.pm | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/PVE/Certificate.pm b/src/PVE/Certificate.pm index b8415e2..45c9bc7 100644 --- a/src/PVE/Certificate.pm +++ b/src/PVE/Certificate.pm @@ -328,6 +328,21 @@ sub get_certificate_info { return $info; } +# Obtain the notBefore timestamp of a X.509 certificate as a UNIX epoch. +sub get_not_before_as_epoch { + my ($cert_path) = @_; + + my $cert = $read_certificate->($cert_path); + my $not_before = eval { convert_asn1_to_epoch(Net::SSLeay::X509_get_notBefore($cert)) }; + my $err = $@; + + Net::SSLeay::X509_free($cert); + + die $err if $err; + + return $not_before; +} + # Obtain the expiration timestamp of a X.509 certificate as a UNIX epoch. sub get_expiration_as_epoch { my ($cert_path) = @_; @@ -354,6 +369,22 @@ sub check_expiry { return ($not_after < $timestamp) ? 1 : 0; } +sub get_cert_renew_lead_time { + my ($cert_path) = @_; + + my $default_lead = 30 * 24 * 60 * 60; + + my $not_before = get_not_before_as_epoch($cert_path); + my $not_after = get_expiration_as_epoch($cert_path); + + my $lifetime = $not_after - $not_before; + return $default_lead if $lifetime <= 0; + + my $scale = $lifetime < (10 * 24 * 60 * 60) ? 2 : 3; + + return int($lifetime / $scale); +} + # Create a CSR and certificate key for a given order # returns path to CSR file or path to CSR and key files sub generate_csr { -- 2.47.3