diff --git a/CHANGELOG.md b/CHANGELOG.md index 39d6b674f..aef504b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Fixed: - Accept '@' in usernames to support bouncers that use the user@identifier/network convention - Prevent rare scenario where broadcast messages' timestamp would not match time the messages are received +- Fix SASL on macos by using RUSTLS backend Changed: diff --git a/Cargo.lock b/Cargo.lock index 538d0cbd4..774723b05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2159,9 +2159,11 @@ dependencies = [ "bytes", "futures", "irc_proto", + "rustls-native-certs", + "rustls-pemfile 2.1.1", "thiserror", "tokio", - "tokio-native-tls", + "tokio-rustls", "tokio-util", ] @@ -3395,7 +3397,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", @@ -3434,6 +3436,21 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "roxmltree" version = "0.19.0" @@ -3498,6 +3515,33 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4d6d8ad9f2492485e13453acbb291dd08f64441b6609c491f1c2cd2c6b4fe1" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.1", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -3507,6 +3551,33 @@ dependencies = [ "base64", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +dependencies = [ + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" + +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustybuzz" version = "0.11.0" @@ -3870,6 +3941,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "svg_fmt" version = "0.4.2" @@ -4130,6 +4207,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.15" @@ -4375,6 +4463,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -5411,6 +5505,12 @@ dependencies = [ "syn 2.0.55", ] +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/data/src/config/server.rs b/data/src/config/server.rs index cb147efbe..1325ea4c9 100644 --- a/data/src/config/server.rs +++ b/data/src/config/server.rs @@ -164,8 +164,8 @@ impl Sasl { } fn external_key(&self) -> Option<&PathBuf> { - if let Self::External { cert, key, .. } = self { - Some(key.as_ref().unwrap_or(cert)) + if let Self::External { key, .. } = self { + key.as_ref() } else { None } diff --git a/data/src/server.rs b/data/src/server.rs index b6ccd8a5f..6307ee9b7 100644 --- a/data/src/server.rs +++ b/data/src/server.rs @@ -72,14 +72,22 @@ impl Map { } if let Some(sasl) = &mut config.sasl { match sasl { - Sasl::Plain { password: password @ None, password_file: Some(pass_file), .. } => { + Sasl::Plain { + password: Some(_), + password_file: Some(_), + .. + } => { + return Err(Error::Parse("Exactly one of sasl.plain.password or sasl.plain.password_file must be set.".to_string())); + } + Sasl::Plain { + password: password @ None, + password_file: Some(pass_file), + .. + } => { let pass = fs::read_to_string(pass_file)?; *password = Some(pass); - }, - Sasl::Plain { password: Some(_), password_file: None, .. } => {}, - _ => { - return Err(Error::Parse("Exactly one of sasl.plain.password or sasl.plain.password_file must be set.".to_string())); } + _ => {} } } } diff --git a/irc/Cargo.toml b/irc/Cargo.toml index 13d1c39e9..0a5fb41b1 100644 --- a/irc/Cargo.toml +++ b/irc/Cargo.toml @@ -9,8 +9,10 @@ bytes = "1.4.0" futures = "0.3.28" thiserror = "1.0.30" tokio = { version = "1.29", features = ["net", "full"] } -tokio-native-tls = "0.3.1" +tokio-rustls = { version = "0.26.0", default-features = false, features = ["tls12", "ring"] } tokio-util = { version = "0.7", features = ["codec"] } +rustls-native-certs = "0.7.0" +rustls-pemfile = "2.1.1" [dependencies.proto] path = "proto" diff --git a/irc/src/connection.rs b/irc/src/connection.rs index 66ffbb671..7b7eeeda0 100644 --- a/irc/src/connection.rs +++ b/irc/src/connection.rs @@ -1,14 +1,14 @@ -use std::io; use std::net::IpAddr; use std::path::PathBuf; use futures::{Sink, SinkExt, Stream, StreamExt}; -use tokio::fs; use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream}; -use tokio_native_tls::native_tls::{Certificate, Identity}; -use tokio_native_tls::{native_tls, TlsConnector, TlsStream}; -use tokio_util::codec::{self, Framed}; +use tokio_rustls::client::TlsStream; +use tokio_util::codec; +use tokio_util::codec::Framed; + +mod tls; pub enum Connection { Tls(Framed, Codec>), @@ -44,25 +44,15 @@ impl Connection { client_key_path, } = config.security { - let mut builder = native_tls::TlsConnector::builder(); - builder.danger_accept_invalid_certs(accept_invalid_certs); - - if let Some(path) = root_cert_path { - let bytes = fs::read(path).await?; - let cert = Certificate::from_pem(&bytes)?; - builder.add_root_certificate(cert); - } - - if let (Some(cert_path), Some(pkcs8_key_path)) = (client_cert_path, client_key_path) { - let cert_bytes = fs::read(cert_path).await?; - let pkcs8_key_bytes = fs::read(pkcs8_key_path).await?; - let identity = Identity::from_pkcs8(&cert_bytes, &pkcs8_key_bytes)?; - builder.identity(identity); - } - - let tls = TlsConnector::from(builder.build()?) - .connect(config.server, tcp) - .await?; + let tls = tls::connect( + tcp, + config.server, + accept_invalid_certs, + root_cert_path, + client_cert_path, + client_key_path, + ) + .await?; Ok(Self::Tls(Framed::new(tls, codec))) } else { @@ -106,9 +96,9 @@ impl Connection { #[derive(Debug, thiserror::Error)] pub enum Error { #[error("tls error: {0}")] - Tls(#[from] tokio_native_tls::native_tls::Error), + Tls(#[from] tls::Error), #[error("io error: {0}")] - Io(#[from] io::Error), + Io(#[from] std::io::Error), } macro_rules! delegate { diff --git a/irc/src/connection/tls.rs b/irc/src/connection/tls.rs new file mode 100644 index 000000000..ce929bcc7 --- /dev/null +++ b/irc/src/connection/tls.rs @@ -0,0 +1,132 @@ +use std::{io::Cursor, path::PathBuf, sync::Arc}; + +use bytes::Bytes; +use tokio::{fs, net::TcpStream}; +use tokio_rustls::{ + client::TlsStream, + rustls::{ + self, + client::danger::{self, ServerCertVerifier}, + pki_types, + }, + TlsConnector, +}; + +pub async fn connect<'a>( + tcp: TcpStream, + server: &str, + accept_invalid_certs: bool, + root_cert_path: Option<&'a PathBuf>, + client_cert_path: Option<&'a PathBuf>, + client_key_path: Option<&'a PathBuf>, +) -> Result, Error> { + let builder = if accept_invalid_certs { + rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(AcceptInvalidCerts)) + } else { + let mut roots = rustls::RootCertStore::empty(); + + for cert in rustls_native_certs::load_native_certs()? { + roots.add(cert).unwrap(); + } + + if let Some(cert_path) = root_cert_path { + let cert_bytes = fs::read(&cert_path).await?; + let certs = rustls_pemfile::certs(&mut Cursor::new(&cert_bytes)) + .collect::, _>>()?; + roots.add_parsable_certificates(certs); + } + + rustls::ClientConfig::builder().with_root_certificates(roots) + }; + + let client_config = if let Some(cert_path) = client_cert_path { + let cert_bytes = Bytes::from(fs::read(&cert_path).await?); + + let key_bytes = if let Some(key_path) = client_key_path { + Bytes::from(fs::read(&key_path).await?) + } else { + cert_bytes.clone() + }; + + let certs = + rustls_pemfile::certs(&mut Cursor::new(&cert_bytes)).collect::, _>>()?; + let key = rustls_pemfile::private_key(&mut Cursor::new(&key_bytes))? + .ok_or(Error::BadPrivateKey)?; + + builder.with_client_auth_cert(certs, key)? + } else { + builder.with_no_client_auth() + }; + + let server_name = pki_types::ServerName::try_from(server.to_string())?; + + Ok(TlsConnector::from(Arc::new(client_config)) + .connect(server_name, tcp) + .await?) +} + +#[derive(Debug)] +pub struct AcceptInvalidCerts; + +impl ServerCertVerifier for AcceptInvalidCerts { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::RSA_PKCS1_SHA1, + rustls::SignatureScheme::ECDSA_SHA1_Legacy, + rustls::SignatureScheme::RSA_PKCS1_SHA256, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::RSA_PKCS1_SHA384, + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::RSA_PKCS1_SHA512, + rustls::SignatureScheme::ECDSA_NISTP521_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA256, + rustls::SignatureScheme::RSA_PSS_SHA384, + rustls::SignatureScheme::RSA_PSS_SHA512, + rustls::SignatureScheme::ED25519, + rustls::SignatureScheme::ED448, + ] + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("rustls error: {0}")] + Tls(#[from] rustls::Error), + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("invalid DNS name: {0}")] + Dns(#[from] pki_types::InvalidDnsNameError), + #[error("missing or invalid private key")] + BadPrivateKey, +}