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 86AC21FF0E2 for ; Thu, 30 Jul 2026 15:32:06 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 5B9C72133E; Thu, 30 Jul 2026 15:32:05 +0200 (CEST) From: Shannon Sterz To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox 03/16] tls-certificates: add self_signed_cert_expires_in to check certificates Date: Thu, 30 Jul 2026 15:31:45 +0200 Message-ID: <20260730133158.418015-4-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260730133158.418015-1-s.sterz@proxmox.com> References: <20260730133158.418015-1-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: 1785418313056 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.125 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: MRKHJ3NSOHWWKCB5KR3QO7PRNT63UFX7 X-Message-ID-Hash: MRKHJ3NSOHWWKCB5KR3QO7PRNT63UFX7 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 | 3 ++ proxmox-tls-certificates/src/lib.rs | 2 +- proxmox-tls-certificates/src/util.rs | 56 ++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/proxmox-tls-certificates/Cargo.toml b/proxmox-tls-certificates/Cargo.toml index 105657f2..3d5ee365 100644 --- a/proxmox-tls-certificates/Cargo.toml +++ b/proxmox-tls-certificates/Cargo.toml @@ -11,11 +11,13 @@ rust-version.workspace = true [dependencies] anyhow = { workspace = true, optional = true } +const_format = { workspace = true, optional = true } foreign-types = { workspace = true, optional = true } 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-sys = { workspace = true, optional = true } @@ -31,6 +33,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 8a822956..d627338c 100644 --- a/proxmox-tls-certificates/src/util.rs +++ b/proxmox-tls-certificates/src/util.rs @@ -1,13 +1,57 @@ use std::mem::MaybeUninit; use anyhow::{Error, bail, format_err}; +use const_format::concatcp; 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 = concatcp!( + // O = $product_name, OU = $uuid, CN = $nodename + r#"^O\s?=\s(?.*)?, OU\s?=\s?[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}, CN\s?=\s?(?.*)$"# + ); +} + +/// 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 +281,18 @@ 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) { + Ok(Some(days)) => assert_eq!(days, 2i32), + Ok(None) => panic!("not a self-signed certificate"), + Err(e) => panic!("error occured checking certificate - {e:#}"), + } + } + #[test] fn self_signed_certificate_correct_with_product_and_name_only() { let cert_info = -- 2.47.3