axum-server icon indicating copy to clipboard operation
axum-server copied to clipboard

Support SNI-based TLS configurations

Open palant opened this issue 4 months ago • 0 comments

As things are right now, only TLS configurations with a single certificate are properly supported. Setting up SNI-based certificate selection is possible but quite a pain, as it requires delving into Rustls. Ideally, there would API analogous to from_pem_chain_file(), for example:

pub async fn sni_from_pem_chain_files(
    mapping: Vec<(String, (impl AsRef<Path>, impl AsRef<Path>))>
) -> Result<Self>

For reference, an approximation of how I currently have to implement this functionality (requires adding rustls and rustls-pemfile as direct dependencies):

use axum_server::tls_rustls::RustlsConfig;
use rustls::{
    server::{ResolvesServerCertUsingSni, ServerConfig},
    sign::{any_supported_type, CertifiedKey},
    Certificate, PrivateKey
};
use std::{
    io::{Error, Result},
    path::Path,
    sync::Arc,
};

async fn read_cert(path: impl AsRef<Path>) -> Result<Vec<Certificate>> {
    let data = tokio::fs::read(path.as_ref()).await?;
    let certs = rustls_pemfile::certs(&mut data.as_ref()).collect::<Result<Vec<_>>>()?;
    Ok(certs.into_iter().map(|c| Certificate(c.to_vec())).collect())
}

async fn read_key(path: impl AsRef<Path>) -> Result<PrivateKey> {
    let data = tokio::fs::read(path.as_ref()).await?;
    let key =
        rustls_pemfile::private_key(&mut data.as_ref())?.ok_or(Error::other("no private key in file"))?;
    Ok(PrivateKey(key.secret_der().to_vec()))
}

async fn load_cert(cert_path: impl AsRef<Path>, key_path: impl AsRef<Path>) -> CertifiedKey {
    CertifiedKey::new(
        read_cert(cert_path).await.expect("failed reading cert file"),
        any_supported_type(&read_key(key_path).await.expect("failed reading key file"))
            .expect("failed converting private key"),
    )
}

async fn add_cert(
    resolver: &mut ResolvesServerCertUsingSni,
    domain: &str,
    cert_path: impl AsRef<Path>,
    key_path: impl AsRef<Path>
) {
    resolver
        .add(domain, load_cert(cert_path, key_path).await)
        .expect("failed adding cert");
}

let mut resolver = ResolvesServerCertUsingSni::new();
resolver.add("example.com", load_cert("example.com.pem", "example.com.key").await);
resolver.add("www.example.com", load_cert("example.com.pem", "example.com.key").await);
resolver.add("example.net", load_cert("example.net.pem", "example.net.key").await);
resolver.add("www.example.net", load_cert("example.net.pem", "example.net.key").await);

let mut config = ServerConfig::builder()
    .with_safe_defaults()
    .with_no_client_auth();
    .with_cert_resolver(Arc::new(resolver));

config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

RustlsConfig::from_config(Arc::new(config))

This is going to get simpler with Rustls 1.22 but still way too much boilerplate for what should be a trivial task.

palant avatar Feb 16 '24 11:02 palant