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

example(bindings): add async ConfigResolver #4477

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
@@ -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
jmayclin marked this conversation as resolved.
Show resolved Hide resolved
// in an async manner which won't block the tokio task.
async fn server_config(
&self,
animal: String,
) -> Result<s2n_tls::config::Config, s2n_tls::error::Error> {
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<Option<Pin<Box<dyn ConnectionFuture>>>, 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<Config, s2n_tls::error::Error>`, 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.
goatgoose marked this conversation as resolved.
Show resolved Hide resolved
//
// 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<dyn Error>> {
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<dyn Error + Send + Sync>>(())
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down