Skip to content

Commit

Permalink
Impl tedge cert renew c8y
Browse files Browse the repository at this point in the history
Signed-off-by: Didier Wenzek <[email protected]>
  • Loading branch information
didier-wenzek committed Nov 28, 2024
1 parent dea9c13 commit 5335af8
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 37 deletions.
43 changes: 8 additions & 35 deletions crates/core/tedge/src/cli/certificate/c8y/download.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
use crate::cli::certificate::create_csr::CreateCsrCmd;
use crate::cli::certificate::c8y::create_device_csr;
use crate::cli::certificate::c8y::store_device_cert;
use crate::command::Command;
use crate::error;
use crate::get_webpki_error_from_reqwest;
use crate::log::MaybeFancy;
use crate::read_cert_to_string;
use crate::warning;
use crate::CertError;
use anyhow::Error;
use camino::Utf8PathBuf;
use certificate::CloudRootCerts;
use certificate::NewCertificateConfig;
use hyper::StatusCode;
use reqwest::blocking::Response;
use reqwest::header::CONTENT_TYPE;
use std::fs::OpenOptions;
use std::io::Write;
use std::time::Duration;
use tedge_config::HostPort;
use tedge_config::HTTPS_PORT;
use tedge_utils::paths::set_permission;
use url::Url;

/// Command to request and download a device certificate from Cumulocity
Expand Down Expand Up @@ -61,7 +57,11 @@ impl Command for DownloadCertCmd {
impl DownloadCertCmd {
fn download_device_certificate(&self) -> Result<(), Error> {
let (common_name, security_token) = self.get_registration_data()?;
let csr = self.create_device_csr(common_name.clone())?;
let csr = create_device_csr(
common_name.clone(),
self.key_path.clone(),
self.csr_path.clone(),
)?;

let http = self.root_certs.blocking_client();
let url = format!("https://{}/.well-known/est/simpleenroll", self.c8y_url);
Expand All @@ -72,7 +72,7 @@ impl DownloadCertCmd {
match result {
Ok(response) if response.status() == StatusCode::OK => {
if let Ok(cert) = response.text() {
self.store_device_cert(cert)?;
store_device_cert(&self.cert_path, cert)?;
return Ok(());
}
error!(
Expand Down Expand Up @@ -125,20 +125,6 @@ impl DownloadCertCmd {
Ok((device_id, security_token))
}

/// Create the device private key and CSR
fn create_device_csr(&self, common_name: String) -> Result<String, CertError> {
let config = NewCertificateConfig::default();
let create_cmd = CreateCsrCmd {
id: common_name,
csr_path: self.csr_path.clone(),
key_path: self.key_path.clone(),
user: "tedge".to_string(),
group: "tedge".to_string(),
};
create_cmd.create_certificate_signing_request(&config)?;
read_cert_to_string(&self.csr_path)
}

/// Post the device CSR
fn post_device_csr(
&self,
Expand All @@ -154,17 +140,4 @@ impl DownloadCertCmd {
.body(csr.to_string())
.send()
}

fn store_device_cert(&self, cert: String) -> Result<(), CertError> {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&self.cert_path)?;

file.write_all(cert.as_bytes())?;
file.sync_all()?;

set_permission(&file, 0o444)?;
Ok(())
}
}
42 changes: 42 additions & 0 deletions crates/core/tedge/src/cli/certificate/c8y/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
mod download;
mod renew;
mod upload;

use crate::cli::certificate::create_csr::CreateCsrCmd;
use crate::read_cert_to_string;
use crate::CertError;
use camino::Utf8PathBuf;
use certificate::NewCertificateConfig;
pub use download::DownloadCertCmd;
pub use renew::RenewCertCmd;
use std::fs::OpenOptions;
use std::io::Write;
use tedge_utils::paths::set_permission;
pub use upload::UploadCertCmd;

/// Create a device private key and CSR
fn create_device_csr(
common_name: String,
key_path: Utf8PathBuf,
csr_path: Utf8PathBuf,
) -> Result<String, CertError> {
let config = NewCertificateConfig::default();
let create_cmd = CreateCsrCmd {
id: common_name,
csr_path: csr_path.clone(),
key_path,
user: "tedge".to_string(),
group: "tedge".to_string(),
};
create_cmd.create_certificate_signing_request(&config)?;
read_cert_to_string(&csr_path)
}

/// Store a device certificate
fn store_device_cert(cert_path: &Utf8PathBuf, cert: String) -> Result<(), CertError> {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(cert_path)?;

file.write_all(cert.as_bytes())?;
file.sync_all()?;

set_permission(&file, 0o444)?;
Ok(())
}
116 changes: 116 additions & 0 deletions crates/core/tedge/src/cli/certificate/c8y/renew.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use crate::cli::certificate::c8y::create_device_csr;
use crate::cli::certificate::c8y::store_device_cert;
use crate::command::Command;
use crate::error;
use crate::get_webpki_error_from_reqwest;
use crate::log::MaybeFancy;
use anyhow::Error;
use camino::Utf8PathBuf;
use certificate::CloudRootCerts;
use hyper::header::AUTHORIZATION;
use hyper::header::CONTENT_TYPE;
use hyper::StatusCode;
use reqwest::blocking::Response;
use tedge_config::HostPort;
use tedge_config::HTTPS_PORT;
use tedge_config::MQTT_TLS_PORT;
use url::Url;

/// Command to renew a device certificate from Cumulocity
pub struct RenewCertCmd {
/// The device identifier to be used as the common name for the certificate
pub device_id: String,

/// Cumulocity MQTT end-point where the device is authenticated
pub c8y_mqtt: HostPort<MQTT_TLS_PORT>,

/// Cumulocity instance from where the device got his current certificate
pub c8y_url: HostPort<HTTPS_PORT>,

/// Root certificates used to authenticate the Cumulocity instance
pub root_certs: CloudRootCerts,

/// The path where the device certificate will be stored
pub cert_path: Utf8PathBuf,

/// The path where the device private key will be stored
pub key_path: Utf8PathBuf,

/// The path where the device CSR file will be stored
pub csr_path: Utf8PathBuf,
}

impl Command for RenewCertCmd {
fn description(&self) -> String {
format!("Renew the device certificate from {}", self.c8y_url)
}

fn execute(&self) -> Result<(), MaybeFancy<Error>> {
Ok(self.renew_device_certificate()?)
}
}

impl RenewCertCmd {
fn renew_device_certificate(&self) -> Result<(), Error> {
let csr = create_device_csr(
self.device_id.clone(),
self.key_path.clone(),
self.csr_path.clone(),
)?;

let jwt_token = self.get_jwt_token();

let http = self.root_certs.blocking_client();
let url = format!("https://{}/.well-known/est/simpleenroll", self.c8y_url);
let url = Url::parse(&url)?;
let result = self.post_device_csr(&http, &url, &jwt_token, &csr);
match result {
Ok(response) if response.status() == StatusCode::OK => {
if let Ok(cert) = response.text() {
store_device_cert(&self.cert_path, cert)?;
return Ok(());
}
error!(
"Fail to extract a certificate from the response returned by {}",
self.c8y_url
);
}
Ok(response) => {
error!(
"The device certificate cannot be renewed on {}: {:?}",
self.c8y_url,
response.status()
);
}
Err(err) => {
error!(
"Fail to connect to {}: {:?}",
self.c8y_url,
get_webpki_error_from_reqwest(err)
)
}
}

Ok(())
}

/// Post the device CSR
fn post_device_csr(
&self,
http: &reqwest::blocking::Client,
url: &Url,
jwt_token: &str,
csr: &str,
) -> Result<Response, reqwest::Error> {
http.post(url.clone())
.header(AUTHORIZATION, format!("Bearer {jwt_token}"))
.header(CONTENT_TYPE, "text/plain")
.body(csr.to_string())
.send()
}

fn get_jwt_token(&self) -> String {
let _ = self.c8y_mqtt.clone();
todo!()
}
}
40 changes: 38 additions & 2 deletions crates/core/tedge/src/cli/certificate/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ pub enum TEdgeCertCli {
},

/// Renew the device certificate
Renew,
Renew {
/// CA from which the certificate will be renew
#[arg(value_enum, default_value = "self-signed")]
ca: CertRenewalCA,

#[clap(long, hide = true)]
profile: Option<ProfileName>,
},

/// Show the device certificate, if any
Show,
Expand Down Expand Up @@ -140,13 +147,33 @@ impl BuildCommand for TEdgeCertCli {
cmd.into_boxed()
}

TEdgeCertCli::Renew => {
TEdgeCertCli::Renew {
ca: CertRenewalCA::SelfSigned,
..
} => {
let cmd = RenewCertCmd {
cert_path: config.device.cert_path.clone(),
key_path: config.device.key_path.clone(),
};
cmd.into_boxed()
}

TEdgeCertCli::Renew {
ca: CertRenewalCA::C8y,
profile,
} => {
let c8y_config = config.c8y.try_get(profile.as_deref())?;
let cmd = c8y::RenewCertCmd {
device_id: config.device.id.try_read(&config)?.clone(),
c8y_mqtt: c8y_config.mqtt.or_err()?.to_owned(),
c8y_url: c8y_config.http.or_err()?.to_owned(),
root_certs: config.cloud_root_certs(),
cert_path: config.device.cert_path.clone(),
key_path: config.device.key_path.clone(),
csr_path: config.device.csr_path.clone(),
};
cmd.into_boxed()
}
};
Ok(cmd)
}
Expand Down Expand Up @@ -227,3 +254,12 @@ pub enum DownloadCertCli {
profile: Option<ProfileName>,
},
}

#[derive(clap::ValueEnum, Clone, Debug)]
pub enum CertRenewalCA {
/// Self-signed a new device certificate
SelfSigned,

/// Renew the device certificate from Cumulocity
C8y,
}

0 comments on commit 5335af8

Please sign in to comment.