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 621F01FF126 for ; Wed, 05 Aug 2026 17:53:37 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id D2AB321A8E; Wed, 05 Aug 2026 17:53:36 +0200 (CEST) From: Shannon Sterz To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox v3 03/16] tls-certificates: add self_signed_cert_expires_in to check certificates Date: Wed, 5 Aug 2026 17:52:56 +0200 Message-ID: <20260805155308.519896-5-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260805155308.519896-2-s.sterz@proxmox.com> References: <20260805155308.519896-2-s.sterz@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1785945198406 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.113 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: SQZOGY5ORBKDOWKFPYXOXNUVVFLKANXI X-Message-ID-Hash: SQZOGY5ORBKDOWKFPYXOXNUVVFLKANXI X-MailFrom: s.sterz@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: this is useful when self-signed certificates are in use. it heuristically checks whether a certificate is self-signed by the specified node and returns for how many more days the certificate is valid if it is a self-signed certificate. Signed-off-by: Shannon Sterz --- proxmox-tls-certificates/Cargo.toml | 2 + proxmox-tls-certificates/src/lib.rs | 2 +- proxmox-tls-certificates/src/util.rs | 55 ++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/proxmox-tls-certificates/Cargo.toml b/proxmox-tls-certificates/Cargo.toml index 3a7b97cd..658a700d 100644 --- a/proxmox-tls-certificates/Cargo.toml +++ b/proxmox-tls-certificates/Cargo.toml @@ -16,6 +16,7 @@ hex = { workspace = true, optional = true } libc = { workspace = true, optional = true } openssl = { workspace = true, optional = true } serde = { workspace = true, features = [ "derive" ] } +regex = { workspace = true, optional = true } proxmox-schema = { workspace = true, features = [ "api-macro" ] } proxmox-time = { workspace = true, optional = true } @@ -29,6 +30,7 @@ impl = [ "dep:hex", "dep:libc", "dep:openssl", + "dep:regex", "dep:proxmox-time", "dep:proxmox-uuid", diff --git a/proxmox-tls-certificates/src/lib.rs b/proxmox-tls-certificates/src/lib.rs index 454c700f..66def7d6 100644 --- a/proxmox-tls-certificates/src/lib.rs +++ b/proxmox-tls-certificates/src/lib.rs @@ -4,4 +4,4 @@ pub use types::CertificateInfo; #[cfg(feature = "impl")] mod util; #[cfg(feature = "impl")] -pub use util::create_self_signed_cert; +pub use util::{create_self_signed_cert, self_signed_cert_expires_in}; diff --git a/proxmox-tls-certificates/src/util.rs b/proxmox-tls-certificates/src/util.rs index 98aa042e..a07903d8 100644 --- a/proxmox-tls-certificates/src/util.rs +++ b/proxmox-tls-certificates/src/util.rs @@ -2,12 +2,54 @@ use std::mem::MaybeUninit; use anyhow::{Error, bail, format_err}; use foreign_types::ForeignTypeRef; +use openssl::asn1::Asn1Time; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; use openssl::x509::{X509, X509Builder}; use crate::CertificateInfo; +proxmox_schema::const_regex! { + SELF_SIGNED_REGEX = + // O = $product_name, OU = $uuid, CN = $nodename + r#"^O\s?=\s(?P.*)?, OU\s?=\s?[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}, CN\s?=\s?(?P.*)$"#; +} + +/// Check whether the current certificate is self-signed and returns the remaining days the +/// certificate is valid for. +pub fn self_signed_cert_expires_in( + product_name: &str, + nodename: &str, + domain: Option<&str>, + cert: CertificateInfo, +) -> Result, Error> { + let Some(captures) = (SELF_SIGNED_REGEX.regex_obj)().captures(&cert.issuer) else { + return Ok(None); + }; + + let mut fqdn = nodename.to_string(); + + if let Some(domain) = domain { + fqdn = format!("{fqdn}.{domain}"); + } + + if captures["product"] != *product_name { + return Ok(None); + } + + if captures["fqdn"] != fqdn { + return Ok(None); + } + + let now = Asn1Time::from_unix(proxmox_time::epoch_i64())?; + let not_after = Asn1Time::from_unix(cert.notafter.ok_or_else(|| { + format_err!("Could not get \"not after\" epoch for current certificate.") + })?)?; + + let diff = now.diff(¬_after)?; + Ok(Some(diff.days)) +} + /// Create a new self-signed certificate and its private key. /// /// * `product_name`: The name of the product, will be used as the organization of the certificate. @@ -237,6 +279,19 @@ mod test { } } + #[test] + fn self_signed_cert_expires_in_gets_correct_days() { + let cert_info = + get_self_signed_certificate_info("Proxmox Test Product", "name", Some("fqdn"), Some(2)); + + match self_signed_cert_expires_in("Proxmox Test Product", "name", Some("fqdn"), cert_info) { + // allow the range [1,2] due to truncation, the test could take too long to always yield "2" here + Ok(Some(days)) => assert!((1..=2).contains(&days)), + Ok(None) => panic!("not a self-signed certificate"), + Err(e) => panic!("error occurred checking certificate - {e:#}"), + } + } + #[test] fn self_signed_certificate_correct_with_product_and_name_only() { let cert_info = -- 2.47.3