use std::sync::Arc;
use axum::{
Extension, Json,
extract::{Path, State},
http::{HeaderValue, StatusCode, header},
response::{IntoResponse, Response},
};
use base64::prelude::*;
use serde::Deserialize;
use tracing::{error, info, instrument, warn};
use crate::AppState;
use crate::error::Problem;
use crate::extractors::acme::{AcmePostAsGet, AcmeRequest};
use crate::filter::{ClientIp, IdentifierStage, Stage as FilterStage};
use crate::handlers::helpers::{
check_csr_matches_order, check_identifiers, csr_identifiers, is_wildcard, load_owned_order,
normalize_dns_name, order_authz_ids, parse_csr, parse_rfc3339, signer_account,
well_formed_name,
};
use crate::signer::{IssueOutcome, RequestedValidity, SignerError};
use crate::sqlite::{
authz::{Authorization, Challenge},
db::Database,
nonce::now_secs,
order::{Identifier, Order},
status::OrderStatus,
};
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct NewOrderPayload {
pub identifiers: Vec<Identifier>,
#[serde(rename = "notBefore")]
pub not_before: Option<String>,
#[serde(rename = "notAfter")]
pub not_after: Option<String>,
pub replaces: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct FinalizePayload {
pub csr: String,
}
fn compound_identifier_problem(mut rejections: Vec<Problem>) -> Problem {
if rejections.len() == 1 {
return rejections.remove(0);
}
let status = rejections
.iter()
.map(Problem::status)
.max()
.unwrap_or(StatusCode::BAD_REQUEST);
Problem::compound(status, "Some of the identifiers requested were rejected")
.with_subproblems(rejections)
}
async fn check_replaces(
cert_id: &str,
profile: &str,
account_id: &str,
identifiers: &[Identifier],
database: &Arc<Database>,
) -> Result<String, Problem> {
let parsed = crate::cert::parse_ari_cert_id(cert_id).map_err(|error| {
warn!(event = "replaces_malformed", outcome = "failure", replaces = %cert_id, error = %error);
Problem::malformed(format!("Invalid `replaces` certID: {error}"))
})?;
let predecessor = Order::find_by_cert_serial(profile, &parsed.serial_hex(), database)
.await
.map_err(|error| {
error!(event = "replaces_lookup_failed", outcome = "failure", error = %error);
Problem::server_internal("Predecessor lookup failed")
})?
.ok_or_else(|| {
warn!(event = "replaces_unknown", outcome = "failure", replaces = %cert_id);
Problem::malformed("`replaces` names no certificate issued here")
})?;
if let Some(certificate) = predecessor.certificate.as_ref()
&& let Ok(leaf_der) = crate::cert::leaf_der_from_chain(certificate)
&& let Ok((aki, _)) = crate::cert::ari_cert_id_parts(&leaf_der)
&& aki != parsed.aki
{
warn!(event = "replaces_aki_mismatch", outcome = "failure", replaces = %cert_id);
return Err(Problem::malformed(
"`replaces` key identifier does not match the certificate",
));
}
if predecessor.account_id != account_id {
warn!(event = "replaces_wrong_account", outcome = "failure", replaces = %cert_id, order_id = %predecessor.id);
return Err(Problem::malformed(
"`replaces` names a certificate belonging to another account",
));
}
let shares_identifier = identifiers.iter().any(|wanted| {
predecessor
.identifiers
.iter()
.any(|had| had.typ == wanted.typ && had.value == wanted.value)
});
if !shares_identifier {
warn!(event = "replaces_no_shared_identifier", outcome = "failure", replaces = %cert_id);
return Err(Problem::malformed(
"`replaces` names a certificate sharing no identifier with this order",
));
}
if let Some(existing) = Order::find_by_replaces(profile, cert_id, database)
.await
.map_err(|error| {
error!(event = "replaces_conflict_lookup_failed", outcome = "failure", error = %error);
Problem::server_internal("Replacement lookup failed")
})?
{
warn!(
event = "replaces_already_claimed",
outcome = "failure",
replaces = %cert_id,
existing_order_id = %existing.id,
);
return Err(Problem::already_replaced(
"This certificate has already been marked as replaced by another order",
));
}
info!(event = "replaces_accepted", outcome = "success", replaces = %cert_id, predecessor_order_id = %predecessor.id);
Ok(cert_id.to_string())
}
fn is_replaces_conflict(error: &sqlx::Error) -> bool {
matches!(error, sqlx::Error::Database(db) if db.is_unique_violation()
&& db.message().contains("orders.replaces"))
}
async fn release_claim(order: &mut Order, database: &Database, id: &str) {
if let Err(error) = order.release_finalize_claim(database).await {
error!(
event = "order_finalize_claim_release_failed",
outcome = "failure",
order_id = %id,
error = %error
);
}
}
fn issue_failed(
profile: &str,
account_id: &str,
order: &Order,
client: &crate::audit::ClientContext,
reason: &'static str,
detail: &str,
) -> crate::audit::AuditRecord {
crate::audit::AuditRecord::new(
crate::audit::AuditEvent::CertificateIssueFailed,
profile,
crate::audit::Actor::acme(account_id),
)
.with_order(order)
.with_client(client.clone())
.with_reason(reason)
.with_detail(detail)
}
#[instrument(name = "post_new_order", skip_all, fields(algorithm = %header.alg))]
pub async fn post_new_order(
State(state): State<AppState>,
Extension(ClientIp(client_ip)): Extension<ClientIp>,
request_context: crate::audit::RequestContext,
AcmeRequest {
header,
payload,
pubkey,
account,
}: AcmeRequest<NewOrderPayload>,
) -> Result<Response, Problem> {
info!(
event = "order_creation_requested",
outcome = "progress",
algorithm = %header.alg
);
let AppState {
database,
profile,
audit,
..
} = state;
let base = &profile.base_url;
let challenges = &profile.challenges;
if payload.identifiers.is_empty() {
warn!(event = "order_no_identifiers", outcome = "failure");
return Err(Problem::malformed("No identifiers"));
}
if payload.identifiers.len() > profile.order.max_identifiers {
warn!(
event = "order_too_many_identifiers",
outcome = "failure",
identifiers_count = payload.identifiers.len(),
limit = profile.order.max_identifiers
);
return Err(Problem::malformed(format!(
"An order may name at most {} identifiers; this one names {}",
profile.order.max_identifiers,
payload.identifiers.len()
)));
}
if let Some(bad) = payload.identifiers.iter().find(|id| id.typ != "dns") {
warn!(event = "order_identifier_type_unsupported", outcome = "failure", typ = %bad.typ);
return Err(Problem::unsupported_identifier(
"Only dns identifiers supported",
));
}
let mut identifiers = payload.identifiers;
for identifier in &mut identifiers {
identifier.value = normalize_dns_name(&identifier.value);
}
let rejections: Vec<Problem> = identifiers
.iter()
.filter_map(|identifier| {
if !well_formed_name(&identifier.value) {
warn!(event = "order_identifier_malformed", outcome = "failure", value = %identifier.value);
Some(
Problem::malformed(format!(
"Malformed identifier {}: `*` is only legal as a single leading `*.`",
identifier.value
))
.with_identifier(identifier),
)
} else if challenges
.types_for(is_wildcard(&identifier.value))
.is_empty()
{
warn!(event = "order_identifier_wildcard_rejected", outcome = "failure", value = %identifier.value);
Some(
Problem::rejected_identifier(format!(
"Wildcard identifier {} requires the dns-01 challenge, which is not enabled",
identifier.value
))
.with_identifier(identifier),
)
} else {
None
}
})
.collect();
if !rejections.is_empty() {
return Err(compound_identifier_problem(rejections));
}
let not_before = match payload.not_before {
Some(ref s) => Some(parse_rfc3339("notBefore", s)?),
None => None,
};
let not_after = match payload.not_after {
Some(ref s) => Some(parse_rfc3339("notAfter", s)?),
None => None,
};
let account = signer_account(account, &profile.name, &pubkey, &database).await?;
check_identifiers(
&profile.filter,
client_ip,
&account.id,
&profile.name,
IdentifierStage::NewOrder,
&identifiers,
&database,
)
.await?;
let replaces = match payload.replaces {
Some(ref cert_id) => Some(
check_replaces(cert_id, &profile.name, &account.id, &identifiers, &database).await?,
),
None => None,
};
let expires = now_secs() + profile.order.validity_seconds as i64;
let client = audit.client(&request_context).await;
let mut order = Order::new(
&profile.name,
&account.id,
identifiers,
expires,
not_before,
not_after,
)
.with_client(&client);
order.replaces = replaces;
let mut authz_ids = Vec::with_capacity(order.identifiers.len());
let persisted = async {
let mut tx = database.pool.begin().await?;
order.insert(&mut *tx).await?;
for identifier in &order.identifiers {
let authz = Authorization::new(&order.id, identifier.clone(), order.expires);
authz.insert(&mut *tx).await?;
for typ in challenges.types_for(is_wildcard(&identifier.value)) {
Challenge::new(&authz.id, typ).insert(&mut *tx).await?;
}
authz_ids.push(authz.id);
}
tx.commit().await
}
.await;
persisted.map_err(|error| {
if is_replaces_conflict(&error) {
warn!(event = "replaces_claim_race_lost", outcome = "failure", account_id = %account.id);
return Problem::already_replaced(
"This certificate has already been marked as replaced by another order",
);
}
error!(
event = "order_creation_failed",
outcome = "failure",
error = %error,
account_id = %account.id
);
Problem::server_internal("Order persistence failed")
})?;
let location = format!("{base}/order/{}", order.id);
info!(
event = "order_created",
outcome = "success",
order_id = %order.id,
account_id = %account.id,
identifiers_count = order.identifiers.len()
);
Ok((
StatusCode::CREATED,
[(header::LOCATION, location)],
Json(order.to_json(base, &authz_ids)),
)
.into_response())
}
#[instrument(name = "post_order", skip_all, fields(order_id = %id))]
pub async fn post_order(
State(state): State<AppState>,
Path(id): Path<String>,
AcmePostAsGet {
pubkey, account, ..
}: AcmePostAsGet,
) -> Result<Response, Problem> {
info!(
event = "order_lookup_requested",
outcome = "progress",
order_id = %id
);
let AppState {
database, profile, ..
} = state;
let base = &profile.base_url;
let account = signer_account(account, &profile.name, &pubkey, &database).await?;
let order = load_owned_order(&id, &account, &database).await?;
let authz_ids = order_authz_ids(&order.id, &database).await?;
Ok(order_response(&order, base, &authz_ids))
}
const PROCESSING_RETRY_AFTER: &str = "5";
fn order_response(order: &Order, base: &str, authz_ids: &[String]) -> Response {
let mut response = Json(order.to_json(base, authz_ids)).into_response();
if order.status == OrderStatus::Processing {
response.headers_mut().insert(
header::RETRY_AFTER,
HeaderValue::from_static(PROCESSING_RETRY_AFTER),
);
}
response
}
#[instrument(name = "post_finalize", skip_all, fields(order_id = %id))]
pub async fn post_finalize(
State(state): State<AppState>,
Path(id): Path<String>,
Extension(ClientIp(client_ip)): Extension<ClientIp>,
request_context: crate::audit::RequestContext,
AcmeRequest {
payload,
pubkey,
account,
..
}: AcmeRequest<FinalizePayload>,
) -> Result<Response, Problem> {
info!(
event = "order_finalize_requested",
outcome = "progress",
order_id = %id
);
let AppState {
database,
profile,
audit,
..
} = state;
let base = &profile.base_url;
let (signer, filter) = (&profile.signer, &profile.filter);
let account = signer_account(account, &profile.name, &pubkey, &database).await?;
let mut order = load_owned_order(&id, &account, &database).await?;
if order.status != OrderStatus::Ready {
warn!(event = "order_finalize_not_ready", outcome = "failure", order_id = %id, status = %order.status);
return Err(Problem::order_not_ready("Order is not ready"));
}
let client = audit.client(&request_context).await;
let failed = |order: &Order, reason: &'static str, detail: &str| {
issue_failed(&profile.name, &account.id, order, &client, reason, detail)
};
let csr_der = match BASE64_URL_SAFE_NO_PAD.decode(&payload.csr) {
Ok(der) => der,
Err(_) => {
audit
.record(failed(&order, "badCSR", "CSR base64 invalid"))
.await;
return Err(Problem::bad_csr("CSR base64 invalid"));
}
};
let csr = match parse_csr(&csr_der) {
Ok(csr) => csr,
Err(problem) => {
audit
.record(failed(&order, "badCSR", "CSR is unparsable"))
.await;
return Err(problem);
}
};
if let Err(problem) = check_csr_matches_order(&csr, &order.identifiers) {
audit
.record(failed(
&order,
"badCSR",
"CSR identifiers do not match the order",
))
.await;
return Err(problem);
}
if filter.has_rules_at(FilterStage::Identifiers) {
let requested = csr_identifiers(&csr);
if let Err(problem) = check_identifiers(
filter,
client_ip,
&order.account_id,
&profile.name,
IdentifierStage::Csr,
&requested,
&database,
)
.await
{
let (reason, detail) = if problem.status() == StatusCode::BAD_REQUEST {
("badCSR", "the filter policy refused the CSR identifiers")
} else {
(
"serverInternal",
"the filter policy could not be evaluated for the CSR identifiers",
)
};
audit.record(failed(&order, reason, detail)).await;
return Err(problem);
}
}
let validity = RequestedValidity {
not_before: order.not_before,
not_after: order.not_after,
};
match order.claim_for_finalize(&database).await {
Ok(true) => {}
Ok(false) => {
warn!(
event = "order_finalize_claim_refused",
outcome = "failure",
order_id = %id
);
return Err(Problem::order_not_ready("Order is already being finalized"));
}
Err(error) => {
error!(
event = "order_mark_processing_failed",
outcome = "failure",
order_id = %id,
error = %error
);
return Err(Problem::server_internal("Order finalize failed"));
}
}
let issued = signer
.issue(&order.id, &csr_der, &order.identifiers, validity)
.await;
let chain = match issued {
Ok(IssueOutcome::Issued(chain)) => chain,
Ok(IssueOutcome::Processing) => {
if let Err(error) =
crate::sqlite::upstream_order::UpstreamOrder::set_client(&id, &client, &database)
.await
{
warn!(
event = "upstream_order_client_context_failed",
outcome = "failure",
order_id = %id,
error = %error
);
}
let authz_ids = order_authz_ids(&order.id, &database).await?;
info!(event = "order_finalize_delegated", outcome = "success", order_id = %id);
return Ok(order_response(&order, base, &authz_ids));
}
Err(SignerError::BadCsr) => {
warn!(
event = "order_finalize_bad_csr",
outcome = "failure",
order_id = %id
);
release_claim(&mut order, &database, &id).await;
audit
.record(failed(
&order,
"badCSR",
"the signer backend rejected the CSR",
))
.await;
return Err(Problem::bad_csr("CSR invalid or does not match order"));
}
Err(SignerError::Internal(detail)) => {
error!(
event = "order_finalize_issuance_failed",
outcome = "failure",
order_id = %id,
detail = %detail
);
audit
.record(failed(&order, "serverInternal", &detail))
.await;
let problem = Problem::server_internal("Certificate issuance failed");
if let Err(error) = order.mark_invalid(problem.to_value(), &database).await {
error!(
event = "order_mark_invalid_failed",
outcome = "failure",
order_id = %id,
error = %error
);
}
return Err(problem);
}
};
let leaf_der = match crate::cert::leaf_der_from_chain(&chain) {
Ok(der) => der,
Err(error) => {
error!(event = "order_finalize_chain_unparsable", outcome = "failure", order_id = %id, error = %error);
release_claim(&mut order, &database, &id).await;
return Err(Problem::server_internal(
"Issued certificate chain is unparsable",
));
}
};
let (cert_serial, cert_pubkey) = match crate::cert::cert_serial_and_spki(&leaf_der) {
Ok(parts) => parts,
Err(error) => {
error!(event = "order_finalize_leaf_unparsable", outcome = "failure", order_id = %id, error = %error);
release_claim(&mut order, &database, &id).await;
return Err(Problem::server_internal("Issued certificate is unparsable"));
}
};
let cert_not_after = crate::cert::cert_validity(&leaf_der)
.ok()
.map(|(_, not_after)| not_after);
order
.finalize(
chain,
cert_serial.clone(),
cert_pubkey,
cert_not_after,
&database,
)
.await
.map_err(|error| {
error!(
event = "order_finalize_persistence_failed",
outcome = "failure",
order_id = %id,
error = %error
);
Problem::server_internal("Order finalize failed")
})?;
let authz_ids = order_authz_ids(&order.id, &database).await?;
info!(
event = "order_finalized",
outcome = "success",
order_id = %id,
cert_serial = %cert_serial
);
audit
.record(
crate::audit::AuditRecord::new(
crate::audit::AuditEvent::CertificateIssued,
&profile.name,
crate::audit::Actor::acme(&account.id),
)
.with_order(&order)
.with_client(client)
.with_serial(cert_serial.clone()),
)
.await;
profile
.notify
.dispatch(crate::notify::NotifyEvent::CertificateIssued(
crate::notify::CertificateIssuedData {
profile: profile.name.clone(),
order_id: order.id.clone(),
account_id: order.account_id.clone(),
cert_serial: cert_serial.clone(),
identifiers: order.identifiers.iter().map(|i| i.value.clone()).collect(),
client_ip: client_ip.map(|ip| crate::filter::canonical(ip).to_string()),
},
))
.await;
Ok(order_response(&order, base, &authz_ids))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sqlite::db::Database;
#[tokio::test]
async fn a_replaces_collision_is_told_apart_from_other_unique_violations() {
let database = Database::connect_in_memory().await.unwrap();
sqlx::query(
"INSERT INTO accounts (id, profile, pubkey, contact, status, created_at) \
VALUES ('acct', 'default', X'00', '[]', 'valid', 0);",
)
.execute(&database.pool)
.await
.unwrap();
let order = |id: &'static str, replaces: &'static str| {
let pool = database.pool.clone();
async move {
sqlx::query(
"INSERT INTO orders (id, profile, account_id, status, identifiers, expires, \
replaces, created_at) VALUES (?, 'default', 'acct', 'pending', '[]', 0, ?, 0);",
)
.bind(id)
.bind(replaces)
.execute(&pool)
.await
}
};
order("first", "predecessor-cert-id").await.unwrap();
let collision = order("second", "predecessor-cert-id").await.unwrap_err();
assert!(is_replaces_conflict(&collision), "got {collision}");
let authz = |id: &'static str| {
let pool = database.pool.clone();
async move {
sqlx::query(
"INSERT INTO authorizations (id, order_id, identifier, status, expires, \
created_at) VALUES (?, 'first', '{\"type\":\"dns\",\"value\":\"a.example.com\"}', \
'pending', 0, 0);",
)
.bind(id)
.execute(&pool)
.await
}
};
authz("authz-one").await.unwrap();
let other = authz("authz-two").await.unwrap_err();
assert!(
!is_replaces_conflict(&other),
"an authorization collision must not read as alreadyReplaced: {other}"
);
let missing = sqlx::query("INSERT INTO orders (id) VALUES ('x');")
.execute(&database.pool)
.await
.unwrap_err();
assert!(!is_replaces_conflict(&missing));
}
}