Skip to main content

netget/server/acme/
mod.rs

1//! ACME (RFC 8555) certificate authority over HTTP/1.1, optionally over TLS. Rust owns the
2//! directory, nonces, JWS verification, the account / order / authorization / challenge state
3//! machine, http-01 fetching, CSR checks and issuance; the handler approves or refuses each
4//! account, order, validation, issuance and revocation.
5pub 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    /// pending / ready / processing / valid / invalid, before authz roll-up.
61    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
306/// An RFC 7807 problem with an ACME error type.
307struct 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
337/// What a successful route returns: status, body, content type, Location, extra Link.
338struct 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
569/// Verify the JWS against its account (kid) and return the account id.
570async 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
599/// Ask the handler; `Ok(None)` is an acme_accept, `Ok(Some(problem))` its refusal.
600async 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
725/// A DNS identifier as RFC 8555 §7.1.4 and the CA/B rules allow it: lower-cased LDH labels,
726/// at most one leading wildcard label.
727fn 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
745/// Roll authorization outcomes up into the order (RFC 8555 §7.1.6).
746fn 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
1156/// http-01 (RFC 8555 §8.3): GET the token from `target` with the identifier as Host.
1157async 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    // An empty payload is POST-as-GET; "{}" asks for validation (RFC 8555 §7.5.1).
1238    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}