use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeZone, Utc};
use foreign_types::ForeignType;
use openssl::asn1::{Asn1Object, Asn1OctetString, Asn1Time};
use openssl::bn::BigNum;
use openssl::ec::{EcGroup, EcKey};
use openssl::error::ErrorStack;
use openssl::hash::{MessageDigest, hash};
use openssl::nid::Nid;
use openssl::pkey::{Id, PKey, Private};
use openssl::rsa::Rsa;
use openssl::stack::Stack;
use openssl::x509::extension::{
AuthorityKeyIdentifier, BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName,
};
use openssl::x509::{
X509, X509Builder, X509Extension, X509NameBuilder, X509Req, X509ReqBuilder, X509StoreContext,
store::X509StoreBuilder,
};
use std::collections::{HashMap, HashSet};
use std::fs::{File, create_dir_all};
use std::io::Write;
use std::path::Path;
use x509_parser::certification_request::X509CertificationRequest;
use x509_parser::extensions::ParsedExtension;
use x509_parser::parse_x509_certificate;
use x509_parser::prelude::FromDer;
unsafe extern "C" {
pub fn X509_sign(
x: *mut openssl_sys::X509,
pkey: *mut openssl_sys::EVP_PKEY,
md: *const openssl_sys::EVP_MD,
) -> ::std::os::raw::c_int;
pub fn X509_sign_ctx(
x: *mut openssl_sys::X509,
ctx: *mut openssl_sys::EVP_MD_CTX,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn X509_REQ_sign(
req: *mut openssl_sys::X509_REQ,
pkey: *mut openssl_sys::EVP_PKEY,
md: *const openssl_sys::EVP_MD,
) -> ::std::os::raw::c_int;
pub fn X509_REQ_sign_ctx(
req: *mut openssl_sys::X509_REQ,
ctx: *mut openssl_sys::EVP_MD_CTX,
) -> ::std::os::raw::c_int;
}
fn sign_certificate_digestless(
cert: &X509,
pkey: &PKey<openssl::pkey::Private>,
) -> Result<(), String> {
if !is_digestless_key(pkey) {
return Err("sign_certificate_digestless called with non-digestless key".to_string());
}
let cert_ptr = cert.as_ptr();
let pkey_ptr = pkey.as_ptr();
if pkey.id() == Id::ED25519 {
let result = unsafe { X509_sign(cert_ptr, pkey_ptr, std::ptr::null()) };
return if result > 0 {
Ok(())
} else {
Err("Failed to sign certificate with Ed25519".to_string())
};
}
let result = unsafe {
let ctx = openssl_sys::EVP_MD_CTX_new();
if ctx.is_null() {
return Err("EVP_MD_CTX_new returned NULL".to_string());
}
let init = openssl_sys::EVP_DigestSignInit(
ctx,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null_mut(),
pkey_ptr,
);
if init <= 0 {
openssl_sys::EVP_MD_CTX_free(ctx);
return Err("EVP_DigestSignInit failed for PQC key".to_string());
}
let rc = X509_sign_ctx(cert_ptr, ctx);
openssl_sys::EVP_MD_CTX_free(ctx);
rc
};
if result > 0 {
Ok(())
} else {
Err("X509_sign_ctx failed for PQC key".to_string())
}
}
fn sign_x509_req_digestless(req: &X509Req, pkey: &PKey<Private>) -> Result<(), String> {
if !is_digestless_key(pkey) {
return Err("sign_x509_req_digestless called with non-digestless key".to_string());
}
let req_ptr = req.as_ptr();
let pkey_ptr = pkey.as_ptr();
if pkey.id() == Id::ED25519 {
let result = unsafe { X509_REQ_sign(req_ptr, pkey_ptr, std::ptr::null()) };
return if result > 0 {
Ok(())
} else {
Err("Failed to sign X509Req with Ed25519".to_string())
};
}
let result = unsafe {
let ctx = openssl_sys::EVP_MD_CTX_new();
if ctx.is_null() {
return Err("EVP_MD_CTX_new returned NULL".to_string());
}
let init = openssl_sys::EVP_DigestSignInit(
ctx,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null_mut(),
pkey_ptr,
);
if init <= 0 {
openssl_sys::EVP_MD_CTX_free(ctx);
return Err("EVP_DigestSignInit failed for PQC key".to_string());
}
let rc = X509_REQ_sign_ctx(req_ptr, ctx);
openssl_sys::EVP_MD_CTX_free(ctx);
rc
};
if result > 0 {
Ok(())
} else {
Err("X509_REQ_sign_ctx failed for PQC key".to_string())
}
}
pub(crate) fn is_digestless_key(pkey: &PKey<Private>) -> bool {
if pkey.id() == Id::ED25519 {
return true;
}
#[cfg(feature = "pqc")]
{
return is_pqc_pkey(pkey);
}
#[allow(unreachable_code)]
false
}
#[cfg(feature = "pqc")]
fn is_pqc_pkey(pkey: &PKey<Private>) -> bool {
use std::ffi::CString;
use std::sync::OnceLock;
static NAMES: OnceLock<[CString; 6]> = OnceLock::new();
let names = NAMES.get_or_init(|| {
[
CString::new("ML-DSA-44").unwrap(),
CString::new("ML-DSA-65").unwrap(),
CString::new("ML-DSA-87").unwrap(),
CString::new("SLH-DSA-SHA2-128s").unwrap(),
CString::new("SLH-DSA-SHA2-192s").unwrap(),
CString::new("SLH-DSA-SHA2-256s").unwrap(),
]
});
use foreign_types::ForeignType;
let ptr = pkey.as_ptr();
names
.iter()
.any(|n| unsafe { pqc::EVP_PKEY_is_a(ptr, n.as_ptr()) } == 1)
}
#[cfg(feature = "pqc")]
mod pqc {
use foreign_types::ForeignType;
use openssl::error::ErrorStack;
use openssl::pkey::{PKey, Private};
use std::ffi::CString;
unsafe extern "C" {
fn EVP_PKEY_CTX_new_from_name(
libctx: *mut std::ffi::c_void,
name: *const std::os::raw::c_char,
propquery: *const std::os::raw::c_char,
) -> *mut openssl_sys::EVP_PKEY_CTX;
fn EVP_PKEY_keygen_init(ctx: *mut openssl_sys::EVP_PKEY_CTX) -> std::os::raw::c_int;
fn EVP_PKEY_generate(
ctx: *mut openssl_sys::EVP_PKEY_CTX,
ppkey: *mut *mut openssl_sys::EVP_PKEY,
) -> std::os::raw::c_int;
fn EVP_PKEY_CTX_free(ctx: *mut openssl_sys::EVP_PKEY_CTX);
pub fn EVP_PKEY_is_a(
pkey: *mut openssl_sys::EVP_PKEY,
name: *const std::os::raw::c_char,
) -> std::os::raw::c_int;
}
struct PkeyCtx(*mut openssl_sys::EVP_PKEY_CTX);
impl Drop for PkeyCtx {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { EVP_PKEY_CTX_free(self.0) }
}
}
}
pub(super) fn generate_pqc_key(alg_name: &str) -> Result<PKey<Private>, ErrorStack> {
let cname = CString::new(alg_name).expect("alg_name contains interior NUL");
let ctx_ptr = unsafe {
EVP_PKEY_CTX_new_from_name(std::ptr::null_mut(), cname.as_ptr(), std::ptr::null())
};
if ctx_ptr.is_null() {
return Err(ErrorStack::get());
}
let ctx = PkeyCtx(ctx_ptr);
if unsafe { EVP_PKEY_keygen_init(ctx.0) } <= 0 {
return Err(ErrorStack::get());
}
let mut pkey_ptr: *mut openssl_sys::EVP_PKEY = std::ptr::null_mut();
if unsafe { EVP_PKEY_generate(ctx.0, &mut pkey_ptr) } <= 0 {
return Err(ErrorStack::get());
}
if pkey_ptr.is_null() {
return Err(ErrorStack::get());
}
Ok(unsafe { PKey::<Private>::from_ptr(pkey_ptr) })
}
}
#[cfg(feature = "pqc")]
use pqc::generate_pqc_key;
macro_rules! vec_str_to_hs {
($vec:expr) => {
$vec.iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>()
};
}
#[derive(Debug, Clone, PartialEq)]
pub enum KeyType {
RSA2048,
RSA4096,
P224,
P256,
P384,
P521,
Ed25519,
#[cfg(feature = "pqc")]
MlDsa44,
#[cfg(feature = "pqc")]
MlDsa65,
#[cfg(feature = "pqc")]
MlDsa87,
#[cfg(feature = "pqc")]
SlhDsaSha2_128s,
#[cfg(feature = "pqc")]
SlhDsaSha2_192s,
#[cfg(feature = "pqc")]
SlhDsaSha2_256s,
}
#[derive(Debug, Clone)]
pub enum HashAlg {
SHA1,
SHA256,
SHA384,
SHA512,
}
#[allow(non_camel_case_types)]
#[derive(Hash, Eq, PartialEq, Debug, Clone)]
pub enum Usage {
certsign,
crlsign,
encipherment,
clientauth,
serverauth,
signature,
contentcommitment,
}
pub trait X509Parts {
fn get_pem(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>>;
fn get_private_key(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>>;
fn pem_extension(&self) -> &'static str;
}
pub trait X509Common {
fn save<P: AsRef<Path>, F: AsRef<Path>>(
&self,
path: P,
filename: F,
) -> Result<(), Box<dyn std::error::Error>>;
}
impl<T: X509Parts> X509Common for T {
fn save<P: AsRef<Path>, F: AsRef<Path>>(
&self,
path: P,
filename: F,
) -> Result<(), Box<dyn std::error::Error>> {
create_dir_all(&path)?;
let os_file = filename
.as_ref()
.file_name()
.ok_or("Failed to extract file name")?;
let write_file = |suffix: &str, content: &[u8]| -> Result<(), Box<dyn std::error::Error>> {
let mut new_name = os_file.to_os_string();
new_name.push(suffix);
let full_path = path.as_ref().join(new_name);
let mut file = File::create(full_path)?;
file.write_all(content)?;
Ok(())
};
if let Ok(ref key) = self.get_private_key() {
write_file("_pkey.pem", key)?;
}
write_file(self.pem_extension(), &self.get_pem()?)?;
Ok(())
}
}
pub struct Csr {
pub csr: X509Req,
pub pkey: Option<PKey<Private>>,
}
impl X509Parts for Csr {
fn get_pem(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
Ok(self.csr.to_pem()?)
}
fn get_private_key(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
match self.pkey {
Some(ref pkey) => Ok(pkey.private_key_to_pem_pkcs8()?),
_ => Err("No private key found".into()),
}
}
fn pem_extension(&self) -> &'static str {
"_csr.pem"
}
}
pub trait CsrX509Common: X509Common {}
impl CsrX509Common for Csr {}
pub struct CsrOptions {
valid_to: Asn1Time,
valid_from: Asn1Time,
ca: bool,
}
impl Default for CsrOptions {
fn default() -> Self {
Self::new()
}
}
impl CsrOptions {
pub fn new() -> Self {
Self {
ca: false,
valid_from: Asn1Time::days_from_now(0).unwrap(), valid_to: Asn1Time::days_from_now(365).unwrap(), }
}
pub fn valid_from(mut self, valid_from: &str) -> Self {
self.valid_from =
create_asn1_time_from_date(valid_from).expect("Failed to parse valid_from date");
self
}
pub fn valid_to(mut self, valid_to: &str) -> Self {
self.valid_to =
create_asn1_time_from_date(valid_to).expect("Failed to parse valid_to date");
self
}
pub fn is_ca(mut self, ca: bool) -> Self {
self.ca = ca;
self
}
}
impl Csr {
pub fn load_csr<C: AsRef<Path>>(csr_pem_file: C) -> Result<Self, Box<dyn std::error::Error>> {
let cert_pem = std::fs::read(csr_pem_file)?;
let cs_req = X509Req::from_pem(&cert_pem)?;
Ok(Self {
csr: cs_req,
pkey: None,
})
}
pub fn build_signed_certificate(
&self,
signer: &Certificate,
options: CsrOptions,
) -> Result<Certificate, Box<dyn std::error::Error>> {
let can_sign = can_sign_cert(&signer.x509)?;
if !can_sign {
let err = format!(
"Trying to sign with non CA and/or no key usage that allow signing for signer certificate:{:?}",
signer.x509.issuer_name()
);
return Err(err.into());
}
let mut builder = X509Builder::new()?;
builder.set_version(2)?;
builder.set_subject_name(self.csr.subject_name())?;
builder.set_issuer_name(signer.x509.subject_name())?;
let csr_public_key = self.csr.public_key()?;
builder.set_pubkey(&csr_public_key)?;
let der = self.csr.to_der()?;
let parsed_csr = X509CertificationRequest::from_der(&der)?;
let req_ext = parsed_csr.1.requested_extensions();
let mut any_key_used = false;
if let Some(exts) = req_ext {
for ext in exts {
match ext {
ParsedExtension::KeyUsage(ku) => {
any_key_used = true;
let mut cert_sign_added = false;
let mut crl_sign_added = false;
let mut usage = openssl::x509::extension::KeyUsage::new();
if ku.digital_signature() {
usage.digital_signature();
}
if ku.key_encipherment() {
usage.key_encipherment();
}
if ku.key_cert_sign() {
cert_sign_added = true;
usage.key_cert_sign();
}
if ku.non_repudiation() {
usage.non_repudiation();
}
if ku.crl_sign() {
crl_sign_added = true;
usage.crl_sign();
}
if options.ca {
if !cert_sign_added {
usage.key_cert_sign();
}
if !crl_sign_added {
usage.crl_sign();
}
}
builder.append_extension(usage.build()?)?;
}
ParsedExtension::ExtendedKeyUsage(eku) => {
let mut ext = openssl::x509::extension::ExtendedKeyUsage::new();
if eku.server_auth {
ext.server_auth();
}
if eku.client_auth {
ext.client_auth();
}
if eku.code_signing {
ext.code_signing();
}
if eku.email_protection {
ext.email_protection();
}
builder.append_extension(ext.build()?)?;
}
ParsedExtension::SubjectAlternativeName(san) => {
let mut openssl_san =
openssl::x509::extension::SubjectAlternativeName::new();
for name in &san.general_names {
if let x509_parser::extensions::GeneralName::DNSName(dns) = name {
openssl_san.dns(dns);
}
}
builder.append_extension(
openssl_san.build(&builder.x509v3_context(None, None))?,
)?;
}
_ => {
println!("Unsupported extension: {:?}", ext);
}
}
}
}
if options.ca {
builder.append_extension(BasicConstraints::new().ca().critical().build()?)?;
if !any_key_used {
let key_usage = KeyUsage::new().key_cert_sign().crl_sign().build().unwrap();
builder.append_extension(key_usage)?;
}
} else {
builder.append_extension(BasicConstraints::new().build()?)?;
}
builder.set_not_before(&options.valid_from)?;
builder.set_not_after(&options.valid_to)?;
let serial_number = {
let mut serial = BigNum::new()?;
serial.rand(159, openssl::bn::MsbOption::MAYBE_ZERO, false)?;
serial.to_asn1_integer()?
};
builder.set_serial_number(&serial_number)?;
if signer.x509.subject_key_id().is_some() {
let aki = AuthorityKeyIdentifier::new()
.keyid(true)
.issuer(false)
.build(&builder.x509v3_context(Some(&signer.x509), None))?;
builder.append_extension(aki)?;
}
let oid = Asn1Object::from_str("2.5.29.14")?; let pubkey_der = self.csr.public_key().unwrap().public_key_to_der()?;
let ski_hash = hash(MessageDigest::sha1(), &pubkey_der)?;
let der_encoded = yasna::construct_der(|writer| {
writer.write_bytes(ski_hash.as_ref());
});
let ski_asn1 = Asn1OctetString::new_from_bytes(&der_encoded)?;
let ext = X509Extension::new_from_der(oid.as_ref(), false, &ski_asn1)?;
builder.append_extension(ext)?;
let cert: X509 = if is_digestless_key(signer.pkey.as_ref().unwrap()) {
let builder_cert = builder.build();
sign_certificate_digestless(&builder_cert, signer.pkey.as_ref().unwrap())
.map_err(|e| format!("Failed to sign certificate with digestless key: {}", e))?;
builder_cert
} else {
builder.sign(signer.pkey.as_ref().unwrap(), MessageDigest::sha256())?;
builder.build()
};
Ok(Certificate {
x509: cert,
pkey: None,
})
}
}
#[derive(Clone)]
pub struct Certificate {
pub x509: X509,
pub pkey: Option<PKey<Private>>,
}
impl X509Parts for Certificate {
fn get_pem(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
Ok(self.x509.to_pem()?)
}
fn get_private_key(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
match self.pkey {
Some(ref pkey) => Ok(pkey.private_key_to_pem_pkcs8()?),
_ => Err("No private key found".into()),
}
}
fn pem_extension(&self) -> &'static str {
"_cert.pem"
}
}
pub trait CertificateX509Common: X509Common {}
impl CertificateX509Common for Certificate {}
impl Certificate {
pub fn load_cert_and_key<C: AsRef<Path>, K: AsRef<Path>>(
cert_pem_file: C,
key_pem_file: K,
) -> Result<Self, Box<dyn std::error::Error>> {
let cert_pem = std::fs::read(cert_pem_file)?;
let key_pem = std::fs::read(key_pem_file)?;
let cert = X509::from_pem(&cert_pem)?;
let pkey = PKey::private_key_from_pem(&key_pem)?;
Ok(Self {
x509: cert,
pkey: Some(pkey),
})
}
}
pub trait BuilderCommon {
fn set_common_name(&mut self, name: &str);
fn set_signer(&mut self, signer: &str);
fn set_country_name(&mut self, name: &str);
fn set_state_province(&mut self, name: &str);
fn set_organization(&mut self, name: &str);
fn set_organization_unit(&mut self, name: &str);
fn set_alternative_names(&mut self, alternative_names: Vec<&str>);
fn set_locality_time(&mut self, locality_time: &str);
fn set_key_type(&mut self, key_type: KeyType);
fn set_signature_alg(&mut self, signature_alg: HashAlg);
fn set_key_usage(&mut self, key_usage: HashSet<Usage>);
}
#[derive(Debug)]
pub struct BuilderFields {
common_name: String,
signer: Option<String>, alternative_names: HashSet<String>,
organization_unit: String,
country_name: String,
state_province: String,
organization: String,
locality_time: String,
key_type: Option<KeyType>,
signature_alg: Option<HashAlg>,
usage: Option<HashSet<Usage>>,
}
impl BuilderCommon for BuilderFields {
fn set_common_name(&mut self, common_name: &str) {
self.common_name = common_name.into();
self.alternative_names.insert(String::from(common_name));
}
fn set_alternative_names(&mut self, alternative_names: Vec<&str>) {
self.alternative_names
.extend(vec_str_to_hs!(alternative_names));
}
fn set_signer(&mut self, signer: &str) {
self.signer = Some(signer.into());
}
fn set_country_name(&mut self, country_name: &str) {
self.country_name = country_name.into();
}
fn set_state_province(&mut self, state_province: &str) {
self.state_province = state_province.into();
}
fn set_organization(&mut self, organization: &str) {
self.organization = organization.into();
}
fn set_organization_unit(&mut self, organization_unit: &str) {
self.organization_unit = organization_unit.into();
}
fn set_locality_time(&mut self, locality_time: &str) {
self.locality_time = locality_time.into();
}
fn set_key_type(&mut self, key_type: KeyType) {
self.key_type = Some(key_type);
}
fn set_signature_alg(&mut self, signature_alg: HashAlg) {
self.signature_alg = Some(signature_alg);
}
fn set_key_usage(&mut self, key_usage: HashSet<Usage>) {
match &mut self.usage {
Some(existing_usage) => {
existing_usage.extend(key_usage);
}
None => {
self.usage = Some(key_usage);
}
};
}
}
impl Default for BuilderFields {
fn default() -> Self {
Self {
common_name: Default::default(),
signer: Default::default(),
alternative_names: Default::default(),
country_name: Default::default(),
state_province: Default::default(),
organization: Default::default(),
organization_unit: Default::default(),
locality_time: Default::default(),
key_type: Default::default(),
signature_alg: Default::default(),
usage: Default::default(),
}
}
}
pub trait UseesBuilderFields: Sized {
fn fields_mut(&mut self) -> &mut BuilderFields;
fn common_name(mut self, common_name: &str) -> Self {
self.fields_mut().set_common_name(common_name);
self
}
fn signer(mut self, signer: &str) -> Self {
self.fields_mut().set_signer(signer);
self
}
fn alternative_names(mut self, alternative_names: Vec<&str>) -> Self {
self.fields_mut().set_alternative_names(alternative_names);
self
}
fn country_name(mut self, country_name: &str) -> Self {
self.fields_mut().set_country_name(country_name);
self
}
fn state_province(mut self, state_province: &str) -> Self {
self.fields_mut().set_state_province(state_province);
self
}
fn organization(mut self, organization: &str) -> Self {
self.fields_mut().set_organization(organization);
self
}
fn locality_time(mut self, locality_time: &str) -> Self {
self.fields_mut().set_locality_time(locality_time);
self
}
fn key_type(mut self, key_type: KeyType) -> Self {
self.fields_mut().set_key_type(key_type);
self
}
fn signature_alg(mut self, signature_alg: HashAlg) -> Self {
self.fields_mut().set_signature_alg(signature_alg);
self
}
fn key_usage(mut self, key_usage: HashSet<Usage>) -> Self {
self.fields_mut().set_key_usage(key_usage);
self
}
}
pub struct CertBuilder {
fields: BuilderFields,
valid_from: Asn1Time,
valid_to: Asn1Time,
ca: bool,
}
impl UseesBuilderFields for CertBuilder {
fn fields_mut(&mut self) -> &mut BuilderFields {
&mut self.fields
}
}
impl Default for CertBuilder {
fn default() -> Self {
Self::new()
}
}
impl CertBuilder {
pub fn new() -> Self {
Self {
fields: BuilderFields::default(),
valid_from: Asn1Time::days_from_now(0).unwrap(), valid_to: Asn1Time::days_from_now(365).unwrap(), ca: false,
}
}
pub fn valid_from(mut self, valid_from: &str) -> Self {
self.valid_from =
create_asn1_time_from_date(valid_from).expect("Failed to parse valid_from date");
self
}
pub fn valid_to(mut self, valid_to: &str) -> Self {
self.valid_to =
create_asn1_time_from_date(valid_to).expect("Failed to parse valid_to date");
self
}
pub fn is_ca(mut self, ca: bool) -> Self {
if ca {
self.ca = ca;
self.fields
.set_key_usage(HashSet::from([Usage::certsign, Usage::crlsign]));
}
self
}
pub fn build_and_self_sign(&self) -> Result<Certificate, Box<dyn std::error::Error>> {
let (mut builder, pkey) = self.prepare_x509_builder(None)?;
let ca_cert: X509 = if is_digestless_key(&pkey) {
let build_cert = builder.build();
sign_certificate_digestless(&build_cert, &pkey)
.map_err(|e| format!("Failed to sign certificate with digestless key: {}", e))?;
build_cert
} else {
builder.sign(&pkey, select_hash(&self.fields.signature_alg))?;
builder.build()
};
Ok(Certificate {
x509: ca_cert,
pkey: Some(pkey),
})
}
pub fn build_and_sign(
&self,
signer: &Certificate,
) -> Result<Certificate, Box<dyn std::error::Error>> {
let can_sign = can_sign_cert(&signer.x509)?;
if !can_sign {
let err = format!(
"Trying to sign with non CA and/or no key usage that allow signing for signer certificate:{:?}",
signer.x509.issuer_name()
);
return Err(err.into());
}
let (mut builder, pkey) = self.prepare_x509_builder(Some(signer))?;
let signer_key = signer.pkey.as_ref().unwrap();
let cert: X509 = if is_digestless_key(signer_key) {
let build_cert = builder.build();
sign_certificate_digestless(&build_cert, signer_key)
.map_err(|e| format!("Failed to sign certificate with digestless key: {}", e))?;
build_cert
} else {
builder.sign(signer_key, select_hash(&self.fields.signature_alg))?;
builder.build()
};
Ok(Certificate {
x509: cert,
pkey: Some(pkey),
})
}
fn prepare_x509_builder(
&self,
signer: Option<&Certificate>,
) -> Result<(X509Builder, PKey<Private>), Box<dyn std::error::Error>> {
let mut name_builder = X509NameBuilder::new()?;
name_builder.append_entry_by_nid(Nid::COMMONNAME, &self.fields.common_name)?;
if !self.fields.country_name.trim().is_empty() {
name_builder.append_entry_by_nid(Nid::COUNTRYNAME, &self.fields.country_name)?;
}
if !self.fields.state_province.trim().is_empty() {
name_builder
.append_entry_by_nid(Nid::STATEORPROVINCENAME, &self.fields.state_province)?;
}
if !self.fields.locality_time.trim().is_empty() {
name_builder.append_entry_by_nid(Nid::LOCALITYNAME, &self.fields.locality_time)?;
}
if !self.fields.organization.trim().is_empty() {
name_builder.append_entry_by_nid(Nid::ORGANIZATIONNAME, &self.fields.organization)?;
}
if !self.fields.organization_unit.trim().is_empty() {
name_builder
.append_entry_by_nid(Nid::ORGANIZATIONALUNITNAME, &self.fields.organization_unit)?;
}
let name = name_builder.build();
let mut builder = X509::builder()?;
builder.set_version(2)?;
let serial_number = {
let mut serial = BigNum::new()?;
serial.rand(159, openssl::bn::MsbOption::MAYBE_ZERO, false)?;
serial.to_asn1_integer()?
};
let pkey = select_key(&self.fields.key_type).unwrap();
builder.set_serial_number(&serial_number)?;
builder.set_subject_name(&name)?;
builder.set_pubkey(&pkey)?;
builder.set_not_before(&self.valid_from)?;
builder.set_not_after(&self.valid_to)?;
match signer {
Some(signer) => builder.set_issuer_name(signer.x509.subject_name())?,
None => builder.set_issuer_name(&name)?,
}
let key_usage = self.fields.usage.clone().unwrap_or_default();
if self.ca {
builder.append_extension(BasicConstraints::new().ca().critical().build()?)?;
} else {
builder.append_extension(BasicConstraints::new().build()?)?;
}
let (tracked_key_usage, tracked_extended_key_usage) = get_key_usage(&Some(key_usage));
if tracked_key_usage.is_used() {
builder.append_extension(tracked_key_usage.into_inner().build()?)?;
}
if tracked_extended_key_usage.is_used() {
builder.append_extension(tracked_extended_key_usage.into_inner().build()?)?;
}
let mut san = SubjectAlternativeName::new();
for s in &self.fields.alternative_names {
san.dns(s);
}
if let Some(signer_cert) = signer {
builder.append_extension(
san.build(&builder.x509v3_context(Some(&signer_cert.x509), None))?,
)?;
if signer_cert.x509.subject_key_id().is_some() {
let aki = AuthorityKeyIdentifier::new()
.keyid(true)
.issuer(false)
.build(&builder.x509v3_context(Some(&signer_cert.x509), None))?;
builder.append_extension(aki)?;
}
} else {
builder.append_extension(san.build(&builder.x509v3_context(None, None))?)?;
let oid = Asn1Object::from_str("2.5.29.35")?; let pubkey_der = pkey.public_key_to_der()?;
let aki_hash = hash(MessageDigest::sha1(), &pubkey_der)?;
let der_encoded = yasna::construct_der(|writer| {
writer.write_sequence(|writer| {
writer
.next()
.write_tagged_implicit(yasna::Tag::context(0), |writer| {
writer.write_bytes(aki_hash.as_ref());
})
})
});
let aki_asn1 = Asn1OctetString::new_from_bytes(&der_encoded)?;
let ext = X509Extension::new_from_der(oid.as_ref(), false, &aki_asn1)?;
builder.append_extension(ext)?;
}
let oid = Asn1Object::from_str("2.5.29.14")?; let pubkey_der = pkey.public_key_to_der()?;
let ski_hash = hash(MessageDigest::sha1(), &pubkey_der)?;
let der_encoded = yasna::construct_der(|writer| {
writer.write_bytes(ski_hash.as_ref());
});
let ski_asn1 = Asn1OctetString::new_from_bytes(&der_encoded)?;
let ext = X509Extension::new_from_der(oid.as_ref(), false, &ski_asn1)?;
builder.append_extension(ext)?;
Ok((builder, pkey))
}
}
pub struct CsrBuilder {
fields: BuilderFields,
}
impl UseesBuilderFields for CsrBuilder {
fn fields_mut(&mut self) -> &mut BuilderFields {
&mut self.fields
}
}
impl Default for CsrBuilder {
fn default() -> Self {
Self::new()
}
}
impl CsrBuilder {
pub fn new() -> Self {
Self {
fields: BuilderFields::default(),
}
}
pub fn certificate_signing_request(self) -> Result<Csr, Box<dyn std::error::Error>> {
let mut name_builder = X509NameBuilder::new()?;
name_builder.append_entry_by_nid(Nid::COMMONNAME, &self.fields.common_name)?;
if !self.fields.country_name.trim().is_empty() {
name_builder.append_entry_by_nid(Nid::COUNTRYNAME, &self.fields.country_name)?;
}
if !self.fields.state_province.trim().is_empty() {
name_builder
.append_entry_by_nid(Nid::STATEORPROVINCENAME, &self.fields.state_province)?;
}
if !self.fields.locality_time.trim().is_empty() {
name_builder.append_entry_by_nid(Nid::LOCALITYNAME, &self.fields.locality_time)?;
}
if !self.fields.organization.trim().is_empty() {
name_builder.append_entry_by_nid(Nid::ORGANIZATIONNAME, &self.fields.organization)?;
}
let name = name_builder.build();
let mut builder = X509ReqBuilder::new()?;
builder.set_version(0)?;
builder.set_subject_name(&name)?;
let pkey = select_key(&self.fields.key_type).unwrap();
builder.set_pubkey(&pkey)?;
let key_usage = self.fields.usage.clone().unwrap_or_default();
let mut extensions = Stack::new()?;
let (tracked_key_usage, tracked_extended_key_usage) = get_key_usage(&Some(key_usage));
if tracked_key_usage.is_used() {
extensions.push(tracked_key_usage.inner.build()?)?;
}
if tracked_extended_key_usage.is_used() {
extensions.push(tracked_extended_key_usage.inner.build()?)?;
}
let mut san = SubjectAlternativeName::new();
for s in &self.fields.alternative_names {
san.dns(s);
}
extensions.push(san.build(&builder.x509v3_context(None))?)?;
builder.add_extensions(&extensions)?;
let csr: X509Req = if is_digestless_key(&pkey) {
let builder_csr = builder.build();
sign_x509_req_digestless(&builder_csr, &pkey)
.map_err(|e| format!("Failed to sign certificate with digestless key: {}", e))?;
builder_csr
} else {
builder.sign(&pkey, select_hash(&self.fields.signature_alg))?;
builder.build()
};
Ok(Csr {
csr,
pkey: Some(pkey),
})
}
}
struct TrackedExtendedKeyUsage {
inner: ExtendedKeyUsage,
used: bool,
}
impl TrackedExtendedKeyUsage {
fn new() -> Self {
Self {
inner: ExtendedKeyUsage::new(),
used: false,
}
}
fn client_auth(&mut self) {
self.inner.client_auth();
self.used = true;
}
fn server_auth(&mut self) {
self.inner.server_auth();
self.used = true;
}
fn is_used(&self) -> bool {
self.used
}
fn into_inner(self) -> ExtendedKeyUsage {
self.inner
}
}
struct TrackedKeyUsage {
inner: KeyUsage,
used: bool,
}
impl TrackedKeyUsage {
fn new() -> Self {
Self {
inner: KeyUsage::new(),
used: false,
}
}
fn digital_signature(&mut self) {
self.inner.digital_signature();
self.used = true;
}
fn non_repudiation(&mut self) {
self.inner.non_repudiation();
self.used = true;
}
fn key_encipherment(&mut self) {
self.inner.key_encipherment();
self.used = true;
}
fn key_cert_sign(&mut self) {
self.inner.key_cert_sign();
self.used = true;
}
fn crl_sign(&mut self) {
self.inner.crl_sign();
self.used = true;
}
fn is_used(&self) -> bool {
self.used
}
fn into_inner(self) -> KeyUsage {
self.inner
}
}
pub fn verify_cert(
cert: &X509,
ca: &X509,
cert_chain: Vec<&X509>,
) -> Result<bool, Box<dyn std::error::Error>> {
let mut store_builder = X509StoreBuilder::new()?;
store_builder.add_cert(ca.clone())?;
let store = store_builder.build();
let mut ctx = X509StoreContext::new()?;
let mut chain = Stack::new()?; cert_chain
.iter()
.try_for_each(|c| chain.push((*c).clone()))?;
ctx.init(&store, cert, &chain, |c| c.verify_cert())?;
let verified = ctx.error() == openssl::x509::X509VerifyResult::OK;
Ok(verified)
}
pub fn create_cert_chain_from_cert_list(
certs: Vec<X509>,
) -> Result<Vec<X509>, Box<dyn std::error::Error>> {
let mut subject_map: HashMap<Vec<u8>, X509> = HashMap::new();
let mut issuer_map: HashMap<Vec<u8>, Vec<u8>> = HashMap::new();
for cert in &certs {
let subject = cert.subject_name().to_der()?;
let issuer = cert.issuer_name().to_der()?;
subject_map.insert(subject.clone(), cert.clone());
issuer_map.insert(subject, issuer);
}
let all_issuers: Vec<Vec<u8>> = issuer_map.values().cloned().collect();
let leaf_certs: Vec<X509> = subject_map
.iter()
.filter(|(subject, _)| !all_issuers.contains(subject))
.map(|(_, cert)| cert.clone())
.collect();
let mut longest_chain = Vec::new();
for leaf in leaf_certs {
let mut chain = vec![leaf.clone()];
let mut current_cert = leaf;
while let Ok(issuer_der) = current_cert.issuer_name().to_der() {
if let Some(parent_cert) = subject_map.get(&issuer_der) {
if parent_cert.subject_name().to_der()? == current_cert.subject_name().to_der()? {
break; }
chain.push(parent_cert.clone());
current_cert = parent_cert.clone();
} else {
break; }
}
if chain.len() > longest_chain.len() {
longest_chain = chain;
}
}
longest_chain.reverse();
Ok(longest_chain)
}
fn create_asn1_time_from_date(date_str: &str) -> Result<Asn1Time, Box<dyn std::error::Error>> {
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
let datetime = NaiveDateTime::new(date, chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap());
let utc_datetime = Utc.from_utc_datetime(&datetime);
let asn1_time_str = utc_datetime.format("%Y%m%d%H%M%SZ").to_string();
let asn1_time = Asn1Time::from_str(&asn1_time_str)?;
Ok(asn1_time)
}
fn select_key(key_type: &Option<KeyType>) -> Result<PKey<Private>, ErrorStack> {
match key_type {
Some(KeyType::P224) => {
let group = EcGroup::from_curve_name(Nid::SECP224R1)?;
let ec_key = EcKey::generate(&group)?;
PKey::from_ec_key(ec_key)
}
Some(KeyType::P256) => {
let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?;
let ec_key = EcKey::generate(&group)?;
PKey::from_ec_key(ec_key)
}
Some(KeyType::P384) => {
let group = EcGroup::from_curve_name(Nid::SECP384R1)?;
let ec_key = EcKey::generate(&group)?;
PKey::from_ec_key(ec_key)
}
Some(KeyType::P521) => {
let group = EcGroup::from_curve_name(Nid::SECP521R1)?;
let ec_key = EcKey::generate(&group)?;
PKey::from_ec_key(ec_key)
}
Some(KeyType::Ed25519) => PKey::generate_ed25519(),
#[cfg(feature = "pqc")]
Some(KeyType::MlDsa44) => generate_pqc_key("ML-DSA-44"),
#[cfg(feature = "pqc")]
Some(KeyType::MlDsa65) => generate_pqc_key("ML-DSA-65"),
#[cfg(feature = "pqc")]
Some(KeyType::MlDsa87) => generate_pqc_key("ML-DSA-87"),
#[cfg(feature = "pqc")]
Some(KeyType::SlhDsaSha2_128s) => generate_pqc_key("SLH-DSA-SHA2-128s"),
#[cfg(feature = "pqc")]
Some(KeyType::SlhDsaSha2_192s) => generate_pqc_key("SLH-DSA-SHA2-192s"),
#[cfg(feature = "pqc")]
Some(KeyType::SlhDsaSha2_256s) => generate_pqc_key("SLH-DSA-SHA2-256s"),
Some(KeyType::RSA4096) => {
let rsa = Rsa::generate(4096)?;
PKey::from_rsa(rsa)
}
_ => {
let rsa = Rsa::generate(2048)?;
PKey::from_rsa(rsa)
}
}
}
fn select_hash(hash_type: &Option<HashAlg>) -> MessageDigest {
match hash_type {
Some(HashAlg::SHA1) => MessageDigest::sha1(),
Some(HashAlg::SHA384) => MessageDigest::sha384(),
Some(HashAlg::SHA512) => MessageDigest::sha512(),
_ => MessageDigest::sha256(),
}
}
fn get_key_usage(usage: &Option<HashSet<Usage>>) -> (TrackedKeyUsage, TrackedExtendedKeyUsage) {
let mut ku = TrackedKeyUsage::new();
let mut eku = TrackedExtendedKeyUsage::new();
if let Some(usages) = usage {
for u in usages {
match u {
Usage::contentcommitment => {
ku.non_repudiation();
}
Usage::encipherment => {
ku.key_encipherment();
}
Usage::certsign => {
ku.key_cert_sign();
}
Usage::clientauth => {
eku.client_auth();
}
Usage::signature => {
ku.digital_signature();
}
Usage::crlsign => {
ku.crl_sign();
}
Usage::serverauth => {
eku.server_auth();
}
}
}
}
(ku, eku)
}
fn can_sign_cert(cert: &X509) -> Result<bool, Box<dyn std::error::Error>> {
let der = cert.to_der()?;
let (_, parsed_cert) = parse_x509_certificate(&der)?;
let mut is_ca = false;
let mut can_sign = false;
let not_after_asn1_time = cert.not_after().to_string();
let naive =
NaiveDateTime::parse_from_str(¬_after_asn1_time, "%b %e %H:%M:%S %Y GMT").unwrap();
let not_after_utc: DateTime<Utc> = Utc.from_utc_datetime(&naive);
let not_before_asn1_time = cert.not_before().to_string();
let naive =
NaiveDateTime::parse_from_str(¬_before_asn1_time, "%b %e %H:%M:%S %Y GMT").unwrap();
let not_before_utc: DateTime<Utc> = Utc.from_utc_datetime(&naive);
let now = Utc::now();
let valid_time = (now < not_after_utc) && (now >= not_before_utc);
for ext in parsed_cert.tbs_certificate.extensions().iter() {
match &ext.parsed_extension() {
ParsedExtension::BasicConstraints(bc) => {
is_ca = bc.ca;
}
ParsedExtension::KeyUsage(ku) => {
can_sign = ku.key_cert_sign();
}
_ => {}
}
}
Ok(is_ca && can_sign && valid_time)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::path::Path;
use tempfile::NamedTempFile;
#[test]
fn save_certificate() {
let ca = CertBuilder::new().common_name("My Test Ca").is_ca(true);
match ca.build_and_self_sign() {
Ok(cert) => {
let output_file = NamedTempFile::new().unwrap();
let full_path = output_file.path();
let parent_dir: &Path = full_path.parent().unwrap();
let file_name: &str = full_path.file_name().unwrap().to_str().unwrap();
cert.save(parent_dir, file_name)
.expect("Failed to save certificate and key");
let written_file_path = parent_dir.join(file_name);
assert!(written_file_path.exists(), "File was not created");
}
Err(_) => panic!("Failed to creat certificate"),
}
}
#[test]
fn read_certificate_and_key_from_file() {
let cert_pem = b"-----BEGIN CERTIFICATE-----
MIICiDCCAemgAwIBAgIUO3+y1WZPRRNs8dmZZTUHMj6TdiowCgYIKoZIzj0EAwQw
WzETMBEGA1UEAwwKTXkgVGVzdCBDYTELMAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0
b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMQ8wDQYDVQQKDAZteSBvcmcwHhcN
MjUwNzA4MTExMzI2WhcNMjYwNzA4MTExMzI2WjBbMRMwEQYDVQQDDApNeSBUZXN0
IENhMQswCQYDVQQGEwJTRTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlT
dG9ja2hvbG0xDzANBgNVBAoMBm15IG9yZzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOB
hgAEADZXcQK2ihgVTJeGx5FKm1x+R+ivygIvMnkv03faq1LpLU3doKX38DEO/cSW
Ev5u+kcjspXeeDPhqJFC8rRAz4awAMk+D0mXEms7xpFPh0HmI6NNcJc5eJ/8ZsEJ
GH1a34y0Yn6259gqlwAh2Eh9Nx1579BAanRr8lr+n1tZ09T/9AQho0gwRjAMBgNV
HRMEBTADAQH/MAsGA1UdDwQEAwIBBjApBgNVHREEIjAgggZjYS5jb22CCnd3dy5j
YS5jb22CCk15IFRlc3QgQ2EwCgYIKoZIzj0EAwQDgYwAMIGIAkIB8NVUgRIuNXmJ
cLCQ74Ub7Dqo71S0+iCrZF1YyJA8/q65aqMCT54k5Yx7HRBUUVHbCEpDXRqGPsIH
frfe5OmS3qICQgDBn07o0CcyfoSEd+Xoj2+/RBuU0vo9lUP7TKj7tssBxzEQFoxX
eE1qT98UIe78FZ+zqjwZTN9MCSsatuim6pXvOA==
-----END CERTIFICATE-----";
let key_pem = b"-----BEGIN PRIVATE KEY-----
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAgvZTeQgGysadAX0r
aZB5Lk4vjHy5iVuKdvcGdYt9NBvYx+Ib3Uk7vqMag7M1jyHL0Xf9uNtT2mxBmzBG
3CF+EgOhgYkDgYYABAA2V3ECtooYFUyXhseRSptcfkfor8oCLzJ5L9N32qtS6S1N
3aCl9/AxDv3ElhL+bvpHI7KV3ngz4aiRQvK0QM+GsADJPg9JlxJrO8aRT4dB5iOj
TXCXOXif/GbBCRh9Wt+MtGJ+tufYKpcAIdhIfTcdee/QQGp0a/Ja/p9bWdPU//QE
IQ==
-----END PRIVATE KEY-----";
let mut cert_file = NamedTempFile::new().expect("Failed to create temp cert file");
let mut key_file = NamedTempFile::new().expect("Failed to create temp key file");
cert_file.write_all(cert_pem).expect("Failed to write cert");
key_file.write_all(key_pem).expect("Failed to write key");
let result = Certificate::load_cert_and_key(cert_file.path(), key_file.path());
assert!(
result.is_ok(),
"Failed to load cert and key: {:?}",
result.err()
);
}
#[test]
fn test_reading_csr_from_file() {
let csr_data = b"-----BEGIN CERTIFICATE REQUEST-----
MIICzDCCAbQCAQAwXTEVMBMGA1UEAwwMZXhhbXBsZTIuY29tMQswCQYDVQQGEwJT
RTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlTdG9ja2hvbG0xDzANBgNV
BAoMBk15IG9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIeAXpCG
hbIayESfdTzOO0DxIMsOAu4kUm0zF0W/+xDUHl6bGy3wlB9S9nzBG/qwqFZ27Om3
o4zrZ8K8DBx0ERWNuhMmr0Nx8QpAWBEyxOc08Gn4c3XVBBkRZSn4AIqr9DGtcUqW
tQZXvMGF6sRRljiEvOxO6zMzZKTGYwzIeQvH85cQ3uXsw0Kknsw/fcuywaAC8SS9
aqs4jiEIgzdhxdH2OVXBNGj4cjVhK309JiWFHS9XJLNV/PKC+F1nkaANQwbW5A4F
9vya4js9gk8f4SfF1u+qOJEvsDvAb+1xdjXPRzf77eGh3rC4KgGWQ6WrWfW8PItF
BDg/jskq3bJXNL8CAwEAAaAqMCgGCSqGSIb3DQEJDjEbMBkwFwYDVR0RBBAwDoIM
ZXhhbXBsZTIuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAHeeSW8C6SMVhWiMvPn7iz
FUHQedHRyPz6kTEfC01eNIbs0r4YghOAcm8PF67jncIXVrqgxo1uzq12qlV+0YYb
jps31IbQNOz0eFLYvij15ielmOYeQZZ/2vqaGi3geVobLc6Ki5tadnA/NhjTN33j
QcqDDic8riAOTbSQ6TH9KPTGJQOPk+taMpDGDHskIW0oME5iT2ewbhBHg6v/kSzy
tss2kBY5O7vo2COtbNcwX5Xp9S2LH9kVUKr0GIjuQjwbv5xl+GNdDey09W9EDACU
jcGV3++2wS4LN4h3CG4pWZ+LTXhm8ymhoWOapN95lfe3xLRAKFJwiLkGwS75++FW
-----END CERTIFICATE REQUEST-----";
let mut csr_file = NamedTempFile::new().expect("Failed to create temp csr file");
csr_file.write_all(csr_data).expect("Failed to write csr");
let result = Csr::load_csr(csr_file.path());
assert!(result.is_ok(), "Failed to load csr: {:?}", result.err());
}
}