Skip to main content

hashiverse_server_lib/transport/
https_transport_cert_refresher.rs

1//! # Let's Encrypt TLS certificate lifecycle manager
2//!
3//! Keeps the HTTPS transport's TLS certificate fresh without operator intervention.
4//! Uses `instant-acme` to talk to Let's Encrypt (production or staging, picked via
5//! [`hashiverse_lib::tools::config::USE_PRODUCTION_LETS_ENCRYPT`]) and
6//! TLS-ALPN-01 to solve domain-validation challenges inline on the same HTTPS port
7//! — no separate HTTP-01 listener needed.
8//!
9//! Two cert slots live side-by-side in `RwLock`s:
10//! - `base_cert` — the currently-serving cert.
11//! - `challenge_cert` — the short-lived self-signed (via `rcgen`) cert rustls serves
12//!   only when ACME is mid-challenge.
13//!
14//! Swapping slots is atomic, so a refresh never drops a live TLS handshake.
15//! Refresh cadence, retry-on-failure cadence, and renewal lead time all come from
16//! the `MILLIS_TO_WAIT_BETWEEN_CERT_*` constants in
17//! [`hashiverse_lib::tools::config`].
18
19use anyhow::Context;
20use hashiverse_lib::tools::config::USE_PRODUCTION_LETS_ENCRYPT;
21use std::path::Path;
22use hashiverse_lib::tools::time::{TimeMillis};
23use hashiverse_lib::tools::{config, tools};
24use instant_acme::{Account, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt, NewAccount, NewOrder, OrderStatus, RetryPolicy};
25use log::{info, trace, warn};
26use parking_lot::RwLock;
27use rcgen::{CertificateParams, SanType, generate_simple_self_signed};
28use rustls::server::{ClientHello, ResolvesServerCert};
29use rustls::sign::CertifiedKey;
30use std::fs;
31use std::net::IpAddr;
32use std::path::PathBuf;
33use std::str::FromStr;
34use std::sync::Arc;
35use tokio_util::sync::CancellationToken;
36use hashiverse_lib::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
37
38pub const FILENAME_LAST_REFRESHED: &str = "last_refreshed";
39pub const FILENAME_CERT: &str = "cert.pem";
40pub const FILENAME_KEY: &str = "key.pem";
41
42#[derive(Debug)]
43pub struct HttpsTransportCertRefresher {
44    force_local_network: bool,
45    path_certs: PathBuf,
46    filename_cert: PathBuf,
47    filename_key: PathBuf,
48    filename_last_refreshed: PathBuf,
49
50    ip: String,
51    port: u16,
52
53    // The "real" certificate for your IP
54    pub base_cert: Arc<RwLock<Option<Arc<CertifiedKey>>>>,
55
56    // The temporary certificate used for the ACME challenge
57    pub challenge_cert: Arc<RwLock<Option<Arc<CertifiedKey>>>>,
58}
59
60impl HttpsTransportCertRefresher {
61    pub async fn refresh_cert(&self) -> anyhow::Result<()> {
62        trace!("refreshing certificate");
63
64        let directory_url = match USE_PRODUCTION_LETS_ENCRYPT {
65            true => LetsEncrypt::Production.url().to_owned(),
66            false => LetsEncrypt::Staging.url().to_owned(),
67        };
68
69        let (account, _credentials) = Account::builder()?
70            .create(
71                &NewAccount {
72                    contact: &[],
73                    terms_of_service_agreed: true,
74                    only_return_existing: false,
75                },
76                directory_url,
77                None,
78            )
79            .await?;
80
81        // info!("letsencrypt.credentials: {}", serde_json::to_string_pretty(&credentials)?);
82
83        let ip_addr = IpAddr::from_str(&self.ip)?;
84        let identifiers = [Identifier::Ip(ip_addr)];
85        let new_order = NewOrder::new(&identifiers).profile("shortlived");
86
87        let mut order = account.new_order(&new_order).await?;
88        let mut challenge_url = "".to_string();
89        // info!("order state: {:#?}", order.state());
90
91        // Fulfil any of the TlsAlpn01 authorizations we can (there should be only one)
92        {
93            let mut authorizations = order.authorizations();
94            while let Some(result) = authorizations.next().await {
95                let mut authz = result?;
96                match authz.status {
97                    AuthorizationStatus::Valid => continue,
98                    AuthorizationStatus::Pending => {}
99                    _ => {
100                        warn!("unexpected AuthorizationStatus {:?}", authz.status);
101                        continue;
102                    }
103                }
104
105                let mut challenge = authz.challenge(ChallengeType::TlsAlpn01).ok_or_else(|| anyhow::anyhow!("no tlsalpn01 challenge found"))?;
106                challenge_url = challenge.url.clone();
107
108                // Create the temporary challenge cert
109                {
110                    let key_auth_sha256 = challenge.key_authorization().digest();
111
112                    let mut certificate_params = CertificateParams::default();
113                    certificate_params.subject_alt_names = vec![SanType::IpAddress(ip_addr)];
114                    certificate_params.custom_extensions.push(rcgen::CustomExtension::new_acme_identifier(key_auth_sha256.as_ref()));
115
116                    let key_pair = rcgen::KeyPair::generate()?;
117                    let cert = certificate_params.self_signed(&key_pair)?;
118
119                    let cert_chain = vec![rustls_pki_types::CertificateDer::from(cert.der().to_vec())];
120                    let key_der = rustls_pki_types::PrivatePkcs8KeyDer::from(key_pair.serialize_der());
121                    let key = rustls::crypto::ring::sign::any_supported_type(&rustls_pki_types::PrivateKeyDer::Pkcs8(key_der)).map_err(|_| anyhow::anyhow!("Unsupported key type"))?;
122
123                    let certified_key = CertifiedKey { cert: cert_chain, key, ocsp: None };
124
125                    *self.challenge_cert.write() = Some(Arc::new(certified_key));
126                }
127
128                // Tell letsencrypt that our challenge handler is ready for them
129                challenge.set_ready().await?;
130            }
131        }
132
133        //
134        trace!("challenge.url: {}", challenge_url);
135
136        // Wait for our request to be validated
137        let status = order.poll_ready(&RetryPolicy::default()).await?;
138        if status != OrderStatus::Ready {
139            anyhow::bail!("unexpected order status: {status:?}");
140        }
141
142        // Get the certificate
143        let private_key_pem = order.finalize().await?;
144        let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?;
145
146        // Write to disk
147        info!("writing new certificate to disk");
148        fs::create_dir_all(&self.path_certs)?;
149        fs::write(&self.filename_cert, cert_chain_pem)?;
150        write_private_key_file(&self.filename_key, private_key_pem.as_bytes())?;
151        fs::write(&self.filename_last_refreshed, challenge_url)?;
152
153        info!("refreshed certificate");
154
155        Ok(())
156    }
157
158    pub fn reload_certs(&self) -> anyhow::Result<()> {
159        trace!("reloading certificate");
160
161        let certified_key: CertifiedKey = {
162            let bytes_cert = fs::read(&self.filename_cert)?;
163            let bytes_key = fs::read(&self.filename_key)?;
164
165            // Return the CertifiedKey
166            let cert_chain = rustls_pemfile::certs(&mut &bytes_cert[..]).collect::<Result<Vec<_>, _>>()?;
167            let key_der = rustls_pemfile::private_key(&mut &bytes_key[..])?.ok_or_else(|| anyhow::anyhow!("No private key found in {}", self.filename_key.display()))?;
168            let key = rustls::crypto::ring::sign::any_supported_type(&key_der).map_err(|_| anyhow::anyhow!("Unsupported key type"))?;
169
170            CertifiedKey { cert: cert_chain, key, ocsp: None }
171        };
172
173        *self.base_cert.write() = Some(Arc::new(certified_key));
174
175        Ok(())
176    }
177
178    fn certs_last_refreshed(&self) -> anyhow::Result<TimeMillis> {
179        let certs_last_refreshed = fs::metadata(&self.filename_last_refreshed)
180            .map(|metadata| metadata.modified())
181            .unwrap_or_else(|_| Ok(std::time::SystemTime::UNIX_EPOCH))
182            .with_context(|| "checking last refreshed filename")?;
183
184        let certs_last_refreshed: TimeMillis = certs_last_refreshed.into();
185
186        Ok(certs_last_refreshed)
187    }
188
189    pub async fn process(&self, cancellation_token: CancellationToken) -> anyhow::Result<()> {
190        let time_provider = RealTimeProvider;
191
192        let mut certs_last_attempted = TimeMillis::zero();
193
194        loop {
195            if cancellation_token.is_cancelled() {
196                break;
197            }
198
199            let result: anyhow::Result<()> = try {
200                // Do nothing if we are forced local
201                if !self.force_local_network {
202                    // If we are in control of port 443, we can do the actual cert renewal from letsencrypt
203                    if 443u16 == self.port {
204                        // And do nothing if we have tried too recently
205                        let now_millis = time_provider.current_time_millis();
206                        if (now_millis - self.certs_last_refreshed()?) > config::MILLIS_TO_WAIT_BETWEEN_CERT_RENEWALS {
207                            if (now_millis - certs_last_attempted) > config::MILLIS_TO_WAIT_BETWEEN_CERT_RENEWAL_FAILURES {
208                                certs_last_attempted = now_millis;
209                                self.refresh_cert().await?;
210                            }
211                            else {
212                                trace!("we refreshed certs too recently to try again");
213                            }
214                        }
215                        else {
216                            trace!("we have a recent enough cert on disk");
217                        }
218                    }
219                    else {
220                        trace!("skipping cert refresh because port {} != 443", self.port);
221                    }
222                }
223                else {
224                    trace!("skipping cert refresh because force_local_network");
225                }
226
227                // Reload our certs from disk
228                self.reload_certs()?;
229            };
230
231            if let Err(e) = result {
232                warn!("error while refreshing certs: {}", e);
233            }
234
235            tools::cancellable_sleep_millis(&time_provider, config::MILLIS_TO_WAIT_BETWEEN_CERT_RENEWAL_CHECKS, &cancellation_token).await;
236        }
237
238        trace!("stopped HttpsTransportCertRefresher");
239
240        Ok(())
241    }
242}
243
244/// Write a private key file with owner-read-only permissions (0600) on Unix.
245/// Falls back to a plain write on non-Unix platforms.
246fn write_private_key_file(path: &Path, contents: &[u8]) -> anyhow::Result<()> {
247    #[cfg(unix)]
248    {
249        use std::io::Write;
250        use std::os::unix::fs::OpenOptionsExt;
251        let mut file = fs::OpenOptions::new()
252            .write(true)
253            .create(true)
254            .truncate(true)
255            .mode(0o600)
256            .open(path)?;
257        file.write_all(contents)?;
258    }
259    #[cfg(not(unix))]
260    {
261        fs::write(path, contents)?;
262    }
263    Ok(())
264}
265
266impl HttpsTransportCertRefresher {
267    pub fn new(path_certs: PathBuf, ip: String, port: u16, force_local_network: bool) -> anyhow::Result<Self> {
268        let filename_cert = path_certs.join(FILENAME_CERT);
269        let filename_key = path_certs.join(FILENAME_KEY);
270        let filename_last_refreshed = path_certs.join(FILENAME_LAST_REFRESHED);
271
272        // Make sure we start with at least some baseline certificates.
273        // Self-signed certs are generated whenever none exist on disk — including force_local_network
274        // (test/local) mode.  For production, Let's Encrypt replaces them via process().
275        if !fs::exists(&filename_cert)? || !fs::exists(&filename_key)? {
276            info!("generating self-signed certs");
277            let subject_alt_names = vec![ip.clone()];
278
279            let certified_key = generate_simple_self_signed(subject_alt_names)?;
280            fs::create_dir_all(&path_certs)?;
281            fs::write(&filename_cert, certified_key.cert.pem())?;
282            write_private_key_file(&filename_key, certified_key.signing_key.serialize_pem().as_bytes())?;
283        }
284
285        // Initially our certs are empty
286        let base_cert = Arc::new(RwLock::new(None));
287        let challenge_cert = Arc::new(RwLock::new(None));
288
289        Ok(Self {
290            force_local_network,
291            path_certs,
292            filename_cert,
293            filename_key,
294            filename_last_refreshed,
295            ip,
296            port,
297            base_cert,
298            challenge_cert,
299        })
300    }
301}
302
303impl ResolvesServerCert for HttpsTransportCertRefresher {
304    fn resolve(&self, client_hello: ClientHello) -> Option<Arc<CertifiedKey>> {
305        // Check if the client (the CA) is specifically asking for the ACME protocol
306        let is_acme = client_hello.alpn().map(|mut iter| iter.any(|proto| proto == b"acme-tls/1")).unwrap_or(false);
307
308        if is_acme {
309            // Return the temporary challenge cert generated from instant-acme tokens
310            info!("we have an acme challenge");
311            self.challenge_cert.read().clone()
312        }
313        else {
314            // Return the standard certificate for your IP
315            self.base_cert.read().clone()
316        }
317    }
318}