1 //! HTTP Client for the ACME protocol.
3 use std
::fs
::OpenOptions
;
5 use std
::os
::unix
::fs
::OpenOptionsExt
;
7 use anyhow
::{bail, format_err}
;
9 use hyper
::{Body, Request}
;
10 use nix
::sys
::stat
::Mode
;
11 use serde
::{Deserialize, Serialize}
;
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
;
21 use crate::api2
::types
::AcmeAccountName
;
22 use crate::config
::acme
::account_path
;
23 use crate::tools
::pbs_simple_http
;
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.
33 account
: AcmeAccountData
,
35 /// The private key as PEM formatted string.
38 /// ToS URL the user agreed to.
39 #[serde(skip_serializing_if = "Option::is_none")]
42 #[serde(skip_serializing_if = "is_false", default)]
45 /// The directory's URL.
46 directory_url
: String
,
50 fn is_false(b
: &bool
) -> bool
{
54 pub struct AcmeClient
{
55 directory_url
: String
,
57 account_path
: Option
<String
>,
59 account
: Option
<Account
>,
60 directory
: Option
<Directory
>,
61 nonce
: Option
<String
>,
62 http_client
: SimpleHttp
,
66 /// Create a new ACME client for a given ACME directory URL.
67 pub fn new(directory_url
: String
) -> Self {
76 http_client
: pbs_simple_http(None
),
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
{
85 Err(err
) if err
.kind() == io
::ErrorKind
::NotFound
=> {
86 bail
!("acme account '{}' does not exist", account_name
)
89 "failed to load acme account from '{}' - {}",
94 let data
: AccountData
= serde_json
::from_slice(&data
).map_err(|err
| {
96 "failed to parse acme account from '{}' - {}",
102 let account
= Account
::from_parts(data
.location
, data
.key
, data
.account
);
104 let mut me
= Self::new(data
.directory_url
);
105 me
.debug
= data
.debug
;
106 me
.account_path
= Some(account_path
);
108 me
.account
= Some(account
);
113 pub async
fn new_account
<'a
>(
115 account_name
: &AcmeAccountName
,
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
)
126 let account
= Account
::creator()
127 .set_contacts(contact
)
128 .agree_to_tos(tos_agreed
);
130 let account
= if let Some(bits
) = rsa_bits
{
131 account
.generate_rsa_key(bits
)?
133 account
.generate_ec_key()?
136 let _
= self.register_account(account
).await?
;
138 crate::config
::acme
::make_acme_account_dir()?
;
139 let account_path
= account_path(account_name
.as_ref());
140 let file
= OpenOptions
::new()
145 .map_err(|err
| format_err
!("failed to open {:?} for writing: {}", account_path
, err
))?
;
146 self.write_to(file
).map_err(|err
| {
148 "failed to write acme account to {:?}: {}",
153 self.account_path
= Some(account_path
);
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())
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")
166 crate::config
::acme
::make_acme_account_dir()?
;
171 .perm(Mode
::from_bits_truncate(0o600))
172 .owner(nix
::unistd
::ROOT
)
173 .group(nix
::unistd
::Gid
::from_raw(0)),
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
)?
)
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
)?
)
189 async
fn register_account(
191 account
: AccountCreator
,
192 ) -> Result
<&Account
, anyhow
::Error
> {
193 let mut retry
= retry();
194 let mut response
= loop {
197 let (directory
, nonce
) = Self::get_dir_nonce(
198 &mut self.http_client
,
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()),
212 let account
= account
.response(response
.location_required()?
, &response
.body
)?
;
214 self.account
= Some(account
);
215 Ok(self.account
.as_ref().unwrap())
218 pub async
fn update_account
<T
: Serialize
>(
221 ) -> Result
<&Account
, anyhow
::Error
> {
222 let account
= Self::need_account(&self.account
)?
;
224 let mut retry
= retry();
225 let response
= loop {
228 let (_directory
, nonce
) = Self::get_dir_nonce(
229 &mut self.http_client
,
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()),
244 // unwrap: we've been keeping an immutable reference to it from the top of the method
246 self.account
.as_mut().unwrap().data
= response
.json()?
;
248 Ok(self.account
.as_ref().unwrap())
251 pub async
fn new_order
<I
>(&mut self, domains
: I
) -> Result
<Order
, anyhow
::Error
>
253 I
: IntoIterator
<Item
= String
>,
255 let account
= Self::need_account(&self.account
)?
;
259 .fold(OrderData
::new(), |order
, domain
| order
.domain(domain
));
261 let mut retry
= retry();
265 let (directory
, nonce
) = Self::get_dir_nonce(
266 &mut self.http_client
,
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(),
281 Ok(response
) => response
,
282 Err(err
) if err
.is_bad_nonce() => continue,
283 Err(err
) => return Err(err
.into()),
287 new_order
.response(response
.location_required()?
, response
.bytes().as_ref())?
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
)?
;
296 let mut retry
= retry();
300 let (_directory
, nonce
) = Self::get_dir_nonce(
301 &mut self.http_client
,
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()),
317 /// Low level POST request.
318 async
fn post
<T
: Serialize
>(
322 ) -> Result
<AcmeResponse
, anyhow
::Error
> {
323 let account
= Self::need_account(&self.account
)?
;
325 let mut retry
= retry();
329 let (_directory
, nonce
) = Self::get_dir_nonce(
330 &mut self.http_client
,
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()),
346 /// Request challenge validation. Afterwards, the challenge should be polled.
347 pub async
fn request_challenge_validation(
350 ) -> Result
<Challenge
, anyhow
::Error
> {
352 .post(url
, &serde_json
::Value
::Object(Default
::default()))
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()?
)
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()?
)
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?
;
375 /// Download a certificate via its 'certificate' URL property.
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
)
382 /// Revoke an existing certificate (PEM or DER formatted).
383 pub async
fn revoke_certificate(
387 ) -> Result
<(), anyhow
::Error
> {
388 // TODO: This can also work without an account.
389 let account
= Self::need_account(&self.account
)?
;
391 let revocation
= account
.revoke_certificate(certificate
, reason
)?
;
393 let mut retry
= retry();
397 let (directory
, nonce
) = Self::get_dir_nonce(
398 &mut self.http_client
,
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()),
414 fn need_account(account
: &Option
<Account
>) -> Result
<&Account
, anyhow
::Error
> {
417 .ok_or_else(|| format_err
!("cannot use client without an account"))
420 pub(crate) fn account(&self) -> Result
<&Account
, anyhow
::Error
> {
421 Self::need_account(&self.account
)
424 pub fn tos(&self) -> Option
<&str> {
428 pub fn directory_url(&self) -> &str {
432 fn to_account_data(&self) -> Result
<AccountData
, anyhow
::Error
> {
433 let account
= self.account()?
;
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()
442 tos
: self.tos
.clone(),
444 directory_url
: self.directory_url
.clone(),
448 fn write_to
<T
: io
::Write
>(&self, out
: T
) -> Result
<(), anyhow
::Error
> {
449 let data
= self.to_account_data()?
;
451 Ok(serde_json
::to_writer_pretty(out
, &data
)?
)
455 struct AcmeResponse
{
457 location
: Option
<String
>,
462 /// Convenience helper to assert that a location header was part of the response.
463 fn location_required(&mut self) -> Result
<String
, anyhow
::Error
> {
466 .ok_or_else(|| format_err
!("missing Location header"))
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
)?
)
474 /// Convenience shortcut to get the body as bytes.
475 fn bytes(&self) -> &[u8] {
481 /// Non-self-borrowing run_request version for borrow workarounds.
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
);
489 let http_request
= if !request
.content_type
.is_empty() {
491 .header("Content-Type", request
.content_type
)
492 .header("Content-Length", request
.body
.len())
493 .body(request
.body
.into())
495 req_builder
.body(Body
::empty())
497 .map_err(|err
| Error
::Custom(format
!("failed to create http request: {}", err
)))?
;
499 let response
= http_client
500 .request(http_request
)
502 .map_err(|err
| Error
::Custom(err
.to_string()))?
;
503 let (parts
, body
) = response
.into_parts();
505 let status
= parts
.status
.as_u16();
506 let body
= hyper
::body
::to_bytes(body
)
508 .map_err(|err
| Error
::Custom(format
!("failed to retrieve response body: {}", err
)))?
;
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: {}",
517 *nonce
= Some(new_nonce
.to_owned());
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: {:?}",
535 header
.to_str().map(str::to_owned
).map_err(|err
| {
536 Error
::Client(format
!(
537 "received invalid location header from ACME server: {}",
544 return Ok(AcmeResponse
{
551 let error
: ErrorResponse
= serde_json
::from_slice(&body
).map_err(|err
| {
552 Error
::Client(format
!(
553 "error status with improper error ACME response: {}",
558 if error
.ty
== proxmox_acme_rs
::error
::BAD_NONCE
{
560 return Err(Error
::InvalidApi(
561 "badNonce without a new Replay-Nonce header".to_string(),
564 return Err(Error
::BadNonce
);
567 Err(Error
::Api(error
))
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
575 async
fn directory(&mut self) -> Result
<&Directory
, Error
> {
576 Ok(Self::get_directory(
577 &mut self.http_client
,
586 async
fn get_directory
<'a
, 'b
>(
587 http_client
: &mut SimpleHttp
,
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()));
596 let response
= Self::execute(
599 url
: directory_url
.to_string(),
609 *directory
= Some(Directory
::from_parts(
610 directory_url
.to_string(),
614 Ok((directory
.as_ref().unwrap(), nonce
.as_deref()))
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
,
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
629 // this is also a lifetime issue...
630 let _
= Self::get_nonce(http_client
, nonce
, dir
.new_nonce_url()).await?
;
632 Ok((dir
, nonce
.as_deref().unwrap()))
635 pub async
fn terms_of_service_url(&mut self) -> Result
<Option
<&str>, Error
> {
636 Ok(self.directory().await?
.terms_of_service_url())
639 async
fn get_nonce
<'a
>(
640 http_client
: &mut SimpleHttp
,
641 nonce
: &'a
mut Option
<String
>,
643 ) -> Result
<&'a
str, Error
> {
644 let response
= Self::execute(
647 url
: new_nonce_url
.to_owned(),
657 if !response
.got_nonce
{
658 return Err(Error
::InvalidApi(
659 "no new nonce received from new nonce URL".to_string(),
665 .ok_or_else(|| Error
::Client("failed to update nonce".to_string()))
669 /// bad nonce retry count helper
672 const fn retry() -> Retry
{
677 fn tick(&mut self) -> Result
<(), Error
> {
679 Err(Error
::Client(format
!("kept getting a badNonce error!")))