use std::time::Duration;
use axum::extract::{FromRef, FromRequest, Request};
use axum::http::StatusCode;
use base64::prelude::*;
use serde::de::DeserializeOwned;
use tracing::{Span, debug, error, instrument, warn};
use crate::AppState;
use crate::error::Problem;
use crate::sqlite::account::Account;
use crate::sqlite::nonce::Nonce;
pub use crate::extractors::jws::*;
pub use crate::extractors::signature::*;
pub struct AcmeRequest<T> {
pub header: ProtectedHeader,
pub payload: T,
pub pubkey: Vec<u8>,
pub account: Option<Account>,
}
const JOSE_JSON: &str = "application/jose+json";
fn is_jose_json(content_type: Option<&str>) -> bool {
content_type.is_some_and(|value| {
let essence = value.split(';').next().unwrap_or_default().trim();
essence.eq_ignore_ascii_case(JOSE_JSON)
})
}
#[instrument(
name = "verify_jws",
skip_all,
fields(
path = %req.uri().path(),
alg = tracing::field::Empty,
account_id = tracing::field::Empty,
)
)]
async fn verify_jws<S>(
req: Request,
state: &S,
) -> Result<(ProtectedHeader, Vec<u8>, Option<Account>, String), Problem>
where
S: Send + Sync,
AppState: FromRef<S>,
{
let app = AppState::from_ref(state);
let request_path = req.uri().path().to_string();
let request_context = crate::audit::RequestContext::from_request(&req);
debug!(event = "jws_request_received", outcome = "progress", path = %request_path);
let content_type = req
.headers()
.get(axum::http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok());
if !is_jose_json(content_type) {
warn!(event = "jws_bad_content_type", outcome = "failure", content_type = ?content_type, path = %request_path);
return Err(Problem::unsupported_media_type(
"Content-Type must be application/jose+json",
));
}
let body_str = String::from_request(req, state)
.await
.map_err(|rejection| {
if rejection.status() == StatusCode::PAYLOAD_TOO_LARGE {
warn!(event = "jws_body_too_large", outcome = "failure", path = %request_path);
return Problem::payload_too_large("Request body exceeds the configured limit");
}
warn!(event = "jws_body_read_failed", outcome = "failure", path = %request_path);
Problem::malformed("Cannot read HTTP Body")
})?;
let jws: AcmeJwsRequest = serde_json::from_str(&body_str).map_err(|_| {
warn!(event = "jws_json_parse_failed", outcome = "failure", body_bytes = body_str.len(), path = %request_path);
Problem::malformed("JSON JWS format invalid")
})?;
let protected_bytes = BASE64_URL_SAFE_NO_PAD.decode(&jws.protected).map_err(|_| {
warn!(event = "jws_protected_decode_failed", outcome = "failure", protected_b64_chars = jws.protected.len(), path = %request_path);
Problem::malformed("Base64 protected invalid")
})?;
let header: ProtectedHeader = serde_json::from_slice(&protected_bytes).map_err(|_| {
warn!(event = "jws_header_parse_failed", outcome = "failure", protected_bytes = protected_bytes.len(), path = %request_path);
Problem::malformed("JSON protected invalid")
})?;
Span::current().record("alg", header.alg.as_str());
if let Some(crit) = &header.crit {
warn!(event = "jws_crit_header_present", outcome = "failure", extensions = ?crit, url = %header.url);
return Err(Problem::malformed(
"Unsupported critical JWS header extension",
));
}
let expected_url = format!("{}{request_path}", app.profile.base_url);
if header.url != expected_url {
warn!(event = "jws_url_mismatch", outcome = "failure", url = %header.url, expected = %expected_url);
return Err(Problem::malformed("URL invalid"));
}
let signing_input = format!("{}.{}", jws.protected, jws.payload);
let mut signer_account: Option<Account> = None;
let pubkey = match (&header.jwk, &header.kid) {
(Some(_), Some(_)) => {
warn!(event = "jws_jwk_and_kid_both_present", outcome = "failure", url = %header.url, algorithm = %header.alg);
return Err(Problem::malformed("jwk and kid are mutually exclusive"));
}
(Some(_), None) => {
debug!(event = "jws_jwk_verification_started", outcome = "progress", algorithm = %header.alg, url = %header.url);
verify_signature_and_get_der(&header, &signing_input, &jws.signature)
.map_err(map_signature_error)?
}
(None, Some(kid)) => {
debug!(event = "jws_kid_verification_started", outcome = "progress", algorithm = %header.alg, kid = %kid);
let base = &app.profile.base_url;
let id = kid
.strip_prefix(&format!("{base}/acct/"))
.ok_or_else(|| {
warn!(event = "jws_kid_prefix_mismatch", outcome = "failure", kid = %kid, expected_prefix = %format!("{base}/acct/"));
Problem::malformed("kid invalid")
})?;
Span::current().record("account_id", id);
let account = Account::find_by_id(&app.profile.name, id, &app.database)
.await
.map_err(|error| {
error!(event = "jws_kid_account_lookup_failed", outcome = "failure", account_id = %id, error = %error);
Problem::server_internal("Account lookup failed")
})?
.ok_or_else(|| Problem::account_does_not_exist("Unknown account"))?;
verify_signature_with_spki(
&header.alg,
&account.pubkey,
&signing_input,
&jws.signature,
)
.map_err(map_signature_error)?;
let pubkey = account.pubkey.clone();
signer_account = Some(account);
pubkey
}
(None, None) => {
warn!(event = "jws_jwk_and_kid_missing", outcome = "failure", url = %header.url, algorithm = %header.alg);
return Err(Problem::malformed("missing jwk or kid"));
}
};
let ttl = Duration::from_secs(app.config.nonce.ttl_seconds);
match Nonce::verify(&header.nonce, &app.database, ttl).await {
Ok(true) => {}
Ok(false) => {
warn!(
event = "nonce_replayed",
outcome = "failure",
nonce_fp = %crate::sqlite::nonce::fingerprint(&header.nonce),
path = %request_path
);
return Err(Problem::bad_nonce("Nonce invalid"));
}
Err(error) => {
error!(
event = "nonce_verification_failed",
outcome = "failure",
nonce_fp = %crate::sqlite::nonce::fingerprint(&header.nonce),
error = %error
);
return Err(Problem::server_internal("Nonce verification failed"));
}
}
if let Some(account) = signer_account.as_mut() {
touch_account(account, &request_context, &app).await;
}
debug!(
event = "jws_request_validated",
outcome = "success",
algorithm = %header.alg,
url = %header.url,
signature_type = match (&header.jwk, &header.kid) {
(Some(_), None) => "jwk",
(None, Some(_)) => "kid",
_ => "unknown",
},
signature_b64_chars = jws.signature.len()
);
Ok((header, pubkey, signer_account, jws.payload))
}
async fn touch_account(
account: &mut Account,
request: &crate::audit::RequestContext,
app: &AppState,
) {
let ip = request
.ip
.map(crate::filter::canonical)
.map(|ip| ip.to_string());
if !account.needs_touch(crate::sqlite::nonce::now_secs(), ip.as_deref()) {
return;
}
let client = app.audit.client(request).await;
if let Err(error) = account.touch(&client, &app.database).await {
warn!(
event = "account_touch_failed",
outcome = "failure",
account_id = %account.id,
error = %error
);
}
}
impl<S, T> FromRequest<S> for AcmeRequest<T>
where
S: Send + Sync,
AppState: FromRef<S>,
T: DeserializeOwned,
{
type Rejection = Problem;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let (header, pubkey, account, payload_b64) = verify_jws(req, state).await?;
let payload_bytes = BASE64_URL_SAFE_NO_PAD.decode(&payload_b64).map_err(|_| {
warn!(
event = "jws_payload_decode_failed",
outcome = "failure",
payload_b64_chars = payload_b64.len()
);
Problem::malformed("Base64 payload invalid")
})?;
let payload: T = serde_json::from_slice(&payload_bytes).map_err(|_| {
warn!(
event = "jws_payload_parse_failed",
outcome = "failure",
payload_bytes = payload_bytes.len()
);
Problem::malformed("Payload JSON invalid for this endpoint")
})?;
Ok(AcmeRequest {
header,
payload,
pubkey,
account,
})
}
}
pub struct AcmePostAsGet {
pub header: ProtectedHeader,
pub pubkey: Vec<u8>,
pub account: Option<Account>,
}
impl<S> FromRequest<S> for AcmePostAsGet
where
S: Send + Sync,
AppState: FromRef<S>,
{
type Rejection = Problem;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let (header, pubkey, account, payload_b64) = verify_jws(req, state).await?;
if !payload_b64.is_empty() {
warn!(
event = "jws_post_as_get_payload_not_empty",
outcome = "failure",
payload_b64_chars = payload_b64.len(),
url = %header.url
);
return Err(Problem::malformed("POST-as-GET payload must be empty"));
}
Ok(AcmePostAsGet {
header,
pubkey,
account,
})
}
}
pub struct AcmeOptionalPayload<T> {
pub header: ProtectedHeader,
pub payload: Option<T>,
pub pubkey: Vec<u8>,
pub account: Option<Account>,
}
impl<S, T> FromRequest<S> for AcmeOptionalPayload<T>
where
S: Send + Sync,
AppState: FromRef<S>,
T: DeserializeOwned,
{
type Rejection = Problem;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let (header, pubkey, account, payload_b64) = verify_jws(req, state).await?;
let payload = if payload_b64.is_empty() {
None
} else {
let payload_bytes = BASE64_URL_SAFE_NO_PAD.decode(&payload_b64).map_err(|_| {
warn!(
event = "jws_payload_decode_failed",
outcome = "failure",
payload_b64_chars = payload_b64.len()
);
Problem::malformed("Base64 payload invalid")
})?;
Some(serde_json::from_slice(&payload_bytes).map_err(|_| {
warn!(
event = "jws_payload_parse_failed",
outcome = "failure",
payload_bytes = payload_bytes.len()
);
Problem::malformed("Payload JSON invalid for this endpoint")
})?)
};
Ok(AcmeOptionalPayload {
header,
payload,
pubkey,
account,
})
}
}
fn map_signature_error(error: SignatureError) -> Problem {
match error {
SignatureError::Malformed(detail) => {
warn!(event = "jws_signature_malformed", outcome = "failure", detail = %detail);
Problem::malformed(detail)
}
SignatureError::BadAlgorithm(detail) => {
warn!(event = "jws_signature_algorithm_unsupported", outcome = "failure", detail = %detail);
Problem::bad_signature_algorithm(detail)
}
SignatureError::BadSignature(_) => {
warn!(
event = "jws_signature_verification_failed",
outcome = "failure"
);
Problem::unauthorized("Signature JWS invalid")
}
SignatureError::Encoding(detail) => {
error!(event = "jws_signature_encoding_failed", outcome = "failure", detail = %detail);
Problem::server_internal(detail)
}
}
}