From 31f37035da4b6d46ea923da5d6d526b598cdfb51 Mon Sep 17 00:00:00 2001 From: Sam Clark <3758302+goatgoose@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:51:09 -0500 Subject: [PATCH] fix(s2n-tls-hyper): Add proper IPv6 address formatting --- bindings/rust/certs/cert_localhost_ipv6.pem | 23 +++++++++ bindings/rust/certs/generate.sh | 13 ++++++ bindings/rust/certs/key_localhost_ipv6.pem | 28 +++++++++++ bindings/rust/s2n-tls-hyper/src/connector.rs | 17 ++++++- bindings/rust/s2n-tls-hyper/tests/http.rs | 49 ++++++++++++++++++++ 5 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 bindings/rust/certs/cert_localhost_ipv6.pem create mode 100644 bindings/rust/certs/key_localhost_ipv6.pem diff --git a/bindings/rust/certs/cert_localhost_ipv6.pem b/bindings/rust/certs/cert_localhost_ipv6.pem new file mode 100644 index 00000000000..2c3ae7365f7 --- /dev/null +++ b/bindings/rust/certs/cert_localhost_ipv6.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID2DCCAsCgAwIBAgIUT1hXpXZFhpbeq6UkbqlnOKAeUoowDQYJKoZIhvcNAQEL +BQAwbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24x +DzANBgNVBAoMBkFtYXpvbjEaMBgGA1UECwwRQW1hem9uV2ViU2VydmljZXMxEjAQ +BgNVBAMMCWxvY2FsaG9zdDAgFw0yNDExMjYwNDUyNDdaGA8yMTI0MTEwMjA0NTI0 +N1owbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24x +DzANBgNVBAoMBkFtYXpvbjEaMBgGA1UECwwRQW1hem9uV2ViU2VydmljZXMxEjAQ +BgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKE6vnPsBDz69ET25yWSikTchned2PvOzZ9zOd7jtQM7A504jHGMOoyXsZaLF+R2 +6M4Fo/7sWQfPImbrfDELIGoZo9V/DbJoETgmEsE6rSj2poEF46ZbNWa0THBNko0O +q3zgpwPmUPtPTF3QHH0JC/Kbm7V9S4yVx4Qit7GSPUr9Cgj3MJF6MAZqi29LpgSK +uMQ1bf6T9t6F5hNH3A4T5dfjtjcQczDhbnkKCUFa8DbOR+5vecQQjI6vJoK3suIU +xS4U6rNRhLYL+grswpqVh2AXI3qsl12jhcer7hbRovRM8MQkDCPnLDb9Chm2zVWW +5HgkUbtsZrpldrWoVdNihqUCAwEAAaNwMG4wHQYDVR0OBBYEFE6+x/LP/KNspKSj +uOY77KG+u/eAMB8GA1UdIwQYMBaAFE6+x/LP/KNspKSjuOY77KG+u/eAMA8GA1Ud +EwEB/wQFMAMBAf8wGwYDVR0RBBQwEocQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG +9w0BAQsFAAOCAQEAE//QpOTdTJfc2OM+/kilicGx/PiV3WjiqoaeQo6uUd7cD7wD +6JuXABtUSndfkxryeQzKQmq2KBedbQCOKnt/OeY0yMqCxbws3l3H+uwPwwAACIUn +3e5+RtotiQQBreSiHJJ6omFpd+cyvluZwZ20t3dhlgGU5NUN3zcHkdc8hGpaxtfJ +AaunssA40QcjQFQYI8ADTiQHW20rZcsVRKkwRkNVps/vMDpLBCyBp96xhTAtkoDH +Xs/Zi1bJiJ8xw3TkDeJFShpP+cQPYHI36qWqNjTei9eHrNX8sNFAdNZVyitoZ3/W +FrPdms/ivlvgQbWWB3EKxD+PsQXoYvjkGhMNmg== +-----END CERTIFICATE----- diff --git a/bindings/rust/certs/generate.sh b/bindings/rust/certs/generate.sh index 063c2d8e59d..b4bbd08f134 100755 --- a/bindings/rust/certs/generate.sh +++ b/bindings/rust/certs/generate.sh @@ -12,3 +12,16 @@ openssl req -x509 -newkey rsa:4096 -keyout key_rsa.pem -out cert_rsa.pem -sha256 # used for TLS 1.3 connections echo "generating ec key and cert" openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -keyout key.pem -out cert.pem -sha256 -days 36500 -nodes -subj "/C=US/ST=AZ/L=Tempe/O=Amazon/OU=AmazonWebServices/CN=localhost" + + +# used for testing IPv6. includes the localhost IPv6 address as a SAN. +echo "generating localhost IPv6 key and cert" +openssl req -x509 \ + -newkey rsa:2048 \ + -keyout key_localhost_ipv6.pem \ + -out cert_localhost_ipv6.pem \ + -sha256 \ + -days 36500 \ + -nodes \ + -subj "/C=US/ST=MA/L=Boston/O=Amazon/OU=AmazonWebServices/CN=localhost" \ + -addext "subjectAltName = IP:0:0:0:0:0:0:0:1" diff --git a/bindings/rust/certs/key_localhost_ipv6.pem b/bindings/rust/certs/key_localhost_ipv6.pem new file mode 100644 index 00000000000..57b8152ef0b --- /dev/null +++ b/bindings/rust/certs/key_localhost_ipv6.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQChOr5z7AQ8+vRE +9uclkopE3IZ3ndj7zs2fczne47UDOwOdOIxxjDqMl7GWixfkdujOBaP+7FkHzyJm +63wxCyBqGaPVfw2yaBE4JhLBOq0o9qaBBeOmWzVmtExwTZKNDqt84KcD5lD7T0xd +0Bx9CQvym5u1fUuMlceEIrexkj1K/QoI9zCRejAGaotvS6YEirjENW3+k/beheYT +R9wOE+XX47Y3EHMw4W55CglBWvA2zkfub3nEEIyOryaCt7LiFMUuFOqzUYS2C/oK +7MKalYdgFyN6rJddo4XHq+4W0aL0TPDEJAwj5yw2/QoZts1VluR4JFG7bGa6ZXa1 +qFXTYoalAgMBAAECggEAOmyjnDUv+fsEbkM8UrCt+zMgZRMlkGYJvBiQpXTFYNTP +Q/c8aV8jzlOf7kocD9WJGjMQEO4LexlzwXDe8ZSzG8+Lv29JgtdUOhEN5ciB/CCZ +CJMeQee2S6/VLTLnAseIm/l6fB7HRLIhHbOuxx5ynmkF/TfYmyqhgH/mKeow3M2O +hhSigu1sIfVWAwMq7pNJ1+Dr6z4LzGVjJOBFHFt/UCSngT9NJehwF+VNU7UW2g98 +buUEwT6qgsDQCWEjLORppWMQTvZOQnPrE/fBf7t/QCLZ42I0y155jrMYXJIx92Q4 +dqlgWmAFliLayNd1+9lJPDMG9JmA+JAv4bfSHqLMRwKBgQDXv5yA29KWVtCkzF83 +QHECOV5Fv5KE3eLP6CnZjOI7G3OqrTc/EEtvl+bXZBQa/97IAYpYVO2KDY/tVbrr +La1ArR7s/zN1OxHNcuSuL9ghD61nHTgQg9psPqvXqVD/lsMiUNu64GtU0Bw6yce5 +yf508Y8vXtm0A+riEv2H5dKtEwKBgQC/Tz5cpYCLi1riL6IthQrp22ILS7AxhPXb +h6EBBAUR3P92Bq27ArFuBOqLobF1fAeBichC2oCxWc+BPwqauJM75PNSKsXgFb/N +W2oDVjXIBk1PRc8d6QUHpl4nxphzL1W1xur/f1bIDixZUltOQjMOx1lgLQ/wwRWT +ekYU0LYMZwKBgQDLT68tI3o04ITn+Av2ZkzYmrVDJz/s46g84nylnYUHzFvYyDja +vgFInS4VZiMoOl13vzPe/9GFmjg6oOJvg3DUFRCip++XFt407IOhvkZ/CWYQWNGf +hpGMFhccOVuyMCGdMfOPDLM4jpE7uTD03Oxkycp0Cn8/i72J4/l1WleJbwKBgQCE +QkiezFx+HK2MSdoZFi1hV6YEoSMCWSWPy8hnZ1wJ6XtDIYLiEw6PPR7ZwcNpsYGO +8K5eaakm8ywd8nNmW8yOT85YM/Hw5ZhgZJ56CBPOYWz5LQ3vY7VygHX/kbC7kTH0 +Jb05PdPFIudOKT2ucN3TjcYgU4b9rr834gSpR1FUaQKBgQC4hCEX1kO/NIlJ1fBz +kHm0LKQT1ggMDRvQ+M5If7a8BqdvXdFN5V9No3DQseXMB1q3OxxbWtJwYiLOskjI +F2TC98TE9XnPaMABLB3nLBnPW2DHN5DS6A9ool48fV7nZeSUnuytgLPR3s1FrqEf +yc2ARE24uiZT4QhFoZY2TG2Vpw== +-----END PRIVATE KEY----- diff --git a/bindings/rust/s2n-tls-hyper/src/connector.rs b/bindings/rust/s2n-tls-hyper/src/connector.rs index a160e605471..c81ea9e312b 100644 --- a/bindings/rust/s2n-tls-hyper/src/connector.rs +++ b/bindings/rust/s2n-tls-hyper/src/connector.rs @@ -118,8 +118,21 @@ where return Box::pin(async move { Err(Error::InvalidScheme) }); } + // IPv6 addresses are enclosed in square brackets within the host of a URI: + // https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 + // IP-literal = "[" ( IPv6address / IPvFuture ) "]" + // + // These square brackets aren't part of the domain itself, so they are trimmed off to ensure + // that the proper server name is provided to s2n-tls-tokio. + let mut domain = req.host().unwrap_or(""); + if let Some(trimmed) = domain.strip_prefix('[') { + if let Some(trimmed) = trimmed.strip_suffix(']') { + domain = trimmed; + } + } + let domain = domain.to_owned(); + let builder = self.conn_builder.clone(); - let host = req.host().unwrap_or("").to_owned(); let call = self.http.call(req); Box::pin(async move { // `HttpsConnector` wraps an HTTP connector that also implements `Service`. @@ -130,7 +143,7 @@ where let connector = TlsConnector::new(builder); let tls = connector - .connect(&host, tcp) + .connect(&domain, tcp) .await .map_err(Error::TlsError)?; diff --git a/bindings/rust/s2n-tls-hyper/tests/http.rs b/bindings/rust/s2n-tls-hyper/tests/http.rs index 8a1a8a37243..1453f63c637 100644 --- a/bindings/rust/s2n-tls-hyper/tests/http.rs +++ b/bindings/rust/s2n-tls-hyper/tests/http.rs @@ -215,3 +215,52 @@ async fn error_matching() -> Result<(), Box> { server_task.abort(); Ok(()) } + +#[tokio::test] +async fn ipv6() -> Result<(), Box> { + let config = { + // The localhost IPv6 certificate contains ::1 in the SAN extension. s2n-tls will not + // successfully validate the certificate unless the sever name is properly formatted, and + // matches this identity. + let localhost_ipv6_cert: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../certs/cert_localhost_ipv6.pem" + )); + let localhost_ipv6_key: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../certs/key_localhost_ipv6.pem" + )); + + let mut builder = config::Config::builder(); + builder.load_pem(localhost_ipv6_cert, localhost_ipv6_key)?; + builder.trust_pem(localhost_ipv6_cert)?; + builder.build()? + }; + + // Listen for IPv6 connections. + let listener = TcpListener::bind("[::1]:0").await?; + let addr = listener.local_addr()?; + + let mut tasks = tokio::task::JoinSet::new(); + tasks.spawn(serve_echo(listener, config.clone())); + + tasks.spawn(async move { + let connector = HttpsConnector::new(config); + let client: Client<_, Empty> = + Client::builder(TokioExecutor::new()).build(connector); + + // Connect to the localhost IPv6 address. s2n-tls hostname verification should ensure that + // the certificate contains the `::1` identity (without square brackets). + let uri = Uri::from_str(format!("https://[::1]:{}", addr.port()).as_str())?; + let response = client.get(uri).await?; + assert_eq!(response.status(), 200); + + Ok(()) + }); + + while let Some(res) = tasks.join_next().await { + res.unwrap()?; + } + + Ok(()) +}