TLS and SNI in Actix

Using TLS and SNI with RustLS in Actix

This tutorial will show you how to use TLS with Actix-Web 2. It will also show you how to present different certificates based on SNI (server name identification)

  • Rust
  • Tutorial
  • Web

In this tutorial I assume that you have already obtained a certificate from LetsEncrypt. If not, please start with that process first. Once you have obtained the certificate, we will need two files that are included int certbots output: fullchain.pem and privkey.pem. Place these into your certificates folder inside the Rust directory.

In order to use RustLS, you will need to build a ServerConfig object.

In your imports add:

use rustls::{NoClientAuth, ServerConfig};
use rustls::internal::pemfile::{certs, pkcs8_private_keys};

And before starting your Actix web HttpServer with Actix App, add:

// Create configuration
let mut config = ServerConfig::new(NoClientAuth::new());

// Load key files
let cert_file = &mut BufReader::new(
    File::open("certificates/fullchain.pem").unwrap());
let key_file = &mut BufReader::new(
    File::open("certificates/privkey.pem").unwrap());

// Parse the certificate and set it in the configuration
let cert_chain = certs(cert_file).unwrap();
let mut keys = pkcs8_private_keys(key_file).unwrap();
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();

This configuration can be used to add your certificate to your HTTPServer. Normally your would bind your HTTPServer like this:

HttpServer::new(move || {
    App::new()
        ...
}).bind(bind_addr)?.run()

But in this case we will bind the RustLS config

HttpServer::new(move || {
    App::new()
        ...
}).bind_rustls(bind_addr, config)?.run()

With these changes, build and launch your app. If you set your hostname to 127.0.0.1 in /etc/hosts, you should encounter a working website! If not, you should get a warning that the certificate is not correct for your current host.

Using SNI with RustLS

You may want to present different certificates based on the hostname the client uses to approach your server. Or you may want to reject service if the hostname does not match the one you are expecting. In that case, a slightly more involved method is required. This method assumes that you have one or more certificates with cert_file.pem and privkey.pem files stored inside subdirectories of your certificates dir.

First off, you need a resolver. Add the following to your imports section:

use rustls::ResolvesServerCertUsingSNI;
use rustls::sign::{SigningKey, RSASigningKey};

And instantiate it after making your ServerConfig:

let mut resolver = ResolvesServerCertUsingSNI::new();

Adding this function above your main will allow you to easily add certificates to your resolver. It takes three arguments: The name of the subdirectory where your certificate is stored, the hostname you wish to target and finally, a mutably borrowed instance of your resolver.

fn add_certificate_to_resolver(
        name: &str, hostname: &str,
        resolver: &mut ResolvesServerCertUsingSNI
    ) {
        let cert_file = &mut BufReader::new(File::open(
                    format!("certificates/{}/fullchain.pem", name)
        ).unwrap());
        let key_file = &mut BufReader::new(File::open(
                format!("certificates/{}/privkey.pem", name)
        ).unwrap());
        let cert_chain = certs(cert_file).unwrap();
        let mut keys = pkcs8_private_keys(key_file).unwrap();
        let signing_key = RSASigningKey::new(
            &keys.remove(0)pr
        ).unwrap();
        let signing_key_boxed: Arc<Box<dyn SigningKey>> = Arc::new(
            Box::new(signing_key)
        );

        resolver.add(hostname, rustls::sign::CertifiedKey::new(
            cert_chain, signing_key_boxed
        )).expect("Invalid certificate for corona.ai");
    }

You can call this function directly after instantiating your resolver, like so:

add_certificate_to_resolver(
    "helixlabs", "api.helixlabs.ai", &mut resolver
);

This call would look in your certicates/helixlabs directory to find the certificate that is presented to the client if contacts your Actix app over the api.helixlabs.ai hostname.

Finally, before starting your server, you need to assign the resolver to your configuration instance.

config.cert_resolver = Arc::new(resolver);

Make sure to add use std::sync::{Arc}; in your import section if you do not have Arc imported already.

If you launch your app and added all the code correctly, you can visit your webapp on localhost. This will likely fail with a TLS error mentioning that a valid certificate was presented, but access was denied. This is expected. Try adding the hostname you intend to use to your /etc/hosts file and visiting your server with that name.