diff --git a/crates/core/tedge/src/cli/certificate/c8y/download.rs b/crates/core/tedge/src/cli/certificate/c8y/download.rs index 8f4b8eba6ca..192c13e5389 100644 --- a/crates/core/tedge/src/cli/certificate/c8y/download.rs +++ b/crates/core/tedge/src/cli/certificate/c8y/download.rs @@ -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 @@ -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); @@ -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!( @@ -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 { - 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, @@ -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(()) - } } diff --git a/crates/core/tedge/src/cli/certificate/c8y/mod.rs b/crates/core/tedge/src/cli/certificate/c8y/mod.rs index 2077ec4b974..b40aefd3e73 100644 --- a/crates/core/tedge/src/cli/certificate/c8y/mod.rs +++ b/crates/core/tedge/src/cli/certificate/c8y/mod.rs @@ -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 { + 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(()) +} diff --git a/crates/core/tedge/src/cli/certificate/c8y/renew.rs b/crates/core/tedge/src/cli/certificate/c8y/renew.rs new file mode 100644 index 00000000000..ba0b9fc40ef --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/c8y/renew.rs @@ -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, + + /// Cumulocity instance from where the device got his current certificate + pub c8y_url: HostPort, + + /// 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> { + 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 { + 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!() + } +} diff --git a/crates/core/tedge/src/cli/certificate/cli.rs b/crates/core/tedge/src/cli/certificate/cli.rs index 4b0c86d82fe..667460cf84b 100644 --- a/crates/core/tedge/src/cli/certificate/cli.rs +++ b/crates/core/tedge/src/cli/certificate/cli.rs @@ -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, + }, /// Show the device certificate, if any Show, @@ -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) } @@ -227,3 +254,12 @@ pub enum DownloadCertCli { profile: Option, }, } + +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum CertRenewalCA { + /// Self-signed a new device certificate + SelfSigned, + + /// Renew the device certificate from Cumulocity + C8y, +}