diff --git a/Cargo.lock b/Cargo.lock index 63ab9d3ca9797..05bfd1470d23a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,13 +1110,18 @@ dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", - "h2 0.3.26", "h2 0.4.12", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", + "http 1.4.2", + "hyper 1.9.0", + "hyper-rustls", + "hyper-util", "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pki-types", "tokio", + "tokio-rustls", + "tower 0.5.3", "tracing", ] @@ -1158,7 +1163,6 @@ checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" dependencies = [ "aws-smithy-async", "aws-smithy-http", - "aws-smithy-http-client", "aws-smithy-observability", "aws-smithy-runtime-api", "aws-smithy-schema", @@ -6079,7 +6083,7 @@ dependencies = [ "anyhow", "aws-config", "aws-sdk-s3", - "aws-smithy-runtime", + "aws-smithy-http-client", "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", @@ -6087,13 +6091,10 @@ dependencies = [ "bytesize", "futures", "http 1.4.2", - "hyper 0.14.32", - "hyper-tls 0.5.0", "mz-ore", "pin-project", "thiserror 2.0.18", "tokio", - "tower-service", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index 04ccfa3b98d27..289abae62a744 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -303,6 +303,10 @@ aws-sdk-s3 = { version = "1.129.0", default-features = false, features = ["rt-to aws-sdk-secretsmanager = { version = "1.103.0", default-features = false, features = ["rt-tokio"] } aws-sdk-sts = { version = "1.41.0", default-features = false, features = ["rt-tokio"] } aws-sigv4 = "1.3.6" +# We deliberately select the non-FIPS `aws_lc_rs` provider (via `rustls-aws-lc`) +# to match the rest of the crypto stack. A follow-up flips to `rustls-aws-lc-fips` +# once the NIST certificate lands. +aws-smithy-http-client = { version = "1.1.12", default-features = false, features = ["rustls-aws-lc"] } aws-smithy-runtime = { version = "1.9.8", features = ["connector-hyper-0-14-x"] } aws-smithy-runtime-api = "1.10.0" aws-smithy-types = { version = "1.1.8", features = ["byte-stream-poll-next"] } diff --git a/src/aws-util/Cargo.toml b/src/aws-util/Cargo.toml index 42f331e635ef7..df51200beae9a 100644 --- a/src/aws-util/Cargo.toml +++ b/src/aws-util/Cargo.toml @@ -13,18 +13,15 @@ workspace = true anyhow.workspace = true aws-config.workspace = true aws-sdk-s3 = { workspace = true, optional = true } +aws-smithy-http-client.workspace = true aws-smithy-runtime-api.workspace = true -aws-smithy-runtime.workspace = true aws-smithy-types.workspace = true aws-types.workspace = true bytes.workspace = true bytesize.workspace = true futures.workspace = true http.workspace = true -hyper-0-14.workspace = true -hyper-tls = "0.5.0" mz-ore = { path = "../ore", features = ["async", "network"], default-features = false } -tower-service.workspace = true pin-project.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/src/aws-util/src/lib.rs b/src/aws-util/src/lib.rs index 3102a91979ea0..69ae7ae0c62e8 100644 --- a/src/aws-util/src/lib.rs +++ b/src/aws-util/src/lib.rs @@ -7,18 +7,10 @@ // the Business Source License, use of this software will be governed // by the Apache License, Version 2.0. -use std::future::Future; -use std::net::SocketAddr; -use std::pin::Pin; -use std::task::{Context, Poll}; - use aws_config::{BehaviorVersion, ConfigLoader}; -use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder; -use aws_smithy_runtime_api::client::http::{HttpClient, SharedHttpClient}; -use hyper_0_14::client::HttpConnector; -use hyper_0_14::client::connect::dns::Name; -use hyper_tls::HttpsConnector; -use tower_service::Service; +use aws_smithy_http_client::{Builder, tls}; +use aws_smithy_runtime_api::client::dns::{DnsFuture, ResolveDns, ResolveDnsError}; +use aws_smithy_runtime_api::client::http::SharedHttpClient; #[cfg(feature = "s3")] pub mod s3; @@ -47,67 +39,62 @@ pub fn defaults() -> ConfigLoader { /// Returns an HTTP client for use with the AWS SDK that is appropriately /// configured for Materialize. -pub fn http_client() -> impl HttpClient { - // The default AWS HTTP client uses rustls, while our company policy is to - // use native TLS. - HyperClientBuilder::new().build(HttpsConnector::new()) +pub fn http_client() -> SharedHttpClient { + // Company policy is to use rustls with the aws-lc-rs crypto provider so the + // AWS SDK path fits the FIPS 140-3 crypto story, rather than the SDK's + // default OS-native TLS stack. We use the non-FIPS `AwsLc` mode to match the + // rest of the stack. A follow-up flips to the FIPS-validated provider once + // the NIST certificate lands. + Builder::new() + .tls_provider(tls::Provider::Rustls( + tls::rustls_provider::CryptoMode::AwsLc, + )) + .build_https() } /// Returns an AWS SDK HTTP client whose DNS resolver delegates to /// [`mz_ore::netio::resolve_address`]. /// -/// Only the IP resolution step is overridden — the SDK still uses the original +/// Only the IP resolution step is overridden. The SDK still uses the original /// hostname for SNI and TLS certificate validation, so HTTPS endpoints work /// unchanged. pub fn http_client_with_resolver(enforce_external_addresses: bool) -> SharedHttpClient { - let resolver = MzAwsResolver { - enforce_external_addresses, - }; - let mut http = HttpConnector::new_with_resolver(resolver); - // The SDK speaks HTTPS to the public AWS API; the wrapper we build below - // handles TLS, but the underlying HTTP connector must allow non-`http://` - // schemes. - http.enforce_http(false); - let https = HttpsConnector::new_with_connector(http); - HyperClientBuilder::new().build(https) + Builder::new() + .tls_provider(tls::Provider::Rustls( + tls::rustls_provider::CryptoMode::AwsLc, + )) + .build_with_resolver(MzAwsResolver { + enforce_external_addresses, + }) } -/// A `tower_service::Service` resolver that delegates to -/// [`mz_ore::netio::resolve_address`], used by [`http_client_with_resolver`]. -#[derive(Clone)] +/// A [`ResolveDns`] resolver that delegates to [`mz_ore::netio::resolve_address`], +/// used by [`http_client_with_resolver`]. +/// +/// Smithy applies the endpoint's port to the resolved IPs itself, so this only +/// yields bare [`IpAddr`](std::net::IpAddr)s. +#[derive(Debug, Clone)] struct MzAwsResolver { enforce_external_addresses: bool, } -impl Service for MzAwsResolver { - type Response = std::vec::IntoIter; - type Error = mz_ore::netio::DnsResolutionError; - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, name: Name) -> Self::Future { +impl ResolveDns for MzAwsResolver { + fn resolve_dns<'a>(&'a self, name: &'a str) -> DnsFuture<'a> { let enforce = self.enforce_external_addresses; - let host = name.as_str().to_string(); - Box::pin(async move { - let ips = mz_ore::netio::resolve_address(&host, enforce).await?; - // Hyper substitutes the URL's port (or the default for the scheme) - // when the SocketAddr's port is 0. - Ok(ips - .into_iter() - .map(|ip| SocketAddr::new(ip, 0)) - .collect::>() - .into_iter()) + let host = name.to_string(); + DnsFuture::new(async move { + let ips = mz_ore::netio::resolve_address(&host, enforce) + .await + .map_err(ResolveDnsError::new)?; + Ok(ips.into_iter().collect()) }) } } #[cfg(test)] mod tests { + use std::error::Error; use std::net::IpAddr; - use std::str::FromStr; use mz_ore::netio::DnsResolutionError; @@ -116,13 +103,20 @@ mod tests { #[mz_ore::test(tokio::test)] #[cfg_attr(miri, ignore)] async fn resolver_rejects_loopback_when_enforced() { - let mut resolver = MzAwsResolver { + let resolver = MzAwsResolver { enforce_external_addresses: true, }; - let name = Name::from_str("127.0.0.1").unwrap(); - let err = resolver.call(name).await.expect_err("must reject loopback"); + let err = resolver + .resolve_dns("127.0.0.1") + .await + .expect_err("must reject loopback"); + // Smithy wraps our `DnsResolutionError` in a `ResolveDnsError`, so peel + // back the source to confirm the rejection reason is the private address. + let source = err + .source() + .and_then(|src| src.downcast_ref::()); assert!( - matches!(err, DnsResolutionError::PrivateAddress), + matches!(source, Some(DnsResolutionError::PrivateAddress)), "got {err:?}" ); } @@ -130,30 +124,26 @@ mod tests { #[mz_ore::test(tokio::test)] #[cfg_attr(miri, ignore)] async fn resolver_allows_loopback_when_not_enforced() { - let mut resolver = MzAwsResolver { + let resolver = MzAwsResolver { enforce_external_addresses: false, }; - let name = Name::from_str("127.0.0.1").unwrap(); - let addrs: Vec = resolver - .call(name) + let addrs: Vec = resolver + .resolve_dns("127.0.0.1") .await - .expect("loopback should resolve when enforcement is off") - .collect(); - assert!(addrs.iter().any(|a| a.ip() == IpAddr::from([127, 0, 0, 1]))); + .expect("loopback should resolve when enforcement is off"); + assert!(addrs.iter().any(|ip| *ip == IpAddr::from([127, 0, 0, 1]))); } #[mz_ore::test(tokio::test)] #[cfg_attr(miri, ignore)] async fn resolver_allows_public_when_enforced() { - let mut resolver = MzAwsResolver { + let resolver = MzAwsResolver { enforce_external_addresses: true, }; - let name = Name::from_str("8.8.8.8").unwrap(); - let addrs: Vec = resolver - .call(name) + let addrs: Vec = resolver + .resolve_dns("8.8.8.8") .await - .expect("public IP should resolve") - .collect(); - assert!(addrs.iter().any(|a| a.ip() == IpAddr::from([8, 8, 8, 8]))); + .expect("public IP should resolve"); + assert!(addrs.iter().any(|ip| *ip == IpAddr::from([8, 8, 8, 8]))); } }