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"
diff --git a/README.md b/README.md
index 054e35d..bbb294c 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 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)
@@ -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 bootstrap certificate for edge CA issuance/renewal
+
+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
+
+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.
diff --git a/src/cli.rs b/src/cli.rs
index 551ccfb..746e253 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,
},
+ /// 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")]
+ 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,
+ /// device id
+ #[arg(short = 'd', long = "device-id")]
+ device_id: 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..392b198 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,
+ device_id,
+ 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_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_edge_ca_cert(
+ Some(&intermediate_full_chain_cert),
+ &cert_info.cert_path,
+ &cert_info.key_path,
img,
)
})?
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();
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index a73af5f..f32b9be 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("-d")
+ .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());