From cd1136f5a115a96cec21adae9e80d8dc3a21c20e Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 2 Dec 2024 11:46:05 -0500 Subject: [PATCH 01/11] RUST-226 Support tlsCertificateKeyFilePassword when using rustls --- Cargo.toml | 3 +- src/client/options.rs | 21 ++++++++ src/runtime/tls_rustls.rs | 104 +++++++++++++++++++++----------------- 3 files changed, 80 insertions(+), 48 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 30e187e1c..013bfb878 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ exclude = [ default = ["compat-3-0-0", "rustls-tls", "dns-resolver"] compat-3-0-0 = [] sync = [] -rustls-tls = ["dep:rustls", "dep:rustls-pemfile", "dep:tokio-rustls"] +rustls-tls = ["dep:rustls", "dep:rustls-pemfile", "dep:tokio-rustls", "dep:pkcs8"] openssl-tls = ["dep:openssl", "dep:openssl-probe", "dep:tokio-openssl"] dns-resolver = ["dep:hickory-resolver", "dep:hickory-proto"] @@ -96,6 +96,7 @@ num_cpus = { version = "1.13.1", optional = true } openssl = { version = "0.10.38", optional = true } openssl-probe = { version = "0.1.5", optional = true } percent-encoding = "2.0.0" +pkcs8 = { version = "0.10.2", features = ["encryption", "pkcs5"], optional = true } rand = { version = "0.8.3", features = ["small_rng"] } rayon = { version = "1.5.3", optional = true } rustc_version_runtime = "0.3.0" diff --git a/src/client/options.rs b/src/client/options.rs index e9bed02f1..3a3d844c2 100644 --- a/src/client/options.rs +++ b/src/client/options.rs @@ -1047,6 +1047,9 @@ pub struct TlsOptions { /// The default value is to error on invalid hostnames. #[cfg(feature = "openssl-tls")] pub allow_invalid_hostnames: Option, + + #[cfg(feature = "rustls-tls")] + pub tls_certificate_key_file_password: Option>, } impl TlsOptions { @@ -2126,6 +2129,24 @@ impl ConnectionString { )) } }, + "tlscertificatekeyfilepassword" => match &mut self.tls { + Some(Tls::Disabled) => { + return Err(ErrorKind::InvalidArgument { + message: "'tlsCertificateKeyFilePassword' can't be set if tls=false".into(), + } + .into()); + } + Some(Tls::Enabled(options)) => { + options.tls_certificate_key_file_password = Some(value.as_bytes().to_vec()); + } + None => { + self.tls = Some(Tls::Enabled( + TlsOptions::builder() + .tls_certificate_key_file_password(value.as_bytes().to_vec()) + .build(), + )) + } + }, "uuidrepresentation" => match value.to_lowercase().as_str() { "csharplegacy" => self.uuid_representation = Some(UuidRepresentation::CSharpLegacy), "javalegacy" => self.uuid_representation = Some(UuidRepresentation::JavaLegacy), diff --git a/src/runtime/tls_rustls.rs b/src/runtime/tls_rustls.rs index 6dfdecd05..7cf5a9646 100644 --- a/src/runtime/tls_rustls.rs +++ b/src/runtime/tls_rustls.rs @@ -86,61 +86,71 @@ fn make_rustls_config(cfg: TlsOptions) -> Result { store.add_trust_anchors(trust_anchors); } - let mut config = if let Some(path) = cfg.cert_key_file_path { - let mut file = BufReader::new(File::open(&path)?); - let certs = match certs(&mut file) { - Ok(certs) => certs.into_iter().map(Certificate).collect(), - Err(error) => { - return Err(ErrorKind::InvalidTlsConfig { - message: format!( - "Unable to parse PEM-encoded client certificate from {}: {}", - path.display(), - error, - ), + let mut config = + if let Some(path) = cfg.cert_key_file_path { + let mut file = BufReader::new(File::open(&path)?); + let mut raw_certs = certs(&mut file).map_err(|error| ErrorKind::InvalidTlsConfig { + message: format!( + "Unable to parse PEM-encoded client certificate from {}: {}", + path.display(), + error, + ), + })?; + if let Some(cert_pw) = cfg.tls_certificate_key_file_password.as_deref() { + for cert in &mut raw_certs { + let encrypted = pkcs8::EncryptedPrivateKeyInfo::try_from(cert.as_slice()) + .map_err(|error| ErrorKind::InvalidTlsConfig { + message: format!("Invalid encrypted client certificate: {}", error), + })?; + let decrypted = encrypted.decrypt(cert_pw).map_err(|error| { + ErrorKind::InvalidTlsConfig { + message: format!("Failed to decrypt client certificate: {}", error), + } + })?; + *cert = decrypted.as_bytes().to_vec(); } - .into()) } - }; + let certs = raw_certs.into_iter().map(Certificate).collect(); - file.rewind()?; - let key = loop { - match read_one(&mut file) { - Ok(Some(Item::PKCS8Key(bytes))) | Ok(Some(Item::RSAKey(bytes))) => { - break rustls::PrivateKey(bytes) - } - Ok(Some(_)) => continue, - Ok(None) => { - return Err(ErrorKind::InvalidTlsConfig { - message: format!("No PEM-encoded keys in {}", path.display()), + file.rewind()?; + let key = loop { + match read_one(&mut file) { + Ok(Some(Item::PKCS8Key(bytes))) | Ok(Some(Item::RSAKey(bytes))) => { + break rustls::PrivateKey(bytes) } - .into()) - } - Err(_) => { - return Err(ErrorKind::InvalidTlsConfig { - message: format!( - "Unable to parse PEM-encoded item from {}", - path.display() - ), + Ok(Some(_)) => continue, + Ok(None) => { + return Err(ErrorKind::InvalidTlsConfig { + message: format!("No PEM-encoded keys in {}", path.display()), + } + .into()) + } + Err(_) => { + return Err(ErrorKind::InvalidTlsConfig { + message: format!( + "Unable to parse PEM-encoded item from {}", + path.display() + ), + } + .into()) } - .into()) } - } + }; + + ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(store) + .with_client_auth_cert(certs, key) + .map_err(|error| ErrorKind::InvalidTlsConfig { + message: error.to_string(), + })? + } else { + ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(store) + .with_no_client_auth() }; - ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(store) - .with_client_auth_cert(certs, key) - .map_err(|error| ErrorKind::InvalidTlsConfig { - message: error.to_string(), - })? - } else { - ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(store) - .with_no_client_auth() - }; - if let Some(true) = cfg.allow_invalid_certificates { // nosemgrep: rustls-dangerous config // mongodb rating: No Fix Needed From 76496b2baa9007645cee44d8dcc40dc669fad54d Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 2 Dec 2024 11:57:07 -0500 Subject: [PATCH 02/11] fix features --- src/client/options.rs | 5 ++++- src/runtime/tls_rustls.rs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/client/options.rs b/src/client/options.rs index 3a3d844c2..4107fb9d9 100644 --- a/src/client/options.rs +++ b/src/client/options.rs @@ -1048,7 +1048,9 @@ pub struct TlsOptions { #[cfg(feature = "openssl-tls")] pub allow_invalid_hostnames: Option, - #[cfg(feature = "rustls-tls")] + /// The password for encrypted certificates in the CA file. If set, all certificates must be + /// encrypted. Only supported with `rustls`. + #[cfg(all(feature = "rustls-tls", not(feature = "openssl-tls")))] pub tls_certificate_key_file_password: Option>, } @@ -2129,6 +2131,7 @@ impl ConnectionString { )) } }, + #[cfg(all(feature = "rustls-tls", not(feature = "openssl-tls")))] "tlscertificatekeyfilepassword" => match &mut self.tls { Some(Tls::Disabled) => { return Err(ErrorKind::InvalidArgument { diff --git a/src/runtime/tls_rustls.rs b/src/runtime/tls_rustls.rs index 7cf5a9646..828d65634 100644 --- a/src/runtime/tls_rustls.rs +++ b/src/runtime/tls_rustls.rs @@ -96,6 +96,7 @@ fn make_rustls_config(cfg: TlsOptions) -> Result { error, ), })?; + #[cfg(not(feature = "openssl-tls"))] if let Some(cert_pw) = cfg.tls_certificate_key_file_password.as_deref() { for cert in &mut raw_certs { let encrypted = pkcs8::EncryptedPrivateKeyInfo::try_from(cert.as_slice()) From 6591c1aa84a7c25c01d485106966525e0331ec80 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 2 Dec 2024 12:58:00 -0500 Subject: [PATCH 03/11] oops, for real this time --- Cargo.toml | 3 +- src/client/options.rs | 6 +-- src/runtime/tls_openssl.rs | 8 ++++ src/runtime/tls_rustls.rs | 97 +++++++++++++++++++++++--------------- 4 files changed, 71 insertions(+), 43 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 013bfb878..6ac810af2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ exclude = [ default = ["compat-3-0-0", "rustls-tls", "dns-resolver"] compat-3-0-0 = [] sync = [] -rustls-tls = ["dep:rustls", "dep:rustls-pemfile", "dep:tokio-rustls", "dep:pkcs8"] +rustls-tls = ["dep:rustls", "dep:rustls-pemfile", "dep:tokio-rustls", "dep:pkcs8", "dep:pem"] openssl-tls = ["dep:openssl", "dep:openssl-probe", "dep:tokio-openssl"] dns-resolver = ["dep:hickory-resolver", "dep:hickory-proto"] @@ -95,6 +95,7 @@ mongodb-internal-macros = { path = "macros", version = "3.1.0" } num_cpus = { version = "1.13.1", optional = true } openssl = { version = "0.10.38", optional = true } openssl-probe = { version = "0.1.5", optional = true } +pem = { version = "3.0.4", optional = true } percent-encoding = "2.0.0" pkcs8 = { version = "0.10.2", features = ["encryption", "pkcs5"], optional = true } rand = { version = "0.8.3", features = ["small_rng"] } diff --git a/src/client/options.rs b/src/client/options.rs index 4107fb9d9..14b550434 100644 --- a/src/client/options.rs +++ b/src/client/options.rs @@ -1048,9 +1048,8 @@ pub struct TlsOptions { #[cfg(feature = "openssl-tls")] pub allow_invalid_hostnames: Option, - /// The password for encrypted certificates in the CA file. If set, all certificates must be - /// encrypted. Only supported with `rustls`. - #[cfg(all(feature = "rustls-tls", not(feature = "openssl-tls")))] + /// If set, the key in `cert_key_file_path` must be encrypted with this password. Only + /// supported with `rustls`. pub tls_certificate_key_file_password: Option>, } @@ -2131,7 +2130,6 @@ impl ConnectionString { )) } }, - #[cfg(all(feature = "rustls-tls", not(feature = "openssl-tls")))] "tlscertificatekeyfilepassword" => match &mut self.tls { Some(Tls::Disabled) => { return Err(ErrorKind::InvalidArgument { diff --git a/src/runtime/tls_openssl.rs b/src/runtime/tls_openssl.rs index 5d570c270..df6de8cc9 100644 --- a/src/runtime/tls_openssl.rs +++ b/src/runtime/tls_openssl.rs @@ -26,6 +26,13 @@ impl TlsConfig { /// Create a new `TlsConfig` from the provided options from the user. /// This operation is expensive, so the resultant `TlsConfig` should be cached. pub(crate) fn new(options: TlsOptions) -> Result { + if options.tls_certificate_key_file_password.is_some() { + return Err(ErrorKind::InvalidArgument { + message: "'tlsCertificateKeyFilePassword' can't be used with 'openssl-tls'".into(), + } + .into()); + } + let verify_hostname = match options.allow_invalid_hostnames { Some(b) => !b, None => true, @@ -74,6 +81,7 @@ fn make_openssl_connector(cfg: TlsOptions) -> std::result::Result Result { store.add_trust_anchors(trust_anchors); } - let mut config = - if let Some(path) = cfg.cert_key_file_path { - let mut file = BufReader::new(File::open(&path)?); - let mut raw_certs = certs(&mut file).map_err(|error| ErrorKind::InvalidTlsConfig { - message: format!( - "Unable to parse PEM-encoded client certificate from {}: {}", - path.display(), - error, - ), + let mut config = if let Some(path) = cfg.cert_key_file_path { + let mut file = BufReader::new(File::open(&path)?); + let certs = match certs(&mut file) { + Ok(certs) => certs.into_iter().map(Certificate).collect(), + Err(error) => { + return Err(ErrorKind::InvalidTlsConfig { + message: format!( + "Unable to parse PEM-encoded client certificate from {}: {}", + path.display(), + error, + ), + } + .into()) + } + }; + + file.rewind()?; + let key = if let Some(key_pw) = cfg.tls_certificate_key_file_password.as_deref() { + let mut contents = vec![]; + file.read_to_end(&mut contents)?; + let pems = pem::parse_many(&contents).map_err(|error| ErrorKind::InvalidTlsConfig { + message: format!("Could not parse {}: {}", path.display(), error), })?; - #[cfg(not(feature = "openssl-tls"))] - if let Some(cert_pw) = cfg.tls_certificate_key_file_password.as_deref() { - for cert in &mut raw_certs { - let encrypted = pkcs8::EncryptedPrivateKeyInfo::try_from(cert.as_slice()) + let mut iter = pems + .into_iter() + .filter(|pem| pem.tag() == "ENCRYPTED PRIVATE KEY"); + match iter.next() { + Some(pem) => { + let encrypted = pkcs8::EncryptedPrivateKeyInfo::try_from(pem.contents()) .map_err(|error| ErrorKind::InvalidTlsConfig { message: format!("Invalid encrypted client certificate: {}", error), })?; - let decrypted = encrypted.decrypt(cert_pw).map_err(|error| { - ErrorKind::InvalidTlsConfig { - message: format!("Failed to decrypt client certificate: {}", error), - } - })?; - *cert = decrypted.as_bytes().to_vec(); + let decrypted = + encrypted + .decrypt(key_pw) + .map_err(|error| ErrorKind::InvalidTlsConfig { + message: format!("Failed to decrypt client certificate: {}", error), + })?; + rustls::PrivateKey(decrypted.as_bytes().to_vec()) + } + None => { + return Err(ErrorKind::InvalidTlsConfig { + message: format!("No PEM-encoded keys in {}", path.display()), + } + .into()) } } - let certs = raw_certs.into_iter().map(Certificate).collect(); - - file.rewind()?; - let key = loop { + } else { + loop { match read_one(&mut file) { Ok(Some(Item::PKCS8Key(bytes))) | Ok(Some(Item::RSAKey(bytes))) => { break rustls::PrivateKey(bytes) @@ -136,22 +156,23 @@ fn make_rustls_config(cfg: TlsOptions) -> Result { .into()) } } - }; - - ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(store) - .with_client_auth_cert(certs, key) - .map_err(|error| ErrorKind::InvalidTlsConfig { - message: error.to_string(), - })? - } else { - ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(store) - .with_no_client_auth() + } }; + ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(store) + .with_client_auth_cert(certs, key) + .map_err(|error| ErrorKind::InvalidTlsConfig { + message: error.to_string(), + })? + } else { + ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(store) + .with_no_client_auth() + }; + if let Some(true) = cfg.allow_invalid_certificates { // nosemgrep: rustls-dangerous config // mongodb rating: No Fix Needed From e79400286d414d1c34969c909688666b99ec6027 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 2 Dec 2024 14:24:11 -0500 Subject: [PATCH 04/11] openssl support --- Cargo.toml | 6 +++--- src/runtime.rs | 1 + src/runtime/pem.rs | 30 ++++++++++++++++++++++++++++++ src/runtime/tls_openssl.rs | 37 ++++++++++++++++++++++++++----------- src/runtime/tls_rustls.rs | 30 +++--------------------------- 5 files changed, 63 insertions(+), 41 deletions(-) create mode 100644 src/runtime/pem.rs diff --git a/Cargo.toml b/Cargo.toml index 6ac810af2..765c45027 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ exclude = [ default = ["compat-3-0-0", "rustls-tls", "dns-resolver"] compat-3-0-0 = [] sync = [] -rustls-tls = ["dep:rustls", "dep:rustls-pemfile", "dep:tokio-rustls", "dep:pkcs8", "dep:pem"] +rustls-tls = ["dep:rustls", "dep:rustls-pemfile", "dep:tokio-rustls"] openssl-tls = ["dep:openssl", "dep:openssl-probe", "dep:tokio-openssl"] dns-resolver = ["dep:hickory-resolver", "dep:hickory-proto"] @@ -95,9 +95,9 @@ mongodb-internal-macros = { path = "macros", version = "3.1.0" } num_cpus = { version = "1.13.1", optional = true } openssl = { version = "0.10.38", optional = true } openssl-probe = { version = "0.1.5", optional = true } -pem = { version = "3.0.4", optional = true } +pem = { version = "3.0.4" } percent-encoding = "2.0.0" -pkcs8 = { version = "0.10.2", features = ["encryption", "pkcs5"], optional = true } +pkcs8 = { version = "0.10.2", features = ["encryption", "pkcs5"] } rand = { version = "0.8.3", features = ["small_rng"] } rayon = { version = "1.5.3", optional = true } rustc_version_runtime = "0.3.0" diff --git a/src/runtime.rs b/src/runtime.rs index f76f9e308..cf9dee13f 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -8,6 +8,7 @@ mod acknowledged_message; ))] mod http; mod join_handle; +mod pem; #[cfg(any(feature = "in-use-encryption", test))] pub(crate) mod process; #[cfg(feature = "dns-resolver")] diff --git a/src/runtime/pem.rs b/src/runtime/pem.rs new file mode 100644 index 000000000..fea1a98e5 --- /dev/null +++ b/src/runtime/pem.rs @@ -0,0 +1,30 @@ +use crate::error::{ErrorKind, Result}; + +pub(crate) fn decrypt_private_key(pem_data: &[u8], password: &[u8]) -> Result> { + let pems = pem::parse_many(&pem_data).map_err(|error| ErrorKind::InvalidTlsConfig { + message: format!("Could not parse pemfile: {}", error), + })?; + let mut iter = pems + .into_iter() + .filter(|pem| pem.tag() == "ENCRYPTED PRIVATE KEY"); + let encrypted_bytes = match iter.next() { + Some(pem) => pem.into_contents(), + None => { + return Err(ErrorKind::InvalidTlsConfig { + message: "No encrypted private keys found".into(), + } + .into()) + } + }; + let encrypted_key = pkcs8::EncryptedPrivateKeyInfo::try_from(encrypted_bytes.as_slice()) + .map_err(|error| ErrorKind::InvalidTlsConfig { + message: format!("Invalid encrypted private key: {}", error), + })?; + let decrypted_key = + encrypted_key + .decrypt(password) + .map_err(|error| ErrorKind::InvalidTlsConfig { + message: format!("Failed to decrypt private key: {}", error), + })?; + Ok(decrypted_key.as_bytes().to_vec()) +} diff --git a/src/runtime/tls_openssl.rs b/src/runtime/tls_openssl.rs index df6de8cc9..73f11f3a8 100644 --- a/src/runtime/tls_openssl.rs +++ b/src/runtime/tls_openssl.rs @@ -12,6 +12,8 @@ use crate::{ error::{Error, ErrorKind, Result}, }; +use super::pem::decrypt_private_key; + pub(super) type TlsStream = SslStream; /// Configuration required to use TLS. Creating this is expensive, so its best to cache this value @@ -38,11 +40,7 @@ impl TlsConfig { None => true, }; - let connector = make_openssl_connector(options).map_err(|e| { - Error::from(ErrorKind::InvalidTlsConfig { - message: e.to_string(), - }) - })?; + let connector = make_openssl_connector(options)?; Ok(TlsConfig { connector, @@ -73,26 +71,43 @@ pub(super) async fn tls_connect( Ok(stream) } -fn make_openssl_connector(cfg: TlsOptions) -> std::result::Result { - let mut builder = SslConnector::builder(SslMethod::tls_client())?; +fn make_openssl_connector(cfg: TlsOptions) -> Result { + let openssl_err = |e: ErrorStack| { + Error::from(ErrorKind::InvalidTlsConfig { + message: e.to_string(), + }) + }; + + let mut builder = SslConnector::builder(SslMethod::tls_client()).map_err(openssl_err)?; let TlsOptions { allow_invalid_certificates, ca_file_path, cert_key_file_path, allow_invalid_hostnames: _, - tls_certificate_key_file_password: _, + tls_certificate_key_file_password, } = cfg; if let Some(true) = allow_invalid_certificates { builder.set_verify(SslVerifyMode::NONE); } if let Some(path) = ca_file_path { - builder.set_ca_file(path)?; + builder.set_ca_file(path).map_err(openssl_err)?; } if let Some(path) = cert_key_file_path { - builder.set_certificate_file(path.clone(), SslFiletype::PEM)?; - builder.set_private_key_file(path, SslFiletype::PEM)?; + builder + .set_certificate_file(path.clone(), SslFiletype::PEM) + .map_err(openssl_err)?; + if let Some(key_pw) = tls_certificate_key_file_password { + let contents = std::fs::read(&path)?; + let key_bytes = decrypt_private_key(&contents, &key_pw)?; + let key = openssl::pkey::PKey::private_key_from_pem(&key_bytes).map_err(openssl_err)?; + builder.set_private_key(&key).map_err(openssl_err)?; + } else { + builder + .set_private_key_file(path, SslFiletype::PEM) + .map_err(openssl_err)?; + } } Ok(builder.build()) diff --git a/src/runtime/tls_rustls.rs b/src/runtime/tls_rustls.rs index fafd54f09..714cb501f 100644 --- a/src/runtime/tls_rustls.rs +++ b/src/runtime/tls_rustls.rs @@ -23,6 +23,8 @@ use crate::{ error::{ErrorKind, Result}, }; +use super::pem::decrypt_private_key; + pub(super) type TlsStream = tokio_rustls::client::TlsStream; /// Configuration required to use TLS. Creating this is expensive, so its best to cache this value @@ -106,33 +108,7 @@ fn make_rustls_config(cfg: TlsOptions) -> Result { let key = if let Some(key_pw) = cfg.tls_certificate_key_file_password.as_deref() { let mut contents = vec![]; file.read_to_end(&mut contents)?; - let pems = pem::parse_many(&contents).map_err(|error| ErrorKind::InvalidTlsConfig { - message: format!("Could not parse {}: {}", path.display(), error), - })?; - let mut iter = pems - .into_iter() - .filter(|pem| pem.tag() == "ENCRYPTED PRIVATE KEY"); - match iter.next() { - Some(pem) => { - let encrypted = pkcs8::EncryptedPrivateKeyInfo::try_from(pem.contents()) - .map_err(|error| ErrorKind::InvalidTlsConfig { - message: format!("Invalid encrypted client certificate: {}", error), - })?; - let decrypted = - encrypted - .decrypt(key_pw) - .map_err(|error| ErrorKind::InvalidTlsConfig { - message: format!("Failed to decrypt client certificate: {}", error), - })?; - rustls::PrivateKey(decrypted.as_bytes().to_vec()) - } - None => { - return Err(ErrorKind::InvalidTlsConfig { - message: format!("No PEM-encoded keys in {}", path.display()), - } - .into()) - } - } + rustls::PrivateKey(decrypt_private_key(&contents, key_pw)?) } else { loop { match read_one(&mut file) { From 71c8366d3bf5d1292f323342b6b13e36a0ef6ff2 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 2 Dec 2024 15:57:21 -0500 Subject: [PATCH 05/11] Update src/runtime/tls_openssl.rs Co-authored-by: Kevin Albertson --- src/runtime/tls_openssl.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/runtime/tls_openssl.rs b/src/runtime/tls_openssl.rs index 73f11f3a8..756279067 100644 --- a/src/runtime/tls_openssl.rs +++ b/src/runtime/tls_openssl.rs @@ -28,13 +28,6 @@ impl TlsConfig { /// Create a new `TlsConfig` from the provided options from the user. /// This operation is expensive, so the resultant `TlsConfig` should be cached. pub(crate) fn new(options: TlsOptions) -> Result { - if options.tls_certificate_key_file_password.is_some() { - return Err(ErrorKind::InvalidArgument { - message: "'tlsCertificateKeyFilePassword' can't be used with 'openssl-tls'".into(), - } - .into()); - } - let verify_hostname = match options.allow_invalid_hostnames { Some(b) => !b, None => true, From 796628bb5806ebc28451f69d08ee0f92499b93ec Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 2 Dec 2024 15:57:38 -0500 Subject: [PATCH 06/11] Update src/runtime/tls_openssl.rs Co-authored-by: Kevin Albertson --- src/runtime/tls_openssl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/tls_openssl.rs b/src/runtime/tls_openssl.rs index 756279067..74f2d8e89 100644 --- a/src/runtime/tls_openssl.rs +++ b/src/runtime/tls_openssl.rs @@ -94,7 +94,7 @@ fn make_openssl_connector(cfg: TlsOptions) -> Result { if let Some(key_pw) = tls_certificate_key_file_password { let contents = std::fs::read(&path)?; let key_bytes = decrypt_private_key(&contents, &key_pw)?; - let key = openssl::pkey::PKey::private_key_from_pem(&key_bytes).map_err(openssl_err)?; + let key = openssl::pkey::PKey::private_key_from_der(&key_bytes).map_err(openssl_err)?; builder.set_private_key(&key).map_err(openssl_err)?; } else { builder From 42089d8ebb0b56b4c12b7cf19ff5f9228c540ef6 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 2 Dec 2024 15:57:49 -0500 Subject: [PATCH 07/11] Update src/client/options.rs Co-authored-by: Kevin Albertson --- src/client/options.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/options.rs b/src/client/options.rs index 14b550434..ebe53a467 100644 --- a/src/client/options.rs +++ b/src/client/options.rs @@ -1048,8 +1048,7 @@ pub struct TlsOptions { #[cfg(feature = "openssl-tls")] pub allow_invalid_hostnames: Option, - /// If set, the key in `cert_key_file_path` must be encrypted with this password. Only - /// supported with `rustls`. + /// If set, the key in `cert_key_file_path` must be encrypted with this password. pub tls_certificate_key_file_password: Option>, } From ea8db2557673468894a0a002f6973b0d4578e844 Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 2 Dec 2024 14:51:28 -0500 Subject: [PATCH 08/11] clippy --- src/runtime/pem.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/pem.rs b/src/runtime/pem.rs index fea1a98e5..ef3c3109f 100644 --- a/src/runtime/pem.rs +++ b/src/runtime/pem.rs @@ -1,7 +1,7 @@ use crate::error::{ErrorKind, Result}; pub(crate) fn decrypt_private_key(pem_data: &[u8], password: &[u8]) -> Result> { - let pems = pem::parse_many(&pem_data).map_err(|error| ErrorKind::InvalidTlsConfig { + let pems = pem::parse_many(pem_data).map_err(|error| ErrorKind::InvalidTlsConfig { message: format!("Could not parse pemfile: {}", error), })?; let mut iter = pems From 8aa876dffdb84eff14ec7b227ebfe31e558b88cb Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Mon, 2 Dec 2024 16:33:16 -0500 Subject: [PATCH 09/11] msrv fix? --- .evergreen/MSRV-Cargo.toml.diff | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.evergreen/MSRV-Cargo.toml.diff b/.evergreen/MSRV-Cargo.toml.diff index d4185946f..81793c287 100644 --- a/.evergreen/MSRV-Cargo.toml.diff +++ b/.evergreen/MSRV-Cargo.toml.diff @@ -1,8 +1,10 @@ -141c141 +116a117 +> url = "=2.5.2" +144c145 < version = "1.17.0" --- > version = "=1.38.0" -150c150 +153c154 < version = "0.7.0" --- > version = "=0.7.11" From 061c5a40cc7f82297e20161230bd01f4b406c8df Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Tue, 3 Dec 2024 11:19:40 -0500 Subject: [PATCH 10/11] make it optional --- Cargo.toml | 5 ++-- src/client/options.rs | 2 ++ src/runtime.rs | 1 + src/runtime/tls_openssl.rs | 25 +++++++++++------- src/runtime/tls_rustls.rs | 53 +++++++++++++++++++------------------- 5 files changed, 47 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 765c45027..b7b3a48ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ sync = [] rustls-tls = ["dep:rustls", "dep:rustls-pemfile", "dep:tokio-rustls"] openssl-tls = ["dep:openssl", "dep:openssl-probe", "dep:tokio-openssl"] dns-resolver = ["dep:hickory-resolver", "dep:hickory-proto"] +cert-key-password = ["dep:pem", "dep:pkcs8"] # Enable support for MONGODB-AWS authentication. # This can only be used with the tokio-runtime feature flag. @@ -95,9 +96,9 @@ mongodb-internal-macros = { path = "macros", version = "3.1.0" } num_cpus = { version = "1.13.1", optional = true } openssl = { version = "0.10.38", optional = true } openssl-probe = { version = "0.1.5", optional = true } -pem = { version = "3.0.4" } +pem = { version = "3.0.4", optional = true } percent-encoding = "2.0.0" -pkcs8 = { version = "0.10.2", features = ["encryption", "pkcs5"] } +pkcs8 = { version = "0.10.2", features = ["encryption", "pkcs5"], optional = true } rand = { version = "0.8.3", features = ["small_rng"] } rayon = { version = "1.5.3", optional = true } rustc_version_runtime = "0.3.0" diff --git a/src/client/options.rs b/src/client/options.rs index ebe53a467..e420834be 100644 --- a/src/client/options.rs +++ b/src/client/options.rs @@ -1049,6 +1049,7 @@ pub struct TlsOptions { pub allow_invalid_hostnames: Option, /// If set, the key in `cert_key_file_path` must be encrypted with this password. + #[cfg(feature = "cert-key-password")] pub tls_certificate_key_file_password: Option>, } @@ -2129,6 +2130,7 @@ impl ConnectionString { )) } }, + #[cfg(feature = "cert-key-password")] "tlscertificatekeyfilepassword" => match &mut self.tls { Some(Tls::Disabled) => { return Err(ErrorKind::InvalidArgument { diff --git a/src/runtime.rs b/src/runtime.rs index cf9dee13f..e46605bb2 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -8,6 +8,7 @@ mod acknowledged_message; ))] mod http; mod join_handle; +#[cfg(feature = "cert-key-password")] mod pem; #[cfg(any(feature = "in-use-encryption", test))] pub(crate) mod process; diff --git a/src/runtime/tls_openssl.rs b/src/runtime/tls_openssl.rs index 74f2d8e89..cbc431aee 100644 --- a/src/runtime/tls_openssl.rs +++ b/src/runtime/tls_openssl.rs @@ -12,8 +12,6 @@ use crate::{ error::{Error, ErrorKind, Result}, }; -use super::pem::decrypt_private_key; - pub(super) type TlsStream = SslStream; /// Configuration required to use TLS. Creating this is expensive, so its best to cache this value @@ -78,6 +76,7 @@ fn make_openssl_connector(cfg: TlsOptions) -> Result { ca_file_path, cert_key_file_path, allow_invalid_hostnames: _, + #[cfg(feature = "cert-key-password")] tls_certificate_key_file_password, } = cfg; @@ -91,16 +90,22 @@ fn make_openssl_connector(cfg: TlsOptions) -> Result { builder .set_certificate_file(path.clone(), SslFiletype::PEM) .map_err(openssl_err)?; - if let Some(key_pw) = tls_certificate_key_file_password { - let contents = std::fs::read(&path)?; - let key_bytes = decrypt_private_key(&contents, &key_pw)?; - let key = openssl::pkey::PKey::private_key_from_der(&key_bytes).map_err(openssl_err)?; - builder.set_private_key(&key).map_err(openssl_err)?; - } else { + // Inner fn so the cert-key-password path can early return + let handle_private_key = || -> Result<()> { + #[cfg(feature = "cert-key-password")] + if let Some(key_pw) = tls_certificate_key_file_password { + let contents = std::fs::read(&path)?; + let key_bytes = super::pem::decrypt_private_key(&contents, &key_pw)?; + let key = + openssl::pkey::PKey::private_key_from_der(&key_bytes).map_err(openssl_err)?; + builder.set_private_key(&key).map_err(openssl_err)?; + return Ok(()); + } builder .set_private_key_file(path, SslFiletype::PEM) - .map_err(openssl_err)?; - } + .map_err(openssl_err) + }; + handle_private_key()?; } Ok(builder.build()) diff --git a/src/runtime/tls_rustls.rs b/src/runtime/tls_rustls.rs index 714cb501f..c60b2af7c 100644 --- a/src/runtime/tls_rustls.rs +++ b/src/runtime/tls_rustls.rs @@ -1,7 +1,7 @@ use std::{ convert::TryFrom, fs::File, - io::{BufReader, Read, Seek}, + io::{BufReader, Seek}, sync::Arc, time::SystemTime, }; @@ -23,8 +23,6 @@ use crate::{ error::{ErrorKind, Result}, }; -use super::pem::decrypt_private_key; - pub(super) type TlsStream = tokio_rustls::client::TlsStream; /// Configuration required to use TLS. Creating this is expensive, so its best to cache this value @@ -105,32 +103,33 @@ fn make_rustls_config(cfg: TlsOptions) -> Result { }; file.rewind()?; - let key = if let Some(key_pw) = cfg.tls_certificate_key_file_password.as_deref() { - let mut contents = vec![]; - file.read_to_end(&mut contents)?; - rustls::PrivateKey(decrypt_private_key(&contents, key_pw)?) - } else { - loop { - match read_one(&mut file) { - Ok(Some(Item::PKCS8Key(bytes))) | Ok(Some(Item::RSAKey(bytes))) => { - break rustls::PrivateKey(bytes) - } - Ok(Some(_)) => continue, - Ok(None) => { - return Err(ErrorKind::InvalidTlsConfig { - message: format!("No PEM-encoded keys in {}", path.display()), - } - .into()) + let key = loop { + #[cfg(feature = "cert-key-password")] + if let Some(key_pw) = cfg.tls_certificate_key_file_password.as_deref() { + use std::io::Read; + let mut contents = vec![]; + file.read_to_end(&mut contents)?; + break rustls::PrivateKey(super::pem::decrypt_private_key(&contents, key_pw)?); + } + match read_one(&mut file) { + Ok(Some(Item::PKCS8Key(bytes))) | Ok(Some(Item::RSAKey(bytes))) => { + break rustls::PrivateKey(bytes) + } + Ok(Some(_)) => continue, + Ok(None) => { + return Err(ErrorKind::InvalidTlsConfig { + message: format!("No PEM-encoded keys in {}", path.display()), } - Err(_) => { - return Err(ErrorKind::InvalidTlsConfig { - message: format!( - "Unable to parse PEM-encoded item from {}", - path.display() - ), - } - .into()) + .into()) + } + Err(_) => { + return Err(ErrorKind::InvalidTlsConfig { + message: format!( + "Unable to parse PEM-encoded item from {}", + path.display() + ), } + .into()) } } }; From 0f85a68ba16ed0ffa5f96bd58088fce465d157aa Mon Sep 17 00:00:00 2001 From: Abraham Egnor Date: Wed, 4 Dec 2024 10:18:00 -0500 Subject: [PATCH 11/11] unskip test --- .evergreen/run-tests.sh | 2 +- src/client/options.rs | 7 +++++++ src/client/options/test.rs | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 2ccc78ee3..b12a69ae7 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -6,7 +6,7 @@ set -o pipefail source .evergreen/env.sh source .evergreen/cargo-test.sh -FEATURE_FLAGS+=("tracing-unstable") +FEATURE_FLAGS+=("tracing-unstable" "cert-key-password") if [ "$OPENSSL" = true ]; then FEATURE_FLAGS+=("openssl-tls") diff --git a/src/client/options.rs b/src/client/options.rs index e420834be..73374adf2 100644 --- a/src/client/options.rs +++ b/src/client/options.rs @@ -1068,6 +1068,8 @@ impl TlsOptions { tlscafile: Option<&'a str>, tlscertificatekeyfile: Option<&'a str>, tlsallowinvalidcertificates: Option, + #[cfg(feature = "cert-key-password")] + tlscertificatekeyfilepassword: Option<&'a str>, } let state = TlsOptionsHelper { @@ -1081,6 +1083,11 @@ impl TlsOptions { .as_ref() .map(|s| s.to_str().unwrap()), tlsallowinvalidcertificates: tls_options.allow_invalid_certificates, + #[cfg(feature = "cert-key-password")] + tlscertificatekeyfilepassword: tls_options + .tls_certificate_key_file_password + .as_deref() + .map(|b| std::str::from_utf8(b).unwrap()), }; state.serialize(serializer) } diff --git a/src/client/options/test.rs b/src/client/options/test.rs index 3d1f4da9e..502ac04ed 100644 --- a/src/client/options/test.rs +++ b/src/client/options/test.rs @@ -20,7 +20,7 @@ static SKIPPED_TESTS: Lazy> = Lazy::new(|| { "tlsInsecure is parsed correctly", // The driver does not support maxPoolSize=0 "maxPoolSize=0 does not error", - // TODO RUST-226: unskip this test + #[cfg(not(feature = "cert-key-password"))] "Valid tlsCertificateKeyFilePassword is parsed correctly", ];