acme-api: add function to extract certificate data from .pem data

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
This commit is contained in:
Dietmar Maurer 2024-06-06 13:42:47 +02:00
parent fcaa4f6758
commit 7e4121d26e
4 changed files with 109 additions and 0 deletions

View File

@ -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"

View File

@ -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",
]

View File

@ -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:

View File

@ -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<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()
// 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<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;
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)
}