trillium_acme/
lib.rs

1//! trillium-acme helps you serve HTTPS with [Trillium](https://trillium.rs) using automatic
2//! certificates, via Let’s Encrypt and ACME tls-alpn-01 challenges.
3//!
4//! To use `trillium-acme`, create an [`AcmeConfig`] to configure the certificate you want, then
5//! call [`trillium_acme::new`] to create an [`Acceptor`] and a future. Spawn the future using the
6//! same stopper as the server, then pass the [`Acceptor`] to the server configuration:
7//!
8//! ```rust,no_run
9//! use trillium_acme::AcmeConfig;
10//! use trillium_acme::rustls_acme::caches::DirCache;
11//!
12//! let config = AcmeConfig::new(["domain.example"])
13//!     .contact_push("mailto:admin@example.org")
14//!     .cache(DirCache::new("/srv/example/acme-cache-dir"));
15//!
16//! let (acceptor, future) = trillium_acme::new(config);
17//! let stopper = trillium_smol::Stopper::new();
18//! let future = stopper.stop_future(future);
19//! trillium_smol::spawn(async {
20//!     future.await;
21//! });
22//! trillium_smol::config()
23//!     .with_port(443)
24//!     .with_host("0.0.0.0")
25//!     .with_nodelay()
26//!     .with_acceptor(acceptor)
27//!     .with_stopper(stopper)
28//!     .run(|conn: trillium::Conn| async move {
29//!        conn.ok("Hello TLS!")
30//!     });
31//! ```
32//!
33//! This will configure the TLS stack to obtain a certificate for the domain `domain.example`,
34//! which must be a domain for which your Trillium server handles HTTPS traffic.
35//!
36//! On initial startup, your server will register a certificate via Let's Encrypt. Let's Encrypt
37//! will verify your server's control of the domain via an
38//! [ACME tls-alpn-01 challenge](https://tools.ietf.org/html/rfc8737), which the TLS listener
39//! configured by `trillium-acme` will respond to.
40//!
41//! You must supply a cache via [`AcmeConfig::cache`] or one of the other cache methods. This cache
42//! will keep the ACME account key and registered certificates between runs, needed to avoid
43//! hitting rate limits. You can use [`rustls_acme::caches::DirCache`] for a simple filesystem
44//! cache, or implement your own caching using the `rustls_acme` cache traits.
45//!
46//! By default, `trillium-acme` will use the Let's Encrypt staging environment, which is suitable
47//! for testing purposes; it produces certificates signed by a staging root so that you can verify
48//! your stack is working, but those certificates will not be trusted in browsers or other HTTPS
49//! clients. The staging environment has more generous rate limits for use while testing.
50//!
51//! When you're ready to deploy to production, you can call `.directory_lets_encrypt(true)` to
52//! switch to the production Let's Encrypt environment, which produces certificates trusted in
53//! browsers and other HTTPS clients. The production environment has
54//! [stricter rate limits](https://letsencrypt.org/docs/rate-limits/).
55//!
56//! `trillium-acme` builds upon the [`rustls-acme`](https://crates.io/crates/rustls-acme) crate.
57
58#![forbid(unsafe_code)]
59#![deny(
60    clippy::dbg_macro,
61    missing_copy_implementations,
62    rustdoc::missing_crate_level_docs,
63    missing_debug_implementations,
64    missing_docs,
65    nonstandard_style,
66    unused_qualifications
67)]
68
69use std::fmt::Debug;
70use std::future::Future;
71use std::sync::Arc;
72
73use futures_lite::{AsyncWriteExt, StreamExt};
74use rustls_acme::futures_rustls::{rustls::ServerConfig, LazyConfigAcceptor};
75use trillium::log::{error, info};
76use trillium_server_common::async_trait;
77
78pub use rustls_acme::{self, AcmeConfig};
79
80mod transport;
81pub use transport::Transport;
82
83/// An acceptor that handles ACME tls-alpn-01 challenges.
84///
85/// After processing a challenge, this acceptor will return a Transport representing a closed
86/// connection.
87#[derive(Clone, Debug)]
88pub struct Acceptor {
89    challenge_server_config: Arc<ServerConfig>,
90    default_server_config: Arc<ServerConfig>,
91}
92
93/// Create a new [`Acceptor`] to pass to [`trillium_server_common::Config::with_acceptor`], and a
94/// new future that must be spawned detached in the background.
95pub fn new<EC: 'static + Debug, EA: 'static + Debug>(
96    config: AcmeConfig<EC, EA>,
97) -> (Acceptor, impl Future) {
98    let mut state = config.state();
99    let challenge_server_config = state.challenge_rustls_config();
100    let default_server_config = state.default_rustls_config();
101
102    let future = async move {
103        loop {
104            match state.next().await.unwrap() {
105                Ok(ok) => info!("ACME event: {:?}", ok),
106                Err(err) => error!("ACME error: {:?}", err),
107            }
108        }
109    };
110
111    (
112        Acceptor {
113            challenge_server_config,
114            default_server_config,
115        },
116        future,
117    )
118}
119
120#[async_trait]
121impl<Input> trillium_server_common::Acceptor<Input> for Acceptor
122where
123    Input: trillium_server_common::Transport,
124{
125    type Output = Transport<Input>;
126    type Error = std::io::Error;
127    async fn accept(&self, input: Input) -> Result<Self::Output, Self::Error> {
128        let start_handshake = LazyConfigAcceptor::new(Default::default(), input).await?;
129        if rustls_acme::is_tls_alpn_challenge(&start_handshake.client_hello()) {
130            let mut tls = start_handshake
131                .into_stream(self.challenge_server_config.clone())
132                .await?;
133            tls.close().await?;
134            Ok(Transport(None))
135        } else {
136            Ok(Transport(Some(
137                start_handshake
138                    .into_stream(self.default_server_config.clone())
139                    .await?,
140            )))
141        }
142    }
143}