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_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}
;
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
>,
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 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
)
127 let mut account
= Account
::creator()
128 .set_contacts(contact
)
129 .agree_to_tos(tos_agreed
);
131 if let Some((eab_kid
, eab_hmac_key
)) = eab_creds
{
132 account
= account
.set_eab_credentials(eab_kid
, eab_hmac_key
)?
;
135 let account
= if let Some(bits
) = rsa_bits
{
136 account
.generate_rsa_key(bits
)?
138 account
.generate_ec_key()?
141 let _
= self.register_account(account
).await?
;
143 crate::config
::acme
::make_acme_account_dir()?
;
144 let account_path
= account_path(account_name
.as_ref());
145 let file
= OpenOptions
::new()
150 .map_err(|err
| format_err
!("failed to open {:?} for writing: {}", account_path
, err
))?
;
151 self.write_to(file
).map_err(|err
| {
153 "failed to write acme account to {:?}: {}",
158 self.account_path
= Some(account_path
);
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())
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")
171 crate::config
::acme
::make_acme_account_dir()?
;
176 .perm(Mode
::from_bits_truncate(0o600))
177 .owner(nix
::unistd
::ROOT
)
178 .group(nix
::unistd
::Gid
::from_raw(0)),
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
)?
)
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
)?
)
194 async
fn register_account(
196 account
: AccountCreator
,
197 ) -> Result
<&Account
, anyhow
::Error
> {
198 let mut retry
= retry();
199 let mut response
= loop {
202 let (directory
, nonce
) = Self::get_dir_nonce(
203 &mut self.http_client
,
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()),
217 let account
= account
.response(response
.location_required()?
, &response
.body
)?
;
219 self.account
= Some(account
);
220 Ok(self.account
.as_ref().unwrap())
223 pub async
fn update_account
<T
: Serialize
>(
226 ) -> Result
<&Account
, anyhow
::Error
> {
227 let account
= Self::need_account(&self.account
)?
;
229 let mut retry
= retry();
230 let response
= loop {
233 let (_directory
, nonce
) = Self::get_dir_nonce(
234 &mut self.http_client
,
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()),
249 // unwrap: we've been keeping an immutable reference to it from the top of the method
251 self.account
.as_mut().unwrap().data
= response
.json()?
;
253 Ok(self.account
.as_ref().unwrap())
256 pub async
fn new_order
<I
>(&mut self, domains
: I
) -> Result
<Order
, anyhow
::Error
>
258 I
: IntoIterator
<Item
= String
>,
260 let account
= Self::need_account(&self.account
)?
;
264 .fold(OrderData
::new(), |order
, domain
| order
.domain(domain
));
266 let mut retry
= retry();
270 let (directory
, nonce
) = Self::get_dir_nonce(
271 &mut self.http_client
,
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(),
286 Ok(response
) => response
,
287 Err(err
) if err
.is_bad_nonce() => continue,
288 Err(err
) => return Err(err
.into()),
292 new_order
.response(response
.location_required()?
, response
.bytes().as_ref())?
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
)?
;
301 let mut retry
= retry();
305 let (_directory
, nonce
) = Self::get_dir_nonce(
306 &mut self.http_client
,
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()),
322 /// Low level POST request.
323 async
fn post
<T
: Serialize
>(
327 ) -> Result
<AcmeResponse
, anyhow
::Error
> {
328 let account
= Self::need_account(&self.account
)?
;
330 let mut retry
= retry();
334 let (_directory
, nonce
) = Self::get_dir_nonce(
335 &mut self.http_client
,
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()),
351 /// Request challenge validation. Afterwards, the challenge should be polled.
352 pub async
fn request_challenge_validation(
355 ) -> Result
<Challenge
, anyhow
::Error
> {
357 .post(url
, &serde_json
::Value
::Object(Default
::default()))
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()?
)
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()?
)
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?
;
380 /// Download a certificate via its 'certificate' URL property.
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
)
387 /// Revoke an existing certificate (PEM or DER formatted).
388 pub async
fn revoke_certificate(
392 ) -> Result
<(), anyhow
::Error
> {
393 // TODO: This can also work without an account.
394 let account
= Self::need_account(&self.account
)?
;
396 let revocation
= account
.revoke_certificate(certificate
, reason
)?
;
398 let mut retry
= retry();
402 let (directory
, nonce
) = Self::get_dir_nonce(
403 &mut self.http_client
,
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()),
419 fn need_account(account
: &Option
<Account
>) -> Result
<&Account
, anyhow
::Error
> {
422 .ok_or_else(|| format_err
!("cannot use client without an account"))
425 pub(crate) fn account(&self) -> Result
<&Account
, anyhow
::Error
> {
426 Self::need_account(&self.account
)
429 pub fn tos(&self) -> Option
<&str> {
433 pub fn directory_url(&self) -> &str {
437 fn to_account_data(&self) -> Result
<AccountData
, anyhow
::Error
> {
438 let account
= self.account()?
;
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()
447 tos
: self.tos
.clone(),
449 directory_url
: self.directory_url
.clone(),
453 fn write_to
<T
: io
::Write
>(&self, out
: T
) -> Result
<(), anyhow
::Error
> {
454 let data
= self.to_account_data()?
;
456 Ok(serde_json
::to_writer_pretty(out
, &data
)?
)
460 struct AcmeResponse
{
462 location
: Option
<String
>,
467 /// Convenience helper to assert that a location header was part of the response.
468 fn location_required(&mut self) -> Result
<String
, anyhow
::Error
> {
471 .ok_or_else(|| format_err
!("missing Location header"))
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
)?
)
479 /// Convenience shortcut to get the body as bytes.
480 fn bytes(&self) -> &[u8] {
486 /// Non-self-borrowing run_request version for borrow workarounds.
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
);
494 let http_request
= if !request
.content_type
.is_empty() {
496 .header("Content-Type", request
.content_type
)
497 .header("Content-Length", request
.body
.len())
498 .body(request
.body
.into())
500 req_builder
.body(Body
::empty())
502 .map_err(|err
| Error
::Custom(format
!("failed to create http request: {}", err
)))?
;
504 let response
= http_client
505 .request(http_request
)
507 .map_err(|err
| Error
::Custom(err
.to_string()))?
;
508 let (parts
, body
) = response
.into_parts();
510 let status
= parts
.status
.as_u16();
511 let body
= hyper
::body
::to_bytes(body
)
513 .map_err(|err
| Error
::Custom(format
!("failed to retrieve response body: {}", err
)))?
;
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: {}",
522 *nonce
= Some(new_nonce
.to_owned());
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: {:?}",
540 header
.to_str().map(str::to_owned
).map_err(|err
| {
541 Error
::Client(format
!(
542 "received invalid location header from ACME server: {}",
549 return Ok(AcmeResponse
{
556 let error
: ErrorResponse
= serde_json
::from_slice(&body
).map_err(|err
| {
557 Error
::Client(format
!(
558 "error status with improper error ACME response: {}",
563 if error
.ty
== proxmox_acme
::error
::BAD_NONCE
{
565 return Err(Error
::InvalidApi(
566 "badNonce without a new Replay-Nonce header".to_string(),
569 return Err(Error
::BadNonce
);
572 Err(Error
::Api(error
))
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
580 pub async
fn directory(&mut self) -> Result
<&Directory
, Error
> {
581 Ok(Self::get_directory(
582 &mut self.http_client
,
591 async
fn get_directory
<'a
, 'b
>(
592 http_client
: &mut Client
,
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()));
601 let response
= Self::execute(
604 url
: directory_url
.to_string(),
614 *directory
= Some(Directory
::from_parts(
615 directory_url
.to_string(),
619 Ok((directory
.as_ref().unwrap(), nonce
.as_deref()))
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
,
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
634 // this is also a lifetime issue...
635 let _
= Self::get_nonce(http_client
, nonce
, dir
.new_nonce_url()).await?
;
637 Ok((dir
, nonce
.as_deref().unwrap()))
640 pub async
fn terms_of_service_url(&mut self) -> Result
<Option
<&str>, Error
> {
641 Ok(self.directory().await?
.terms_of_service_url())
644 async
fn get_nonce
<'a
>(
645 http_client
: &mut Client
,
646 nonce
: &'a
mut Option
<String
>,
648 ) -> Result
<&'a
str, Error
> {
649 let response
= Self::execute(
652 url
: new_nonce_url
.to_owned(),
662 if !response
.got_nonce
{
663 return Err(Error
::InvalidApi(
664 "no new nonce received from new nonce URL".to_string(),
670 .ok_or_else(|| Error
::Client("failed to update nonce".to_string()))
674 /// bad nonce retry count helper
677 const fn retry() -> Retry
{
682 fn tick(&mut self) -> Result
<(), Error
> {
684 Err(Error
::Client("kept getting a badNonce error!".to_string()))