273 lines
9.9 KiB
Rust
273 lines
9.9 KiB
Rust
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<Result<(), error::Unexpected>>;
|
|
)]
|
|
pub trait Manager {
|
|
type SyncDomainFuture<'mgr>: future::Future<Output = Result<(), error::Unexpected>>
|
|
+ Send
|
|
+ Unpin
|
|
+ 'mgr
|
|
where
|
|
Self: 'mgr;
|
|
|
|
fn sync_domain(&self, domain: domain::Name) -> Self::SyncDomainFuture<'_>;
|
|
|
|
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError>;
|
|
}
|
|
|
|
pub trait BoxedManager: Manager + Send + Sync + Clone + 'static {}
|
|
|
|
struct ManagerImpl<Store>
|
|
where
|
|
Store: acme::store::BoxedStore,
|
|
{
|
|
store: Store,
|
|
account: sync::Arc<acme2::Account>,
|
|
}
|
|
|
|
pub async fn new<Store>(
|
|
store: Store,
|
|
contact_email: &str,
|
|
) -> Result<impl BoxedManager, error::Unexpected>
|
|
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<Store> BoxedManager for sync::Arc<ManagerImpl<Store>> where Store: acme::store::BoxedStore {}
|
|
|
|
impl<Store> Manager for sync::Arc<ManagerImpl<Store>>
|
|
where
|
|
Store: acme::store::BoxedStore,
|
|
{
|
|
type SyncDomainFuture<'mgr> = pin::Pin<Box<dyn future::Future<Output = Result<(), error::Unexpected>> + 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<String, GetHttp01ChallengeKeyError> {
|
|
self.store.get_http01_challenge_key(token)
|
|
}
|
|
}
|