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_router
::SubdirMap
;
11 use proxmox_router
::{Permission, Router, RpcEnvironment}
;
12 use proxmox_router
::list_subdirs_api_method
;
13 use proxmox_schema
::api
;
14 use proxmox_sys
::{task_log, task_warn}
;
16 use pbs_api_types
::{NODE_SCHEMA, PRIV_SYS_MODIFY}
;
17 use pbs_buildcfg
::configdir
;
20 use crate::acme
::AcmeClient
;
21 use crate::api2
::types
::AcmeDomain
;
22 use crate::config
::node
::NodeConfig
;
23 use proxmox_rest_server
::WorkerTask
;
25 pub const ROUTER
: Router
= Router
::new()
26 .get(&list_subdirs_api_method
!(SUBDIRS
))
29 const SUBDIRS
: SubdirMap
= &[
30 ("acme", &ACME_ROUTER
),
34 .post(&API_METHOD_UPLOAD_CUSTOM_CERTIFICATE
)
35 .delete(&API_METHOD_DELETE_CUSTOM_CERTIFICATE
),
37 ("info", &Router
::new().get(&API_METHOD_GET_INFO
)),
40 const ACME_ROUTER
: Router
= Router
::new()
41 .get(&list_subdirs_api_method
!(ACME_SUBDIRS
))
42 .subdirs(ACME_SUBDIRS
);
44 const ACME_SUBDIRS
: SubdirMap
= &[(
47 .post(&API_METHOD_NEW_ACME_CERT
)
48 .put(&API_METHOD_RENEW_ACME_CERT
),
56 description
: "A SubjectAlternateName entry.",
62 /// Certificate information.
63 #[derive(Deserialize, Serialize)]
64 #[serde(rename_all = "kebab-case")]
65 pub struct CertificateInfo
{
66 /// Certificate file name.
67 #[serde(skip_serializing_if = "Option::is_none")]
68 filename
: Option
<String
>,
70 /// Certificate subject name.
73 /// List of certificate's SubjectAlternativeName entries.
76 /// Certificate issuer name.
79 /// Certificate's notBefore timestamp (UNIX epoch).
80 #[serde(skip_serializing_if = "Option::is_none")]
81 notbefore
: Option
<i64>,
83 /// Certificate's notAfter timestamp (UNIX epoch).
84 #[serde(skip_serializing_if = "Option::is_none")]
85 notafter
: Option
<i64>,
87 /// Certificate in PEM format.
88 #[serde(skip_serializing_if = "Option::is_none")]
91 /// Certificate's public key algorithm.
92 public_key_type
: String
,
94 /// Certificate's public key size if available.
95 #[serde(skip_serializing_if = "Option::is_none")]
96 public_key_bits
: Option
<u32>,
98 /// The SSL Fingerprint.
99 fingerprint
: Option
<String
>,
102 impl TryFrom
<&cert
::CertInfo
> for CertificateInfo
{
105 fn try_from(info
: &cert
::CertInfo
) -> Result
<Self, Self::Error
> {
106 let pubkey
= info
.public_key()?
;
110 subject
: info
.subject_name()?
,
115 // FIXME: Support `.ipaddress()`?
116 .filter_map(|name
| name
.dnsname().map(str::to_owned
))
119 .unwrap_or_default(),
120 issuer
: info
.issuer_name()?
,
121 notbefore
: info
.not_before_unix().ok(),
122 notafter
: info
.not_after_unix().ok(),
124 public_key_type
: openssl
::nid
::Nid
::from_raw(pubkey
.id().as_raw())
126 .unwrap_or("<unsupported key type>")
128 public_key_bits
: Some(pubkey
.bits()),
129 fingerprint
: Some(info
.fingerprint()?
),
134 fn get_certificate_pem() -> Result
<String
, Error
> {
135 let cert_path
= configdir
!("/proxy.pem");
136 let cert_pem
= proxmox
::tools
::fs
::file_get_contents(&cert_path
)?
;
137 String
::from_utf8(cert_pem
)
138 .map_err(|_
| format_err
!("certificate in {:?} is not a valid PEM file", cert_path
))
141 // to deduplicate error messages
142 fn pem_to_cert_info(pem
: &[u8]) -> Result
<cert
::CertInfo
, Error
> {
143 cert
::CertInfo
::from_pem(pem
)
144 .map_err(|err
| format_err
!("error loading proxy certificate: {}", err
))
150 node
: { schema: NODE_SCHEMA }
,
154 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
158 items
: { type: CertificateInfo }
,
159 description
: "List of certificate infos.",
162 /// Get certificate info.
163 pub fn get_info() -> Result
<Vec
<CertificateInfo
>, Error
> {
164 let cert_pem
= get_certificate_pem()?
;
165 let cert
= pem_to_cert_info(cert_pem
.as_bytes())?
;
167 Ok(vec
![CertificateInfo
{
168 filename
: Some("proxy.pem".to_string()), // we only have the one
170 ..CertificateInfo
::try_from(&cert
)?
177 node
: { schema: NODE_SCHEMA }
,
178 certificates
: { description: "PEM encoded certificate (chain)." }
,
179 key
: { description: "PEM encoded private key." }
,
180 // FIXME: widget-toolkit should have an option to disable using these 2 parameters...
182 description
: "UI compatibility parameter, ignored",
188 description
: "Force replacement of existing files.",
196 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
200 items
: { type: CertificateInfo }
,
201 description
: "List of certificate infos.",
205 /// Upload a custom certificate.
206 pub async
fn upload_custom_certificate(
207 certificates
: String
,
209 ) -> Result
<Vec
<CertificateInfo
>, Error
> {
210 let certificates
= X509
::stack_from_pem(certificates
.as_bytes())
211 .map_err(|err
| format_err
!("failed to decode certificate chain: {}", err
))?
;
212 let key
= PKey
::private_key_from_pem(key
.as_bytes())
213 .map_err(|err
| format_err
!("failed to parse private key: {}", err
))?
;
215 let certificates
= certificates
217 .try_fold(Vec
::<u8>::new(), |mut stack
, cert
| -> Result
<_
, Error
> {
218 if !stack
.is_empty() {
221 stack
.extend(cert
.to_pem()?
);
224 .map_err(|err
| format_err
!("error formatting certificate chain as PEM: {}", err
))?
;
226 let key
= key
.private_key_to_pem_pkcs8()?
;
228 crate::config
::set_proxy_certificate(&certificates
, &key
)?
;
229 crate::server
::reload_proxy_certificate().await?
;
237 node
: { schema: NODE_SCHEMA }
,
239 description
: "UI compatibility parameter, ignored",
247 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
251 /// Delete the current certificate and regenerate a self signed one.
252 pub async
fn delete_custom_certificate() -> Result
<(), Error
> {
253 let cert_path
= configdir
!("/proxy.pem");
254 // Here we fail since if this fails nothing else breaks anyway
255 std
::fs
::remove_file(&cert_path
)
256 .map_err(|err
| format_err
!("failed to unlink {:?} - {}", cert_path
, err
))?
;
258 let key_path
= configdir
!("/proxy.key");
259 if let Err(err
) = std
::fs
::remove_file(&key_path
) {
260 // Here we just log since the certificate is already gone and we'd rather try to generate
261 // the self-signed certificate even if this fails:
263 "failed to remove certificate private key {:?} - {}",
269 crate::config
::update_self_signed_cert(true)?
;
270 crate::server
::reload_proxy_certificate().await?
;
275 struct OrderedCertificate
{
276 certificate
: hyper
::body
::Bytes
,
277 private_key_pem
: Vec
<u8>,
280 async
fn order_certificate(
281 worker
: Arc
<WorkerTask
>,
282 node_config
: &NodeConfig
,
283 ) -> Result
<Option
<OrderedCertificate
>, Error
> {
284 use proxmox_acme_rs
::authorization
::Status
;
285 use proxmox_acme_rs
::order
::Identifier
;
287 let domains
= node_config
.acme_domains().try_fold(
288 Vec
::<AcmeDomain
>::new(),
289 |mut acc
, domain
| -> Result
<_
, Error
> {
290 let mut domain
= domain?
;
291 domain
.domain
.make_ascii_lowercase();
292 if let Some(alias
) = &mut domain
.alias
{
293 alias
.make_ascii_lowercase();
300 let get_domain_config
= |domain
: &str| {
303 .find(|d
| d
.domain
== domain
)
304 .ok_or_else(|| format_err
!("no config for domain '{}'", domain
))
307 if domains
.is_empty() {
308 task_log
!(worker
, "No domains configured to be ordered from an ACME server.");
312 let (plugins
, _
) = crate::config
::acme
::plugin
::config()?
;
314 let mut acme
= node_config
.acme_client().await?
;
316 task_log
!(worker
, "Placing ACME order");
318 .new_order(domains
.iter().map(|d
| d
.domain
.to_ascii_lowercase()))
320 task_log
!(worker
, "Order URL: {}", order
.location
);
322 let identifiers
: Vec
<String
> = order
326 .map(|identifier
| match identifier
{
327 Identifier
::Dns(domain
) => domain
.clone(),
331 for auth_url
in &order
.data
.authorizations
{
332 task_log
!(worker
, "Getting authorization details from '{}'", auth_url
);
333 let mut auth
= acme
.get_authorization(&auth_url
).await?
;
335 let domain
= match &mut auth
.identifier
{
336 Identifier
::Dns(domain
) => domain
.to_ascii_lowercase(),
339 if auth
.status
== Status
::Valid
{
340 task_log
!(worker
, "{} is already validated!", domain
);
344 task_log
!(worker
, "The validation for {} is pending", domain
);
345 let domain_config
: &AcmeDomain
= get_domain_config(&domain
)?
;
346 let plugin_id
= domain_config
.plugin
.as_deref().unwrap_or("standalone");
348 crate::acme
::get_acme_plugin(&plugins
, plugin_id
)?
.ok_or_else(|| {
349 format_err
!("plugin '{}' for domain '{}' not found!", plugin_id
, domain
)
352 task_log
!(worker
, "Setting up validation plugin");
353 let validation_url
= plugin_cfg
354 .setup(&mut acme
, &auth
, domain_config
, Arc
::clone(&worker
))
357 let result
= request_validation(&worker
, &mut acme
, auth_url
, validation_url
).await
;
359 if let Err(err
) = plugin_cfg
360 .teardown(&mut acme
, &auth
, domain_config
, Arc
::clone(&worker
))
365 "Failed to teardown plugin '{}' for domain '{}' - {}",
366 plugin_id
, domain
, err
373 task_log
!(worker
, "All domains validated");
374 task_log
!(worker
, "Creating CSR");
376 let csr
= proxmox_acme_rs
::util
::Csr
::generate(&identifiers
, &Default
::default())?
;
377 let mut finalize_error_cnt
= 0u8;
378 let order_url
= &order
.location
;
381 use proxmox_acme_rs
::order
::Status
;
383 order
= acme
.get_order(order_url
).await?
;
387 task_log
!(worker
, "still pending, trying to finalize anyway");
391 .ok_or_else(|| format_err
!("missing 'finalize' URL in order"))?
;
392 if let Err(err
) = acme
.finalize(finalize
, &csr
.data
).await
{
393 if finalize_error_cnt
>= 5 {
394 return Err(err
.into());
397 finalize_error_cnt
+= 1;
399 tokio
::time
::sleep(Duration
::from_secs(5)).await
;
402 task_log
!(worker
, "order is ready, finalizing");
406 .ok_or_else(|| format_err
!("missing 'finalize' URL in order"))?
;
407 acme
.finalize(finalize
, &csr
.data
).await?
;
408 tokio
::time
::sleep(Duration
::from_secs(5)).await
;
410 Status
::Processing
=> {
411 task_log
!(worker
, "still processing, trying again in 30 seconds");
412 tokio
::time
::sleep(Duration
::from_secs(30)).await
;
415 task_log
!(worker
, "valid");
418 other
=> bail
!("order status: {:?}", other
),
422 task_log
!(worker
, "Downloading certificate");
423 let certificate
= acme
428 .ok_or_else(|| format_err
!("missing certificate url in finalized order"))?
,
432 Ok(Some(OrderedCertificate
{
434 private_key_pem
: csr
.private_key_pem
,
438 async
fn request_validation(
440 acme
: &mut AcmeClient
,
442 validation_url
: &str,
443 ) -> Result
<(), Error
> {
444 task_log
!(worker
, "Triggering validation");
445 acme
.request_challenge_validation(&validation_url
).await?
;
447 task_log
!(worker
, "Sleeping for 5 seconds");
448 tokio
::time
::sleep(Duration
::from_secs(5)).await
;
451 use proxmox_acme_rs
::authorization
::Status
;
453 let auth
= acme
.get_authorization(&auth_url
).await?
;
456 task_log
!(worker
, "Status is still 'pending', trying again in 10 seconds");
457 tokio
::time
::sleep(Duration
::from_secs(10)).await
;
459 Status
::Valid
=> return Ok(()),
461 "validating challenge '{}' failed - status: {:?}",
472 node
: { schema: NODE_SCHEMA }
,
474 description
: "Force replacement of existing files.",
482 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
486 /// Order a new ACME certificate.
487 pub fn new_acme_cert(force
: bool
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<String
, Error
> {
488 spawn_certificate_worker("acme-new-cert", force
, rpcenv
)
494 node
: { schema: NODE_SCHEMA }
,
496 description
: "Force replacement of existing files.",
504 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
508 /// Renew the current ACME certificate if it expires within 30 days (or always if the `force`
509 /// parameter is set).
510 pub fn renew_acme_cert(force
: bool
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<String
, Error
> {
511 if !cert_expires_soon()?
&& !force
{
512 bail
!("Certificate does not expire within the next 30 days and 'force' is not set.")
515 spawn_certificate_worker("acme-renew-cert", force
, rpcenv
)
518 /// Check whether the current certificate expires within the next 30 days.
519 pub fn cert_expires_soon() -> Result
<bool
, Error
> {
520 let cert
= pem_to_cert_info(get_certificate_pem()?
.as_bytes())?
;
521 cert
.is_expired_after_epoch(proxmox_time
::epoch_i64() + 30 * 24 * 60 * 60)
522 .map_err(|err
| format_err
!("Failed to check certificate expiration date: {}", err
))
525 fn spawn_certificate_worker(
528 rpcenv
: &mut dyn RpcEnvironment
,
529 ) -> Result
<String
, Error
> {
530 // We only have 1 certificate path in PBS which makes figuring out whether or not it is a
531 // custom one too hard... We keep the parameter because the widget-toolkit may be using it...
534 let (node_config
, _digest
) = crate::config
::node
::config()?
;
536 let auth_id
= rpcenv
.get_auth_id().unwrap();
538 WorkerTask
::spawn(name
, None
, auth_id
, true, move |worker
| async
move {
539 if let Some(cert
) = order_certificate(worker
, &node_config
).await?
{
540 crate::config
::set_proxy_certificate(&cert
.certificate
, &cert
.private_key_pem
)?
;
541 crate::server
::reload_proxy_certificate().await?
;
550 node
: { schema: NODE_SCHEMA }
,
554 permission
: &Permission
::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY
, false),
558 /// Renew the current ACME certificate if it expires within 30 days (or always if the `force`
559 /// parameter is set).
560 pub fn revoke_acme_cert(rpcenv
: &mut dyn RpcEnvironment
) -> Result
<String
, Error
> {
561 let (node_config
, _digest
) = crate::config
::node
::config()?
;
563 let cert_pem
= get_certificate_pem()?
;
565 let auth_id
= rpcenv
.get_auth_id().unwrap();
572 move |worker
| async
move {
573 task_log
!(worker
, "Loading ACME account");
574 let mut acme
= node_config
.acme_client().await?
;
575 task_log
!(worker
, "Revoking old certificate");
576 acme
.revoke_certificate(cert_pem
.as_bytes(), None
).await?
;
577 task_log
!(worker
, "Deleting certificate and regenerating a self-signed one");
578 delete_custom_certificate().await?
;