]> git.proxmox.com Git - proxmox-backup.git/blob - src/acme/client.rs
update to proxmox-sys 0.2 crate
[proxmox-backup.git] / src / acme / client.rs
1 //! HTTP Client for the ACME protocol.
2
3 use std::fs::OpenOptions;
4 use std::io;
5 use std::os::unix::fs::OpenOptionsExt;
6
7 use anyhow::{bail, format_err};
8 use bytes::Bytes;
9 use hyper::{Body, Request};
10 use nix::sys::stat::Mode;
11 use serde::{Deserialize, Serialize};
12
13 use proxmox_sys::fs::{replace_file, CreateOptions};
14 use proxmox_acme_rs::account::AccountCreator;
15 use proxmox_acme_rs::account::AccountData as AcmeAccountData;
16 use proxmox_acme_rs::order::{Order, OrderData};
17 use proxmox_acme_rs::Request as AcmeRequest;
18 use proxmox_acme_rs::{Account, Authorization, Challenge, Directory, Error, ErrorResponse};
19 use proxmox_http::client::SimpleHttp;
20
21 use crate::api2::types::AcmeAccountName;
22 use crate::config::acme::account_path;
23 use crate::tools::pbs_simple_http;
24
25 /// Our on-disk format inherited from PVE's proxmox-acme code.
26 #[derive(Deserialize, Serialize)]
27 #[serde(rename_all = "camelCase")]
28 pub struct AccountData {
29 /// The account's location URL.
30 location: String,
31
32 /// The account data.
33 account: AcmeAccountData,
34
35 /// The private key as PEM formatted string.
36 key: String,
37
38 /// ToS URL the user agreed to.
39 #[serde(skip_serializing_if = "Option::is_none")]
40 tos: Option<String>,
41
42 #[serde(skip_serializing_if = "is_false", default)]
43 debug: bool,
44
45 /// The directory's URL.
46 directory_url: String,
47 }
48
49 #[inline]
50 fn is_false(b: &bool) -> bool {
51 !*b
52 }
53
54 pub struct AcmeClient {
55 directory_url: String,
56 debug: bool,
57 account_path: Option<String>,
58 tos: Option<String>,
59 account: Option<Account>,
60 directory: Option<Directory>,
61 nonce: Option<String>,
62 http_client: SimpleHttp,
63 }
64
65 impl AcmeClient {
66 /// Create a new ACME client for a given ACME directory URL.
67 pub fn new(directory_url: String) -> Self {
68 Self {
69 directory_url,
70 debug: false,
71 account_path: None,
72 tos: None,
73 account: None,
74 directory: None,
75 nonce: None,
76 http_client: pbs_simple_http(None),
77 }
78 }
79
80 /// Load an existing ACME account by name.
81 pub async fn load(account_name: &AcmeAccountName) -> Result<Self, anyhow::Error> {
82 let account_path = account_path(account_name.as_ref());
83 let data = match tokio::fs::read(&account_path).await {
84 Ok(data) => data,
85 Err(err) if err.kind() == io::ErrorKind::NotFound => {
86 bail!("acme account '{}' does not exist", account_name)
87 }
88 Err(err) => bail!(
89 "failed to load acme account from '{}' - {}",
90 account_path,
91 err
92 ),
93 };
94 let data: AccountData = serde_json::from_slice(&data).map_err(|err| {
95 format_err!(
96 "failed to parse acme account from '{}' - {}",
97 account_path,
98 err
99 )
100 })?;
101
102 let account = Account::from_parts(data.location, data.key, data.account);
103
104 let mut me = Self::new(data.directory_url);
105 me.debug = data.debug;
106 me.account_path = Some(account_path);
107 me.tos = data.tos;
108 me.account = Some(account);
109
110 Ok(me)
111 }
112
113 pub async fn new_account<'a>(
114 &'a mut self,
115 account_name: &AcmeAccountName,
116 tos_agreed: bool,
117 contact: Vec<String>,
118 rsa_bits: Option<u32>,
119 ) -> Result<&'a Account, anyhow::Error> {
120 self.tos = if tos_agreed {
121 self.terms_of_service_url().await?.map(str::to_owned)
122 } else {
123 None
124 };
125
126 let account = Account::creator()
127 .set_contacts(contact)
128 .agree_to_tos(tos_agreed);
129
130 let account = if let Some(bits) = rsa_bits {
131 account.generate_rsa_key(bits)?
132 } else {
133 account.generate_ec_key()?
134 };
135
136 let _ = self.register_account(account).await?;
137
138 crate::config::acme::make_acme_account_dir()?;
139 let account_path = account_path(account_name.as_ref());
140 let file = OpenOptions::new()
141 .write(true)
142 .create_new(true)
143 .mode(0o600)
144 .open(&account_path)
145 .map_err(|err| format_err!("failed to open {:?} for writing: {}", account_path, err))?;
146 self.write_to(file).map_err(|err| {
147 format_err!(
148 "failed to write acme account to {:?}: {}",
149 account_path,
150 err
151 )
152 })?;
153 self.account_path = Some(account_path);
154
155 // unwrap: Setting `self.account` is literally this function's job, we just can't keep
156 // the borrow from from `self.register_account()` active due to clashes.
157 Ok(self.account.as_ref().unwrap())
158 }
159
160 fn save(&self) -> Result<(), anyhow::Error> {
161 let mut data = Vec::<u8>::new();
162 self.write_to(&mut data)?;
163 let account_path = self.account_path.as_ref().ok_or_else(|| {
164 format_err!("no account path set, cannot save upated account information")
165 })?;
166 crate::config::acme::make_acme_account_dir()?;
167 replace_file(
168 account_path,
169 &data,
170 CreateOptions::new()
171 .perm(Mode::from_bits_truncate(0o600))
172 .owner(nix::unistd::ROOT)
173 .group(nix::unistd::Gid::from_raw(0)),
174 true,
175 )
176 }
177
178 /// Shortcut to `account().ok_or_else(...).key_authorization()`.
179 pub fn key_authorization(&self, token: &str) -> Result<String, anyhow::Error> {
180 Ok(Self::need_account(&self.account)?.key_authorization(token)?)
181 }
182
183 /// Shortcut to `account().ok_or_else(...).dns_01_txt_value()`.
184 /// the key authorization value.
185 pub fn dns_01_txt_value(&self, token: &str) -> Result<String, anyhow::Error> {
186 Ok(Self::need_account(&self.account)?.dns_01_txt_value(token)?)
187 }
188
189 async fn register_account(
190 &mut self,
191 account: AccountCreator,
192 ) -> Result<&Account, anyhow::Error> {
193 let mut retry = retry();
194 let mut response = loop {
195 retry.tick()?;
196
197 let (directory, nonce) = Self::get_dir_nonce(
198 &mut self.http_client,
199 &self.directory_url,
200 &mut self.directory,
201 &mut self.nonce,
202 )
203 .await?;
204 let request = account.request(directory, nonce)?;
205 match self.run_request(request).await {
206 Ok(response) => break response,
207 Err(err) if err.is_bad_nonce() => continue,
208 Err(err) => return Err(err.into()),
209 }
210 };
211
212 let account = account.response(response.location_required()?, &response.body)?;
213
214 self.account = Some(account);
215 Ok(self.account.as_ref().unwrap())
216 }
217
218 pub async fn update_account<T: Serialize>(
219 &mut self,
220 data: &T,
221 ) -> Result<&Account, anyhow::Error> {
222 let account = Self::need_account(&self.account)?;
223
224 let mut retry = retry();
225 let response = loop {
226 retry.tick()?;
227
228 let (_directory, nonce) = Self::get_dir_nonce(
229 &mut self.http_client,
230 &self.directory_url,
231 &mut self.directory,
232 &mut self.nonce,
233 )
234 .await?;
235
236 let request = account.post_request(&account.location, &nonce, data)?;
237 match Self::execute(&mut self.http_client, request, &mut self.nonce).await {
238 Ok(response) => break response,
239 Err(err) if err.is_bad_nonce() => continue,
240 Err(err) => return Err(err.into()),
241 }
242 };
243
244 // unwrap: we've been keeping an immutable reference to it from the top of the method
245 let _ = account;
246 self.account.as_mut().unwrap().data = response.json()?;
247 self.save()?;
248 Ok(self.account.as_ref().unwrap())
249 }
250
251 pub async fn new_order<I>(&mut self, domains: I) -> Result<Order, anyhow::Error>
252 where
253 I: IntoIterator<Item = String>,
254 {
255 let account = Self::need_account(&self.account)?;
256
257 let order = domains
258 .into_iter()
259 .fold(OrderData::new(), |order, domain| order.domain(domain));
260
261 let mut retry = retry();
262 loop {
263 retry.tick()?;
264
265 let (directory, nonce) = Self::get_dir_nonce(
266 &mut self.http_client,
267 &self.directory_url,
268 &mut self.directory,
269 &mut self.nonce,
270 )
271 .await?;
272
273 let mut new_order = account.new_order(&order, directory, nonce)?;
274 let mut response = match Self::execute(
275 &mut self.http_client,
276 new_order.request.take().unwrap(),
277 &mut self.nonce,
278 )
279 .await
280 {
281 Ok(response) => response,
282 Err(err) if err.is_bad_nonce() => continue,
283 Err(err) => return Err(err.into()),
284 };
285
286 return Ok(
287 new_order.response(response.location_required()?, response.bytes().as_ref())?
288 );
289 }
290 }
291
292 /// Low level "POST-as-GET" request.
293 async fn post_as_get(&mut self, url: &str) -> Result<AcmeResponse, anyhow::Error> {
294 let account = Self::need_account(&self.account)?;
295
296 let mut retry = retry();
297 loop {
298 retry.tick()?;
299
300 let (_directory, nonce) = Self::get_dir_nonce(
301 &mut self.http_client,
302 &self.directory_url,
303 &mut self.directory,
304 &mut self.nonce,
305 )
306 .await?;
307
308 let request = account.get_request(url, nonce)?;
309 match Self::execute(&mut self.http_client, request, &mut self.nonce).await {
310 Ok(response) => return Ok(response),
311 Err(err) if err.is_bad_nonce() => continue,
312 Err(err) => return Err(err.into()),
313 }
314 }
315 }
316
317 /// Low level POST request.
318 async fn post<T: Serialize>(
319 &mut self,
320 url: &str,
321 data: &T,
322 ) -> Result<AcmeResponse, anyhow::Error> {
323 let account = Self::need_account(&self.account)?;
324
325 let mut retry = retry();
326 loop {
327 retry.tick()?;
328
329 let (_directory, nonce) = Self::get_dir_nonce(
330 &mut self.http_client,
331 &self.directory_url,
332 &mut self.directory,
333 &mut self.nonce,
334 )
335 .await?;
336
337 let request = account.post_request(url, nonce, data)?;
338 match Self::execute(&mut self.http_client, request, &mut self.nonce).await {
339 Ok(response) => return Ok(response),
340 Err(err) if err.is_bad_nonce() => continue,
341 Err(err) => return Err(err.into()),
342 }
343 }
344 }
345
346 /// Request challenge validation. Afterwards, the challenge should be polled.
347 pub async fn request_challenge_validation(
348 &mut self,
349 url: &str,
350 ) -> Result<Challenge, anyhow::Error> {
351 Ok(self
352 .post(url, &serde_json::Value::Object(Default::default()))
353 .await?
354 .json()?)
355 }
356
357 /// Assuming the provided URL is an 'Authorization' URL, get and deserialize it.
358 pub async fn get_authorization(&mut self, url: &str) -> Result<Authorization, anyhow::Error> {
359 Ok(self.post_as_get(url).await?.json()?)
360 }
361
362 /// Assuming the provided URL is an 'Order' URL, get and deserialize it.
363 pub async fn get_order(&mut self, url: &str) -> Result<OrderData, anyhow::Error> {
364 Ok(self.post_as_get(url).await?.json()?)
365 }
366
367 /// Finalize an Order via its `finalize` URL property and the DER encoded CSR.
368 pub async fn finalize(&mut self, url: &str, csr: &[u8]) -> Result<(), anyhow::Error> {
369 let csr = base64::encode_config(csr, base64::URL_SAFE_NO_PAD);
370 let data = serde_json::json!({ "csr": csr });
371 self.post(url, &data).await?;
372 Ok(())
373 }
374
375 /// Download a certificate via its 'certificate' URL property.
376 ///
377 /// The certificate will be a PEM certificate chain.
378 pub async fn get_certificate(&mut self, url: &str) -> Result<Bytes, anyhow::Error> {
379 Ok(self.post_as_get(url).await?.body)
380 }
381
382 /// Revoke an existing certificate (PEM or DER formatted).
383 pub async fn revoke_certificate(
384 &mut self,
385 certificate: &[u8],
386 reason: Option<u32>,
387 ) -> Result<(), anyhow::Error> {
388 // TODO: This can also work without an account.
389 let account = Self::need_account(&self.account)?;
390
391 let revocation = account.revoke_certificate(certificate, reason)?;
392
393 let mut retry = retry();
394 loop {
395 retry.tick()?;
396
397 let (directory, nonce) = Self::get_dir_nonce(
398 &mut self.http_client,
399 &self.directory_url,
400 &mut self.directory,
401 &mut self.nonce,
402 )
403 .await?;
404
405 let request = revocation.request(&directory, nonce)?;
406 match Self::execute(&mut self.http_client, request, &mut self.nonce).await {
407 Ok(_response) => return Ok(()),
408 Err(err) if err.is_bad_nonce() => continue,
409 Err(err) => return Err(err.into()),
410 }
411 }
412 }
413
414 fn need_account(account: &Option<Account>) -> Result<&Account, anyhow::Error> {
415 account
416 .as_ref()
417 .ok_or_else(|| format_err!("cannot use client without an account"))
418 }
419
420 pub(crate) fn account(&self) -> Result<&Account, anyhow::Error> {
421 Self::need_account(&self.account)
422 }
423
424 pub fn tos(&self) -> Option<&str> {
425 self.tos.as_deref()
426 }
427
428 pub fn directory_url(&self) -> &str {
429 &self.directory_url
430 }
431
432 fn to_account_data(&self) -> Result<AccountData, anyhow::Error> {
433 let account = self.account()?;
434
435 Ok(AccountData {
436 location: account.location.clone(),
437 key: account.private_key.clone(),
438 account: AcmeAccountData {
439 only_return_existing: false, // don't actually write this out in case it's set
440 ..account.data.clone()
441 },
442 tos: self.tos.clone(),
443 debug: self.debug,
444 directory_url: self.directory_url.clone(),
445 })
446 }
447
448 fn write_to<T: io::Write>(&self, out: T) -> Result<(), anyhow::Error> {
449 let data = self.to_account_data()?;
450
451 Ok(serde_json::to_writer_pretty(out, &data)?)
452 }
453 }
454
455 struct AcmeResponse {
456 body: Bytes,
457 location: Option<String>,
458 got_nonce: bool,
459 }
460
461 impl AcmeResponse {
462 /// Convenience helper to assert that a location header was part of the response.
463 fn location_required(&mut self) -> Result<String, anyhow::Error> {
464 self.location
465 .take()
466 .ok_or_else(|| format_err!("missing Location header"))
467 }
468
469 /// Convenience shortcut to perform json deserialization of the returned body.
470 fn json<T: for<'a> Deserialize<'a>>(&self) -> Result<T, Error> {
471 Ok(serde_json::from_slice(&self.body)?)
472 }
473
474 /// Convenience shortcut to get the body as bytes.
475 fn bytes(&self) -> &[u8] {
476 &self.body
477 }
478 }
479
480 impl AcmeClient {
481 /// Non-self-borrowing run_request version for borrow workarounds.
482 async fn execute(
483 http_client: &mut SimpleHttp,
484 request: AcmeRequest,
485 nonce: &mut Option<String>,
486 ) -> Result<AcmeResponse, Error> {
487 let req_builder = Request::builder().method(request.method).uri(&request.url);
488
489 let http_request = if !request.content_type.is_empty() {
490 req_builder
491 .header("Content-Type", request.content_type)
492 .header("Content-Length", request.body.len())
493 .body(request.body.into())
494 } else {
495 req_builder.body(Body::empty())
496 }
497 .map_err(|err| Error::Custom(format!("failed to create http request: {}", err)))?;
498
499 let response = http_client
500 .request(http_request)
501 .await
502 .map_err(|err| Error::Custom(err.to_string()))?;
503 let (parts, body) = response.into_parts();
504
505 let status = parts.status.as_u16();
506 let body = hyper::body::to_bytes(body)
507 .await
508 .map_err(|err| Error::Custom(format!("failed to retrieve response body: {}", err)))?;
509
510 let got_nonce = if let Some(new_nonce) = parts.headers.get(proxmox_acme_rs::REPLAY_NONCE) {
511 let new_nonce = new_nonce.to_str().map_err(|err| {
512 Error::Client(format!(
513 "received invalid replay-nonce header from ACME server: {}",
514 err
515 ))
516 })?;
517 *nonce = Some(new_nonce.to_owned());
518 true
519 } else {
520 false
521 };
522
523 if parts.status.is_success() {
524 if status != request.expected {
525 return Err(Error::InvalidApi(format!(
526 "ACME server responded with unexpected status code: {:?}",
527 parts.status
528 )));
529 }
530
531 let location = parts
532 .headers
533 .get("Location")
534 .map(|header| {
535 header.to_str().map(str::to_owned).map_err(|err| {
536 Error::Client(format!(
537 "received invalid location header from ACME server: {}",
538 err
539 ))
540 })
541 })
542 .transpose()?;
543
544 return Ok(AcmeResponse {
545 body,
546 location,
547 got_nonce,
548 });
549 }
550
551 let error: ErrorResponse = serde_json::from_slice(&body).map_err(|err| {
552 Error::Client(format!(
553 "error status with improper error ACME response: {}",
554 err
555 ))
556 })?;
557
558 if error.ty == proxmox_acme_rs::error::BAD_NONCE {
559 if !got_nonce {
560 return Err(Error::InvalidApi(
561 "badNonce without a new Replay-Nonce header".to_string(),
562 ));
563 }
564 return Err(Error::BadNonce);
565 }
566
567 Err(Error::Api(error))
568 }
569
570 /// Low-level API to run an n API request. This automatically updates the current nonce!
571 async fn run_request(&mut self, request: AcmeRequest) -> Result<AcmeResponse, Error> {
572 Self::execute(&mut self.http_client, request, &mut self.nonce).await
573 }
574
575 async fn directory(&mut self) -> Result<&Directory, Error> {
576 Ok(Self::get_directory(
577 &mut self.http_client,
578 &self.directory_url,
579 &mut self.directory,
580 &mut self.nonce,
581 )
582 .await?
583 .0)
584 }
585
586 async fn get_directory<'a, 'b>(
587 http_client: &mut SimpleHttp,
588 directory_url: &str,
589 directory: &'a mut Option<Directory>,
590 nonce: &'b mut Option<String>,
591 ) -> Result<(&'a Directory, Option<&'b str>), Error> {
592 if let Some(d) = directory {
593 return Ok((d, nonce.as_deref()));
594 }
595
596 let response = Self::execute(
597 http_client,
598 AcmeRequest {
599 url: directory_url.to_string(),
600 method: "GET",
601 content_type: "",
602 body: String::new(),
603 expected: 200,
604 },
605 nonce,
606 )
607 .await?;
608
609 *directory = Some(Directory::from_parts(
610 directory_url.to_string(),
611 response.json()?,
612 ));
613
614 Ok((directory.as_ref().unwrap(), nonce.as_deref()))
615 }
616
617 /// Like `get_directory`, but if the directory provides no nonce, also performs a `HEAD`
618 /// request on the new nonce URL.
619 async fn get_dir_nonce<'a, 'b>(
620 http_client: &mut SimpleHttp,
621 directory_url: &str,
622 directory: &'a mut Option<Directory>,
623 nonce: &'b mut Option<String>,
624 ) -> Result<(&'a Directory, &'b str), Error> {
625 // this let construct is a lifetime workaround:
626 let _ = Self::get_directory(http_client, directory_url, directory, nonce).await?;
627 let dir = directory.as_ref().unwrap(); // the above fails if it couldn't fill this option
628 if nonce.is_none() {
629 // this is also a lifetime issue...
630 let _ = Self::get_nonce(http_client, nonce, dir.new_nonce_url()).await?;
631 };
632 Ok((dir, nonce.as_deref().unwrap()))
633 }
634
635 pub async fn terms_of_service_url(&mut self) -> Result<Option<&str>, Error> {
636 Ok(self.directory().await?.terms_of_service_url())
637 }
638
639 async fn get_nonce<'a>(
640 http_client: &mut SimpleHttp,
641 nonce: &'a mut Option<String>,
642 new_nonce_url: &str,
643 ) -> Result<&'a str, Error> {
644 let response = Self::execute(
645 http_client,
646 AcmeRequest {
647 url: new_nonce_url.to_owned(),
648 method: "HEAD",
649 content_type: "",
650 body: String::new(),
651 expected: 200,
652 },
653 nonce,
654 )
655 .await?;
656
657 if !response.got_nonce {
658 return Err(Error::InvalidApi(
659 "no new nonce received from new nonce URL".to_string(),
660 ));
661 }
662
663 nonce
664 .as_deref()
665 .ok_or_else(|| Error::Client("failed to update nonce".to_string()))
666 }
667 }
668
669 /// bad nonce retry count helper
670 struct Retry(usize);
671
672 const fn retry() -> Retry {
673 Retry(0)
674 }
675
676 impl Retry {
677 fn tick(&mut self) -> Result<(), Error> {
678 if self.0 >= 3 {
679 Err(Error::Client(format!("kept getting a badNonce error!")))
680 } else {
681 self.0 += 1;
682 Ok(())
683 }
684 }
685 }