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