diff --git a/central_backend/Cargo.lock b/central_backend/Cargo.lock index 2470f55..8574760 100644 --- a/central_backend/Cargo.lock +++ b/central_backend/Cargo.lock @@ -598,9 +598,11 @@ dependencies = [ "openssl-sys", "rand", "reqwest", + "semver", "serde", "serde_json", "thiserror", + "uuid", ] [[package]] @@ -1828,6 +1830,9 @@ name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -2238,6 +2243,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +dependencies = [ + "getrandom", + "serde", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/central_backend/Cargo.toml b/central_backend/Cargo.toml index 0503bb9..0adc282 100644 --- a/central_backend/Cargo.toml +++ b/central_backend/Cargo.toml @@ -26,4 +26,6 @@ actix-identity = "0.7.1" actix-session = { version = "0.9.0", features = ["cookie-session"] } actix-cors = "0.7.0" actix-remote-ip = "0.1.0" -futures-util = "0.3.30" \ No newline at end of file +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 diff --git a/central_backend/src/app_config.rs b/central_backend/src/app_config.rs index b60584e..3974779 100644 --- a/central_backend/src/app_config.rs +++ b/central_backend/src/app_config.rs @@ -1,3 +1,4 @@ +use crate::devices::device::DeviceId; use clap::{Parser, Subcommand}; use std::path::{Path, PathBuf}; @@ -154,7 +155,7 @@ impl AppConfig { /// Get PKI root CA cert path pub fn root_ca_cert_path(&self) -> PathBuf { - self.pki_path().join("root_ca.pem") + self.pki_path().join("root_ca.crt") } /// Get PKI root CA CRL path @@ -169,7 +170,7 @@ impl AppConfig { /// Get PKI web CA cert path pub fn web_ca_cert_path(&self) -> PathBuf { - self.pki_path().join("web_ca.pem") + self.pki_path().join("web_ca.crt") } /// Get PKI web CA CRL path @@ -184,7 +185,7 @@ impl AppConfig { /// Get PKI devices CA cert path pub fn devices_ca_cert_path(&self) -> PathBuf { - self.pki_path().join("devices_ca.pem") + self.pki_path().join("devices_ca.crt") } /// Get PKI devices CA CRL path @@ -199,13 +200,33 @@ impl AppConfig { /// Get PKI server cert path pub fn server_cert_path(&self) -> PathBuf { - self.pki_path().join("server.pem") + self.pki_path().join("server.crt") } /// Get PKI server private key path pub fn server_priv_key_path(&self) -> PathBuf { self.pki_path().join("server.key") } + + /// Get devices configuration storage path + pub fn devices_config_path(&self) -> PathBuf { + self.storage_path().join("devices") + } + + /// Get device configuration path + pub fn device_config_path(&self, id: &DeviceId) -> PathBuf { + self.devices_config_path().join(format!("{}.conf", id.0)) + } + + /// Get device certificate path + pub fn device_cert_path(&self, id: &DeviceId) -> PathBuf { + self.devices_config_path().join(format!("{}.crt", id.0)) + } + + /// Get device CSR path + pub fn device_csr_path(&self, id: &DeviceId) -> PathBuf { + self.devices_config_path().join(format!("{}.csr", id.0)) + } } #[cfg(test)] diff --git a/central_backend/src/devices/device.rs b/central_backend/src/devices/device.rs new file mode 100644 index 0000000..2c65785 --- /dev/null +++ b/central_backend/src/devices/device.rs @@ -0,0 +1,52 @@ +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct DeviceInfo { + reference: String, + version: semver::Version, + max_relays: usize, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] +pub struct DeviceId(pub String); + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct Device { + /// The device ID + id: DeviceId, + /// Information about the device + device: DeviceInfo, + /// Name given to the device on the Web UI + name: String, + /// Description given to the device on the Web UI + description: String, + /// Specify whether the device is enabled or not + enabled: bool, + /// Specify whether the device has been validated or not + validated: bool, + /// Information about the relays handled by the device + relays: Vec, +} + +/// Structure that contains information about the minimal expected execution +/// time of a device +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct DailyMinRuntime { + min_runtime: usize, + reset_time: usize, + catch_up_hours: Vec, +} + +#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] +pub struct DeviceRelayID(uuid::Uuid); + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct DeviceRelay { + id: DeviceRelayID, + name: String, + enabled: bool, + priority: usize, + consumption: usize, + minimal_uptime: usize, + minimal_downtime: usize, + daily_runtime: Option, + depends_on: Vec, +} diff --git a/central_backend/src/devices/devices_list.rs b/central_backend/src/devices/devices_list.rs new file mode 100644 index 0000000..22ab413 --- /dev/null +++ b/central_backend/src/devices/devices_list.rs @@ -0,0 +1,36 @@ +use crate::app_config::AppConfig; +use crate::devices::device::{Device, DeviceId}; +use std::collections::HashMap; + +pub struct DevicesList(HashMap); + +impl DevicesList { + /// Load the list of devices. This method should be called only once during the whole execution + /// of the program + pub fn load() -> anyhow::Result { + let mut list = Self(HashMap::new()); + + for f in std::fs::read_dir(AppConfig::get().devices_config_path())? { + let f = f?.file_name(); + let f = f.to_string_lossy(); + + let dev_id = match f.strip_suffix(".conf") { + Some(s) => DeviceId(s.to_string()), + + // This is not a device configuration file + None => continue, + }; + + let device_conf = std::fs::read(AppConfig::get().device_config_path(&dev_id))?; + + list.0.insert(dev_id, serde_json::from_slice(&device_conf)?); + } + + Ok(list) + } + + /// Check if a device with a given id exists or not + pub fn exists(&self, id: &DeviceId) -> bool { + self.0.contains_key(id) + } +} diff --git a/central_backend/src/devices/mod.rs b/central_backend/src/devices/mod.rs new file mode 100644 index 0000000..4022388 --- /dev/null +++ b/central_backend/src/devices/mod.rs @@ -0,0 +1,2 @@ +pub mod device; +pub mod devices_list; diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index 593ddf1..186b119 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -1,16 +1,20 @@ use crate::constants; +use crate::devices::device::DeviceId; +use crate::devices::devices_list::DevicesList; use crate::energy::consumption; use crate::energy::consumption::EnergyConsumption; use actix::prelude::*; pub struct EnergyActor { curr_consumption: EnergyConsumption, + devices: DevicesList, } impl EnergyActor { pub async fn new() -> anyhow::Result { Ok(Self { curr_consumption: consumption::get_curr_consumption().await?, + devices: DevicesList::load()?, }) } @@ -62,3 +66,16 @@ impl Handler for EnergyActor { self.curr_consumption } } + +/// Get current consumption +#[derive(Message)] +#[rtype(result = "bool")] +pub struct CheckDeviceExists(DeviceId); + +impl Handler for EnergyActor { + type Result = bool; + + fn handle(&mut self, msg: CheckDeviceExists, _ctx: &mut Context) -> Self::Result { + self.devices.exists(&msg.0) + } +} diff --git a/central_backend/src/lib.rs b/central_backend/src/lib.rs index c75a6e9..16c1e6c 100644 --- a/central_backend/src/lib.rs +++ b/central_backend/src/lib.rs @@ -1,6 +1,7 @@ pub mod app_config; pub mod constants; pub mod crypto; +pub mod devices; pub mod energy; pub mod server; pub mod utils; diff --git a/central_backend/src/main.rs b/central_backend/src/main.rs index ad28125..fefc806 100644 --- a/central_backend/src/main.rs +++ b/central_backend/src/main.rs @@ -15,6 +15,7 @@ async fn main() -> std::io::Result<()> { // Initialize storage create_directory_if_missing(AppConfig::get().pki_path()).unwrap(); + create_directory_if_missing(AppConfig::get().devices_config_path()).unwrap(); // Initialize PKI pki::initialize_root_ca().expect("Failed to initialize Root CA!"); diff --git a/central_backend/src/server/custom_error.rs b/central_backend/src/server/custom_error.rs index a7569cb..0bdda98 100644 --- a/central_backend/src/server/custom_error.rs +++ b/central_backend/src/server/custom_error.rs @@ -103,6 +103,12 @@ impl From for HttpErr { } } +impl From for HttpErr { + fn from(value: openssl::error::ErrorStack) -> Self { + HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into()) + } +} + impl From for HttpErr { fn from(value: HttpResponse) -> Self { HttpErr::HTTPResponse(value) diff --git a/central_backend/src/server/devices_api/mgmt_controller.rs b/central_backend/src/server/devices_api/mgmt_controller.rs new file mode 100644 index 0000000..76fa5d6 --- /dev/null +++ b/central_backend/src/server/devices_api/mgmt_controller.rs @@ -0,0 +1,32 @@ +use crate::devices::device::DeviceInfo; +use crate::server::custom_error::HttpResult; +use actix_web::{web, HttpResponse}; +use openssl::x509::X509Req; + +#[derive(Debug, serde::Deserialize)] +pub struct EnrollRequest { + /// Device CSR + csr: String, + /// Associated device information + info: DeviceInfo, +} + +/// Enroll a new device +pub async fn enroll(req: web::Json) -> HttpResult { + let csr = match X509Req::from_pem(req.csr.as_bytes()) { + Ok(r) => r, + Err(e) => { + log::error!("Failed to parse given CSR! {e}"); + return Ok(HttpResponse::BadRequest().json("Failed to parse given CSR!")); + } + }; + + if !csr.verify(csr.public_key()?.as_ref())? { + log::error!("Invalid CSR signature!"); + return Ok(HttpResponse::BadRequest().json("Could not verify CSR signature!")); + } + + println!("{:#?}", &req); + + Ok(HttpResponse::Ok().json("go on")) +} diff --git a/central_backend/src/server/devices_api/mod.rs b/central_backend/src/server/devices_api/mod.rs index e7c75c0..416e535 100644 --- a/central_backend/src/server/devices_api/mod.rs +++ b/central_backend/src/server/devices_api/mod.rs @@ -1 +1,2 @@ +pub mod mgmt_controller; pub mod utils_controller; diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 1cc4d4e..6cfe6b5 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -3,7 +3,7 @@ use crate::constants; use crate::crypto::pki; use crate::energy::energy_actor::EnergyActorAddr; use crate::server::auth_middleware::AuthChecker; -use crate::server::devices_api::utils_controller; +use crate::server::devices_api::{mgmt_controller, utils_controller}; use crate::server::unsecure_server::*; use crate::server::web_api::*; use actix_cors::Cors; @@ -136,6 +136,10 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/devices_api/utils/time", web::get().to(utils_controller::curr_time), ) + .route( + "/devices_api/mgmt/enroll", + web::post().to(mgmt_controller::enroll), + ) }) .bind_openssl(&AppConfig::get().listen_address, builder)? .run() diff --git a/central_backend/src/server/unsecure_server/unsecure_pki_controller.rs b/central_backend/src/server/unsecure_server/unsecure_pki_controller.rs index e73c5b6..91f63b2 100644 --- a/central_backend/src/server/unsecure_server/unsecure_pki_controller.rs +++ b/central_backend/src/server/unsecure_server/unsecure_pki_controller.rs @@ -12,7 +12,7 @@ pub async fn serve_pki_file(path: web::Path) -> HttpResult { for f in std::fs::read_dir(AppConfig::get().pki_path())? { let f = f?; let file_name = f.file_name().to_string_lossy().to_string(); - if !file_name.ends_with(".crl") && !file_name.ends_with(".pem") { + if !file_name.ends_with(".crl") && !file_name.ends_with(".crt") { continue; } diff --git a/python_device/README.md b/python_device/README.md index 346b7e2..99920e2 100644 --- a/python_device/README.md +++ b/python_device/README.md @@ -1,5 +1,11 @@ # Python client +Reformat code: + +```bash +black src/*.py +``` + Run the client: ```bash diff --git a/python_device/src/api.py b/python_device/src/api.py index 2fb165d..7b50980 100644 --- a/python_device/src/api.py +++ b/python_device/src/api.py @@ -1,5 +1,7 @@ import requests from src.args import args +import src.constants as constants + def get_secure_origin() -> str: res = requests.get(f"{args.unsecure_origin}/secure_origin") @@ -7,8 +9,32 @@ def get_secure_origin() -> str: raise Exception(f"Get secure origin failed with status {res.status_code}") return res.text + def get_root_ca() -> str: - res = requests.get(f"{args.unsecure_origin}/pki/root_ca.pem") + res = requests.get(f"{args.unsecure_origin}/pki/root_ca.crt") if res.status_code < 200 or res.status_code > 299: raise Exception(f"Get root CA failed with status {res.status_code}") return res.text + + +def device_info(): + """ + Get device information to return with enrollment and sync requests + """ + return { + "reference": constants.DEV_REFERENCE, + "version": constants.DEV_VERSION, + "max_relays": len(args.relay_gpios_list), + } + + +def enroll_device(csr: str) -> str: + res = requests.post( + f"{args.secure_origin}/devices_api/mgmt/enroll", + json={"csr": csr, "info": device_info()}, + verify=args.root_ca_path, + ) + if res.status_code < 200 or res.status_code > 299: + print(res.text) + raise Exception(f"Enrollment failed with status {res.status_code}") + return res.text diff --git a/python_device/src/args.py b/python_device/src/args.py index 29f1ece..c5ae135 100644 --- a/python_device/src/args.py +++ b/python_device/src/args.py @@ -1,15 +1,25 @@ import argparse import os -parser = argparse.ArgumentParser( - description='SolarEnergy Python-based client') +parser = argparse.ArgumentParser(description="SolarEnergy Python-based client") -parser.add_argument("--unsecure_origin", help="Change unsecure API origin", default="http://localhost:8080") +parser.add_argument( + "--unsecure_origin", + help="Change unsecure API origin", + default="http://localhost:8080", +) parser.add_argument("--storage", help="Change storage location", default="storage") +parser.add_argument( + "--relay_gpios", + help="Comma-separated list of GPIO used to modify relays", + default="5,6,7", +) args = parser.parse_args() args.secure_origin_path = os.path.join(args.storage, "SECURE_ORIGIN") -args.root_ca_path = os.path.join(args.storage, "root_ca.pem") +args.root_ca_path = os.path.join(args.storage, "root_ca.crt") args.dev_priv_key_path = os.path.join(args.storage, "dev.key") -args.dev_csr_path = os.path.join(args.storage, "dev.csr") \ No newline at end of file +args.dev_csr_path = os.path.join(args.storage, "dev.csr") +args.dev_crt_path = os.path.join(args.storage, "dev.crt") +args.relay_gpios_list = list(map(lambda x: int(x), args.relay_gpios.split(","))) diff --git a/python_device/src/constants.py b/python_device/src/constants.py new file mode 100644 index 0000000..ca77ac6 --- /dev/null +++ b/python_device/src/constants.py @@ -0,0 +1,5 @@ +# Device reference. This value should never be changed +DEV_REFERENCE = "PyDev" + +# Current device version. Must follow semver semantic +DEV_VERSION = "0.0.1" diff --git a/python_device/src/main.py b/python_device/src/main.py index 72a087f..67b92b0 100644 --- a/python_device/src/main.py +++ b/python_device/src/main.py @@ -21,7 +21,6 @@ with open(args.secure_origin_path, "r") as f: print(f"Secure origin = {args.secure_origin}") - print("Check system root CA") if not os.path.isfile(args.root_ca_path): origin = api.get_root_ca() @@ -43,3 +42,12 @@ if not os.path.isfile(args.dev_csr_path): csr = pki.gen_csr(priv_key=priv_key, cn=f"PyDev {utils.rand_str(10)}") with open(args.dev_csr_path, "w") as f: f.write(csr) + +print("Check device enrollment...") +if not os.path.isfile(args.dev_crt_path): + with open(args.dev_csr_path, "r") as f: + csr = "".join(f.read()) + + print("Enrolling device...") + crt = api.enroll_device(csr) + print("res" + crt) diff --git a/python_device/src/pki.py b/python_device/src/pki.py index 5457ba8..61b2d2d 100644 --- a/python_device/src/pki.py +++ b/python_device/src/pki.py @@ -1,13 +1,16 @@ from OpenSSL import crypto + def gen_priv_key(): key = crypto.PKey() key.generate_key(crypto.TYPE_RSA, 2048) return crypto.dump_privatekey(crypto.FILETYPE_PEM, key).decode("utf-8") + def parse_priv_key(priv_key: str) -> crypto.PKey: return crypto.load_privatekey(crypto.FILETYPE_PEM, priv_key) + def gen_csr(priv_key: str, cn: str) -> str: priv_key = parse_priv_key(priv_key) @@ -15,5 +18,5 @@ def gen_csr(priv_key: str, cn: str) -> str: req.get_subject().CN = cn req.set_pubkey(priv_key) req.sign(priv_key, "sha256") - + return crypto.dump_certificate_request(crypto.FILETYPE_PEM, req).decode("utf-8") diff --git a/python_device/src/utils.py b/python_device/src/utils.py index cf91073..86c8085 100644 --- a/python_device/src/utils.py +++ b/python_device/src/utils.py @@ -1,5 +1,8 @@ import string import random + def rand_str(len: int) -> str: - return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(len)) \ No newline at end of file + return "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(len) + )