From 2673aa078759801a636da642afc991fe4d3af3fc Mon Sep 17 00:00:00 2001 From: PradeepKiruvale Date: Mon, 5 Sep 2022 19:10:01 +0530 Subject: move rust tls code from tedge connect to common place (#1373) Signed-off-by: Pradeep Kumar K J Signed-off-by: Pradeep Kumar K J --- Cargo.lock | 5 +- crates/common/certificate/Cargo.toml | 3 + crates/common/certificate/src/lib.rs | 53 ++++++- .../certificate/src/parse_root_certificate.rs | 132 ++++++++++++++++++ crates/core/tedge/Cargo.toml | 2 - crates/core/tedge/src/cli/certificate/error.rs | 43 +----- .../tedge/src/cli/connect/c8y_direct_connection.rs | 153 ++------------------- crates/core/tedge/src/cli/connect/error.rs | 7 +- 8 files changed, 206 insertions(+), 192 deletions(-) create mode 100644 crates/common/certificate/src/parse_root_certificate.rs diff --git a/Cargo.lock b/Cargo.lock index dee24d04..e55d6429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -517,7 +517,10 @@ dependencies = [ "base64", "pem", "rcgen", + "rustls 0.19.1", + "rustls 0.20.6", "sha-1 0.10.0", + "tempfile", "thiserror", "time", "x509-parser", @@ -2790,8 +2793,6 @@ dependencies = [ "reqwest", "rpassword", "rumqttc", - "rustls 0.19.1", - "rustls 0.20.6", "serde", "serde_json", "tedge_config", diff --git a/crates/common/certificate/Cargo.toml b/crates/common/certificate/Cargo.toml index 1fa6f19c..d06f3c2b 100644 --- a/crates/common/certificate/Cargo.toml +++ b/crates/common/certificate/Cargo.toml @@ -9,6 +9,8 @@ rust-version = "1.58.1" [dependencies] rcgen = { version = "0.9", features = ["pem", "zeroize"] } +rustls_0_19 = {package = "rustls", version = "0.19.0" } +rustls = "0.20.6" sha-1 = "0.10" thiserror = "1.0" time = "0.3" @@ -20,4 +22,5 @@ anyhow = "1.0" assert_matches = "1.5" base64 = "0.13" pem = "1.0" +tempfile = "3.2" time = {version = "0.3", features = ["macros"]} diff --git a/crates/common/certificate/src/lib.rs b/crates/common/certificate/src/lib.rs index ecfbd88a..bdec16a0 100644 --- a/crates/common/certificate/src/lib.rs +++ b/crates/common/certificate/src/lib.rs @@ -6,8 +6,8 @@ use sha1::{Digest, Sha1}; use std::path::Path; use time::{Duration, OffsetDateTime}; use zeroize::Zeroizing; - pub mod device_id; +pub mod parse_root_certificate; pub struct PemCertificate { pem: x509_parser::pem::Pem, } @@ -146,6 +146,45 @@ impl KeyCertPair { } } +pub fn translate_rustls_error(err: &(dyn std::error::Error + 'static)) -> Option { + if let Some(rustls::Error::InvalidCertificateData(inner)) = err.downcast_ref::() + { + match inner { + msg if msg.contains("CaUsedAsEndEntity") => Some(CertificateError::CertificateValidationFailure { + 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: msg.to_string(), + }), + + msg if msg.contains("CertExpired") => Some(CertificateError::CertificateValidationFailure { + hint: "The server certificate has expired, the time it is being validated for is later than the certificate's `notAfter` time.".into(), + msg: msg.to_string(), + }), + + msg if msg.contains("CertNotValidYet") => Some(CertificateError::CertificateValidationFailure { + 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: msg.to_string(), + }), + + msg if msg.contains("EndEntityUsedAsCa") => Some(CertificateError::CertificateValidationFailure { + 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: msg.to_string(), + }), + + msg if msg.contains("InvalidCertValidity") => Some(CertificateError::CertificateValidationFailure { + hint: "The server certificate validity period (`notBefore`, `notAfter`) is invalid, maybe the `notAfter` time is earlier than the `notBefore` time.".into(), + msg: msg.to_string(), + }), + + _ => Some(CertificateError::CertificateValidationFailure { + hint: "Server certificate validation error.".into(), + msg: inner.to_string(), + }), + } + } else { + None + } +} + #[derive(thiserror::Error, Debug)] pub enum CertificateError { #[error(transparent)] @@ -162,6 +201,18 @@ pub enum CertificateError { #[error("DeviceID Error")] InvalidDeviceID(#[from] DeviceIdError), + + #[error("Fail to parse the private key")] + UnknownPrivateKeyFormat, + + #[error("Could not parse certificate")] + CertificateParseFailed, + + #[error("HTTP Connection Problem: {msg} \nHint: {hint}")] + CertificateValidationFailure { hint: String, msg: String }, + + #[error(transparent)] + CertParse(#[from] rustls_0_19::TLSError), } pub struct NewCertificateConfig { diff --git a/crates/common/certificate/src/parse_root_certificate.rs b/crates/common/certificate/src/parse_root_certificate.rs new file mode 100644 index 00000000..38486565 --- /dev/null +++ b/crates/common/certificate/src/parse_root_certificate.rs @@ -0,0 +1,132 @@ +use rustls_0_19::{ + internal::pemfile::{certs, pkcs8_private_keys, rsa_private_keys}, + ClientConfig, +}; +use std::{fs, fs::File, io::BufReader, path::PathBuf}; + +use crate::CertificateError; +use std::io::{Error, ErrorKind}; + +pub fn create_tls_config() -> rustls_0_19::ClientConfig { + ClientConfig::new() +} + +pub fn load_root_certs( + root_store: &mut rustls_0_19::RootCertStore, + cert_path: PathBuf, +) -> Result<(), CertificateError> { + if fs::metadata(&cert_path)?.is_dir() { + for file_entry in fs::read_dir(cert_path)? { + add_root_cert(root_store, file_entry?.path())?; + } + } else { + add_root_cert(root_store, cert_path)?; + } + Ok(()) +} + +pub fn add_root_cert( + root_store: &mut rustls_0_19::RootCertStore, + cert_path: PathBuf, +) -> Result<(), CertificateError> { + let f = File::open(cert_path)?; + let mut rd = BufReader::new(f); + let _ = root_store.add_pem_file(&mut rd).map(|_| ()).map_err(|()| { + Error::new( + ErrorKind::InvalidData, + "could not load PEM file".to_string(), + ) + }); + Ok(()) +} + +pub fn read_pvt_key(key_file: PathBuf) -> Result { + parse_pkcs8_key(key_file.clone()).or_else(|_| parse_rsa_key(key_file)) +} + +pub fn parse_pkcs8_key(key_file: PathBuf) -> Result { + let f = File::open(&key_file)?; + let mut key_reader = BufReader::new(f); + match pkcs8_private_keys(&mut key_reader) { + Ok(key) if !key.is_empty() => Ok(key[0].clone()), + _ => Err(CertificateError::UnknownPrivateKeyFormat), + } +} + +pub fn parse_rsa_key(key_file: PathBuf) -> Result { + let f = File::open(&key_file)?; + let mut key_reader = BufReader::new(f); + match rsa_private_keys(&mut key_reader) { + Ok(key) if !key.is_empty() => Ok(key[0].clone()), + _ => Err(CertificateError::UnknownPrivateKeyFormat), + } +} + +pub fn read_cert_chain( + cert_file: PathBuf, +) -> Result, CertificateError> { + let f = File::open(cert_file)?; + let mut cert_reader = BufReader::new(f); + certs(&mut cert_reader).map_err(|_| CertificateError::CertificateParseFailed) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn parse_private_rsa_key() { + let key = concat!( + "-----BEGIN RSA PRIVATE KEY-----\n", + "MC4CAQ\n", + "-----END RSA PRIVATE KEY-----" + ); + let mut temp_file = NamedTempFile::new().unwrap(); + temp_file.write_all(key.as_bytes()).unwrap(); + let result = parse_rsa_key(temp_file.path().into()).unwrap(); + let pvt_key = rustls_0_19::PrivateKey(vec![48, 46, 2, 1]); + assert_eq!(result, pvt_key); + } + + #[test] + fn parse_private_pkcs8_key() { + let key = concat! { + "-----BEGIN PRIVATE KEY-----\n", + "MC4CAQ\n", + "-----END PRIVATE KEY-----"}; + let mut temp_file = NamedTempFile::new().unwrap(); + temp_file.write_all(key.as_bytes()).unwrap(); + let result = parse_pkcs8_key(temp_file.path().into()).unwrap(); + let pvt_key = rustls_0_19::PrivateKey(vec![48, 46, 2, 1]); + assert_eq!(result, pvt_key); + } + + #[test] + fn parse_supported_key() { + let key = concat!( + "-----BEGIN RSA PRIVATE KEY-----\n", + "MC4CAQ\n", + "-----END RSA PRIVATE KEY-----" + ); + let mut temp_file = NamedTempFile::new().unwrap(); + temp_file.write_all(key.as_bytes()).unwrap(); + let parsed_key = read_pvt_key(temp_file.path().into()).unwrap(); + let expected_pvt_key = rustls_0_19::PrivateKey(vec![48, 46, 2, 1]); + assert_eq!(parsed_key, expected_pvt_key); + } + + #[test] + fn parse_unsupported_key() { + let key = concat!( + "-----BEGIN DSA PRIVATE KEY-----\n", + "MC4CAQ\n", + "-----END DSA PRIVATE KEY-----" + ); + let mut temp_file = NamedTempFile::new().unwrap(); + temp_file.write_all(key.as_bytes()).unwrap(); + let err = read_pvt_key(temp_file.path().into()).unwrap_err(); + assert!(matches!(err, CertificateError::UnknownPrivateKeyFormat)); + } +} diff --git a/crates/core/tedge/Cargo.toml b/crates/core/tedge/Cargo.toml index f4f125b6..495aa091 100644 --- a/crates/core/tedge/Cargo.toml +++ b/crates/core/tedge/Cargo.toml @@ -23,8 +23,6 @@ hyper = { version = "0.14", default-features = false } reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls", "stream"] } rpassword = "5.0" rumqttc = "0.10" -rustls = "0.20.2" -rustls_0_19 = {package = "rustls", version = "0.19.0" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tedge_config = { path = "../../common/tedge_config" } diff --git a/crates/core/tedge/src/cli/certificate/error.rs b/crates/core/tedge/src/cli/certificate/error.rs index 8193350d..67094153 100644 --- a/crates/core/tedge/src/cli/certificate/error.rs +++ b/crates/core/tedge/src/cli/certificate/error.rs @@ -1,3 +1,4 @@ +use certificate::translate_rustls_error; use reqwest::StatusCode; use std::error::Error; use tedge_config::FilePath; @@ -71,9 +72,6 @@ pub enum CertError { #[error(transparent)] UrlParseError(#[from] url::ParseError), - #[error("HTTP Connection Problem: {msg} \nHint: {hint}")] - CertificateValidationFailure { hint: String, msg: String }, - #[error(transparent)] TedgeConfigError(#[from] TEdgeConfigError), @@ -122,8 +120,8 @@ impl CertError { // ) // At the last layer we have the InvalidCertificateData error which is a Box<&dyn Error> derived from WebpkiError not included anymore, just as a String // This chain may break if underlying crates change. -pub(crate) fn get_webpki_error_from_reqwest(err: reqwest::Error) -> CertError { - if let Some(rustls::Error::InvalidCertificateData(inner)) = err +pub fn get_webpki_error_from_reqwest(err: reqwest::Error) -> CertError { + if let Some(tls_error) = err // get `hyper::Error::Connect` .source() .and_then(|err| err.source()) @@ -134,40 +132,9 @@ pub(crate) fn get_webpki_error_from_reqwest(err: reqwest::Error) -> CertError { // This is our second `Custom`. .and_then(|custom_error2| custom_error2.downcast_ref::()) .and_then(|custom_error2| custom_error2.get_ref()) - // Get final error type from `rustls::Error`. - .and_then(|rustls_error| rustls_error.downcast_ref::()) + .and_then(|err| translate_rustls_error(err)) { - match inner { - msg if msg.contains("CaUsedAsEndEntity") => CertError::CertificateValidationFailure { - 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: msg.to_string(), - }, - - msg if msg.contains("CertExpired") => CertError::CertificateValidationFailure { - hint: "The server certificate has expired, the time it is being validated for is later than the certificate's `notAfter` time.".into(), - msg: msg.to_string(), - }, - - msg if msg.contains("CertNotValidYet") => CertError::CertificateValidationFailure { - 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: msg.to_string(), - }, - - msg if msg.contains("EndEntityUsedAsCa") => CertError::CertificateValidationFailure { - 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: msg.to_string(), - }, - - msg if msg.contains("InvalidCertValidity") => CertError::CertificateValidationFailure { - hint: "The server certificate validity period (`notBefore`, `notAfter`) is invalid, maybe the `notAfter` time is earlier than the `notBefore` time.".into(), - msg: msg.to_string(), - }, - - _ => CertError::CertificateValidationFailure { - hint: "Server certificate validation error.".into(), - msg: inner.to_string(), - }, - } + CertError::CertificateError(tls_error) } else { CertError::ReqwestError(err) // any other Error type than `hyper::Error` } diff --git a/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs b/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs index 1683b4d8..51c91be8 100644 --- a/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs +++ b/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs @@ -1,17 +1,6 @@ use super::{BridgeConfig, ConnectError}; - -use rumqttc::{ - self, certs, pkcs8_private_keys, rsa_private_keys, Client, Event, Incoming, MqttOptions, - Outgoing, Packet, QoS, Transport, -}; - -use rustls_0_19::ClientConfig; - -use std::fs; -use std::io::{Error, ErrorKind}; -use std::path::PathBuf; -use std::{fs::File, io::BufReader}; -use tedge_config::FilePath; +use certificate::parse_root_certificate::*; +use rumqttc::{self, Client, Event, Incoming, MqttOptions, Outgoing, Packet, QoS, Transport}; // Connect directly to the c8y cloud over mqtt and publish device create message. pub fn create_device_with_direct_connection( @@ -27,18 +16,18 @@ pub fn create_device_with_direct_connection( let mut mqtt_options = MqttOptions::new(bridge_config.remote_clientid.clone(), host[0], 8883); mqtt_options.set_keep_alive(std::time::Duration::from_secs(5)); - let mut client_config = ClientConfig::new(); + let mut tls_config = create_tls_config(); load_root_certs( - &mut client_config.root_store, - bridge_config.bridge_root_cert_path.clone(), + &mut tls_config.root_store, + bridge_config.bridge_root_cert_path.clone().into(), )?; - let pvt_key = read_pvt_key(bridge_config.bridge_keyfile.clone())?; - let cert_chain = read_cert_chain(bridge_config.bridge_certfile.clone())?; + let pvt_key = read_pvt_key(bridge_config.bridge_keyfile.clone().into())?; + let cert_chain = read_cert_chain(bridge_config.bridge_certfile.clone().into())?; - let _ = client_config.set_single_client_cert(cert_chain, pvt_key); - mqtt_options.set_transport(Transport::tls_with_config(client_config.into())); + let _ = tls_config.set_single_client_cert(cert_chain, pvt_key); + mqtt_options.set_transport(Transport::tls_with_config(tls_config.into())); let (mut client, mut connection) = Client::new(mqtt_options, 10); @@ -106,127 +95,3 @@ fn publish_device_create_message( )?; Ok(()) } - -fn load_root_certs( - root_store: &mut rustls_0_19::RootCertStore, - cert_path: FilePath, -) -> Result<(), ConnectError> { - if fs::metadata(&cert_path)?.is_dir() { - for file_entry in fs::read_dir(cert_path)? { - add_root_cert(root_store, file_entry?.path())?; - } - } else { - add_root_cert(root_store, cert_path.into())?; - } - Ok(()) -} - -fn add_root_cert( - root_store: &mut rustls_0_19::RootCertStore, - cert_path: PathBuf, -) -> Result<(), ConnectError> { - let f = File::open(cert_path)?; - let mut rd = BufReader::new(f); - let _ = root_store.add_pem_file(&mut rd).map(|_| ()).map_err(|()| { - Error::new( - ErrorKind::InvalidData, - "could not load PEM file".to_string(), - ) - }); - Ok(()) -} - -fn read_pvt_key(key_file: tedge_config::FilePath) -> Result { - parse_pkcs8_key(key_file.clone()).or_else(|_| parse_rsa_key(key_file)) -} - -fn parse_pkcs8_key( - key_file: tedge_config::FilePath, -) -> Result { - let f = File::open(&key_file)?; - let mut key_reader = BufReader::new(f); - match pkcs8_private_keys(&mut key_reader) { - Ok(key) if !key.is_empty() => Ok(key[0].clone()), - _ => Err(ConnectError::UnknownPrivateKeyFormat), - } -} - -fn parse_rsa_key( - key_file: tedge_config::FilePath, -) -> Result { - let f = File::open(&key_file)?; - let mut key_reader = BufReader::new(f); - match rsa_private_keys(&mut key_reader) { - Ok(key) if !key.is_empty() => Ok(key[0].clone()), - _ => Err(ConnectError::UnknownPrivateKeyFormat), - } -} - -fn read_cert_chain( - cert_file: tedge_config::FilePath, -) -> Result, ConnectError> { - let f = File::open(cert_file)?; - let mut cert_reader = BufReader::new(f); - certs(&mut cert_reader).map_err(|_| ConnectError::RumqttcCertificate) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - use tempfile::NamedTempFile; - - #[test] - fn parse_private_rsa_key() { - let key = concat!( - "-----BEGIN RSA PRIVATE KEY-----\n", - "MC4CAQ\n", - "-----END RSA PRIVATE KEY-----" - ); - let mut temp_file = NamedTempFile::new().unwrap(); - temp_file.write_all(key.as_bytes()).unwrap(); - let result = parse_rsa_key(temp_file.path().into()).unwrap(); - let pvt_key = rustls_0_19::PrivateKey(vec![48, 46, 2, 1]); - assert_eq!(result, pvt_key); - } - - #[test] - fn parse_private_pkcs8_key() { - let key = concat! { - "-----BEGIN PRIVATE KEY-----\n", - "MC4CAQ\n", - "-----END PRIVATE KEY-----"}; - let mut temp_file = NamedTempFile::new().unwrap(); - temp_file.write_all(key.as_bytes()).unwrap(); - let result = parse_pkcs8_key(temp_file.path().into()).unwrap(); - let pvt_key = rustls_0_19::PrivateKey(vec![48, 46, 2, 1]); - assert_eq!(result, pvt_key); - } - - #[test] - fn parse_supported_key() { - let key = concat!( - "-----BEGIN RSA PRIVATE KEY-----\n", - "MC4CAQ\n", - "-----END RSA PRIVATE KEY-----" - ); - let mut temp_file = NamedTempFile::new().unwrap(); - temp_file.write_all(key.as_bytes()).unwrap(); - let parsed_key = read_pvt_key(temp_file.path().into()).unwrap(); - let expected_pvt_key = rustls_0_19::PrivateKey(vec![48, 46, 2, 1]); - assert_eq!(parsed_key, expected_pvt_key); - } - - #[test] - fn parse_unsupported_key() { - let key = concat!( - "-----BEGIN DSA PRIVATE KEY-----\n", - "MC4CAQ\n", - "-----END DSA PRIVATE KEY-----" - ); - let mut temp_file = NamedTempFile::new().unwrap(); - temp_file.write_all(key.as_bytes()).unwrap(); - let err = read_pvt_key(temp_file.path().into()).unwrap_err(); - assert!(matches!(err, ConnectError::UnknownPrivateKeyFormat)); - } -} diff --git a/crates/core/tedge/src/cli/connect/error.rs b/crates/core/tedge/src/cli/connect/error.rs index 07ddbfbc..f3375504 100644 --- a/crates/core/tedge/src/cli/connect/error.rs +++ b/crates/core/tedge/src/cli/connect/error.rs @@ -44,9 +44,6 @@ pub enum ConnectError { )] InvalidJWTToken { token: String, reason: String }, - #[error("Fail to parse the private key")] - UnknownPrivateKeyFormat, - - #[error("Could not parse certificate")] - RumqttcCertificate, + #[error(transparent)] + CertificateError(#[from] certificate::CertificateError), } -- cgit v1.2.3