From e64a444bd0c6a3f91a98d63d905db0a51200fd2b Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Jul 2024 22:24:03 +0200 Subject: [PATCH] Can issue certificate for devices --- central_backend/Cargo.lock | 24 ++++ central_backend/Cargo.toml | 3 +- central_backend/src/crypto/pki.rs | 135 +++++++++++++----- central_backend/src/devices/device.rs | 14 ++ central_backend/src/energy/energy_actor.rs | 2 +- .../src/server/devices_api/mgmt_controller.rs | 44 +++++- 6 files changed, 181 insertions(+), 41 deletions(-) diff --git a/central_backend/Cargo.lock b/central_backend/Cargo.lock index 8574760..f523954 100644 --- a/central_backend/Cargo.lock +++ b/central_backend/Cargo.lock @@ -591,6 +591,7 @@ dependencies = [ "foreign-types-shared", "futures", "futures-util", + "lazy-regex", "lazy_static", "libc", "log", @@ -1294,6 +1295,29 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy-regex" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + [[package]] name = "lazy_static" version = "1.5.0" diff --git a/central_backend/Cargo.toml b/central_backend/Cargo.toml index 0adc282..68a4928 100644 --- a/central_backend/Cargo.toml +++ b/central_backend/Cargo.toml @@ -28,4 +28,5 @@ actix-cors = "0.7.0" actix-remote-ip = "0.1.0" futures-util = "0.3.30" uuid = { version = "1.9.1", features = ["v4", "serde"] } -semver = { version = "1.0.23", features = ["serde"] } \ No newline at end of file +semver = { version = "1.0.23", features = ["serde"] } +lazy-regex = "3.1.0" \ No newline at end of file diff --git a/central_backend/src/crypto/pki.rs b/central_backend/src/crypto/pki.rs index 8dba3c5..66ae61a 100644 --- a/central_backend/src/crypto/pki.rs +++ b/central_backend/src/crypto/pki.rs @@ -13,7 +13,7 @@ use openssl::pkey::{PKey, Private}; use openssl::x509::extension::{ BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName, SubjectKeyIdentifier, }; -use openssl::x509::{X509Crl, X509NameBuilder, X509}; +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, @@ -102,25 +102,30 @@ 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> { - cn: &'a str, - issuer: Option<&'a CertData>, - ca: bool, - web_server: bool, - subject_alternative_names: Vec<&'a str>, + 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<(Vec, Vec)> { - // Generate root private key - let key_pair = gen_private_key()?; - - let mut x509_name = X509NameBuilder::new()?; - x509_name.append_entry_by_text("C", "FR")?; - x509_name.append_entry_by_text("CN", req.cn)?; - let x509_name = x509_name.build(); - +fn gen_certificate(req: GenCertificateReq) -> anyhow::Result<(Option>, Vec)> { let mut cert_builder = X509::builder()?; cert_builder.set_version(2)?; let serial_number = { @@ -129,6 +134,18 @@ fn gen_certificate(req: GenCertificateReq) -> anyhow::Result<(Vec, Vec)> 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 { @@ -138,8 +155,6 @@ fn gen_certificate(req: GenCertificateReq) -> anyhow::Result<(Vec, Vec)> Some(i) => cert_builder.set_issuer_name(i.cert.subject_name())?, } - cert_builder.set_pubkey(&key_pair)?; - let not_before = Asn1Time::days_from_now(0)?; cert_builder.set_not_before(¬_before)?; @@ -182,6 +197,11 @@ fn gen_certificate(req: GenCertificateReq) -> anyhow::Result<(Vec, Vec)> .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 { @@ -202,17 +222,42 @@ fn gen_certificate(req: GenCertificateReq) -> anyhow::Result<(Vec, Vec)> SubjectKeyIdentifier::new().build(&cert_builder.x509v3_context(None, None))?; cert_builder.append_extension(subject_key_identifier)?; - // Sign certificate - cert_builder.sign( - match req.issuer { - None => &key_pair, - Some(i) => &i.key, - }, - MessageDigest::sha256(), - )?; - let cert = cert_builder.build(); + // Public key + match req.sub { + // Private key known + GenCertificatSubjectReq::Subject { .. } => { + let key_pair = gen_private_key()?; + cert_builder.set_pubkey(&key_pair)?; - Ok((key_pair.private_key_to_pem_pkcs8()?, cert.to_pem()?)) + // 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 @@ -226,14 +271,16 @@ pub fn initialize_root_ca() -> anyhow::Result<()> { log::info!("Generating root ca..."); let (key, cert) = gen_certificate(GenCertificateReq { - cn: "SolarEnergy Root CA", + 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)?; + std::fs::write(AppConfig::get().root_ca_priv_key_path(), key.unwrap())?; std::fs::write(AppConfig::get().root_ca_cert_path(), cert)?; Ok(()) @@ -250,14 +297,16 @@ pub fn initialize_web_ca() -> anyhow::Result<()> { log::info!("Generating web ca..."); let (key, cert) = gen_certificate(GenCertificateReq { - cn: "SolarEnergy Web CA", + 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)?; + std::fs::write(AppConfig::get().web_ca_priv_key_path(), key.unwrap())?; std::fs::write(AppConfig::get().web_ca_cert_path(), cert)?; Ok(()) @@ -274,14 +323,16 @@ pub fn initialize_devices_ca() -> anyhow::Result<()> { log::info!("Generating devices ca..."); let (key, cert) = gen_certificate(GenCertificateReq { - cn: "SolarEnergy Devices CA", + 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)?; + std::fs::write(AppConfig::get().devices_ca_priv_key_path(), key.unwrap())?; std::fs::write(AppConfig::get().devices_ca_cert_path(), cert)?; Ok(()) @@ -298,14 +349,16 @@ pub fn initialize_server_ca() -> anyhow::Result<()> { log::info!("Generating server certificate..."); let (key, cert) = gen_certificate(GenCertificateReq { - cn: &AppConfig::get().hostname, + 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)?; + std::fs::write(AppConfig::get().server_priv_key_path(), key.unwrap())?; std::fs::write(AppConfig::get().server_cert_path(), cert)?; Ok(()) @@ -386,3 +439,15 @@ pub fn refresh_crls() -> anyhow::Result<()> { 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)?) +} diff --git a/central_backend/src/devices/device.rs b/central_backend/src/devices/device.rs index 2c65785..d4b2f3f 100644 --- a/central_backend/src/devices/device.rs +++ b/central_backend/src/devices/device.rs @@ -5,6 +5,20 @@ pub struct DeviceInfo { max_relays: usize, } +impl DeviceInfo { + pub fn error(&self) -> Option<&str> { + if self.reference.trim().is_empty() { + return Some("Given device reference is empty or blank!"); + } + + if self.max_relays == 0 { + return Some("Given device cannot handle any relay!"); + } + + None + } +} + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] pub struct DeviceId(pub String); diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index 186b119..d60285a 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -70,7 +70,7 @@ impl Handler for EnergyActor { /// Get current consumption #[derive(Message)] #[rtype(result = "bool")] -pub struct CheckDeviceExists(DeviceId); +pub struct CheckDeviceExists(pub DeviceId); impl Handler for EnergyActor { type Result = bool; diff --git a/central_backend/src/server/devices_api/mgmt_controller.rs b/central_backend/src/server/devices_api/mgmt_controller.rs index 76fa5d6..089242e 100644 --- a/central_backend/src/server/devices_api/mgmt_controller.rs +++ b/central_backend/src/server/devices_api/mgmt_controller.rs @@ -1,6 +1,10 @@ -use crate::devices::device::DeviceInfo; +use crate::crypto::pki; +use crate::devices::device::{DeviceId, DeviceInfo}; +use crate::energy::energy_actor; use crate::server::custom_error::HttpResult; +use crate::server::WebEnergyActor; use actix_web::{web, HttpResponse}; +use openssl::nid::Nid; use openssl::x509::X509Req; #[derive(Debug, serde::Deserialize)] @@ -12,7 +16,14 @@ pub struct EnrollRequest { } /// Enroll a new device -pub async fn enroll(req: web::Json) -> HttpResult { +pub async fn enroll(req: web::Json, actor: WebEnergyActor) -> HttpResult { + // Check device information + if let Some(e) = req.info.error() { + log::error!("Failed to validate device information! {e}"); + return Ok(HttpResponse::BadRequest().json(e)); + } + + // Check CSR let csr = match X509Req::from_pem(req.csr.as_bytes()) { Ok(r) => r, Err(e) => { @@ -26,7 +37,32 @@ pub async fn enroll(req: web::Json) -> HttpResult { return Ok(HttpResponse::BadRequest().json("Could not verify CSR signature!")); } - println!("{:#?}", &req); + let cn = match csr.subject_name().entries_by_nid(Nid::COMMONNAME).next() { + None => { + log::error!("Missing Common Name in CSR!"); + return Ok(HttpResponse::BadRequest().json("Missing Common Name in CSR!")); + } + Some(cn) => cn.data().as_utf8()?.to_string(), + }; - Ok(HttpResponse::Ok().json("go on")) + if !lazy_regex::regex!("[a-zA-Z0-9 ]{1,100}").is_match(&cn) { + log::error!("Given Common Name is invalid!"); + return Ok(HttpResponse::BadRequest().json("Invalid Common Name in CSR!")); + } + + let device_id = DeviceId(cn); + log::info!("Received enrollment request for device with ID {device_id:?}",); + + if actor + .send(energy_actor::CheckDeviceExists(device_id.clone())) + .await? + { + log::error!("Device could not be enrolled: it already exists!"); + return Ok(HttpResponse::Conflict().json("Device ")); + } + + log::info!("Issue certificate for device..."); + let cert = pki::gen_certificate_for_device(&csr)?; + + Ok(HttpResponse::Ok().body(cert)) }