diff --git a/Cargo.toml b/Cargo.toml index 9ee8da7a..b3c97808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ crossbeam-channel = "0.5" endian_trait = "0.6" env_logger = "0.10" flate2 = "1.0" +foreign-types = "0.3" form_urlencoded = "1.1" futures = "0.3" handlebars = "3.0" diff --git a/proxmox-acme-api/Cargo.toml b/proxmox-acme-api/Cargo.toml index ad55f644..5ecbc200 100644 --- a/proxmox-acme-api/Cargo.toml +++ b/proxmox-acme-api/Cargo.toml @@ -19,8 +19,12 @@ futures = { workspace = true, optional = true } http = { workspace = true, optional = true } log = { workspace = true, optional = true } nix = { workspace = true, optional = true } +hex = { workspace = true, optional = true } lazy_static = { workspace = true, optional = true } + +libc = { workspace = true, optional = true } openssl = { workspace = true, optional = true } +foreign-types = { workspace = true, optional = true } proxmox-serde.workspace = true @@ -30,6 +34,7 @@ proxmox-router = { workspace = true, optional = true } proxmox-sys = { workspace = true, optional = true } proxmox-schema = { workspace = true, features = ["api-macro", "api-types"] } proxmox-uuid = { workspace = true, optional = true } +proxmox-time = { workspace = true, optional = true } proxmox-acme = { workspace = true, features = ["api-types"] } proxmox-config-digest = { workspace = true, optional = true } proxmox-product-config = { workspace = true, optional = true } @@ -38,12 +43,14 @@ proxmox-product-config = { workspace = true, optional = true } default = [] impl = [ "dep:proxmox-uuid", + "dep:proxmox-time", "dep:proxmox-config-digest", "proxmox-config-digest?/openssl", "dep:proxmox-product-config", "proxmox-acme/impl", "proxmox-acme/async-client", "dep:proxmox-section-config", + "dep:hex", "dep:lazy_static", "dep:log", "dep:nix", @@ -55,5 +62,7 @@ impl = [ "dep:proxmox-rest-server", "dep:proxmox-router", "dep:base64", + "dep:libc", "dep:openssl", + "dep:foreign-types", ] diff --git a/proxmox-acme-api/debian/control b/proxmox-acme-api/debian/control index 83617043..460e76c9 100644 --- a/proxmox-acme-api/debian/control +++ b/proxmox-acme-api/debian/control @@ -58,12 +58,16 @@ Depends: ${misc:Depends}, librust-proxmox-acme-api-dev (= ${binary:Version}), librust-base64-0.13+default-dev, + librust-foreign-types-0.3+default-dev, librust-futures-0.3+default-dev, + librust-hex-0.4+default-dev, librust-http-0.2+default-dev, librust-hyper-0.14+default-dev (>= 0.14.5-~~), librust-lazy-static-1+default-dev (>= 1.4-~~), + librust-libc-0.2+default-dev (>= 0.2.107-~~), librust-log-0.4+default-dev (>= 0.4.17-~~), librust-nix-0.26+default-dev (>= 0.26.1-~~), + librust-openssl-0.10+default-dev, librust-proxmox-acme-0.5+api-types-dev (>= 0.5.2-~~), librust-proxmox-acme-0.5+async-client-dev (>= 0.5.2-~~), librust-proxmox-acme-0.5+impl-dev (>= 0.5.2-~~), @@ -74,6 +78,8 @@ Depends: librust-proxmox-router-2+default-dev (>= 2.1.3-~~), librust-proxmox-section-config-2+default-dev, librust-proxmox-sys-0.5+default-dev (>= 0.5.5-~~), + librust-proxmox-time-1+default-dev (>= 1.1.6-~~), + librust-proxmox-uuid-1+default-dev (>= 1.0.1-~~), librust-tokio-1+default-dev (>= 1.6-~~), librust-tokio-1+fs-dev (>= 1.6-~~) Provides: diff --git a/proxmox-acme-api/src/certificate_helpers.rs b/proxmox-acme-api/src/certificate_helpers.rs index ae9463a1..2b82d476 100644 --- a/proxmox-acme-api/src/certificate_helpers.rs +++ b/proxmox-acme-api/src/certificate_helpers.rs @@ -1,6 +1,9 @@ +use std::mem::MaybeUninit; use std::sync::Arc; use std::time::Duration; +use foreign_types::ForeignTypeRef; + use anyhow::{bail, format_err, Error}; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; @@ -11,6 +14,7 @@ use proxmox_rest_server::WorkerTask; use proxmox_sys::{task_log, task_warn}; use crate::types::{AcmeConfig, AcmeDomain}; +use crate::CertificateInfo; pub async fn revoke_certificate(acme_config: &AcmeConfig, certificate: &[u8]) -> Result<(), Error> { let mut acme = super::account_config::load_account_config(&acme_config.account) @@ -302,3 +306,92 @@ pub fn create_self_signed_cert( Ok((privkey, x509.build())) } + +impl CertificateInfo { + pub fn from_pem(filename: &str, cert_pem: &[u8]) -> Result { + 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::>() + .join(":"); + + let public_key_type = openssl::nid::Nid::from_raw(pubkey.id().as_raw()) + .long_name() + .unwrap_or("") + .to_owned(); + + let san = x509 + .subject_alt_names() + .map(|san| { + san.into_iter() + // FIXME: Support `.ipaddress()`? + .filter_map(|name| name.dnsname().map(str::to_owned)) + .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 { + if let Some(notafter) = self.notafter { + Ok(notafter < epoch) + } else { + Ok(false) + } + } +} + +fn x509name_to_string(name: &openssl::x509::X509NameRef) -> Result { + 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 = ::CType; + +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 { + let mut c_tm = MaybeUninit::::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) +}