← Week 3: TLS Integration and CLI

Day 17: mTLS Echo Server + Client

Phase 2 · July 3, 2026

← Week 3: TLS Integration and CLI

Agenda (2–3 hours)

  • Build (150 min): Standalone binary mtls-echo using toy-pki certs
  • Verify (30 min): Test with multiple clients including a revoked one

This is a synthesis day. No new reading — wire together everything from Days 15–16
into a demonstrable mTLS application that uses your toy PKI end-to-end.

← Week 3: TLS Integration and CLI

The Echo Server

// server/main.rs (or a subcommand of toy-pki)
async fn run_echo_server(config: EchoServerConfig) -> anyhow::Result<()> {
    let server_cfg = tls::server_config(
        &[&config.server_cert_pem, &config.intermediate_cert_pem],
        &config.server_key_pem,
        true,                   // require client cert
        Some(&config.ca_pem),   // trust this CA for client certs
    )?;

    let acceptor = TlsAcceptor::from(Arc::new(server_cfg));
    let listener = TcpListener::bind(&config.addr).await?;
    println!("mTLS echo server on {}", config.addr);

    loop {
        let (stream, peer) = listener.accept().await?;
        let acceptor = acceptor.clone();
        tokio::spawn(async move {
            if let Err(e) = handle_connection(stream, acceptor, peer).await {
                eprintln!("Connection error: {e}");
            }
        });
    }
}
← Week 3: TLS Integration and CLI

Connection Handler

async fn handle_connection(
    stream: TcpStream,
    acceptor: TlsAcceptor,
    peer: SocketAddr,
) -> anyhow::Result<()> {
    let tls = acceptor.accept(stream).await?;
    let (io, server_conn) = tls.get_ref();

    // Extract client identity from cert
    let identity = server_conn.peer_certificates()
        .and_then(|certs| certs.first())
        .and_then(|c| X509Certificate::from_der(c.as_ref()).ok())
        .map(|(_, cert)| cert.subject().to_string())
        .unwrap_or_else(|| "unknown".to_string());

    println!("[{peer}] Authenticated: {identity}");

    // Echo loop
    let mut buf = [0u8; 1024];
    loop {
        let n = tls.read(&mut buf).await?;
        if n == 0 { break; }
        tls.write_all(&buf[..n]).await?;
    }
    Ok(())
}
← Week 3: TLS Integration and CLI

The Echo Client

async fn run_echo_client(config: EchoClientConfig, message: &str) -> anyhow::Result<()> {
    let client_cfg = tls::client_config(
        &config.ca_pem,
        Some(&config.client_cert_pem),
        Some(&config.client_key_pem),
    )?;

    let mut stream = tls::connect(&config.host, config.port, Arc::new(client_cfg)).await?;
    stream.write_all(message.as_bytes()).await?;

    let mut response = String::new();
    stream.read_to_string(&mut response).await?;
    println!("Echo: {response}");
    Ok(())
}
← Week 3: TLS Integration and CLI

Challenge Assignment

Implement the mTLS echo server and client as subcommands:

  • pki echo-server --port 4433
  • pki echo-client --host localhost --port 4433 --identity device-001 --message "hello"

Then run this full scenario:

  1. Issue server cert for localhost
  2. Issue client cert for device-001
  3. Issue client cert for device-002
  4. Revoke device-002
  5. Start echo server
  6. device-001 connects and sends "hello" → receives "hello" echoed
  7. device-002 connects → rejected (either by CRL check or server policy)
  8. Document the rejection mechanism you chose and why
← Week 3: TLS Integration and CLI

Resources

  • tokio::io::{AsyncReadExt, AsyncWriteExt} for stream I/O
  • tokio-rustls TlsAcceptor, TlsStream — peer_certificates after accept
  • Your tls::server_config() and tls::client_config() from Days 15–16