use std::cmp::Ordering; use std::path::{Path, PathBuf}; use foreign_types_shared::ForeignType; use foreign_types_shared::ForeignTypeRef; use libc::c_long; use openssl::asn1::Asn1Time; use openssl::bn::{BigNum, MsbOption}; use openssl::ec::EcGroup; use openssl::hash::MessageDigest; use openssl::nid::Nid; use openssl::pkey::{PKey, Private}; use openssl::x509::extension::{ BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName, SubjectKeyIdentifier, }; use openssl::x509::{X509Crl, X509Name, X509NameBuilder, X509Req, X509}; use openssl_sys::{ X509_CRL_add0_revoked, X509_CRL_set1_lastUpdate, X509_CRL_set1_nextUpdate, X509_CRL_set_issuer_name, X509_CRL_set_version, X509_CRL_sign, X509_REVOKED_dup, }; use crate::app_config::AppConfig; use crate::crypto::crl_extension::CRLDistributionPointExt; #[derive(thiserror::Error, Debug)] pub enum PKIError { #[error("Certification Authority does not have a CRL")] MissingCRL, #[error("Certification Authority does not have a CRL next update time")] MissingCRLNextUpdate, #[error("Failed to initialize CRL! {0}")] GenCRLError(&'static str), } /// Certificate and private key pub struct CertData { pub cert: X509, pub key: PKey, pub crl: Option, } impl CertData { /// Load root CA fn load_root_ca() -> anyhow::Result { Ok(Self { cert: load_certificate_from_file(AppConfig::get().root_ca_cert_path())?, key: load_priv_key_from_file(AppConfig::get().root_ca_priv_key_path())?, crl: Some(AppConfig::get().root_ca_crl_path()), }) } /// Load web CA pub fn load_web_ca() -> anyhow::Result { Ok(Self { cert: load_certificate_from_file(AppConfig::get().web_ca_cert_path())?, key: load_priv_key_from_file(AppConfig::get().web_ca_priv_key_path())?, crl: Some(AppConfig::get().web_ca_crl_path()), }) } /// Load devices CA pub fn load_devices_ca() -> anyhow::Result { Ok(Self { cert: load_certificate_from_file(AppConfig::get().devices_ca_cert_path())?, key: load_priv_key_from_file(AppConfig::get().devices_ca_priv_key_path())?, crl: Some(AppConfig::get().devices_ca_crl_path()), }) } /// Load server CA pub fn load_server() -> anyhow::Result { Ok(Self { cert: load_certificate_from_file(AppConfig::get().server_cert_path())?, key: load_priv_key_from_file(AppConfig::get().server_priv_key_path())?, crl: None, }) } } /// Generate private key fn gen_private_key() -> anyhow::Result> { let nid = Nid::X9_62_PRIME256V1; // NIST P-256 curve let group = EcGroup::from_curve_name(nid)?; let key = openssl::ec::EcKey::generate(&group)?; let key_pair = PKey::from_ec_key(key.clone())?; Ok(key_pair) } /// Load private key from PEM file fn load_priv_key_from_file>(path: P) -> anyhow::Result> { Ok(PKey::private_key_from_pem(&std::fs::read(path)?)?) } /// Load certificate from PEM file fn load_certificate_from_file>(path: P) -> anyhow::Result { Ok(X509::from_pem(&std::fs::read(path)?)?) } /// Load CRL from PEM file fn load_crl_from_file>(path: P) -> anyhow::Result { Ok(X509Crl::from_pem(&std::fs::read(path)?)?) } #[allow(clippy::upper_case_acronyms)] enum GenCertificatSubjectReq<'a> { Subject { cn: &'a str }, CSR { csr: &'a X509Req }, } impl<'a> Default for GenCertificatSubjectReq<'a> { fn default() -> Self { Self::Subject { cn: "" } } } #[derive(Default)] struct GenCertificateReq<'a> { pub sub: GenCertificatSubjectReq<'a>, pub issuer: Option<&'a CertData>, pub ca: bool, pub web_server: bool, pub web_client: bool, pub subject_alternative_names: Vec<&'a str>, } /// Generate certificate fn gen_certificate(req: GenCertificateReq) -> anyhow::Result<(Option>, Vec)> { let mut cert_builder = X509::builder()?; cert_builder.set_version(2)?; let serial_number = { let mut serial = BigNum::new()?; serial.rand(159, MsbOption::MAYBE_ZERO, false)?; serial.to_asn1_integer()? }; cert_builder.set_serial_number(&serial_number)?; // Process subject let x509_name = match req.sub { GenCertificatSubjectReq::Subject { cn } => { let mut x509_name = X509NameBuilder::new()?; x509_name.append_entry_by_text("C", "FR")?; x509_name.append_entry_by_text("CN", cn)?; x509_name.build() } GenCertificatSubjectReq::CSR { csr } => X509Name::from_der(&csr.subject_name().to_der()?)?, }; cert_builder.set_subject_name(&x509_name)?; match req.issuer { // Self-signed certificate None => cert_builder.set_issuer_name(&x509_name)?, // Certificate signed by another CA Some(i) => cert_builder.set_issuer_name(i.cert.subject_name())?, } let not_before = Asn1Time::days_from_now(0)?; cert_builder.set_not_before(¬_before)?; let not_after = Asn1Time::days_from_now(365 * 30)?; cert_builder.set_not_after(¬_after)?; // Specify CRL URL if let Some(issuer) = req.issuer { if let Some(crl) = &issuer.crl { let crl_url = format!( "{}/pki/{}", AppConfig::get().unsecure_origin(), crl.file_name().unwrap().to_string_lossy() ); cert_builder .append_extension(CRLDistributionPointExt { url: crl_url }.as_extension()?)?; } } // If cert is a CA or not let mut basic = BasicConstraints::new(); if req.ca { basic.ca(); } cert_builder.append_extension(basic.critical().build()?)?; // Key usage let mut key_usage = KeyUsage::new(); let mut eku = None; if req.ca { key_usage.key_cert_sign().crl_sign(); } if req.web_server { key_usage.digital_signature().key_encipherment(); eku = Some( ExtendedKeyUsage::new() .server_auth() .client_auth() .build()?, ); } if req.web_client { key_usage.digital_signature().key_encipherment(); eku = Some(ExtendedKeyUsage::new().client_auth().build()?); } cert_builder.append_extension(key_usage.critical().build()?)?; if let Some(eku) = eku { cert_builder.append_extension(eku)?; } // Subject alternative names if !req.subject_alternative_names.is_empty() { let mut ext = SubjectAlternativeName::new(); for subj in req.subject_alternative_names { ext.dns(subj); } cert_builder.append_extension(ext.build(&cert_builder.x509v3_context(None, None))?)?; } // Subject key identifier let subject_key_identifier = SubjectKeyIdentifier::new().build(&cert_builder.x509v3_context(None, None))?; cert_builder.append_extension(subject_key_identifier)?; // Public key match req.sub { // Private key known GenCertificatSubjectReq::Subject { .. } => { let key_pair = gen_private_key()?; cert_builder.set_pubkey(&key_pair)?; // Sign certificate cert_builder.sign( match req.issuer { None => &key_pair, Some(i) => &i.key, }, MessageDigest::sha256(), )?; let cert = cert_builder.build(); Ok((Some(key_pair.private_key_to_pem_pkcs8()?), cert.to_pem()?)) } // Private key unknown GenCertificatSubjectReq::CSR { csr } => { let pub_key = csr.public_key()?; cert_builder.set_pubkey(pub_key.as_ref())?; // Sign certificate cert_builder.sign( &req.issuer .expect("Cannot issue certificate for CSR if issuer is not specified!") .key, MessageDigest::sha256(), )?; let cert = cert_builder.build(); Ok((None, cert.to_pem()?)) } } } /// Initialize Root CA, if required pub fn initialize_root_ca() -> anyhow::Result<()> { if AppConfig::get().root_ca_cert_path().exists() && AppConfig::get().root_ca_priv_key_path().exists() { return Ok(()); } log::info!("Generating root ca..."); let (key, cert) = gen_certificate(GenCertificateReq { sub: GenCertificatSubjectReq::Subject { cn: "SolarEnergy Root CA", }, issuer: None, ca: true, ..Default::default() })?; // Serialize generated web CA std::fs::write(AppConfig::get().root_ca_priv_key_path(), key.unwrap())?; std::fs::write(AppConfig::get().root_ca_cert_path(), cert)?; Ok(()) } /// Initialize web CA, if required pub fn initialize_web_ca() -> anyhow::Result<()> { if AppConfig::get().web_ca_cert_path().exists() && AppConfig::get().web_ca_priv_key_path().exists() { return Ok(()); } log::info!("Generating web ca..."); let (key, cert) = gen_certificate(GenCertificateReq { sub: GenCertificatSubjectReq::Subject { cn: "SolarEnergy Web CA", }, issuer: Some(&CertData::load_root_ca()?), ca: true, ..Default::default() })?; // Serialize generated web CA std::fs::write(AppConfig::get().web_ca_priv_key_path(), key.unwrap())?; std::fs::write(AppConfig::get().web_ca_cert_path(), cert)?; Ok(()) } /// Initialize devices CA, if required pub fn initialize_devices_ca() -> anyhow::Result<()> { if AppConfig::get().devices_ca_cert_path().exists() && AppConfig::get().devices_ca_priv_key_path().exists() { return Ok(()); } log::info!("Generating devices ca..."); let (key, cert) = gen_certificate(GenCertificateReq { sub: GenCertificatSubjectReq::Subject { cn: "SolarEnergy Devices CA", }, issuer: Some(&CertData::load_root_ca()?), ca: true, ..Default::default() })?; // Serialize generated devices CA std::fs::write(AppConfig::get().devices_ca_priv_key_path(), key.unwrap())?; std::fs::write(AppConfig::get().devices_ca_cert_path(), cert)?; Ok(()) } /// Initialize server certificate, if required pub fn initialize_server_ca() -> anyhow::Result<()> { if AppConfig::get().server_cert_path().exists() && AppConfig::get().server_priv_key_path().exists() { return Ok(()); } log::info!("Generating server certificate..."); let (key, cert) = gen_certificate(GenCertificateReq { sub: GenCertificatSubjectReq::Subject { cn: AppConfig::get().hostname.as_str(), }, issuer: Some(&CertData::load_web_ca()?), web_server: true, subject_alternative_names: vec![AppConfig::get().hostname.as_str()], ..Default::default() })?; std::fs::write(AppConfig::get().server_priv_key_path(), key.unwrap())?; std::fs::write(AppConfig::get().server_cert_path(), cert)?; Ok(()) } /// Initialize or refresh a CRL fn refresh_crl(d: &CertData) -> anyhow::Result<()> { let crl_path = d.crl.as_ref().ok_or(PKIError::MissingCRL)?; let old_crl = if crl_path.exists() { let crl = load_crl_from_file(crl_path)?; // Check if revocation is un-needed let next_update = crl.next_update().ok_or(PKIError::MissingCRLNextUpdate)?; if next_update.compare(Asn1Time::days_from_now(0)?.as_ref())? == Ordering::Greater { return Ok(()); } Some(crl) } else { None }; log::info!("Generating a new CRL..."); // based on https://github.com/openssl/openssl/blob/master/crypto/x509/x509_vfy.c unsafe { let crl = openssl_sys::X509_CRL_new(); if crl.is_null() { return Err(PKIError::GenCRLError("Could not construct CRL!").into()); } const X509_CRL_VERSION_2: c_long = 1; if X509_CRL_set_version(crl, X509_CRL_VERSION_2) == 0 { return Err(PKIError::GenCRLError("X509_CRL_set_version").into()); } if X509_CRL_set_issuer_name(crl, d.cert.subject_name().as_ptr()) == 0 { return Err(PKIError::GenCRLError("X509_CRL_set_issuer_name").into()); } let last_update = Asn1Time::days_from_now(0)?; if X509_CRL_set1_lastUpdate(crl, last_update.as_ptr()) == 0 { return Err(PKIError::GenCRLError("X509_CRL_set1_lastUpdate").into()); } let next_update = Asn1Time::days_from_now(10)?; if X509_CRL_set1_nextUpdate(crl, next_update.as_ptr()) == 0 { return Err(PKIError::GenCRLError("X509_CRL_set1_nextUpdate").into()); } // Add old entries if let Some(old_crl) = old_crl { if let Some(entries) = old_crl.get_revoked() { for entry in entries { if X509_CRL_add0_revoked(crl, X509_REVOKED_dup(entry.as_ptr())) == 0 { return Err(PKIError::GenCRLError("X509_CRL_add0_revoked").into()); } } } } let md = MessageDigest::sha256(); if X509_CRL_sign(crl, d.key.as_ptr(), md.as_ptr()) == 0 { return Err(PKIError::GenCRLError("X509_CRL_sign").into()); } let crl = X509Crl::from_ptr(crl); std::fs::write(crl_path, crl.to_pem()?)?; } Ok(()) } /// Refresh revocation lists pub fn refresh_crls() -> anyhow::Result<()> { refresh_crl(&CertData::load_root_ca()?)?; refresh_crl(&CertData::load_web_ca()?)?; refresh_crl(&CertData::load_devices_ca()?)?; Ok(()) } /// Generate a certificate for a device pub fn gen_certificate_for_device(csr: &X509Req) -> anyhow::Result { let (_, cert) = gen_certificate(GenCertificateReq { sub: GenCertificatSubjectReq::CSR { csr }, issuer: Some(&CertData::load_devices_ca()?), web_client: true, ..Default::default() })?; Ok(String::from_utf8(cert)?) }