1 use std
::convert
::TryFrom
;
3 use std
::time
::Duration
;
5 use anyhow
::{bail, format_err, Error}
;
6 use openssl
::pkey
::PKey
;
7 use openssl
::x509
::X509
;
8 use serde
::{Deserialize, Serialize}
;
10 use proxmox
::api
::router
::SubdirMap
;
11 use proxmox
::api
::{api, Permission, Router, RpcEnvironment}
;
12 use proxmox
::list_subdirs_api_method
;
14 use pbs_api_types
::{NODE_SCHEMA, PRIV_SYS_MODIFY}
;
15 use pbs_buildcfg
::configdir
;
18 use crate::acme
::AcmeClient
;
19 use crate::api2
::types
::AcmeDomain
;
20 use crate::config
::node
::NodeConfig
;
21 use proxmox_rest_server
::WorkerTask
;
23 pub const ROUTER
: Router
= Router
::new()
24 .get(&list_subdirs_api_method
!(SUBDIRS
))
27 const SUBDIRS
: SubdirMap
= &[
28 ("acme", &ACME_ROUTER
),
32 .post(&API_METHOD_UPLOAD_CUSTOM_CERTIFICATE
)
33 .delete(&API_METHOD_DELETE_CUSTOM_CERTIFICATE
),
35 ("info", &Router
::new().get(&API_METHOD_GET_INFO
)),
38 const ACME_ROUTER
: Router
= Router
::new()
39 .get(&list_subdirs_api_method
!(ACME_SUBDIRS
))
40 .subdirs(ACME_SUBDIRS
);
42 const ACME_SUBDIRS
: SubdirMap
= &[(
45 .post(&API_METHOD_NEW_ACME_CERT
)
46 .put(&API_METHOD_RENEW_ACME_CERT
),
54 description
: "A SubjectAlternateName entry.",
60 /// Certificate information.
61 #[derive(Deserialize, Serialize)]
62 #[serde(rename_all = "kebab-case")]
63 pub struct CertificateInfo
{
64 /// Certificate file name.
65 #[serde(skip_serializing_if = "Option::is_none")]
66 filename
: Option
<String
>,
68 /// Certificate subject name.
71 /// List of certificate's SubjectAlternativeName entries.
74 /// Certificate issuer name.
77 /// Certificate's notBefore timestamp (UNIX epoch).
78 #[serde(skip_serializing_if = "Option::is_none")]
79 notbefore
: Option
<i64>,
81 /// Certificate's notAfter timestamp (UNIX epoch).
82 #[serde(skip_serializing_if = "Option::is_none")]
83 notafter
: Option
<i64>,
85 /// Certificate in PEM format.
86 #[serde(skip_serializing_if = "Option::is_none")]
89 /// Certificate's public key algorithm.
90 public_key_type
: String
,
92 /// Certificate's public key size if available.
93 #[serde(skip_serializing_if = "Option::is_none")]
94 public_key_bits
: Option
<u32>,
96 /// The SSL Fingerprint.
97 fingerprint
: Option
<String
>,
100 impl TryFrom
<&cert
::CertInfo
> for CertificateInfo
{
103 fn try_from(info
: &cert
::CertInfo
) -> Result
<Self, Self::Error
> {
104 let pubkey
= info
.public_key()?
;
108 subject
: info
.subject_name()?
,
113 // FIXME: Support `.ipaddress()`?
114 .filter_map(|name
| name
.dnsname().map(str::to_owned
))
117 .unwrap_or_default(),
118 issuer
: info
.issuer_name()?
,
119 notbefore
: info
.not_before_unix().ok(),
120 notafter
: info
.not_after_unix().ok(),
122 public_key_type
: openssl
::nid
::Nid
::from_raw(pubkey
.id().as_raw())
124 .unwrap_or("<unsupported key type>")
126 public_key_bits
: Some(pubkey
.bits()),
127 fingerprint
: Some(info
.fingerprint()?
),
132 fn get_certificate_pem() -> Result
<String
, Error
> {
133 let cert_path
= configdir
!("/proxy.pem");
134 let cert_pem
= proxmox
::tools
::fs
::file_get_contents(&cert_path
)?
;
135 String
::from_utf8(cert_pem
)
136 .map_err(|_
| format_err
!("certificate in {:?} is not a valid PEM file", cert_path
))
139 // to deduplicate error messages
140 fn pem_to_cert_info(pem
: &[u8]) -> Result
<cert
::CertInfo
, Error
> {
141 cert
::CertInfo
::from_pem(pem
)
142 .map_err(|err
| format_err
!("error loading proxy certificate: {}", err
))
148 node
: { schema: NODE_SCHEMA }
,
152 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
156 items
: { type: CertificateInfo }
,
157 description
: "List of certificate infos.",
160 /// Get certificate info.
161 pub fn get_info() -> Result
<Vec
<CertificateInfo
>, Error
> {
162 let cert_pem
= get_certificate_pem()?
;
163 let cert
= pem_to_cert_info(cert_pem
.as_bytes())?
;
165 Ok(vec
![CertificateInfo
{
166 filename
: Some("proxy.pem".to_string()), // we only have the one
168 ..CertificateInfo
::try_from(&cert
)?
175 node
: { schema: NODE_SCHEMA }
,
176 certificates
: { description: "PEM encoded certificate (chain)." }
,
177 key
: { description: "PEM encoded private key." }
,
178 // FIXME: widget-toolkit should have an option to disable using these 2 parameters...
180 description
: "UI compatibility parameter, ignored",
186 description
: "Force replacement of existing files.",
194 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
198 items
: { type: CertificateInfo }
,
199 description
: "List of certificate infos.",
203 /// Upload a custom certificate.
204 pub async
fn upload_custom_certificate(
205 certificates
: String
,
207 ) -> Result
<Vec
<CertificateInfo
>, Error
> {
208 let certificates
= X509
::stack_from_pem(certificates
.as_bytes())
209 .map_err(|err
| format_err
!("failed to decode certificate chain: {}", err
))?
;
210 let key
= PKey
::private_key_from_pem(key
.as_bytes())
211 .map_err(|err
| format_err
!("failed to parse private key: {}", err
))?
;
213 let certificates
= certificates
215 .try_fold(Vec
::<u8>::new(), |mut stack
, cert
| -> Result
<_
, Error
> {
216 if !stack
.is_empty() {
219 stack
.extend(cert
.to_pem()?
);
222 .map_err(|err
| format_err
!("error formatting certificate chain as PEM: {}", err
))?
;
224 let key
= key
.private_key_to_pem_pkcs8()?
;
226 crate::config
::set_proxy_certificate(&certificates
, &key
)?
;
227 crate::server
::reload_proxy_certificate().await?
;
235 node
: { schema: NODE_SCHEMA }
,
237 description
: "UI compatibility parameter, ignored",
245 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
249 /// Delete the current certificate and regenerate a self signed one.
250 pub async
fn delete_custom_certificate() -> Result
<(), Error
> {
251 let cert_path
= configdir
!("/proxy.pem");
252 // Here we fail since if this fails nothing else breaks anyway
253 std
::fs
::remove_file(&cert_path
)
254 .map_err(|err
| format_err
!("failed to unlink {:?} - {}", cert_path
, err
))?
;
256 let key_path
= configdir
!("/proxy.key");
257 if let Err(err
) = std
::fs
::remove_file(&key_path
) {
258 // Here we just log since the certificate is already gone and we'd rather try to generate
259 // the self-signed certificate even if this fails:
261 "failed to remove certificate private key {:?} - {}",
267 crate::config
::update_self_signed_cert(true)?
;
268 crate::server
::reload_proxy_certificate().await?
;
273 struct OrderedCertificate
{
274 certificate
: hyper
::body
::Bytes
,
275 private_key_pem
: Vec
<u8>,
278 async
fn order_certificate(
279 worker
: Arc
<WorkerTask
>,
280 node_config
: &NodeConfig
,
281 ) -> Result
<Option
<OrderedCertificate
>, Error
> {
282 use proxmox_acme_rs
::authorization
::Status
;
283 use proxmox_acme_rs
::order
::Identifier
;
285 let domains
= node_config
.acme_domains().try_fold(
286 Vec
::<AcmeDomain
>::new(),
287 |mut acc
, domain
| -> Result
<_
, Error
> {
288 let mut domain
= domain?
;
289 domain
.domain
.make_ascii_lowercase();
290 if let Some(alias
) = &mut domain
.alias
{
291 alias
.make_ascii_lowercase();
298 let get_domain_config
= |domain
: &str| {
301 .find(|d
| d
.domain
== domain
)
302 .ok_or_else(|| format_err
!("no config for domain '{}'", domain
))
305 if domains
.is_empty() {
306 worker
.log("No domains configured to be ordered from an ACME server.");
310 let (plugins
, _
) = crate::config
::acme
::plugin
::config()?
;
312 let mut acme
= node_config
.acme_client().await?
;
314 worker
.log("Placing ACME order");
316 .new_order(domains
.iter().map(|d
| d
.domain
.to_ascii_lowercase()))
318 worker
.log(format
!("Order URL: {}", order
.location
));
320 let identifiers
: Vec
<String
> = order
324 .map(|identifier
| match identifier
{
325 Identifier
::Dns(domain
) => domain
.clone(),
329 for auth_url
in &order
.data
.authorizations
{
330 worker
.log(format
!("Getting authorization details from '{}'", auth_url
));
331 let mut auth
= acme
.get_authorization(&auth_url
).await?
;
333 let domain
= match &mut auth
.identifier
{
334 Identifier
::Dns(domain
) => domain
.to_ascii_lowercase(),
337 if auth
.status
== Status
::Valid
{
338 worker
.log(format
!("{} is already validated!", domain
));
342 worker
.log(format
!("The validation for {} is pending", domain
));
343 let domain_config
: &AcmeDomain
= get_domain_config(&domain
)?
;
344 let plugin_id
= domain_config
.plugin
.as_deref().unwrap_or("standalone");
346 crate::acme
::get_acme_plugin(&plugins
, plugin_id
)?
.ok_or_else(|| {
347 format_err
!("plugin '{}' for domain '{}' not found!", plugin_id
, domain
)
350 worker
.log("Setting up validation plugin");
351 let validation_url
= plugin_cfg
352 .setup(&mut acme
, &auth
, domain_config
, Arc
::clone(&worker
))
355 let result
= request_validation(&worker
, &mut acme
, auth_url
, validation_url
).await
;
357 if let Err(err
) = plugin_cfg
358 .teardown(&mut acme
, &auth
, domain_config
, Arc
::clone(&worker
))
362 "Failed to teardown plugin '{}' for domain '{}' - {}",
363 plugin_id
, domain
, err
370 worker
.log("All domains validated");
371 worker
.log("Creating CSR");
373 let csr
= proxmox_acme_rs
::util
::Csr
::generate(&identifiers
, &Default
::default())?
;
374 let mut finalize_error_cnt
= 0u8;
375 let order_url
= &order
.location
;
378 use proxmox_acme_rs
::order
::Status
;
380 order
= acme
.get_order(order_url
).await?
;
384 worker
.log("still pending, trying to finalize anyway");
388 .ok_or_else(|| format_err
!("missing 'finalize' URL in order"))?
;
389 if let Err(err
) = acme
.finalize(finalize
, &csr
.data
).await
{
390 if finalize_error_cnt
>= 5 {
391 return Err(err
.into());
394 finalize_error_cnt
+= 1;
396 tokio
::time
::sleep(Duration
::from_secs(5)).await
;
399 worker
.log("order is ready, finalizing");
403 .ok_or_else(|| format_err
!("missing 'finalize' URL in order"))?
;
404 acme
.finalize(finalize
, &csr
.data
).await?
;
405 tokio
::time
::sleep(Duration
::from_secs(5)).await
;
407 Status
::Processing
=> {
408 worker
.log("still processing, trying again in 30 seconds");
409 tokio
::time
::sleep(Duration
::from_secs(30)).await
;
415 other
=> bail
!("order status: {:?}", other
),
419 worker
.log("Downloading certificate");
420 let certificate
= acme
425 .ok_or_else(|| format_err
!("missing certificate url in finalized order"))?
,
429 Ok(Some(OrderedCertificate
{
431 private_key_pem
: csr
.private_key_pem
,
435 async
fn request_validation(
437 acme
: &mut AcmeClient
,
439 validation_url
: &str,
440 ) -> Result
<(), Error
> {
441 worker
.log("Triggering validation");
442 acme
.request_challenge_validation(&validation_url
).await?
;
444 worker
.log("Sleeping for 5 seconds");
445 tokio
::time
::sleep(Duration
::from_secs(5)).await
;
448 use proxmox_acme_rs
::authorization
::Status
;
450 let auth
= acme
.get_authorization(&auth_url
).await?
;
453 worker
.log("Status is still 'pending', trying again in 10 seconds");
454 tokio
::time
::sleep(Duration
::from_secs(10)).await
;
456 Status
::Valid
=> return Ok(()),
458 "validating challenge '{}' failed - status: {:?}",
469 node
: { schema: NODE_SCHEMA }
,
471 description
: "Force replacement of existing files.",
479 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
483 /// Order a new ACME certificate.
484 pub fn new_acme_cert(force
: bool
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<String
, Error
> {
485 spawn_certificate_worker("acme-new-cert", force
, rpcenv
)
491 node
: { schema: NODE_SCHEMA }
,
493 description
: "Force replacement of existing files.",
501 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
505 /// Renew the current ACME certificate if it expires within 30 days (or always if the `force`
506 /// parameter is set).
507 pub fn renew_acme_cert(force
: bool
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<String
, Error
> {
508 if !cert_expires_soon()?
&& !force
{
509 bail
!("Certificate does not expire within the next 30 days and 'force' is not set.")
512 spawn_certificate_worker("acme-renew-cert", force
, rpcenv
)
515 /// Check whether the current certificate expires within the next 30 days.
516 pub fn cert_expires_soon() -> Result
<bool
, Error
> {
517 let cert
= pem_to_cert_info(get_certificate_pem()?
.as_bytes())?
;
518 cert
.is_expired_after_epoch(proxmox
::tools
::time
::epoch_i64() + 30 * 24 * 60 * 60)
519 .map_err(|err
| format_err
!("Failed to check certificate expiration date: {}", err
))
522 fn spawn_certificate_worker(
525 rpcenv
: &mut dyn RpcEnvironment
,
526 ) -> Result
<String
, Error
> {
527 // We only have 1 certificate path in PBS which makes figuring out whether or not it is a
528 // custom one too hard... We keep the parameter because the widget-toolkit may be using it...
531 let (node_config
, _digest
) = crate::config
::node
::config()?
;
533 let auth_id
= rpcenv
.get_auth_id().unwrap();
535 WorkerTask
::spawn(name
, None
, auth_id
, true, move |worker
| async
move {
536 if let Some(cert
) = order_certificate(worker
, &node_config
).await?
{
537 crate::config
::set_proxy_certificate(&cert
.certificate
, &cert
.private_key_pem
)?
;
538 crate::server
::reload_proxy_certificate().await?
;
547 node
: { schema: NODE_SCHEMA }
,
551 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
555 /// Renew the current ACME certificate if it expires within 30 days (or always if the `force`
556 /// parameter is set).
557 pub fn revoke_acme_cert(rpcenv
: &mut dyn RpcEnvironment
) -> Result
<String
, Error
> {
558 let (node_config
, _digest
) = crate::config
::node
::config()?
;
560 let cert_pem
= get_certificate_pem()?
;
562 let auth_id
= rpcenv
.get_auth_id().unwrap();
569 move |worker
| async
move {
570 worker
.log("Loading ACME account");
571 let mut acme
= node_config
.acme_client().await?
;
572 worker
.log("Revoking old certificate");
573 acme
.revoke_certificate(cert_pem
.as_bytes(), None
).await?
;
574 worker
.log("Deleting certificate and regenerating a self-signed one");
575 delete_custom_certificate().await?
;