From eb18876440b6bbcac189022dac9db5d3a18b5ce8 Mon Sep 17 00:00:00 2001 From: Krzysztof Piotrowski Date: Thu, 14 Mar 2024 18:51:49 +0000 Subject: [PATCH 1/6] feat: Add tedge config for csr file path Signed-off-by: Krzysztof Piotrowski --- .../src/tedge_config_cli/tedge_config.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index e84c620144e..f080bf1c3a3 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -346,6 +346,11 @@ define_tedge_config! { #[doku(as = "PathBuf")] cert_path: Utf8PathBuf, + /// Path where the device's certificate signing request is stored + #[tedge_config(example = "/etc/tedge/device-certs/tedge.csr", default(function = "default_device_csr"))] + #[doku(as = "PathBuf")] + csr_path: Utf8PathBuf, + /// The default device type #[tedge_config(example = "thin-edge.io", default(value = "thin-edge.io"))] #[tedge_config(rename = "type")] @@ -943,6 +948,13 @@ fn default_device_cert(location: &TEdgeConfigLocation) -> Utf8PathBuf { .join("tedge-certificate.pem") } +fn default_device_csr(location: &TEdgeConfigLocation) -> Utf8PathBuf { + location + .tedge_config_root_path() + .join("device-certs") + .join("tedge.csr") +} + fn default_mqtt_port() -> NonZeroU16 { NonZeroU16::try_from(1883).unwrap() } From 60531bd28d295f331b92b22950627c8e9d44f04c Mon Sep 17 00:00:00 2001 From: Krzysztof Piotrowski Date: Thu, 14 Mar 2024 20:06:01 +0000 Subject: [PATCH 2/6] feat: Adjust certificate parameters to support certificate signing request Signed-off-by: Krzysztof Piotrowski --- crates/common/certificate/src/lib.rs | 137 ++++++++++++++++++++++++--- 1 file changed, 123 insertions(+), 14 deletions(-) diff --git a/crates/common/certificate/src/lib.rs b/crates/common/certificate/src/lib.rs index d395fe6d938..d4385f4d012 100644 --- a/crates/common/certificate/src/lib.rs +++ b/crates/common/certificate/src/lib.rs @@ -113,15 +113,19 @@ impl KeyCertPair { ) -> Result { let today = OffsetDateTime::now_utc(); let not_before = today - Duration::days(1); // Ensure the certificate is valid today - KeyCertPair::new_selfsigned_certificate_at(config, id, not_before, key_kind) + let params = + Self::create_selfsigned_certificate_parameters(config, id, key_kind, not_before)?; + Ok(KeyCertPair { + certificate: Zeroizing::new(Certificate::from_params(params)?), + }) } - fn new_selfsigned_certificate_at( + fn create_selfsigned_certificate_parameters( config: &NewCertificateConfig, id: &str, + key_kind: &KeyKind, not_before: OffsetDateTime, - cert_kind: &KeyKind, - ) -> Result { + ) -> Result { KeyCertPair::check_identifier(id, config.max_cn_size)?; let mut distinguished_name = rcgen::DistinguishedName::new(); distinguished_name.push(rcgen::DnType::CommonName, id); @@ -131,23 +135,64 @@ impl KeyCertPair { &config.organizational_unit_name, ); - let not_after = not_before + Duration::days(config.validity_period_days.into()); - let mut params = CertificateParams::default(); + params.distinguished_name = distinguished_name; + + let not_after = not_before + Duration::days(config.validity_period_days.into()); params.not_before = not_before; params.not_after = not_after; - params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; // ECDSA signing using the P-256 curves and SHA-256 hashing as per RFC 5758 + params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); // IsCa::SelfSignedOnly is rejected by C8Y - if let KeyKind::Reuse { keypair_pem } = cert_kind { + + params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; // ECDSA signing using the P-256 curves and SHA-256 hashing as per RFC 5758 + + if let KeyKind::Reuse { keypair_pem } = key_kind { params.key_pair = Some(KeyPair::from_pem(keypair_pem)?); } + Ok(params) + } + + // Create Certificate without `not_before` and `not_after` fields + // as rcgen library will not parse it for certificate signing request + pub fn new_certificate_sign_request( + config: &NewCertificateConfig, + id: &str, + key_kind: &KeyKind, + ) -> Result { + let params = Self::create_certificate_sign_request_parameters(config, id, key_kind)?; Ok(KeyCertPair { certificate: Zeroizing::new(Certificate::from_params(params)?), }) } + fn create_certificate_sign_request_parameters( + config: &NewCertificateConfig, + id: &str, + key_kind: &KeyKind, + ) -> Result { + KeyCertPair::check_identifier(id, config.max_cn_size)?; + let mut distinguished_name = rcgen::DistinguishedName::new(); + distinguished_name.push(rcgen::DnType::CommonName, id); + distinguished_name.push(rcgen::DnType::OrganizationName, &config.organization_name); + distinguished_name.push( + rcgen::DnType::OrganizationalUnitName, + &config.organizational_unit_name, + ); + + let mut params = CertificateParams::default(); + params.distinguished_name = distinguished_name; + + params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; // ECDSA signing using the P-256 curves and SHA-256 hashing as per RFC 5758 + + if let KeyKind::Reuse { keypair_pem } = key_kind { + params.key_pair = Some(KeyPair::from_pem(keypair_pem)?); + } + + Ok(params) + } + pub fn certificate_pem_string(&self) -> Result { Ok(self.certificate.serialize_pem()?) } @@ -156,6 +201,10 @@ impl KeyCertPair { Ok(Zeroizing::new(self.certificate.serialize_private_key_pem())) } + pub fn certificate_signing_request_string(&self) -> Result { + Ok(self.certificate.serialize_request_pem()?) + } + fn check_identifier(id: &str, max_cn_size: usize) -> Result<(), CertificateError> { Ok(device_id::is_valid_device_id(id, max_cn_size)?) } @@ -243,6 +292,7 @@ mod tests { use super::*; use std::error::Error; use time::macros::datetime; + use x509_parser::der_parser::asn1_rs::FromDer; impl KeyCertPair { fn new_selfsigned_certificate_with_new_key( @@ -260,6 +310,24 @@ mod tests { PemCertificate::from_pem_string(&pem_string).expect("Fail to decode the certificate PEM") } + fn subject_of_csr(keypair: &KeyCertPair) -> String { + let csr = keypair + .certificate_signing_request_string() + .expect("Failed to read the CSR string"); + + let pem = x509_parser::pem::Pem::iter_from_buffer(csr.as_bytes()) + .next() + .unwrap() + .expect("Reading PEM block failed"); + + x509_parser::certification_request::X509CertificationRequest::from_der(&pem.contents) + .unwrap() + .1 + .certification_request_info + .subject + .to_string() + } + #[test] fn self_signed_cert_subject_is_the_device() { // Create a certificate with a given subject @@ -326,9 +394,19 @@ mod tests { let id = "some-id"; let birthdate = datetime!(2021-03-31 16:39:57 +01:00); - let keypair = - KeyCertPair::new_selfsigned_certificate_at(&config, id, birthdate, &KeyKind::New) - .expect("Fail to create a certificate"); + let params = KeyCertPair::create_selfsigned_certificate_parameters( + &config, + id, + &KeyKind::New, + birthdate, + ) + .expect("Fail to get a certificate parameters"); + + let keypair = KeyCertPair { + certificate: Zeroizing::new( + Certificate::from_params(params).expect("Fail to create a certificate"), + ), + }; // Check the not_before date let pem = pem_of_keypair(&keypair); @@ -348,9 +426,19 @@ mod tests { let id = "some-id"; let birthdate = datetime!(2021-03-31 16:39:57 +01:00); - let keypair = - KeyCertPair::new_selfsigned_certificate_at(&config, id, birthdate, &KeyKind::New) - .expect("Fail to create a certificate"); + let params = KeyCertPair::create_selfsigned_certificate_parameters( + &config, + id, + &KeyKind::New, + birthdate, + ) + .expect("Fail to get a certificate parameters"); + + let keypair = KeyCertPair { + certificate: Zeroizing::new( + Certificate::from_params(params).expect("Fail to create a certificate"), + ), + }; // Check the not_after date let pem = pem_of_keypair(&keypair); @@ -358,6 +446,27 @@ mod tests { assert_eq!(not_after, "Sat, 10 Apr 2021 15:39:57 +0000"); } + #[test] + fn create_certificate_sign_request() { + // Create a certificate with a given birthdate. + let config = NewCertificateConfig::default(); + let id = "some-id"; + + let params = + KeyCertPair::create_certificate_sign_request_parameters(&config, id, &KeyKind::New) + .expect("Fail to get a certificate parameters"); + + let keypair = KeyCertPair { + certificate: Zeroizing::new( + Certificate::from_params(params).expect("Fail to create a certificate"), + ), + }; + + // Check the subject + let subject = subject_of_csr(&keypair); + assert_eq!(subject, "CN=some-id, O=Thin Edge, OU=Test Device"); + } + #[test] fn check_certificate_thumbprint_b64_decode_sha1() { // Create a certificate key pair From 6d5ba3d6f864dec03f7c299942fa9718cf57d0ca Mon Sep 17 00:00:00 2001 From: Krzysztof Piotrowski Date: Thu, 14 Mar 2024 22:47:12 +0000 Subject: [PATCH 3/6] feat: Add command to create a certificate signing request Signed-off-by: Krzysztof Piotrowski --- crates/common/tedge_utils/src/file.rs | 14 ++ crates/core/tedge/src/cli/certificate/cli.rs | 19 +++ .../core/tedge/src/cli/certificate/create.rs | 88 ++++++++-- .../tedge/src/cli/certificate/create_csr.rs | 156 ++++++++++++++++++ .../core/tedge/src/cli/certificate/error.rs | 7 + crates/core/tedge/src/cli/certificate/mod.rs | 1 + .../core/tedge/src/cli/certificate/renew.rs | 23 +-- 7 files changed, 276 insertions(+), 32 deletions(-) create mode 100644 crates/core/tedge/src/cli/certificate/create_csr.rs diff --git a/crates/common/tedge_utils/src/file.rs b/crates/common/tedge_utils/src/file.rs index ec99f1edf34..6636a11cd95 100644 --- a/crates/common/tedge_utils/src/file.rs +++ b/crates/common/tedge_utils/src/file.rs @@ -114,6 +114,20 @@ pub fn create_file_with_mode( perm_entry.create_file(file.as_ref(), content) } +pub fn create_file_with_mode_or_overwrite( + file: impl AsRef, + content: Option<&str>, + mode: u32, +) -> Result<(), FileError> { + match content { + Some(content) if file.as_ref().exists() => overwrite_file(file.as_ref(), content), + _ => { + let perm_entry = PermissionEntry::new(None, None, Some(mode)); + perm_entry.create_file(file.as_ref(), content) + } + } +} + pub fn create_file_with_user_group( file: impl AsRef, user: &str, diff --git a/crates/core/tedge/src/cli/certificate/cli.rs b/crates/core/tedge/src/cli/certificate/cli.rs index 5fc7f66a504..457afeb9087 100644 --- a/crates/core/tedge/src/cli/certificate/cli.rs +++ b/crates/core/tedge/src/cli/certificate/cli.rs @@ -1,6 +1,7 @@ use tedge_config::OptionalConfigError; use super::create::CreateCertCmd; +use super::create_csr::CreateCsrCmd; use super::remove::RemoveCertCmd; use super::renew::RenewCertCmd; use super::show::ShowCertCmd; @@ -20,6 +21,13 @@ pub enum TEdgeCertCli { id: String, }, + /// Create a certificate signing request + CreateCsr { + /// The device identifier to be used as the common name for the certificate + #[clap(long = "device-id")] + id: Option, + }, + /// Renew the device certificate Renew, @@ -44,6 +52,17 @@ impl BuildCommand for TEdgeCertCli { id, cert_path: config.device.cert_path.clone(), key_path: config.device.key_path.clone(), + csr_path: None, + }; + cmd.into_boxed() + } + + TEdgeCertCli::CreateCsr { id } => { + let cmd = CreateCsrCmd { + id, + cert_path: config.device.cert_path.clone(), + key_path: config.device.key_path.clone(), + csr_path: config.device.csr_path.clone(), }; cmd.into_boxed() } diff --git a/crates/core/tedge/src/cli/certificate/create.rs b/crates/core/tedge/src/cli/certificate/create.rs index 03f5a190375..98c196f9dc0 100644 --- a/crates/core/tedge/src/cli/certificate/create.rs +++ b/crates/core/tedge/src/cli/certificate/create.rs @@ -4,13 +4,15 @@ use camino::Utf8PathBuf; use certificate::KeyCertPair; use certificate::KeyKind; use certificate::NewCertificateConfig; +use certificate::PemCertificate; use std::fs::File; use std::fs::OpenOptions; use std::io::prelude::*; +use std::io::ErrorKind; use std::path::Path; +use tedge_utils::file::create_file_with_mode_or_overwrite; use tedge_utils::paths::set_permission; use tedge_utils::paths::validate_parent_dir_exists; - /// Create a self-signed device certificate pub struct CreateCertCmd { /// The device identifier @@ -21,6 +23,9 @@ pub struct CreateCertCmd { /// 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: Option, } impl Command for CreateCertCmd { @@ -38,7 +43,13 @@ impl Command for CreateCertCmd { impl CreateCertCmd { pub fn create_test_certificate(&self, config: &NewCertificateConfig) -> Result<(), CertError> { - self.create_test_certificate_for(config, &KeyKind::New) + // Reuse private key if it already exists + let key_kind = match std::fs::read_to_string(&self.key_path) { + Ok(keypair_pem) => KeyKind::Reuse { keypair_pem }, + Err(err) if err.kind() == ErrorKind::NotFound => KeyKind::New, + Err(err) => return Err(CertError::IoError(err).cert_context(self.cert_path.clone())), + }; + self.create_test_certificate_for(config, &key_kind) } pub fn renew_test_certificate(&self, config: &NewCertificateConfig) -> Result<(), CertError> { @@ -47,6 +58,19 @@ impl CreateCertCmd { self.create_test_certificate_for(config, &KeyKind::Reuse { keypair_pem }) } + pub fn create_certificate_signing_request( + &self, + config: &NewCertificateConfig, + ) -> Result<(), CertError> { + // Reuse private key if it already exists + let key_kind = match std::fs::read_to_string(&self.key_path) { + Ok(keypair_pem) => KeyKind::Reuse { keypair_pem }, + Err(err) if err.kind() == ErrorKind::NotFound => KeyKind::New, + Err(err) => return Err(CertError::IoError(err).cert_context(self.cert_path.clone())), + }; + self.create_test_certificate_for(config, &key_kind) + } + fn create_test_certificate_for( &self, config: &NewCertificateConfig, @@ -55,22 +79,38 @@ impl CreateCertCmd { 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, key_kind)?; + let cert = match &self.csr_path { + Some(csr_path) => { + validate_parent_dir_exists(csr_path).map_err(CertError::CsrPathError)?; - // TODO cope with broker user being tedge - // Creating files with permission 644 owned by the MQTT broker - let mut cert_file = - create_new_file(&self.cert_path, crate::BROKER_USER, crate::BROKER_GROUP) - .map_err(|err| err.cert_context(self.cert_path.clone()))?; + let cert = KeyCertPair::new_certificate_sign_request(config, &self.id, key_kind)?; + let cert_csr = cert.certificate_signing_request_string()?; - let cert_pem = cert.certificate_pem_string()?; - cert_file.write_all(cert_pem.as_bytes())?; - cert_file.sync_all()?; + create_file_with_mode_or_overwrite(csr_path, Some(cert_csr.as_str()), 0o444)?; - // Prevent the certificate to be overwritten - set_permission(&cert_file, 0o444)?; + cert + } + None => { + let cert = KeyCertPair::new_selfsigned_certificate(config, &self.id, key_kind)?; + + // Creating files with permission 644 owned by the MQTT broker + let mut cert_file = + create_new_file(&self.cert_path, crate::BROKER_USER, crate::BROKER_GROUP) + .map_err(|err| err.cert_context(self.cert_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)?; + + cert + } + }; if let KeyKind::New = key_kind { + // TODO cope with broker user being tedge let mut key_file = create_new_file(&self.key_path, crate::BROKER_USER, crate::BROKER_GROUP) .map_err(|err| err.key_context(self.key_path.clone()))?; @@ -105,6 +145,23 @@ fn create_new_file(path: impl AsRef, user: &str, group: &str) -> Result Result { + let pem = PemCertificate::from_pem_file(cert_path).map_err(|err| match err { + certificate::CertificateError::IoError(from) => { + CertError::IoError(from).cert_context(cert_path.clone()) + } + from => CertError::CertificateError(from), + })?; + + if pem.issuer()? == pem.subject()? { + Ok(pem.subject_common_name()?) + } else { + Err(CertError::NotASelfSignedCertificate { + path: cert_path.clone(), + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -123,6 +180,7 @@ mod tests { id: String::from(id), cert_path: cert_path.clone(), key_path: key_path.clone(), + csr_path: None, }; assert_matches!( @@ -150,6 +208,7 @@ mod tests { id: "my-device-id".into(), cert_path: cert_path.clone(), key_path: key_path.clone(), + csr_path: None, }; assert!(cmd @@ -171,6 +230,7 @@ mod tests { id: "my-device-id".into(), cert_path, key_path, + csr_path: None, }; let cert_error = cmd @@ -189,6 +249,7 @@ mod tests { id: "my-device-id".into(), cert_path, key_path, + csr_path: None, }; let cert_error = cmd @@ -207,6 +268,7 @@ mod tests { id: "my-device-id".into(), cert_path, key_path, + csr_path: None, }; let cert_error = cmd diff --git a/crates/core/tedge/src/cli/certificate/create_csr.rs b/crates/core/tedge/src/cli/certificate/create_csr.rs new file mode 100644 index 00000000000..eb9fd4685ef --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/create_csr.rs @@ -0,0 +1,156 @@ +use super::create::cn_of_self_signed_certificate; +use super::error::CertError; +use crate::command::Command; +use crate::CreateCertCmd; +use camino::Utf8PathBuf; +use certificate::NewCertificateConfig; + +pub struct CreateCsrCmd { + pub id: Option, + pub cert_path: Utf8PathBuf, + pub key_path: Utf8PathBuf, + pub csr_path: Utf8PathBuf, +} + +impl Command for CreateCsrCmd { + fn description(&self) -> String { + "Generate a Certificate Signing Request.".into() + } + + fn execute(&self) -> anyhow::Result<()> { + let config = NewCertificateConfig::default(); + self.create_certificate_signing_request(&config)?; + eprintln!("Certificate Signing Request was successfully created."); + Ok(()) + } +} + +impl CreateCsrCmd { + pub fn create_certificate_signing_request( + &self, + config: &NewCertificateConfig, + ) -> Result<(), CertError> { + // Use id of public certificate if not provided + let id = match &self.id { + Some(id) => id.clone(), + None => cn_of_self_signed_certificate(&self.cert_path)?, + }; + + let create_cmd = CreateCertCmd { + id, + cert_path: self.cert_path.clone(), + key_path: self.key_path.clone(), + csr_path: Some(self.csr_path.clone()), + }; + + create_cmd.create_certificate_signing_request(config) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CreateCertCmd; + use assert_matches::assert_matches; + use std::path::Path; + use tempfile::*; + use x509_parser::der_parser::asn1_rs::FromDer; + use x509_parser::nom::AsBytes; + + #[test] + fn create_signing_request_when_private_key_does_not_exist() { + 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 csr_path = temp_file_path(&dir, "my-device-csr.csr"); + let id = "my-device-id"; + + let cmd = CreateCsrCmd { + id: Some(String::from(id)), + cert_path: cert_path.clone(), + key_path: key_path.clone(), + csr_path: csr_path.clone(), + }; + + assert_matches!( + cmd.create_certificate_signing_request(&NewCertificateConfig::default()), + Ok(()) + ); + + assert_eq!(parse_pem_file(&csr_path).label, "CERTIFICATE REQUEST"); + assert_eq!(parse_pem_file(&key_path).label, "PRIVATE KEY"); + } + + #[test] + fn create_signing_request_when_both_private_key_and_public_cert_exist() { + 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 csr_path = temp_file_path(&dir, "my-device-csr.csr"); + let id = "my-device-id"; + + let cmd = CreateCertCmd { + id: String::from(id), + cert_path: cert_path.clone(), + key_path: key_path.clone(), + csr_path: None, + }; + + // create private key and public cert with standard command + assert_matches!( + cmd.create_test_certificate(&NewCertificateConfig::default()), + Ok(()) + ); + + // Keep the cert and key data for validation + let first_key = parse_pem_file(&key_path); + let first_pem = parse_pem_file(&cert_path); + let first_x509_cert = first_pem.parse_x509().expect("X.509: decoding DER failed"); + + let cmd = CreateCsrCmd { + id: Some(String::from(id)), + cert_path: cert_path.clone(), + key_path: key_path.clone(), + csr_path: csr_path.clone(), + }; + + // create csr using existing private key and device_id from public cert + assert_matches!( + cmd.create_certificate_signing_request(&NewCertificateConfig::default()), + Ok(()) + ); + + // Get the csr and key data for validation + let second_key = parse_pem_file(&key_path); + let csr_pem = parse_pem_file(&csr_path); + let csr_subject = get_subject_from_csr(csr_pem.contents); + + // Check that private key remained the same + assert_eq!(first_key.contents, second_key.contents); + + // Check if subject is the same + assert_eq!(csr_subject, first_x509_cert.subject.to_string()); + } + + fn temp_file_path(dir: &TempDir, filename: &str) -> Utf8PathBuf { + dir.path().join(filename).try_into().unwrap() + } + + fn parse_pem_file(path: impl AsRef) -> x509_parser::pem::Pem { + let content = std::fs::read(path).expect("fail to read {path}"); + + x509_parser::pem::Pem::iter_from_buffer(&content) + .next() + .unwrap() + .expect("Reading PEM block failed") + } + + fn get_subject_from_csr(content: Vec) -> String { + x509_parser::certification_request::X509CertificationRequest::from_der(content.as_bytes()) + .unwrap() + .1 + .certification_request_info + .subject + .to_string() + } +} diff --git a/crates/core/tedge/src/cli/certificate/error.rs b/crates/core/tedge/src/cli/certificate/error.rs index 9b08dccb7ac..50cb63d113d 100644 --- a/crates/core/tedge/src/cli/certificate/error.rs +++ b/crates/core/tedge/src/cli/certificate/error.rs @@ -4,6 +4,7 @@ use reqwest::StatusCode; use std::error::Error; use tedge_config::ConfigSettingError; use tedge_config::TEdgeConfigError; +use tedge_utils::file::FileError; use tedge_utils::paths::PathsError; #[derive(thiserror::Error, Debug)] @@ -52,6 +53,9 @@ pub enum CertError { #[error("Invalid device.key_path path: {0}")] KeyPathError(PathsError), + #[error("Invalid device.csr_path path: {0}")] + CsrPathError(PathsError), + #[error(transparent)] CertificateError(#[from] certificate::CertificateError), @@ -79,6 +83,9 @@ pub enum CertError { #[error(transparent)] TedgeConfigSettingError(#[from] ConfigSettingError), + #[error(transparent)] + FileError(#[from] FileError), + #[error("Root certificate path {0} does not exist")] RootCertificatePathDoesNotExist(String), diff --git a/crates/core/tedge/src/cli/certificate/mod.rs b/crates/core/tedge/src/cli/certificate/mod.rs index 8eec9a04128..b69139a47cb 100644 --- a/crates/core/tedge/src/cli/certificate/mod.rs +++ b/crates/core/tedge/src/cli/certificate/mod.rs @@ -2,6 +2,7 @@ pub use self::cli::TEdgeCertCli; mod cli; mod create; +mod create_csr; mod error; mod remove; mod renew; diff --git a/crates/core/tedge/src/cli/certificate/renew.rs b/crates/core/tedge/src/cli/certificate/renew.rs index ca874e61761..771fcd088c1 100644 --- a/crates/core/tedge/src/cli/certificate/renew.rs +++ b/crates/core/tedge/src/cli/certificate/renew.rs @@ -1,9 +1,9 @@ +use super::create::cn_of_self_signed_certificate; use super::error::CertError; use crate::command::Command; use crate::CreateCertCmd; use camino::Utf8PathBuf; use certificate::NewCertificateConfig; -use certificate::PemCertificate; pub struct RenewCertCmd { pub cert_path: Utf8PathBuf, @@ -25,7 +25,7 @@ impl Command for RenewCertCmd { impl RenewCertCmd { fn renew_test_certificate(&self, config: &NewCertificateConfig) -> Result<(), CertError> { - let id = self.cn_of_self_signed_certificate()?; + let id = cn_of_self_signed_certificate(&self.cert_path)?; // Remove only certificate std::fs::remove_file(&self.cert_path) @@ -36,27 +36,11 @@ impl RenewCertCmd { id, cert_path: self.cert_path.clone(), key_path: self.key_path.clone(), + csr_path: None, }; create_cmd.renew_test_certificate(config) } - - fn cn_of_self_signed_certificate(&self) -> Result { - 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), - })?; - - if pem.issuer()? == pem.subject()? { - Ok(pem.subject_common_name()?) - } else { - Err(CertError::NotASelfSignedCertificate { - path: self.cert_path.clone(), - }) - } - } } #[cfg(test)] @@ -77,6 +61,7 @@ mod tests { id: String::from(id), cert_path: cert_path.clone(), key_path: key_path.clone(), + csr_path: None, }; // First create both cert and key From 60bb0c8b8ff3b5eb8df5d3aefb80c8776bb7ae9f Mon Sep 17 00:00:00 2001 From: Krzysztof Piotrowski Date: Tue, 19 Mar 2024 20:05:44 +0000 Subject: [PATCH 4/6] feat: Pass a CSR output file location as a command argument Signed-off-by: Krzysztof Piotrowski --- crates/core/tedge/src/cli/certificate/cli.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/core/tedge/src/cli/certificate/cli.rs b/crates/core/tedge/src/cli/certificate/cli.rs index 457afeb9087..b95d0ee0aff 100644 --- a/crates/core/tedge/src/cli/certificate/cli.rs +++ b/crates/core/tedge/src/cli/certificate/cli.rs @@ -1,3 +1,4 @@ +use camino::Utf8PathBuf; use tedge_config::OptionalConfigError; use super::create::CreateCertCmd; @@ -26,6 +27,10 @@ pub enum TEdgeCertCli { /// The device identifier to be used as the common name for the certificate #[clap(long = "device-id")] id: Option, + + /// Path where a Certificate signing request will be stored + #[clap(long = "output-path")] + output_path: Option, }, /// Renew the device certificate @@ -57,12 +62,13 @@ impl BuildCommand for TEdgeCertCli { cmd.into_boxed() } - TEdgeCertCli::CreateCsr { id } => { + TEdgeCertCli::CreateCsr { id, output_path } => { let cmd = CreateCsrCmd { id, cert_path: config.device.cert_path.clone(), key_path: config.device.key_path.clone(), - csr_path: config.device.csr_path.clone(), + // Use output file instead of csr_path from tedge config if provided + csr_path: output_path.unwrap_or_else(|| config.device.csr_path.clone()), }; cmd.into_boxed() } From 70fa6a9c308085950ae4aa2048755e180b16d930 Mon Sep 17 00:00:00 2001 From: Krzysztof Piotrowski Date: Fri, 15 Mar 2024 12:23:02 +0000 Subject: [PATCH 5/6] docs: Add documentation for generating certificate signing request Signed-off-by: Krzysztof Piotrowski --- .../security/certificate-signing-request.md | 100 ++++++++++++++++++ docs/src/references/cli/tedge-cert.md | 15 +++ 2 files changed, 115 insertions(+) create mode 100644 docs/src/operate/security/certificate-signing-request.md diff --git a/docs/src/operate/security/certificate-signing-request.md b/docs/src/operate/security/certificate-signing-request.md new file mode 100644 index 00000000000..970298f3fba --- /dev/null +++ b/docs/src/operate/security/certificate-signing-request.md @@ -0,0 +1,100 @@ +--- +title: Certificate signing request +tags: [Operate, Security, Cloud] +description: Generate certificate signing request for %%te%% +--- + +If you want to use a device certificate which is signed by a Certificate Authority (CA), you can generate the Certificate Signing Request (CSR), which is later used by CA to generate a device certificate. This process requires additional tooling as %%te%% provides you only with the CSR. + +## Create a certificate signing request + +To create a CSR you can use [`tedge cert create-csr`](../../references/cli/tedge-cert.md) %%te%% command: + +```sh +sudo tedge cert create-csr --device-id alpha +``` + +or + +```sh +sudo tedge cert create-csr +``` +if a device certificate already exists and you want to reuse device name. + + +```text title="Output" +Certificate Signing Request was successfully created. +``` + +:::note +`tedge cert` requires `sudo` privilege as creating a private key owned by the MQTT broker. This command provides no output on success. +::: + +Now you should have a CSR in the `/etc/tedge/device-certs/` directory: + +```sh +ls -l /etc/tedge/device-certs/ +``` + +```text title="Output" +total 8 +-r--r--r-- 1 mosquitto mosquitto 664 May 31 09:26 tedge.csr +-r-------- 1 mosquitto mosquitto 246 May 31 09:26 tedge-private-key.pem +``` + +[`sudo tedge cert create-csr`](../../references/cli/tedge-cert.md) creates the certificate signing request in a default location (`/etc/tedge/device-certs/`). To use a custom location, refer to [`tedge config`](../../references/cli/tedge-config.md) or provide absolute path as a command argument: + +```sh +sudo tedge cert create-csr --device-id alpha --output-path /custom/path/mycsr.csr +``` + +:::note +`tedge cert create-csr` will reuse the private key if already created, e.g by the `tedge cert create` command. +::: + +To check the content of CSR, you can use external tools, like `openssl`. + +```sh +openssl req -in /etc/tedge/device-certs/tedge.csr -noout -text +``` + +```text title="Output" +Certificate Request: + Data: + Version: 1 (0x0) + Subject: CN = alpha, O = Thin Edge, OU = Test Device + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:95:e6:48:48:9b:8e:03:a0:fd:07:41:e6:e7:25: + 21:3b:ed:c7:8d:13:2f:69:a7:94:17:43:7c:da:ca: + 33:fb:bb:93:fe:eb:c1:50:65:c2:47:70:87:5e:ab: + a3:d5:ec:9b:5c:65:7a:ba:7d:92:20:a1:80:9b:d6: + 79:71:be:15:56 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + Attributes: + (none) + Requested Extensions: + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:46:02:21:00:81:28:11:28:9b:92:cb:b8:d9:d2:1c:3c:8d: + 00:1f:4e:44:ae:ba:61:7f:ca:17:75:d9:d4:11:04:fa:11:e8: + a2:02:21:00:f2:f4:11:77:5c:32:c8:d5:86:66:29:d5:ae:27: + 3f:64:31:be:f8:4a:89:29:bf:e0:01:b4:f2:63:1f:f0:f0:fb +``` + +## Errors + +### Certificate Signing Request creation fails due to invalid device id + +If non-supported characters are used for the device id then the cert create-csr will fail with below error: + +```text +Error: failed to Generate the Certificate Signing Request. + +Caused by: + 0: DeviceID Error + 1: The string '"+"' contains characters which cannot be used in a name [use only A-Z, a-z, 0-9, ' = ( ) , - . ? % * _ ! @] +``` diff --git a/docs/src/references/cli/tedge-cert.md b/docs/src/references/cli/tedge-cert.md index 7b979553c00..c18bc89e8be 100644 --- a/docs/src/references/cli/tedge-cert.md +++ b/docs/src/references/cli/tedge-cert.md @@ -38,6 +38,21 @@ OPTIONS: -h, --help Print help information ``` +## Create-csr + +```sh title="tedge cert create-csr" +tedge-cert-create-csr +Create certificate signing request + +Usage: tedge cert create-csr [OPTIONS] + +Options: + --device-id The device identifier to be used as the common name for the certificate + --output-path Path where a Certificate signing request will be stored + --config-dir [default: /etc/tedge] + -h, --help Print help +``` + ## Show ```sh title="tedge cert show" From 139516770864388cc5f33ce10d66d6bcf1eef531 Mon Sep 17 00:00:00 2001 From: Krzysztof Piotrowski Date: Mon, 18 Mar 2024 19:14:17 +0000 Subject: [PATCH 6/6] tests: Add system tests for generating a CSR Signed-off-by: Krzysztof Piotrowski --- .../tedge/certificate_signing_request.robot | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/RobotFramework/tests/tedge/certificate_signing_request.robot diff --git a/tests/RobotFramework/tests/tedge/certificate_signing_request.robot b/tests/RobotFramework/tests/tedge/certificate_signing_request.robot new file mode 100644 index 00000000000..fad404ee13b --- /dev/null +++ b/tests/RobotFramework/tests/tedge/certificate_signing_request.robot @@ -0,0 +1,65 @@ +*** Settings *** +Resource ../../resources/common.resource +Library ThinEdgeIO + +Test Teardown Get Logs + +Test Tags theme:cli + + +*** Test Cases *** +Generate CSR using the device-id from an existing certificate and private key + [Setup] Setup With Self-Signed Certificate + ThinEdgeIO.File Should Exist /etc/tedge/device-certs/tedge-certificate.pem + ThinEdgeIO.File Should Exist /etc/tedge/device-certs/tedge-private-key.pem + + ${hash_before_cert}= Execute Command md5sum /etc/tedge/device-certs/tedge-certificate.pem + ${hash_before_private_key}= Execute Command md5sum /etc/tedge/device-certs/tedge-private-key.pem + + Execute Command sudo tedge cert create-csr + + ${output_cert_subject}= Execute Command + ... openssl x509 -noout -subject -in /etc/tedge/device-certs/tedge-certificate.pem + ${output_csr_subject}= Execute Command + ... openssl req -noout -subject -in /etc/tedge/device-certs/tedge.csr + Should Be Equal ${output_cert_subject} ${output_csr_subject} + + ${output_private_key_md5}= Execute Command + ... openssl pkey -in /etc/tedge/device-certs/tedge-private-key.pem -pubout | openssl md5 + ${output_csr_md5}= Execute Command + ... openssl req -in /etc/tedge/device-certs/tedge.csr -pubkey -noout | openssl md5 + Should Be Equal ${output_private_key_md5} ${output_csr_md5} + + ${hash_after_cert}= Execute Command md5sum /etc/tedge/device-certs/tedge-certificate.pem + ${hash_after_private_key}= Execute Command md5sum /etc/tedge/device-certs/tedge-private-key.pem + Should Be Equal ${hash_before_cert} ${hash_after_cert} + Should Be Equal ${hash_before_private_key} ${hash_after_private_key} + +Generate CSR without an existing certificate and private key + [Setup] Setup Without Certificate + File Should Not Exist /etc/tedge/device-certs/tedge-certificate.pem + File Should Not Exist /etc/tedge/device-certs/tedge-private-key.pem + + Execute Command sudo tedge cert create-csr --device-id test-user + + ${output_csr_subject}= Execute Command + ... openssl req -noout -subject -in /etc/tedge/device-certs/tedge.csr + Should Contain ${output_csr_subject} subject=CN = test-user + + ${output_private_key_md5}= Execute Command + ... openssl pkey -in /etc/tedge/device-certs/tedge-private-key.pem -pubout | openssl md5 + ${output_csr_md5}= Execute Command + ... openssl req -in /etc/tedge/device-certs/tedge.csr -pubkey -noout | openssl md5 + Should Be Equal ${output_private_key_md5} ${output_csr_md5} + + +*** Keywords *** +Setup With Self-Signed Certificate + ${DEVICE_SN}= Setup skip_bootstrap=${True} + Set Test Variable $DEVICE_SN + Execute Command test -f ./bootstrap.sh && ./bootstrap.sh --cert-method selfsigned + +Setup Without Certificate + ${DEVICE_SN}= Setup skip_bootstrap=${True} + Set Test Variable $DEVICE_SN + Execute Command test -f ./bootstrap.sh && ./bootstrap.sh --install --no-bootstrap --no-connect