use std::ops::Deref;
use base64ct::Encoding as _;
use chrono::{DateTime, Utc};
use jaws::key::JsonWebKey;
use serde::ser::SerializeMap;
use serde::{ser, Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::protocol::errors::AcmeErrorDocument;
use crate::protocol::Url;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChallengeInfo {
url: Url,
status: ChallengeStatus,
#[serde(default)]
validated: Option<DateTime<Utc>>,
#[serde(default)]
error: Option<AcmeErrorDocument>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase", tag = "type")]
#[non_exhaustive]
pub enum Challenge {
#[serde(rename = "http-01")]
Http01(Http01Challenge),
#[serde(rename = "dns-01")]
Dns01(Dns01Challenge),
#[serde(other)]
UnknownChallenge,
}
impl Challenge {
fn info(&self) -> Option<&ChallengeInfo> {
match self {
Challenge::Http01(http) => Some(&http.info),
Challenge::Dns01(dns) => Some(&dns.info),
_ => None,
}
}
pub fn name(&self) -> Option<&'static str> {
match self {
Challenge::Http01(_) => Some("http-01"),
Challenge::Dns01(_) => Some("dns-01"),
_ => None,
}
}
pub fn kind(&self) -> ChallengeKind {
self.into()
}
pub fn url(&self) -> Option<Url> {
self.info().map(|i| i.url.clone())
}
pub fn status(&self) -> Option<ChallengeStatus> {
self.info().map(|i| i.status)
}
pub fn is_finished(&self) -> bool {
matches!(
self.info().map(|i| i.status),
Some(ChallengeStatus::Valid) | Some(ChallengeStatus::Invalid)
)
}
pub fn is_valid(&self) -> bool {
matches!(self.info().map(|i| i.status), Some(ChallengeStatus::Valid))
}
pub fn validated_at(&self) -> Option<DateTime<Utc>> {
self.info().and_then(|i| i.validated)
}
pub fn error(&self) -> Option<&AcmeErrorDocument> {
self.info().and_then(|i| i.error.as_ref())
}
pub fn http01(&self) -> Option<&Http01Challenge> {
match self {
Challenge::Http01(http) => Some(http),
_ => None,
}
}
pub fn dns01(&self) -> Option<&Dns01Challenge> {
match self {
Challenge::Dns01(dns) => Some(dns),
_ => None,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum ChallengeStatus {
Pending,
Processing,
Valid,
Invalid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ChallengeKind {
#[serde(rename = "http-01")]
Http01,
#[serde(rename = "dns-01")]
Dns01,
#[serde(other)]
Unknown,
}
impl From<&Challenge> for ChallengeKind {
fn from(value: &Challenge) -> Self {
match value {
Challenge::Http01(_) => ChallengeKind::Http01,
Challenge::Dns01(_) => ChallengeKind::Dns01,
_ => ChallengeKind::Unknown,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct KeyAuthorization(String);
impl KeyAuthorization {
fn new<K>(token: &str, key: &K) -> KeyAuthorization
where
K: jaws::key::SerializePublicJWK,
{
let thumb: jaws::key::Thumbprint<Sha256> =
jaws::key::Thumbprint::from_jwk(&JsonWebKey::build_public(key))
.expect("invalid key for thumbprint");
KeyAuthorization(format!("{token}.{thumb}"))
}
pub fn b64digest(&self) -> String {
let digest = sha2::Sha256::digest(self.as_bytes());
base64ct::Base64UrlUnpadded::encode_string(&digest)
}
}
impl Deref for KeyAuthorization {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0.deref()
}
}
impl From<String> for KeyAuthorization {
fn from(value: String) -> Self {
KeyAuthorization(value)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Http01Challenge {
#[serde(flatten)]
info: ChallengeInfo,
token: String,
}
impl Http01Challenge {
pub fn token(&self) -> &str {
&self.token
}
pub fn target_url(&self) -> Url {
format!(".well-known/acme-challenge/{}", self.token)
.parse()
.unwrap()
}
pub fn url(&self) -> Url {
self.info.url.clone()
}
pub fn authorization<K>(&self, account_key: &K) -> KeyAuthorization
where
K: jaws::key::SerializePublicJWK,
{
KeyAuthorization::new(&self.token, account_key)
}
fn info(&self) -> Option<&ChallengeInfo> {
Some(&self.info)
}
pub fn is_finished(&self) -> bool {
matches!(
self.info().map(|i| i.status),
Some(ChallengeStatus::Valid) | Some(ChallengeStatus::Invalid)
)
}
pub fn is_valid(&self) -> bool {
matches!(self.info().map(|i| i.status), Some(ChallengeStatus::Valid))
}
pub fn validated_at(&self) -> Option<DateTime<Utc>> {
self.info().and_then(|i| i.validated)
}
pub fn error(&self) -> Option<&AcmeErrorDocument> {
self.info().and_then(|i| i.error.as_ref())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dns01Challenge {
#[serde(flatten)]
info: ChallengeInfo,
token: String,
}
impl Dns01Challenge {
pub fn token(&self) -> &str {
&self.token
}
pub fn record(&self, domain: &str) -> String {
format!("_acme-challenge.{domain}.")
}
pub fn digest<K>(&self, account_key: &K) -> String
where
K: jaws::key::SerializePublicJWK,
{
self.authorization(account_key).b64digest()
}
pub fn authorization<K>(&self, account_key: &K) -> KeyAuthorization
where
K: jaws::key::SerializePublicJWK,
{
KeyAuthorization::new(&self.token, account_key)
}
}
#[derive(Debug, Default)]
pub struct ChallengeReadyRequest;
impl ser::Serialize for ChallengeReadyRequest {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let map = serializer.serialize_map(Some(0))?;
map.end()
}
}