diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32893ee7..3ac91ec3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,10 @@ jobs: if: matrix.target.os == 'ubuntu-latest' run: ./scripts/free-disk-space.sh + - name: Install nasm + if: matrix.target.os == 'windows-latest' + uses: ilammy/setup-nasm@v1.5.1 + - name: Install OpenSSL if: matrix.target.os == 'windows-latest' shell: bash diff --git a/actix-tls/CHANGES.md b/actix-tls/CHANGES.md index 3a5dafae..cc8c6137 100644 --- a/actix-tls/CHANGES.md +++ b/actix-tls/CHANGES.md @@ -2,11 +2,12 @@ ## Unreleased +- Add `rustls-0_23`, `rustls-0_23-webpki-roots`, and `rustls-0_23-native-roots` crate features. - Minimum supported Rust version (MSRV) is now 1.70. ## 3.3.0 -- Add `rustls-0_22` create feature which excludes any root certificate methods or re-exports. +- Add `rustls-0_22` crate feature which excludes any root certificate methods or re-exports. ## 3.2.0 diff --git a/actix-tls/Cargo.toml b/actix-tls/Cargo.toml index 80f19ed8..f0dfaf81 100755 --- a/actix-tls/Cargo.toml +++ b/actix-tls/Cargo.toml @@ -61,6 +61,11 @@ rustls-0_22 = ["dep:tokio-rustls-025", "dep:rustls-pki-types-1"] rustls-0_22-webpki-roots = ["rustls-0_22", "dep:webpki-roots-026"] rustls-0_22-native-roots = ["rustls-0_22", "dep:rustls-native-certs-07"] +# use rustls v0.23 impls +rustls-0_23 = ["dep:tokio-rustls-026", "dep:rustls-pki-types-1"] +rustls-0_23-webpki-roots = ["rustls-0_23", "dep:webpki-roots-026"] +rustls-0_23-native-roots = ["rustls-0_23", "dep:rustls-native-certs-07"] + # use native-tls impls native-tls = ["dep:tokio-native-tls"] @@ -98,9 +103,12 @@ tokio-rustls-024 = { package = "tokio-rustls", version = "0.24", optional = true webpki-roots-025 = { package = "webpki-roots", version = "0.25", optional = true } # rustls v0.22 -rustls-pki-types-1 = { package = "rustls-pki-types", version = "1", optional = true } +rustls-pki-types-1 = { package = "rustls-pki-types", version = "1", optional = true } # Also used for rustls v0.23 tokio-rustls-025 = { package = "tokio-rustls", version = "0.25", optional = true } -webpki-roots-026 = { package = "webpki-roots", version = "0.26", optional = true } +webpki-roots-026 = { package = "webpki-roots", version = "0.26", optional = true } # Also used for rustls v0.23 + +# rustls v0.23 +tokio-rustls-026 = { package = "tokio-rustls", version = "0.26", optional = true } # native root certificates for rustls impls rustls-native-certs-06 = { package = "rustls-native-certs", version = "0.6", optional = true } @@ -119,9 +127,9 @@ futures-util = { version = "0.3.17", default-features = false, features = ["sink itertools = "0.12" rcgen = "0.12" rustls-pemfile = "2" -tokio-rustls-025 = { package = "tokio-rustls", version = "0.25" } +tokio-rustls-026 = { package = "tokio-rustls", version = "0.26", features = ["ring"] } trust-dns-resolver = "0.23" [[example]] name = "accept-rustls" -required-features = ["accept", "rustls-0_22"] +required-features = ["accept", "rustls-0_23"] diff --git a/actix-tls/examples/accept-rustls.rs b/actix-tls/examples/accept-rustls.rs index 200ab149..b94a01e7 100644 --- a/actix-tls/examples/accept-rustls.rs +++ b/actix-tls/examples/accept-rustls.rs @@ -30,13 +30,13 @@ use std::{ use actix_rt::net::TcpStream; use actix_server::Server; use actix_service::ServiceFactoryExt as _; -use actix_tls::accept::rustls_0_22::{Acceptor as RustlsAcceptor, TlsStream}; +use actix_tls::accept::rustls_0_23::{Acceptor as RustlsAcceptor, TlsStream}; use futures_util::future::ok; use itertools::Itertools as _; use rustls::server::ServerConfig; use rustls_pemfile::{certs, rsa_private_keys}; use rustls_pki_types_1::PrivateKeyDer; -use tokio_rustls_025::rustls; +use tokio_rustls_026::rustls; use tracing::info; #[actix_rt::main] diff --git a/actix-tls/src/accept/mod.rs b/actix-tls/src/accept/mod.rs index 2302723e..c9d6cd73 100644 --- a/actix-tls/src/accept/mod.rs +++ b/actix-tls/src/accept/mod.rs @@ -25,6 +25,9 @@ pub mod rustls_0_21; #[cfg(feature = "rustls-0_22")] pub mod rustls_0_22; +#[cfg(feature = "rustls-0_23")] +pub mod rustls_0_23; + #[cfg(feature = "native-tls")] pub mod native_tls; @@ -35,6 +38,7 @@ pub(crate) static MAX_CONN: AtomicUsize = AtomicUsize::new(256); feature = "rustls-0_20", feature = "rustls-0_21", feature = "rustls-0_22", + feature = "rustls-0_23", feature = "native-tls", ))] pub(crate) const DEFAULT_TLS_HANDSHAKE_TIMEOUT: std::time::Duration = diff --git a/actix-tls/src/accept/rustls_0_23.rs b/actix-tls/src/accept/rustls_0_23.rs new file mode 100644 index 00000000..9d2025ba --- /dev/null +++ b/actix-tls/src/accept/rustls_0_23.rs @@ -0,0 +1,198 @@ +//! `rustls` v0.23 based TLS connection acceptor service. +//! +//! See [`Acceptor`] for main service factory docs. + +use std::{ + convert::Infallible, + future::Future, + io::{self, IoSlice}, + pin::Pin, + sync::Arc, + task::{Context, Poll}, + time::Duration, +}; + +use actix_rt::{ + net::{ActixStream, Ready}, + time::{sleep, Sleep}, +}; +use actix_service::{Service, ServiceFactory}; +use actix_utils::{ + counter::{Counter, CounterGuard}, + future::{ready, Ready as FutReady}, +}; +use pin_project_lite::pin_project; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio_rustls::{Accept, TlsAcceptor}; +use tokio_rustls_026 as tokio_rustls; + +use super::{TlsError, DEFAULT_TLS_HANDSHAKE_TIMEOUT, MAX_CONN_COUNTER}; + +pub mod reexports { + //! Re-exports from `rustls` that are useful for acceptors. + + pub use tokio_rustls_026::rustls::ServerConfig; +} + +/// Wraps a `rustls` based async TLS stream in order to implement [`ActixStream`]. +pub struct TlsStream(tokio_rustls::server::TlsStream); + +impl_more::impl_from!( in tokio_rustls::server::TlsStream => TlsStream); +impl_more::impl_deref_and_mut!( in TlsStream => tokio_rustls::server::TlsStream); + +impl AsyncRead for TlsStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut **self.get_mut()).poll_read(cx, buf) + } +} + +impl AsyncWrite for TlsStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut **self.get_mut()).poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut **self.get_mut()).poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut **self.get_mut()).poll_shutdown(cx) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll> { + Pin::new(&mut **self.get_mut()).poll_write_vectored(cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + (**self).is_write_vectored() + } +} + +impl ActixStream for TlsStream { + fn poll_read_ready(&self, cx: &mut Context<'_>) -> Poll> { + IO::poll_read_ready((**self).get_ref().0, cx) + } + + fn poll_write_ready(&self, cx: &mut Context<'_>) -> Poll> { + IO::poll_write_ready((**self).get_ref().0, cx) + } +} + +/// Accept TLS connections via the `rustls` crate. +pub struct Acceptor { + config: Arc, + handshake_timeout: Duration, +} + +impl Acceptor { + /// Constructs `rustls` based acceptor service factory. + pub fn new(config: reexports::ServerConfig) -> Self { + Acceptor { + config: Arc::new(config), + handshake_timeout: DEFAULT_TLS_HANDSHAKE_TIMEOUT, + } + } + + /// Limit the amount of time that the acceptor will wait for a TLS handshake to complete. + /// + /// Default timeout is 3 seconds. + pub fn set_handshake_timeout(&mut self, handshake_timeout: Duration) -> &mut Self { + self.handshake_timeout = handshake_timeout; + self + } +} + +impl Clone for Acceptor { + fn clone(&self) -> Self { + Self { + config: self.config.clone(), + handshake_timeout: self.handshake_timeout, + } + } +} + +impl ServiceFactory for Acceptor { + type Response = TlsStream; + type Error = TlsError; + type Config = (); + type Service = AcceptorService; + type InitError = (); + type Future = FutReady>; + + fn new_service(&self, _: ()) -> Self::Future { + let res = MAX_CONN_COUNTER.with(|conns| { + Ok(AcceptorService { + acceptor: self.config.clone().into(), + conns: conns.clone(), + handshake_timeout: self.handshake_timeout, + }) + }); + + ready(res) + } +} + +/// Rustls based acceptor service. +pub struct AcceptorService { + acceptor: TlsAcceptor, + conns: Counter, + handshake_timeout: Duration, +} + +impl Service for AcceptorService { + type Response = TlsStream; + type Error = TlsError; + type Future = AcceptFut; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + if self.conns.available(cx) { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } + } + + fn call(&self, req: IO) -> Self::Future { + AcceptFut { + fut: self.acceptor.accept(req), + timeout: sleep(self.handshake_timeout), + _guard: self.conns.get(), + } + } +} + +pin_project! { + /// Accept future for Rustls service. + #[doc(hidden)] + pub struct AcceptFut { + fut: Accept, + #[pin] + timeout: Sleep, + _guard: CounterGuard, + } +} + +impl Future for AcceptFut { + type Output = Result, TlsError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + match Pin::new(&mut this.fut).poll(cx) { + Poll::Ready(Ok(stream)) => Poll::Ready(Ok(TlsStream(stream))), + Poll::Ready(Err(err)) => Poll::Ready(Err(TlsError::Tls(err))), + Poll::Pending => this.timeout.poll(cx).map(|_| Err(TlsError::Timeout)), + } + } +} diff --git a/actix-tls/src/connect/mod.rs b/actix-tls/src/connect/mod.rs index 1e77d98a..04337daa 100644 --- a/actix-tls/src/connect/mod.rs +++ b/actix-tls/src/connect/mod.rs @@ -49,6 +49,9 @@ pub mod rustls_0_21; #[cfg(feature = "rustls-0_22")] pub mod rustls_0_22; +#[cfg(feature = "rustls-0_23")] +pub mod rustls_0_23; + #[cfg(feature = "native-tls")] pub mod native_tls; diff --git a/actix-tls/src/connect/rustls_0_23.rs b/actix-tls/src/connect/rustls_0_23.rs new file mode 100644 index 00000000..4ea9aba3 --- /dev/null +++ b/actix-tls/src/connect/rustls_0_23.rs @@ -0,0 +1,163 @@ +//! Rustls based connector service. +//! +//! See [`TlsConnector`] for main connector service factory docs. + +use std::{ + future::Future, + io, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use actix_rt::net::ActixStream; +use actix_service::{Service, ServiceFactory}; +use actix_utils::future::{ok, Ready}; +use futures_core::ready; +use rustls_pki_types_1::ServerName; +use tokio_rustls::{ + client::TlsStream as AsyncTlsStream, rustls::ClientConfig, Connect as RustlsConnect, + TlsConnector as RustlsTlsConnector, +}; +use tokio_rustls_026 as tokio_rustls; + +use crate::connect::{Connection, Host}; + +pub mod reexports { + //! Re-exports from the `rustls` v0.23 ecosystem that are useful for connectors. + + pub use tokio_rustls_026::{client::TlsStream as AsyncTlsStream, rustls::ClientConfig}; + #[cfg(feature = "rustls-0_23-webpki-roots")] + pub use webpki_roots_026::TLS_SERVER_ROOTS; +} + +/// Returns root certificates via `rustls-native-certs` crate as a rustls certificate store. +/// +/// See [`rustls_native_certs::load_native_certs()`] for more info on behavior and errors. +/// +/// [`rustls_native_certs::load_native_certs()`]: rustls_native_certs_07::load_native_certs() +#[cfg(feature = "rustls-0_23-native-roots")] +pub fn native_roots_cert_store() -> io::Result { + let mut root_certs = tokio_rustls::rustls::RootCertStore::empty(); + + for cert in rustls_native_certs_07::load_native_certs()? { + root_certs.add(cert).unwrap(); + } + + Ok(root_certs) +} + +/// Returns standard root certificates from `webpki-roots` crate as a rustls certificate store. +#[cfg(feature = "rustls-0_23-webpki-roots")] +pub fn webpki_roots_cert_store() -> tokio_rustls::rustls::RootCertStore { + let mut root_certs = tokio_rustls::rustls::RootCertStore::empty(); + root_certs.extend(webpki_roots_026::TLS_SERVER_ROOTS.to_owned()); + root_certs +} + +/// Connector service factory using `rustls`. +#[derive(Clone)] +pub struct TlsConnector { + connector: Arc, +} + +impl TlsConnector { + /// Constructs new connector service factory from a `rustls` client configuration. + pub fn new(connector: Arc) -> Self { + TlsConnector { connector } + } + + /// Constructs new connector service from a `rustls` client configuration. + pub fn service(connector: Arc) -> TlsConnectorService { + TlsConnectorService { connector } + } +} + +impl ServiceFactory> for TlsConnector +where + R: Host, + IO: ActixStream + 'static, +{ + type Response = Connection>; + type Error = io::Error; + type Config = (); + type Service = TlsConnectorService; + type InitError = (); + type Future = Ready>; + + fn new_service(&self, _: ()) -> Self::Future { + ok(TlsConnectorService { + connector: self.connector.clone(), + }) + } +} + +/// Connector service using `rustls`. +#[derive(Clone)] +pub struct TlsConnectorService { + connector: Arc, +} + +impl Service> for TlsConnectorService +where + R: Host, + IO: ActixStream, +{ + type Response = Connection>; + type Error = io::Error; + type Future = ConnectFut; + + actix_service::always_ready!(); + + fn call(&self, connection: Connection) -> Self::Future { + tracing::trace!("TLS handshake start for: {:?}", connection.hostname()); + let (stream, conn) = connection.replace_io(()); + + match ServerName::try_from(conn.hostname()) { + Ok(host) => ConnectFut::Future { + connect: RustlsTlsConnector::from(Arc::clone(&self.connector)) + .connect(host.to_owned(), stream), + connection: Some(conn), + }, + Err(_) => ConnectFut::InvalidServerName, + } + } +} + +/// Connect future for Rustls service. +#[doc(hidden)] +#[allow(clippy::large_enum_variant)] +pub enum ConnectFut { + InvalidServerName, + Future { + connect: RustlsConnect, + connection: Option>, + }, +} + +impl Future for ConnectFut +where + R: Host, + IO: ActixStream, +{ + type Output = io::Result>>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.get_mut() { + Self::InvalidServerName => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "connection parameters specified invalid server name", + ))), + + Self::Future { + connect, + connection, + } => { + let stream = ready!(Pin::new(connect).poll(cx))?; + let connection = connection.take().unwrap(); + tracing::trace!("TLS handshake success: {:?}", connection.hostname()); + Poll::Ready(Ok(connection.replace_io(stream).1)) + } + } + } +} diff --git a/actix-tls/tests/accept-openssl.rs b/actix-tls/tests/accept-openssl.rs index e571a282..ecafbf76 100644 --- a/actix-tls/tests/accept-openssl.rs +++ b/actix-tls/tests/accept-openssl.rs @@ -3,7 +3,7 @@ #![cfg(all( feature = "accept", feature = "connect", - feature = "rustls-0_22", + feature = "rustls-0_23", feature = "openssl" ))] @@ -14,11 +14,11 @@ use actix_server::TestServer; use actix_service::ServiceFactoryExt as _; use actix_tls::{ accept::openssl::{Acceptor, TlsStream}, - connect::rustls_0_22::reexports::ClientConfig, + connect::rustls_0_23::reexports::ClientConfig, }; use actix_utils::future::ok; use rustls_pki_types_1::ServerName; -use tokio_rustls_025::rustls::RootCertStore; +use tokio_rustls_026::rustls::RootCertStore; fn new_cert_and_key() -> (String, String) { let cert = @@ -51,7 +51,7 @@ fn openssl_acceptor(cert: String, key: String) -> tls_openssl::ssl::SslAcceptor mod danger { use rustls_pki_types_1::{CertificateDer, ServerName, UnixTime}; - use tokio_rustls_025::rustls; + use tokio_rustls_026::rustls; /// Disables certificate verification to allow self-signed certs from rcgen. #[derive(Debug)] @@ -63,7 +63,7 @@ mod danger { _end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>, - _ocsp_response: &[u8], + _ocsp: &[u8], _now: UnixTime, ) -> Result { Ok(rustls::client::danger::ServerCertVerified::assertion()) @@ -111,6 +111,10 @@ fn rustls_connector(_cert: String, _key: String) -> ClientConfig { #[actix_rt::test] async fn accepts_connections() { + tokio_rustls_026::rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .unwrap(); + let (cert, key) = new_cert_and_key(); let srv = TestServer::start({ @@ -137,13 +141,13 @@ async fn accepts_connections() { let config = rustls_connector(cert, key); let config = Arc::new(config); - let mut conn = tokio_rustls_025::rustls::ClientConnection::new( + let mut conn = tokio_rustls_026::rustls::ClientConnection::new( config, ServerName::try_from("localhost").unwrap(), ) .unwrap(); - let mut stream = tokio_rustls_025::rustls::Stream::new(&mut conn, &mut sock); + let mut stream = tokio_rustls_026::rustls::Stream::new(&mut conn, &mut sock); stream.flush().expect("TLS handshake failed"); } diff --git a/actix-tls/tests/accept-rustls.rs b/actix-tls/tests/accept-rustls.rs index bed2f6c3..bc29971c 100644 --- a/actix-tls/tests/accept-rustls.rs +++ b/actix-tls/tests/accept-rustls.rs @@ -3,7 +3,7 @@ #![cfg(all( feature = "accept", feature = "connect", - feature = "rustls-0_22", + feature = "rustls-0_23", feature = "openssl" ))] @@ -15,7 +15,7 @@ use actix_rt::net::TcpStream; use actix_server::TestServer; use actix_service::ServiceFactoryExt as _; use actix_tls::{ - accept::rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream}, + accept::rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream}, connect::openssl::reexports::SslConnector, }; use actix_utils::future::ok; @@ -73,6 +73,10 @@ fn openssl_connector(cert: String, key: String) -> SslConnector { #[actix_rt::test] async fn accepts_connections() { + tokio_rustls_026::rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .unwrap(); + let (cert, key) = new_cert_and_key(); let srv = TestServer::start({ diff --git a/actix-tls/tests/test_connect.rs b/actix-tls/tests/test_connect.rs index 820df7e3..c0a7fa76 100644 --- a/actix-tls/tests/test_connect.rs +++ b/actix-tls/tests/test_connect.rs @@ -30,7 +30,7 @@ async fn test_string() { assert_eq!(con.peer_addr().unwrap(), srv.addr()); } -#[cfg(feature = "rustls-0_22")] +#[cfg(feature = "rustls-0_23")] #[actix_rt::test] async fn test_rustls_string() { let srv = TestServer::start(|| { @@ -112,7 +112,7 @@ async fn test_openssl_uri() { assert_eq!(con.peer_addr().unwrap(), srv.addr()); } -#[cfg(all(feature = "rustls-0_22", feature = "uri"))] +#[cfg(all(feature = "rustls-0_23", feature = "uri"))] #[actix_rt::test] async fn test_rustls_uri_http1() { let srv = TestServer::start(|| { @@ -129,7 +129,7 @@ async fn test_rustls_uri_http1() { assert_eq!(con.peer_addr().unwrap(), srv.addr()); } -#[cfg(all(feature = "rustls-0_22", feature = "uri"))] +#[cfg(all(feature = "rustls-0_23", feature = "uri"))] #[actix_rt::test] async fn test_rustls_uri() { let srv = TestServer::start(|| {