#![cfg(feature = "acme")]
use instant_acme::{
Account, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt, NewAccount, NewOrder,
OrderStatus, RetryPolicy,
};
use std::{collections::HashMap, sync::Arc};
use tokio::sync::RwLock;
pub type ChallengeStore = Arc<RwLock<HashMap<String, String>>>;
pub async fn obtain_certificate(
domain: &str,
email: &str,
challenge_store: ChallengeStore,
staging: bool,
) -> Result<(Vec<u8>, Vec<u8>), Box<dyn std::error::Error>> {
let url = if staging {
LetsEncrypt::Staging.url().to_owned()
} else {
LetsEncrypt::Production.url().to_owned()
};
tracing::info!(domain, "Starting ACME certificate provisioning");
let (account, _credentials) = Account::builder()?
.create(
&NewAccount {
contact: &[&format!("mailto:{email}")],
terms_of_service_agreed: true,
only_return_existing: false,
},
url,
None,
)
.await?;
let mut order = account
.new_order(&NewOrder::new(&[Identifier::Dns(domain.to_string())]))
.await?;
let mut authorizations = order.authorizations();
while let Some(result) = authorizations.next().await {
let mut authz = result?;
match authz.status {
AuthorizationStatus::Valid => continue,
AuthorizationStatus::Pending => {}
_ => return Err(format!("unexpected authorization status: {:?}", authz.status).into()),
}
let mut challenge = authz
.challenge(ChallengeType::Http01)
.ok_or("No HTTP-01 challenge available for this domain")?;
let key_auth = challenge.key_authorization().as_str().to_string();
let token = challenge.token.clone();
challenge_store.write().await.insert(token, key_auth);
challenge.set_ready().await?;
}
let status = order.poll_ready(&RetryPolicy::default()).await?;
if status != OrderStatus::Ready {
return Err(format!("ACME order not ready: {status:?}").into());
}
let key_pem = order.finalize().await?;
let cert_pem = order.poll_certificate(&RetryPolicy::default()).await?;
tracing::info!(domain, "Certificate obtained successfully");
Ok((cert_pem.into_bytes(), key_pem.into_bytes()))
}