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:
- Rocket's
http
crate contains all of the relevant HTTP server handling code, including handling TLS connections and certificates. The latter functionality is contained in thetls
module. - Of particular importance is
TlsListener
, which is initialized by the mainRocket
and handles all TLS connections:
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:
- 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. - 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`.