public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Shannon Sterz <s.sterz@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox 01/16] acme-api/tls-certificates: add new crate collecting tls realted helpers
Date: Thu, 30 Jul 2026 15:31:43 +0200	[thread overview]
Message-ID: <20260730133158.418015-2-s.sterz@proxmox.com> (raw)
In-Reply-To: <20260730133158.418015-1-s.sterz@proxmox.com>

and types. everything that is useful for dealing with tls certificates
but is not directly acme related should now be collected here. types
and functions that were previously part of proxmox-acme-api are
reexported there to avoid breakage. no functional changes intended for
users of proxmox-acme-api.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---

Notes:
    feel free to bikeshed about the name of the new crate btw. imo the
    name is a bit unwieldy even if it conveys the intended decently.

 Cargo.toml                                  |   2 +
 proxmox-acme-api/Cargo.toml                 |   2 +
 proxmox-acme-api/src/certificate_helpers.rs | 197 +-----------------
 proxmox-acme-api/src/types.rs               |  51 +----
 proxmox-tls-certificates/Cargo.toml         |  38 ++++
 proxmox-tls-certificates/src/lib.rs         |   7 +
 proxmox-tls-certificates/src/types.rs       |  54 +++++
 proxmox-tls-certificates/src/util.rs        | 209 ++++++++++++++++++++
 8 files changed, 315 insertions(+), 245 deletions(-)
 create mode 100644 proxmox-tls-certificates/Cargo.toml
 create mode 100644 proxmox-tls-certificates/src/lib.rs
 create mode 100644 proxmox-tls-certificates/src/types.rs
 create mode 100644 proxmox-tls-certificates/src/util.rs

diff --git a/Cargo.toml b/Cargo.toml
index 16e91c94..c876267e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,6 +63,7 @@ members = [
     "proxmox-tfa",
     "proxmox-time",
     "proxmox-time-api",
+    "proxmox-tls-certificates",
     "proxmox-upgrade-checks",
     "proxmox-uuid",
     "proxmox-wireguard",
@@ -197,6 +198,7 @@ proxmox-sys = { version = "1.0.1", path = "proxmox-sys" }
 proxmox-systemd = { version = "1.0.0", path = "proxmox-systemd" }
 proxmox-tfa = { version = "6.0.0", path = "proxmox-tfa" }
 proxmox-time = { version = "2.1.0", path = "proxmox-time" }
+proxmox-tls-certificates = { version = "1.0.0", path = "proxmox-tls-certificates" }
 proxmox-uuid = { version = "1.1.0", path = "proxmox-uuid" }
 proxmox-worker-task = { version = "1.0.0", path = "proxmox-worker-task" }
 proxmox-node-status = { version = "1.0.0", path = "proxmox-node-status" }
diff --git a/proxmox-acme-api/Cargo.toml b/proxmox-acme-api/Cargo.toml
index 4bb1720b..741987e0 100644
--- a/proxmox-acme-api/Cargo.toml
+++ b/proxmox-acme-api/Cargo.toml
@@ -39,6 +39,7 @@ proxmox-section-config = { workspace = true, optional = true }
 proxmox-serde.workspace = true
 proxmox-sys = { workspace = true, optional = true }
 proxmox-time = { workspace = true, optional = true }
+proxmox-tls-certificates = { workspace = true }
 proxmox-uuid = { workspace = true, optional = true }
 
 [features]
@@ -69,4 +70,5 @@ impl = [
     "proxmox-acme/async-client",
     "proxmox-acme/impl",
     "proxmox-config-digest?/openssl",
+    "proxmox-tls-certificates/impl",
 ]
diff --git a/proxmox-acme-api/src/certificate_helpers.rs b/proxmox-acme-api/src/certificate_helpers.rs
index 3921b18e..47b72f36 100644
--- a/proxmox-acme-api/src/certificate_helpers.rs
+++ b/proxmox-acme-api/src/certificate_helpers.rs
@@ -1,19 +1,14 @@
-use std::mem::MaybeUninit;
 use std::sync::Arc;
 use std::time::Duration;
 
-use foreign_types::ForeignTypeRef;
-
 use anyhow::{Error, bail, format_err};
-use openssl::pkey::{PKey, Private};
-use openssl::rsa::Rsa;
-use openssl::x509::{X509, X509Builder};
 
 use proxmox_acme::async_client::AcmeClient;
 use proxmox_log::{info, warn};
 use proxmox_rest_server::WorkerTask;
 
-use crate::CertificateInfo;
+pub use proxmox_tls_certificates::create_self_signed_cert;
+
 use crate::types::{AcmeConfig, AcmeDomain};
 
 pub async fn revoke_certificate(acme_config: &AcmeConfig, certificate: &[u8]) -> Result<(), Error> {
@@ -210,191 +205,3 @@ async fn request_validation(
     }
 }
 
-pub fn create_self_signed_cert(
-    product_name: &str,
-    nodename: &str,
-    domain: Option<&str>,
-) -> Result<(PKey<Private>, X509), Error> {
-    let rsa = Rsa::generate(4096).unwrap();
-
-    let mut x509 = X509Builder::new()?;
-
-    x509.set_version(2)?;
-
-    let today = openssl::asn1::Asn1Time::days_from_now(0)?;
-    x509.set_not_before(&today)?;
-    let expire = openssl::asn1::Asn1Time::days_from_now(365 * 1000)?;
-    x509.set_not_after(&expire)?;
-
-    let mut fqdn = nodename.to_owned();
-
-    if let Some(domain) = domain {
-        fqdn.push('.');
-        fqdn.push_str(domain);
-    }
-
-    // we try to generate an unique 'subject' to avoid browser problems
-    //(reused serial numbers, ..)
-    let uuid = proxmox_uuid::Uuid::generate();
-
-    let mut subject_name = openssl::x509::X509NameBuilder::new()?;
-    subject_name.append_entry_by_text("O", product_name)?;
-    subject_name.append_entry_by_text("OU", &format!("{uuid:X}"))?;
-    subject_name.append_entry_by_text("CN", &fqdn)?;
-    let subject_name = subject_name.build();
-
-    x509.set_subject_name(&subject_name)?;
-    x509.set_issuer_name(&subject_name)?;
-
-    let bc = openssl::x509::extension::BasicConstraints::new(); // CA = false
-    let bc = bc.build()?;
-    x509.append_extension(bc)?;
-
-    let usage = openssl::x509::extension::ExtendedKeyUsage::new()
-        .server_auth()
-        .build()?;
-    x509.append_extension(usage)?;
-
-    let context = x509.x509v3_context(None, None);
-
-    let mut alt_names = openssl::x509::extension::SubjectAlternativeName::new();
-
-    alt_names.ip("127.0.0.1");
-    alt_names.ip("::1");
-
-    alt_names.dns("localhost");
-
-    if nodename != "localhost" {
-        alt_names.dns(nodename);
-    }
-    if nodename != fqdn {
-        alt_names.dns(&fqdn);
-    }
-
-    let alt_names = alt_names.build(&context)?;
-
-    x509.append_extension(alt_names)?;
-
-    let pub_pem = rsa.public_key_to_pem()?;
-    let pubkey = PKey::public_key_from_pem(&pub_pem)?;
-
-    x509.set_pubkey(&pubkey)?;
-
-    let context = x509.x509v3_context(None, None);
-    let ext = openssl::x509::extension::SubjectKeyIdentifier::new().build(&context)?;
-    x509.append_extension(ext)?;
-
-    let context = x509.x509v3_context(None, None);
-    let ext = openssl::x509::extension::AuthorityKeyIdentifier::new()
-        .keyid(true)
-        .build(&context)?;
-    x509.append_extension(ext)?;
-
-    let privkey = PKey::from_rsa(rsa)?;
-
-    x509.sign(&privkey, openssl::hash::MessageDigest::sha256())?;
-
-    Ok((privkey, x509.build()))
-}
-
-impl CertificateInfo {
-    pub fn from_pem(filename: &str, cert_pem: &[u8]) -> Result<Self, Error> {
-        let x509 = openssl::x509::X509::from_pem(cert_pem)?;
-
-        let cert_pem = String::from_utf8(cert_pem.to_vec())
-            .map_err(|_| format_err!("certificate in {:?} is not a valid PEM file", filename))?;
-
-        let pubkey = x509.public_key()?;
-
-        let subject = x509name_to_string(x509.subject_name())?;
-        let issuer = x509name_to_string(x509.issuer_name())?;
-
-        let fingerprint = x509.digest(openssl::hash::MessageDigest::sha256())?;
-        let fingerprint = hex::encode(fingerprint)
-            .as_bytes()
-            .chunks(2)
-            .map(|v| std::str::from_utf8(v).unwrap())
-            .collect::<Vec<&str>>()
-            .join(":");
-
-        let public_key_type = openssl::nid::Nid::from_raw(pubkey.id().as_raw())
-            .long_name()
-            .unwrap_or("<unsupported key type>")
-            .to_owned();
-
-        let san = x509
-            .subject_alt_names()
-            .map(|san| {
-                san.into_iter()
-                    .filter_map(|name| {
-                        // this is not actually a map and we don't want to break the pattern
-                        #[allow(clippy::manual_map)]
-                        if let Some(name) = name.dnsname() {
-                            Some(format!("DNS: {name}"))
-                        } else if let Some(ip) = name.ipaddress() {
-                            Some(format!("IP: {ip:?}"))
-                        } else if let Some(email) = name.email() {
-                            Some(format!("EMAIL: {email}"))
-                        } else if let Some(uri) = name.uri() {
-                            Some(format!("URI: {uri}"))
-                        } else {
-                            None
-                        }
-                    })
-                    .collect()
-            })
-            .unwrap_or_default();
-
-        Ok(CertificateInfo {
-            filename: filename.to_string(),
-            pem: Some(cert_pem),
-            subject,
-            issuer,
-            fingerprint: Some(fingerprint),
-            public_key_bits: Some(pubkey.bits()),
-            notbefore: asn1_time_to_unix(x509.not_before()).ok(),
-            notafter: asn1_time_to_unix(x509.not_after()).ok(),
-            public_key_type,
-            san,
-        })
-    }
-
-    /// Check if the certificate is expired at or after a specific unix epoch.
-    pub fn is_expired_after_epoch(&self, epoch: i64) -> Result<bool, Error> {
-        if let Some(notafter) = self.notafter {
-            Ok(notafter < epoch)
-        } else {
-            Ok(false)
-        }
-    }
-}
-
-fn x509name_to_string(name: &openssl::x509::X509NameRef) -> Result<String, Error> {
-    let mut parts = Vec::new();
-    for entry in name.entries() {
-        parts.push(format!(
-            "{} = {}",
-            entry.object().nid().short_name()?,
-            entry.data().as_utf8()?
-        ));
-    }
-    Ok(parts.join(", "))
-}
-
-// C type:
-#[allow(non_camel_case_types)]
-type ASN1_TIME = <openssl::asn1::Asn1TimeRef as ForeignTypeRef>::CType;
-
-unsafe extern "C" {
-    fn ASN1_TIME_to_tm(s: *const ASN1_TIME, tm: *mut libc::tm) -> libc::c_int;
-}
-
-fn asn1_time_to_unix(time: &openssl::asn1::Asn1TimeRef) -> Result<i64, Error> {
-    let mut c_tm = MaybeUninit::<libc::tm>::uninit();
-    let rc = unsafe { ASN1_TIME_to_tm(time.as_ptr(), c_tm.as_mut_ptr()) };
-    if rc != 1 {
-        bail!("failed to parse ASN1 time");
-    }
-    let mut c_tm = unsafe { c_tm.assume_init() };
-    proxmox_time::timegm(&mut c_tm)
-}
diff --git a/proxmox-acme-api/src/types.rs b/proxmox-acme-api/src/types.rs
index 934e6ece..15b59908 100644
--- a/proxmox-acme-api/src/types.rs
+++ b/proxmox-acme-api/src/types.rs
@@ -11,56 +11,7 @@ use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema, Updater, ap
 
 use proxmox_acme::types::AccountData as AcmeAccountData;
 
-#[api(
-    properties: {
-        san: {
-            type: Array,
-            items: {
-                description: "A SubjectAlternateName entry.",
-                type: String,
-            },
-        },
-    },
-)]
-/// Certificate information.
-#[derive(PartialEq, Clone, Deserialize, Serialize)]
-#[serde(rename_all = "kebab-case")]
-pub struct CertificateInfo {
-    /// Certificate file name.
-    pub filename: String,
-
-    /// Certificate subject name.
-    pub subject: String,
-
-    /// List of certificate's SubjectAlternativeName entries.
-    pub san: Vec<String>,
-
-    /// Certificate issuer name.
-    pub issuer: String,
-
-    /// Certificate's notBefore timestamp (UNIX epoch).
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub notbefore: Option<i64>,
-
-    /// Certificate's notAfter timestamp (UNIX epoch).
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub notafter: Option<i64>,
-
-    /// Certificate in PEM format.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub pem: Option<String>,
-
-    /// Certificate's public key algorithm.
-    pub public_key_type: String,
-
-    /// Certificate's public key size if available.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub public_key_bits: Option<u32>,
-
-    /// The SSL Fingerprint.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub fingerprint: Option<String>,
-}
+pub use proxmox_tls_certificates::CertificateInfo;
 
 proxmox_schema::api_string_type! {
     #[api(format: &SAFE_ID_FORMAT)]
diff --git a/proxmox-tls-certificates/Cargo.toml b/proxmox-tls-certificates/Cargo.toml
new file mode 100644
index 00000000..105657f2
--- /dev/null
+++ b/proxmox-tls-certificates/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "proxmox-tls-certificates"
+version = "1.0.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+anyhow = { 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" ] }
+
+proxmox-schema = { workspace = true, features = [ "api-macro" ] }
+proxmox-sys = { workspace = true, optional = true }
+proxmox-time = { workspace = true, optional = true }
+proxmox-uuid = { workspace = true, optional = true }
+
+[features]
+default = []
+impl = [
+    "dep:anyhow",
+    "dep:const_format",
+    "dep:foreign-types",
+    "dep:hex",
+    "dep:libc",
+    "dep:openssl",
+
+    "dep:proxmox-time",
+    "dep:proxmox-uuid",
+    "dep:proxmox-sys",
+]
diff --git a/proxmox-tls-certificates/src/lib.rs b/proxmox-tls-certificates/src/lib.rs
new file mode 100644
index 00000000..454c700f
--- /dev/null
+++ b/proxmox-tls-certificates/src/lib.rs
@@ -0,0 +1,7 @@
+mod types;
+pub use types::CertificateInfo;
+
+#[cfg(feature = "impl")]
+mod util;
+#[cfg(feature = "impl")]
+pub use util::create_self_signed_cert;
diff --git a/proxmox-tls-certificates/src/types.rs b/proxmox-tls-certificates/src/types.rs
new file mode 100644
index 00000000..7ca2b004
--- /dev/null
+++ b/proxmox-tls-certificates/src/types.rs
@@ -0,0 +1,54 @@
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::api;
+
+#[api(
+    properties: {
+        san: {
+            type: Array,
+            items: {
+                description: "A SubjectAlternateName entry.",
+                type: String,
+            },
+        },
+    },
+)]
+/// Certificate information.
+#[derive(PartialEq, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct CertificateInfo {
+    /// Certificate file name.
+    pub filename: String,
+
+    /// Certificate subject name.
+    pub subject: String,
+
+    /// List of certificate's SubjectAlternativeName entries.
+    pub san: Vec<String>,
+
+    /// Certificate issuer name.
+    pub issuer: String,
+
+    /// Certificate's notBefore timestamp (UNIX epoch).
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub notbefore: Option<i64>,
+
+    /// Certificate's notAfter timestamp (UNIX epoch).
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub notafter: Option<i64>,
+
+    /// Certificate in PEM format.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pem: Option<String>,
+
+    /// Certificate's public key algorithm.
+    pub public_key_type: String,
+
+    /// Certificate's public key size if available.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub public_key_bits: Option<u32>,
+
+    /// The SSL Fingerprint.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub fingerprint: Option<String>,
+}
diff --git a/proxmox-tls-certificates/src/util.rs b/proxmox-tls-certificates/src/util.rs
new file mode 100644
index 00000000..2eac2252
--- /dev/null
+++ b/proxmox-tls-certificates/src/util.rs
@@ -0,0 +1,209 @@
+use std::mem::MaybeUninit;
+
+use anyhow::{Error, bail, format_err};
+use foreign_types::ForeignTypeRef;
+use openssl::pkey::{PKey, Private};
+use openssl::rsa::Rsa;
+use openssl::x509::{X509, X509Builder};
+
+use crate::CertificateInfo;
+
+/// 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.
+/// * `nodename`: The name of the node where the certificate will be used. The common name and
+///   subject alternative names will be set based on this value.
+/// * `domain`: The optional domain which the node is in. If set, it will be appended to `nodename`
+///   to derive an FQDN as common and subject alternative name.
+pub fn create_self_signed_cert(
+    product_name: &str,
+    nodename: &str,
+    domain: Option<&str>,
+) -> Result<(PKey<Private>, X509), Error> {
+    let rsa = Rsa::generate(4096).unwrap();
+
+    let mut x509 = X509Builder::new()?;
+
+    x509.set_version(2)?;
+
+    let today = openssl::asn1::Asn1Time::days_from_now(0)?;
+    x509.set_not_before(&today)?;
+    let expire = openssl::asn1::Asn1Time::days_from_now(365 * 10)?;
+    x509.set_not_after(&expire)?;
+
+    let mut fqdn = nodename.to_owned();
+
+    if let Some(domain) = domain {
+        fqdn.push('.');
+        fqdn.push_str(domain);
+    }
+
+    // we try to generate an unique 'subject' to avoid browser problems
+    //(reused serial numbers, ..)
+    let uuid = proxmox_uuid::Uuid::generate();
+
+    let mut subject_name = openssl::x509::X509NameBuilder::new()?;
+    subject_name.append_entry_by_text("O", product_name)?;
+    subject_name.append_entry_by_text("OU", &format!("{uuid:X}"))?;
+    subject_name.append_entry_by_text("CN", &fqdn)?;
+    let subject_name = subject_name.build();
+
+    x509.set_subject_name(&subject_name)?;
+    x509.set_issuer_name(&subject_name)?;
+
+    let bc = openssl::x509::extension::BasicConstraints::new(); // CA = false
+    let bc = bc.build()?;
+    x509.append_extension(bc)?;
+
+    let usage = openssl::x509::extension::ExtendedKeyUsage::new()
+        .server_auth()
+        .build()?;
+    x509.append_extension(usage)?;
+
+    let context = x509.x509v3_context(None, None);
+
+    let mut alt_names = openssl::x509::extension::SubjectAlternativeName::new();
+
+    alt_names.ip("127.0.0.1");
+    alt_names.ip("::1");
+
+    alt_names.dns("localhost");
+
+    if nodename != "localhost" {
+        alt_names.dns(nodename);
+    }
+    if nodename != fqdn {
+        alt_names.dns(&fqdn);
+    }
+
+    let alt_names = alt_names.build(&context)?;
+
+    x509.append_extension(alt_names)?;
+
+    let pub_pem = rsa.public_key_to_pem()?;
+    let pubkey = PKey::public_key_from_pem(&pub_pem)?;
+
+    x509.set_pubkey(&pubkey)?;
+
+    let context = x509.x509v3_context(None, None);
+    let ext = openssl::x509::extension::SubjectKeyIdentifier::new().build(&context)?;
+    x509.append_extension(ext)?;
+
+    let context = x509.x509v3_context(None, None);
+    let ext = openssl::x509::extension::AuthorityKeyIdentifier::new()
+        .keyid(true)
+        .build(&context)?;
+    x509.append_extension(ext)?;
+
+    let privkey = PKey::from_rsa(rsa)?;
+
+    x509.sign(&privkey, openssl::hash::MessageDigest::sha256())?;
+
+    Ok((privkey, x509.build()))
+}
+
+impl CertificateInfo {
+    /// Parse a PEM-encoded file to a `CertificateInfo` struct.
+    ///
+    /// * `filename`: The location of the PEM file.
+    /// * `cert_pem`: The PEM data as bytes.
+    pub fn from_pem(filename: &str, cert_pem: &[u8]) -> Result<Self, Error> {
+        let x509 = openssl::x509::X509::from_pem(cert_pem)?;
+
+        let cert_pem = String::from_utf8(cert_pem.to_vec())
+            .map_err(|_| format_err!("certificate in {:?} is not a valid PEM file", filename))?;
+
+        let pubkey = x509.public_key()?;
+
+        let subject = x509name_to_string(x509.subject_name())?;
+        let issuer = x509name_to_string(x509.issuer_name())?;
+
+        let fingerprint = x509.digest(openssl::hash::MessageDigest::sha256())?;
+        let fingerprint = hex::encode(fingerprint)
+            .as_bytes()
+            .chunks(2)
+            .map(|v| std::str::from_utf8(v).unwrap())
+            .collect::<Vec<&str>>()
+            .join(":");
+
+        let public_key_type = openssl::nid::Nid::from_raw(pubkey.id().as_raw())
+            .long_name()
+            .unwrap_or("<unsupported key type>")
+            .to_owned();
+
+        let san = x509
+            .subject_alt_names()
+            .map(|san| {
+                san.into_iter()
+                    .filter_map(|name| {
+                        // this is not actually a map and we don't want to break the pattern
+                        #[allow(clippy::manual_map)]
+                        if let Some(name) = name.dnsname() {
+                            Some(format!("DNS: {name}"))
+                        } else if let Some(ip) = name.ipaddress() {
+                            Some(format!("IP: {ip:?}"))
+                        } else if let Some(email) = name.email() {
+                            Some(format!("EMAIL: {email}"))
+                        } else if let Some(uri) = name.uri() {
+                            Some(format!("URI: {uri}"))
+                        } else {
+                            None
+                        }
+                    })
+                    .collect()
+            })
+            .unwrap_or_default();
+
+        Ok(CertificateInfo {
+            filename: filename.to_string(),
+            pem: Some(cert_pem),
+            subject,
+            issuer,
+            fingerprint: Some(fingerprint),
+            public_key_bits: Some(pubkey.bits()),
+            notbefore: asn1_time_to_unix(x509.not_before()).ok(),
+            notafter: asn1_time_to_unix(x509.not_after()).ok(),
+            public_key_type,
+            san,
+        })
+    }
+
+    /// Check if the certificate is expired at or after a specific unix epoch.
+    pub fn is_expired_after_epoch(&self, epoch: i64) -> Result<bool, Error> {
+        if let Some(notafter) = self.notafter {
+            Ok(notafter < epoch)
+        } else {
+            Ok(false)
+        }
+    }
+}
+
+fn x509name_to_string(name: &openssl::x509::X509NameRef) -> Result<String, Error> {
+    let mut parts = Vec::new();
+    for entry in name.entries() {
+        parts.push(format!(
+            "{} = {}",
+            entry.object().nid().short_name()?,
+            entry.data().as_utf8()?
+        ));
+    }
+    Ok(parts.join(", "))
+}
+
+// C type:
+#[allow(non_camel_case_types)]
+type ASN1_TIME = <openssl::asn1::Asn1TimeRef as ForeignTypeRef>::CType;
+
+unsafe extern "C" {
+    fn ASN1_TIME_to_tm(s: *const ASN1_TIME, tm: *mut libc::tm) -> libc::c_int;
+}
+
+fn asn1_time_to_unix(time: &openssl::asn1::Asn1TimeRef) -> Result<i64, Error> {
+    let mut c_tm = MaybeUninit::<libc::tm>::uninit();
+    let rc = unsafe { ASN1_TIME_to_tm(time.as_ptr(), c_tm.as_mut_ptr()) };
+    if rc != 1 {
+        bail!("failed to parse ASN1 time");
+    }
+    let mut c_tm = unsafe { c_tm.assume_init() };
+    proxmox_time::timegm(&mut c_tm)
+}
-- 
2.47.3





  reply	other threads:[~2026-07-30 13:32 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-30 13:31 [PATCH datacenter-manager/proxmox{,-backup} 00/16] TLS Certificate Rotation Shannon Sterz
2026-07-30 13:31 ` Shannon Sterz [this message]
2026-07-30 13:31 ` [PATCH proxmox 02/16] tls-certificates: add days_valid parameter to create_self_signed_cert Shannon Sterz
2026-07-30 13:31 ` [PATCH proxmox 03/16] tls-certificates: add self_signed_cert_expires_in to check certificates Shannon Sterz
2026-07-30 13:31 ` [PATCH proxmox 04/16] acme-api: stop re-exporting create_self_signed_cert Shannon Sterz
2026-07-30 13:31 ` [PATCH proxmox-backup 05/16] config: use proxmox_tls_certificates for generating self-signed certificates Shannon Sterz
2026-07-30 13:31 ` [PATCH proxmox-backup 06/16] config/server/api: add certificate renewal logic including notifications Shannon Sterz
2026-07-30 13:31 ` [PATCH proxmox-backup 07/16] daily update: warn about excessive self-signed certificate lifetime Shannon Sterz
2026-07-30 13:31 ` [PATCH proxmox-backup 08/16] docs: document force refreshing long-lived certificates Shannon Sterz
2026-07-30 13:31 ` [PATCH proxmox-backup 09/16] backup-manager cli: `cert update` can create auth and csrf key Shannon Sterz
2026-07-30 13:31 ` [PATCH proxmox-backup 10/16] notifications: use `Severity::Error` for acme renewal failures Shannon Sterz
2026-07-30 13:31 ` [PATCH datacenter-manager 11/16] certs: use proxmox-tls-certificates directly, add days_valid paramter Shannon Sterz
2026-07-30 13:31 ` [PATCH datacenter-manager 12/16] api/auth/bin: add certificate renewal logic Shannon Sterz
2026-07-30 13:31 ` [PATCH datacenter-manager 13/16] cli: expose certificate management endpoints via the cli Shannon Sterz
2026-07-30 13:31 ` [PATCH datacenter-manager 14/16] daily-update: warn about certificates with excessive lifetimes Shannon Sterz
2026-07-30 13:31 ` [PATCH datacenter-manager 15/16] docs: add section on forcing a new self-signed certificate Shannon Sterz
2026-07-30 13:31 ` [PATCH datacenter-manager 16/16] docs/certificates: use correct certificate file name Shannon Sterz
2026-07-30 13:44 ` [PATCH datacenter-manager/proxmox{,-backup} 00/16] TLS Certificate Rotation Shannon Sterz

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260730133158.418015-2-s.sterz@proxmox.com \
    --to=s.sterz@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal