diff --git a/bindings/rust-examples/Cargo.toml b/bindings/rust-examples/Cargo.toml index ee3b3d7beef..30ae0bc7bc3 100644 --- a/bindings/rust-examples/Cargo.toml +++ b/bindings/rust-examples/Cargo.toml @@ -2,6 +2,7 @@ members = [ "client-hello-config-resolution", "hyper-server-client", "key-logging", + "mutual-tls", "tokio-server-client", ] resolver = "2" diff --git a/bindings/rust-examples/mutual-tls/Cargo.toml b/bindings/rust-examples/mutual-tls/Cargo.toml new file mode 100644 index 00000000000..2c3275d766d --- /dev/null +++ b/bindings/rust-examples/mutual-tls/Cargo.toml @@ -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"] } diff --git a/bindings/rust-examples/mutual-tls/README.md b/bindings/rust-examples/mutual-tls/README.md new file mode 100644 index 00000000000..8091365e112 --- /dev/null +++ b/bindings/rust-examples/mutual-tls/README.md @@ -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). diff --git a/bindings/rust-examples/mutual-tls/src/bin/client.rs b/bindings/rust-examples/mutual-tls/src/bin/client.rs new file mode 100644 index 00000000000..ecb66e43596 --- /dev/null +++ b/bindings/rust-examples/mutual-tls/src/bin/client.rs @@ -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> { + // 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> { + 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(()) +} diff --git a/bindings/rust-examples/mutual-tls/src/bin/server.rs b/bindings/rust-examples/mutual-tls/src/bin/server.rs new file mode 100644 index 00000000000..7bce09b5951 --- /dev/null +++ b/bindings/rust-examples/mutual-tls/src/bin/server.rs @@ -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> { + // 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>(()) + }); + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + 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(()) +}