From c3cf7b4cc256d14e9dadbf10953eab288a5161c7 Mon Sep 17 00:00:00 2001 From: Tobias Langer Date: Thu, 13 Mar 2025 09:31:15 +0100 Subject: [PATCH 1/8] style: fix lint checks --- tests/common/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index c16e535..348f5a9 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -36,7 +36,7 @@ impl Testrunner { pub fn to_pathbuf(&self, file: &str) -> PathBuf { let destfile = String::from(file); - let destfile = destfile.split('/').last().unwrap(); + let destfile = destfile.split('/').next_back().unwrap(); let path = PathBuf::from(format!("{}/{}", self.dirpath, destfile)); copy(file, &path).unwrap(); From a80a2a730c817647eb9b2f954b4c1733e87f026b Mon Sep 17 00:00:00 2001 From: Tobias Langer Date: Thu, 13 Mar 2025 09:31:45 +0100 Subject: [PATCH 2/8] feat: add set-edge-ca-certificate command --- src/cli.rs | 24 ++++++++ src/file/mod.rs | 90 +++++++++++++++++++++++------ src/lib.rs | 113 +++++++++++++++++++++++++++++-------- tests/integration_tests.rs | 71 +++++++++++++++++++++++ 4 files changed, 256 insertions(+), 42 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 551ccfb..95adaa3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -179,6 +179,30 @@ pub enum IdentityConfig { #[arg(short = 'p', long = "pack-image", value_enum)] compress_image: Option, }, + /// set edge-ca certificates in order to support X.509 based DPS provisioning and certificate renewal via EST for web services provided by the device + SetEdgeCaCertificate { + /// path to intermediate full-chain-certificate pem file + #[arg(short = 'c', long = "intermediate-full-chain-cert")] + intermediate_full_chain_cert: PathBuf, + /// path to intermediate key pem file + #[arg(short = 'k', long = "intermediate-key")] + intermediate_key: PathBuf, + /// path to wic image file (optionally compressed with xz, bzip2 or gzip) + #[arg(short = 'i', long = "image")] + image: PathBuf, + /// subject name for the edge ca + #[arg(short = 's', long = "subject")] + subject: String, + /// period of validity in days + #[arg(short = 'D', long = "days")] + days: u32, + /// optional: generate bmap file (currently not working in docker image) + #[arg(short = 'b', long = "generate-bmap-file")] + generate_bmap: bool, + /// optional: pack image [xz, bzip2, gzip] (for xz default level '9' is used, which can be overwritten by setting 'XZ_COMPRESSION_LEVEL=') + #[arg(short = 'p', long = "pack-image", value_enum)] + compress_image: Option, + }, } #[derive(Parser, Debug)] diff --git a/src/file/mod.rs b/src/file/mod.rs index 2be0486..00db801 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -115,35 +115,91 @@ pub fn set_identity_config( copy_to_image(&file_copies, image_file) } -pub fn set_device_cert( - intermediate_full_chain_cert_path: Option<&Path>, - device_cert_path: &Path, - device_key_path: &Path, +struct IntermediateFullChainCertDescr<'a> { + src: &'a Path, + name: &'a str, +} + +struct CopyDescr<'a> { + src: &'a Path, + dest: &'a Path, +} + +fn set_cert( + intermediate_full_chain_cert: Option, + cert: CopyDescr, + key: CopyDescr, image_file: &Path, ) -> Result<()> { let mut copy_params = vec![ - FileCopyToParams::new( - device_cert_path, - Partition::cert, - Path::new("/priv/device_id_cert.pem"), - ), - FileCopyToParams::new( - device_key_path, - Partition::cert, - Path::new("/priv/device_id_cert_key.pem"), - ), + FileCopyToParams::new(cert.src, Partition::cert, cert.dest), + FileCopyToParams::new(key.src, Partition::cert, key.dest), ]; - if let Some(p) = intermediate_full_chain_cert_path { + if let Some(p) = intermediate_full_chain_cert { copy_params.append(&mut vec![ - FileCopyToParams::new(p, Partition::cert, Path::new("/priv/ca.crt.pem")), - FileCopyToParams::new(p, Partition::cert, Path::new("/ca/ca.crt")), + FileCopyToParams::new( + p.src, + Partition::cert, + Path::new(&format!("/priv/{}.crt.pem", p.name)), + ), + FileCopyToParams::new( + p.src, + Partition::cert, + Path::new(&format!("/ca/{}.crt", p.name)), + ), ]) } copy_to_image(©_params, image_file) } +pub fn set_device_cert( + intermediate_full_chain_cert_path: Option<&Path>, + device_cert_path: &Path, + device_key_path: &Path, + image_file: &Path, +) -> Result<()> { + let full_chain_descr = intermediate_full_chain_cert_path + .map(|p| IntermediateFullChainCertDescr { src: p, name: "ca" }); + + set_cert( + full_chain_descr, + CopyDescr { + src: device_cert_path, + dest: Path::new("/priv/device_id_cert.pem"), + }, + CopyDescr { + src: device_key_path, + dest: Path::new("/priv/device_id_cert_key.pem"), + }, + image_file, + ) +} + +pub fn set_edge_ca_cert( + intermediate_full_chain_cert_path: Option<&Path>, + device_cert_path: &Path, + device_key_path: &Path, + image_file: &Path, +) -> Result<()> { + let full_chain_descr = intermediate_full_chain_cert_path + .map(|p| IntermediateFullChainCertDescr { src: p, name: "ca" }); + + set_cert( + full_chain_descr, + CopyDescr { + src: device_cert_path, + dest: Path::new("/priv/edge_ca_cert.pem"), + }, + CopyDescr { + src: device_key_path, + dest: Path::new("/priv/edge_ca_cert_key.pem"), + }, + image_file, + ) +} + pub fn set_iot_hub_device_update_config(du_config_file: &Path, image_file: &Path) -> Result<()> { device_update::validate_config(du_config_file)?; diff --git a/src/lib.rs b/src/lib.rs index 06ba136..ee6d8dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,15 +14,15 @@ use cli::{ Docker::Inject, File::{CopyFromImage, CopyToImage}, IdentityConfig::{ - SetConfig, SetDeviceCertificate, SetDeviceCertificateNoEst, SetIotLeafSasConfig, - SetIotedgeGatewayConfig, + SetConfig, SetDeviceCertificate, SetDeviceCertificateNoEst, SetEdgeCaCertificate, + SetIotLeafSasConfig, SetIotedgeGatewayConfig, }, IotHubDeviceUpdate::{self, SetDeviceConfig as IotHubDeviceUpdateSet}, SshConfig::{SetCertificate, SetConnection}, }; use file::{compression::Compression, functions::FileCopyToParams}; use log::error; -use std::{fs, path::PathBuf}; +use std::{fs, path::Path, path::PathBuf}; use tokio::fs::remove_dir_all; use uuid::Uuid; @@ -150,6 +150,46 @@ where Ok(()) } +struct CertInfo { + cert_path: PathBuf, + key_path: PathBuf, +} + +struct CertificateOptions<'a> { + intermediate_full_chain_cert: &'a Path, + intermediate_key: &'a Path, + target_cert: &'a str, + target_key: &'a str, + subject: &'a str, + validity_days: u32, +} + +fn create_image_cert(image: &Path, cert_opts: CertificateOptions) -> Result { + let intermediate_full_chain_cert_str = + std::fs::read_to_string(cert_opts.intermediate_full_chain_cert) + .context("create_and_set_image_cert: couldn't read intermediate fullchain cert")?; + let intermediate_key_str = std::fs::read_to_string(cert_opts.intermediate_key) + .context("create_and_set_image_cert: couldn't read intermediate key")?; + let crypto = omnect_crypto::Crypto::new( + intermediate_key_str.as_bytes(), + intermediate_full_chain_cert_str.as_bytes(), + )?; + let (cert_pem, key_pem) = crypto + .create_cert_and_key(cert_opts.subject, &None, cert_opts.validity_days) + .context("create_and_set_image_cert: couldn't create device cert and key")?; + + let cert_path = file::get_file_path(image, cert_opts.target_cert)?; + let key_path = file::get_file_path(image, cert_opts.target_key)?; + + fs::write(&cert_path, cert_pem).context("create_and_set_image_cert: write device_cert_path")?; + fs::write(&key_path, key_pem).context("create_and_set_image_cert: write device_key_path")?; + + Ok(CertInfo { + cert_path: cert_path.to_path_buf(), + key_path: key_path.to_path_buf(), + }) +} + pub fn run() -> Result<()> { match cli::from_args() { Command::Docker(Inject { @@ -211,32 +251,55 @@ pub fn run() -> Result<()> { generate_bmap, compress_image, }) => { - let intermediate_full_chain_cert_str = - std::fs::read_to_string(&intermediate_full_chain_cert) - .context("couldn't read intermediate fullchain cert")?; - let intermediate_key_str = std::fs::read_to_string(intermediate_key) - .context("couldn't read intermediate key")?; - let crypto = omnect_crypto::Crypto::new( - intermediate_key_str.as_bytes(), - intermediate_full_chain_cert_str.as_bytes(), - )?; - let (device_cert_pem, device_key_pem) = crypto - .create_cert_and_key(&device_id, &None, days) - .context("couldn't create device cert and key")?; - - let device_cert_path = file::get_file_path(&image, "device_cert_path.pem")?; - let device_key_path = file::get_file_path(&image, "device_key_path.key.pem")?; - - fs::write(&device_cert_path, device_cert_pem) - .context("set_device_cert: write device_cert_path")?; - fs::write(&device_key_path, device_key_pem) - .context("set_device_cert: write device_key_path")?; + let cert_info = create_image_cert( + &image, + CertificateOptions { + intermediate_full_chain_cert: &intermediate_full_chain_cert, + intermediate_key: &intermediate_key, + target_cert: "device_cert_path.pem", + target_key: "device_key_path.key.pem", + subject: &device_id, + validity_days: days, + }, + ) + .context("set_edge_ca_certificate: could not create certificate")?; run_image_command(image, generate_bmap, compress_image, |img| { file::set_device_cert( Some(&intermediate_full_chain_cert), - &device_cert_path, - &device_key_path, + &cert_info.cert_path, + &cert_info.key_path, + img, + ) + })? + } + Command::Identity(SetEdgeCaCertificate { + intermediate_full_chain_cert, + intermediate_key, + image, + subject, + days, + generate_bmap, + compress_image, + }) => { + let cert_info = create_image_cert( + &image, + CertificateOptions { + intermediate_full_chain_cert: &intermediate_full_chain_cert, + intermediate_key: &intermediate_key, + target_cert: "edge_ca_cert_path.pem", + target_key: "edge_ca_path.key.pem", + subject: &subject, + validity_days: days, + }, + ) + .context("set_edge_ca_certificate: could not create certificate")?; + + run_image_command(image, generate_bmap, compress_image, |img| { + file::set_edge_ca_cert( + Some(&intermediate_full_chain_cert), + &cert_info.cert_path, + &cert_info.key_path, img, ) })? diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index a73af5f..0f68485 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -525,6 +525,77 @@ fn check_set_device_cert_no_est() { )); } +#[test] +fn check_set_edge_ca_cert() { + let tr = Testrunner::new(function_name!().split("::").last().unwrap()); + let image_path = tr.to_pathbuf("testfiles/image.wic"); + let intermediate_full_chain_crt_path = tr.to_pathbuf("testfiles/test-int-ca_fullchain.pem"); + let intermediate_full_chain_crt_key_path = tr.to_pathbuf("testfiles/test-int-ca.key"); + + let mut set_device_certificate = Command::cargo_bin("omnect-cli").unwrap(); + let assert = set_device_certificate + .arg("identity") + .arg("set-edge-ca-certificate") + .arg("-c") + .arg(&intermediate_full_chain_crt_path) + .arg("-k") + .arg(&intermediate_full_chain_crt_key_path) + .arg("-i") + .arg(&image_path) + .arg("-s") + .arg("edge-ca") + .arg("-D") + .arg("1") + .assert(); + assert.success(); + + let mut edge_ca_cert_out_path = tr.pathbuf(); + edge_ca_cert_out_path.push("dir1"); + create_dir_all(edge_ca_cert_out_path.clone()).unwrap(); + + let mut edge_ca_cert_key_out_path = edge_ca_cert_out_path.clone(); + let mut ca_crt_pem_out_path = edge_ca_cert_out_path.clone(); + let mut ca_pem_out_path = edge_ca_cert_out_path.clone(); + + edge_ca_cert_out_path.push("edge_ca_cert_out_path"); + let edge_ca_cert_out_path = edge_ca_cert_out_path.to_str().unwrap(); + + edge_ca_cert_key_out_path.push("edge_ca_cert_key_out_path"); + let edge_ca_cert_key_out_path = edge_ca_cert_key_out_path.to_str().unwrap(); + + ca_crt_pem_out_path.push("ca_crt_pem_out_path"); + let ca_crt_pem_out_path = ca_crt_pem_out_path.to_str().unwrap(); + + ca_pem_out_path.push("ca_pem_out_path"); + let ca_pem_out_path = ca_pem_out_path.to_str().unwrap(); + + let mut copy_from_img = Command::cargo_bin("omnect-cli").unwrap(); + let assert = copy_from_img + .arg("file") + .arg("copy-from-image") + .arg("-f") + .arg(format!( + "cert:/priv/edge_ca_cert.pem,{edge_ca_cert_out_path}" + )) + .arg("-f") + .arg(format!( + "cert:/priv/edge_ca_cert_key.pem,{edge_ca_cert_key_out_path}" + )) + .arg("-f") + .arg(format!("cert:/priv/ca.crt.pem,{ca_crt_pem_out_path}")) + .arg("-f") + .arg(format!("cert:/ca/ca.crt,{ca_pem_out_path}")) + .arg("-i") + .arg(&image_path) + .assert(); + assert.success(); + + assert!(file_diff::diff( + intermediate_full_chain_crt_path.to_str().unwrap(), + ca_crt_pem_out_path + )); +} + #[test] fn check_set_iot_hub_device_update_template() { let tr = Testrunner::new(function_name!().split("::").last().unwrap()); From edde89910d04af3910cff2df99036597ed86e60a Mon Sep 17 00:00:00 2001 From: Tobias Langer Date: Fri, 14 Mar 2025 11:24:34 +0100 Subject: [PATCH 3/8] docs: fix command description string. --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 95adaa3..045446f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -179,7 +179,7 @@ pub enum IdentityConfig { #[arg(short = 'p', long = "pack-image", value_enum)] compress_image: Option, }, - /// set edge-ca certificates in order to support X.509 based DPS provisioning and certificate renewal via EST for web services provided by the device + /// set edge-ca certificate that will be used for edge module device/server certificate generation. SetEdgeCaCertificate { /// path to intermediate full-chain-certificate pem file #[arg(short = 'c', long = "intermediate-full-chain-cert")] From 39751d5f6dc53b94fc0a448fe37e7dbf6553572c Mon Sep 17 00:00:00 2001 From: Tobias Langer Date: Fri, 14 Mar 2025 11:49:01 +0100 Subject: [PATCH 4/8] fix: rename subject argument to device_id --- src/cli.rs | 6 +++--- src/lib.rs | 4 ++-- tests/integration_tests.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 045446f..5c53260 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -190,9 +190,9 @@ pub enum IdentityConfig { /// path to wic image file (optionally compressed with xz, bzip2 or gzip) #[arg(short = 'i', long = "image")] image: PathBuf, - /// subject name for the edge ca - #[arg(short = 's', long = "subject")] - subject: String, + /// device id + #[arg(short = 'd', long = "device-id")] + device_id: String, /// period of validity in days #[arg(short = 'D', long = "days")] days: u32, diff --git a/src/lib.rs b/src/lib.rs index ee6d8dc..9af73aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,7 +277,7 @@ pub fn run() -> Result<()> { intermediate_full_chain_cert, intermediate_key, image, - subject, + device_id, days, generate_bmap, compress_image, @@ -289,7 +289,7 @@ pub fn run() -> Result<()> { intermediate_key: &intermediate_key, target_cert: "edge_ca_cert_path.pem", target_key: "edge_ca_path.key.pem", - subject: &subject, + subject: &device_id, validity_days: days, }, ) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 0f68485..f32b9be 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -542,7 +542,7 @@ fn check_set_edge_ca_cert() { .arg(&intermediate_full_chain_crt_key_path) .arg("-i") .arg(&image_path) - .arg("-s") + .arg("-d") .arg("edge-ca") .arg("-D") .arg("1") From a7dda066e921cbd75b371b3880ed757f37b80b06 Mon Sep 17 00:00:00 2001 From: Tobias Langer Date: Fri, 14 Mar 2025 11:50:20 +0100 Subject: [PATCH 5/8] fix: rename target key file name --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9af73aa..392b198 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,7 +288,7 @@ pub fn run() -> Result<()> { intermediate_full_chain_cert: &intermediate_full_chain_cert, intermediate_key: &intermediate_key, target_cert: "edge_ca_cert_path.pem", - target_key: "edge_ca_path.key.pem", + target_key: "edge_ca_key_path.key.pem", subject: &device_id, validity_days: days, }, From 6972240dfd8f50e830d466a332f7f5a08765cf3f Mon Sep 17 00:00:00 2001 From: Tobias Langer Date: Mon, 17 Mar 2025 13:15:29 +0100 Subject: [PATCH 6/8] chore: bump version to 0.25.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12bed4c..b41bcf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2401,7 +2401,7 @@ dependencies = [ [[package]] name = "omnect-cli" -version = "0.24.16" +version = "0.25.0" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index a11635a..2538910 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" name = "omnect-cli" readme = "README.md" repository = "https://github.com/omnect/omnect-cli" -version = "0.24.16" +version = "0.25.0" [dependencies] actix-web = "4.9" From 4d53225a565b1511aca5384c1f828fd42776b190 Mon Sep 17 00:00:00 2001 From: Tobias Langer Date: Mon, 17 Mar 2025 14:41:55 +0100 Subject: [PATCH 7/8] docs: add README entry for new command --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 054e35d..d2e0752 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ omnect-cli is a command-line tool to manage omnect-os empowered devices. It prov - Identity configuration: - Inject general identity configuration for AIS (Azure Identity Service) - Inject a device certificate and key + - Inject edge CA certificate for device/server certificate creation - Device Update for IoT Hub: - manage updates (create, import, remove) (https://learn.microsoft.com/en-us/azure/iot-hub-device-update/import-concepts) - inject configuration file `du-config.json` (https://docs.microsoft.com/en-us/azure/iot-hub-device-update/device-update-configuration-file) @@ -103,6 +104,20 @@ omnect-cli identity set-device-certificate-no-est --help **Note1**: "device_id" has to match the `registration_id` respectively the `device_id` configured in `config.toml`.
**Note2**: see [`config.toml.no-est.template`](conf/config.toml.no-est.template) as a corresponding `config.toml` in case of using `EST service`. +### Inject edge CA certificate for module device/server certificate generation + +Generate and set edge CA certificate for the generation of device and server +certificates. Technically, this command functions similarly `set-device-identity`: + + 1. generates device specific credentials from a given intermediate certificate and key + 2. injects credentials into a firmware image + +Detailed description: +```sh +omnect-cli identity set-edge-ca-certificate --help +``` +**Note**: "device_id" has to match the `registration_id` respectively the `device_id` configured in `config.toml`.
+ ## Device Update for IoT Hub ### Create import manifest This command creates the device update import manifest which is used later by the `import-update` command. From 2f6998b983e6ff7cb59ed3d9721f40594c44ce8c Mon Sep 17 00:00:00 2001 From: Tobias Langer Date: Wed, 19 Mar 2025 12:59:38 +0100 Subject: [PATCH 8/8] docs: adapt documentation for the new feature --- README.md | 8 ++++---- src/cli.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d2e0752..bbb294c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ omnect-cli is a command-line tool to manage omnect-os empowered devices. It prov - Identity configuration: - Inject general identity configuration for AIS (Azure Identity Service) - Inject a device certificate and key - - Inject edge CA certificate for device/server certificate creation + - Inject bootstrap certificate for edge CA issuance/renewal - Device Update for IoT Hub: - manage updates (create, import, remove) (https://learn.microsoft.com/en-us/azure/iot-hub-device-update/import-concepts) - inject configuration file `du-config.json` (https://docs.microsoft.com/en-us/azure/iot-hub-device-update/device-update-configuration-file) @@ -104,10 +104,10 @@ omnect-cli identity set-device-certificate-no-est --help **Note1**: "device_id" has to match the `registration_id` respectively the `device_id` configured in `config.toml`.
**Note2**: see [`config.toml.no-est.template`](conf/config.toml.no-est.template) as a corresponding `config.toml` in case of using `EST service`. -### Inject edge CA certificate for module device/server certificate generation +### Inject bootstrap certificate for edge CA issuance/renewal -Generate and set edge CA certificate for the generation of device and server -certificates. Technically, this command functions similarly `set-device-identity`: +Generates a bootstrap certificate for edge CA issuance and renewal for production over EST. Technically, this command functions similarly +`set-device-identity`: 1. generates device specific credentials from a given intermediate certificate and key 2. injects credentials into a firmware image diff --git a/src/cli.rs b/src/cli.rs index 5c53260..746e253 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -179,7 +179,7 @@ pub enum IdentityConfig { #[arg(short = 'p', long = "pack-image", value_enum)] compress_image: Option, }, - /// set edge-ca certificate that will be used for edge module device/server certificate generation. + /// generate and set bootstrap certificate for edge ca issuance/renewal. SetEdgeCaCertificate { /// path to intermediate full-chain-certificate pem file #[arg(short = 'c', long = "intermediate-full-chain-cert")]