use serde_json::json;
use crate::{
acme::{get_header, AcmeError, Auth, Challenge, ChallengeType, Directory, Identifier, Order},
cache::AcmeCache,
crypto::EcdsaP256SHA256KeyPair,
jose::{jose_req, key_authorization_sha256},
B64_URL_SAFE_NO_PAD,
};
use base64::Engine;
use generic_async_http_client::Response;
#[derive(Debug)]
pub struct Account {
key_pair: EcdsaP256SHA256KeyPair,
directory: Directory,
kid: String,
}
impl Account {
pub async fn load_or_create<'a, C, S, I>(
directory: Directory,
cache: Option<&C>,
contact: I,
) -> Result<Self, AcmeError>
where
C: AcmeCache,
S: AsRef<str> + 'a,
I: IntoIterator<Item = &'a S>,
{
let contact: Vec<&'a str> = contact.into_iter().map(AsRef::<str>::as_ref).collect();
let pkcs8 = match &cache {
Some(cache) => cache
.read_account(&contact)
.await
.map_err(AcmeError::cache)?,
None => None,
};
let key_pair = match pkcs8 {
Some(pkcs8) => {
log::info!("found cached account key");
EcdsaP256SHA256KeyPair::load(&pkcs8)
}
None => {
log::info!("creating a new account key");
match EcdsaP256SHA256KeyPair::generate() {
Ok(pkcs8) => {
let data = pkcs8.as_ref();
if let Some(cache) = &cache {
cache
.write_account(&contact, data)
.await
.map_err(AcmeError::cache)?;
}
EcdsaP256SHA256KeyPair::load(data)
}
Err(_) => Err(()),
}
}
}
.map_err(|_| {
AcmeError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"could not create key pair",
))
})?;
let payload = json!({
"termsOfServiceAgreed": true,
"contact": contact,
})
.to_string();
let response = jose_req(
&key_pair,
None,
&directory.nonce().await?,
&directory.new_account,
&payload,
)
.await?;
let kid = get_header(&response, "Location")?;
Ok(Account {
key_pair,
kid,
directory,
})
}
async fn request(&self, url: impl AsRef<str>, payload: &str) -> Result<Response, AcmeError> {
jose_req(
&self.key_pair,
Some(&self.kid),
&self.directory.nonce().await?,
url.as_ref(),
payload,
)
.await
}
pub async fn new_order(&self, domains: Vec<String>) -> Result<Order, AcmeError> {
let domains: Vec<Identifier> = domains.into_iter().map(Identifier::Dns).collect();
let payload = format!("{{\"identifiers\":{}}}", serde_json::to_string(&domains)?);
let mut response = self.request(&self.directory.new_order, &payload).await?;
Ok(response.json().await?)
}
pub async fn check_auth(&self, url: impl AsRef<str>) -> Result<Auth, AcmeError> {
let payload = "".to_string();
let mut response = self.request(url, &payload).await?;
Ok(response.json().await?)
}
pub async fn trigger_challenge(&self, url: impl AsRef<str>) -> Result<(), AcmeError> {
self.request(&url, "{}").await?;
Ok(())
}
pub async fn send_csr(&self, url: impl AsRef<str>, csr: Vec<u8>) -> Result<Order, AcmeError> {
let payload = format!("{{\"csr\":\"{}\"}}", B64_URL_SAFE_NO_PAD.encode(csr));
let mut response = self.request(&url, &payload).await?;
Ok(response.json().await?)
}
pub async fn obtain_certificate(&self, url: impl AsRef<str>) -> Result<String, AcmeError> {
Ok(self.request(&url, "").await?.text().await?)
}
pub fn tls_alpn_01<'a>(
&self,
challenges: &'a [Challenge],
) -> Result<(&'a Challenge, impl AsRef<[u8]>), AcmeError> {
let challenge = challenges
.iter()
.find(|c| c.typ == ChallengeType::TlsAlpn01);
let challenge = match challenge {
Some(challenge) => challenge,
None => return Err(AcmeError::NoTlsAlpn01Challenge),
};
let key_auth = key_authorization_sha256(&self.key_pair, &*challenge.token)?;
Ok((challenge, key_auth))
}
}