From d2263504376b629a5540f4549b6394353812480f Mon Sep 17 00:00:00 2001 From: James Mayclin Date: Wed, 27 Mar 2024 07:44:32 +0000 Subject: [PATCH] example(bindings): add async ConfigResolver --- .../client-hello-config-resolution/README.md | 3 + .../src/bin/async_load_server.rs | 132 ++++++++++++++++++ .../src/bin/server.rs | 1 + 3 files changed, 136 insertions(+) create mode 100644 bindings/rust-examples/client-hello-config-resolution/src/bin/async_load_server.rs diff --git a/bindings/rust-examples/client-hello-config-resolution/README.md b/bindings/rust-examples/client-hello-config-resolution/README.md index a0e22f99629..302770d5dae 100644 --- a/bindings/rust-examples/client-hello-config-resolution/README.md +++ b/bindings/rust-examples/client-hello-config-resolution/README.md @@ -41,3 +41,6 @@ TlsStream { The server said Hello, you are speaking to www.wombat.com ``` Once again there is a successful handshake showing that the server responded with the proper certificate. In this case, the config that the server configured for `www.wombat.com` did not support TLS 1.3, so the TLS 1.2 was negotiated instead. + +## Async Config Resolution +The [async load server](src/bin/async_load_server.rs) has the same functionality as the default [server](src/bin/server.rs), but implements the config resolution in an asynchronous manner. This allows the certificates to be loaded from disk without blocking the tokio runtime. A similar technique could be used to retrieve certificates over the network without blocking the runtime. diff --git a/bindings/rust-examples/client-hello-config-resolution/src/bin/async_load_server.rs b/bindings/rust-examples/client-hello-config-resolution/src/bin/async_load_server.rs new file mode 100644 index 00000000000..13db53e6985 --- /dev/null +++ b/bindings/rust-examples/client-hello-config-resolution/src/bin/async_load_server.rs @@ -0,0 +1,132 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use s2n_tls::{ + callbacks::{ClientHelloCallback, ConfigResolver, ConnectionFuture}, + security::{Policy, DEFAULT_TLS13}, +}; +use s2n_tls_tokio::TlsAcceptor; +use std::{error::Error, pin::Pin}; +use tokio::{io::AsyncWriteExt, net::*, try_join}; + +const PORT: u16 = 1738; + +#[derive(Clone)] +pub struct AsyncAnimalConfigResolver { + // the directory that contains the relevant certs + cert_directory: String, +} + +impl AsyncAnimalConfigResolver { + fn new(cert_directory: String) -> Self { + AsyncAnimalConfigResolver { cert_directory } + } + + // This method will lookup the appropriate certificates and read them from disc + // in an async manner which won't block the tokio task. + async fn server_config( + &self, + animal: String, + ) -> Result { + let cert_path = format!("{}/{}-chain.pem", self.cert_directory, animal); + let key_path = format!("{}/{}-key.pem", self.cert_directory, animal); + // we asynchronously read the cert chain and key from disk + let (cert, key) = try_join!(tokio::fs::read(cert_path), tokio::fs::read(key_path)) + // we map any IO errors to the s2n-tls Error type, as required by the ConfigResolver bounds. + .map_err(|io_error| s2n_tls::error::Error::application(Box::new(io_error)))?; + + let mut config = s2n_tls::config::Builder::new(); + // we can set different policies for different configs. "20190214" doesn't + // support TLS 1.3, so any customer requesting www.wombat.com won't be able + // to negotiate TLS 1.3 + let security_policy = match animal.as_str() { + "wombat" => Policy::from_version("20190214")?, + _ => DEFAULT_TLS13, + }; + config.set_security_policy(&security_policy)?; + config.load_pem(&cert, &key)?; + config.build() + } +} + +impl ClientHelloCallback for AsyncAnimalConfigResolver { + fn on_client_hello( + &self, + connection: &mut s2n_tls::connection::Connection, + ) -> Result>>, s2n_tls::error::Error> { + let sni = match connection.server_name() { + Some(sni) => sni, + None => { + println!("connection contained no SNI"); + return Err(s2n_tls::error::Error::application("no sni".into())); + } + }; + + // simple, limited logic to parse "animal" from "www.animal.com". + let mut tokens = sni.split('.'); + tokens.next(); // "www" + let animal = match tokens.next() { + Some(animal) => animal.to_owned(), // "animal" + None => { + println!("unable to parse sni"); + return Err(s2n_tls::error::Error::application( + format!("unable to parse sni: {}", sni).into(), + )); + } + }; + + // A ConfigResolver can be constructed from a future that returns + // `Result`, with the main additional + // requirements that the future is `'static`, which generally means that + // it can't have any interior references. This will prevent you from + // doing something like + // ``` + // let config_resolver = ConfigResolver::new(self.server_config(animal)); + // ``` + // because the compiler will complain that `&self` doesn't live long enough. + // + // One easy way to get around this is to create a new async block that + // owns all of the necessary data. Here we do this by first cloning the + // async resolver and then passing it into a closure which owns all of + // it's data (using the `move` keyword). + let async_resolver_clone = self.clone(); + let config_resolver = + ConfigResolver::new(async move { async_resolver_clone.server_config(animal).await }); + Ok(Some(Box::pin(config_resolver))) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cert_directory = format!("{}/certs", env!("CARGO_MANIFEST_DIR")); + let resolver = AsyncAnimalConfigResolver::new(cert_directory); + let mut initial_config = s2n_tls::config::Builder::new(); + initial_config.set_client_hello_callback(resolver)?; + + let server = TlsAcceptor::new(initial_config.build()?); + + let listener = TcpListener::bind(&format!("0.0.0.0:{PORT}")).await?; + loop { + let server = server.clone(); + let (stream, _) = listener.accept().await?; + tokio::spawn(async move { + // handshake with the client + let handshake = server.accept(stream).await; + let mut tls = match handshake { + Ok(tls) => tls, + Err(e) => { + println!("error during handshake: {:?}", e); + return Ok(()); + } + }; + + let connection = tls.as_ref(); + let offered_sni = connection.server_name().unwrap(); + let _ = tls + .write(format!("Hello, you are speaking to {offered_sni}").as_bytes()) + .await?; + tls.shutdown().await?; + Ok::<(), Box>(()) + }); + } +} diff --git a/bindings/rust-examples/client-hello-config-resolution/src/bin/server.rs b/bindings/rust-examples/client-hello-config-resolution/src/bin/server.rs index 7ad887cfa0b..958fdb51633 100644 --- a/bindings/rust-examples/client-hello-config-resolution/src/bin/server.rs +++ b/bindings/rust-examples/client-hello-config-resolution/src/bin/server.rs @@ -30,6 +30,7 @@ impl Default for AnimalConfigResolver { // using the ConfigResolver: https://docs.rs/s2n-tls/latest/s2n_tls/callbacks/struct.ConfigResolver.html# // This is useful if servers need to read from disk or make network calls as part // of the configuration, and want to avoid blocking the tokio task while doing so. +// An example of this implementation is contained in the "async_load_server". impl ClientHelloCallback for AnimalConfigResolver { fn on_client_hello( &self,