From 1b02a812b4486aa64f47b7640b3a8c2219301779 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 4 Sep 2024 22:43:23 +0200 Subject: [PATCH] Start to build sync route --- central_backend/Cargo.lock | 65 +++++++++++++++ central_backend/Cargo.toml | 3 +- central_backend/src/crypto/pki.rs | 24 +++--- central_backend/src/energy/energy_actor.rs | 25 ++++++ .../src/server/devices_api/mgmt_controller.rs | 83 ++++++++++++++++++- central_backend/src/server/servers.rs | 4 + python_device/src/api.py | 1 + 7 files changed, 191 insertions(+), 14 deletions(-) diff --git a/central_backend/Cargo.lock b/central_backend/Cargo.lock index 0478c65..692306b 100644 --- a/central_backend/Cargo.lock +++ b/central_backend/Cargo.lock @@ -510,6 +510,12 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -612,6 +618,7 @@ dependencies = [ "foreign-types-shared", "futures", "futures-util", + "jsonwebtoken", "lazy-regex", "lazy_static", "libc", @@ -1033,8 +1040,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1357,6 +1366,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1498,12 +1522,31 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1607,6 +1650,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2067,6 +2120,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" diff --git a/central_backend/Cargo.toml b/central_backend/Cargo.toml index 9ffe5be..67fe668 100644 --- a/central_backend/Cargo.toml +++ b/central_backend/Cargo.toml @@ -33,4 +33,5 @@ lazy-regex = "3.2.0" tokio = { version = "1.39.2", features = ["full"] } tokio_schedule = "0.3.2" mime_guess = "2.0.5" -rust-embed = "8.5.0" \ No newline at end of file +rust-embed = "8.5.0" +jsonwebtoken = { version = "9.3.0", features = ["use_pem"] } \ No newline at end of file diff --git a/central_backend/src/crypto/pki.rs b/central_backend/src/crypto/pki.rs index b8d8562..4f9cd5a 100644 --- a/central_backend/src/crypto/pki.rs +++ b/central_backend/src/crypto/pki.rs @@ -76,6 +76,17 @@ impl CertData { crl: None, }) } + + /// Check if a certificate is revoked + pub fn is_revoked(&self, cert: &X509) -> anyhow::Result { + let crl = X509Crl::from_pem(&std::fs::read( + self.crl.as_ref().ok_or(PKIError::MissingCRL)?, + )?)?; + + let res = crl.get_by_cert(cert); + + Ok(matches!(res, CrlStatus::Revoked(_))) + } } /// Generate private key @@ -480,21 +491,10 @@ pub fn gen_certificate_for_device(csr: &X509Req) -> anyhow::Result { Ok(String::from_utf8(cert)?) } -/// Check if a certificate is revoked -fn is_revoked(cert: &X509, ca: &CertData) -> anyhow::Result { - let crl = X509Crl::from_pem(&std::fs::read( - ca.crl.as_ref().ok_or(PKIError::MissingCRL)?, - )?)?; - - let res = crl.get_by_cert(cert); - - Ok(matches!(res, CrlStatus::Revoked(_))) -} - /// Revoke a certificate pub fn revoke(cert: &X509, ca: &CertData) -> anyhow::Result<()> { // Check if certificate is already revoked - if is_revoked(cert, ca)? { + if ca.is_revoked(cert)? { // No op return Ok(()); } diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index 8ecff6a..982cccf 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -238,3 +238,28 @@ impl Handler for EnergyActor { self.devices.relay_delete(msg.0) } } + +#[derive(serde::Serialize)] +pub struct RelaySyncStatus { + enabled: bool, +} + +/// Synchronize a device +#[derive(Message)] +#[rtype(result = "anyhow::Result>")] +pub struct SynchronizeDevice(pub DeviceId, pub DeviceInfo); + +impl Handler for EnergyActor { + type Result = anyhow::Result>; + + fn handle(&mut self, msg: SynchronizeDevice, _ctx: &mut Context) -> Self::Result { + // TODO : implement real code + let mut v = vec![]; + for i in 0..msg.1.max_relays { + v.push(RelaySyncStatus { + enabled: i % 2 == 0, + }); + } + Ok(v) + } +} diff --git a/central_backend/src/server/devices_api/mgmt_controller.rs b/central_backend/src/server/devices_api/mgmt_controller.rs index 6848f60..9fac4eb 100644 --- a/central_backend/src/server/devices_api/mgmt_controller.rs +++ b/central_backend/src/server/devices_api/mgmt_controller.rs @@ -1,11 +1,14 @@ use crate::app_config::AppConfig; +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 jsonwebtoken::{Algorithm, DecodingKey, Validation}; use openssl::nid::Nid; -use openssl::x509::X509Req; +use openssl::x509::{X509Req, X509}; +use std::collections::HashSet; #[derive(Debug, serde::Deserialize)] pub struct EnrollRequest { @@ -124,3 +127,81 @@ pub async fn get_certificate(query: web::Query, actor: WebEnergyAc .content_type("application/x-pem-file") .body(cert)) } + +#[derive(serde::Deserialize)] +pub struct SyncRequest { + payload: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct Claims { + info: DeviceInfo, +} + +/// Synchronize device +pub async fn sync_device(body: web::Json, actor: WebEnergyActor) -> HttpResult { + // First, we need to extract device kid from query + let Ok(jwt_header) = jsonwebtoken::decode_header(&body.payload) else { + log::error!("Failed to decode JWT header!"); + return Ok(HttpResponse::BadRequest().json("Failed to decode JWT header!")); + }; + + let Some(kid) = jwt_header.kid else { + log::error!("Missing KID in JWT!"); + return Ok(HttpResponse::BadRequest().json("Missing KID in JWT!")); + }; + + // Fetch device information + let Some(device) = actor + .send(energy_actor::GetSingleDevice(DeviceId(kid))) + .await? + else { + log::error!("Sent a JWT for a device which does not exists!"); + return Ok(HttpResponse::NotFound().json("Sent a JWT for a device which does not exists!")); + }; + + if !device.validated { + log::error!("Sent a JWT for a device which is not validated!"); + return Ok(HttpResponse::PreconditionFailed() + .json("Sent a JWT for a device which is not validated!")); + } + + // Check certificate revocation status + let cert_bytes = std::fs::read(AppConfig::get().device_cert_path(&device.id))?; + let certificate = X509::from_pem(&cert_bytes)?; + + if pki::CertData::load_devices_ca()?.is_revoked(&certificate)? { + log::error!("Sent a JWT using a revoked certificate!"); + return Ok( + HttpResponse::PreconditionFailed().json("Sent a JWT using a revoked certificate!") + ); + } + + let (key, alg) = match DecodingKey::from_ec_pem(&cert_bytes) { + Ok(key) => (key, Algorithm::ES256), + Err(e) => { + log::warn!("Failed to decode certificate as EC certificate {e}, trying RSA..."); + ( + DecodingKey::from_rsa_pem(&cert_bytes).expect("Failed to decode RSA certificate"), + Algorithm::RS256, + ) + } + }; + let mut validation = Validation::new(alg); + validation.validate_exp = false; + validation.required_spec_claims = HashSet::default(); + + let c = match jsonwebtoken::decode::(&body.payload, &key, &validation) { + Ok(c) => c, + Err(e) => { + log::error!("Failed to validate JWT! {e}"); + return Ok(HttpResponse::PreconditionFailed().json("Failed to validate JWT!")); + } + }; + + let res = actor + .send(energy_actor::SynchronizeDevice(device.id, c.claims.info)) + .await??; + + Ok(HttpResponse::Ok().json(res)) +} diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index f18e63f..fd3f7c8 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -194,6 +194,10 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/devices_api/mgmt/get_certificate", web::get().to(mgmt_controller::get_certificate), ) + .route( + "/devices_api/mgmt/sync", + web::post().to(mgmt_controller::sync_device), + ) // Web app .route("/", web::get().to(web_app_controller::root_index)) .route( diff --git a/python_device/src/api.py b/python_device/src/api.py index 6c66057..e06c9a3 100644 --- a/python_device/src/api.py +++ b/python_device/src/api.py @@ -91,3 +91,4 @@ def sync_device(dev_id: str, privkey): print(encoded) print(res) + print(res.text)