1 //! `PMG::RS::Acme` perl module.
3 //! The functions in here are perl bindings.
5 use std
::fs
::OpenOptions
;
6 use std
::io
::{self, Write}
;
7 use std
::os
::unix
::fs
::OpenOptionsExt
;
9 use anyhow
::{format_err, Error}
;
10 use serde
::{Deserialize, Serialize}
;
12 use proxmox_acme
::account
::AccountData
as AcmeAccountData
;
13 use proxmox_acme
::{Account, Client}
;
15 /// Our on-disk format inherited from PVE's proxmox-acme code.
16 #[derive(Deserialize, Serialize)]
17 #[serde(rename_all = "camelCase")]
18 pub struct AccountData
{
19 /// The account's location URL.
23 account
: AcmeAccountData
,
25 /// The private key as PEM formatted string.
28 /// ToS URL the user agreed to.
29 #[serde(skip_serializing_if = "Option::is_none")]
32 #[serde(skip_serializing_if = "is_false", default)]
35 /// The directory's URL.
36 directory_url
: String
,
40 fn is_false(b
: &bool
) -> bool
{
46 account_path
: Option
<String
>,
52 pub fn new(api_directory
: String
) -> Result
<Self, Error
> {
54 client
: Client
::new(api_directory
),
61 pub fn load(account_path
: String
) -> Result
<Self, Error
> {
62 let data
= std
::fs
::read(&account_path
)?
;
63 let data
: AccountData
= serde_json
::from_slice(&data
)?
;
65 let mut client
= Client
::new(data
.directory_url
);
66 client
.set_account(Account
::from_parts(data
.location
, data
.key
, data
.account
));
70 account_path
: Some(account_path
),
81 rsa_bits
: Option
<u32>,
82 eab_creds
: Option
<(String
, String
)>,
83 ) -> Result
<(), Error
> {
84 self.tos
= if tos_agreed
{
85 self.client
.terms_of_service_url()?
.map(str::to_owned
)
92 .new_account(contact
, tos_agreed
, rsa_bits
, eab_creds
)?
;
93 let file
= OpenOptions
::new()
98 .map_err(|err
| format_err
!("failed to open {:?} for writing: {}", account_path
, err
))?
;
99 self.write_to(file
).map_err(|err
| {
101 "failed to write acme account to {:?}: {}",
106 self.account_path
= Some(account_path
);
111 /// Convenience helper around `.client.account().ok_or_else(||...)`
112 fn account(&self) -> Result
<&Account
, Error
> {
115 .ok_or_else(|| format_err
!("missing account"))
118 fn to_account_data(&self) -> Result
<AccountData
, Error
> {
119 let account
= self.account()?
;
122 location
: account
.location
.clone(),
123 key
: account
.private_key
.clone(),
124 account
: AcmeAccountData
{
125 only_return_existing
: false, // don't actually write this out in case it's set
126 ..account
.data
.clone()
128 tos
: self.tos
.clone(),
130 directory_url
: self.client
.directory_url().to_owned(),
134 fn write_to
<T
: io
::Write
>(&mut self, out
: T
) -> Result
<(), Error
> {
135 let data
= self.to_account_data()?
;
137 Ok(serde_json
::to_writer_pretty(out
, &data
)?
)
140 pub fn update_account
<T
: Serialize
>(&mut self, data
: &T
) -> Result
<(), Error
> {
141 let account_path
= self
144 .ok_or_else(|| format_err
!("missing account path"))?
;
145 self.client
.update_account(data
)?
;
147 let tmp_path
= format
!("{}.tmp", account_path
);
148 // FIXME: move proxmox::tools::replace_file & make_temp out into a nice *little* crate...
149 let mut file
= OpenOptions
::new()
154 .map_err(|err
| format_err
!("failed to open {:?} for writing: {}", tmp_path
, err
))?
;
155 self.write_to(&mut file
).map_err(|err
| {
156 format_err
!("failed to write acme account to {:?}: {}", tmp_path
, err
)
158 file
.flush().map_err(|err
| {
159 format_err
!("failed to flush acme account file {:?}: {}", tmp_path
, err
)
162 // re-borrow since we needed `self` as mut earlier
163 let account_path
= self.account_path
.as_deref().unwrap();
164 std
::fs
::rename(&tmp_path
, account_path
).map_err(|err
| {
166 "failed to rotate temp file into place ({:?} -> {:?}): {}",
176 pub fn revoke_certificate(&mut self, data
: &[u8], reason
: Option
<u32>) -> Result
<(), Error
> {
177 Ok(self.client
.revoke_certificate(data
, reason
)?
)
180 pub fn set_proxy(&mut self, proxy
: String
) {
181 self.client
.set_proxy(proxy
)
185 #[perlmod::package(name = "PMG::RS::Acme")]
187 use std
::collections
::HashMap
;
188 use std
::sync
::Mutex
;
191 use serde_bytes
::{ByteBuf, Bytes}
;
194 use proxmox_acme
::directory
::Meta
;
195 use proxmox_acme
::order
::OrderData
;
196 use proxmox_acme
::{Authorization, Challenge, Order}
;
198 use super::{AccountData, Inner}
;
200 perlmod
::declare_magic
!(Box
<Acme
> : &Acme
as "PMG::RS::Acme");
202 /// An Acme client instance.
207 /// Create a new ACME client instance given an account path and an API directory URL.
208 #[export(raw_return)]
209 pub fn new(#[raw] class: Value, api_directory: String) -> Result<Value, Error> {
210 Ok(perlmod
::instantiate_magic
!(
212 MAGIC
=> Box
::new(Acme
{
213 inner
: Mutex
::new(Inner
::new(api_directory
)?
),
218 /// Load an existing account.
219 #[export(raw_return)]
220 pub fn load(#[raw] class: Value, account_path: String) -> Result<Value, Error> {
221 Ok(perlmod
::instantiate_magic
!(
223 MAGIC
=> Box
::new(Acme
{
224 inner
: Mutex
::new(Inner
::load(account_path
)?
),
229 /// Create a new account.
231 /// `tos_agreed` is usually not optional, but may be set later via an update.
232 /// The `contact` list should be a list of `mailto:` strings (or others, if the directory
235 /// In case an RSA key should be generated, an `rsa_bits` parameter should be provided.
236 /// Otherwise a P-256 EC key will be generated.
239 #[try_from_ref] this: &Acme,
240 account_path
: String
,
242 contact
: Vec
<String
>,
243 rsa_bits
: Option
<u32>,
244 eab_kid
: Option
<String
>,
245 eab_hmac_key
: Option
<String
>,
246 ) -> Result
<(), Error
> {
247 this
.inner
.lock().unwrap().new_account(
252 eab_kid
.zip(eab_hmac_key
),
256 /// Get the directory's meta information.
258 pub fn get_meta(#[try_from_ref] this: &Acme) -> Result<Option<Meta>, Error> {
259 match this
.inner
.lock().unwrap().client
.directory()?
.meta() {
260 Some(meta
) => Ok(Some(meta
.clone())),
265 /// Get the account's directory URL.
267 pub fn directory(#[try_from_ref] this: &Acme) -> Result<String, Error> {
268 Ok(this
.inner
.lock().unwrap().client
.directory()?
.url
.clone())
271 /// Serialize the account data.
273 pub fn account(#[try_from_ref] this: &Acme) -> Result<AccountData, Error> {
274 this
.inner
.lock().unwrap().to_account_data()
277 /// Get the account's location URL.
279 pub fn location(#[try_from_ref] this: &Acme) -> Result<String, Error> {
280 Ok(this
.inner
.lock().unwrap().account()?
.location
.clone())
283 /// Get the account's agreed-to ToS URL.
285 pub fn tos_url(#[try_from_ref] this: &Acme) -> Option<String> {
286 this
.inner
.lock().unwrap().tos
.clone()
289 /// Get the debug flag.
291 pub fn debug(#[try_from_ref] this: &Acme) -> bool {
292 this
.inner
.lock().unwrap().debug
295 /// Get the debug flag.
297 pub fn set_debug(#[try_from_ref] this: &Acme, on: bool) {
298 this
.inner
.lock().unwrap().debug
= on
;
301 /// Place a new order.
304 #[try_from_ref] this: &Acme,
305 domains
: Vec
<String
>,
306 ) -> Result
<(String
, OrderData
), Error
> {
307 let order
: Order
= this
.inner
.lock().unwrap().client
.new_order(domains
)?
;
308 Ok((order
.location
, order
.data
))
311 /// Get the authorization info given an authorization URL.
313 /// This should be an URL found in the `authorizations` array in the `OrderData` returned from
316 pub fn get_authorization(
317 #[try_from_ref] this: &Acme,
319 ) -> Result
<Authorization
, Error
> {
320 Ok(this
.inner
.lock().unwrap().client
.get_authorization(url
)?
)
323 /// Query an order given its URL.
325 /// The corresponding URL is returned as first value from the `new_order` call.
327 pub fn get_order(#[try_from_ref] this: &Acme, url: &str) -> Result<OrderData, Error> {
328 Ok(this
.inner
.lock().unwrap().client
.get_order(url
)?
)
331 /// Get the key authorization string for a challenge given a token.
333 pub fn key_authorization(#[try_from_ref] this: &Acme, token: &str) -> Result<String, Error> {
334 Ok(this
.inner
.lock().unwrap().client
.key_authorization(token
)?
)
337 /// Get the key dns-01 TXT challenge value for a token.
339 pub fn dns_01_txt_value(#[try_from_ref] this: &Acme, token: &str) -> Result<String, Error> {
340 Ok(this
.inner
.lock().unwrap().client
.dns_01_txt_value(token
)?
)
343 /// Request validation of a challenge by URL.
345 /// Given an `Authorization`, it'll contain `challenges`. These contain `url`s pointing to a
346 /// method used to request challenge authorization. This is the URL used for this method,
347 /// *after* performing the necessary steps to satisfy the challenge. (Eg. after setting up a
348 /// DNS TXT entry using the `dns-01` type challenge's key authorization.
350 pub fn request_challenge_validation(
351 #[try_from_ref] this: &Acme,
353 ) -> Result
<Challenge
, Error
> {
359 .request_challenge_validation(url
)?
)
362 /// Request finalization of an order.
364 /// The `url` should be the 'finalize' URL of the order.
366 pub fn finalize_order(
367 #[try_from_ref] this: &Acme,
370 ) -> Result
<(), Error
> {
371 Ok(this
.inner
.lock().unwrap().client
.finalize(url
, csr
)?
)
374 /// Download the certificate for an order.
376 /// The `url` should be the 'certificate' URL of the order.
378 pub fn get_certificate(#[try_from_ref] this: &Acme, url: &str) -> Result<ByteBuf, Error> {
380 this
.inner
.lock().unwrap().client
.get_certificate(url
)?
,
384 /// Update account data.
386 /// This can be used for example to deactivate an account or agree to ToS later on.
388 pub fn update_account(
389 #[try_from_ref] this: &Acme,
390 data
: HashMap
<String
, serde_json
::Value
>,
391 ) -> Result
<(), Error
> {
392 this
.inner
.lock().unwrap().update_account(&data
)?
;
396 /// Revoke an existing certificate using the certificate in PEM or DER form.
398 pub fn revoke_certificate(
399 #[try_from_ref] this: &Acme,
402 ) -> Result
<(), Error
> {
406 .revoke_certificate(&data
, reason
)?
;
412 pub fn set_proxy(#[try_from_ref] this: &Acme, proxy: String) {
413 this
.inner
.lock().unwrap().set_proxy(proxy
)