diff --git a/Cargo.lock b/Cargo.lock index 5737da4..4f99ae4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1523,6 +1523,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", + "tempfile", "tokio", "webpki-roots", "x509-parser", diff --git a/Cargo.toml b/Cargo.toml index fa5c634..f9d1d7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,5 +25,8 @@ webpki-roots = "0.26.3" x509-parser = "0.16.0" zbus = "4.3.1" +[dev-dependencies] +tempfile = "3.4" + [features] test_mode = [] diff --git a/src/main.rs b/src/main.rs index 2a8a76e..8c98217 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,12 +7,12 @@ #![warn(clippy::print_stdout)] #![warn(clippy::print_stderr)] -use std::{io::ErrorKind, path::Path, time::Duration}; +use std::{io::ErrorKind, time::Duration}; use dbus::ServiceEvent; use log::*; -use eyre::{Context, OptionExt}; +use eyre::Context; use rumqttc::{ tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}, ConnectionError, @@ -20,81 +20,12 @@ use rumqttc::{ use tokio::sync::mpsc::Sender; mod dbus; +mod utils; +mod utils_test; -type Result = std::result::Result; - -// blocking -fn load_cert>(filename: P) -> Result> { - let certfile = - std::fs::File::open(&filename).context(format!("opening {:?}", filename.as_ref()))?; - let mut reader = std::io::BufReader::new(&certfile); - let mut all_certs: Vec = rustls_pemfile::certs(&mut reader).flatten().collect(); - - if all_certs.len() != 1 { - eyre::bail!( - "invalid number of certificates in {:?}, expected 1 got {:?}", - filename.as_ref(), - all_certs.len() - ); - } - - #[allow(clippy::unwrap_used)] // length checked - Ok(all_certs.pop().unwrap()) -} - -// blocking -fn load_private_key>(filename: P) -> Result> { - let keyfile = - std::fs::File::open(&filename).context(format!("read {:?}", filename.as_ref()))?; - let mut reader = std::io::BufReader::new(keyfile); +use utils::{load_cert, load_private_key, read_device_id, parse_payload}; - loop { - match rustls_pemfile::read_one(&mut reader)? { - Some(rustls_pemfile::Item::Pkcs1Key(key)) => return Ok(key.into()), - Some(rustls_pemfile::Item::Pkcs8Key(key)) => return Ok(key.into()), - Some(rustls_pemfile::Item::Sec1Key(key)) => return Ok(key.into()), - None => break, - _ => {} - } - } - - eyre::bail!( - "no keys found in {:?} (encrypted keys not supported)", - filename.as_ref() - ); -} - -fn read_device_id(cert: &CertificateDer) -> Result { - use x509_parser::prelude::*; - - let (_, res) = X509Certificate::from_der(cert)?; - - let common_name = res - .subject - .iter_common_name() - .flat_map(|c| c.as_str()) - .next(); - - Ok(common_name - .ok_or(eyre::eyre!("Could not extract uuid from certificate CN"))? - .to_owned()) -} - -fn parse_payload(payload: &[u8]) -> Result<(String, serde_json::Value)> { - let mut payload_json: serde_json::Value = serde_json::from_slice(payload)?; - - let args_json = payload_json - .pointer_mut("/args") - .ok_or_eyre("message payload did not include `args`")? - .take(); - - let command = payload_json - .pointer("/command") - .and_then(|s| s.as_str().map(|s| s.to_owned())) - .ok_or_eyre("message payload did not include `command`")?; - - Ok((command, args_json)) -} +type Result = std::result::Result; async fn run() -> Result<()> { let mqtt_hostname = std::env::var("TZN_MQTT_HOST").unwrap_or("mqtt.dev.torizon.io".to_owned()); diff --git a/src/test_data/client.crt b/src/test_data/client.crt new file mode 100644 index 0000000..e4a66ba --- /dev/null +++ b/src/test_data/client.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBdTCCARygAwIBAgIUXTaOb4/pIH1jZ+RyhAZvdi41/0QwCgYIKoZIzj0EAwIw +GTEXMBUGA1UEAwwOb3RhLWRldmljZXMtQ0EwIBcNMjQwODE5MTQxMzQ3WhgPMjEy +NDA4MTkxNDEzNDdaMC8xLTArBgNVBAMTJDg4OGIyNTQ1LWEzMGYtNDg5Ni1hMzc3 +LWI0N2M0YjcyNTM3NDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMQh0ZB30O5x +Pp7k5vICJ0QOdxoubO0VtTCJX+zQ2oCmjFBatfUH5gjuNsdmCw1zf7FeQCDyYnP9 +OQRoHLPBcwyjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEF +BQcDAjAKBggqhkjOPQQDAgNHADBEAiBmj6SenB1MEca664lGUkDIdsPzYAQ7G1Ml +tipTcMGIpwIgPbJMmfRXiSZjLGjdmhr43cG8S3ALrcXIHDyQdVmA8lI= +-----END CERTIFICATE----- diff --git a/src/test_data/client.key b/src/test_data/client.key new file mode 100644 index 0000000..43b3a2d --- /dev/null +++ b/src/test_data/client.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIM5yZsvOEcDQY571vHH6UJQwKPjvyoYgcgwKF4pwu6tgoAoGCCqGSM49 +AwEHoUQDQgAExCHRkHfQ7nE+nuTm8gInRA53Gi5s7RW1MIlf7NDagKaMUFq19Qfm +CO42x2YLDXN/sV5AIPJic/05BGgcs8FzDA== +-----END EC PRIVATE KEY----- diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..820e4f6 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,83 @@ +// Copyright 2024 Toradex A.G. +// SPDX-License-Identifier: Apache-2.0 + +use std::fs::File; +use std::io::BufReader; +use std::path::Path; +use rustls_pemfile; +use eyre::{Context, Result, OptionExt}; +use x509_parser::prelude::*; +use serde_json; +use crate::CertificateDer; +use crate::PrivateKeyDer; + +// blocking +pub fn load_cert>(filename: P) -> Result> { + let certfile = + File::open(&filename).context(format!("opening {:?}", filename.as_ref()))?; + let mut reader = BufReader::new(&certfile); + let mut all_certs: Vec = rustls_pemfile::certs(&mut reader).flatten().collect(); + + if all_certs.len() != 1 { + eyre::bail!( + "invalid number of certificates in {:?}, expected 1 got {:?}", + filename.as_ref(), + all_certs.len() + ); + } + + #[allow(clippy::unwrap_used)] // length checked + Ok(all_certs.pop().unwrap()) +} + +// blocking +pub fn load_private_key>(filename: P) -> Result> { + let keyfile = + File::open(&filename).context(format!("read {:?}", filename.as_ref()))?; + let mut reader = BufReader::new(keyfile); + + loop { + match rustls_pemfile::read_one(&mut reader)? { + Some(rustls_pemfile::Item::Pkcs1Key(key)) => return Ok(key.into()), + Some(rustls_pemfile::Item::Pkcs8Key(key)) => return Ok(key.into()), + Some(rustls_pemfile::Item::Sec1Key(key)) => return Ok(key.into()), + None => break, + _ => {} + } + } + + eyre::bail!( + "no keys found in {:?} (encrypted keys not supported)", + filename.as_ref() + ); +} + +pub fn read_device_id(cert: &CertificateDer) -> Result { + let (_, res) = X509Certificate::from_der(cert)?; + + let common_name = res + .subject + .iter_common_name() + .flat_map(|c| c.as_str()) + .next(); + + Ok(common_name + .ok_or(eyre::eyre!("Could not extract uuid from certificate CN"))? + .to_owned()) +} + +pub fn parse_payload(payload: &[u8]) -> Result<(String, serde_json::Value)> { + let mut payload_json: serde_json::Value = serde_json::from_slice(payload)?; + + let args_json = payload_json + .pointer_mut("/args") + .ok_or_eyre("message payload did not include `args`")? + .take(); + + let command = payload_json + .pointer("/command") + .and_then(|s| s.as_str().map(|s| s.to_owned())) + .ok_or_eyre("message payload did not include `command`")?; + + Ok((command, args_json)) +} diff --git a/src/utils_test.rs b/src/utils_test.rs new file mode 100644 index 0000000..9dd782e --- /dev/null +++ b/src/utils_test.rs @@ -0,0 +1,65 @@ +// Copyright 2024 Toradex A.G. +// SPDX-License-Identifier: Apache-2.0 + +use std::path::{Path}; + +use crate::utils::{load_cert, load_private_key, read_device_id, parse_payload}; + +#[cfg(test)] +mod tests { + use super::*; + + + #[test] + fn test_load_cert() { + let cert_path = Path::new("src/test_data/client.crt"); + let result = load_cert(cert_path); + assert!(result.is_ok(), "Failed to load certificate: {:?}", result.err()); + + let cert = result.expect("Valid certificate"); + assert!(cert.len() > 0, "Certificate data should not be empty"); + } + + #[test] + fn test_load_private_key() { + let key_path = Path::new("src/test_data/client.key"); + let result = load_private_key(key_path); + assert!(result.is_ok(), "Failed to load private key: {:?}", result.err()); + } + + #[test] + fn test_read_device_id() { + let cert_path = Path::new("src/test_data/client.crt"); + let cert = load_cert(cert_path).expect("Failed to load cert"); + let result = read_device_id(&cert); + assert!(result.is_ok(), "Failed to read device id: {:?}", result.err()); + + let device_id = result.expect("Valid device ID"); + assert!(!device_id.is_empty(), "Device ID should not be empty"); + } + + #[test] + fn test_parse_payload() { + let payload = br#"{"command": "test", "args": {"key": "value"}}"#; + let result = parse_payload(payload); + assert!(result.is_ok(), "Failed to parse payload: {:?}", result.err()); + + let (command, args) = result.expect("Valid command and args"); + assert_eq!(command, "test"); + assert_eq!(args["key"], "value"); + } + + #[test] + fn test_parse_payload_missing_args() { + let payload = br#"{"command": "test"}"#; + let result = parse_payload(payload); + assert!(result.is_err(), "Expected error for missing `args`, got: {:?}", result); + } + + #[test] + fn test_parse_payload_missing_command() { + let payload = br#"{"args": {"key": "value"}}"#; + let result = parse_payload(payload); + assert!(result.is_err(), "Expected error for missing `command`, got: {:?}", result); + } +}