├── .gitignore ├── Cargo.toml ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | /target 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tide-acme" 3 | version = "0.2.0" 4 | authors = ["Josh Triplett "] 5 | description = "Automatic HTTPS certificates for Tide, via Let's Encrypt and ACME tls-alpn-01 challenges" 6 | keywords = ["acme", "autocert", "https", "tls", "alpn"] 7 | edition = "2018" 8 | repository = "https://github.com/http-rs/tide-acme" 9 | license = "MIT OR Apache-2.0" 10 | categories = ["web-programming::http-server", "web-programming"] 11 | 12 | [dependencies] 13 | async-std = "1.11.0" 14 | async-trait = "0.1.48" 15 | futures-lite = "1.12.0" 16 | rustls-acme = "0.3.0" 17 | tide-rustls = "0.3.0" 18 | tracing = { version = "0.1.34", default-features = false } 19 | 20 | [dev-dependencies] 21 | tide = "0.16.0" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `tide-acme` helps you serve HTTPS with Tide using automatic certificates, via 2 | Let's Encrypt and ACME tls-alpn-01 challenges. 3 | 4 | [Documentation](https://docs.rs/tide-acme) 5 | 6 | To use `tide-acme`, set up HTTPS with Tide normally using `tide_rustls`, but 7 | instead of specifying a certificate and key, call the `acme` method to 8 | configure automatic certificates in the TLS listener: 9 | 10 | ```rust 11 | use tide_acme::{AcmeConfig, TideRustlsExt}; 12 | use tide_acme::rustls_acme::caches::DirCache; 13 | 14 | let mut app = tide::new(); 15 | app.at("/").get(|_| async { Ok("Hello TLS") }); 16 | app.listen( 17 | tide_rustls::TlsListener::build().addrs("0.0.0.0:443").acme( 18 | AcmeConfig::new(vec!["domain.example"]) 19 | .contact_push("mailto:admin@example.org") 20 | .cache(DirCache::new("/srv/example/tide-acme-cache-dir")), 21 | ), 22 | ) 23 | .await?; 24 | ``` 25 | 26 | This will configure the TLS stack to obtain a certificate for the domain 27 | `domain.example`, which must be a domain for which your Tide server handles 28 | HTTPS traffic. 29 | 30 | On initial startup, your server will register a certificate via Let's Encrypt. 31 | Let's Encrypt will verify your server's control of the domain via an [ACME 32 | tls-alpn-01 challenge](https://tools.ietf.org/html/rfc8737), which the TLS 33 | listener configured by `tide-acme` will respond to. 34 | 35 | You must supply a cache via [`AcmeConfig::cache`] or one of the other cache 36 | methods. This cache will keep the ACME account key and registered certificates 37 | between runs, needed to avoid hitting rate limits. You can use 38 | [`rustls_acme::caches::DirCache`] for a simple filesystem cache, or implement 39 | your own caching using the `rustls_acme` cache traits. 40 | 41 | By default, `tide-acme` will use the Let's Encrypt staging environment, which 42 | is suitable for testing purposes; it produces certificates signed by a staging 43 | root so that you can verify your stack is working, but those certificates will 44 | not be trusted in browsers or other HTTPS clients. The staging environment has 45 | more generous rate limits for use while testing. 46 | 47 | When you're ready to deploy to production, you can call 48 | `.directory_lets_encrypt(true)` to switch to the production Let's Encrypt 49 | environment, which produces certificates trusted in browsers and other HTTPS 50 | clients. The production environment has [stricter rate 51 | limits](https://letsencrypt.org/docs/rate-limits/). 52 | 53 | `tide-acme` builds upon [`tide-rustls`](https://crates.io/crates/tide-rustls) 54 | and [`rustls-acme`](https://crates.io/crates/rustls-acme). 55 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `tide-acme` helps you serve HTTPS with Tide using automatic certificates, via Let's Encrypt and 2 | //! ACME tls-alpn-01 challenges. 3 | //! 4 | //! To use `tide-acme`, set up HTTPS with Tide normally using `tide_rustls`, but instead of 5 | //! specifying a certificate and key, call the `acme` method to configure automatic certificates in 6 | //! the TLS listener: 7 | //! 8 | //! ```no_run 9 | //! use tide_acme::{AcmeConfig, TideRustlsExt}; 10 | //! use tide_acme::rustls_acme::caches::DirCache; 11 | //! 12 | //! # async_std::task::block_on(async { 13 | //! let mut app = tide::new(); 14 | //! app.at("/").get(|_| async { Ok("Hello TLS") }); 15 | //! app.listen( 16 | //! tide_rustls::TlsListener::build().addrs("0.0.0.0:443").acme( 17 | //! AcmeConfig::new(vec!["domain.example"]) 18 | //! .contact_push("mailto:admin@example.org") 19 | //! .cache(DirCache::new("/srv/example/tide-acme-cache-dir")), 20 | //! ), 21 | //! ) 22 | //! .await?; 23 | //! # tide::Result::Ok(()) 24 | //! # }); 25 | //! ``` 26 | //! 27 | //! This will configure the TLS stack to obtain a certificate for the domain `domain.example`, 28 | //! which must be a domain for which your Tide server handles HTTPS traffic. 29 | //! 30 | //! On initial startup, your server will register a certificate via Let's Encrypt. Let's Encrypt 31 | //! will verify your server's control of the domain via an [ACME tls-alpn-01 32 | //! challenge](https://tools.ietf.org/html/rfc8737), which the TLS listener configured by 33 | //! `tide-acme` will respond to. 34 | //! 35 | //! You must supply a cache via [`AcmeConfig::cache`] or one of the other cache methods. This cache 36 | //! will keep the ACME account key and registered certificates between runs, needed to avoid 37 | //! hitting rate limits. You can use [`rustls_acme::caches::DirCache`] for a simple filesystem 38 | //! cache, or implement your own caching using the `rustls_acme` cache traits. 39 | //! 40 | //! By default, `tide-acme` will use the Let's Encrypt staging environment, which is suitable for 41 | //! testing purposes; it produces certificates signed by a staging root so that you can verify your 42 | //! stack is working, but those certificates will not be trusted in browsers or other HTTPS 43 | //! clients. The staging environment has more generous rate limits for use while testing. 44 | //! 45 | //! When you're ready to deploy to production, you can call `.directory_lets_encrypt(true)` to 46 | //! switch to the production Let's Encrypt environment, which produces certificates trusted in 47 | //! browsers and other HTTPS clients. The production environment has [stricter rate 48 | //! limits](https://letsencrypt.org/docs/rate-limits/). 49 | //! 50 | //! `tide-acme` builds upon [`tide-rustls`](https://crates.io/crates/tide-rustls) and 51 | //! [`rustls-acme`](https://crates.io/crates/rustls-acme). 52 | 53 | #![forbid(unsafe_code)] 54 | #![deny(missing_docs)] 55 | 56 | use std::fmt::Debug; 57 | 58 | use async_std::{net::TcpStream, stream::StreamExt}; 59 | use futures_lite::io::AsyncWriteExt; 60 | pub use rustls_acme::{self, AcmeConfig}; 61 | use tide_rustls::async_rustls::{server::TlsStream, TlsAcceptor}; 62 | use tide_rustls::rustls::Session; 63 | use tracing::{error, info, info_span, Instrument}; 64 | 65 | /// Custom TLS acceptor that answers ACME tls-alpn-01 challenges. 66 | pub struct AcmeTlsAcceptor(TlsAcceptor); 67 | 68 | impl AcmeTlsAcceptor { 69 | /// Create a new TLS acceptor that answers ACME tls-alpn-01 challenges, based on the specified 70 | /// configuration. 71 | /// 72 | /// This will start a background task to manage certificates via ACME. 73 | pub fn new(config: AcmeConfig) -> Self { 74 | let mut state = config.state(); 75 | let acceptor = state.acceptor(); 76 | async_std::task::spawn(async move { 77 | loop { 78 | async { 79 | match state 80 | .next() 81 | .await 82 | .expect("AcmeState::next() always returns Some") 83 | { 84 | Ok(event) => info!(?event, "AcmeState::next() processed an event"), 85 | Err(event) => error!(?event, "AcmeState::next() returned an error"), 86 | } 87 | } 88 | .instrument(info_span!("AcmeState::next()")) 89 | .await 90 | } 91 | }); 92 | Self(acceptor) 93 | } 94 | } 95 | 96 | #[async_trait::async_trait] 97 | impl tide_rustls::CustomTlsAcceptor for AcmeTlsAcceptor { 98 | async fn accept(&self, stream: TcpStream) -> std::io::Result>> { 99 | let mut tls = self.0.accept(stream).await?; 100 | match tls.get_ref().1.get_alpn_protocol() { 101 | Some(rustls_acme::acme::ACME_TLS_ALPN_NAME) => { 102 | info_span!("AcmeTlsAcceptor::accept()") 103 | .in_scope(|| info!("received acme-tls/1 validation request")); 104 | tls.close().await?; 105 | Ok(None) 106 | } 107 | _ => Ok(Some(tls)), 108 | } 109 | } 110 | } 111 | 112 | /// Extension trait for [`tide_rustls::TlsListenerBuilder`] 113 | /// 114 | /// With this trait imported, `TlsListenerBuilder` will have an `acme` method to set up a custom 115 | /// TLS acceptor that answers ACME tls-alpn-01 challenges. 116 | pub trait TideRustlsExt { 117 | /// Set up a custom TLS acceptor that answers ACME tls-alpn-01 challenges, using the specified 118 | /// configuration. 119 | /// 120 | /// This creates an [`AcmeTlsAcceptor`], which will start a background task to manage 121 | /// certificates via ACME. 122 | fn acme(self, config: AcmeConfig) -> Self; 123 | } 124 | 125 | impl TideRustlsExt for tide_rustls::TlsListenerBuilder { 126 | fn acme(self, config: AcmeConfig) -> Self { 127 | self.tls_acceptor(std::sync::Arc::new(AcmeTlsAcceptor::new(config))) 128 | } 129 | } 130 | --------------------------------------------------------------------------------