1pub mod actions;
6pub mod ca;
7pub mod jws;
8
9use crate::llm::action_helper::call_llm;
10use crate::llm::actions::protocol_trait::ActionResult;
11use crate::logging::emit::Log;
12use crate::protocol::{Event, SpawnContext};
13use crate::server::accept_bounded::{accept_bounded, ConnectionLimiter, DEFAULT_MAX_CONNECTIONS};
14use crate::server::connection::ConnectionId;
15use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
16use crate::utils::clock::Instant;
17use bytes::Bytes;
18use http_body_util::{BodyExt, Full, Limited};
19use hyper::{
20 body::Incoming, header, server::conn::http1, service::service_fn, Method, Request, Response,
21 StatusCode,
22};
23use hyper_util::rt::{TokioIo, TokioTimer};
24use jws::Jws;
25use ring::rand::SecureRandom;
26use serde_json::{json, Map, Value};
27use std::collections::{HashMap, HashSet, VecDeque};
28use std::{convert::Infallible, net::SocketAddr, sync::Arc, time::Duration};
29use tokio::sync::Mutex;
30
31pub const DEFAULT_CA_NAME: &str = "NetGet ACME Test CA";
32pub const DEFAULT_VALIDITY_DAYS: u32 = 90;
33pub const DEFAULT_CHALLENGES: &[&str] = &["http-01", "dns-01"];
34pub const MAX_BODY: usize = 64 * 1024;
35const MAX_ACCOUNTS: usize = 1024;
36const MAX_ORDERS: usize = 4096;
37const MAX_CERTS: usize = 4096;
38const MAX_NONCES: usize = 8192;
39const MAX_IDENTIFIERS: usize = 100;
40const ALGORITHMS: &[&str] = &["ES256", "ES384", "RS256", "EdDSA"];
41const HEADER_TIMEOUT: Duration = Duration::from_secs(30);
42const BODY_TIMEOUT: Duration = Duration::from_secs(30);
43const TLS_TIMEOUT: Duration = Duration::from_secs(10);
44const HTTP01_TIMEOUT: Duration = Duration::from_secs(10);
45const LIFETIME_DAYS: i64 = 7;
46
47struct Account {
48 jwk: Map<String, Value>,
49 thumbprint: String,
50 contact: Vec<String>,
51 tos: bool,
52 status: &'static str,
53 orders: Vec<String>,
54}
55
56struct Order {
57 account: String,
58 identifiers: Vec<String>,
59 authzs: Vec<String>,
60 status: &'static str,
62 cert: Option<String>,
63 expires: String,
64 error: Option<Value>,
65}
66
67struct Challenge {
68 id: String,
69 kind: String,
70 token: String,
71 status: &'static str,
72 validated: Option<String>,
73 error: Option<Value>,
74}
75
76struct Authz {
77 account: String,
78 identifier: String,
79 status: &'static str,
80 expires: String,
81 challenges: Vec<Challenge>,
82}
83
84struct Cert {
85 account: String,
86 der: Vec<u8>,
87 pem: String,
88 serial: String,
89 identifiers: Vec<String>,
90 revoked: bool,
91}
92
93#[derive(Default)]
94struct Store {
95 nonces: HashSet<String>,
96 nonce_order: VecDeque<String>,
97 accounts: HashMap<String, Account>,
98 by_thumbprint: HashMap<String, String>,
99 orders: HashMap<String, Order>,
100 authzs: HashMap<String, Authz>,
101 challenge_authz: HashMap<String, String>,
102 certs: HashMap<String, Cert>,
103}
104
105struct Shared {
106 ctx: SpawnContext,
107 ca: ca::Ca,
108 validity_days: u32,
109 challenge_types: Vec<String>,
110 http01_target: Option<String>,
111 terms_of_service: Option<String>,
112 scheme: &'static str,
113 store: Mutex<Store>,
114}
115
116pub async fn spawn(ctx: SpawnContext) -> anyhow::Result<SocketAddr> {
117 let p = ctx.startup_params.as_ref();
118 let s = |k: &str| -> anyhow::Result<Option<String>> {
119 Ok(p.map(|p| p.get_optional_string(k)).transpose()?.flatten())
120 };
121 let ca_name = s("ca_name")?.unwrap_or_else(|| DEFAULT_CA_NAME.to_owned());
122 anyhow::ensure!(
123 !ca_name.is_empty()
124 && ca_name.len() <= 64
125 && !crate::utils::sanitize::has_controls(&ca_name),
126 "ca_name is 1 to 64 printable characters"
127 );
128 let validity_days = p
129 .map(|p| p.get_optional_u64("validity_days"))
130 .transpose()?
131 .flatten()
132 .unwrap_or(DEFAULT_VALIDITY_DAYS as u64);
133 anyhow::ensure!(
134 (1..=397).contains(&validity_days),
135 "validity_days must be 1..=397"
136 );
137 let challenge_types: Vec<String> = match p
138 .map(|p| p.get_optional_array("challenge_types"))
139 .transpose()?
140 .flatten()
141 {
142 None => DEFAULT_CHALLENGES.iter().map(|c| c.to_string()).collect(),
143 Some(list) => list
144 .iter()
145 .map(|c| {
146 c.as_str()
147 .filter(|c| matches!(*c, "http-01" | "dns-01"))
148 .map(str::to_owned)
149 .ok_or_else(|| anyhow::anyhow!("challenge_types holds http-01 and/or dns-01"))
150 })
151 .collect::<anyhow::Result<_>>()?,
152 };
153 anyhow::ensure!(
154 !challenge_types.is_empty(),
155 "challenge_types must offer at least one type"
156 );
157 let http01_target = s("http01_target")?;
158 if let Some(t) = &http01_target {
159 anyhow::ensure!(
160 t.parse::<SocketAddr>().is_ok(),
161 "http01_target is host:port with a literal address, e.g. 127.0.0.1:5002"
162 );
163 }
164 let terms_of_service = s("terms_of_service")?;
165 if let Some(t) = &terms_of_service {
166 anyhow::ensure!(
167 t.starts_with("https://") || t.starts_with("http://"),
168 "terms_of_service is an http(s) URL"
169 );
170 }
171 let tls = match (s("tls_cert_file")?, s("tls_key_file")?) {
172 (Some(c), Some(k)) => Some(tokio_rustls::TlsAcceptor::from(
173 crate::server::tls_cert_manager::load_tls_config_from_files(&c, &k)?,
174 )),
175 (None, None) => None,
176 _ => anyhow::bail!("tls_cert_file and tls_key_file go together"),
177 };
178 let ca = ca::Ca::new(&ca_name)?;
179 let listener = tokio::net::TcpListener::bind(ctx.legacy_listen_addr()).await?;
180 let local = listener.local_addr()?;
181 let scheme = if tls.is_some() { "https" } else { "http" };
182 Log::new(Some(&ctx.status_tx)).info(format!(
183 "ACME directory at {scheme}://{local}/directory (CA \"{ca_name}\")"
184 ));
185 let shared = Arc::new(Shared {
186 ctx: ctx.clone(),
187 ca,
188 validity_days: validity_days as u32,
189 challenge_types,
190 http01_target,
191 terms_of_service,
192 scheme,
193 store: Mutex::default(),
194 });
195 let server_id = ctx.server_id;
196 let accept = tokio::spawn(async move {
197 let limiter = ConnectionLimiter::new(DEFAULT_MAX_CONNECTIONS);
198 loop {
199 let (stream, peer, permit) = match accept_bounded(&listener, &limiter, b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", "ACME", Some(&shared.ctx.status_tx)).await {
200 Ok(v) => v,
201 Err(_) => break,
202 };
203 let id = ConnectionId::new(shared.ctx.state.get_next_unified_id().await);
204 let now = Instant::now();
205 shared
206 .ctx
207 .state
208 .add_connection_to_server(
209 server_id,
210 ConnectionState {
211 id,
212 remote_addr: peer,
213 local_addr: local,
214 bytes_sent: 0,
215 bytes_received: 0,
216 packets_sent: 0,
217 packets_received: 0,
218 last_activity: now,
219 status: ConnectionStatus::Active,
220 status_changed_at: now,
221 protocol_info: ProtocolConnectionInfo::empty(),
222 },
223 )
224 .await;
225 let child = shared.clone();
226 let tls = tls.clone();
227 shared
228 .ctx
229 .state
230 .spawn_server_task(server_id, async move {
231 let _permit = permit;
232 let svc_shared = child.clone();
233 let service = service_fn(move |req| {
234 let shared = svc_shared.clone();
235 async move { Ok::<_, Infallible>(handle(&shared, id, req).await) }
236 });
237 let mut builder = http1::Builder::new();
238 builder
239 .timer(TokioTimer::new())
240 .header_read_timeout(HEADER_TIMEOUT)
241 .max_headers(64)
242 .max_buf_size(32 * 1024);
243 let served = match tls {
244 Some(acceptor) => {
245 match tokio::time::timeout(TLS_TIMEOUT, acceptor.accept(stream)).await {
246 Ok(Ok(s)) => builder
247 .serve_connection(TokioIo::new(s), service)
248 .await
249 .map_err(|e| e.to_string()),
250 Ok(Err(e)) => Err(format!("TLS handshake: {e}")),
251 Err(_) => Err("TLS handshake timed out".into()),
252 }
253 }
254 None => builder
255 .serve_connection(TokioIo::new(stream), service)
256 .await
257 .map_err(|e| e.to_string()),
258 };
259 if let Err(e) = served {
260 Log::new(Some(&child.ctx.status_tx))
261 .debug(format!("ACME connection {id}: {e}"));
262 }
263 child
264 .ctx
265 .state
266 .update_connection_status(server_id, id, ConnectionStatus::Closed)
267 .await;
268 let _ = child.ctx.status_tx.send("__UPDATE_UI__".into());
269 })
270 .await;
271 }
272 });
273 ctx.state.register_server_task(server_id, accept).await;
274 Ok(local)
275}
276
277type Reply = Response<Full<Bytes>>;
278
279fn random_id() -> String {
280 let mut b = [0u8; 16];
281 let _ = ring::rand::SystemRandom::new().fill(&mut b);
282 jws::b64(&b)
283}
284
285fn rfc3339(offset_days: i64) -> String {
286 (time::OffsetDateTime::now_utc() + time::Duration::days(offset_days))
287 .replace_nanosecond(0)
288 .ok()
289 .and_then(|t| {
290 t.format(&time::format_description::well_known::Rfc3339)
291 .ok()
292 })
293 .unwrap_or_default()
294}
295
296fn outcome(ctx: &SpawnContext, id: ConnectionId, operation: &str, decision: &str) {
297 let summary = format!("ACME connection {id} operation={operation} decision={decision}");
298 let log = Log::new(Some(&ctx.status_tx));
299 if decision.starts_with("fail_closed_") || decision == "model_silent" {
300 log.error(summary);
301 } else {
302 log.info(summary);
303 }
304}
305
306struct Problem {
308 status: u16,
309 kind: &'static str,
310 detail: String,
311 extra: Option<(&'static str, Value)>,
312}
313
314fn problem(status: u16, kind: &'static str, detail: impl Into<String>) -> Problem {
315 Problem {
316 status,
317 kind,
318 detail: detail.into(),
319 extra: None,
320 }
321}
322
323fn malformed(detail: impl Into<String>) -> Problem {
324 problem(400, "malformed", detail)
325}
326
327impl Problem {
328 fn body(&self) -> Value {
329 let mut b = json!({"type": format!("urn:ietf:params:acme:error:{}", self.kind), "detail": self.detail, "status": self.status});
330 if let Some((k, v)) = &self.extra {
331 b[*k] = v.clone();
332 }
333 b
334 }
335}
336
337struct Ok200 {
339 status: u16,
340 body: Bytes,
341 content_type: &'static str,
342 location: Option<String>,
343 link: Option<String>,
344}
345
346fn json_reply(status: u16, body: &Value) -> Ok200 {
347 Ok200 {
348 status,
349 body: Bytes::from(body.to_string()),
350 content_type: "application/json",
351 location: None,
352 link: None,
353 }
354}
355
356async fn handle(shared: &Arc<Shared>, id: ConnectionId, req: Request<Incoming>) -> Reply {
357 let ctx = &shared.ctx;
358 ctx.state
359 .update_connection_stats(
360 ctx.server_id,
361 id,
362 Some(req.uri().to_string().len() as u64),
363 None,
364 Some(1),
365 None,
366 )
367 .await;
368 let host = req
369 .headers()
370 .get(header::HOST)
371 .and_then(|h| h.to_str().ok())
372 .filter(|h| !h.is_empty() && h.len() <= 255 && h.bytes().all(|b| b.is_ascii_graphic()))
373 .map(str::to_owned);
374 let response = match host {
375 None => finish(shared, "", Err(malformed("a Host header is required"))).await,
376 Some(host) => {
377 let base = format!("{}://{host}", shared.scheme);
378 let result = route(shared, id, &base, req).await;
379 finish(shared, &base, result).await
380 }
381 };
382 let sent = hyper::body::Body::size_hint(response.body())
383 .exact()
384 .unwrap_or(0);
385 ctx.state
386 .update_connection_stats(ctx.server_id, id, None, Some(sent), None, Some(1))
387 .await;
388 response
389}
390
391async fn new_nonce(shared: &Shared) -> String {
392 let nonce = random_id();
393 let mut st = shared.store.lock().await;
394 st.nonces.insert(nonce.clone());
395 st.nonce_order.push_back(nonce.clone());
396 while st.nonce_order.len() > MAX_NONCES {
397 if let Some(old) = st.nonce_order.pop_front() {
398 st.nonces.remove(&old);
399 }
400 }
401 nonce
402}
403
404async fn finish(shared: &Shared, base: &str, result: Result<Ok200, Problem>) -> Reply {
405 let nonce = new_nonce(shared).await;
406 let (status, body, content_type, location, link) = match result {
407 Ok(o) => (o.status, o.body, o.content_type, o.location, o.link),
408 Err(p) => (
409 p.status,
410 Bytes::from(p.body().to_string()),
411 "application/problem+json",
412 None,
413 None,
414 ),
415 };
416 let has_body = !body.is_empty();
417 let mut r = Response::new(Full::new(body));
418 *r.status_mut() = StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
419 let h = r.headers_mut();
420 let mut set = |name: header::HeaderName, value: String| {
421 if let Ok(v) = header::HeaderValue::from_str(&value) {
422 h.append(name, v);
423 }
424 };
425 if has_body {
426 set(header::CONTENT_TYPE, content_type.to_owned());
427 }
428 set(header::HeaderName::from_static("replay-nonce"), nonce);
429 set(header::CACHE_CONTROL, "no-store".into());
430 if !base.is_empty() {
431 set(header::LINK, format!("<{base}/directory>;rel=\"index\""));
432 }
433 if let Some(l) = link {
434 set(header::LINK, l);
435 }
436 if let Some(l) = location {
437 set(header::LOCATION, l);
438 }
439 r
440}
441
442async fn route(
443 shared: &Arc<Shared>,
444 id: ConnectionId,
445 base: &str,
446 req: Request<Incoming>,
447) -> Result<Ok200, Problem> {
448 let path = req.uri().path().to_owned();
449 let method = req.method().clone();
450 let segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
451 match (method.clone(), segments.as_slice()) {
452 (Method::GET, ["directory"]) => Ok(json_reply(200, &directory(shared, base))),
453 (Method::HEAD, ["new-nonce"]) => Ok(Ok200 {
454 status: 200,
455 body: Bytes::new(),
456 content_type: "application/json",
457 location: None,
458 link: None,
459 }),
460 (Method::GET, ["new-nonce"]) => Ok(Ok200 {
461 status: 204,
462 body: Bytes::new(),
463 content_type: "application/json",
464 location: None,
465 link: None,
466 }),
467 (Method::GET, ["roots", "0"]) => Ok(Ok200 {
468 status: 200,
469 body: Bytes::from(shared.ca.pem()),
470 content_type: "application/pem-certificate-chain",
471 location: None,
472 link: None,
473 }),
474 (Method::POST, ["new-account" | "new-order" | "revoke-cert"])
475 | (Method::POST, ["acct" | "order" | "authz" | "chall" | "cert", _])
476 | (Method::POST, ["acct", _, "orders"])
477 | (Method::POST, ["order", _, "finalize"]) => {
478 let ct = req
479 .headers()
480 .get(header::CONTENT_TYPE)
481 .and_then(|v| v.to_str().ok())
482 .unwrap_or_default()
483 .to_ascii_lowercase();
484 if ct.split(';').next().map(str::trim) != Some("application/jose+json") {
485 return Err(problem(
486 415,
487 "malformed",
488 "POST bodies are application/jose+json",
489 ));
490 }
491 let body = match tokio::time::timeout(
492 BODY_TIMEOUT,
493 Limited::new(req.into_body(), MAX_BODY).collect(),
494 )
495 .await
496 {
497 Ok(Ok(b)) => b.to_bytes(),
498 Ok(Err(_)) => {
499 return Err(problem(
500 413,
501 "malformed",
502 format!("request body over {MAX_BODY} bytes"),
503 ))
504 }
505 Err(_) => return Err(malformed("request body timed out")),
506 };
507 let jws = Jws::parse(&body).map_err(|e| malformed(format!("{e:#}")))?;
508 if !ALGORITHMS.contains(&jws.alg.as_str()) {
509 let mut p = problem(
510 400,
511 "badSignatureAlgorithm",
512 format!("unsupported alg {}", jws.alg),
513 );
514 p.extra = Some(("algorithms", json!(ALGORITHMS)));
515 return Err(p);
516 }
517 {
518 let mut st = shared.store.lock().await;
519 if !st.nonces.remove(&jws.nonce) {
520 return Err(problem(
521 400,
522 "badNonce",
523 "the nonce is unknown or was already used",
524 ));
525 }
526 }
527 if jws.url != format!("{base}{path}") {
528 return Err(problem(
529 403,
530 "unauthorized",
531 "the JWS url does not match the request URL",
532 ));
533 }
534 post(shared, id, base, &segments, jws).await
535 }
536 (
537 _,
538 ["directory" | "new-nonce" | "roots" | "new-account" | "new-order" | "revoke-cert"
539 | "acct" | "order" | "authz" | "chall" | "cert", ..],
540 ) => Err(problem(
541 405,
542 "malformed",
543 format!("{method} is not allowed here"),
544 )),
545 _ => Err(problem(404, "malformed", "no such resource")),
546 }
547}
548
549fn directory(shared: &Shared, base: &str) -> Value {
550 let mut meta = json!({"externalAccountRequired": false});
551 if let Some(t) = &shared.terms_of_service {
552 meta["termsOfService"] = json!(t);
553 }
554 json!({
555 "newNonce": format!("{base}/new-nonce"),
556 "newAccount": format!("{base}/new-account"),
557 "newOrder": format!("{base}/new-order"),
558 "revokeCert": format!("{base}/revoke-cert"),
559 "meta": meta,
560 })
561}
562
563fn key_type(jwk: &Map<String, Value>) -> String {
564 jws::PublicKey::from_jwk(jwk)
565 .map(|k| k.kind().to_owned())
566 .unwrap_or_default()
567}
568
569async fn authenticate(shared: &Shared, base: &str, jws: &Jws) -> Result<String, Problem> {
571 let kid = jws.kid.as_deref().ok_or_else(|| {
572 malformed("this request is signed with the account key, identified by kid")
573 })?;
574 let account = kid
575 .strip_prefix(&format!("{base}/acct/"))
576 .filter(|a| !a.contains('/'))
577 .ok_or_else(|| {
578 problem(
579 400,
580 "accountDoesNotExist",
581 "kid is not an account URL of this server",
582 )
583 })?;
584 let jwk = {
585 let st = shared.store.lock().await;
586 let a = st
587 .accounts
588 .get(account)
589 .ok_or_else(|| problem(400, "accountDoesNotExist", "no such account"))?;
590 if a.status != "valid" {
591 return Err(problem(403, "unauthorized", "the account is deactivated"));
592 }
593 a.jwk.clone()
594 };
595 jws.verify(&jwk).map_err(|e| malformed(format!("{e:#}")))?;
596 Ok(account.to_owned())
597}
598
599async fn ask(
601 shared: &Shared,
602 id: ConnectionId,
603 event: Event,
604 operation: &str,
605) -> Result<Option<Problem>, Problem> {
606 let ctx = &shared.ctx;
607 let unavailable = |e: &anyhow::Error| {
608 problem(
609 500,
610 "serverInternal",
611 crate::utils::WireFailure::classify(e).text(),
612 )
613 };
614 let result = match call_llm(
615 &ctx.llm_client,
616 &ctx.state,
617 ctx.server_id,
618 Some(id),
619 &event,
620 &actions::AcmeProtocol,
621 )
622 .await
623 {
624 Ok(r) => r,
625 Err(e) => {
626 outcome(ctx, id, operation, "fail_closed_llm_error");
627 return Err(unavailable(&e));
628 }
629 };
630 let invalid = || {
631 problem(
632 500,
633 "serverInternal",
634 "the CA could not decide this request",
635 )
636 };
637 if !result.failures.is_empty() {
638 outcome(ctx, id, operation, "fail_closed_invalid_reply");
639 return Err(invalid());
640 }
641 let mut answers = Vec::new();
642 let mut pending = result.protocol_results;
643 while let Some(r) = pending.pop() {
644 match r {
645 ActionResult::Custom { data, .. } => answers.push(data),
646 ActionResult::Multiple(items) => pending.extend(items),
647 _ => {}
648 }
649 }
650 let answer = match answers.len() {
651 1 => answers.remove(0),
652 0 => {
653 outcome(ctx, id, operation, "model_silent");
654 return Err(invalid());
655 }
656 _ => {
657 outcome(ctx, id, operation, "fail_closed_invalid_reply");
658 return Err(invalid());
659 }
660 };
661 if answer["type"] == "acme_accept" {
662 outcome(ctx, id, operation, "model_answer");
663 return Ok(None);
664 }
665 outcome(ctx, id, operation, "model_reject");
666 let kind = answer["error"].as_str().unwrap_or_default();
667 let (kind, status) = actions::REJECT_ERRORS
668 .iter()
669 .find(|(n, _)| *n == kind)
670 .copied()
671 .unwrap_or(("unauthorized", 403));
672 Ok(Some(problem(
673 status,
674 kind,
675 answer["detail"].as_str().unwrap_or_default().to_owned(),
676 )))
677}
678
679fn account_json(base: &str, id: &str, a: &Account) -> Value {
680 let mut v = json!({"status": a.status, "contact": a.contact, "orders": format!("{base}/acct/{id}/orders")});
681 if a.tos {
682 v["termsOfServiceAgreed"] = json!(true);
683 }
684 v
685}
686
687fn check_contact(payload: &Value) -> Result<Vec<String>, Problem> {
688 let Some(c) = payload.get("contact").filter(|c| !c.is_null()) else {
689 return Ok(vec![]);
690 };
691 let list = c
692 .as_array()
693 .filter(|l| l.len() <= 10)
694 .ok_or_else(|| malformed("contact is an array of at most 10 URLs"))?;
695 list.iter()
696 .map(|u| {
697 let u = u
698 .as_str()
699 .ok_or_else(|| malformed("contact entries are strings"))?;
700 let addr = u.strip_prefix("mailto:").ok_or_else(|| {
701 problem(
702 400,
703 "unsupportedContact",
704 format!("only mailto: contacts are supported, not {u:?}"),
705 )
706 })?;
707 let (local, domain) = addr.split_once('@').unwrap_or_default();
708 if local.is_empty()
709 || domain.is_empty()
710 || u.len() > 256
711 || addr.contains(['?', ',', ' '])
712 || crate::utils::sanitize::has_controls(&addr)
713 {
714 return Err(problem(
715 400,
716 "invalidContact",
717 format!("{u:?} is not a single email address"),
718 ));
719 }
720 Ok(u.to_owned())
721 })
722 .collect()
723}
724
725fn dns_name(v: &str) -> Option<String> {
728 let v = v.to_ascii_lowercase();
729 let rest = v.strip_prefix("*.").unwrap_or(&v);
730 let ok = !rest.is_empty()
731 && v.len() <= 253
732 && rest.contains('.')
733 && rest.split('.').all(|l| {
734 !l.is_empty()
735 && l.len() <= 63
736 && !l.starts_with('-')
737 && !l.ends_with('-')
738 && l.bytes()
739 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
740 })
741 && rest.parse::<std::net::IpAddr>().is_err();
742 ok.then_some(v)
743}
744
745fn order_status(st: &Store, o: &Order) -> &'static str {
747 match o.status {
748 "pending" => {
749 let states: Vec<&str> = o
750 .authzs
751 .iter()
752 .filter_map(|a| st.authzs.get(a))
753 .map(|a| a.status)
754 .collect();
755 if states.iter().any(|s| *s != "pending" && *s != "valid") {
756 "invalid"
757 } else if !states.is_empty() && states.iter().all(|s| *s == "valid") {
758 "ready"
759 } else {
760 "pending"
761 }
762 }
763 s => s,
764 }
765}
766
767fn order_json(st: &Store, base: &str, id: &str, o: &Order) -> Value {
768 let mut v = json!({
769 "status": order_status(st, o),
770 "expires": o.expires,
771 "identifiers": o.identifiers.iter().map(|i| json!({"type": "dns", "value": i})).collect::<Vec<_>>(),
772 "authorizations": o.authzs.iter().map(|a| format!("{base}/authz/{a}")).collect::<Vec<_>>(),
773 "finalize": format!("{base}/order/{id}/finalize"),
774 });
775 if let Some(c) = &o.cert {
776 v["certificate"] = json!(format!("{base}/cert/{c}"));
777 }
778 if let Some(e) = &o.error {
779 v["error"] = e.clone();
780 }
781 v
782}
783
784fn challenge_json(base: &str, c: &Challenge) -> Value {
785 let mut v = json!({"type": c.kind, "url": format!("{base}/chall/{}", c.id), "status": c.status, "token": c.token});
786 if let Some(t) = &c.validated {
787 v["validated"] = json!(t);
788 }
789 if let Some(e) = &c.error {
790 v["error"] = e.clone();
791 }
792 v
793}
794
795fn authz_json(base: &str, a: &Authz) -> Value {
796 let wildcard = a.identifier.starts_with("*.");
797 let mut v = json!({
798 "status": a.status,
799 "expires": a.expires,
800 "identifier": {"type": "dns", "value": a.identifier.trim_start_matches("*.")},
801 "challenges": a.challenges.iter().map(|c| challenge_json(base, c)).collect::<Vec<_>>(),
802 });
803 if wildcard {
804 v["wildcard"] = json!(true);
805 }
806 v
807}
808
809fn owned<'a, T>(
810 item: Option<&'a T>,
811 owner: impl Fn(&T) -> &str,
812 account: &str,
813) -> Result<&'a T, Problem> {
814 let item = item.ok_or_else(|| problem(404, "malformed", "no such resource"))?;
815 if owner(item) != account {
816 return Err(problem(
817 403,
818 "unauthorized",
819 "this resource belongs to another account",
820 ));
821 }
822 Ok(item)
823}
824
825async fn post(
826 shared: &Arc<Shared>,
827 id: ConnectionId,
828 base: &str,
829 segments: &[&str],
830 jws: Jws,
831) -> Result<Ok200, Problem> {
832 match segments {
833 ["new-account"] => new_account(shared, id, base, jws).await,
834 ["revoke-cert"] => {
835 let account = authenticate(shared, base, &jws).await?;
836 revoke(shared, id, base, &account, &jws).await
837 }
838 ["new-order"] => {
839 let account = authenticate(shared, base, &jws).await?;
840 new_order(shared, id, base, &account, &jws).await
841 }
842 ["acct", acct] => {
843 let account = authenticate(shared, base, &jws).await?;
844 if account != *acct {
845 return Err(problem(
846 403,
847 "unauthorized",
848 "the kid names another account",
849 ));
850 }
851 let mut st = shared.store.lock().await;
852 let a = st
853 .accounts
854 .get_mut(*acct)
855 .ok_or_else(|| problem(400, "accountDoesNotExist", "no such account"))?;
856 if !jws.payload.is_empty() {
857 let payload = jws
858 .payload_json()
859 .map_err(|e| malformed(format!("{e:#}")))?;
860 if payload.get("contact").is_some_and(|c| !c.is_null()) {
861 a.contact = check_contact(&payload)?;
862 }
863 match payload.get("status").and_then(Value::as_str) {
864 Some("deactivated") => a.status = "deactivated",
865 Some(other) => {
866 return Err(malformed(format!("an account cannot be set to {other}")))
867 }
868 None => {}
869 }
870 }
871 Ok(json_reply(200, &account_json(base, acct, a)))
872 }
873 ["acct", acct, "orders"] => {
874 let account = authenticate(shared, base, &jws).await?;
875 if account != *acct {
876 return Err(problem(
877 403,
878 "unauthorized",
879 "the kid names another account",
880 ));
881 }
882 let st = shared.store.lock().await;
883 let orders: Vec<String> = st
884 .accounts
885 .get(*acct)
886 .map(|a| {
887 a.orders
888 .iter()
889 .map(|o| format!("{base}/order/{o}"))
890 .collect()
891 })
892 .unwrap_or_default();
893 Ok(json_reply(200, &json!({"orders": orders})))
894 }
895 ["order", order] => {
896 let account = authenticate(shared, base, &jws).await?;
897 let st = shared.store.lock().await;
898 let o = owned(st.orders.get(*order), |o: &Order| &o.account, &account)?;
899 Ok(json_reply(200, &order_json(&st, base, order, o)))
900 }
901 ["order", order, "finalize"] => {
902 let account = authenticate(shared, base, &jws).await?;
903 finalize(shared, id, base, &account, order, &jws).await
904 }
905 ["authz", authz] => {
906 let account = authenticate(shared, base, &jws).await?;
907 let mut st = shared.store.lock().await;
908 owned(st.authzs.get(*authz), |a: &Authz| &a.account, &account)?;
909 if !jws.payload.is_empty() {
910 let payload = jws
911 .payload_json()
912 .map_err(|e| malformed(format!("{e:#}")))?;
913 if payload.get("status").and_then(Value::as_str) != Some("deactivated") {
914 return Err(malformed("an authorization can only be deactivated"));
915 }
916 if let Some(a) = st.authzs.get_mut(*authz) {
917 if a.status == "pending" || a.status == "valid" {
918 a.status = "deactivated";
919 }
920 }
921 }
922 let a = st
923 .authzs
924 .get(*authz)
925 .ok_or_else(|| problem(404, "malformed", "no such resource"))?;
926 Ok(json_reply(200, &authz_json(base, a)))
927 }
928 ["chall", chall] => {
929 let account = authenticate(shared, base, &jws).await?;
930 challenge(shared, id, base, &account, chall, &jws).await
931 }
932 ["cert", cert] => {
933 let account = authenticate(shared, base, &jws).await?;
934 if !jws.payload.is_empty() {
935 return Err(malformed("certificates are fetched with POST-as-GET"));
936 }
937 let st = shared.store.lock().await;
938 let c = owned(st.certs.get(*cert), |c: &Cert| &c.account, &account)?;
939 Ok(Ok200 {
940 status: 200,
941 body: Bytes::from(c.pem.clone()),
942 content_type: "application/pem-certificate-chain",
943 location: None,
944 link: None,
945 })
946 }
947 _ => Err(problem(404, "malformed", "no such resource")),
948 }
949}
950
951async fn new_account(
952 shared: &Shared,
953 id: ConnectionId,
954 base: &str,
955 jws: Jws,
956) -> Result<Ok200, Problem> {
957 let jwk = jws
958 .jwk
959 .clone()
960 .ok_or_else(|| malformed("newAccount is signed with the new key, embedded as jwk"))?;
961 jws.verify(&jwk).map_err(|e| malformed(format!("{e:#}")))?;
962 let thumbprint = jws::thumbprint(&jwk).map_err(|e| malformed(format!("{e:#}")))?;
963 let payload = jws
964 .payload_json()
965 .map_err(|e| malformed(format!("{e:#}")))?;
966 {
967 let st = shared.store.lock().await;
968 if let Some(existing) = st.by_thumbprint.get(&thumbprint) {
969 let a = &st.accounts[existing];
970 let mut r = json_reply(200, &account_json(base, existing, a));
971 r.location = Some(format!("{base}/acct/{existing}"));
972 return Ok(r);
973 }
974 }
975 if payload.get("onlyReturnExisting").and_then(Value::as_bool) == Some(true) {
976 return Err(problem(
977 400,
978 "accountDoesNotExist",
979 "no account exists for this key",
980 ));
981 }
982 let contact = check_contact(&payload)?;
983 let tos = payload.get("termsOfServiceAgreed").and_then(Value::as_bool) == Some(true);
984 if shared.terms_of_service.is_some() && !tos {
985 return Err(malformed("the terms of service must be agreed to"));
986 }
987 let event = Event::new(
988 &actions::NEW_ACCOUNT_EVENT,
989 json!({"contact": contact, "terms_of_service_agreed": tos, "key_type": key_type(&jwk), "thumbprint": thumbprint}),
990 );
991 if let Some(refusal) = ask(shared, id, event, "new-account").await? {
992 return Err(refusal);
993 }
994 let mut st = shared.store.lock().await;
995 if let Some(existing) = st.by_thumbprint.get(&thumbprint).cloned() {
996 let mut r = json_reply(200, &account_json(base, &existing, &st.accounts[&existing]));
997 r.location = Some(format!("{base}/acct/{existing}"));
998 return Ok(r);
999 }
1000 if st.accounts.len() >= MAX_ACCOUNTS {
1001 return Err(problem(
1002 429,
1003 "rateLimited",
1004 "this CA holds its maximum number of accounts",
1005 ));
1006 }
1007 let acct = random_id();
1008 let a = Account {
1009 jwk,
1010 thumbprint: thumbprint.clone(),
1011 contact,
1012 tos,
1013 status: "valid",
1014 orders: vec![],
1015 };
1016 let body = account_json(base, &acct, &a);
1017 st.accounts.insert(acct.clone(), a);
1018 st.by_thumbprint.insert(thumbprint, acct.clone());
1019 let mut r = json_reply(201, &body);
1020 r.location = Some(format!("{base}/acct/{acct}"));
1021 Ok(r)
1022}
1023
1024async fn new_order(
1025 shared: &Shared,
1026 id: ConnectionId,
1027 base: &str,
1028 account: &str,
1029 jws: &Jws,
1030) -> Result<Ok200, Problem> {
1031 let payload = jws
1032 .payload_json()
1033 .map_err(|e| malformed(format!("{e:#}")))?;
1034 if payload.get("notBefore").is_some_and(|v| !v.is_null())
1035 || payload.get("notAfter").is_some_and(|v| !v.is_null())
1036 {
1037 return Err(malformed(
1038 "notBefore and notAfter are not supported; validity is set by the CA",
1039 ));
1040 }
1041 let list = payload
1042 .get("identifiers")
1043 .and_then(Value::as_array)
1044 .filter(|l| !l.is_empty() && l.len() <= MAX_IDENTIFIERS)
1045 .ok_or_else(|| {
1046 malformed(format!(
1047 "identifiers is an array of 1 to {MAX_IDENTIFIERS} objects"
1048 ))
1049 })?;
1050 let mut identifiers: Vec<String> = Vec::new();
1051 for i in list {
1052 if i.get("type").and_then(Value::as_str) != Some("dns") {
1053 return Err(problem(
1054 400,
1055 "unsupportedIdentifier",
1056 "only dns identifiers are supported",
1057 ));
1058 }
1059 let raw = i.get("value").and_then(Value::as_str).unwrap_or_default();
1060 let name = dns_name(raw).ok_or_else(|| {
1061 problem(
1062 400,
1063 "rejectedIdentifier",
1064 format!("{raw:?} is not a valid DNS name"),
1065 )
1066 })?;
1067 if name.starts_with("*.") && !shared.challenge_types.iter().any(|c| c == "dns-01") {
1068 return Err(problem(
1069 400,
1070 "rejectedIdentifier",
1071 "wildcards need dns-01, which this CA does not offer",
1072 ));
1073 }
1074 if !identifiers.contains(&name) {
1075 identifiers.push(name);
1076 }
1077 }
1078 let contact = shared
1079 .store
1080 .lock()
1081 .await
1082 .accounts
1083 .get(account)
1084 .map(|a| a.contact.clone())
1085 .unwrap_or_default();
1086 let event = Event::new(
1087 &actions::NEW_ORDER_EVENT,
1088 json!({"account": format!("{base}/acct/{account}"), "contact": contact, "identifiers": identifiers}),
1089 );
1090 if let Some(refusal) = ask(shared, id, event, "new-order").await? {
1091 return Err(refusal);
1092 }
1093 let mut st = shared.store.lock().await;
1094 if st.orders.len() >= MAX_ORDERS {
1095 return Err(problem(
1096 429,
1097 "rateLimited",
1098 "this CA holds its maximum number of orders",
1099 ));
1100 }
1101 let expires = rfc3339(LIFETIME_DAYS);
1102 let order = random_id();
1103 let mut authzs = Vec::new();
1104 for name in &identifiers {
1105 let authz = random_id();
1106 let kinds: Vec<&String> = shared
1107 .challenge_types
1108 .iter()
1109 .filter(|c| !name.starts_with("*.") || *c == "dns-01")
1110 .collect();
1111 let challenges: Vec<Challenge> = kinds
1112 .into_iter()
1113 .map(|k| Challenge {
1114 id: random_id(),
1115 kind: k.clone(),
1116 token: random_id(),
1117 status: "pending",
1118 validated: None,
1119 error: None,
1120 })
1121 .collect();
1122 for c in &challenges {
1123 st.challenge_authz.insert(c.id.clone(), authz.clone());
1124 }
1125 st.authzs.insert(
1126 authz.clone(),
1127 Authz {
1128 account: account.to_owned(),
1129 identifier: name.clone(),
1130 status: "pending",
1131 expires: expires.clone(),
1132 challenges,
1133 },
1134 );
1135 authzs.push(authz);
1136 }
1137 let o = Order {
1138 account: account.to_owned(),
1139 identifiers,
1140 authzs,
1141 status: "pending",
1142 cert: None,
1143 expires,
1144 error: None,
1145 };
1146 let body = order_json(&st, base, &order, &o);
1147 st.orders.insert(order.clone(), o);
1148 if let Some(a) = st.accounts.get_mut(account) {
1149 a.orders.push(order.clone());
1150 }
1151 let mut r = json_reply(201, &body);
1152 r.location = Some(format!("{base}/order/{order}"));
1153 Ok(r)
1154}
1155
1156async fn fetch_http01(target: &str, identifier: &str, token: &str) -> Result<String, String> {
1158 let fetch = async {
1159 let stream = tokio::net::TcpStream::connect(target)
1160 .await
1161 .map_err(|e| format!("connection refused: {e}"))?;
1162 let (mut sender, conn) =
1163 hyper::client::conn::http1::handshake::<_, Full<Bytes>>(TokioIo::new(stream))
1164 .await
1165 .map_err(|e| e.to_string())?;
1166 tokio::spawn(conn);
1167 let req = Request::get(format!("/.well-known/acme-challenge/{token}"))
1168 .header(header::HOST, identifier)
1169 .body(Full::new(Bytes::new()))
1170 .map_err(|e| e.to_string())?;
1171 let resp = sender.send_request(req).await.map_err(|e| e.to_string())?;
1172 let status = resp.status();
1173 let body = Limited::new(resp.into_body(), 1024)
1174 .collect()
1175 .await
1176 .map_err(|_| "response body over 1 KiB".to_owned())?
1177 .to_bytes();
1178 if status != StatusCode::OK {
1179 return Err(format!("the response status was {status}"));
1180 }
1181 Ok(String::from_utf8_lossy(&body).trim_end().to_owned())
1182 };
1183 tokio::time::timeout(HTTP01_TIMEOUT, fetch)
1184 .await
1185 .map_err(|_| "the fetch timed out".to_owned())?
1186}
1187
1188async fn challenge(
1189 shared: &Shared,
1190 id: ConnectionId,
1191 base: &str,
1192 account: &str,
1193 chall: &str,
1194 jws: &Jws,
1195) -> Result<Ok200, Problem> {
1196 let (authz, kind, token, identifier, status, thumbprint) = {
1197 let st = shared.store.lock().await;
1198 let authz = st
1199 .challenge_authz
1200 .get(chall)
1201 .cloned()
1202 .ok_or_else(|| problem(404, "malformed", "no such resource"))?;
1203 let a = owned(st.authzs.get(&authz), |a: &Authz| &a.account, account)?;
1204 let c = a
1205 .challenges
1206 .iter()
1207 .find(|c| c.id == chall)
1208 .ok_or_else(|| problem(404, "malformed", "no such resource"))?;
1209 let thumbprint = st
1210 .accounts
1211 .get(account)
1212 .map(|a| a.thumbprint.clone())
1213 .unwrap_or_default();
1214 (
1215 authz.clone(),
1216 c.kind.clone(),
1217 c.token.clone(),
1218 a.identifier.trim_start_matches("*.").to_owned(),
1219 (c.status, a.status),
1220 thumbprint,
1221 )
1222 };
1223 let respond = |st: &Store| -> Result<Ok200, Problem> {
1224 let a = st
1225 .authzs
1226 .get(&authz)
1227 .ok_or_else(|| problem(404, "malformed", "no such resource"))?;
1228 let c = a
1229 .challenges
1230 .iter()
1231 .find(|c| c.id == chall)
1232 .ok_or_else(|| problem(404, "malformed", "no such resource"))?;
1233 let mut r = json_reply(200, &challenge_json(base, c));
1234 r.link = Some(format!("<{base}/authz/{authz}>;rel=\"up\""));
1235 Ok(r)
1236 };
1237 if jws.payload.is_empty() || status != ("pending", "pending") {
1239 return respond(&*shared.store.lock().await);
1240 }
1241 if !jws
1242 .payload_json()
1243 .map_err(|e| malformed(format!("{e:#}")))?
1244 .is_object()
1245 {
1246 return Err(malformed("a challenge response is a JSON object"));
1247 }
1248 let key_authorization = format!("{token}.{thumbprint}");
1249 let mut verified = Value::Null;
1250 if kind == "http-01" {
1251 if let Some(target) = &shared.http01_target {
1252 match fetch_http01(target, &identifier, &token).await {
1253 Ok(body) if body == key_authorization => verified = json!(true),
1254 result => {
1255 let detail = match result {
1256 Ok(body) => format!(
1257 "the key authorization at {identifier} was {:?}",
1258 crate::utils::truncate::truncate_for_log(&body, 64)
1259 ),
1260 Err(e) => format!(
1261 "fetching http://{identifier}/.well-known/acme-challenge/{token}: {e}"
1262 ),
1263 };
1264 outcome(&shared.ctx, id, "validate", "protocol_refusal");
1265 let mut st = shared.store.lock().await;
1266 fail_challenge(
1267 &mut st,
1268 &authz,
1269 chall,
1270 problem(403, "incorrectResponse", detail).body(),
1271 );
1272 return respond(&st);
1273 }
1274 }
1275 }
1276 }
1277 let event = Event::new(
1278 &actions::VALIDATE_EVENT,
1279 json!({"account": format!("{base}/acct/{account}"), "identifier": identifier, "challenge_type": kind, "token": token, "key_authorization": key_authorization, "verified": verified}),
1280 );
1281 let decision = ask(shared, id, event, "validate").await?;
1282 let mut st = shared.store.lock().await;
1283 match decision {
1284 None => {
1285 if let Some(a) = st.authzs.get_mut(&authz) {
1286 if a.status == "pending" {
1287 a.status = "valid";
1288 if let Some(c) = a.challenges.iter_mut().find(|c| c.id == chall) {
1289 c.status = "valid";
1290 c.validated = Some(rfc3339(0));
1291 }
1292 }
1293 }
1294 }
1295 Some(refusal) => fail_challenge(&mut st, &authz, chall, refusal.body()),
1296 }
1297 respond(&st)
1298}
1299
1300fn fail_challenge(st: &mut Store, authz: &str, chall: &str, error: Value) {
1301 if let Some(a) = st.authzs.get_mut(authz) {
1302 a.status = "invalid";
1303 if let Some(c) = a.challenges.iter_mut().find(|c| c.id == chall) {
1304 c.status = "invalid";
1305 c.error = Some(error);
1306 }
1307 }
1308}
1309
1310async fn finalize(
1311 shared: &Shared,
1312 id: ConnectionId,
1313 base: &str,
1314 account: &str,
1315 order: &str,
1316 jws: &Jws,
1317) -> Result<Ok200, Problem> {
1318 let identifiers = {
1319 let st = shared.store.lock().await;
1320 let o = owned(st.orders.get(order), |o: &Order| &o.account, account)?;
1321 let status = order_status(&st, o);
1322 if status != "ready" {
1323 return Err(problem(
1324 403,
1325 "orderNotReady",
1326 format!("the order is {status}, not ready"),
1327 ));
1328 }
1329 o.identifiers.clone()
1330 };
1331 let payload = jws
1332 .payload_json()
1333 .map_err(|e| malformed(format!("{e:#}")))?;
1334 let der = payload
1335 .get("csr")
1336 .and_then(Value::as_str)
1337 .ok_or_else(|| malformed("csr is required"))
1338 .and_then(|c| jws::unb64(c).map_err(|_| problem(400, "badCSR", "csr is not base64url")))?;
1339 let csr = ca::Ca::read_csr(&der).map_err(|e| problem(400, "badCSR", format!("{e:#}")))?;
1340 let mut asked: Vec<String> = csr.dns_names.clone();
1341 asked.sort();
1342 asked.dedup();
1343 let mut wanted = identifiers.clone();
1344 wanted.sort();
1345 if csr.other_names > 0 || asked != wanted {
1346 return Err(problem(
1347 400,
1348 "badCSR",
1349 format!("the CSR names {asked:?}; the order is for {wanted:?}"),
1350 ));
1351 }
1352 if let Some(cn) = &csr.common_name {
1353 if !wanted.contains(&cn.to_ascii_lowercase()) {
1354 return Err(problem(
1355 400,
1356 "badCSR",
1357 format!("the CSR common name {cn:?} is not one of the order's identifiers"),
1358 ));
1359 }
1360 }
1361 let event = Event::new(
1362 &actions::FINALIZE_EVENT,
1363 json!({"account": format!("{base}/acct/{account}"), "order": format!("{base}/order/{order}"), "identifiers": identifiers, "validity_days": shared.validity_days}),
1364 );
1365 let decision = ask(shared, id, event, "finalize").await?;
1366 let mut st = shared.store.lock().await;
1367 if let Some(refusal) = decision {
1368 if let Some(o) = st.orders.get_mut(order) {
1369 o.status = "invalid";
1370 o.error = Some(refusal.body());
1371 }
1372 return Err(refusal);
1373 }
1374 if st.certs.len() >= MAX_CERTS {
1375 return Err(problem(
1376 429,
1377 "rateLimited",
1378 "this CA holds its maximum number of certificates",
1379 ));
1380 }
1381 let issued = shared
1382 .ca
1383 .issue(csr, &identifiers, shared.validity_days)
1384 .map_err(|e| problem(500, "serverInternal", format!("issuance failed: {e:#}")))?;
1385 let cert = random_id();
1386 st.certs.insert(
1387 cert.clone(),
1388 Cert {
1389 account: account.to_owned(),
1390 der: issued.der,
1391 pem: issued.pem,
1392 serial: issued.serial,
1393 identifiers: identifiers.clone(),
1394 revoked: false,
1395 },
1396 );
1397 let o = st
1398 .orders
1399 .get_mut(order)
1400 .ok_or_else(|| problem(404, "malformed", "no such resource"))?;
1401 o.status = "valid";
1402 o.cert = Some(cert);
1403 let o = &st.orders[order];
1404 let mut r = json_reply(200, &order_json(&st, base, order, o));
1405 r.location = Some(format!("{base}/order/{order}"));
1406 Ok(r)
1407}
1408
1409async fn revoke(
1410 shared: &Shared,
1411 id: ConnectionId,
1412 base: &str,
1413 account: &str,
1414 jws: &Jws,
1415) -> Result<Ok200, Problem> {
1416 let payload = jws
1417 .payload_json()
1418 .map_err(|e| malformed(format!("{e:#}")))?;
1419 let der = payload
1420 .get("certificate")
1421 .and_then(Value::as_str)
1422 .and_then(|c| jws::unb64(c).ok())
1423 .ok_or_else(|| malformed("certificate is base64url DER"))?;
1424 let reason = match payload.get("reason").filter(|r| !r.is_null()) {
1425 None => None,
1426 Some(r) => match r.as_u64() {
1427 Some(n @ (0..=6 | 8..=10)) => Some(n),
1428 _ => {
1429 return Err(problem(
1430 400,
1431 "badRevocationReason",
1432 "reason is an RFC 5280 code 0 to 10, not 7",
1433 ))
1434 }
1435 },
1436 };
1437 let (cert, serial, identifiers) = {
1438 let st = shared.store.lock().await;
1439 let (cert, c) =
1440 st.certs.iter().find(|(_, c)| c.der == der).ok_or_else(|| {
1441 problem(404, "malformed", "this CA did not issue that certificate")
1442 })?;
1443 if c.account != account {
1444 return Err(problem(
1445 403,
1446 "unauthorized",
1447 "the certificate belongs to another account",
1448 ));
1449 }
1450 if c.revoked {
1451 return Err(problem(
1452 400,
1453 "alreadyRevoked",
1454 "the certificate is already revoked",
1455 ));
1456 }
1457 (cert.clone(), c.serial.clone(), c.identifiers.clone())
1458 };
1459 let event = Event::new(
1460 &actions::REVOKE_EVENT,
1461 json!({"account": format!("{base}/acct/{account}"), "serial": serial, "identifiers": identifiers, "reason": reason}),
1462 );
1463 if let Some(refusal) = ask(shared, id, event, "revoke").await? {
1464 return Err(refusal);
1465 }
1466 if let Some(c) = shared.store.lock().await.certs.get_mut(&cert) {
1467 c.revoked = true;
1468 }
1469 Ok(Ok200 {
1470 status: 200,
1471 body: Bytes::new(),
1472 content_type: "application/json",
1473 location: None,
1474 link: None,
1475 })
1476}