Rocket hot reloading TLS certificate

I have been trying to contribute to a Rust Project for a long time and I choose the Rocket Framework
particularly in this case here:

##Getting familiar with the issue:

use crate::http::tls::TlsListener;

let conf = config.to_native_config().map_err(ErrorKind::Io)?;
let l = TlsListener::bind(addr, conf).await.map_err(ErrorKind::Bind)?;
addr = l.local_addr().unwrap_or(addr);
self.config.address = addr.ip();
self.config.port = addr.port();
ready(&mut self).await;
return self.http_server(l).await;

You should read through the TlsListener code first and make sure you understand how it works. Particularly, the TlsListener::bind() static method sets up certificate handling using rustls' with_single_cert.

Using Dynamic Certificate Resolution

TlsListener::bind() will need to be changed so that the rustls' struct uses dynamic TLS resolution as opposed to the current static resolution via with_single_cert. There are a few options:

  1. Continue to use the builder and change with_single_cert to with_cert_resolver.This is the easiest approach. Going this route would require implementing the ResolvesServerCert trait. The implementation would likely need to run a task in the background to detect changes. That task, and the implementing resolver struct, will need to synchronize in some way to keep certificates up to date. This synchronization should ideally be lock-free and likely involve a single atomic swap when new certificates are available.
  2. Modify the entire processing chain to use the more flexible Acceptor. If you choose to go this route or need to go this route, you'll likely need to adapt quite a bit of code to work with this. I wouldn't suggest going this route unless you need to.

##Sergio has put up quite a good step-by-step solution which I am trying to do:

1. Read the code. Make sure you understand it.
2. Implement a [ResolvesServerCert](https://docs.rs/rustls/latest/rustls/server/trait.ResolvesServerCert.html#) struct that always resolves to the same cert/key. This means we add no new functionality yet but keep the existing functionality while having a path for the new features.
3. Test that everything works as expected via the `tls` example. You'll need to run this example directly and check it in your browser/external client. We don't have an automated way to test this, unfortunately.
4. Have your dynamic resolver spawn a task that changes certificates based on some simple condition (say, after the 30s). Ensure that the resolver uses the "current" certificates all the time. This is just to test that everything related to synchronization and background tasks works as expected. The background task should be fully synchronous, and the resolver should *never* block waiting for anything. Synchronization should be as free as possible: we can and should expect at most N (N cores) cache line transfer if a cert is changed (once to write max one line in the background, once to read in the foreground). We could even do a single transfer by using `N` slots, if desired, to avoid a stampede, but we could also likely use relaxed atomics for even better performance.
5. Make the background task update certs if the configured certs change. This will require choosing some way to be notified of changes. It's unlikely we want to poll or be notified by the disk - we may want to ignore some changes. A common approach is to reload based on a signal, say `USR1`. That's a good approach for now.

I already come up with a solution for the first part which is under the PR:

However, I still struggle to update the cert on the thread that is supposed to be signed by the Resolver:

My code so far:

Resolver Implementation

pub struct Resolver<'a, R> where R: io::BufRead + std::clone::Clone {
    config: &'a mut Arc<RwLock<Config<R>>>,
}

impl<'a, R> Resolver<'a, R> where R: io::BufRead + std::clone::Clone {
    pub fn new(config: &'a mut Arc<RwLock<Config<R>>>) -> Self {
        Resolver {
            config,
        }
    }
}


where Config is the class that will be filled with the cert and private key

#[derive(Clone)]
pub struct Config<R> where R: io::BufRead + std::clone::Clone{
    pub cert_chain: R,
    pub private_key: R,
    pub ciphersuites: Vec<rustls::SupportedCipherSuite>,
    pub prefer_server_order: bool,
    pub ca_certs: Option<R>,
    pub mandatory_mtls: bool,
}

The struct Tls implements a tls listeners which is called:

impl TlsListener {
    pub async fn bind<R>(addr: SocketAddr, mut c: Config<R>) -> io::Result<TlsListener>
        where R: io::BufRead + std::clone::Clone
    {
        use rustls::server::{AllowAnyAuthenticatedClient, AllowAnyAnonymousOrAuthenticatedClient};
        use rustls::server::{NoClientAuth, ServerSessionMemoryCache, ServerConfig};

        let lock = Arc::new(RwLock::new(c.clone()));
        let c_lock = &mut Arc::clone(&lock);
 
        let Resolver = Resolver::new(c_lock);

        let cert_chain = load_certs(&mut c.cert_chain)
            .map_err(|e| io::Error::new(e.kind(), format!("bad TLS cert chain: {}", e)))?;

        let key = load_private_key(&mut c.private_key)
            .map_err(|e| io::Error::new(e.kind(), format!("bad TLS private key: {}", e)))?;

        let client_auth = match c.ca_certs {
            Some(ref mut ca_certs) => match load_ca_certs(ca_certs) {
                Ok(ca) if c.mandatory_mtls => AllowAnyAuthenticatedClient::new(ca).boxed(),
                Ok(ca) => AllowAnyAnonymousOrAuthenticatedClient::new(ca).boxed(),
                Err(e) => return Err(io::Error::new(e.kind(), format!("bad CA cert(s): {}", e))),
            },
            None => NoClientAuth::boxed(),
        };

        let mut tls_config = ServerConfig::builder()
            .with_cipher_suites(&c.ciphersuites)
            .with_safe_default_kx_groups()
            .with_safe_default_protocol_versions()
            .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bad TLS config: {}", e)))?
            .with_client_cert_verifier(client_auth)
            .with_single_cert(cert_chain, key)
            .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bad TLS config: {}", e)))?;

        tls_config.ignore_client_order = c.prefer_server_order;

        tls_config.alpn_protocols = vec![b"http/1.1".to_vec()];
        if cfg!(feature = "http2") {
            tls_config.alpn_protocols.insert(0, b"h2".to_vec());
        }

        tls_config.session_storage = ServerSessionMemoryCache::new(1024);
        tls_config.ticketer = rustls::Ticketer::new()
            .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bad TLS ticketer: {}", e)))?;

        let listener = TcpListener::bind(addr).await?;
        let acceptor = TlsAcceptor::from(Arc::new(tls_config));
        Ok(TlsListener { listener, acceptor })
    }
}

I changed also

    trait CopyReader: std::io::Read + std::clone::Clone {}
    type Reader = Box<dyn CopyReader + Sync + Send>;

    fn to_reader(value: &Either<RelativePathBuf, Vec<u8>>) -> io::Result<Reader> {
        match value {
            Either::Left(path) => {
                let path = path.relative();
                let file = fs::File::open(&path).map_err(move |e| {
                    Error::new(e.kind(), format!("error reading TLS file `{}`: {}",
                            Paint::white(figment::Source::File(path)), e))
                })?;

                Ok(Box::new(io::BufReader::new(file)))
            }
            Either::Right(vec) => Ok(Box::new(io::Cursor::new(vec.clone()))),
        }
    }

    impl TlsConfig {
        /// This is only called when TLS is enabled.
        pub(crate) fn to_native_config(&self) -> io::Result<Config<Reader>> {
            Ok(Config {
                cert_chain: to_reader(&self.certs)?,
                private_key: to_reader(&self.key)?,
                ciphersuites: self.rustls_ciphers().collect(),
                prefer_server_order: self.prefer_server_cipher_order,
                #[cfg(not(feature = "mtls"))]
                mandatory_mtls: false,
                #[cfg(not(feature = "mtls"))]
                ca_certs: None,
                #[cfg(feature = "mtls")]
                mandatory_mtls: self.mutual.as_ref().map_or(false, |m| m.mandatory),
                #[cfg(feature = "mtls")]
                ca_certs: match self.mutual {
                    Some(ref mtls) => Some(to_reader(&mtls.ca_certs)?),
                    None => None
                },
            })
        }

Must someone help with this error?

❯ cargo build
warning: unused variable: `Resolver`
  --> core/http/src/tls/listener.rs:98:13
   |
98 |         let Resolver = Resolver::new(c_lock);
   |             ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_Resolver`
   |
   = note: `#[warn(unused_variables)]` on by default

warning: field `config` is never read
  --> core/http/src/tls/listener.rs:17:5
   |
16 | pub struct Resolver<'a, R> where R: io::BufRead + std::clone::Clone {
   |            -------- field in this struct
17 |     config: &'a mut Arc<RwLock<Config<R>>>,
   |     ^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default

warning: variable `Resolver` should have a snake case name
  --> core/http/src/tls/listener.rs:98:13
   |
98 |         let Resolver = Resolver::new(c_lock);
   |             ^^^^^^^^ help: convert the identifier to snake case: `resolver`
   |
   = note: `#[warn(non_snake_case)]` on by default

warning: `rocket_http` (lib) generated 3 warnings (run `cargo fix --lib -p rocket_http` to apply 1 suggestion)
   Compiling rocket v0.5.0-rc.3 (/home/gentb/Rocket/core/lib)
error[E0038]: the trait `CopyReader` cannot be made into an object
   --> core/lib/src/config/tls.rs:645:74
    |
645 |     fn to_reader(value: &Either<RelativePathBuf, Vec<u8>>) -> io::Result<Reader> {
    |                                                                          ^^^^^^ `CopyReader` cannot be made into an object
    |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
   --> core/lib/src/config/tls.rs:642:39
    |
642 |     trait CopyReader: std::io::Read + std::clone::Clone {}
    |           ----------                  ^^^^^^^^^^^^^^^^^ ...because it requires `Self: Sized`
    |           |
    |           this trait cannot be made into an object...

error[E0038]: the trait `CopyReader` cannot be made into an object
   --> core/lib/src/config/tls.rs:662:68
    |
662 |         pub(crate) fn to_native_config(&self) -> io::Result<Config<Reader>> {
    |                                                                    ^^^^^^ `CopyReader` cannot be made into an object
    |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
   --> core/lib/src/config/tls.rs:642:39
    |
642 |     trait CopyReader: std::io::Read + std::clone::Clone {}
    |           ----------                  ^^^^^^^^^^^^^^^^^ ...because it requires `Self: Sized`
    |           |
    |           this trait cannot be made into an object...

error[E0277]: the trait bound `(dyn CopyReader + std::marker::Send + Sync + 'static): BufRead` is not satisfied
   --> core/lib/src/config/tls.rs:662:50
    |
662 |         pub(crate) fn to_native_config(&self) -> io::Result<Config<Reader>> {
    |                                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `BufRead` is not implemented for `(dyn CopyReader + std::marker::Send + Sync + 'static)`
    |
    = help: the following other types implement trait `BufRead`:
              &[u8]
              &mut B
              AllowStdIo<T>
              Box<B>
              StdinLock<'_>
              bytes::buf::Reader<B>
              either::Either<L, R>
              std::io::BufReader<R>
            and 4 others
    = note: required for `Box<(dyn CopyReader + std::marker::Send + Sync + 'static)>` to implement `BufRead`
note: required by a bound in `rocket_http::tls::Config`
   --> /home/gentb/Rocket/core/http/src/tls/listener.rs:71:31
    |
71  | pub struct Config<R> where R: io::BufRead + std::clone::Clone{
    |                               ^^^^^^^^^^^ required by this bound in `Config`

error[E0277]: the size for values of type `(dyn CopyReader + std::marker::Send + Sync + 'static)` cannot be known at compilation time
   --> core/lib/src/config/tls.rs:662:50
    |
662 |         pub(crate) fn to_native_config(&self) -> io::Result<Config<Reader>> {
    |                                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
    |
    = help: the trait `Sized` is not implemented for `(dyn CopyReader + std::marker::Send + Sync + 'static)`
    = note: required for `Box<(dyn CopyReader + std::marker::Send + Sync + 'static)>` to implement `Clone`
note: required by a bound in `rocket_http::tls::Config`
   --> /home/gentb/Rocket/core/http/src/tls/listener.rs:71:45
    |
71  | pub struct Config<R> where R: io::BufRead + std::clone::Clone{
    |                                             ^^^^^^^^^^^^^^^^^ required by this bound in `Config`

Some errors have detailed explanations: E0038, E0277.
For more information about an error, try `rustc --explain E0038`.

You can't have Clone in a trait object. Clone::clone returns a concrete type, but for a trait object the concrete type is not known. Instead you could add an fn clone_box(&self) -> Box<Self> method to CopyReader and remoce the Clone bound. Also maybe rename CopyReader to CloneReader as the reader doesn't need to implement Copy?

A general pattern for that is

trait DynClone {
    fn dyn_clone<'s>(&self) -> Box<dyn Trait + 's> where Self: 's; 
}   

impl<T: Clone + Trait> DynClone for T {
    fn dyn_clone<'s>(&self) -> Box<dyn Trait + 's> where Self: 's {
        Box::new(self.clone())
    }
}

// Your trait
trait Trait: DynClone {}

// Optional
impl Trait for Box<dyn Trait + '_> {}

impl Clone for Box<dyn Trait + '_> {
    fn clone(&self) -> Self {
        (**self).dyn_clone()
    }
}

(Some tweaking perhaps needed depending on your sepcific use case, if you want to require 'static, etc.)

1 Like

I have come up with this:

    trait TraitReader: BufRead + Clone {}


    trait DynReader {
        fn dyn_clone<'s> (&self) -> Box<dyn TraitReader + 's> where Self: 's;
    }

    impl<T: Clone + TraitReader> DynReader for T {
        fn dyn_clone<'s> (&self) -> Box<dyn TraitReader + 's> where Self: 's {
            Box::new(self.clone())
        }
    }

    trait CloneReader: DynReader {}

    impl Clone for Box<dyn TraitReader + Sync + Send> {
        fn clone(&self) -> Self {
            (**self).dyn_clone()
        }
    }

    type Reader = Box<dyn CloneReader>;

I get these errors

error[E0038]: the trait `TraitReader` cannot be made into an object
   --> core/lib/src/config/tls.rs:656:20
    |
656 |     impl Clone for Box<dyn TraitReader + Sync + Send> {
    |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `TraitReader` cannot be made into an object
    |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
   --> core/lib/src/config/tls.rs:641:34
    |
641 |     trait TraitReader: BufRead + Clone {}
    |           -----------            ^^^^^ ...because it requires `Self: Sized`
    |           |
    |           this trait cannot be made into an object...

error[E0038]: the trait `TraitReader` cannot be made into an object
   --> core/lib/src/config/tls.rs:649:41
    |
649 |         fn dyn_clone<'s> (&self) -> Box<dyn TraitReader + 's> where Self: 's {
    |                                         ^^^^^^^^^^^^^^^^^^^^ `TraitReader` cannot be made into an object
    |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
   --> core/lib/src/config/tls.rs:641:34
    |
641 |     trait TraitReader: BufRead + Clone {}
    |           -----------            ^^^^^ ...because it requires `Self: Sized`
    |           |
    |           this trait cannot be made into an object...

error[E0038]: the trait `TraitReader` cannot be made into an object
   --> core/lib/src/config/tls.rs:657:18
    |
657 |         fn clone(&self) -> Self {
    |                  ^^^^^ `TraitReader` cannot be made into an object
    |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
   --> core/lib/src/config/tls.rs:641:34
    |
641 |     trait TraitReader: BufRead + Clone {}
    |           -----------            ^^^^^ ...because it requires `Self: Sized`
    |           |
    |           this trait cannot be made into an object...

error[E0277]: the trait bound `(dyn CloneReader + 'static): BufRead` is not satisfied
   --> core/lib/src/config/tls.rs:681:50
    |
681 |         pub(crate) fn to_native_config(&self) -> io::Result<Config<Reader>> {
    |                                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `BufRead` is not implemented for `(dyn CloneReader + 'static)`
    |
    = help: the following other types implement trait `BufRead`:
              &[u8]
              &mut B
              AllowStdIo<T>
              Box<B>
              StdinLock<'_>
              bytes::buf::Reader<B>
              either::Either<L, R>
              std::io::BufReader<R>
            and 4 others
    = note: required for `Box<(dyn CloneReader + 'static)>` to implement `BufRead`
note: required by a bound in `rocket_http::tls::Config`
   --> /home/gentb/Rocket/core/http/src/tls/listener.rs:71:31
    |
71  | pub struct Config<R> where R: io::BufRead{
    |                               ^^^^^^^^^^^ required by this bound in `Config`

error[E0038]: the trait `TraitReader` cannot be made into an object
   --> core/lib/src/config/tls.rs:645:41
    |
645 |         fn dyn_clone<'s> (&self) -> Box<dyn TraitReader + 's> where Self: 's;
    |                                         ^^^^^^^^^^^^^^^^^^^^ `TraitReader` cannot be made into an object
    |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
   --> core/lib/src/config/tls.rs:641:34
    |
641 |     trait TraitReader: BufRead + Clone {}
    |           -----------            ^^^^^ ...because it requires `Self: Sized`
    |           |
    |           this trait cannot be made into an object...

Some errors have detailed explanations: E0038, E0277.
For more information about an error, try `rustc --explain E0038`.
error: could not compile `rocket` due to 5 previous errors


Changes:

  • Clone can't be a supertrait of TraitReader, as that implies Sized which makes the trait non-object-safe

  • DynReader needs to be a supertrait of TraitReader, so that dyn TraitReader + ... can call dyn_clone

  • dyn Trait and dyn Trait + Send + Sync are different types, so if you need those bounds, include them everywhere

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.