diff --git a/.github/workflows/ci_rust.yml b/.github/workflows/ci_rust.yml index 97637855b09..2ced1e01d7e 100644 --- a/.github/workflows/ci_rust.yml +++ b/.github/workflows/ci_rust.yml @@ -58,6 +58,10 @@ jobs: working-directory: ${{env.ROOT_PATH}} run: cargo test --features unstable-renegotiate + - name: Network-enabled integration tests + working-directory: ${{env.ROOT_PATH}}/integration + run: RUST_LOG=TRACE cargo test --features network-tests + - name: Test external build # if this test is failing, make sure that api headers are appropriately # included. For a symbol to be visible in a shared lib, the diff --git a/bindings/rust/integration/Cargo.toml b/bindings/rust/integration/Cargo.toml index 684cc4714ac..8c1f75470a0 100644 --- a/bindings/rust/integration/Cargo.toml +++ b/bindings/rust/integration/Cargo.toml @@ -5,7 +5,34 @@ authors = ["AWS s2n"] edition = "2021" publish = false +[features] +default = ["pq"] + +# Network tests are useful but relatively slow and inherently flaky. So they are +# behind this feature flag. +network-tests = [] + +# Not all libcryptos support PQ capabilities. Tests relying on PQ functionality +# can be disabled by turning off this feature. +pq = [] + [dependencies] -s2n-tls = { path = "../s2n-tls"} +s2n-tls = { path = "../s2n-tls", features = ["unstable-testing"]} +s2n-tls-hyper = { path = "../s2n-tls-hyper" } +s2n-tls-tokio = { path = "../s2n-tls-tokio" } s2n-tls-sys = { path = "../s2n-tls-sys" } +[dev-dependencies] +tokio = { version = "1", features = ["macros", "test-util"] } + +tracing = "0.1" +tracing-subscriber = "0.3" +# TODO: Unpin when s2n-tls MSRV >= 1.71, https://github.com/aws/s2n-tls/issues/4893 +test-log = { version = "=0.2.14", default-features = false, features = ["trace"]} +test-log-macros = "=0.2.14" + +http = "1.1" +http-body-util = "0.1" +bytes = "1.8" +hyper = "1.5" +hyper-util = "0.1" diff --git a/bindings/rust/integration/src/lib.rs b/bindings/rust/integration/src/lib.rs new file mode 100644 index 00000000000..612f362e2dd --- /dev/null +++ b/bindings/rust/integration/src/lib.rs @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(all(feature = "network-tests", test))] +mod network; + +#[cfg(test)] +mod tests { + use s2n_tls::{ + security::Policy, + testing::{self, TestPair}, + }; + + /// This test provides a helpful debug message if the PQ feature is incorrectly + /// configured. + #[cfg(feature = "pq")] + #[test] + fn pq_sanity_check() -> Result<(), Box> { + let config = testing::build_config(&Policy::from_version("KMS-PQ-TLS-1-0-2020-07")?)?; + let mut pair = TestPair::from_config(&config); + pair.handshake()?; + + if pair.client.kem_name().is_none() { + panic!( + "PQ tests are enabled, but PQ functionality is unavailable. \ + Are you sure that the libcrypto supports PQ?" + ); + } + Ok(()) + } +} diff --git a/bindings/rust/integration/src/network/https_client.rs b/bindings/rust/integration/src/network/https_client.rs new file mode 100644 index 00000000000..84c90e5a10a --- /dev/null +++ b/bindings/rust/integration/src/network/https_client.rs @@ -0,0 +1,97 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use bytes::Bytes; +use http::{Response, StatusCode, Uri}; +use http_body_util::{BodyExt, Empty}; +use hyper::body::Incoming; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; +use s2n_tls::{ + config::Config, + security::{self, Policy}, +}; +use s2n_tls_hyper::connector::HttpsConnector; +use std::str::FromStr; + +#[derive(Debug)] +struct TestCase { + pub query_target: &'static str, + pub expected_status_code: u16, +} + +impl TestCase { + const fn new(domain: &'static str, expected_status_code: u16) -> Self { + TestCase { + query_target: domain, + expected_status_code, + } + } +} + +const TEST_CASES: &[TestCase] = &[ + // this is a link to the s2n-tls unit test coverage report, hosted on cloudfront + TestCase::new("https://dx1inn44oyl7n.cloudfront.net/main/index.html", 200), + // this is a link to a non-existent S3 item + TestCase::new("https://notmybucket.s3.amazonaws.com/folder/afile.jpg", 403), + TestCase::new("https://www.amazon.com", 200), + TestCase::new("https://www.apple.com", 200), + TestCase::new("https://www.att.com", 200), + TestCase::new("https://www.cloudflare.com", 200), + TestCase::new("https://www.ebay.com", 200), + TestCase::new("https://www.google.com", 200), + TestCase::new("https://www.mozilla.org", 200), + TestCase::new("https://www.netflix.com", 200), + TestCase::new("https://www.openssl.org", 200), + TestCase::new("https://www.t-mobile.com", 200), + TestCase::new("https://www.verizon.com", 200), + TestCase::new("https://www.wikipedia.org", 200), + TestCase::new("https://www.yahoo.com", 200), + TestCase::new("https://www.youtube.com", 200), + TestCase::new("https://www.github.com", 301), + TestCase::new("https://www.samsung.com", 301), + TestCase::new("https://www.twitter.com", 301), + TestCase::new("https://www.facebook.com", 302), + TestCase::new("https://www.microsoft.com", 302), + TestCase::new("https://www.ibm.com", 303), + TestCase::new("https://www.f5.com", 403), +]; + +/// perform an HTTP GET request against `uri` using an s2n-tls config with +/// `security_policy`. +async fn https_get( + uri: &str, + security_policy: &Policy, +) -> Result, hyper_util::client::legacy::Error> { + let mut config = Config::builder(); + config.set_security_policy(security_policy).unwrap(); + + let connector = HttpsConnector::new(config.build().unwrap()); + let client: Client<_, Empty> = Client::builder(TokioExecutor::new()).build(connector); + + let uri = Uri::from_str(uri).unwrap(); + client.get(uri).await +} + +/// Ensure that s2n-tls is compatible with other http/TLS implementations. +/// +/// This test uses s2n-tls-hyper to make http requests over a TLS connection to +/// a number of well known http sites. +#[test_log::test(tokio::test)] +async fn http_get_test() -> Result<(), Box> { + for test_case in TEST_CASES { + for policy in [security::DEFAULT, security::DEFAULT_TLS13] { + tracing::info!("executing test case {:#?} with {:?}", test_case, policy); + + let response = https_get(test_case.query_target, &policy).await?; + let expected_status = StatusCode::from_u16(test_case.expected_status_code).unwrap(); + assert_eq!(response.status(), expected_status); + + if expected_status == StatusCode::OK { + let body = response.into_body().collect().await?.to_bytes(); + assert!(!body.is_empty()); + } + } + } + + Ok(()) +} diff --git a/bindings/rust/integration/src/network/mod.rs b/bindings/rust/integration/src/network/mod.rs new file mode 100644 index 00000000000..07d0cabf7de --- /dev/null +++ b/bindings/rust/integration/src/network/mod.rs @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +mod https_client; +mod tls_client; diff --git a/bindings/rust/integration/src/network/tls_client.rs b/bindings/rust/integration/src/network/tls_client.rs new file mode 100644 index 00000000000..c94325a7692 --- /dev/null +++ b/bindings/rust/integration/src/network/tls_client.rs @@ -0,0 +1,95 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use s2n_tls::{config::Config, enums::Version, security::Policy}; +use s2n_tls_tokio::{TlsConnector, TlsStream}; +use tokio::net::TcpStream; + +/// Perform a TLS handshake with port 443 of `domain`. +/// +/// * `domain`: The domain to perform the handshake with +/// * `security_policy`: The security policy to set on the handshaking client. +/// +/// Returns an open `TlsStream` if the handshake was successful, otherwise an +/// `Err``. +async fn handshake_with_domain( + domain: &str, + security_policy: &str, +) -> Result, Box> { + tracing::info!("querying {domain} with {security_policy}"); + const PORT: u16 = 443; + + let mut config = Config::builder(); + config.set_security_policy(&Policy::from_version(security_policy)?)?; + + let client = TlsConnector::new(config.build()?); + // open the TCP stream + let stream = TcpStream::connect((domain, PORT)).await?; + // complete the TLS handshake + Ok(client.connect(domain, stream).await?) +} + +#[cfg(feature = "pq")] +mod kms_pq { + use super::*; + + const DOMAIN: &str = "kms.us-east-1.amazonaws.com"; + + // confirm that we successfully negotiate a supported PQ key exchange. + // + // Note: In the future KMS will deprecate kyber_r3 in favor of ML-KEM. + // At that point this test should be updated with a security policy that + // supports ML-KEM. + #[test_log::test(tokio::test)] + async fn pq_handshake() -> Result<(), Box> { + let tls = handshake_with_domain(DOMAIN, "KMS-PQ-TLS-1-0-2020-07").await?; + + assert_eq!( + tls.as_ref().cipher_suite()?, + "ECDHE-KYBER-RSA-AES256-GCM-SHA384" + ); + assert_eq!(tls.as_ref().kem_name(), Some("kyber512r3")); + + Ok(()) + } + + // We want to confirm that non-supported kyber drafts successfully fall + // back to a full handshake. + #[test_log::test(tokio::test)] + async fn early_draft_falls_back_to_classical() -> Result<(), Box> { + const EARLY_DRAFT_PQ_POLICIES: &[&str] = &[ + "KMS-PQ-TLS-1-0-2019-06", + "PQ-SIKE-TEST-TLS-1-0-2019-11", + "KMS-PQ-TLS-1-0-2020-02", + "PQ-SIKE-TEST-TLS-1-0-2020-02", + ]; + + for security_policy in EARLY_DRAFT_PQ_POLICIES { + let tls = handshake_with_domain(DOMAIN, security_policy).await?; + + assert_eq!(tls.as_ref().cipher_suite()?, "ECDHE-RSA-AES256-GCM-SHA384"); + assert_eq!(tls.as_ref().kem_name(), None); + } + Ok(()) + } +} + +#[test_log::test(tokio::test)] +async fn tls_client() -> Result<(), Box> { + // The akamai request should be in internet_https_client.rs but Akamai + // http requests hang indefinitely. This behavior is also observed with + // curl and chrome. https://github.com/aws/s2n-tls/issues/4883 + const DOMAINS: &[&str] = &["www.akamai.com"]; + + for domain in DOMAINS { + tracing::info!("querying {domain}"); + + let tls12 = handshake_with_domain(domain, "default").await?; + assert_eq!(tls12.as_ref().actual_protocol_version()?, Version::TLS12); + + let tls13 = handshake_with_domain(domain, "default_tls13").await?; + assert_eq!(tls13.as_ref().actual_protocol_version()?, Version::TLS13); + } + + Ok(()) +} diff --git a/bindings/rust/s2n-tls-hyper/tests/web_client.rs b/bindings/rust/s2n-tls-hyper/tests/web_client.rs deleted file mode 100644 index 41a72970ed3..00000000000 --- a/bindings/rust/s2n-tls-hyper/tests/web_client.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use bytes::Bytes; -use http::{status, Uri}; -use http_body_util::{BodyExt, Empty}; -use hyper_util::{client::legacy::Client, rt::TokioExecutor}; -use s2n_tls::config::Config; -use s2n_tls_hyper::connector::HttpsConnector; -use std::{error::Error, str::FromStr}; - -#[tokio::test] -async fn test_get_request() -> Result<(), Box> { - let connector = HttpsConnector::new(Config::default()); - let client: Client<_, Empty> = Client::builder(TokioExecutor::new()).build(connector); - - let uri = Uri::from_str("https://www.amazon.com")?; - let response = client.get(uri).await?; - assert_eq!(response.status(), status::StatusCode::OK); - - let body = response.into_body().collect().await?.to_bytes(); - assert!(!body.is_empty()); - - Ok(()) -} diff --git a/bindings/rust/s2n-tls/src/connection.rs b/bindings/rust/s2n-tls/src/connection.rs index 0ac0a389650..e65225cf111 100644 --- a/bindings/rust/s2n-tls/src/connection.rs +++ b/bindings/rust/s2n-tls/src/connection.rs @@ -972,6 +972,34 @@ impl Connection { } } + pub fn kem_name(&self) -> Option<&str> { + let name_bytes = { + let name = unsafe { s2n_connection_get_kem_name(self.connection.as_ptr()) }; + if name.is_null() { + return None; + } + name + }; + + let name_str = unsafe { + // SAFETY: The data is null terminated because it is declared as a C + // string literal. + // SAFETY: kem_name has a static lifetime because it lives on a const + // struct s2n_kem with file scope. + const_str!(name_bytes) + }; + + match name_str { + Ok("NONE") => None, + Ok(name) => Some(name), + Err(_) => { + // Unreachable: This would indicate a non-utf-8 string literal in + // the s2n-tls C codebase. + None + } + } + } + pub fn selected_curve(&self) -> Result<&str, Error> { let curve = unsafe { s2n_connection_get_curve(self.connection.as_ptr()).into_result()? }; unsafe { diff --git a/bindings/rust/s2n-tls/src/testing/s2n_tls.rs b/bindings/rust/s2n-tls/src/testing/s2n_tls.rs index a641eb993f0..1d5d920c384 100644 --- a/bindings/rust/s2n-tls/src/testing/s2n_tls.rs +++ b/bindings/rust/s2n-tls/src/testing/s2n_tls.rs @@ -12,6 +12,7 @@ mod tests { use alloc::sync::Arc; use core::sync::atomic::Ordering; use futures_test::task::{new_count_waker, noop_waker}; + use security::Policy; use std::{fs, path::Path, pin::Pin, sync::atomic::AtomicUsize}; #[test] @@ -26,6 +27,34 @@ mod tests { assert!(TestPair::handshake_with_config(&config).is_ok()); } + #[test] + fn kem_name_retrieval() -> Result<(), Error> { + // PQ isn't supported + { + let policy = Policy::from_version("20240501")?; + let config = build_config(&policy)?; + let mut pair = TestPair::from_config(&config); + + // before negotiation, kem_name is none + assert!(pair.client.kem_name().is_none()); + + pair.handshake().unwrap(); + assert!(pair.client.kem_name().is_none()); + } + + // PQ is supported + { + let policy = Policy::from_version("KMS-PQ-TLS-1-0-2020-07")?; + let config = build_config(&policy)?; + let mut pair = TestPair::from_config(&config); + + pair.handshake().unwrap(); + assert_eq!(pair.client.kem_name(), Some("kyber512r3")); + } + + Ok(()) + } + #[test] fn default_config_and_clone_interaction() -> Result<(), Error> { let config = build_config(&security::DEFAULT_TLS13)?; diff --git a/tests/integrationv2/test_well_known_endpoints.py b/tests/integrationv2/test_well_known_endpoints.py index 7bce5c2248a..6d5d7e9d5a3 100644 --- a/tests/integrationv2/test_well_known_endpoints.py +++ b/tests/integrationv2/test_well_known_endpoints.py @@ -1,130 +1,13 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import pytest -from constants import TRUST_STORE_BUNDLE, TRUST_STORE_TRUSTED_BUNDLE -from configuration import PROTOCOLS -from common import ProviderOptions, Ciphers, pq_enabled -from fixtures import managed_process # lgtm [py/unused-import] -from global_flags import get_flag, S2N_FIPS_MODE -from providers import Provider, S2N -from test_pq_handshake import PQ_ENABLED_FLAG -from utils import invalid_test_parameters, get_parameter_name, to_bytes +def test_well_known_endpoints(): + ''' + This is a stub test, which allows the existing CI to continue passing while + https://github.com/aws/s2n-tls/pull/4884 is merged in. - -ENDPOINTS = [ - "www.akamai.com", - "www.amazon.com", - "kms.us-east-1.amazonaws.com", - "s3.us-west-2.amazonaws.com", - "www.apple.com", - "www.att.com", - # "www.badssl.com", - # "mozilla-intermediate.badssl.com", - # "mozilla-modern.badssl.com", - # "rsa2048.badssl.com", - # "rsa4096.badssl.com", - # "sha256.badssl.com", - # "sha384.badssl.com", - # "sha512.badssl.com", - # "tls-v1-0.badssl.com", - # "tls-v1-1.badssl.com", - # "tls-v1-2.badssl.com", - "www.cloudflare.com", - "www.ebay.com", - "www.f5.com", - "www.facebook.com", - "www.google.com", - "www.github.com", - "www.ibm.com", - "www.microsoft.com", - # https://github.com/aws/s2n-tls/issues/4879 - # "www.mozilla.org", - "www.netflix.com", - "www.openssl.org", - "www.samsung.com", - "www.t-mobile.com", - "www.twitter.com", - "www.verizon.com", - "www.wikipedia.org", - "www.yahoo.com", - "www.youtube.com", -] - -CIPHERS = [ - None, # `None` will default to the appropriate `test_all` cipher preference in the S2N client provider - Ciphers.KMS_PQ_TLS_1_0_2019_06, - Ciphers.PQ_SIKE_TEST_TLS_1_0_2019_11, - Ciphers.KMS_PQ_TLS_1_0_2020_07, - Ciphers.KMS_PQ_TLS_1_0_2020_02, - Ciphers.PQ_SIKE_TEST_TLS_1_0_2020_02 -] - - -if pq_enabled(): - EXPECTED_RESULTS = { - ("kms.us-east-1.amazonaws.com", Ciphers.KMS_PQ_TLS_1_0_2019_06): - {"cipher": "ECDHE-RSA-AES256-GCM-SHA384", "kem": None}, - ("kms.us-east-1.amazonaws.com", Ciphers.PQ_SIKE_TEST_TLS_1_0_2019_11): - {"cipher": "ECDHE-RSA-AES256-GCM-SHA384", "kem": None}, - ("kms.us-east-1.amazonaws.com", Ciphers.KMS_PQ_TLS_1_0_2020_07): - {"cipher": "ECDHE-KYBER-RSA-AES256-GCM-SHA384", "kem": "kyber512r3"}, - ("kms.us-east-1.amazonaws.com", Ciphers.KMS_PQ_TLS_1_0_2020_02): - {"cipher": "ECDHE-RSA-AES256-GCM-SHA384", "kem": None}, - ("kms.us-east-1.amazonaws.com", Ciphers.PQ_SIKE_TEST_TLS_1_0_2020_02): - {"cipher": "ECDHE-RSA-AES256-GCM-SHA384", "kem": None}, - } -else: - EXPECTED_RESULTS = { - ("kms.us-east-1.amazonaws.com", Ciphers.KMS_PQ_TLS_1_0_2019_06): - {"cipher": "ECDHE-RSA-AES256-GCM-SHA384", "kem": None}, - ("kms.us-east-1.amazonaws.com", Ciphers.PQ_SIKE_TEST_TLS_1_0_2019_11): - {"cipher": "ECDHE-RSA-AES256-GCM-SHA384", "kem": None}, - ("kms.us-east-1.amazonaws.com", Ciphers.KMS_PQ_TLS_1_0_2020_07): - {"cipher": "ECDHE-RSA-AES256-GCM-SHA384", "kem": None}, - ("kms.us-east-1.amazonaws.com", Ciphers.KMS_PQ_TLS_1_0_2020_02): - {"cipher": "ECDHE-RSA-AES256-GCM-SHA384", "kem": None}, - ("kms.us-east-1.amazonaws.com", Ciphers.PQ_SIKE_TEST_TLS_1_0_2020_02): - {"cipher": "ECDHE-RSA-AES256-GCM-SHA384", "kem": None}, - } - - -@pytest.mark.uncollect_if(func=invalid_test_parameters) -@pytest.mark.parametrize("protocol", PROTOCOLS, ids=get_parameter_name) -@pytest.mark.parametrize("endpoint", ENDPOINTS, ids=get_parameter_name) -@pytest.mark.parametrize("provider", [S2N], ids=get_parameter_name) -@pytest.mark.parametrize("cipher", CIPHERS, ids=get_parameter_name) -@pytest.mark.flaky(reruns=5, reruns_delay=4) -def test_well_known_endpoints(managed_process, protocol, endpoint, provider, cipher): - port = "443" - - client_options = ProviderOptions( - mode=Provider.ClientMode, - host=endpoint, - port=port, - insecure=False, - trust_store=TRUST_STORE_BUNDLE, - protocol=protocol, - cipher=cipher) - - if get_flag(S2N_FIPS_MODE) is True: - client_options.trust_store = TRUST_STORE_TRUSTED_BUNDLE - - # expect_stderr=True because S2N sometimes receives OCSP responses: - # https://github.com/aws/s2n-tls/blob/14ed186a13c1ffae7fbb036ed5d2849ce7c17403/bin/echo.c#L180-L184 - client = managed_process(provider, client_options, - timeout=5, expect_stderr=True) - - expected_result = EXPECTED_RESULTS.get((endpoint, cipher), None) - - for results in client.get_results(): - results.assert_success() - - if expected_result is not None: - assert to_bytes(expected_result['cipher']) in results.stdout - if expected_result['kem']: - assert to_bytes(expected_result['kem']) in results.stdout - assert to_bytes(PQ_ENABLED_FLAG) in results.stdout - else: - assert to_bytes('KEM:') not in results.stdout - assert to_bytes(PQ_ENABLED_FLAG) not in results.stdout + Once the PR is merged, the Codebuild spec for NixIntegV2Batch will be updated + to remove the "well_known_endpoints" argument (manual process) and then this test + can be fully removed (PR). + ''' + assert 1 == 1