Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add rust well-known-endpoint tests #4884

Merged
merged 15 commits into from
Nov 16, 2024
Merged
4 changes: 4 additions & 0 deletions .github/workflows/ci_rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion bindings/rust/integration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
31 changes: 31 additions & 0 deletions bindings/rust/integration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
let config = testing::build_config(&Policy::from_version("KMS-PQ-TLS-1-0-2020-07")?)?;
Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"default_pq" might be a better / more readable option.

Might also be worth opening a little follow-up issue to add "default_pq" to the pq integration tests 🤔

Copy link
Contributor Author

@jmayclin jmayclin Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default_pq will require us to add the kem_group API.

I think it's gonna be neater to to that in a separate PR, so I opened an issue to track this: #4896 . The issue explicitly calls out

  • updating the integration test
  • updating the pq sanity check
    as requirements for closing the issue.

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(())
}
}
97 changes: 97 additions & 0 deletions bindings/rust/integration/src/network/https_client.rs
Original file line number Diff line number Diff line change
@@ -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<Response<Incoming>, 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<Bytes>> = 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<dyn std::error::Error>> {
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(())
}
5 changes: 5 additions & 0 deletions bindings/rust/integration/src/network/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
95 changes: 95 additions & 0 deletions bindings/rust/integration/src/network/tls_client.rs
Original file line number Diff line number Diff line change
@@ -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<TlsStream<TcpStream>, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
// 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(())
}
25 changes: 0 additions & 25 deletions bindings/rust/s2n-tls-hyper/tests/web_client.rs

This file was deleted.

28 changes: 28 additions & 0 deletions bindings/rust/s2n-tls/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth on whether to make this an actual unreachable! statement. I ended up deciding no?

// 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 {
Expand Down
29 changes: 29 additions & 0 deletions bindings/rust/s2n-tls/src/testing/s2n_tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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)?;
Expand Down
Loading
Loading