1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! `tide-acme` helps you serve HTTPS with Tide using automatic certificates, via Let's Encrypt and
//! ACME tls-alpn-01 challenges.
//!
//! To use `tide-acme`, set up HTTPS with Tide normally using `tide_rustls`, but instead of
//! specifying a certificate and key, call the `acme` method to configure automatic certificates in
//! the TLS listener:
//!
//! ```no_run
//! use tide_acme::{AcmeConfig, TideRustlsExt};
//!
//! # async_std::task::block_on(async {
//! let mut app = tide::new();
//! app.at("/").get(|_| async { Ok("Hello TLS") });
//! app.listen(
//!     tide_rustls::TlsListener::build().addrs("0.0.0.0:443").acme(
//!         AcmeConfig::new()
//!             .domains(vec!["domain.example".to_string()])
//!             .cache_dir("/srv/example/tide-acme-cache-dir"),
//!     ),
//! )
//! .await?;
//! # tide::Result::Ok(())
//! # });
//! ```
//!
//! This will configure the TLS stack to obtain a certificate for the domain `domain.example`,
//! which must be a domain for which your Tide server handles HTTPS traffic.
//!
//! On initial startup, your server will register a certificate via Let's Encrypt. Let's Encrypt
//! will verify your server's control of the domain via an [ACME tls-alpn-01
//! challenge](https://tools.ietf.org/html/rfc8737), which the TLS listener configured by
//! `tide-acme` will respond to.
//!
//! You must supply a persistent cache directory via [`AcmeConfig::cache_dir`]. This cache
//! directory will keep the ACME account key and registered certificates between runs, needed to
//! avoid hitting rate limits.
//!
//! By default, `tide-acme` will use the Let's Encrypt staging environment, which is suitable for
//! testing purposes; it produces certificates signed by a staging root so that you can verify your
//! stack is working, but those certificates will not be trusted in browsers or other HTTPS
//! clients. The staging environment has more generous rate limits for use while testing.
//!
//! When you're ready to deploy to production, you can call the [`AcmeConfig::production`] method
//! to switch to the production Let's Encrypt environment, which produces certificates trusted in
//! browsers and other HTTPS clients. The production environment has [stricter rate
//! limits](https://letsencrypt.org/docs/rate-limits/).
//!
//! `tide-acme` builds upon [`tide-rustls`](https://crates.io/crates/tide-rustls) and
//! [`rustls-acme`](https://crates.io/crates/rustls-acme).

#![forbid(unsafe_code)]
#![deny(missing_docs)]

use std::path::{Path, PathBuf};
use std::sync::Arc;

use async_std::net::TcpStream;
use tide_rustls::async_rustls::server::TlsStream;
use tide_rustls::rustls::{self, ServerConfig};

enum Environment {
    Staging,
    Production,
    Custom(&'static str),
}

/// Configuration for registering a certificate via ACME.
pub struct AcmeConfig {
    domains: Option<Vec<String>>,
    cache_dir: Option<PathBuf>,
    environment: Environment,
    server_config: Option<ServerConfig>,
}

impl AcmeConfig {
    /// Create a new configuration builder to register a certificate.
    ///
    /// By default, this registers certificates in the Let's Encrypt staging environment.
    pub fn new() -> Self {
        Self {
            domains: None,
            cache_dir: None,
            environment: Environment::Staging,
            server_config: None,
        }
    }

    /// Register a certificate for the specified domains.
    ///
    /// The list of domains must not be empty.
    pub fn domains(mut self, domains: Vec<String>) -> Self {
        self.domains = Some(domains);
        self
    }

    /// Register the certificate in the Let's Encrypt production environment.
    ///
    /// If not set, defaults to the Let's Encrypt staging environment.
    ///
    /// Note that the production environment has [strict rate
    /// limits](https://letsencrypt.org/docs/rate-limits/). Use the default staging environment for
    /// test purposes.
    pub fn production(mut self) -> Self {
        self.environment = Environment::Production;
        self
    }

    /// Set the ACME API URL to use to register the certificate.
    ///
    /// If not set, defaults to the Let's Encrypt staging environment.
    pub fn acme_api_url(mut self, environment: &'static str) -> Self {
        self.environment = Environment::Custom(environment);
        self
    }

    /// Set the persistent cache directory to remember registered certificates between runs.
    /// (Required.)
    ///
    /// Keep this directory private and secure.
    pub fn cache_dir(mut self, cache_dir: impl AsRef<Path>) -> Self {
        self.cache_dir = Some(cache_dir.as_ref().to_path_buf());
        self
    }

    /// Set the TLS server configuration.
    ///
    /// If not set, uses the rustls default configuration.
    pub fn server_config(mut self, server_config: ServerConfig) -> Self {
        self.server_config = Some(server_config);
        self
    }
}

/// Custom TLS acceptor that answers ACME tls-alpn-01 challenges.
pub struct AcmeTlsAcceptor(rustls_acme::TlsAcceptor);

impl AcmeTlsAcceptor {
    /// Create a new TLS acceptor that answers ACME tls-alpn-01 challenges, based on the specified
    /// configuration.
    ///
    /// This will start a background task to manage certificates via ACME.
    pub fn new(config: AcmeConfig) -> Self {
        let domains = config.domains.expect("AcmeConfig must set domains");
        assert!(!domains.is_empty());
        let cache_dir = config.cache_dir.expect("AcmeConfig must set cache_dir");
        let environment = match config.environment {
            Environment::Staging => rustls_acme::acme::LETS_ENCRYPT_STAGING_DIRECTORY,
            Environment::Production => rustls_acme::acme::LETS_ENCRYPT_PRODUCTION_DIRECTORY,
            Environment::Custom(environment) => environment,
        };
        let server_config = config
            .server_config
            .unwrap_or_else(|| ServerConfig::new(rustls::NoClientAuth::new()));

        let resolver = rustls_acme::ResolvesServerCertUsingAcme::new();
        let acceptor = rustls_acme::TlsAcceptor::new(server_config, resolver.clone());
        async_std::task::spawn(async move {
            resolver.run(environment, domains, Some(cache_dir)).await;
        });

        Self(acceptor)
    }
}

#[async_trait::async_trait]
impl tide_rustls::CustomTlsAcceptor for AcmeTlsAcceptor {
    async fn accept(&self, stream: TcpStream) -> std::io::Result<Option<TlsStream<TcpStream>>> {
        self.0.accept(stream).await
    }
}

/// Extension trait for [`tide_rustls::TlsListenerBuilder`]
///
/// With this trait imported, `TlsListenerBuilder` will have an `acme` method to set up a custom
/// TLS acceptor that answers ACME tls-alpn-01 challenges.
pub trait TideRustlsExt {
    /// Set up a custom TLS acceptor that answers ACME tls-alpn-01 challenges, using the specified
    /// configuration.
    ///
    /// This creates an [`AcmeTlsAcceptor`], which will start a background task to manage
    /// certificates via ACME.
    fn acme(self, config: AcmeConfig) -> Self;
}

impl<State> TideRustlsExt for tide_rustls::TlsListenerBuilder<State> {
    fn acme(self, config: AcmeConfig) -> Self {
        self.tls_acceptor(Arc::new(AcmeTlsAcceptor::new(config)))
    }
}