use std::sync::Arc;
use uuid::Uuid;
use axum::{
Extension, Json,
extract::{Path, State},
http::{StatusCode, header},
response::{IntoResponse, Response},
};
use serde::Deserialize;
use serde_json::{Value, json};
use tracing::{error, info, instrument, warn};
use crate::AppState;
use crate::eab;
use crate::error::Problem;
use crate::extractors::acme::{AcmePostAsGet, AcmeRequest, ProtectedHeader, spki_to_jwk};
use crate::filter::ClientIp;
use crate::handlers::helpers::{signer_account, validate_contacts};
use crate::key_change;
use crate::notify::{AccountCreatedData, AccountDeactivatedData, NotifyEvent};
use crate::sqlite::{
account::{Account, pubkey_fingerprint},
db::Database,
eab::Eab,
order::Order,
};
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct NewAccountPayload {
pub contact: Vec<String>,
#[serde(alias = "termsOfServiceAgreed")]
pub terms_of_service_agreed: bool,
#[serde(alias = "onlyReturnExisting")]
pub only_return_existing: bool,
#[serde(alias = "externalAccountBinding")]
pub external_account_binding: Option<eab::EabJws>,
}
fn refuse_deactivated(account: &Account, only_return_existing: bool) -> Result<(), Problem> {
if account.status != "deactivated" {
return Ok(());
}
warn!(
event = "account_deactivated_registration_refused",
outcome = "failure",
account_id = %account.id,
only_return_existing = only_return_existing
);
Err(Problem::unauthorized("Account is deactivated"))
}
#[instrument(name = "post_new_account", skip_all, fields(algorithm = %header.alg))]
pub async fn post_new_account(
State(state): State<AppState>,
Extension(ClientIp(client_ip)): Extension<ClientIp>,
request_context: crate::audit::RequestContext,
AcmeRequest {
header,
payload,
pubkey,
..
}: AcmeRequest<NewAccountPayload>,
) -> Result<Response, Problem> {
info!(
event = "account_creation_requested",
outcome = "progress",
algorithm = %header.alg
);
let AppState {
database,
profile,
audit,
..
} = state;
let base = &profile.base_url;
if payload.only_return_existing {
let account = Account::find_by_pubkey(&profile.name, &pubkey, &database)
.await
.map_err(|error| {
error!(event = "account_only_return_existing_lookup_failed", outcome = "failure", error = %error);
Problem::server_internal("Account lookup failed")
})?
.ok_or_else(|| {
info!(event = "account_only_return_existing_miss", outcome = "failure");
Problem::account_does_not_exist("No account for this key")
})?;
refuse_deactivated(&account, true)?;
let location = format!("{base}/acct/{}", account.id);
return Ok((
StatusCode::OK,
[(header::LOCATION, location)],
Json(account.to_json(base)),
)
.into_response());
}
validate_contacts(&payload.contact)?;
if !profile.meta.terms_of_service.is_empty() && !payload.terms_of_service_agreed {
warn!(event = "account_terms_not_agreed", outcome = "failure");
let problem = Problem::user_action_required(
"Terms of service must be agreed to before an account can be created",
);
return Ok((
StatusCode::FORBIDDEN,
[
(header::CONTENT_TYPE, "application/problem+json".to_string()),
(
header::LINK,
format!(
"<{}>;rel=\"terms-of-service\"",
profile.meta.terms_of_service
),
),
],
Json(problem.to_value()),
)
.into_response());
}
let eab_kid = if profile.eab.enabled {
Some(
verify_eab(
payload.external_account_binding.as_ref(),
&header,
&profile.name,
&database,
)
.await?,
)
} else {
None
};
let client = audit.client(&request_context).await;
let (mut account, created) =
Account::find_or_create(&profile.name, &pubkey, payload.contact, &client, &database)
.await
.map_err(|error| {
error!(event = "account_creation_failed", outcome = "failure", error = %error);
Problem::server_internal("Account persistence failed")
})?;
if !created {
refuse_deactivated(&account, false)?;
}
if created {
if let Some(kid) = &eab_kid
&& let Err(error) = account.set_eab_kid(*kid, &database).await
{
error!(event = "account_eab_kid_persist_failed", outcome = "failure", account_id = %account.id, error = %error);
}
if !profile.meta.terms_of_service.is_empty()
&& let Err(error) = account.set_terms_agreed(&database).await
{
error!(event = "account_terms_agreed_persist_failed", outcome = "failure", account_id = %account.id, error = %error);
}
profile
.notify
.dispatch(NotifyEvent::AccountCreated(AccountCreatedData {
profile: profile.name.clone(),
account_id: account.id.to_string(),
contact: account.contact.clone(),
client_ip: client_ip.map(|ip| crate::filter::canonical(ip).to_string()),
}))
.await;
}
let status = if created {
StatusCode::CREATED
} else {
StatusCode::OK
};
let location = format!("{base}/acct/{}", account.id);
info!(
event = "account_created",
outcome = "success",
account_id = %account.id,
created = created,
status = %status
);
Ok((
status,
[(header::LOCATION, location)],
Json(account.to_json(base)),
)
.into_response())
}
#[instrument(name = "verify_eab", skip_all)]
pub async fn verify_eab(
eab_jws: Option<&eab::EabJws>,
header: &ProtectedHeader,
profile: &str,
database: &Arc<Database>,
) -> Result<Uuid, Problem> {
let eab_jws = eab_jws.ok_or_else(|| {
warn!(event = "eab_required", outcome = "failure", profile);
Problem::external_account_required("This server requires External Account Binding")
})?;
let outer_jwk = header.jwk.as_ref().ok_or_else(|| {
warn!(event = "eab_missing_jwk", outcome = "failure", profile);
Problem::malformed("newAccount requires an embedded jwk for External Account Binding")
})?;
let eab_header = eab::parse_header(eab_jws, &header.url).map_err(eab::eab_problem)?;
let key = Eab::find_by_kid(&eab_header.kid, profile, database)
.await
.map_err(|error| {
error!(event = "eab_lookup_failed", outcome = "failure", kid = %eab_header.kid, error = %error);
Problem::server_internal("External Account Binding lookup failed")
})?
.filter(Eab::is_active)
.ok_or_else(|| {
warn!(event = "eab_unknown_or_revoked_kid", outcome = "failure", kid = %eab_header.kid);
Problem::unauthorized("Unknown or revoked External Account Binding key")
})?;
eab::verify_payload_and_signature(eab_jws, &key.secret, outer_jwk).map_err(eab::eab_problem)?;
info!(event = "eab_verified", outcome = "success", kid = %key.kid);
Ok(key.kid)
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct UpdateAccountPayload {
pub contact: Option<Vec<String>>,
pub status: Option<String>,
}
#[instrument(name = "post_account", skip_all, fields(account_id = %id))]
pub async fn post_account(
State(state): State<AppState>,
Path(id): Path<String>,
Extension(ClientIp(client_ip)): Extension<ClientIp>,
AcmeRequest {
header,
payload,
pubkey,
..
}: AcmeRequest<UpdateAccountPayload>,
) -> Result<Json<Value>, Problem> {
info!(
event = "account_update_requested",
outcome = "progress",
account_id = %id,
algorithm = %header.alg
);
let AppState {
database, profile, ..
} = state;
let base = &profile.base_url;
let mut account = match Account::find_by_id(&profile.name, &id, &database).await {
Ok(Some(account)) => account,
Ok(None) => {
warn!(event = "account_not_found", outcome = "failure", account_id = %id);
return Err(Problem::account_does_not_exist("Unknown account"));
}
Err(error) => {
error!(
event = "account_update_lookup_failed",
outcome = "failure",
account_id = %id,
error = %error
);
return Err(Problem::server_internal("Account lookup failed"));
}
};
if account.pubkey != pubkey {
warn!(
event = "account_key_mismatch",
outcome = "failure",
account_id = %id,
expected_pubkey_fp = %pubkey_fingerprint(&account.pubkey),
actual_pubkey_fp = %pubkey_fingerprint(&pubkey)
);
return Err(Problem::unauthorized("Signed by a different account key"));
}
if account.status == "deactivated" {
warn!(
event = "account_deactivated_modify_refused",
outcome = "failure",
account_id = %id
);
return Err(Problem::unauthorized("Account deactivated"));
}
if let Some(status) = payload.status {
if status != "deactivated" {
warn!(event = "account_update_bad_status", outcome = "failure", account_id = %id, status = %status);
return Err(Problem::malformed("Only 'deactivated' status is accepted"));
}
account.deactivate(&database).await.map_err(|error| {
error!(
event = "account_deactivation_failed",
outcome = "failure",
account_id = %id,
error = %error
);
Problem::server_internal("Account update failed")
})?;
info!(
event = "account_deactivated",
outcome = "success",
account_id = %id
);
profile
.notify
.dispatch(NotifyEvent::AccountDeactivated(AccountDeactivatedData {
profile: profile.name.clone(),
account_id: id.clone(),
client_ip: client_ip.map(|ip| crate::filter::canonical(ip).to_string()),
}))
.await;
} else if let Some(contact) = payload.contact {
validate_contacts(&contact)?;
account
.update_contact(contact, &database)
.await
.map_err(|error| {
error!(
event = "account_contact_update_failed",
outcome = "failure",
account_id = %id,
error = %error
);
Problem::server_internal("Account update failed")
})?;
info!(
event = "account_contact_updated",
outcome = "success",
account_id = %id
);
}
info!(
event = "account_updated",
outcome = "success",
account_id = %id
);
Ok(Json(account.to_json(base)))
}
#[instrument(name = "post_key_change", skip_all)]
pub async fn post_key_change(
State(state): State<AppState>,
AcmeRequest {
header,
payload: inner_jws,
pubkey: old_pubkey,
account,
..
}: AcmeRequest<key_change::KeyChangeJws>,
) -> Result<Response, Problem> {
info!(event = "key_change_requested", outcome = "progress",);
let AppState {
database, profile, ..
} = state;
let base = &profile.base_url;
let mut old_account = signer_account(account, &profile.name, &old_pubkey, &database).await?;
let inner_header = key_change::parse_header(&inner_jws, &header.url)
.map_err(key_change::key_change_problem)?;
let new_pubkey = key_change::verify_signature(&inner_jws, &inner_header)
.map_err(key_change::key_change_problem)?;
let account_url = format!("{base}/acct/{}", old_account.id);
let old_key_jwk = spki_to_jwk(&old_account.pubkey).map_err(|error| {
error!(event = "key_change_old_key_decode_failed", outcome = "failure", account_id = %old_account.id, error = %error);
Problem::server_internal("Stored account key could not be decoded")
})?;
key_change::verify_payload(&inner_jws, &account_url, &old_key_jwk)
.map_err(key_change::key_change_problem)?;
let conflicting_account =
Account::find_by_pubkey(&profile.name, &new_pubkey, &database)
.await
.map_err(|error| {
error!(event = "key_change_conflict_lookup_failed", outcome = "failure", account_id = %old_account.id, error = %error);
Problem::server_internal("Account lookup failed")
})?;
if let Some(existing) = conflicting_account {
warn!(event = "key_change_conflict", outcome = "failure", account_id = %old_account.id, conflicting_account_id = %existing.id);
return Ok(key_change_conflict(base, existing.id.to_string().as_str()));
}
if let Err(error) = old_account.update_pubkey(&new_pubkey, &database).await {
if crate::sqlite::account::is_pubkey_conflict(&error)
&& let Ok(Some(winner)) =
Account::find_by_pubkey(&profile.name, &new_pubkey, &database).await
{
warn!(event = "key_change_conflict", outcome = "failure", account_id = %old_account.id, conflicting_account_id = %winner.id);
return Ok(key_change_conflict(base, winner.id.to_string().as_str()));
}
error!(event = "key_change_persist_failed", outcome = "failure", account_id = %old_account.id, error = %error);
return Err(Problem::server_internal("Account key update failed"));
}
info!(event = "account_key_changed", outcome = "success", account_id = %old_account.id);
Ok(Json(old_account.to_json(base)).into_response())
}
fn key_change_conflict(base: &str, holder_id: &str) -> Response {
let problem =
Problem::key_change_conflict("The new key is already associated with a different account");
(
StatusCode::CONFLICT,
[
(header::CONTENT_TYPE, "application/problem+json".to_string()),
(header::LOCATION, format!("{base}/acct/{holder_id}")),
],
Json(problem.to_value()),
)
.into_response()
}
#[instrument(name = "post_account_orders", skip_all, fields(account_id = %id))]
pub async fn post_account_orders(
State(state): State<AppState>,
Path(id): Path<String>,
AcmePostAsGet {
pubkey, account, ..
}: AcmePostAsGet,
) -> Result<Json<Value>, Problem> {
info!(
event = "account_orders_requested",
outcome = "progress",
account_id = %id
);
let AppState {
database, profile, ..
} = state;
let base = &profile.base_url;
let account = signer_account(account, &profile.name, &pubkey, &database).await?;
if account.id.to_string() != id {
warn!(
event = "account_orders_ownership_mismatch",
outcome = "failure",
requested = %id,
signer = %account.id
);
return Err(Problem::unauthorized("Not your account"));
}
let orders = Order::find_active_by_account(account.id, &database)
.await
.map_err(|error| {
error!(
event = "account_orders_lookup_failed",
outcome = "failure",
account_id = %id,
error = %error
);
Problem::server_internal("Order list failed")
})?;
let urls: Vec<Value> = orders
.iter()
.map(|o| Value::String(format!("{base}/order/{}", o.id)))
.collect();
Ok(Json(json!({ "orders": urls })))
}