1use std::time::Duration;
35
36use axum::extract::{FromRef, FromRequest, Request};
37use axum::http::StatusCode;
38use base64::prelude::*;
39use serde::de::DeserializeOwned;
40use tracing::{Span, debug, error, instrument, warn};
41
42use crate::router::AppState;
43use acme_proxy_core::error::Problem;
44use acme_proxy_store::account::Account;
45use acme_proxy_store::nonce::Nonce;
46
47use acme_proxy_core::jws::AcmeJwsRequest;
48use acme_proxy_core::jws::ProtectedHeader;
49use acme_proxy_core::jws::signature::SignatureError;
50use acme_proxy_core::jws::signature::verify_signature_and_get_der;
51use acme_proxy_core::jws::signature::verify_signature_with_spki;
52
53pub struct AcmeRequest<T> {
55 pub header: ProtectedHeader,
56 pub payload: T,
57 pub pubkey: Vec<u8>,
58 pub account: Option<Account>,
59}
60
61const JOSE_JSON: &str = "application/jose+json";
63
64fn is_jose_json(content_type: Option<&str>) -> bool {
71 content_type.is_some_and(|value| {
72 let essence = value.split(';').next().unwrap_or_default().trim();
73 essence.eq_ignore_ascii_case(JOSE_JSON)
74 })
75}
76
77#[instrument(
85 name = "verify_jws",
86 skip_all,
87 fields(
88 path = %req.uri().path(),
89 alg = tracing::field::Empty,
90 account_id = tracing::field::Empty,
91 )
92)]
93async fn verify_jws<S>(
94 req: Request,
95 state: &S,
96) -> Result<(ProtectedHeader, Vec<u8>, Option<Account>, String), Problem>
97where
98 S: Send + Sync,
99 AppState: FromRef<S>,
100{
101 let app = AppState::from_ref(state);
102 let request_path = req.uri().path().to_string();
103 let request_context = acme_proxy_core::audit::RequestContext::from_request(&req);
106 debug!(event = "jws_request_received", outcome = "progress", path = %request_path);
107
108 let content_type = req
115 .headers()
116 .get(axum::http::header::CONTENT_TYPE)
117 .and_then(|value| value.to_str().ok());
118 if !is_jose_json(content_type) {
119 warn!(event = "jws_bad_content_type", outcome = "failure", content_type = ?content_type, path = %request_path);
120 return Err(Problem::unsupported_media_type(
121 "Content-Type must be application/jose+json",
122 ));
123 }
124
125 let body_str = String::from_request(req, state)
126 .await
127 .map_err(|rejection| {
128 if rejection.status() == StatusCode::PAYLOAD_TOO_LARGE {
133 warn!(event = "jws_body_too_large", outcome = "failure", path = %request_path);
134 return Problem::payload_too_large("Request body exceeds the configured limit");
135 }
136 warn!(event = "jws_body_read_failed", outcome = "failure", path = %request_path);
137 Problem::malformed("Cannot read HTTP Body")
138 })?;
139
140 let jws: AcmeJwsRequest = serde_json::from_str(&body_str).map_err(|_| {
141 warn!(event = "jws_json_parse_failed", outcome = "failure", body_bytes = body_str.len(), path = %request_path);
142 Problem::malformed("JSON JWS format invalid")
143 })?;
144
145 let protected_bytes = BASE64_URL_SAFE_NO_PAD.decode(&jws.protected).map_err(|_| {
146 warn!(event = "jws_protected_decode_failed", outcome = "failure", protected_b64_chars = jws.protected.len(), path = %request_path);
147 Problem::malformed("Base64 protected invalid")
148 })?;
149
150 let header: ProtectedHeader = serde_json::from_slice(&protected_bytes).map_err(|_| {
151 warn!(event = "jws_header_parse_failed", outcome = "failure", protected_bytes = protected_bytes.len(), path = %request_path);
152 Problem::malformed("JSON protected invalid")
153 })?;
154
155 Span::current().record("alg", header.alg.as_str());
156
157 if let Some(crit) = &header.crit {
162 warn!(event = "jws_crit_header_present", outcome = "failure", extensions = ?crit, url = %header.url);
163 return Err(Problem::malformed(
164 "Unsupported critical JWS header extension",
165 ));
166 }
167
168 let expected_url = format!("{}{request_path}", app.profile.base_url);
178 if header.url != expected_url {
179 warn!(event = "jws_url_mismatch", outcome = "failure", url = %header.url, expected = %expected_url);
180 return Err(Problem::malformed("URL invalid"));
181 }
182
183 let signing_input = format!("{}.{}", jws.protected, jws.payload);
184
185 let (pubkey, mut signer_account) =
186 resolve_signing_key(&app, &header, &request_path, &signing_input, &jws.signature).await?;
187 consume_nonce(&app, &header, &request_path).await?;
188
189 if let Some(account) = signer_account.as_mut() {
196 touch_account(account, &request_context, &app).await;
197 }
198
199 debug!(
200 event = "jws_request_validated",
201 outcome = "success",
202 algorithm = %header.alg,
203 url = %header.url,
204 signature_type = match (&header.jwk, &header.kid) {
205 (Some(_), None) => "jwk",
206 (None, Some(_)) => "kid",
207 _ => "unknown",
208 },
209 signature_b64_chars = jws.signature.len()
210 );
211
212 Ok((header, pubkey, signer_account, jws.payload))
213}
214
215async fn resolve_signing_key(
222 app: &AppState,
223 header: &ProtectedHeader,
224 request_path: &str,
225 signing_input: &str,
226 signature: &str,
227) -> Result<(Vec<u8>, Option<Account>), Problem> {
228 let mut signer_account: Option<Account> = None;
229 let pubkey = match (&header.jwk, &header.kid) {
230 (Some(_), Some(_)) => {
231 warn!(event = "jws_jwk_and_kid_both_present", outcome = "failure", url = %header.url, algorithm = %header.alg);
232 return Err(Problem::malformed("jwk and kid are mutually exclusive"));
233 }
234 (Some(_), None) => {
235 if !matches!(
247 request_path,
248 acme_proxy_core::routes::NEW_ACCOUNT
249 | acme_proxy_core::routes::REVOKE_CERT
250 | acme_proxy_core::routes::DIRECTORY
251 | acme_proxy_core::routes::NEW_NONCE
252 ) {
253 warn!(event = "jws_jwk_not_allowed_here", outcome = "failure", url = %header.url, path = %request_path);
254 return Err(Problem::malformed(
255 "This request must be signed with kid, not an embedded jwk",
256 ));
257 }
258 debug!(event = "jws_jwk_verification_started", outcome = "progress", algorithm = %header.alg, url = %header.url);
259 verify_signature_and_get_der(header, signing_input, signature)
260 .map_err(map_signature_error)?
261 }
262 (None, Some(kid)) => {
263 debug!(event = "jws_kid_verification_started", outcome = "progress", algorithm = %header.alg, kid = %kid);
264 let base = &app.profile.base_url;
268 let id = kid
269 .strip_prefix(&format!("{base}/acct/"))
270 .ok_or_else(|| {
271 warn!(event = "jws_kid_prefix_mismatch", outcome = "failure", kid = %kid, expected_prefix = %format!("{base}/acct/"));
272 Problem::malformed("kid invalid")
273 })?;
274
275 Span::current().record("account_id", id);
276
277 let account = Account::find_by_id(&app.profile.name, id, &app.database)
278 .await
279 .map_err(|error| {
280 error!(event = "jws_kid_account_lookup_failed", outcome = "failure", account_id = %id, error = %error);
281 Problem::server_internal("Account lookup failed")
282 })?
283 .ok_or_else(|| Problem::account_does_not_exist("Unknown account"))?;
284
285 verify_signature_with_spki(&header.alg, &account.pubkey, signing_input, signature)
286 .map_err(map_signature_error)?;
287
288 let pubkey = account.pubkey.clone();
289 signer_account = Some(account);
290 pubkey
291 }
292 (None, None) => {
293 warn!(event = "jws_jwk_and_kid_missing", outcome = "failure", url = %header.url, algorithm = %header.alg);
294 return Err(Problem::malformed("missing jwk or kid"));
295 }
296 };
297 Ok((pubkey, signer_account))
298}
299
300async fn consume_nonce(
304 app: &AppState,
305 header: &ProtectedHeader,
306 request_path: &str,
307) -> Result<(), Problem> {
308 let ttl = Duration::from_secs(app.config.nonce.ttl_seconds);
309 match Nonce::verify(&header.nonce, &app.database, ttl).await {
310 Ok(true) => Ok(()),
311 Ok(false) => {
312 warn!(
317 event = "nonce_replayed",
318 outcome = "failure",
319 nonce_fp = %acme_proxy_store::nonce::fingerprint(&header.nonce),
320 path = %request_path
321 );
322 Err(Problem::bad_nonce("Nonce invalid"))
323 }
324 Err(error) => {
325 error!(
329 event = "nonce_verification_failed",
330 outcome = "failure",
331 nonce_fp = %acme_proxy_store::nonce::fingerprint(&header.nonce),
332 error = %error
333 );
334 Err(Problem::server_internal("Nonce verification failed"))
335 }
336 }
337}
338
339async fn touch_account(
353 account: &mut Account,
354 request: &acme_proxy_core::audit::RequestContext,
355 app: &AppState,
356) {
357 let ip = request
358 .ip
359 .map(acme_proxy_core::client::canonical)
360 .map(|ip| ip.to_string());
361 if !account.needs_touch(acme_proxy_store::nonce::now_secs(), ip.as_deref()) {
362 return;
363 }
364 let client = app.audit.client(request).await;
365 if let Err(error) = account.touch(&client, &app.database).await {
366 warn!(
367 event = "account_touch_failed",
368 outcome = "failure",
369 account_id = %account.id,
370 error = %error
371 );
372 }
373}
374
375fn decode_payload<T: DeserializeOwned>(payload_b64: &str) -> Result<T, Problem> {
383 let payload_bytes = BASE64_URL_SAFE_NO_PAD.decode(payload_b64).map_err(|_| {
384 warn!(
385 event = "jws_payload_decode_failed",
386 outcome = "failure",
387 payload_b64_chars = payload_b64.len()
388 );
389 Problem::malformed("Base64 payload invalid")
390 })?;
391
392 serde_json::from_slice(&payload_bytes).map_err(|_| {
393 warn!(
394 event = "jws_payload_parse_failed",
395 outcome = "failure",
396 payload_bytes = payload_bytes.len()
397 );
398 Problem::malformed("Payload JSON invalid for this endpoint")
399 })
400}
401
402impl<S, T> FromRequest<S> for AcmeRequest<T>
403where
404 S: Send + Sync,
405 AppState: FromRef<S>,
406 T: DeserializeOwned,
407{
408 type Rejection = Problem;
409
410 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
411 let (header, pubkey, account, payload_b64) = verify_jws(req, state).await?;
412
413 let payload: T = decode_payload(&payload_b64)?;
414
415 Ok(AcmeRequest {
416 header,
417 payload,
418 pubkey,
419 account,
420 })
421 }
422}
423
424pub struct AcmePostAsGet {
426 pub header: ProtectedHeader,
427 pub pubkey: Vec<u8>,
428 pub account: Option<Account>,
429}
430
431impl<S> FromRequest<S> for AcmePostAsGet
432where
433 S: Send + Sync,
434 AppState: FromRef<S>,
435{
436 type Rejection = Problem;
437
438 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
439 let (header, pubkey, account, payload_b64) = verify_jws(req, state).await?;
440
441 if !payload_b64.is_empty() {
442 warn!(
443 event = "jws_post_as_get_payload_not_empty",
444 outcome = "failure",
445 payload_b64_chars = payload_b64.len(),
446 url = %header.url
447 );
448 return Err(Problem::malformed("POST-as-GET payload must be empty"));
449 }
450
451 Ok(AcmePostAsGet {
452 header,
453 pubkey,
454 account,
455 })
456 }
457}
458
459pub struct AcmeOptionalPayload<T> {
470 pub header: ProtectedHeader,
471 pub payload: Option<T>,
473 pub pubkey: Vec<u8>,
474 pub account: Option<Account>,
475}
476
477impl<S, T> FromRequest<S> for AcmeOptionalPayload<T>
478where
479 S: Send + Sync,
480 AppState: FromRef<S>,
481 T: DeserializeOwned,
482{
483 type Rejection = Problem;
484
485 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
486 let (header, pubkey, account, payload_b64) = verify_jws(req, state).await?;
487
488 let payload = match payload_b64.is_empty() {
489 true => None,
490 false => Some(decode_payload(&payload_b64)?),
491 };
492
493 Ok(AcmeOptionalPayload {
494 header,
495 payload,
496 pubkey,
497 account,
498 })
499 }
500}
501
502fn map_signature_error(error: SignatureError) -> Problem {
504 match error {
505 SignatureError::Malformed(detail) => {
506 warn!(event = "jws_signature_malformed", outcome = "failure", detail = %detail);
507 Problem::malformed(detail)
508 }
509 SignatureError::BadAlgorithm(detail) => {
510 warn!(event = "jws_signature_algorithm_unsupported", outcome = "failure", detail = %detail);
511 Problem::bad_signature_algorithm(detail)
512 }
513 SignatureError::BadSignature(_) => {
514 warn!(
515 event = "jws_signature_verification_failed",
516 outcome = "failure"
517 );
518 Problem::unauthorized("Signature JWS invalid")
519 }
520 SignatureError::Encoding(detail) => {
524 error!(event = "jws_signature_encoding_failed", outcome = "failure", detail = %detail);
525 Problem::server_internal("Signature could not be verified")
526 }
527 }
528}