use std::{future, pin, sync, time}; use crate::domain::{self, acme}; use crate::error; use crate::error::{MapUnexpected, ToUnexpected}; const LETS_ENCRYPT_URL: &'static str = "https://acme-v02.api.letsencrypt.org/directory"; pub type GetHttp01ChallengeKeyError = acme::store::GetHttp01ChallengeKeyError; #[mockall::automock( type SyncDomainFuture=future::Ready>; )] pub trait Manager { type SyncDomainFuture<'mgr>: future::Future> + Send + Unpin + 'mgr where Self: 'mgr; fn sync_domain(&self, domain: domain::Name) -> Self::SyncDomainFuture<'_>; fn get_http01_challenge_key(&self, token: &str) -> Result; } pub trait BoxedManager: Manager + Send + Sync + Clone + 'static {} struct ManagerImpl where Store: acme::store::BoxedStore, { store: Store, account: sync::Arc, } pub async fn new( store: Store, contact_email: &str, ) -> Result where Store: acme::store::BoxedStore, { let dir = acme2::DirectoryBuilder::new(LETS_ENCRYPT_URL.to_string()) .build() .await .map_unexpected()?; let mut contact = String::from("mailto:"); contact.push_str(contact_email); let mut builder = acme2::AccountBuilder::new(dir); builder.contact(vec![contact]); builder.terms_of_service_agreed(true); match store.get_account_key() { Ok(account_key) => { builder.private_key(account_key); } Err(acme::store::GetAccountKeyError::NotFound) => (), Err(acme::store::GetAccountKeyError::Unexpected(err)) => return Err(err.to_unexpected()), } let account = builder.build().await.map_unexpected()?; store .set_account_key(&account.private_key()) .map_unexpected()?; Ok(sync::Arc::new(ManagerImpl { store, account })) } impl BoxedManager for sync::Arc> where Store: acme::store::BoxedStore {} impl Manager for sync::Arc> where Store: acme::store::BoxedStore, { type SyncDomainFuture<'mgr> = pin::Pin> + Send + 'mgr>> where Self: 'mgr; fn sync_domain(&self, domain: domain::Name) -> Self::SyncDomainFuture<'_> { // if there's an existing cert, and its expiry (determined by the soonest value of // not_after amongst its parts) is later than 30 days from now, then we consider it to be // synced. if let Ok(cert) = self.store.get_certificate(domain.as_str()) { let thirty_days = openssl::asn1::Asn1Time::days_from_now(30) .expect("parsed thirty days from now as Asn1Time"); let soonest_not_after = cert[1..] .into_iter() .map(|cert_part| cert_part.not_after()) .fold(cert[0].not_after(), |a, b| if a < b { a } else { b }); if thirty_days < soonest_not_after { return Box::pin(future::ready(Ok(()))); } } println!("fetching a new certificate for domain {}", domain.as_str()); Box::pin(async move { let mut builder = acme2::OrderBuilder::new(self.account.clone()); builder.add_dns_identifier(domain.as_str().to_string()); let order = builder.build().await.map_unexpected()?; let authorizations = order.authorizations().await.map_unexpected()?; for auth in authorizations { let challenge = auth .get_challenge("http-01") .ok_or(error::Unexpected::from("expected http-01 challenge"))?; let challenge_token = challenge .token .as_ref() .ok_or(error::Unexpected::from("expected challenge to have token"))?; let challenge_key = challenge .key_authorization() .map_unexpected()? .ok_or(error::Unexpected::from("expected challenge to have key"))?; self.store .set_http01_challenge_key(challenge_token, &challenge_key) .map_unexpected()?; // At this point the manager is prepared to serve the challenge key via the // `get_http01_challenge_key` method. It is expected that there is some http // server, with this domain pointing at it, which is prepared to serve that // challenge token/key under the `/.well-known/acme-challenge` path. The // `validate()` call below will instigate the acme server to make this check, and // block until it succeeds. println!( "waiting for ACME challenge to be validated for domain {}", domain.as_str(), ); let challenge = challenge.validate().await.map_unexpected()?; // Poll the challenge every 5 seconds until it is in either the // `valid` or `invalid` state. let challenge_res = challenge.wait_done(time::Duration::from_secs(5), 3).await; // no matter what the result is, clean up the challenge key self.store .del_http01_challenge_key(&challenge_token) .map_unexpected()?; let challenge = challenge_res.map_unexpected()?; if challenge.status != acme2::ChallengeStatus::Valid { return Err(error::Unexpected::from( format!( "expected challenge status to be valid, instead it was {:?}", challenge.status ) .as_str(), )); } // Poll the authorization every 5 seconds until it is in either the // `valid` or `invalid` state. println!( "waiting for ACME authorization to be validated for domain {}", domain.as_str(), ); let authorization = auth .wait_done(time::Duration::from_secs(5), 3) .await .map_unexpected()?; if authorization.status != acme2::AuthorizationStatus::Valid { return Err(error::Unexpected::from( format!( "expected authorization status to be valid, instead it was {:?}", authorization.status, ) .as_str(), )); } } // Poll the order every 5 seconds until it is in either the `ready` or `invalid` state. // Ready means that it is now ready for finalization (certificate creation). println!( "waiting for ACME order to be made ready for domain {}", domain.as_str(), ); let order = order .wait_ready(time::Duration::from_secs(5), 3) .await .map_unexpected()?; if order.status != acme2::OrderStatus::Ready { return Err(error::Unexpected::from( format!( "expected order status to be ready, instead it was {:?}", order.status, ) .as_str(), )); } // Generate an RSA private key for the certificate. let pkey = acme2::gen_rsa_private_key(4096).map_unexpected()?; // Create a certificate signing request for the order, and request // the certificate. let order = order .finalize(acme2::Csr::Automatic(pkey)) .await .map_unexpected()?; // Poll the order every 5 seconds until it is in either the // `valid` or `invalid` state. Valid means that the certificate // has been provisioned, and is now ready for download. println!( "waiting for ACME order to be validated for domain {}", domain.as_str(), ); let order = order .wait_done(time::Duration::from_secs(5), 3) .await .map_unexpected()?; if order.status != acme2::OrderStatus::Valid { return Err(error::Unexpected::from( format!( "expected order status to be valid, instead it was {:?}", order.status, ) .as_str(), )); } // Download the certificate, and panic if it doesn't exist. println!("fetching certificate for domain {}", domain.as_str()); let cert = order .certificate() .await .map_unexpected()? .ok_or(error::Unexpected::from( "expected the order to return a certificate", ))?; if cert.len() <= 1 { return Err(error::Unexpected::from( format!( "expected more than one certificate to be returned, instead got {}", cert.len(), ) .as_str(), )); } println!("certificate for {} successfully retrieved", domain.as_str()); self.store .set_certificate(domain.as_str(), cert) .map_unexpected()?; Ok(()) }) } fn get_http01_challenge_key(&self, token: &str) -> Result { self.store.get_http01_challenge_key(token) } }