← Week 3: TLS Integration and CLI

Day 16: rustls ClientConfig with Client Cert (mTLS)

Phase 2 · July 2, 2026

← Week 3: TLS Integration and CLI

Agenda (2–3 hours)

  • Read (20 min): rustls ClientConfig with with_client_auth_cert docs
  • Build (90 min): Implement tls::client_config() in tls.rs
  • Verify (45 min): Connect to the mTLS server from Day 15
← Week 3: TLS Integration and CLI

rustls ClientConfig for mTLS

use rustls::{ClientConfig, RootCertStore};
use rustls_pki_types::ServerName;

pub fn client_config(
    trusted_ca_pem: &str,           // the CA the server's cert chains to
    client_cert_pem: Option<&str>,  // present for mTLS, None for standard TLS
    client_key_pem:  Option<&str>,
) -> anyhow::Result<ClientConfig> {
    let mut roots = RootCertStore::empty();
    roots.add(parse_cert_pem(trusted_ca_pem)?)?;

    let builder = ClientConfig::builder()
        .with_root_certificates(roots);

    match (client_cert_pem, client_key_pem) {
        (Some(cert), Some(key)) => {
            let certs = vec![parse_cert_pem(cert)?];
            let key   = parse_private_key_pem(key)?;
            builder.with_client_auth_cert(certs, key).map_err(Into::into)
        },
        _ => Ok(builder.with_no_client_auth()),
    }
}
← Week 3: TLS Integration and CLI

Making a Connection

use tokio_rustls::TlsConnector;

pub async fn connect(
    host: &str,
    port: u16,
    cfg: Arc<ClientConfig>,
) -> anyhow::Result<tokio_rustls::client::TlsStream<tokio::net::TcpStream>> {
    let connector = TlsConnector::from(cfg);
    let stream = tokio::net::TcpStream::connect((host, port)).await?;
    let server_name = ServerName::try_from(host.to_string())?;
    connector.connect(server_name, stream)
        .await
        .map_err(Into::into)
}
← Week 3: TLS Integration and CLI

What the Server Sees After mTLS

After a successful mTLS handshake, the server can inspect the client cert:

// In the server's accept handler:
let tls_stream = acceptor.accept(tcp_stream).await?;
let (_, server_conn) = tls_stream.get_ref();

if let Some(certs) = server_conn.peer_certificates() {
    let (_, client_cert) = X509Certificate::from_der(certs[0].as_ref())?;
    let identity = client_cert.subject().to_string();
    println!("Authenticated client: {}", identity);
}

This is how your provisioning service would extract the device identity from the client cert.

← Week 3: TLS Integration and CLI

Connection Failure Cases

Test that the client rejects:

  1. Server cert signed by an untrusted CA → UnknownIssuerCert
  2. Server cert with wrong hostname in SAN → InvalidCertificate(BadEncoding) or similar
  3. Server expects client cert but client sends none → server sends certificate_required alert
// Expect a connection error
let result = connect("localhost", port, untrusted_client_cfg).await;
assert!(result.is_err(), "should have rejected untrusted server cert");
← Week 3: TLS Integration and CLI

Challenge Assignment

Implement tls::client_config() in tls.rs.

Write a test test_mtls_full_handshake that:

  1. Starts an mTLS server (requires clientAuth cert from intermediate CA)
  2. Connects with a valid client cert → assert success and print client identity
  3. Connects with NO client cert → assert rejected with a meaningful error
  4. Connects with a client cert from a DIFFERENT CA → assert rejected
← Week 3: TLS Integration and CLI

Resources

  • rustls ClientConfig::builder().with_client_auth_cert(): docs.rs/rustls
  • tokio-rustls TlsConnector
  • rustls ServerConnection::peer_certificates()