Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/src/acme/client.rs

61.3 KiB, 213 runs

created by r1870400018:9519, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! ACME client state machine for RFC 8555 via the `tls-alpn-01` challenge.
2//!
3//! [`AcmeClient`] drives one end-to-end issuance against a CA such as Let's
4//! Encrypt. The happy path is:
5//!
6//! 1. Fetch the CA directory (cached on the client after first call).
7//! 2. Fetch a fresh nonce.
8//! 3. Register (or recover) the ACME account.
9//! 4. Submit a new order for one or more DNS identifiers.
10//! 5. For each authorisation URL the CA returns, fetch it, locate the
11//! `tls-alpn-01` challenge, build an ephemeral challenge certificate
12//! via [`crate::acme::challenge`], install it into the caller's
13//! resolver (via [`ChallengeInstaller`]), and POST the challenge URL
14//! to signal readiness.
15//! 6. Poll the authorisation until it reaches `valid` or `invalid`.
16//! 7. Poll the order until it reaches `ready`.
17//! 8. Generate a fresh P-256 key pair and a CSR for the requested DNS
18//! names, POST the CSR to the order's finalise URL, and poll the
19//! order until it reaches `valid`.
20//! 9. POST-as-GET the order's certificate URL and return the PEM chain
21//! plus the matching PKCS#8 private key.
22//!
23//! Every POST to the CA is wrapped in a JWS produced by
24//! [`crate::acme::jose::JwsSigner`]. The first request (new-account)
25//! carries the full public key in the `jwk` header field; subsequent
26//! requests carry the account URL in a `kid` field as RFC 8555 §6.2
27//! requires.
28//!
29//! Nonces are threaded through every request by extracting the
30//! `Replay-Nonce` response header from each successful reply and stashing
31//! it for the next request. When the CA rejects a request with a
32//! `badNonce` error we automatically retry once with the fresh nonce the
33//! server returned in the same response.
34//!
35//! The HTTP transport is [`crate::http::client::https_request`], which is
36//! the caller-agnostic `tokio` + `tokio_rustls` + `HttpMessage` client
37//! also used for any other outbound HTTPS call in `fe2o3_net`. The caller
38//! supplies an `Arc<ClientConfig>` that pins the Let's Encrypt root
39//! anchors; see [`crate::acme::trust::letsencrypt_client_config`].
40//!
41//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
42//! Anthropic Claude
43
44use crate::{
45 acme::{
46 challenge::{
47 build_tls_alpn_01_cert,
48 ChallengeCert,
49 },
50 jose::{
51 base64url_encode,
52 JwsSigner,
53 },
54 rfc8555::{
55 finalize_request,
56 new_account_request,
57 new_order_request,
58 parse_json_response,
59 Authorization,
60 AuthorizationStatus,
61 Challenge,
62 ChallengeStatus,
63 Directory,
64 Order,
65 OrderStatus,
66 Problem,
67 },
68 },
69 http::{
70 client::https_request,
71 fields::{
72 HeaderFieldValue,
73 HeaderName,
74 },
75 header::HttpMethod,
76 msg::HttpMessage,
77 },
78};
79
80use oxedyne_fe2o3_core::prelude::*;
81use oxedyne_fe2o3_jdat::prelude::*;
82
83use std::{
84 sync::Arc,
85 time::Duration,
86};
87
88use rcgen::{
89 Certificate,
90 CertificateParams,
91 DistinguishedName,
92 DnType,
93};
94use tokio_rustls::rustls::ClientConfig;
95
96
97// ┌───────────────────────────────────────────────────────────────────────────┐
98// │ PUBLIC TYPES │
99// └───────────────────────────────────────────────────────────────────────────┘
100
101/// An installer callback that plugs and removes `tls-alpn-01` challenge
102/// certificates from the caller's live rustls cert resolver while an ACME
103/// issuance is in flight.
104///
105/// The methods are synchronous because the typical installer is an
106/// `Arc<RwLock<HashMap<String, Arc<CertifiedKey>>>>` whose inserts and
107/// removes are non-blocking, and keeping the trait synchronous avoids the
108/// ergonomic friction of `async fn` in traits.
109pub trait ChallengeInstaller: Send + Sync {
110
111 /// By the time this returns, any incoming TLS handshake for `hostname` that
112 /// advertises the `acme-tls/1` ALPN protocol must be answered with `cert`.
113 fn install(&self, hostname: &str, cert: &ChallengeCert) -> Outcome<()>;
114
115 /// Called once the CA has validated the challenge or given up on it, either
116 /// way.
117 fn remove(&self, hostname: &str) -> Outcome<()>;
118}
119
120/// A freshly-issued certificate chain plus its matching private key.
121///
122/// The key is **not** the ACME account key: it is the fresh P-256 pair rcgen
123/// minted while building the CSR, whose public half the CA therefore knows.
124#[derive(Clone, Debug)]
125pub struct IssuedCertificate {
126 pub cert_pem: Vec<u8>, // PEM chain, exactly as the CA sent it
127 pub key_pkcs8: Vec<u8>, // PKCS#8 DER, matching the leaf cert
128}
129
130
131// ┌───────────────────────────────────────────────────────────────────────────┐
132// │ ACME CLIENT │
133// └───────────────────────────────────────────────────────────────────────────┘
134
135/// ACME client state held across the steps of a single issuance.
136///
137/// The client is deliberately single-threaded: every method takes `&mut
138/// self` and every request must complete before the next one begins. This
139/// matches the protocol -- which is intrinsically serial because of the
140/// nonce chain -- and avoids any need for locks inside the client.
141pub struct AcmeClient {
142 directory_url: String, // full URL of the CA directory endpoint
143 contact_email: String, // the account is registered with this
144 tls_config: Arc<ClientConfig>, // trusts the CA's root anchors
145 signer: JwsSigner, // account key, minted or loaded by the caller
146 directory: Option<Directory>, // cached after the first fetch
147 kid: Option<String>, // account URL, set by register_account
148 nonce: Option<String>, // latest Replay-Nonce, spent on the next POST
149}
150
151impl AcmeClient {
152
153 /// Does no I/O; the directory and the first nonce are fetched lazily.
154 pub fn new(
155 directory_url: impl Into<String>,
156 contact_email: impl Into<String>,
157 tls_config: Arc<ClientConfig>,
158 signer: JwsSigner,
159 )
160 -> Self
161 {
162 Self {
163 directory_url: directory_url.into(),
164 contact_email: contact_email.into(),
165 tls_config,
166 signer,
167 directory: None,
168 kid: None,
169 nonce: None,
170 }
171 }
172
173 pub fn signer(&self) -> &JwsSigner {
174 &self.signer
175 }
176
177 /// The account URL the CA assigned, `None` until `register_account` has run.
178 pub fn kid(&self) -> Option<&str> {
179 self.kid.as_deref()
180 }
181
182 // ---- low-level helpers -----------------------------------------------
183
184 async fn ensure_directory(&mut self) -> Outcome<&Directory> {
185 if self.directory.is_none() {
186 let dir = res!(self.fetch_directory().await);
187 self.directory = Some(dir);
188 }
189 match &self.directory {
190 Some(d) => Ok(d),
191 None => Err(err!(
192 "Internal: ensure_directory left self.directory empty.";
193 Bug)),
194 }
195 }
196
197 async fn fetch_directory(&self) -> Outcome<Directory> {
198 let (host, port, path) = res!(split_https_url(&self.directory_url));
199 let msg = res!(https_request(
200 &host,
201 port,
202 HttpMethod::GET,
203 &path,
204 &[],
205 &[],
206 self.tls_config.clone(),
207 ).await);
208 res!(require_success(&msg, "GET directory"));
209 parse_json_response(&msg.body)
210 }
211
212 /// RFC 8555 §7.2 permits GET or HEAD on the new-nonce endpoint. GET is used
213 /// because the HTTP reader here always expects a body frame, possibly empty.
214 async fn refresh_nonce(&mut self) -> Outcome<()> {
215 let new_nonce_url = {
216 let dir = res!(self.ensure_directory().await);
217 dir.new_nonce.clone()
218 };
219 let (host, port, path) = res!(split_https_url(&new_nonce_url));
220 let msg = res!(https_request(
221 &host,
222 port,
223 HttpMethod::GET,
224 &path,
225 &[],
226 &[],
227 self.tls_config.clone(),
228 ).await);
229 res!(require_success(&msg, "GET new-nonce"));
230 self.nonce = Some(res!(read_replay_nonce(&msg, "new-nonce response")));
231 Ok(())
232 }
233
234 /// Fetches one first where none is stashed, the reply stashing it into
235 /// `self.nonce` for this call to take straight back out.
236 async fn take_nonce(&mut self) -> Outcome<String> {
237 if self.nonce.is_none() {
238 res!(self.refresh_nonce().await);
239 }
240 match self.nonce.take() {
241 Some(n) => Ok(n),
242 None => Err(err!(
243 "Internal: take_nonce found no nonce after refresh_nonce \
244 returned Ok.";
245 Bug)),
246 }
247 }
248
249 /// Only the new-account request signs under `jwk`, the CA knowing no account
250 /// URL for it yet.
251 fn sign_with_jwk(
252 &self,
253 url: &str,
254 nonce: &str,
255 payload: &Dat,
256 )
257 -> Outcome<Vec<u8>>
258 {
259 let jwk = res!(self.signer.public_jwk());
260 let header = mapdat!{
261 "alg" => "ES256",
262 "nonce" => nonce,
263 "url" => url,
264 "jwk" => jwk,
265 };
266 let payload_bytes = res!(payload.json()).into_bytes();
267 let jws = res!(self.signer.sign_flattened(&header, &payload_bytes));
268 Ok(res!(jws.json()).into_bytes())
269 }
270
271 /// Every authenticated request after `register_account` signs under `kid`.
272 fn sign_with_kid(
273 &self,
274 url: &str,
275 nonce: &str,
276 payload: &Dat,
277 )
278 -> Outcome<Vec<u8>>
279 {
280 let kid = match &self.kid {
281 Some(k) => k.clone(),
282 None => return Err(err!(
283 "sign_with_kid called before register_account; no account \
284 URL is known yet.";
285 Bug)),
286 };
287 let header = mapdat!{
288 "alg" => "ES256",
289 "nonce" => nonce,
290 "url" => url,
291 "kid" => kid,
292 };
293 let payload_bytes = res!(payload.json()).into_bytes();
294 let jws = res!(self.signer.sign_flattened(&header, &payload_bytes));
295 Ok(res!(jws.json()).into_bytes())
296 }
297
298 /// The empty-payload POST-as-GET of RFC 8555 §6.3, under a `kid` header.
299 fn sign_post_as_get(
300 &self,
301 url: &str,
302 nonce: &str,
303 )
304 -> Outcome<Vec<u8>>
305 {
306 let kid = match &self.kid {
307 Some(k) => k.clone(),
308 None => return Err(err!(
309 "sign_post_as_get called before register_account.";
310 Bug)),
311 };
312 let header = mapdat!{
313 "alg" => "ES256",
314 "nonce" => nonce,
315 "url" => url,
316 "kid" => kid,
317 };
318 let jws = res!(self.signer.sign_flattened(&header, b""));
319 Ok(res!(jws.json()).into_bytes())
320 }
321
322 /// `use_jwk` picks the `jwk` header of first contact over the `kid` of every
323 /// later request. The stashed nonce is updated from the reply, and a
324 /// `badNonce` from the server is retried once with the nonce it returned.
325 async fn post_jose(
326 &mut self,
327 url: &str,
328 payload: &Dat,
329 use_jwk: bool,
330 )
331 -> Outcome<HttpMessage>
332 {
333 let mut attempts_remaining: u8 = 2;
334 loop {
335 attempts_remaining -= 1;
336
337 let nonce = res!(self.take_nonce().await);
338 let body = if use_jwk {
339 res!(self.sign_with_jwk(url, &nonce, payload))
340 } else {
341 res!(self.sign_with_kid(url, &nonce, payload))
342 };
343
344 let (host, port, path) = res!(split_https_url(url));
345 let msg = res!(https_request(
346 &host,
347 port,
348 HttpMethod::POST,
349 &path,
350 &[("Content-Type", "application/jose+json")],
351 &body,
352 self.tls_config.clone(),
353 ).await);
354
355 // Stash the fresh nonce the CA gave us, if any, before doing
356 // anything else. This is required for the retry path as well
357 // as the normal success path.
358 if let Ok(n) = read_replay_nonce(&msg, "POST JOSE response") {
359 self.nonce = Some(n);
360 }
361
362 let status = http_status_code(&msg);
363 if status / 100 == 2 {
364 return Ok(msg);
365 }
366 if status == 400 && attempts_remaining > 0 {
367 // Only retry if the problem document indicates badNonce.
368 if let Ok(Some(problem)) = parse_problem_body(&msg.body) {
369 if problem.typ.ends_with(":badNonce") {
370 continue;
371 }
372 }
373 }
374 return Err(res!(acme_error_from_response(&msg, url)));
375 }
376 }
377
378 /// `post_jose` with an empty payload, still under `kid`.
379 async fn post_as_get(
380 &mut self,
381 url: &str,
382 )
383 -> Outcome<HttpMessage>
384 {
385 let mut attempts_remaining: u8 = 2;
386 loop {
387 attempts_remaining -= 1;
388
389 let nonce = res!(self.take_nonce().await);
390 let body = res!(self.sign_post_as_get(url, &nonce));
391
392 let (host, port, path) = res!(split_https_url(url));
393 let msg = res!(https_request(
394 &host,
395 port,
396 HttpMethod::POST,
397 &path,
398 &[("Content-Type", "application/jose+json")],
399 &body,
400 self.tls_config.clone(),
401 ).await);
402
403 if let Ok(n) = read_replay_nonce(&msg, "POST-as-GET response") {
404 self.nonce = Some(n);
405 }
406
407 let status = http_status_code(&msg);
408 if status / 100 == 2 {
409 return Ok(msg);
410 }
411 if status == 400 && attempts_remaining > 0 {
412 if let Ok(Some(problem)) = parse_problem_body(&msg.body) {
413 if problem.typ.ends_with(":badNonce") {
414 continue;
415 }
416 }
417 }
418 return Err(res!(acme_error_from_response(&msg, url)));
419 }
420 }
421
422 // ---- protocol steps --------------------------------------------------
423
424 /// Registers or recovers the account belonging to the signer key, storing
425 /// the reply's `Location` header as the `kid`.
426 pub async fn register_account(&mut self) -> Outcome<()> {
427 let new_account_url = {
428 let dir = res!(self.ensure_directory().await);
429 dir.new_account.clone()
430 };
431 let payload = new_account_request(&self.contact_email, true);
432 let msg = res!(self.post_jose(&new_account_url, &payload, true).await);
433
434 let kid = res!(read_location(&msg, "new-account response"));
435 self.kid = Some(kid);
436 Ok(())
437 }
438
439 /// The string is the order's `Location` URL, which is POST-as-GET'd to poll
440 /// the status.
441 pub async fn new_order(
442 &mut self,
443 dns_names: &[String],
444 )
445 -> Outcome<(String, Order)>
446 {
447 let new_order_url = {
448 let dir = res!(self.ensure_directory().await);
449 dir.new_order.clone()
450 };
451 let payload = new_order_request(dns_names);
452 let msg = res!(self.post_jose(&new_order_url, &payload, false).await);
453
454 let order_url = res!(read_location(&msg, "new-order response"));
455 let order: Order = res!(parse_json_response(&msg.body));
456 Ok((order_url, order))
457 }
458
459 pub async fn fetch_authorization(
460 &mut self,
461 authz_url: &str,
462 )
463 -> Outcome<Authorization>
464 {
465 let msg = res!(self.post_as_get(authz_url).await);
466 parse_json_response(&msg.body)
467 }
468
469 /// POSTs `{}` to the challenge URL, which is how readiness is signalled.
470 pub async fn signal_challenge_ready(
471 &mut self,
472 challenge_url: &str,
473 )
474 -> Outcome<Challenge>
475 {
476 let payload = mapdat!{};
477 let msg = res!(self.post_jose(challenge_url, &payload, false).await);
478 parse_json_response(&msg.body)
479 }
480
481 pub async fn poll_order(
482 &mut self,
483 order_url: &str,
484 )
485 -> Outcome<Order>
486 {
487 let msg = res!(self.post_as_get(order_url).await);
488 parse_json_response(&msg.body)
489 }
490
491 pub async fn finalize_order(
492 &mut self,
493 finalize_url: &str,
494 csr_der: &[u8],
495 )
496 -> Outcome<Order>
497 {
498 let csr_b64 = base64url_encode(csr_der);
499 let payload = finalize_request(&csr_b64);
500 let msg = res!(self.post_jose(finalize_url, &payload, false).await);
501 parse_json_response(&msg.body)
502 }
503
504 /// The body verbatim, an `application/pem-certificate-chain` PEM chain.
505 pub async fn download_certificate(
506 &mut self,
507 cert_url: &str,
508 )
509 -> Outcome<Vec<u8>>
510 {
511 let msg = res!(self.post_as_get(cert_url).await);
512 Ok(msg.body)
513 }
514
515 // ---- high-level driver -----------------------------------------------
516
517 /// Drives the whole RFC 8555 cycle, installing challenge certs through
518 /// `installer` and removing every one of them afterwards, success or not.
519 pub async fn issue_certificate<I: ChallengeInstaller>(
520 &mut self,
521 dns_names: &[String],
522 installer: &I,
523 )
524 -> Outcome<IssuedCertificate>
525 {
526 if dns_names.is_empty() {
527 return Err(err!(
528 "AcmeClient::issue_certificate called with an empty \
529 dns_names slice.";
530 Invalid, Input, Missing));
531 }
532
533 // Register account if we haven't already this session.
534 if self.kid.is_none() {
535 res!(self.register_account().await);
536 }
537
538 // Submit the order and fetch its authorisation URLs.
539 let (order_url, mut order) = res!(self.new_order(dns_names).await);
540
541 // RFC 8555 §7.1.3: act on the state the order actually arrived in.
542 // A brand new order is usually `pending`, but when the CA still holds
543 // cached validations for every name it can hand us one that is already
544 // `ready`, with no authorisation work left to do at all.
545 match res!(order_step(&order, &order_url)) {
546 OrderStep::Authorise => {
547 // Remember the hostnames we installed challenge certs for, so
548 // we can remove them all at the end regardless of success.
549 let mut installed_hosts: Vec<String> = Vec::new();
550
551 // Drive each authorisation to the "valid" state.
552 let drive_result = self.drive_all_authorisations(
553 &order,
554 installer,
555 &mut installed_hosts,
556 ).await;
557
558 // Uninstall challenge certs unconditionally.
559 for host in &installed_hosts {
560 if let Err(e) = installer.remove(host) {
561 // Log-worthy but not fatal; the issuance may still be
562 // on track.
563 warn!("ACME: installer.remove({:?}) failed: {:?}", host, e);
564 }
565 }
566 res!(drive_result);
567
568 // Poll the order until the CA marks it ready to be finalised.
569 order = res!(self.poll_until_ready(&order_url).await);
570 },
571 OrderStep::Finalise => {
572 info!(
573 "ACME: order for {:?} arrived 'ready'; every \
574 authorisation was already valid, going straight to \
575 finalisation.", dns_names,
576 );
577 },
578 }
579
580 // Build the CSR key pair for the end-entity cert, generate the
581 // CSR, and finalise the order. We do not bind the finalise reply
582 // to a local because the subsequent poll loop re-reads the order
583 // anyway; we only care that the POST returned 2xx.
584 let (csr_der, key_pkcs8) = res!(build_csr(dns_names));
585 let _ = res!(self.finalize_order(&order.finalize, &csr_der).await);
586
587 // Poll until valid.
588 let order = res!(self.poll_until_valid(&order_url).await);
589
590 if order.certificate.is_empty() {
591 return Err(err!(
592 "Order reached status 'valid' but did not include a \
593 certificate URL.";
594 IO, Network, Missing, Invalid));
595 }
596 let cert_pem = res!(self.download_certificate(&order.certificate).await);
597
598 Ok(IssuedCertificate {
599 cert_pem,
600 key_pkcs8,
601 })
602 }
603
604 /// Walk every authorisation attached to `order` and bring each one to the
605 /// `valid` state, doing only the work its current status calls for.
606 ///
607 /// An authorisation that is already `valid` is skipped without a challenge
608 /// POST: the CA caches successful validations (RFC 8555 §7.1.4), so this
609 /// is the normal shape of any order placed after a previous issuance for
610 /// the same name got as far as validating it.
611 async fn drive_all_authorisations<I: ChallengeInstaller>(
612 &mut self,
613 order: &Order,
614 installer: &I,
615 installed_hosts: &mut Vec<String>,
616 )
617 -> Outcome<()>
618 {
619 for authz_url in &order.authorizations {
620 let authz = res!(self.fetch_authorization(authz_url).await);
621
622 // We only satisfy DNS identifiers via tls-alpn-01.
623 let hostname = res!(dns_identifier(&authz));
624
625 let step = res!(authz_step(&authz, &hostname));
626 // Whether to signal readiness is decided here, with the challenge borrowed rather than
627 // taken, because the challenge is needed either way and the decision is needed after.
628 let (chall, prove) = match &step {
629 AuthzStep::Skip => {
630 // Already validated by the CA; nothing to prove. POSTing
631 // the challenge here is exactly what Boulder answers with
632 // `400 malformed`.
633 info!(
634 "ACME: authorisation for {:?} is already valid; \
635 no challenge needed.", hostname,
636 );
637 continue;
638 },
639 AuthzStep::Prove(c) => (c, true),
640 AuthzStep::AwaitValidation(c) => (c, false),
641 };
642
643 // The challenge cert has to be reachable before the CA looks, and
644 // it must stay up while a validation that is already in flight
645 // completes -- so we install it for `AwaitValidation` too.
646 let thumbprint = res!(self.signer.jwk_thumbprint_sha256());
647 let key_auth = chall.key_authorization(&thumbprint);
648 let cert = res!(build_tls_alpn_01_cert(&hostname, &key_auth));
649
650 res!(installer.install(&hostname, &cert));
651 installed_hosts.push(hostname.clone());
652
653 // Only signal readiness on a challenge that is still `pending`.
654 // Re-POSTing one the CA is already validating is at best wasted
655 // and at worst rejected.
656 if prove {
657 let _ = res!(self.signal_challenge_ready(&chall.url).await);
658 }
659
660 // Poll the authorisation itself until it reaches a terminal state.
661 let final_authz = res!(self.poll_authorisation_until_final(authz_url).await);
662 match res!(final_authz.typed_status()) {
663 AuthorizationStatus::Valid => (),
664 other => return Err(err!(
665 "Authorisation for {:?} ended in status {:?} instead of \
666 'valid'.", hostname, other.as_wire();
667 IO, Network, Invalid)),
668 }
669 }
670 Ok(())
671 }
672
673 async fn poll_authorisation_until_final(
674 &mut self,
675 authz_url: &str,
676 )
677 -> Outcome<Authorization>
678 {
679 for _ in 0..POLL_MAX_ATTEMPTS {
680 let authz = res!(self.fetch_authorization(authz_url).await);
681 // RFC 8555 §7.1.6: an authorisation has no `processing` state; it
682 // leaves `pending` straight for a terminal one.
683 match res!(authz.typed_status()) {
684 AuthorizationStatus::Pending => (),
685 _ => return Ok(authz),
686 }
687 tokio::time::sleep(POLL_INTERVAL).await;
688 }
689 Err(err!(
690 "Authorisation {:?} did not leave 'pending' within {} poll \
691 attempts.", authz_url, POLL_MAX_ATTEMPTS;
692 IO, Network, Timeout))
693 }
694
695 /// `valid` returns as well as `ready`: an order the CA finalised while this
696 /// was polling is past ready, not short of it.
697 async fn poll_until_ready(
698 &mut self,
699 order_url: &str,
700 )
701 -> Outcome<Order>
702 {
703 for _ in 0..POLL_MAX_ATTEMPTS {
704 let order = res!(self.poll_order(order_url).await);
705 match res!(order.typed_status()) {
706 OrderStatus::Ready | OrderStatus::Valid => return Ok(order),
707 OrderStatus::Invalid => return Err(res!(order_invalid_error(
708 &order,
709 order_url,
710 "while waiting for authorisations to complete",
711 ))),
712 // Still settling: the CA has not yet caught up with the
713 // authorisations we just satisfied.
714 OrderStatus::Pending | OrderStatus::Processing => (),
715 }
716 tokio::time::sleep(POLL_INTERVAL).await;
717 }
718 Err(err!(
719 "Order {:?} did not reach 'ready' within {} poll attempts.",
720 order_url, POLL_MAX_ATTEMPTS;
721 IO, Network, Timeout))
722 }
723
724 async fn poll_until_valid(
725 &mut self,
726 order_url: &str,
727 )
728 -> Outcome<Order>
729 {
730 for _ in 0..POLL_MAX_ATTEMPTS {
731 let order = res!(self.poll_order(order_url).await);
732 match res!(order.typed_status()) {
733 OrderStatus::Valid => return Ok(order),
734 OrderStatus::Invalid => return Err(res!(order_invalid_error(
735 &order,
736 order_url,
737 "during finalisation",
738 ))),
739 // `processing` is the CA issuing; `pending` and `ready` mean
740 // it has not yet registered our CSR.
741 OrderStatus::Pending
742 | OrderStatus::Ready
743 | OrderStatus::Processing => (),
744 }
745 tokio::time::sleep(POLL_INTERVAL).await;
746 }
747 Err(err!(
748 "Order {:?} did not reach 'valid' within {} poll attempts \
749 after finalisation.",
750 order_url, POLL_MAX_ATTEMPTS;
751 IO, Network, Timeout))
752 }
753}
754
755
756// ┌───────────────────────────────────────────────────────────────────────────┐
757// │ CONSTANTS │
758// └───────────────────────────────────────────────────────────────────────────┘
759
760const POLL_INTERVAL: Duration = Duration::from_secs(2);
761
762const POLL_MAX_ATTEMPTS: u32 = 30; // 30 x 2 s, about a minute per transition
763
764
765// ┌───────────────────────────────────────────────────────────────────────────┐
766// │ HELPERS │
767// └───────────────────────────────────────────────────────────────────────────┘
768
769/// Splits `https://host[:port]/path...` into host, port and path.
770///
771/// An IPv6 literal host, `https://[::1]/path`, is not supported: ACME traffic
772/// goes to DNS names in practice, and the bracketed form costs more parser than
773/// it is worth.
774pub(super) fn split_https_url(url: &str) -> Outcome<(String, u16, String)> {
775 let rest = match url.strip_prefix("https://") {
776 Some(r) => r,
777 None => return Err(err!(
778 "URL {:?} does not start with the https:// scheme.", url;
779 Invalid, Input, Mismatch)),
780 };
781 let (authority, path) = match rest.find('/') {
782 Some(pos) => (&rest[..pos], &rest[pos..]),
783 None => (rest, "/"),
784 };
785 if authority.is_empty() {
786 return Err(err!(
787 "URL {:?} has an empty authority component.", url;
788 Invalid, Input, Missing));
789 }
790 let (host, port) = match authority.rfind(':') {
791 Some(pos) => {
792 let port_str = &authority[pos + 1..];
793 let port: u16 = match port_str.parse() {
794 Ok(p) => p,
795 Err(e) => return Err(err!(e,
796 "URL {:?} has an invalid port {:?}.", url, port_str;
797 Invalid, Input, Mismatch)),
798 };
799 (&authority[..pos], port)
800 },
801 None => (authority, 443u16),
802 };
803 Ok((host.to_string(), port, path.to_string()))
804}
805
806fn http_status_code(msg: &HttpMessage) -> u16 {
807 match &msg.header.headline {
808 crate::http::header::HttpHeadline::Response { status } => *status as u16,
809 _ => 0,
810 }
811}
812
813fn read_replay_nonce(msg: &HttpMessage, context: &str) -> Outcome<String> {
814 match msg.header.get_a_field_value(&HeaderName::ReplayNonce) {
815 Some(HeaderFieldValue::Generic(s)) => Ok(s.clone()),
816 Some(other) => Err(err!(
817 "{}: Replay-Nonce header had unexpected parsed form {:?}.",
818 context, other;
819 IO, Network, Invalid, Mismatch)),
820 None => Err(err!(
821 "{}: Replay-Nonce header was missing from the response.", context;
822 IO, Network, Missing)),
823 }
824}
825
826fn read_location(msg: &HttpMessage, context: &str) -> Outcome<String> {
827 match msg.header.get_a_field_value(&HeaderName::Location) {
828 Some(HeaderFieldValue::Generic(s)) => Ok(s.clone()),
829 Some(other) => Err(err!(
830 "{}: Location header had unexpected parsed form {:?}.",
831 context, other;
832 IO, Network, Invalid, Mismatch)),
833 None => Err(err!(
834 "{}: Location header was missing from the response.", context;
835 IO, Network, Missing)),
836 }
837}
838
839/// Anything but a 2xx becomes an error with the embedded ACME problem document
840/// folded into its message.
841fn require_success(msg: &HttpMessage, context: &str) -> Outcome<()> {
842 let status = http_status_code(msg);
843 if status / 100 == 2 {
844 return Ok(());
845 }
846 Err(res!(acme_error_from_response(msg, context)))
847}
848
849/// A body that parses as a Problem document contributes its `type` and `detail`
850/// to the error text.
851fn acme_error_from_response(
852 msg: &HttpMessage,
853 context: &str,
854)
855 -> Outcome<Error<ErrTag>>
856{
857 let status = http_status_code(msg);
858 let mut message = fmt!(
859 "ACME server returned status {} on {}.",
860 status, context,
861 );
862 if let Ok(Some(problem)) = parse_problem_body(&msg.body) {
863 message.push_str(&fmt!(
864 " Problem type: {:?}, detail: {:?}.",
865 problem.typ, problem.detail,
866 ));
867 }
868 Ok(err!(message.clone(); IO, Network, Unknown))
869}
870
871/// RFC 7807. `Ok(None)` where the body is empty or is not a JSON object, since a
872/// missing problem document is not itself a failure.
873fn parse_problem_body(body: &[u8]) -> Outcome<Option<Problem>> {
874 if body.is_empty() {
875 return Ok(None);
876 }
877 // Try to parse. If it fails, treat as no problem document.
878 match parse_json_response::<Problem>(body) {
879 Ok(p) => Ok(Some(p)),
880 Err(_) => Ok(None),
881 }
882}
883
884// ┌───────────────────────────────────────────────────────────────────────────┐
885// │ STATE DECISIONS (RFC 8555 §7.1.3, §7.1.4, §7.1.6) │
886// └───────────────────────────────────────────────────────────────────────────┘
887//
888// The two functions below are the whole of the client's protocol reasoning,
889// deliberately kept pure: they take a parsed order or authorisation and say
890// what must happen next, with no I/O anywhere near them. The async drivers are
891// thin executors of their verdict. That split is what makes the state machine
892// testable against captured CA responses without a network.
893
894/// What the client must do with a single authorisation.
895#[derive(Clone, Debug)]
896pub(super) enum AuthzStep {
897 Skip, // already validated, nothing left to prove
898 Prove(Challenge), // install the cert, then POST the challenge
899 AwaitValidation(Challenge), // keep the cert up and poll, but do not re-POST
900}
901
902/// What the client must do with a freshly-created order.
903#[derive(Clone, Copy, Debug, Eq, PartialEq)]
904pub(super) enum OrderStep {
905 Authorise, // authorisations are outstanding and must be satisfied first
906 Finalise, // every authorisation is already valid, go straight to the CSR
907}
908
909/// RFC 8555 §7.1.4 and §7.1.6.
910///
911/// The `valid` arm is the one that matters: a CA caches successful validations
912/// (Let's Encrypt for roughly 30 days), so any order placed after an issuance
913/// that got as far as validating a name will carry an authorisation that is
914/// already `valid`. Treating that as though it were `pending` -- which is what
915/// an unconditional challenge POST amounts to -- turns a single transient
916/// failure into a permanent one, because every retry thereafter POSTs a
917/// challenge whose authorisation is no longer pending and is refused.
918pub(super) fn authz_step(
919 authz: &Authorization,
920 hostname: &str,
921)
922 -> Outcome<AuthzStep>
923{
924 match res!(authz.typed_status()) {
925 // Nothing to do. This is the cached-validation case.
926 AuthorizationStatus::Valid => Ok(AuthzStep::Skip),
927
928 // Dead authorisations. Naming both the domain and the status matters:
929 // this error is the only thing an operator will see, and "invalid" and
930 // "expired" call for quite different responses.
931 AuthorizationStatus::Invalid
932 | AuthorizationStatus::Expired
933 | AuthorizationStatus::Revoked
934 | AuthorizationStatus::Deactivated => {
935 let status = res!(authz.typed_status());
936 let mut msg = fmt!(
937 "Authorisation for {:?} is in status {:?} and cannot be \
938 satisfied; a new order is required.",
939 hostname, status.as_wire(),
940 );
941 // Surface the CA's own explanation when it attached one to the
942 // challenge that failed.
943 if let Ok(Some(chall)) = authz.tls_alpn_01_challenge() {
944 if let Ok(Some(problem)) = chall.typed_error() {
945 msg.push_str(&fmt!(
946 " CA problem type: {:?}, detail: {:?}.",
947 problem.typ, problem.detail,
948 ));
949 }
950 }
951 Err(err!(msg.clone(); IO, Network, Invalid))
952 },
953
954 // The ordinary first-issuance path: something still has to be proved.
955 AuthorizationStatus::Pending => {
956 let chall = match res!(authz.tls_alpn_01_challenge()) {
957 Some(c) => c,
958 None => return Err(err!(
959 "Authorisation for {:?} did not offer a tls-alpn-01 \
960 challenge.", hostname;
961 IO, Network, Missing, Invalid)),
962 };
963 if chall.token.is_empty() {
964 return Err(err!(
965 "tls-alpn-01 challenge for {:?} has an empty token.",
966 hostname;
967 IO, Network, Missing, Invalid));
968 }
969 match res!(chall.typed_status()) {
970 ChallengeStatus::Pending => {
971 if chall.url.is_empty() {
972 return Err(err!(
973 "tls-alpn-01 challenge for {:?} has an empty url, \
974 so readiness cannot be signalled.", hostname;
975 IO, Network, Missing, Invalid));
976 }
977 Ok(AuthzStep::Prove(chall))
978 },
979 // Already signalled, or already validated while the enclosing
980 // authorisation has yet to catch up: either way, keep the cert
981 // up and wait rather than POSTing again.
982 ChallengeStatus::Processing
983 | ChallengeStatus::Valid => Ok(AuthzStep::AwaitValidation(chall)),
984 ChallengeStatus::Invalid => {
985 let mut msg = fmt!(
986 "The tls-alpn-01 challenge for {:?} is in status \
987 'invalid'; a new order is required.", hostname,
988 );
989 if let Ok(Some(problem)) = chall.typed_error() {
990 msg.push_str(&fmt!(
991 " CA problem type: {:?}, detail: {:?}.",
992 problem.typ, problem.detail,
993 ));
994 }
995 Err(err!(msg.clone(); IO, Network, Invalid))
996 },
997 }
998 },
999 }
1000}
1001
1002/// RFC 8555 §7.1.3.
1003///
1004/// `pending` and `ready` are the two states a `newOrder` reply can legitimately
1005/// arrive in. The other three are handled explicitly rather than swept into a
1006/// catch-all, because each means something quite specific has gone sideways.
1007pub(super) fn order_step(
1008 order: &Order,
1009 order_url: &str,
1010)
1011 -> Outcome<OrderStep>
1012{
1013 match res!(order.typed_status()) {
1014 OrderStatus::Pending => Ok(OrderStep::Authorise),
1015
1016 // Every authorisation is already valid, from the CA's cache. There is
1017 // no challenge to answer -- the order wants a CSR and nothing else.
1018 OrderStatus::Ready => Ok(OrderStep::Finalise),
1019
1020 OrderStatus::Invalid => Err(res!(order_invalid_error(
1021 order,
1022 order_url,
1023 "on creation",
1024 ))),
1025
1026 // Both of these mean the order has already been finalised, against a
1027 // CSR whose private key belonged to some earlier issuance attempt.
1028 // That key is not recoverable here (we mint a fresh one per issuance),
1029 // so the certificate at the far end of this order is unusable to us:
1030 // installing it would mean serving a chain whose private key we do not
1031 // hold, and every TLS handshake would fail. Erroring lets the caller's
1032 // retry place a clean order, which is the recoverable outcome.
1033 OrderStatus::Processing
1034 | OrderStatus::Valid => {
1035 let status = res!(order.typed_status());
1036 Err(err!(
1037 "Order {:?} arrived in status {:?}, meaning it was already \
1038 finalised against a CSR from an earlier attempt whose private \
1039 key this process does not hold. The resulting certificate \
1040 cannot be served. A fresh order is required.",
1041 order_url, status.as_wire();
1042 IO, Network, Invalid, Conflict))
1043 },
1044 }
1045}
1046
1047/// Folds in the CA's problem document where one is attached; RFC 8555 §7.1.3
1048/// puts it on `error`.
1049fn order_invalid_error(
1050 order: &Order,
1051 order_url: &str,
1052 context: &str,
1053)
1054 -> Outcome<Error<ErrTag>>
1055{
1056 let mut msg = fmt!(
1057 "Order {:?} transitioned to 'invalid' {}.", order_url, context,
1058 );
1059 match order.typed_error() {
1060 Ok(Some(problem)) => msg.push_str(&fmt!(
1061 " CA problem type: {:?}, title: {:?}, detail: {:?}.",
1062 problem.typ, problem.title, problem.detail,
1063 )),
1064 Ok(None) => msg.push_str(" The CA attached no problem document."),
1065 Err(e) => msg.push_str(&fmt!(
1066 " The CA's problem document could not be parsed: {:?}.", e,
1067 )),
1068 }
1069 Ok(err!(msg.clone(); IO, Network, Invalid))
1070}
1071
1072/// Errors unless the identifier is of type `dns`, which is the only kind this
1073/// client can satisfy.
1074fn dns_identifier(authz: &Authorization) -> Outcome<String> {
1075 match &authz.identifier {
1076 Dat::Map(m) => {
1077 let typ = match m.get(&dat!("type")) {
1078 Some(Dat::Str(s)) => s.clone(),
1079 _ => return Err(err!(
1080 "Authorisation identifier has no `type` field.";
1081 IO, Network, Missing, Invalid)),
1082 };
1083 if typ != "dns" {
1084 return Err(err!(
1085 "Authorisation identifier type {:?} is not `dns`.", typ;
1086 IO, Network, Invalid, Mismatch));
1087 }
1088 match m.get(&dat!("value")) {
1089 Some(Dat::Str(s)) => Ok(s.clone()),
1090 _ => Err(err!(
1091 "Authorisation identifier has no `value` field.";
1092 IO, Network, Missing, Invalid)),
1093 }
1094 },
1095 other => Err(err!(
1096 "Authorisation identifier is not a JSON object; got {:?}.", other;
1097 IO, Network, Invalid, Mismatch)),
1098 }
1099}
1100
1101/// A CSR over a fresh P-256 key pair, returned as `(csr_der, key_pkcs8_der)`.
1102///
1103/// `rcgen::CertificateParams::new` defaults the distinguished name's CommonName
1104/// to the literal `"rcgen self signed cert"`, which Let's Encrypt rejects at the
1105/// finalise step with `rejectedIdentifier: Domain name contains an invalid
1106/// character`: LE reads the CN as a candidate domain identifier and that string
1107/// has spaces in it. The distinguished name is replaced with one whose CN is the
1108/// first DNS name requested, matching the CN to a valid SAN.
1109fn build_csr(dns_names: &[String]) -> Outcome<(Vec<u8>, Vec<u8>)> {
1110 let mut params = CertificateParams::new(dns_names.to_vec());
1111 let mut dn = DistinguishedName::new();
1112 if let Some(first) = dns_names.first() {
1113 dn.push(DnType::CommonName, first.clone());
1114 }
1115 params.distinguished_name = dn;
1116 let cert = match Certificate::from_params(params) {
1117 Ok(c) => c,
1118 Err(e) => return Err(err!(e,
1119 "rcgen::Certificate::from_params failed while building an \
1120 ACME CSR for {:?}.", dns_names;
1121 Init, Invalid)),
1122 };
1123 let csr_der = match cert.serialize_request_der() {
1124 Ok(b) => b,
1125 Err(e) => return Err(err!(e,
1126 "rcgen::Certificate::serialize_request_der failed while \
1127 building an ACME CSR for {:?}.", dns_names;
1128 Init, Invalid)),
1129 };
1130 let key_pkcs8 = cert.serialize_private_key_der();
1131 Ok((csr_der, key_pkcs8))
1132}
1133
1134
1135// ┌───────────────────────────────────────────────────────────────────────────┐
1136// │ TESTS │
1137// └───────────────────────────────────────────────────────────────────────────┘
1138
1139#[cfg(test)]
1140mod tests {
1141 use super::*;
1142
1143 // ---- authorisation state machine (RFC 8555 §7.1.4, §7.1.6) -----------
1144
1145 /// Build an authorisation body with the given authorisation status and
1146 /// tls-alpn-01 challenge status, shaped as Let's Encrypt actually sends
1147 /// them.
1148 fn authz_json(authz_status: &str, chall_status: &str) -> Vec<u8> {
1149 fmt!(r#"{{
1150 "status": "{}",
1151 "expires": "2026-08-01T12:00:00Z",
1152 "identifier": {{"type":"dns","value":"example.com"}},
1153 "challenges": [
1154 {{
1155 "type": "http-01",
1156 "status": "{}",
1157 "url": "https://acme-v02.api.letsencrypt.org/acme/chall/1/a",
1158 "token": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1159 }},
1160 {{
1161 "type": "tls-alpn-01",
1162 "status": "{}",
1163 "url": "https://acme-v02.api.letsencrypt.org/acme/chall/1/c",
1164 "token": "cccccccccccccccccccccccccccccccc"
1165 }}
1166 ]
1167 }}"#, authz_status, chall_status, chall_status).into_bytes()
1168 }
1169
1170 /// **Defect regression.** An authorisation the CA has already validated --
1171 /// which is what a renewal order carries, because Let's Encrypt caches a
1172 /// successful validation for around 30 days -- must be skipped outright.
1173 ///
1174 /// The old code POSTed the challenge unconditionally. Boulder answers a
1175 /// POST to a challenge of a non-pending authorisation with `400 malformed`,
1176 /// so every retry after a transient mid-issuance failure failed the same
1177 /// way, every 24 hours, until the certificate expired.
1178 #[test]
1179 fn test_authz_step_skips_already_valid_authorisation() -> Outcome<()> {
1180 let authz: Authorization = res!(parse_json_response(
1181 &authz_json("valid", "valid")));
1182 match res!(authz_step(&authz, "example.com")) {
1183 AuthzStep::Skip => Ok(()),
1184 other => Err(err!(
1185 "A `valid` authorisation must be skipped, not challenged; \
1186 authz_step returned {:?}.", other;
1187 Test, Mismatch)),
1188 }
1189 }
1190
1191 /// The ordinary first-issuance path: a pending authorisation with a
1192 /// pending challenge must still be proved.
1193 #[test]
1194 fn test_authz_step_proves_pending_authorisation() -> Outcome<()> {
1195 let authz: Authorization = res!(parse_json_response(
1196 &authz_json("pending", "pending")));
1197 match res!(authz_step(&authz, "example.com")) {
1198 AuthzStep::Prove(c) => {
1199 if c.token != "cccccccccccccccccccccccccccccccc" {
1200 return Err(err!(
1201 "authz_step selected the wrong challenge: token {:?}.",
1202 c.token;
1203 Test, Mismatch));
1204 }
1205 if !c.url.ends_with("/chall/1/c") {
1206 return Err(err!(
1207 "authz_step selected the wrong challenge: url {:?}.",
1208 c.url;
1209 Test, Mismatch));
1210 }
1211 Ok(())
1212 },
1213 other => Err(err!(
1214 "A `pending` authorisation must be proved; authz_step \
1215 returned {:?}.", other;
1216 Test, Mismatch)),
1217 }
1218 }
1219
1220 /// A challenge already being validated must not be POSTed a second time,
1221 /// but its cert must still be installed -- so the step is
1222 /// `AwaitValidation`, not `Prove` and not `Skip`.
1223 #[test]
1224 fn test_authz_step_awaits_processing_challenge() -> Outcome<()> {
1225 let authz: Authorization = res!(parse_json_response(
1226 &authz_json("pending", "processing")));
1227 match res!(authz_step(&authz, "example.com")) {
1228 AuthzStep::AwaitValidation(_) => Ok(()),
1229 other => Err(err!(
1230 "A `processing` challenge must be awaited, not re-POSTed; \
1231 authz_step returned {:?}.", other;
1232 Test, Mismatch)),
1233 }
1234 }
1235
1236 /// Every dead authorisation status must produce an error that names both
1237 /// the domain and the status -- never a silent skip, which would let the
1238 /// client sail on to finalisation and fail confusingly later.
1239 #[test]
1240 fn test_authz_step_errors_on_dead_statuses() -> Outcome<()> {
1241 for status in ["invalid", "expired", "revoked", "deactivated"] {
1242 let authz: Authorization = res!(parse_json_response(
1243 &authz_json(status, "invalid")));
1244 match authz_step(&authz, "example.com") {
1245 Ok(step) => return Err(err!(
1246 "Authorisation status {:?} must be an error, but \
1247 authz_step returned {:?}.", status, step;
1248 Test, Mismatch)),
1249 Err(e) => {
1250 let msg = fmt!("{}", e);
1251 if !msg.contains("example.com") {
1252 return Err(err!(
1253 "The error for status {:?} does not name the \
1254 domain: {}", status, msg;
1255 Test, Missing));
1256 }
1257 if !msg.contains(status) {
1258 return Err(err!(
1259 "The error for status {:?} does not name the \
1260 status: {}", status, msg;
1261 Test, Missing));
1262 }
1263 },
1264 }
1265 }
1266 Ok(())
1267 }
1268
1269 /// When the CA explains why a challenge failed, that explanation must
1270 /// reach the operator rather than being swallowed.
1271 #[test]
1272 fn test_authz_step_surfaces_ca_problem_document() -> Outcome<()> {
1273 let body = br#"{
1274 "status": "invalid",
1275 "identifier": {"type":"dns","value":"example.com"},
1276 "challenges": [
1277 {
1278 "type": "tls-alpn-01",
1279 "status": "invalid",
1280 "url": "https://acme-v02.api.letsencrypt.org/acme/chall/1/c",
1281 "token": "cccccccccccccccccccccccccccccccc",
1282 "error": {
1283 "type": "urn:ietf:params:acme:error:unauthorized",
1284 "detail": "Timeout during connect (likely firewall problem)",
1285 "status": 403
1286 }
1287 }
1288 ]
1289 }"#;
1290 let authz: Authorization = res!(parse_json_response(body));
1291 match authz_step(&authz, "example.com") {
1292 Ok(step) => Err(err!(
1293 "An invalid authorisation must error, got {:?}.", step;
1294 Test, Mismatch)),
1295 Err(e) => {
1296 let msg = fmt!("{}", e);
1297 if !msg.contains("Timeout during connect") {
1298 return Err(err!(
1299 "The CA's problem detail was not surfaced: {}", msg;
1300 Test, Missing));
1301 }
1302 Ok(())
1303 },
1304 }
1305 }
1306
1307 /// A pending authorisation that offers no tls-alpn-01 challenge at all is
1308 /// unsatisfiable by this client and must say so.
1309 #[test]
1310 fn test_authz_step_errors_when_no_tls_alpn_challenge() -> Outcome<()> {
1311 let body = br#"{
1312 "status": "pending",
1313 "identifier": {"type":"dns","value":"example.com"},
1314 "challenges": [
1315 {
1316 "type": "http-01",
1317 "status": "pending",
1318 "url": "https://acme-v02.api.letsencrypt.org/acme/chall/1/a",
1319 "token": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1320 }
1321 ]
1322 }"#;
1323 let authz: Authorization = res!(parse_json_response(body));
1324 match authz_step(&authz, "example.com") {
1325 Ok(step) => Err(err!(
1326 "Expected an error when no tls-alpn-01 challenge is offered, \
1327 got {:?}.", step;
1328 Test, Mismatch)),
1329 Err(_) => Ok(()),
1330 }
1331 }
1332
1333 // ---- order state machine (RFC 8555 §7.1.3) ---------------------------
1334
1335 /// Build an order body in the given status.
1336 fn order_json(status: &str, extra: &str) -> Vec<u8> {
1337 fmt!(r#"{{
1338 "status": "{}",
1339 "identifiers": [{{"type":"dns","value":"example.com"}}],
1340 "authorizations": ["https://acme-v02.api.letsencrypt.org/acme/authz/1"],
1341 "finalize": "https://acme-v02.api.letsencrypt.org/acme/finalize/1"{}
1342 }}"#, status, extra).into_bytes()
1343 }
1344
1345 /// A `pending` order means authorisations are outstanding.
1346 #[test]
1347 fn test_order_step_pending_authorises() -> Outcome<()> {
1348 let order: Order = res!(parse_json_response(&order_json("pending", "")));
1349 let step = res!(order_step(&order, "https://example.test/order/1"));
1350 if step != OrderStep::Authorise {
1351 return Err(err!(
1352 "A `pending` order must be authorised, got {:?}.", step;
1353 Test, Mismatch));
1354 }
1355 Ok(())
1356 }
1357
1358 /// **Defect regression.** An order that arrives already `ready` -- every
1359 /// authorisation validated from the CA's cache -- must go straight to
1360 /// finalisation, with no challenge POSTed for any of its authorisations.
1361 #[test]
1362 fn test_order_step_ready_goes_straight_to_finalisation() -> Outcome<()> {
1363 let order: Order = res!(parse_json_response(&order_json("ready", "")));
1364 let step = res!(order_step(&order, "https://example.test/order/1"));
1365 if step != OrderStep::Finalise {
1366 return Err(err!(
1367 "A `ready` order must go straight to finalisation, got {:?}.",
1368 step;
1369 Test, Mismatch));
1370 }
1371 Ok(())
1372 }
1373
1374 /// An `invalid` order must error, and must carry the CA's problem
1375 /// document into the message rather than dropping it.
1376 #[test]
1377 fn test_order_step_invalid_surfaces_problem_document() -> Outcome<()> {
1378 let extra = r#",
1379 "error": {
1380 "type": "urn:ietf:params:acme:error:rateLimited",
1381 "title": "Too many certificates already issued",
1382 "detail": "too many certificates already issued for example.com",
1383 "status": 429
1384 }"#;
1385 let order: Order = res!(parse_json_response(&order_json("invalid", extra)));
1386 match order_step(&order, "https://example.test/order/1") {
1387 Ok(step) => Err(err!(
1388 "An `invalid` order must error, got {:?}.", step;
1389 Test, Mismatch)),
1390 Err(e) => {
1391 let msg = fmt!("{}", e);
1392 if !msg.contains("too many certificates already issued") {
1393 return Err(err!(
1394 "The CA's problem detail was not surfaced: {}", msg;
1395 Test, Missing));
1396 }
1397 if !msg.contains("rateLimited") {
1398 return Err(err!(
1399 "The CA's problem type was not surfaced: {}", msg;
1400 Test, Missing));
1401 }
1402 Ok(())
1403 },
1404 }
1405 }
1406
1407 /// An order that is already `valid` or `processing` was finalised against
1408 /// a CSR from an earlier attempt, whose private key we do not hold. Its
1409 /// certificate is therefore unusable and must not be quietly returned:
1410 /// serving a chain whose key we lack would fail every TLS handshake while
1411 /// looking, to the renewal check, like a perfectly fresh certificate.
1412 #[test]
1413 fn test_order_step_errors_on_already_finalised_order() -> Outcome<()> {
1414 for status in ["valid", "processing"] {
1415 let extra = r#",
1416 "certificate": "https://acme-v02.api.letsencrypt.org/acme/cert/abc""#;
1417 let order: Order = res!(parse_json_response(&order_json(status, extra)));
1418 match order_step(&order, "https://example.test/order/1") {
1419 Ok(step) => return Err(err!(
1420 "An order in status {:?} must error rather than yield \
1421 {:?}.", status, step;
1422 Test, Mismatch)),
1423 Err(e) => {
1424 let msg = fmt!("{}", e);
1425 if !msg.contains(status) {
1426 return Err(err!(
1427 "The error for an order in status {:?} does not \
1428 name the status: {}", status, msg;
1429 Test, Missing));
1430 }
1431 },
1432 }
1433 }
1434 Ok(())
1435 }
1436
1437 /// A status the RFC does not define must be refused outright rather than
1438 /// silently treated as one we know.
1439 #[test]
1440 fn test_order_step_rejects_unknown_status() -> Outcome<()> {
1441 let order: Order = res!(parse_json_response(&order_json("frobnicating", "")));
1442 match order_step(&order, "https://example.test/order/1") {
1443 Ok(step) => Err(err!(
1444 "An unknown order status must error, got {:?}.", step;
1445 Test, Mismatch)),
1446 Err(_) => Ok(()),
1447 }
1448 }
1449
1450 // ---- URL splitting ---------------------------------------------------
1451
1452 /// A bare hostname-only URL must parse to (host, 443, "/").
1453 #[test]
1454 fn test_split_url_default_port_root_path() -> Outcome<()> {
1455 let (host, port, path) = res!(split_https_url("https://acme.example"));
1456 if host != "acme.example" || port != 443 || path != "/" {
1457 return Err(err!(
1458 "Parsed as {:?}, {}, {:?}.", host, port, path;
1459 Test, Mismatch));
1460 }
1461 Ok(())
1462 }
1463
1464 /// Host + path must preserve the path verbatim.
1465 #[test]
1466 fn test_split_url_with_path() -> Outcome<()> {
1467 let (host, port, path) = res!(split_https_url(
1468 "https://acme-v02.api.letsencrypt.org/directory"));
1469 if host != "acme-v02.api.letsencrypt.org"
1470 || port != 443
1471 || path != "/directory"
1472 {
1473 return Err(err!(
1474 "Parsed as {:?}, {}, {:?}.", host, port, path;
1475 Test, Mismatch));
1476 }
1477 Ok(())
1478 }
1479
1480 /// Deeper paths and explicit ports must both parse correctly.
1481 #[test]
1482 fn test_split_url_with_port_and_deep_path() -> Outcome<()> {
1483 let (host, port, path) = res!(split_https_url(
1484 "https://acme-staging-v02.api.letsencrypt.org:8443/acme/authz/abc/1"));
1485 if host != "acme-staging-v02.api.letsencrypt.org"
1486 || port != 8443
1487 || path != "/acme/authz/abc/1"
1488 {
1489 return Err(err!(
1490 "Parsed as {:?}, {}, {:?}.", host, port, path;
1491 Test, Mismatch));
1492 }
1493 Ok(())
1494 }
1495
1496 /// Query strings on the path must be preserved too.
1497 #[test]
1498 fn test_split_url_preserves_query() -> Outcome<()> {
1499 let (_host, _port, path) = res!(split_https_url(
1500 "https://example.test/acme/foo?bar=baz"));
1501 if path != "/acme/foo?bar=baz" {
1502 return Err(err!(
1503 "Expected path with query preserved, got {:?}.", path;
1504 Test, Mismatch));
1505 }
1506 Ok(())
1507 }
1508
1509 /// Missing scheme must error.
1510 #[test]
1511 fn test_split_url_rejects_missing_scheme() -> Outcome<()> {
1512 match split_https_url("http://acme.example/") {
1513 Ok(_) => Err(err!(
1514 "split_https_url accepted a non-https scheme.";
1515 Test, Mismatch)),
1516 Err(_) => Ok(()),
1517 }
1518 }
1519
1520 /// Empty authority must error.
1521 #[test]
1522 fn test_split_url_rejects_empty_authority() -> Outcome<()> {
1523 match split_https_url("https:///directory") {
1524 Ok(_) => Err(err!(
1525 "split_https_url accepted an empty authority.";
1526 Test, Mismatch)),
1527 Err(_) => Ok(()),
1528 }
1529 }
1530
1531 /// Non-numeric port must error.
1532 #[test]
1533 fn test_split_url_rejects_non_numeric_port() -> Outcome<()> {
1534 match split_https_url("https://acme.example:abc/directory") {
1535 Ok(_) => Err(err!(
1536 "split_https_url accepted a non-numeric port.";
1537 Test, Mismatch)),
1538 Err(_) => Ok(()),
1539 }
1540 }
1541
1542 /// `build_csr` must produce non-empty CSR and private key DER blobs
1543 /// for a one-name request, and the hostname bytes must appear in the
1544 /// CSR (IA5String encoding) confirming the SAN was written.
1545 #[test]
1546 fn test_build_csr_single_name() -> Outcome<()> {
1547 let names = vec!["example.com".to_string()];
1548 let (csr, key) = res!(build_csr(&names));
1549 if csr.is_empty() {
1550 return Err(err!("CSR DER was empty."; Test, Mismatch));
1551 }
1552 if key.is_empty() {
1553 return Err(err!("CSR private key was empty."; Test, Mismatch));
1554 }
1555 let needle = b"example.com";
1556 let found = csr.windows(needle.len()).any(|w| w == needle);
1557 if !found {
1558 return Err(err!(
1559 "CSR DER does not contain the requested hostname as a SAN.";
1560 Test, Missing));
1561 }
1562 Ok(())
1563 }
1564
1565 /// Multi-name CSR must contain every requested hostname.
1566 #[test]
1567 fn test_build_csr_multi_name() -> Outcome<()> {
1568 let names = vec![
1569 "example.com".to_string(),
1570 "www.example.com".to_string(),
1571 ];
1572 let (csr, _key) = res!(build_csr(&names));
1573 for host in &names {
1574 let needle = host.as_bytes();
1575 if !csr.windows(needle.len()).any(|w| w == needle) {
1576 return Err(err!(
1577 "CSR DER does not contain {:?}.", host;
1578 Test, Missing));
1579 }
1580 }
1581 Ok(())
1582 }
1583}