Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`.<br>
**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`.<br>

## 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.
Expand Down
24 changes: 24 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,30 @@ pub enum IdentityConfig {
#[arg(short = 'p', long = "pack-image", value_enum)]
compress_image: Option<Compression>,
},
/// 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<Compression>,
},
}

#[derive(Parser, Debug)]
Expand Down
90 changes: 73 additions & 17 deletions src/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IntermediateFullChainCertDescr>,
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(&copy_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)?;

Expand Down
113 changes: 88 additions & 25 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<CertInfo> {
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 {
Expand Down Expand Up @@ -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,
)
})?
Expand Down
2 changes: 1 addition & 1 deletion tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading