Certificates

In this chapter, we discuss the configuration of the certificates that are required for a working Quinn connection.

As QUIC uses TLS 1.3 for authentication of connections, the server needs to provide the client with a certificate confirming its identity, and the client must be configured to trust the certificates it receives from the server.

Insecure Connection

For our example use case, the easiest way to allow the client to trust our server is to disable certificate verification (don't do this in production!). When the rustls dangerous_configuration feature flag is enabled, a client can be configured to trust any server.

Start by adding a rustls dependency with the dangerous_configuration feature flag to your Cargo.toml file.

quinn = "*"
rustls = { version = "*", features = ["dangerous_configuration", "quic"] }

Then, allow the client to skip the certificate validation by implementing ServerCertVerifier and letting it assert verification for any server.

#![allow(unused)]
fn main() {
// Implementation of `ServerCertVerifier` that verifies everything as trustworthy.
struct SkipServerVerification;

impl SkipServerVerification {
    fn new() -> Arc<Self> {
        Arc::new(Self)
    }
}

impl rustls::client::ServerCertVerifier for SkipServerVerification {
    fn verify_server_cert(
        &self,
        _end_entity: &rustls::Certificate,
        _intermediates: &[rustls::Certificate],
        _server_name: &rustls::ServerName,
        _scts: &mut dyn Iterator<Item = &[u8]>,
        _ocsp_response: &[u8],
        _now: std::time::SystemTime,
    ) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
        Ok(rustls::client::ServerCertVerified::assertion())
    }
}
}

After that, modify the ClientConfig to use this ServerCertVerifier implementation.

#![allow(unused)]
fn main() {
fn configure_client() -> ClientConfig {
    let crypto = rustls::ClientConfig::builder()
        .with_safe_defaults()
        .with_custom_certificate_verifier(SkipServerVerification::new())
        .with_no_client_auth();

    ClientConfig::new(Arc::new(crypto))
}
}

Finally, if you plug this ClientConfig into the Endpoint::set_default_client_config() your client endpoint should verify all connections as trustworthy.

Using Certificates

In this section, we look at certifying an endpoint with a certificate. The certificate can be signed with its key, or with a certificate authority's key.

Self Signed Certificates

Relying on self-signed certificates means that clients allow servers to sign their certificates. This is simpler because no third party is involved in signing the server's certificate. However, self-signed certificates do not protect users from person-in-the-middle attacks, because an interceptor can trivially replace the certificate with one that it has signed. Self-signed certificates, among other options, can be created using the rcgen crate or the openssl binary. This example uses rcgen to generate a certificate.

Let's look at an example:

#![allow(unused)]
fn main() {
fn generate_self_signed_cert() -> Result<(rustls::Certificate, rustls::PrivateKey), Box<dyn Error>>
{
    let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()])?;
    let key = rustls::PrivateKey(cert.serialize_private_key_der());
    Ok((rustls::Certificate(cert.serialize_der()?), key))
}
}

Note that generate_simple_self_signed returns a Certificate that can be serialized to both .der and .pem formats.

Non-self-signed Certificates

For this example, we use Let's Encrypt, a well-known Certificate Authority (CA) (certificate issuer) which distributes certificates for free.

Generate Certificate

certbot can be used with Let's Encrypt to generate certificates; its website comes with clear instructions. Because we're generating a certificate for an internal test server, the process used will be slightly different compared to what you would do when generating certificates for an existing (public) website.

On the certbot website, select that you do not have a public web server and follow the given installation instructions. certbot must answer a cryptographic challenge of the Let's Encrypt API to prove that you control the domain. It needs to listen on port 80 (HTTP) or 443 (HTTPS) to achieve this. Open the appropriate port in your firewall and router.

If certbot is installed, run certbot certonly --standalone, this command will start a web server in the background and start the challenge. certbot asks for the required data and writes the certificates to fullchain.pem and the private key to privkey.pem. These files can then be referenced in code.

#![allow(unused)]
fn main() {
use std::{error::Error, fs::File, io::BufReader};

pub fn read_certs_from_file(
) -> Result<(Vec<rustls::Certificate>, rustls::PrivateKey), Box<dyn Error>> {
    let mut cert_chain_reader = BufReader::new(File::open("./fullchain.pem")?);
    let certs = rustls_pemfile::certs(&mut cert_chain_reader)?
        .into_iter()
        .map(rustls::Certificate)
        .collect();

    let mut key_reader = BufReader::new(File::open("./privkey.pem")?);
    // if the file starts with "BEGIN RSA PRIVATE KEY"
    // let mut keys = rustls_pemfile::rsa_private_keys(&mut key_reader)?;
    // if the file starts with "BEGIN PRIVATE KEY"
    let mut keys = rustls_pemfile::pkcs8_private_keys(&mut key_reader)?;

    assert_eq!(keys.len(), 1);
    let key = rustls::PrivateKey(keys.remove(0));

    Ok((certs, key))
}
}

Configuring Certificates

Now that you have a valid certificate, the client and server need to be configured to use it. After configuring plug the configuration into the Endpoint.

Configure Server

#![allow(unused)]
fn main() {
let server_config = ServerConfig::with_single_cert(certs, key)?;
}

This is the only thing you need to do for your server to be secured.

Configure Client

#![allow(unused)]
fn main() {
let client_config = ClientConfig::with_native_roots();
}

This is the only thing you need to do for your client to trust a server certificate signed by a conventional certificate authority.



Next, let's have a look at how to set up a connection.