diff options
author | Lukasz Woznicki <75632179+makr11st@users.noreply.github.com> | 2021-11-24 20:54:56 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-24 20:54:56 +0000 |
commit | a4ffeccf60090e4456755bc53a6e3b8c8038e855 (patch) | |
tree | 9583f187114913a92866571920dd3bb205bd50a3 /crates/core/tedge/src/cli | |
parent | 8217e80670e76dbf9168780f5e0545355a39f8f3 (diff) |
Restructure directories of the workspace (#559)
* Restructure directories of the workspace
* Rename c8y_translator_lib to c8y_translator
* Update comment on how to get dummy plugin path
Signed-off-by: Lukasz Woznicki <lukasz.woznicki@softwareag.com>
Diffstat (limited to 'crates/core/tedge/src/cli')
33 files changed, 3192 insertions, 0 deletions
diff --git a/crates/core/tedge/src/cli/certificate/cli.rs b/crates/core/tedge/src/cli/certificate/cli.rs new file mode 100644 index 00000000..7ef78fb5 --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/cli.rs @@ -0,0 +1,87 @@ +use super::{create::CreateCertCmd, remove::RemoveCertCmd, show::ShowCertCmd, upload::*}; + +use crate::command::{BuildCommand, BuildContext, Command}; +use crate::ConfigError; + +use structopt::StructOpt; +use tedge_config::*; + +#[derive(StructOpt, Debug)] +pub enum TEdgeCertCli { + /// Create a self-signed device certificate + Create { + /// The device identifier to be used as the common name for the certificate + #[structopt(long = "device-id")] + id: String, + }, + + /// Show the device certificate, if any + Show, + + /// Remove the device certificate + Remove, + + /// Upload root certificate + Upload(UploadCertCli), +} + +impl BuildCommand for TEdgeCertCli { + fn build_command(self, context: BuildContext) -> Result<Box<dyn Command>, ConfigError> { + let config = context.config_repository.load()?; + + let cmd = match self { + TEdgeCertCli::Create { id } => { + let cmd = CreateCertCmd { + id, + cert_path: config.query(DeviceCertPathSetting)?, + key_path: config.query(DeviceKeyPathSetting)?, + user_manager: context.user_manager, + }; + cmd.into_boxed() + } + + TEdgeCertCli::Show => { + let cmd = ShowCertCmd { + cert_path: config.query(DeviceCertPathSetting)?, + }; + cmd.into_boxed() + } + + TEdgeCertCli::Remove => { + let cmd = RemoveCertCmd { + cert_path: config.query(DeviceCertPathSetting)?, + key_path: config.query(DeviceKeyPathSetting)?, + user_manager: context.user_manager, + }; + cmd.into_boxed() + } + + TEdgeCertCli::Upload(cmd) => { + let cmd = match cmd { + UploadCertCli::C8y { username } => UploadCertCmd { + device_id: config.query(DeviceIdSetting)?, + path: config.query(DeviceCertPathSetting)?, + host: config.query(C8yUrlSetting)?, + username, + }, + }; + cmd.into_boxed() + } + }; + + Ok(cmd) + } +} + +#[derive(StructOpt, Debug)] +pub enum UploadCertCli { + /// Upload root certificate to Cumulocity + /// + /// The command will upload root certificate to Cumulocity. + C8y { + #[structopt(long = "user")] + /// Provided username should be a Cumulocity user with tenant management permissions. + /// The password is requested on /dev/tty, unless the $C8YPASS env var is set to the user password. + username: String, + }, +} diff --git a/crates/core/tedge/src/cli/certificate/create.rs b/crates/core/tedge/src/cli/certificate/create.rs new file mode 100644 index 00000000..560c2a8f --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/create.rs @@ -0,0 +1,188 @@ +use super::error::CertError; +use crate::command::Command; +use certificate::{KeyCertPair, NewCertificateConfig}; +use std::{ + fs::{File, OpenOptions}, + io::prelude::*, + path::Path, +}; +use tedge_config::*; +use tedge_users::UserManager; +use tedge_utils::paths::{set_permission, validate_parent_dir_exists}; + +/// Create a self-signed device certificate +pub struct CreateCertCmd { + /// The device identifier + pub id: String, + + /// The path where the device certificate will be stored + pub cert_path: FilePath, + + /// The path where the device private key will be stored + pub key_path: FilePath, + + /// The UserManager required to change effective user id + pub user_manager: UserManager, +} + +impl Command for CreateCertCmd { + fn description(&self) -> String { + format!("create a test certificate for the device {}.", self.id) + } + + fn execute(&self) -> anyhow::Result<()> { + let config = NewCertificateConfig::default(); + let () = self.create_test_certificate(&config)?; + Ok(()) + } +} + +impl CreateCertCmd { + fn create_test_certificate(&self, config: &NewCertificateConfig) -> Result<(), CertError> { + let _user_guard = self.user_manager.become_user(tedge_users::BROKER_USER)?; + + validate_parent_dir_exists(&self.cert_path).map_err(CertError::CertPathError)?; + validate_parent_dir_exists(&self.key_path).map_err(CertError::KeyPathError)?; + + let cert = KeyCertPair::new_selfsigned_certificate(config, &self.id)?; + + // Creating files with permission 644 + let mut cert_file = create_new_file(&self.cert_path) + .map_err(|err| err.cert_context(self.cert_path.clone()))?; + let mut key_file = create_new_file(&self.key_path) + .map_err(|err| err.key_context(self.key_path.clone()))?; + + let cert_pem = cert.certificate_pem_string()?; + cert_file.write_all(cert_pem.as_bytes())?; + cert_file.sync_all()?; + + // Prevent the certificate to be overwritten + set_permission(&cert_file, 0o444)?; + + { + // Make sure the key is secret, before write + set_permission(&key_file, 0o600)?; + + // Zero the private key on drop + let cert_key = cert.private_key_pem_string()?; + key_file.write_all(cert_key.as_bytes())?; + key_file.sync_all()?; + + // Prevent the key to be overwritten + set_permission(&key_file, 0o400)?; + } + + Ok(()) + } +} + +fn create_new_file(path: impl AsRef<Path>) -> Result<File, CertError> { + Ok(OpenOptions::new().write(true).create_new(true).open(path)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use std::fs; + use tedge_users::UserManager; + use tempfile::*; + + #[test] + fn basic_usage() { + let dir = tempdir().unwrap(); + let cert_path = temp_file_path(&dir, "my-device-cert.pem"); + let key_path = temp_file_path(&dir, "my-device-key.pem"); + let id = "my-device-id"; + + let cmd = CreateCertCmd { + id: String::from(id), + cert_path: cert_path.clone(), + key_path: key_path.clone(), + user_manager: UserManager::new(), + }; + + assert_matches!( + cmd.create_test_certificate(&NewCertificateConfig::default()), + Ok(()) + ); + assert_eq!(parse_pem_file(&cert_path).unwrap().tag, "CERTIFICATE"); + assert_eq!(parse_pem_file(&key_path).unwrap().tag, "PRIVATE KEY"); + } + + #[test] + fn check_certificate_is_not_overwritten() { + let dir = tempdir().unwrap(); + + let cert_path = temp_file_path(&dir, "my-device-cert.pem"); + let key_path = temp_file_path(&dir, "my-device-key.pem"); + + let cert_content = "some cert content"; + let key_content = "some key content"; + + fs::write(&cert_path, cert_content).unwrap(); + fs::write(&key_path, key_content).unwrap(); + + let cmd = CreateCertCmd { + id: "my-device-id".into(), + cert_path: cert_path.clone(), + key_path: key_path.clone(), + user_manager: UserManager::new(), + }; + + assert!(cmd + .create_test_certificate(&NewCertificateConfig::default()) + .ok() + .is_none()); + + assert_eq!(fs::read(&cert_path).unwrap(), cert_content.as_bytes()); + assert_eq!(fs::read(&key_path).unwrap(), key_content.as_bytes()); + } + + #[test] + fn create_certificate_in_non_existent_directory() { + let dir = tempdir().unwrap(); + let key_path = temp_file_path(&dir, "my-device-key.pem"); + let cert_path = FilePath::from("/non/existent/cert/path"); + + let cmd = CreateCertCmd { + id: "my-device-id".into(), + cert_path, + key_path, + user_manager: UserManager::new(), + }; + + let cert_error = cmd + .create_test_certificate(&NewCertificateConfig::default()) + .unwrap_err(); + assert_matches!(cert_error, CertError::CertPathError { .. }); + } + + #[test] + fn create_key_in_non_existent_directory() { + let dir = tempdir().unwrap(); + let cert_path = temp_file_path(&dir, "my-device-cert.pem"); + let key_path = FilePath::from("/non/existent/key/path"); + + let cmd = CreateCertCmd { + id: "my-device-id".into(), + cert_path, + key_path, + user_manager: UserManager::new(), + }; + + let cert_error = cmd + .create_test_certificate(&NewCertificateConfig::default()) + .unwrap_err(); + assert_matches!(cert_error, CertError::KeyPathError { .. }); + } + + fn temp_file_path(dir: &TempDir, filename: &str) -> FilePath { + dir.path().join(filename).into() + } + + fn parse_pem_file(path: impl AsRef<Path>) -> Result<pem::Pem, String> { + let content = fs::read(path).map_err(|err| err.to_string())?; + pem::parse(content).map_err(|err| err.to_string()) + } +} diff --git a/crates/core/tedge/src/cli/certificate/error.rs b/crates/core/tedge/src/cli/certificate/error.rs new file mode 100644 index 00000000..41e03c9a --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/error.rs @@ -0,0 +1,172 @@ +use reqwest::StatusCode; +use std::error::Error; +use tedge_config::FilePath; +use tedge_users::UserSwitchError; +use tedge_utils::paths::PathsError; + +#[derive(thiserror::Error, Debug)] +pub enum CertError { + #[error( + r#"A certificate already exists and would be overwritten. + Existing file: "{path}" + Run `tedge cert remove` first to generate a new certificate. + "# + )] + CertificateAlreadyExists { path: FilePath }, + + #[error( + r#"No certificate has been attached to that device. + Missing file: {path:?} + Run `tedge cert create` to generate a new certificate. + "# + )] + CertificateNotFound { path: FilePath }, + + #[error( + r#"No private key has been attached to that device. + Missing file: {path:?} + Run `tedge cert create` to generate a new key and certificate. + "# + )] + KeyNotFound { path: FilePath }, + + #[error( + r#"A private key already exists and would be overwritten. + Existing file: {path:?} + Run `tedge cert remove` first to generate a new certificate and private key. + "# + )] + KeyAlreadyExists { path: FilePath }, + + #[error(transparent)] + ConfigError(#[from] crate::ConfigError), + + #[error("I/O error")] + IoError(#[from] std::io::Error), + + #[error("Invalid device.cert.path path: {0}")] + CertPathError(PathsError), + + #[error("Invalid device.key.path path: {0}")] + KeyPathError(PathsError), + + #[error(transparent)] + CertificateError(#[from] certificate::CertificateError), + + #[error( + r#"Certificate read error at: {1:?} + Run `tedge cert create` if you want to create a new certificate."# + )] + CertificateReadFailed(#[source] std::io::Error, String), + + #[error(transparent)] + PathsError(#[from] PathsError), + + #[error(transparent)] + ReqwestError(#[from] reqwest::Error), + + #[error("Request returned with code: {0}")] + StatusCode(StatusCode), + + #[error(transparent)] + UrlParseError(#[from] url::ParseError), + + #[error(transparent)] + UserSwitchError(#[from] UserSwitchError), + + #[error("HTTP Connection Problem: {msg} \nHint: {hint}")] + WebpkiValidation { hint: String, msg: String }, +} + +impl CertError { + /// Improve the error message in case the error in a IO error on the certificate file. + pub fn cert_context(self, path: FilePath) -> CertError { + match self { + CertError::IoError(ref err) => match err.kind() { + std::io::ErrorKind::AlreadyExists => CertError::CertificateAlreadyExists { path }, + std::io::ErrorKind::NotFound => CertError::CertificateNotFound { path }, + _ => self, + }, + _ => self, + } + } + + /// Improve the error message in case the error in a IO error on the private key file. + pub fn key_context(self, path: FilePath) -> CertError { + match self { + CertError::IoError(ref err) => match err.kind() { + std::io::ErrorKind::AlreadyExists => CertError::KeyAlreadyExists { path }, + std::io::ErrorKind::NotFound => CertError::KeyNotFound { path }, + _ => self, + }, + _ => self, + } + } +} + +// Our source of error here is quite deep into the dependencies and we need to dig through that to get to our certificates validator errors which are Box<&dyn Error> through 3-4 levels +// source: hyper::Error( +// Connect, +// Custom { +// kind: Other, +// error: Custom { +// kind: InvalidData, +// error: WebPKIError( +// ..., // This is where we need to get +// ), +// }, +// }, +// ) +// This chain may break if underlying crates change. +pub(crate) fn get_webpki_error_from_reqwest(err: reqwest::Error) -> CertError { + if let Some(rustls::TLSError::WebPKIError(cert_validation_error)) = err + // get `hyper::Error::Connect` + .source() + .and_then(|hyper_error| hyper_error.downcast_ref::<hyper::Error>()) + .and_then(|hyper_error| hyper_error.source()) + // Surprise: `Custom` type is `std::io::Error`; this is our first `Custom`. + .and_then(|connect_error| connect_error.downcast_ref::<std::io::Error>()) + // A shortcut to get ref to our error 2 layers down. + .and_then(|custom_error| custom_error.get_ref()) + // This is our second `Custom`. + .and_then(|custom_error2| custom_error2.downcast_ref::<std::io::Error>()) + // Get final error type from `Custom`. + .and_then(|custom_error2| custom_error2.get_ref()) + .and_then(|webpki_error| webpki_error.downcast_ref::<rustls::TLSError>()) + { + match cert_validation_error { + webpki::Error::CAUsedAsEndEntity => CertError::WebpkiValidation { + hint: "A CA certificate is used as an end-entity server certificate. Make sure that the certificate used is an end-entity certificate signed by CA certificate.".into(), + msg: cert_validation_error.to_string(), + }, + + webpki::Error::CertExpired => CertError::WebpkiValidation { + hint: "The server certificate has expired, the time it is being validated for is later than the certificate's `notAfter` time." + .into(), + msg: cert_validation_error.to_string(), + }, + + webpki::Error::CertNotValidYet => CertError::WebpkiValidation { + hint: "The server certificate is not valid yet, the time it is being validated for is earlier than the certificate's `notBefore` time.".into(), + msg: cert_validation_error.to_string(), + }, + + webpki::Error::EndEntityUsedAsCA => CertError::WebpkiValidation { + hint: "An end-entity certificate is used as a server CA certificate. Make sure that the certificate used is signed by a correct CA certificate.".into(), + msg: cert_validation_error.to_string(), + }, + + webpki::Error::InvalidCertValidity => CertError::WebpkiValidation { + hint: "The server certificate validity period (`notBefore`, `notAfter`) is invalid, maybe the `notAfter` time is earlier than the `notBefore` time.".into(), + msg: cert_validation_error.to_string(), + }, + + _ => CertError::WebpkiValidation { + hint: "Server certificate validation error.".into(), + msg: cert_validation_error.to_string(), + }, + } + } else { + CertError::ReqwestError(err) // any other Error type than `hyper::Error` + } +} diff --git a/crates/core/tedge/src/cli/certificate/mod.rs b/crates/core/tedge/src/cli/certificate/mod.rs new file mode 100644 index 00000000..e37f2fb6 --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/mod.rs @@ -0,0 +1,8 @@ +pub use self::cli::TEdgeCertCli; + +mod cli; +mod create; +mod error; +mod remove; +mod show; +mod upload; diff --git a/crates/core/tedge/src/cli/certificate/remove.rs b/crates/core/tedge/src/cli/certificate/remove.rs new file mode 100644 index 00000000..e46acfbf --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/remove.rs @@ -0,0 +1,38 @@ +use super::error::CertError; +use crate::command::Command; +use tedge_config::*; +use tedge_users::UserManager; +use tedge_utils::paths::ok_if_not_found; + +/// Remove the device certificate +pub struct RemoveCertCmd { + /// The path of the certificate to be removed + pub cert_path: FilePath, + + /// The path of the private key to be removed + pub key_path: FilePath, + + /// The UserManager required to change effective user id. + pub user_manager: UserManager, +} + +impl Command for RemoveCertCmd { + fn description(&self) -> String { + "remove the device certificate".into() + } + + fn execute(&self) -> anyhow::Result<()> { + let () = self.remove_certificate()?; + Ok(()) + } +} + +impl RemoveCertCmd { + fn remove_certificate(&self) -> Result<(), CertError> { + let _user_guard = self.user_manager.become_user(tedge_users::BROKER_USER)?; + std::fs::remove_file(&self.cert_path).or_else(ok_if_not_found)?; + std::fs::remove_file(&self.key_path).or_else(ok_if_not_found)?; + + Ok(()) + } +} diff --git a/crates/core/tedge/src/cli/certificate/show.rs b/crates/core/tedge/src/cli/certificate/show.rs new file mode 100644 index 00000000..8890f308 --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/show.rs @@ -0,0 +1,41 @@ +use super::error::CertError; +use crate::command::Command; + +use certificate::PemCertificate; +use tedge_config::*; + +/// Show the device certificate, if any +pub struct ShowCertCmd { + /// The path where the device certificate will be stored + pub cert_path: FilePath, +} + +impl Command for ShowCertCmd { + fn description(&self) -> String { + "show the device certificate".into() + } + + fn execute(&self) -> anyhow::Result<()> { + let () = self.show_certificate()?; + Ok(()) + } +} + +impl ShowCertCmd { + fn show_certificate(&self) -> Result<(), CertError> { + let pem = PemCertificate::from_pem_file(&self.cert_path).map_err(|err| match err { + certificate::CertificateError::IoError(from) => { + CertError::IoError(from).cert_context(self.cert_path.clone()) + } + from => CertError::CertificateError(from), + })?; + + println!("Device certificate: {}", self.cert_path); + println!("Subject: {}", pem.subject()?); + println!("Issuer: {}", pem.issuer()?); + println!("Valid from: {}", pem.not_before()?); + println!("Valid up to: {}", pem.not_after()?); + println!("Thumbprint: {}", pem.thumbprint()?); + Ok(()) + } +} diff --git a/crates/core/tedge/src/cli/certificate/upload.rs b/crates/core/tedge/src/cli/certificate/upload.rs new file mode 100644 index 00000000..ba5141d8 --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/upload.rs @@ -0,0 +1,251 @@ +use super::error::{get_webpki_error_from_reqwest, CertError}; +use crate::command::Command; +use reqwest::{StatusCode, Url}; +use std::{io::prelude::*, path::Path}; +use tedge_config::*; +use tedge_utils::paths::pathbuf_to_string; + +#[derive(Debug, serde::Deserialize)] +struct CumulocityResponse { + name: String, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct UploadCertBody { + name: String, + cert_in_pem_format: String, + auto_registration_enabled: bool, + status: String, +} + +pub struct UploadCertCmd { + pub device_id: String, + pub path: FilePath, + pub host: ConnectUrl, + pub username: String, +} + +impl Command for UploadCertCmd { + fn description(&self) -> String { + "upload root certificate".into() + } + + fn execute(&self) -> anyhow::Result<()> { + Ok(self.upload_certificate()?) + } +} + +impl UploadCertCmd { + fn upload_certificate(&self) -> Result<(), CertError> { + // Read the password from /dev/tty + // Unless a password is provided using the `C8YPASS` env var. + let password = match std::env::var("C8YPASS") { + Ok(password) => password, + Err(_) => rpassword::read_password_from_tty(Some("Enter password: "))?, + }; + + // Use a builder instead of `Client::new`, `new` could panic, builder adds option to allow invalid certs. + let client = reqwest::blocking::Client::builder().build()?; + + // To post certificate c8y requires one of the following endpoints: + // https://<tenant_id>.cumulocity.url.io/tenant/tenants/<tenant_id>/trusted-certificates + // https://<tenant_domain>.cumulocity.url.io/tenant/tenants/<tenant_id>/trusted-certificates + // and therefore we need to get tenant_id. + let tenant_id = get_tenant_id_blocking( + &client, + build_get_tenant_id_url(self.host.as_str())?, + &self.username, + &password, + )?; + self.post_certificate(&client, &tenant_id, &password) + } + + fn post_certificate( + &self, + client: &reqwest::blocking::Client, + tenant_id: &str, + password: &str, + ) -> Result<(), CertError> { + let post_url = build_upload_certificate_url(self.host.as_str(), tenant_id)?; + + let post_body = UploadCertBody { + auto_registration_enabled: true, + cert_in_pem_format: read_cert_to_string(&self.path)?, + name: self.device_id.clone(), + status: "ENABLED".into(), + }; |