use axum::{
Extension, Json,
extract::{Path, State},
http::{HeaderValue, StatusCode, header},
response::{IntoResponse, Response},
};
use serde::Deserialize;
use serde_json::Value;
use tracing::{error, info, instrument, warn};
use crate::AppState;
use crate::challenge::ValidationContext;
use crate::error::Problem;
use crate::extractors::acme::{AcmeOptionalPayload, AcmeRequest, jwk_thumbprint};
use crate::filter::ClientIp;
use crate::handlers::helpers::{
challenge_problem, load_owned_authz, load_owned_challenge, signer_account,
};
use crate::notify::{ChallengeFailedData, NotifyEvent};
use crate::sqlite::{
authz::{Authorization, Challenge},
db::Database,
nonce::now_secs,
order::Order,
status::{AuthzStatus, ChallengeStatus, OrderStatus},
};
use std::sync::Arc;
#[derive(Debug, Deserialize)]
pub struct AuthzUpdatePayload {
pub status: Option<String>,
}
#[instrument(name = "post_authz", skip_all, fields(authz_id = %id))]
pub async fn post_authz(
State(state): State<AppState>,
Path(id): Path<String>,
AcmeOptionalPayload {
payload,
pubkey,
account,
..
}: AcmeOptionalPayload<AuthzUpdatePayload>,
) -> Result<Response, Problem> {
info!(
event = "authz_lookup_requested",
outcome = "progress",
authz_id = %id,
deactivating = payload.is_some(),
);
let AppState {
database, profile, ..
} = state;
let base = &profile.base_url;
let account = signer_account(account, &profile.name, &pubkey, &database).await?;
let (mut authz, mut order) = load_owned_authz(&id, &account, &database).await?;
if let Some(update) = payload {
if update.status.as_deref() != Some("deactivated") {
warn!(event = "authz_update_unsupported", outcome = "failure", authz_id = %id, status = ?update.status);
return Err(Problem::malformed(
"Only {\"status\": \"deactivated\"} is supported on an authorization",
));
}
deactivate_authz(&mut authz, &mut order, &database).await?;
}
let challenges = Challenge::find_by_authz(authz.id, &database)
.await
.map_err(|error| {
error!(
event = "challenge_list_failed",
outcome = "failure",
authz_id = %id,
error = %error
);
Problem::server_internal("Challenge lookup failed")
})?;
let mut response = Json(authz.to_json(base, &challenges)).into_response();
add_pending_retry_after(&mut response, authz.status.as_str());
Ok(response)
}
const PENDING_RETRY_AFTER: &str = "5";
fn add_pending_retry_after(response: &mut Response, status: &str) {
if status == "pending" || status == "processing" {
response.headers_mut().insert(
header::RETRY_AFTER,
HeaderValue::from_static(PENDING_RETRY_AFTER),
);
}
}
async fn deactivate_authz(
authz: &mut Authorization,
order: &mut Order,
database: &Arc<Database>,
) -> Result<(), Problem> {
if authz.status == AuthzStatus::Deactivated {
return Ok(());
}
if order.status == OrderStatus::Valid {
warn!(event = "authz_deactivate_refused_order_valid", outcome = "failure", authz_id = %authz.id, order_id = %order.id);
return Err(Problem::malformed(
"Cannot deactivate an authorization whose order has already been issued; revoke the certificate instead",
));
}
if authz.status != AuthzStatus::Pending && authz.status != AuthzStatus::Valid {
warn!(event = "authz_deactivate_refused_terminal", outcome = "failure", authz_id = %authz.id, status = %authz.status);
return Err(Problem::malformed(
"Authorization is in a terminal state and cannot be deactivated",
));
}
let demote = order.status == OrderStatus::Ready;
let outcome = async {
let mut tx = database.pool.begin().await?;
Authorization::set_deactivated(authz.id, &mut *tx).await?;
if demote {
Order::set_pending(order.id, &mut *tx).await?;
}
tx.commit().await
}
.await;
outcome.map_err(|error| {
error!(event = "authz_deactivate_failed", outcome = "failure", authz_id = %authz.id, error = %error);
Problem::server_internal("Authorization deactivation failed")
})?;
authz.status = AuthzStatus::Deactivated;
if demote {
order.status = OrderStatus::Pending;
}
info!(event = "authz_deactivated", outcome = "success", authz_id = %authz.id, order_id = %order.id);
Ok(())
}
async fn commit_validation(
challenge: &mut Challenge,
authz: &mut Authorization,
order: &mut Order,
database: &Arc<Database>,
) -> Result<(), Problem> {
let validated = now_secs();
let outcome = async {
let mut tx = database.pool.begin().await?;
Challenge::set_valid(challenge.id, validated, &mut *tx).await?;
Authorization::set_valid(authz.id, &mut *tx).await?;
let promote = order.status == OrderStatus::Pending && {
let authzs = Authorization::find_by_order_with(order.id, &mut *tx).await?;
authzs.len() == order.identifiers.len()
&& authzs
.iter()
.all(|authz| authz.status == AuthzStatus::Valid)
};
if promote {
Order::set_ready(order.id, &mut *tx).await?;
}
tx.commit().await?;
Ok::<bool, sqlx::Error>(promote)
}
.await;
match outcome {
Ok(promoted) => {
challenge.status = ChallengeStatus::Valid;
challenge.validated = Some(validated);
authz.status = AuthzStatus::Valid;
if promoted {
order.status = OrderStatus::Ready;
}
Ok(())
}
Err(error) => {
error!(
event = "challenge_validation_persist_failed",
outcome = "failure",
challenge_id = %challenge.id,
authz_id = %authz.id,
order_id = %order.id,
error = %error
);
Err(Problem::server_internal("Challenge validation failed"))
}
}
}
async fn commit_validation_failure(
challenge: &mut Challenge,
authz: &mut Authorization,
order: &mut Order,
problem: &Value,
database: &Arc<Database>,
) -> Result<(), Problem> {
let outcome = async {
let mut tx = database.pool.begin().await?;
Challenge::set_invalid(challenge.id, problem, &mut *tx).await?;
Authorization::set_invalid(authz.id, &mut *tx).await?;
Order::set_invalid(order.id, problem, &mut *tx).await?;
tx.commit().await
}
.await;
match outcome {
Ok(()) => {
challenge.status = ChallengeStatus::Invalid;
challenge.error = Some(problem.clone());
authz.status = AuthzStatus::Invalid;
order.status = OrderStatus::Invalid;
order.error = Some(problem.clone());
Ok(())
}
Err(error) => {
error!(
event = "challenge_failure_persist_failed",
outcome = "failure",
challenge_id = %challenge.id,
authz_id = %authz.id,
order_id = %order.id,
error = %error
);
Err(Problem::server_internal("Challenge validation failed"))
}
}
}
#[instrument(name = "post_challenge", skip_all, fields(challenge_id = %id))]
pub async fn post_challenge(
State(state): State<AppState>,
Path(id): Path<String>,
Extension(ClientIp(client_ip)): Extension<ClientIp>,
AcmeRequest {
pubkey, account, ..
}: AcmeRequest<Value>,
) -> Result<Response, Problem> {
info!(
event = "challenge_trigger_requested",
outcome = "progress",
challenge_id = %id
);
let AppState {
database, profile, ..
} = state;
let base = &profile.base_url;
let challenges = &profile.challenges;
let account = signer_account(account, &profile.name, &pubkey, &database).await?;
let (mut challenge, mut authz, mut order) =
load_owned_challenge(&id, &account, &database).await?;
if authz.status != AuthzStatus::Valid && authz.expires <= now_secs() {
warn!(event = "authz_expired", outcome = "failure", authz_id = %authz.id, expires = authz.expires);
return Err(Problem::malformed("Authorization has expired"));
}
if authz.status == AuthzStatus::Deactivated {
warn!(event = "authz_already_deactivated", outcome = "failure", authz_id = %authz.id);
return Err(Problem::malformed("Authorization has been deactivated"));
}
let decided = challenge.status == ChallengeStatus::Valid
|| challenge.status == ChallengeStatus::Invalid
|| authz.status == AuthzStatus::Valid;
let claimed = !decided && challenge.claim_for_validation(&database).await.map_err(|error| {
error!(event = "challenge_claim_failed", outcome = "failure", challenge_id = %id, error = %error);
Problem::server_internal("Challenge could not be claimed for validation")
})?;
if claimed {
let thumbprint = jwk_thumbprint(&account.pubkey).map_err(|error| {
error!(event = "authz_thumbprint_failed", outcome = "failure", account_id = %account.id, error = %error);
Problem::server_internal("Key authorization could not be computed")
})?;
let key_authorization = format!("{}.{}", challenge.token, thumbprint);
let challenge_id = challenge.id.to_string();
let context = ValidationContext {
identifier: authz.base_identifier(),
wildcard: authz.is_wildcard(),
token: &challenge.token,
key_authorization: &key_authorization,
challenge_id: &challenge_id,
};
match challenges.validate(&challenge.typ, &context).await {
Ok(()) => {
commit_validation(&mut challenge, &mut authz, &mut order, &database).await?;
}
Err(error) => {
let problem = challenge_problem(&error).to_value();
warn!(
event = "challenge_failed",
outcome = "failure",
challenge_id = %id,
typ = %challenge.typ,
kind = error.kind()
);
commit_validation_failure(
&mut challenge,
&mut authz,
&mut order,
&problem,
&database,
)
.await?;
profile
.notify
.dispatch(NotifyEvent::ChallengeFailed(ChallengeFailedData {
profile: profile.name.clone(),
order_id: order.id.to_string(),
account_id: account.id.to_string(),
authz_id: authz.id.to_string(),
challenge_id: challenge.id.clone().to_string(),
challenge_type: challenge.typ.clone(),
identifier: authz.base_identifier().to_string(),
error: error.kind().to_string(),
client_ip: client_ip.map(|ip| crate::filter::canonical(ip).to_string()),
}))
.await;
}
}
}
info!(
event = "challenge_answered",
outcome = "success",
challenge_id = %id,
authz_id = %authz.id,
order_id = %order.id,
status = %challenge.status
);
let up_link = format!("<{base}/authz/{}>;rel=\"up\"", authz.id);
let mut response = (
StatusCode::OK,
[(header::LINK, up_link)],
Json(challenge.to_json(base)),
)
.into_response();
add_pending_retry_after(&mut response, challenge.status.as_str());
Ok(response)
}