Skip to content
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
Expand Up @@ -2,6 +2,7 @@
members = [
"client-hello-config-resolution",
"hyper-server-client", "key-logging",
"mutual-tls",
"tokio-server-client",
]
resolver = "2"
Expand Down
13 changes: 13 additions & 0 deletions bindings/rust-examples/mutual-tls/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "mutual-tls"
version.workspace = true
authors.workspace = true
publish.workspace = true
license.workspace = true
edition.workspace = true

[dependencies]
s2n-tls = { path = "../../rust/extended/s2n-tls" }
s2n-tls-tokio = { path = "../../rust/extended/s2n-tls-tokio" }
tokio = { version = "1", features = ["full"] }
clap = { version = "4", features = ["derive"] }
58 changes: 58 additions & 0 deletions bindings/rust-examples/mutual-tls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
This example shows how to configure mutual TLS (client authentication), where the server requires clients to prove their identity with a client certificate. The [server](src/bin/server.rs) sets `ClientAuthType::Required` and validates client certificates against its trust store. The [client](src/bin/client.rs) loads a certificate and private key, just like a server would, and presents them when the server requests a certificate.

After the handshake the client sends a greeting, the server responds, and the two sides perform a graceful shutdown: the server closes its side of the connection once it is done writing, the client reads the response up to end-of-stream and then closes its own side, and the server reads the client's end-of-stream. Skipping this shutdown sequence means a peer can see an unexpected end-of-stream error instead of a clean close.

Note that when client authentication is used, the server MUST implement a host name verification callback to validate the identity on the client certificate: the default behavior will likely reject all client certificates. See [Client / Mutual Authentication](../../../docs/usage-guide/topics/ch09-certificates.md#client--mutual-authentication). In this example the server only accepts client certificates issued to `www.wombat.com`.

To run this example, first start the server in one terminal
```
cargo run --bin server
```
The server prints the address it is listening on, e.g. `Listening on 127.0.0.1:9443`. Then run the client in another terminal, using that address.

### Authenticated client
```
cargo run --bin client 127.0.0.1:9443
```
```
TlsStream {
connection: Connection {
handshake_type: "NEGOTIATED|FULL_HANDSHAKE|CLIENT_AUTH|MIDDLEBOX_COMPAT",
cipher_suite: "TLS_AES_128_GCM_SHA256",
actual_protocol_version: TLS13,
selected_key_exchange_group: "secp256r1",
..
},
}
The server says: good byte from the server
```
The `CLIENT_AUTH` flag in the handshake type shows that the client proved its identity with its `www.wombat.com` certificate. The server receives the greeting over the mutually authenticated connection and both sides shut down cleanly:
```
The client says: hello from the client
Connection from 127.0.0.1:55104 closed
```

### Client without a certificate
A client that doesn't present a certificate is rejected. For example, the client from the [tokio-server-client](../tokio-server-client) example doesn't load a client certificate:
```
cargo run -p tokio-server-client --bin client -- 127.0.0.1:9443
```
The server rejects the connection:
```
Rejected connection from 127.0.0.1:55110: Server requires client certificate
```

### Client with an untrusted identity
A client certificate that chains to a trusted CA is still rejected if the host name verification callback doesn't accept its identity. The `www.kangaroo.com` certificate is issued by the same example CA, but the server only trusts `www.wombat.com`:
```
cargo run --bin client -- --cert ../certs/kangaroo-chain.pem --key ../certs/kangaroo-key.pem 127.0.0.1:9443
```
The server rejects the connection:
```
Rejected connection from 127.0.0.1:35188: Certificate is not valid for the supplied hostname
```
and the rejected client receives no response and fails with an error instead of shutting down cleanly.

Two behaviors to be aware of when experimenting with the rejected clients:
* The server's rejection errors only surface after a delay: s2n-tls "blinds" handshake failures by 10-30 seconds to protect against timing side-channels.
* The rejected TLS1.3 clients still print a successful handshake. With TLS1.3, the client considers the handshake complete before the server has validated the client certificate, and the rejection only arrives as an alert on a later read. This protocol quirk is described in [Client / Mutual Authentication](../../../docs/usage-guide/topics/ch09-certificates.md#client--mutual-authentication).
80 changes: 80 additions & 0 deletions bindings/rust-examples/mutual-tls/src/bin/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use clap::Parser;
use s2n_tls::{config::Config, enums::ClientAuthType, security::DEFAULT_TLS13};
use s2n_tls_tokio::TlsConnector;
use std::{error::Error, fs};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};

/// NOTE: this certificate, key, and ca are to be used for demonstration purposes only!
const DEFAULT_CA: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../certs/ca-cert.pem");
const DEFAULT_CERT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../certs/wombat-chain.pem");
const DEFAULT_KEY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../certs/wombat-key.pem");

#[derive(Parser, Debug)]
struct Args {
#[clap(short, long, default_value_t = String::from(DEFAULT_CA))]
trust: String,
/// Certificate presented to the server to prove the client's identity.
#[clap(short, long, requires = "key", default_value_t = String::from(DEFAULT_CERT))]
cert: String,
#[clap(short, long, requires = "cert", default_value_t = String::from(DEFAULT_KEY))]
key: String,
addr: String,
}

async fn run_client(
trust_pem: &[u8],
cert_pem: &[u8],
key_pem: &[u8],
addr: &str,
) -> Result<(), Box<dyn Error>> {
// Set up the configuration for new connections.
// As with a normal TLS client, you will need a trust store.
let mut config = Config::builder();
config.set_security_policy(&DEFAULT_TLS13)?;
config.trust_pem(trust_pem)?;

// Load a certificate and private key for the client. The server
// authenticates the client, so the client needs its own certificate,
// just like a server does.
config.set_client_auth_type(ClientAuthType::Required)?;
config.load_pem(cert_pem, key_pem)?;

// Create the TlsConnector based on the configuration.
let client = TlsConnector::new(config.build()?);

// Connect to the server.
let stream = TcpStream::connect(addr).await?;
let mut tls = client.connect("www.kangaroo.com", stream).await?;
println!("{:#?}", tls);

// Send a greeting to the server.
tls.write_all(b"hello from the client").await?;

// Receive the server's response. The server closes its side of the
// connection when it is done writing, so read until end-of-stream.
let mut response = Vec::new();
tls.read_to_end(&mut response).await?;
println!("The server says: {}", String::from_utf8_lossy(&response));

// The server already closed its side of the connection, so close
// ours to complete the graceful two-way shutdown.
tls.shutdown().await?;

Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse();
let trust_pem = fs::read(args.trust)?;
let cert_pem = fs::read(args.cert)?;
let key_pem = fs::read(args.key)?;
run_client(&trust_pem, &cert_pem, &key_pem, &args.addr).await?;
Ok(())
}
139 changes: 139 additions & 0 deletions bindings/rust-examples/mutual-tls/src/bin/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use clap::Parser;
use s2n_tls::{
callbacks::VerifyHostNameCallback, config::Config, enums::ClientAuthType,
security::DEFAULT_TLS13,
};
use s2n_tls_tokio::TlsAcceptor;
use std::{error::Error, fs};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpListener,
};

/// NOTE: this certificate, key, and ca are to be used for demonstration purposes only!
const DEFAULT_CERT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../certs/kangaroo-chain.pem");
const DEFAULT_KEY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../certs/kangaroo-key.pem");
const DEFAULT_CA: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../certs/ca-cert.pem");

/// The name that the server expects to find on trusted client certificates.
const TRUSTED_CLIENT_NAME: &str = "www.wombat.com";

#[derive(Parser, Debug)]
struct Args {
#[clap(short, long, requires = "key", default_value_t = String::from(DEFAULT_CERT))]
cert: String,
#[clap(short, long, requires = "cert", default_value_t = String::from(DEFAULT_KEY))]
key: String,
/// CA used to validate client certificates.
#[clap(short, long, default_value_t = String::from(DEFAULT_CA))]
trust: String,
#[clap(short, long, default_value_t = String::from("127.0.0.1:0"))]
addr: String,
}

/// Verifies the identity on client certificates.
///
/// When client authentication is used, the server MUST implement a host name
/// verification callback: the default behavior will likely reject all client
/// certificates.
struct TrustedClientName;
impl VerifyHostNameCallback for TrustedClientName {
fn verify_host_name(&self, host_name: &str) -> bool {
host_name == TRUSTED_CLIENT_NAME
}
}

async fn run_server(
cert_pem: &[u8],
key_pem: &[u8],
trust_pem: &[u8],
addr: &str,
) -> Result<(), Box<dyn Error>> {
// Set up the configuration for new connections.
// As with a normal TLS server, you will need a certificate and private key.
let mut builder = Config::builder();
builder.set_security_policy(&DEFAULT_TLS13)?;
builder.load_pem(cert_pem, key_pem)?;

// Require clients to prove their identity with a client certificate.
builder.set_client_auth_type(ClientAuthType::Required)?;

// Client certificates are validated against the server's trust store.
// Only trust the example CA, not the default system certificates:
// any certificate signed by a system CA should not be treated as
// a valid client identity.
builder.trust_pem(trust_pem)?;
builder.with_system_certs(false)?;
builder.set_verify_host_callback(TrustedClientName)?;

let config = builder.build()?;

// Create the TlsAcceptor based on the configuration.
let server = TlsAcceptor::new(config);

// Bind to an address and listen for connections.
// ":0" can be used to automatically assign a port.
let listener = TcpListener::bind(&addr).await?;
let addr = listener
.local_addr()
.map(|x| x.to_string())
.unwrap_or_else(|_| "UNKNOWN".to_owned());
println!("Listening on {}", addr);

loop {
// Wait for a client to connect.
let (stream, peer_addr) = listener.accept().await?;
println!("Connection from {:?}", peer_addr);

// Spawn a new task to handle the connection.
// We probably want to spawn the task BEFORE calling TcpAcceptor::accept,
// because the TLS handshake can be slow.
let server = server.clone();
tokio::spawn(async move {
// The handshake fails if the client can't prove its identity,
// so unauthorized clients are rejected here.
let mut tls = match server.accept(stream).await {
Ok(tls) => tls,
Err(error) => {
println!("Rejected connection from {:?}: {}", peer_addr, error);
return Ok(());
}
};
println!("{:#?}", tls);

// Receive the client's greeting.
let mut buffer = [0; 1024];
let bytes_read = tls.read(&mut buffer).await?;
println!(
"The client says: {}",
String::from_utf8_lossy(&buffer[..bytes_read])
);

// Respond, then close our side of the connection to signal
// that we are done writing.
tls.write_all(b"good byte from the server").await?;
tls.shutdown().await?;

// The connection isn't fully closed until the client also
// closes its side by sending its own close_notify alert.
let bytes_read = tls.read(&mut buffer).await?;
assert_eq!(bytes_read, 0, "expected the client to close the connection");
println!("Connection from {:?} closed", peer_addr);

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse();
let cert_pem = fs::read(args.cert)?;
let key_pem = fs::read(args.key)?;
let trust_pem = fs::read(args.trust)?;
run_server(&cert_pem, &key_pem, &trust_pem, &args.addr).await?;
Ok(())
}
Loading