3 Commits

Author SHA1 Message Date
6bdebe6932 Revert bad change 2024-09-04 22:45:51 +02:00
1b02a812b4 Start to build sync route 2024-09-04 22:43:23 +02:00
ee938a3aa6 Encode JWT 2024-09-04 20:17:11 +02:00
9 changed files with 223 additions and 15 deletions

View File

@@ -510,6 +510,12 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@@ -612,6 +618,7 @@ dependencies = [
"foreign-types-shared", "foreign-types-shared",
"futures", "futures",
"futures-util", "futures-util",
"jsonwebtoken",
"lazy-regex", "lazy-regex",
"lazy_static", "lazy_static",
"libc", "libc",
@@ -1033,8 +1040,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1357,6 +1366,21 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "language-tags" name = "language-tags"
version = "0.3.2" version = "0.3.2"
@@ -1498,12 +1522,31 @@ dependencies = [
"tempfile", "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]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -1607,6 +1650,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@@ -2067,6 +2120,18 @@ dependencies = [
"libc", "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]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"

View File

@@ -33,4 +33,5 @@ lazy-regex = "3.2.0"
tokio = { version = "1.39.2", features = ["full"] } tokio = { version = "1.39.2", features = ["full"] }
tokio_schedule = "0.3.2" tokio_schedule = "0.3.2"
mime_guess = "2.0.5" mime_guess = "2.0.5"
rust-embed = "8.5.0" rust-embed = "8.5.0"
jsonwebtoken = { version = "9.3.0", features = ["use_pem"] }

View File

@@ -76,6 +76,17 @@ impl CertData {
crl: None, crl: None,
}) })
} }
/// Check if a certificate is revoked
pub fn is_revoked(&self, cert: &X509) -> anyhow::Result<bool> {
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 /// Generate private key
@@ -480,21 +491,10 @@ pub fn gen_certificate_for_device(csr: &X509Req) -> anyhow::Result<String> {
Ok(String::from_utf8(cert)?) Ok(String::from_utf8(cert)?)
} }
/// Check if a certificate is revoked
fn is_revoked(cert: &X509, ca: &CertData) -> anyhow::Result<bool> {
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 /// Revoke a certificate
pub fn revoke(cert: &X509, ca: &CertData) -> anyhow::Result<()> { pub fn revoke(cert: &X509, ca: &CertData) -> anyhow::Result<()> {
// Check if certificate is already revoked // Check if certificate is already revoked
if is_revoked(cert, ca)? { if ca.is_revoked(cert)? {
// No op // No op
return Ok(()); return Ok(());
} }

View File

@@ -238,3 +238,28 @@ impl Handler<DeleteDeviceRelay> for EnergyActor {
self.devices.relay_delete(msg.0) self.devices.relay_delete(msg.0)
} }
} }
#[derive(serde::Serialize)]
pub struct RelaySyncStatus {
enabled: bool,
}
/// Synchronize a device
#[derive(Message)]
#[rtype(result = "anyhow::Result<Vec<RelaySyncStatus>>")]
pub struct SynchronizeDevice(pub DeviceId, pub DeviceInfo);
impl Handler<SynchronizeDevice> for EnergyActor {
type Result = anyhow::Result<Vec<RelaySyncStatus>>;
fn handle(&mut self, msg: SynchronizeDevice, _ctx: &mut Context<Self>) -> 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)
}
}

View File

@@ -1,11 +1,14 @@
use crate::app_config::AppConfig; use crate::app_config::AppConfig;
use crate::crypto::pki;
use crate::devices::device::{DeviceId, DeviceInfo}; use crate::devices::device::{DeviceId, DeviceInfo};
use crate::energy::energy_actor; use crate::energy::energy_actor;
use crate::server::custom_error::HttpResult; use crate::server::custom_error::HttpResult;
use crate::server::WebEnergyActor; use crate::server::WebEnergyActor;
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
use openssl::nid::Nid; use openssl::nid::Nid;
use openssl::x509::X509Req; use openssl::x509::{X509Req, X509};
use std::collections::HashSet;
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
pub struct EnrollRequest { pub struct EnrollRequest {
@@ -124,3 +127,81 @@ pub async fn get_certificate(query: web::Query<ReqWithDevID>, actor: WebEnergyAc
.content_type("application/x-pem-file") .content_type("application/x-pem-file")
.body(cert)) .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<SyncRequest>, 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::<Claims>(&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))
}

View File

@@ -194,6 +194,10 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
"/devices_api/mgmt/get_certificate", "/devices_api/mgmt/get_certificate",
web::get().to(mgmt_controller::get_certificate), web::get().to(mgmt_controller::get_certificate),
) )
.route(
"/devices_api/mgmt/sync",
web::post().to(mgmt_controller::sync_device),
)
// Web app // Web app
.route("/", web::get().to(web_app_controller::root_index)) .route("/", web::get().to(web_app_controller::root_index))
.route( .route(

View File

@@ -1,5 +1,10 @@
# Python client # Python client
Dependencies:
```bash
apt install python3-jwt
```
Reformat code: Reformat code:
```bash ```bash

View File

@@ -1,6 +1,9 @@
import requests import requests
from src.args import args from src.args import args
import src.constants as constants import src.constants as constants
from cryptography.x509 import load_pem_x509_certificate
from cryptography import utils
import jwt
def get_secure_origin() -> str: def get_secure_origin() -> str:
@@ -70,3 +73,22 @@ def device_certificate() -> str:
print(res.text) print(res.text)
raise Exception(f"Failed to check enrollment with status {res.status_code}") raise Exception(f"Failed to check enrollment with status {res.status_code}")
return res.text return res.text
def sync_device(dev_id: str, privkey):
"""
Synchronize device with backend
"""
encoded = jwt.encode(
{"info": device_info()}, privkey, algorithm="RS256", headers={"kid": dev_id}
)
res = requests.post(
f"{args.secure_origin}/devices_api/mgmt/sync",
json={"payload": encoded},
verify=args.root_ca_path,
)
print(encoded)
print(res)
print(res.text)

View File

@@ -43,6 +43,10 @@ if not os.path.isfile(args.dev_priv_key_path):
with open(args.dev_priv_key_path, "w") as f: with open(args.dev_priv_key_path, "w") as f:
f.write(key) f.write(key)
with open(args.dev_priv_key_path, "r") as f:
args.priv_key = f.read()
print("Check CSR") print("Check CSR")
if not os.path.isfile(args.dev_csr_path): if not os.path.isfile(args.dev_csr_path):
print("Generate CSR...") print("Generate CSR...")
@@ -64,7 +68,6 @@ if status == "Unknown":
print("Device is unknown on the system, need to submit a CSR...") print("Device is unknown on the system, need to submit a CSR...")
with open(args.dev_csr_path, "r") as f: with open(args.dev_csr_path, "r") as f:
csr = "".join(f.read()) csr = "".join(f.read())
print("Enrolling device...") print("Enrolling device...")
api.enroll_device(csr) api.enroll_device(csr)
print("Done. Please accept the device on central system web UI") print("Done. Please accept the device on central system web UI")
@@ -85,4 +88,6 @@ if not os.path.isfile(args.dev_crt_path):
with open(args.dev_crt_path, "w") as f: with open(args.dev_crt_path, "w") as f:
f.write(cert) f.write(cert)
print("Done. ready to operate.") print("Done. ready to operate.")
api.sync_device(args.dev_id, args.priv_key)