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 session resumption example #4573

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bindings/rust-examples/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"client-hello-config-resolution",
"session-resumption",
]
resolver = "2"

Expand Down
12 changes: 12 additions & 0 deletions bindings/rust-examples/session-resumption/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "session-resumption"
version.workspace = true
authors.workspace = true
publish.workspace = true
license.workspace = true
edition.workspace = true

[dependencies]
s2n-tls = { path = "../../rust/s2n-tls" }
s2n-tls-tokio = { path = "../../rust/s2n-tls-tokio" }
tokio = { version = "1", features = ["full"] }
13 changes: 13 additions & 0 deletions bindings/rust-examples/session-resumption/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
This example demonstrates the s2n-tls [session resumption](https://aws.github.io/s2n-tls/usage-guide/ch11-resumption.html) feature. First, the client connects to the server and retrieves a session ticket. Then, the client connects to the server again, but uses the retrieved session ticket to resume the previous session.
Copy link
Contributor

Choose a reason for hiding this comment

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

Yay for more examples 🕺 !

Two broad points

  1. The "ApplicationContext" demonstration feels a bit shoe-horned into this example. There is no need in this scenario to lock session tickets to a specific IP address, and generally client implementers should not do that unless they have specific information about distributed session resumption not being supported. I worry that someone is going to look at this and say "oh, session resumption should only happen with the same ip address". I think we should either
    • only focus on session resumption in this example
    • expand the example to use two servers with different STEKs, and demonstrate the success (ip specific resumption) and failure (attempt distributed resumption) modes.
  2. If we choose to go the latter route, I'd vote for using turmoil in the example. This allows all of the clients/servers to be simulated in the single process (no need for 3 terminal windows) and also allows the example to be explicitly testable. You can see an example of the turmoil stuff here

Note that if your goal is to just have an example of using the set_application_context method, a doc comment might be sufficient?


This example also demonstrates how the application context can be used to associate arbitrary information, such as an IP address, with an s2n-tls connection. The IP address is used in this example to determine whether a particular session ticket is viable for use on a subsequent connection.

To run this example, first start the server in one terminal:
```
cargo run --bin server
```

Then run the client in another terminal:
```
cargo run --bin client
```
20 changes: 20 additions & 0 deletions bindings/rust-examples/session-resumption/certs/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash

# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

# immediately bail if any command fails
set -e

echo "generating self-signed certificate"
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of generating new certificates, should we move the "certs" folder from the client-hello-resolver example into the rust-examples directory, and then have other examples depend on that?

openssl req -new -noenc -x509 \
-newkey ec \
-pkeyopt ec_paramgen_curve:P-384 \
-keyout test-key.pem \
-out test-cert.pem \
-days 65536 \
-SHA384 \
-subj "/C=US/CN=s2n" \
-addext "basicConstraints = critical,CA:true" \
-addext "keyUsage = critical,keyCertSign" \
-addext "subjectAltName = DNS:127.0.0.1"
13 changes: 13 additions & 0 deletions bindings/rust-examples/session-resumption/certs/test-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB7zCCAXagAwIBAgIUfg8ui1w1uHtk3bsIWhWitT3cicAwCgYIKoZIzj0EAwMw
GzELMAkGA1UEBhMCVVMxDDAKBgNVBAMMA3MybjAgFw0yNDA1MTUxNTQ4MTBaGA8y
MjAzMTAyMTE1NDgxMFowGzELMAkGA1UEBhMCVVMxDDAKBgNVBAMMA3MybjB2MBAG
ByqGSM49AgEGBSuBBAAiA2IABBaRabOi6hcEixRizyLHhezXXxeJzQmzl+8N21do
4NwVW38mCop94fkRX2yYCPE/NB976zbdbf2NI4fg6TFjMyjiuUfHzbto/9xUCbxU
Vhk/zQs/lQoWwZfmjMLVIEAjlaN5MHcwHQYDVR0OBBYEFEvgyZ9xCcmq66Jv4IWu
J66psVP1MB8GA1UdIwQYMBaAFEvgyZ9xCcmq66Jv4IWuJ66psVP1MA8GA1UdEwEB
/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgIEMBQGA1UdEQQNMAuCCTEyNy4wLjAuMTAK
BggqhkjOPQQDAwNnADBkAjBec3GrlGlPF2Hg3EWP6iFQicbsZJtpZIC0OEpunn57
vAuvCAD+1PolgVdggV1wyp4CMCwxEse93GCdBXZJb11bA//BAamrmLAaoX/AmqvY
vmUPPK9C3WbBT0IfTYkJG2XXQg==
-----END CERTIFICATE-----
6 changes: 6 additions & 0 deletions bindings/rust-examples/session-resumption/certs/test-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCNXvpHT/NBaXX8KIHy
5r62KOLHN9AMb7xCxZtfYIqTNJRdZSCf3hs6kdGMZWa/NoKhZANiAAQWkWmzouoX
BIsUYs8ix4Xs118Xic0Js5fvDdtXaODcFVt/JgqKfeH5EV9smAjxPzQfe+s23W39
jSOH4OkxYzMo4rlHx827aP/cVAm8VFYZP80LP5UKFsGX5ozC1SBAI5U=
-----END PRIVATE KEY-----
120 changes: 120 additions & 0 deletions bindings/rust-examples/session-resumption/src/bin/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use s2n_tls::{
callbacks::{SessionTicket, SessionTicketCallback},
connection::{Connection, ModifiedBuilder},
security::DEFAULT_TLS13,
};
use s2n_tls_tokio::TlsConnector;
use std::{
collections::HashMap,
error::Error,
net::IpAddr,
sync::{Arc, Mutex},
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};

struct ApplicationContext {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit, in this case, we are the application so this naming feels a bit odd. ConnectionContext, perhaps?

ip_addr: IpAddr,
tickets_received: u32,
}

#[derive(Default, Clone)]
pub struct SessionTicketHandler {
session_tickets: Arc<Mutex<HashMap<IpAddr, Vec<u8>>>>,
}

impl SessionTicketCallback for SessionTicketHandler {
fn on_session_ticket(&self, connection: &mut Connection, session_ticket: &SessionTicket) {
let app_context = connection
.application_context_mut::<ApplicationContext>()
.unwrap();

let size = session_ticket.len().unwrap();
let mut data = vec![0; size];
session_ticket.data(&mut data).unwrap();

// Associate the received session ticket with the connection's IP address.
let mut session_tickets = self.session_tickets.lock().unwrap();
session_tickets.insert(app_context.ip_addr, data);

// Indicate that the connection has received a session ticket.
app_context.tickets_received += 1;
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let cert_path = format!("{}/certs/test-cert.pem", env!("CARGO_MANIFEST_DIR"));
let cert = std::fs::read(cert_path).unwrap();

let session_ticket_handler = SessionTicketHandler::default();

let config = {
let mut builder = s2n_tls::config::Builder::new();
builder.set_security_policy(&DEFAULT_TLS13).unwrap();
builder.trust_pem(&cert).unwrap();
builder
.set_session_ticket_callback(session_ticket_handler.clone())
.unwrap();
builder.enable_session_tickets(true).unwrap();
builder.build()?
};

for connection_idx in 0..3 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: what does x mean?

let stream = TcpStream::connect("127.0.0.1:9000").await?;
let ip = stream.peer_addr().unwrap().ip();

let builder = ModifiedBuilder::new(config.clone(), |conn| {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd vote for a comment explaining this.

this Modified builder allows us to automatically set an available session ticket whenever a new connection is created ..., we do this because ...

// Associate the IP address with the new connection.
conn.set_application_context(ApplicationContext {
ip_addr: ip,
tickets_received: 0,
});

// If a session ticket exists that corresponds with the IP address, resume the
// connection.
let session_tickets = session_ticket_handler.session_tickets.lock().unwrap();
if let Some(session_ticket) = session_tickets.get(&ip) {
conn.set_session_ticket(session_ticket)?;
}

Ok(conn)
});
let client = TlsConnector::new(builder);

let handshake = client.connect("127.0.0.1", stream).await;
let mut tls = match handshake {
Ok(tls) => tls,
Err(e) => {
println!("error during handshake: {e}");
return Ok(());
}
};

let mut response = String::new();
tls.read_to_string(&mut response).await?;
println!("server response: {response}");

tls.shutdown().await?;

let connection = tls.as_ref();
if connection_idx == 0 {
assert!(!connection.resumed());
} else {
assert!(connection.resumed());
println!("connection resumed!");
}

let app_ctx = connection
.application_context::<ApplicationContext>()
.unwrap();
assert_eq!(app_ctx.tickets_received, 1);
}

Ok(())
}
49 changes: 49 additions & 0 deletions bindings/rust-examples/session-resumption/src/bin/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use s2n_tls::security::DEFAULT_TLS13;
use s2n_tls_tokio::TlsAcceptor;
use std::{error::Error, time::SystemTime};
use tokio::{io::AsyncWriteExt, net::TcpListener};

const KEY: [u8; 16] = [0; 16];
const KEY_NAME: [u8; 3] = [1, 3, 4];

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let cert_path = format!("{}/certs/test-cert.pem", env!("CARGO_MANIFEST_DIR"));
let key_path = format!("{}/certs/test-key.pem", env!("CARGO_MANIFEST_DIR"));
let cert = std::fs::read(cert_path).unwrap();
let key = std::fs::read(key_path).unwrap();

let mut config = s2n_tls::config::Builder::new();
config.set_security_policy(&DEFAULT_TLS13).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we're already returning Box<dyn Error> from the main function, prefer ? over unwrap.

config
.add_session_ticket_key(&KEY_NAME, &KEY, SystemTime::now())
.unwrap();
config.load_pem(&cert, &key).unwrap();
let config = config.build()?;
let server = TlsAcceptor::new(config);

let listener = TcpListener::bind("0.0.0.0:9000").await?;
loop {
let server = server.clone();
let (stream, _) = listener.accept().await?;

tokio::spawn(async move {
let handshake = server.accept(stream).await;
let mut tls = match handshake {
Ok(tls) => tls,
Err(e) => {
println!("error during handshake: {e}");
return Ok(());
}
};

let _ = tls.write("hello from server.".as_bytes()).await?;
tls.shutdown().await?;

Ok::<(), Box<dyn Error + Send + Sync>>(())
});
}
}
Loading