Domani connects your domain to whatever you want to host on it, all with no account needed
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
domani/src/domain/acme/manager.rs

355 lines
13 KiB

use std::{sync, time};
use crate::domain::acme;
use crate::domain::tls::{Certificate, CertificateChain, PrivateKey};
use crate::error::unexpected::{self, Mappable};
use crate::{domain, token, util};
const LETS_ENCRYPT_URL: &str = "https://acme-v02.api.letsencrypt.org/directory";
#[derive(thiserror::Error, Debug)]
pub enum GetHttp01ChallengeKeyError {
#[error("not found")]
NotFound,
#[error(transparent)]
Unexpected(#[from] unexpected::Error),
}
#[mockall::automock]
pub trait Manager {
fn sync_domain(
&self,
domain: domain::Name,
external_config: Option<domain::ConfigExternalDomain>,
) -> util::BoxFuture<'_, unexpected::Result<()>>;
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError>;
fn get_certificate(
&self,
domain: &str,
) -> unexpected::Result<Option<(PrivateKey, CertificateChain)>>;
}
type BoxExternalStoreFn = Box<
dyn Fn(
&domain::ConfigExternalDomain,
) -> unexpected::Result<Box<dyn acme::store::Store + Send + Sync>>
+ Send
+ Sync,
>;
pub struct ManagerImpl {
store: Box<dyn acme::store::Store + Send + Sync>,
token_store: Box<dyn token::Store + Send + Sync>,
account: sync::Arc<acme2::Account>,
external_store_fn: BoxExternalStoreFn,
}
impl ManagerImpl {
pub async fn new<ExternalAcmeStoreFn, AcmeStore, TokenStore, AccountKeyStore>(
store: AcmeStore,
token_store: TokenStore,
account_key_store: AccountKeyStore,
external_store_fn: ExternalAcmeStoreFn,
config: &domain::ConfigACME,
) -> unexpected::Result<Self>
where
ExternalAcmeStoreFn: Fn(
&domain::ConfigExternalDomain,
) -> unexpected::Result<Box<dyn acme::store::Store + Send + Sync>>
+ Send
+ Sync
+ 'static,
AcmeStore: acme::store::Store + Send + Sync + 'static,
TokenStore: token::Store + Send + Sync + 'static,
AccountKeyStore: acme::account_key_store::Store + Send + Sync + 'static,
{
let dir = acme2::DirectoryBuilder::new(LETS_ENCRYPT_URL.to_string())
.build()
.await
.or_unexpected_while("creating acme2 directory builder")?;
let mut contact = String::from("mailto:");
contact.push_str(config.contact_email.as_str());
let mut builder = acme2::AccountBuilder::new(dir);
builder.contact(vec![contact]);
builder.terms_of_service_agreed(true);
if let Some(account_key) = account_key_store
.get()
.or_unexpected_while("fetching account key")?
{
builder.private_key(
(&account_key)
.try_into()
.or_unexpected_while("parsing private key")?,
);
}
let account = builder
.build()
.await
.or_unexpected_while("building account")?;
let account_key: PrivateKey = account
.private_key()
.as_ref()
.try_into()
.or_unexpected_while("parsing private key back out")?;
account_key_store
.set(&account_key)
.or_unexpected_while("storing account key")?;
Ok(Self {
store: Box::from(store),
token_store: Box::from(token_store),
account,
external_store_fn: Box::from(external_store_fn),
})
}
}
impl Manager for ManagerImpl {
fn sync_domain(
&self,
domain: domain::Name,
external_config: Option<domain::ConfigExternalDomain>,
) -> util::BoxFuture<'_, Result<(), unexpected::Error>> {
Box::pin(async move {
let external_store;
let store = match external_config {
None => &self.store,
Some(config) => {
external_store = (self.external_store_fn)(&config).or_unexpected()?;
&external_store
}
};
// 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(Some((_, certs))) = 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 cert_with_soonest_not_after = util::try_collect(
certs
.into_iter()
.map(|cert| openssl::x509::X509::try_from(&cert)),
)
.or_unexpected_while("parsing x509 certs")?
.into_iter()
.reduce(|a, b| if a.not_after() < b.not_after() { a } else { b })
.ok_or(unexpected::Error::from(
"expected there to be more than one cert",
))?;
if thirty_days < cert_with_soonest_not_after.not_after() {
return Ok(());
}
}
log::info!("Fetching a new certificate for domain {}", domain.as_str());
let mut builder = acme2::OrderBuilder::new(self.account.clone());
builder.add_dns_identifier(domain.as_str().to_string());
let order = builder
.build()
.await
.or_unexpected_while("building order")?;
let authorizations = order
.authorizations()
.await
.or_unexpected_while("fetching authorizations")?;
for auth in authorizations {
let challenge = auth
.get_challenge("http-01")
.ok_or(unexpected::Error::from("expected http-01 challenge"))?;
let challenge_token = challenge
.token
.as_ref()
.ok_or(unexpected::Error::from("expected challenge to have token"))?;
let challenge_key = challenge
.key_authorization()
.or_unexpected_while("getting challenge key from authorization")?
.ok_or(unexpected::Error::from("expected challenge to have key"))?;
self.token_store
.set(challenge_token.clone(), challenge_key)
.or_unexpected_while("storing challenge token")?;
// 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.
log::info!(
"Waiting for ACME challenge to be validated for domain {}",
domain.as_str(),
);
let challenge = challenge
.validate()
.await
.or_unexpected_while("initiating challenge validation")?;
// 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.token_store
.del(challenge_token)
.or_unexpected_while("deleting challenge token")?;
let challenge = challenge_res.or_unexpected_while("getting challenge status")?;
if challenge.status != acme2::ChallengeStatus::Valid {
return Err(unexpected::Error::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.
log::info!(
"Waiting for ACME authorization to be validated for domain {}",
domain.as_str(),
);
let authorization = auth
.wait_done(time::Duration::from_secs(5), 3)
.await
.or_unexpected_while("waiting for authorization status")?;
if authorization.status != acme2::AuthorizationStatus::Valid {
return Err(unexpected::Error::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).
log::info!(
"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
.or_unexpected_while("waiting for order to be ready")?;
if order.status != acme2::OrderStatus::Ready {
return Err(unexpected::Error::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 = PrivateKey::new();
let acme2_pkey = (&pkey)
.try_into()
.or_unexpected_while("parsing new private key")?;
// Create a certificate signing request for the order, and request
// the certificate.
let order = order
.finalize(acme2::Csr::Automatic(acme2_pkey))
.await
.or_unexpected_while("finalizing order")?;
// 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.
log::info!(
"Waiting for ACME order to be validated for domain {}",
domain.as_str(),
);
let order = order
.wait_done(time::Duration::from_secs(5), 3)
.await
.or_unexpected_while("waiting for order to be validated")?;
if order.status != acme2::OrderStatus::Valid {
return Err(unexpected::Error::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.
log::info!("Fetching certificate for domain {}", domain.as_str());
let certs = util::try_collect(
order
.certificate()
.await
.or_unexpected_while("fetching certificate")?
.ok_or(unexpected::Error::from(
"expected the order to return a certificate",
))?
.into_iter()
.map(|cert| Certificate::try_from(cert.as_ref())),
)
.or_unexpected_while("parsing certificate")?;
if certs.len() <= 1 {
return Err(unexpected::Error::from(
format!(
"expected more than one certificate to be returned, instead got {}",
certs.len(),
)
.as_str(),
));
}
log::info!("Certificate for {} successfully retrieved", domain.as_str());
store
.set_certificate(domain.as_str(), pkey, certs.into())
.or_unexpected_while("storing new cert")?;
Ok(())
})
}
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError> {
match self.token_store.get(token) {
Ok(Some(v)) => Ok(v),
Ok(None) => Err(GetHttp01ChallengeKeyError::NotFound),
Err(e) => Err(e.into()),
}
}
/// Returned vec is guaranteed to have len > 0
fn get_certificate(
&self,
domain: &str,
) -> unexpected::Result<Option<(PrivateKey, CertificateChain)>> {
self.store.get_certificate(domain)
}
}