]>
Commit | Line | Data |
---|---|---|
4088d5bc WB |
1 | use std::convert::TryFrom; |
2 | use std::sync::Arc; | |
3 | use std::time::Duration; | |
4 | ||
5 | use anyhow::{bail, format_err, Error}; | |
6 | use openssl::pkey::PKey; | |
7 | use openssl::x509::X509; | |
8 | use serde::{Deserialize, Serialize}; | |
9 | ||
6ef1b649 WB |
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; | |
4088d5bc | 14 | |
049a22a3 | 15 | use pbs_api_types::{NODE_SCHEMA, PRIV_SYS_MODIFY}; |
af06decd | 16 | use pbs_buildcfg::configdir; |
1ec0d70d | 17 | use pbs_tools::{task_log, task_warn, cert}; |
af06decd | 18 | |
4088d5bc | 19 | use crate::acme::AcmeClient; |
39c5db7f | 20 | use crate::api2::types::AcmeDomain; |
4088d5bc | 21 | use crate::config::node::NodeConfig; |
b9700a9f | 22 | use proxmox_rest_server::WorkerTask; |
4088d5bc WB |
23 | |
24 | pub const ROUTER: Router = Router::new() | |
25 | .get(&list_subdirs_api_method!(SUBDIRS)) | |
26 | .subdirs(SUBDIRS); | |
27 | ||
28 | const SUBDIRS: SubdirMap = &[ | |
29 | ("acme", &ACME_ROUTER), | |
30 | ( | |
31 | "custom", | |
32 | &Router::new() | |
33 | .post(&API_METHOD_UPLOAD_CUSTOM_CERTIFICATE) | |
34 | .delete(&API_METHOD_DELETE_CUSTOM_CERTIFICATE), | |
35 | ), | |
36 | ("info", &Router::new().get(&API_METHOD_GET_INFO)), | |
37 | ]; | |
38 | ||
39 | const ACME_ROUTER: Router = Router::new() | |
40 | .get(&list_subdirs_api_method!(ACME_SUBDIRS)) | |
41 | .subdirs(ACME_SUBDIRS); | |
42 | ||
43 | const ACME_SUBDIRS: SubdirMap = &[( | |
44 | "certificate", | |
45 | &Router::new() | |
46 | .post(&API_METHOD_NEW_ACME_CERT) | |
47 | .put(&API_METHOD_RENEW_ACME_CERT), | |
48 | )]; | |
49 | ||
50 | #[api( | |
51 | properties: { | |
52 | san: { | |
53 | type: Array, | |
54 | items: { | |
55 | description: "A SubjectAlternateName entry.", | |
56 | type: String, | |
57 | }, | |
58 | }, | |
59 | }, | |
60 | )] | |
61 | /// Certificate information. | |
62 | #[derive(Deserialize, Serialize)] | |
63 | #[serde(rename_all = "kebab-case")] | |
64 | pub struct CertificateInfo { | |
65 | /// Certificate file name. | |
66 | #[serde(skip_serializing_if = "Option::is_none")] | |
67 | filename: Option<String>, | |
68 | ||
69 | /// Certificate subject name. | |
70 | subject: String, | |
71 | ||
72 | /// List of certificate's SubjectAlternativeName entries. | |
73 | san: Vec<String>, | |
74 | ||
75 | /// Certificate issuer name. | |
76 | issuer: String, | |
77 | ||
78 | /// Certificate's notBefore timestamp (UNIX epoch). | |
79 | #[serde(skip_serializing_if = "Option::is_none")] | |
80 | notbefore: Option<i64>, | |
81 | ||
82 | /// Certificate's notAfter timestamp (UNIX epoch). | |
83 | #[serde(skip_serializing_if = "Option::is_none")] | |
84 | notafter: Option<i64>, | |
85 | ||
86 | /// Certificate in PEM format. | |
87 | #[serde(skip_serializing_if = "Option::is_none")] | |
88 | pem: Option<String>, | |
89 | ||
90 | /// Certificate's public key algorithm. | |
91 | public_key_type: String, | |
92 | ||
93 | /// Certificate's public key size if available. | |
94 | #[serde(skip_serializing_if = "Option::is_none")] | |
95 | public_key_bits: Option<u32>, | |
96 | ||
97 | /// The SSL Fingerprint. | |
98 | fingerprint: Option<String>, | |
99 | } | |
100 | ||
101 | impl TryFrom<&cert::CertInfo> for CertificateInfo { | |
102 | type Error = Error; | |
103 | ||
104 | fn try_from(info: &cert::CertInfo) -> Result<Self, Self::Error> { | |
105 | let pubkey = info.public_key()?; | |
106 | ||
107 | Ok(Self { | |
108 | filename: None, | |
109 | subject: info.subject_name()?, | |
110 | san: info | |
111 | .subject_alt_names() | |
112 | .map(|san| { | |
113 | san.into_iter() | |
114 | // FIXME: Support `.ipaddress()`? | |
115 | .filter_map(|name| name.dnsname().map(str::to_owned)) | |
116 | .collect() | |
117 | }) | |
118 | .unwrap_or_default(), | |
119 | issuer: info.issuer_name()?, | |
120 | notbefore: info.not_before_unix().ok(), | |
121 | notafter: info.not_after_unix().ok(), | |
122 | pem: None, | |
123 | public_key_type: openssl::nid::Nid::from_raw(pubkey.id().as_raw()) | |
124 | .long_name() | |
125 | .unwrap_or("<unsupported key type>") | |
126 | .to_owned(), | |
127 | public_key_bits: Some(pubkey.bits()), | |
128 | fingerprint: Some(info.fingerprint()?), | |
129 | }) | |
130 | } | |
131 | } | |
132 | ||
133 | fn get_certificate_pem() -> Result<String, Error> { | |
134 | let cert_path = configdir!("/proxy.pem"); | |
135 | let cert_pem = proxmox::tools::fs::file_get_contents(&cert_path)?; | |
136 | String::from_utf8(cert_pem) | |
137 | .map_err(|_| format_err!("certificate in {:?} is not a valid PEM file", cert_path)) | |
138 | } | |
139 | ||
140 | // to deduplicate error messages | |
141 | fn pem_to_cert_info(pem: &[u8]) -> Result<cert::CertInfo, Error> { | |
142 | cert::CertInfo::from_pem(pem) | |
143 | .map_err(|err| format_err!("error loading proxy certificate: {}", err)) | |
144 | } | |
145 | ||
146 | #[api( | |
147 | input: { | |
148 | properties: { | |
149 | node: { schema: NODE_SCHEMA }, | |
150 | }, | |
151 | }, | |
152 | access: { | |
153 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
154 | }, | |
155 | returns: { | |
156 | type: Array, | |
157 | items: { type: CertificateInfo }, | |
158 | description: "List of certificate infos.", | |
159 | }, | |
160 | )] | |
161 | /// Get certificate info. | |
162 | pub fn get_info() -> Result<Vec<CertificateInfo>, Error> { | |
163 | let cert_pem = get_certificate_pem()?; | |
164 | let cert = pem_to_cert_info(cert_pem.as_bytes())?; | |
165 | ||
166 | Ok(vec![CertificateInfo { | |
167 | filename: Some("proxy.pem".to_string()), // we only have the one | |
168 | pem: Some(cert_pem), | |
169 | ..CertificateInfo::try_from(&cert)? | |
170 | }]) | |
171 | } | |
172 | ||
173 | #[api( | |
174 | input: { | |
175 | properties: { | |
176 | node: { schema: NODE_SCHEMA }, | |
177 | certificates: { description: "PEM encoded certificate (chain)." }, | |
178 | key: { description: "PEM encoded private key." }, | |
fca1cef2 | 179 | // FIXME: widget-toolkit should have an option to disable using these 2 parameters... |
4088d5bc | 180 | restart: { |
fca1cef2 WB |
181 | description: "UI compatibility parameter, ignored", |
182 | type: Boolean, | |
4088d5bc WB |
183 | optional: true, |
184 | default: false, | |
185 | }, | |
4088d5bc WB |
186 | force: { |
187 | description: "Force replacement of existing files.", | |
188 | type: Boolean, | |
189 | optional: true, | |
190 | default: false, | |
191 | }, | |
192 | }, | |
193 | }, | |
194 | access: { | |
195 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
196 | }, | |
197 | returns: { | |
198 | type: Array, | |
199 | items: { type: CertificateInfo }, | |
200 | description: "List of certificate infos.", | |
201 | }, | |
202 | protected: true, | |
203 | )] | |
204 | /// Upload a custom certificate. | |
fca1cef2 | 205 | pub async fn upload_custom_certificate( |
4088d5bc WB |
206 | certificates: String, |
207 | key: String, | |
4088d5bc WB |
208 | ) -> Result<Vec<CertificateInfo>, Error> { |
209 | let certificates = X509::stack_from_pem(certificates.as_bytes()) | |
210 | .map_err(|err| format_err!("failed to decode certificate chain: {}", err))?; | |
211 | let key = PKey::private_key_from_pem(key.as_bytes()) | |
212 | .map_err(|err| format_err!("failed to parse private key: {}", err))?; | |
213 | ||
214 | let certificates = certificates | |
215 | .into_iter() | |
216 | .try_fold(Vec::<u8>::new(), |mut stack, cert| -> Result<_, Error> { | |
217 | if !stack.is_empty() { | |
218 | stack.push(b'\n'); | |
219 | } | |
220 | stack.extend(cert.to_pem()?); | |
221 | Ok(stack) | |
222 | }) | |
223 | .map_err(|err| format_err!("error formatting certificate chain as PEM: {}", err))?; | |
224 | ||
225 | let key = key.private_key_to_pem_pkcs8()?; | |
226 | ||
fca1cef2 WB |
227 | crate::config::set_proxy_certificate(&certificates, &key)?; |
228 | crate::server::reload_proxy_certificate().await?; | |
4088d5bc WB |
229 | |
230 | get_info() | |
231 | } | |
232 | ||
233 | #[api( | |
234 | input: { | |
235 | properties: { | |
236 | node: { schema: NODE_SCHEMA }, | |
237 | restart: { | |
fca1cef2 WB |
238 | description: "UI compatibility parameter, ignored", |
239 | type: Boolean, | |
4088d5bc WB |
240 | optional: true, |
241 | default: false, | |
242 | }, | |
243 | }, | |
244 | }, | |
245 | access: { | |
246 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
247 | }, | |
248 | protected: true, | |
249 | )] | |
250 | /// Delete the current certificate and regenerate a self signed one. | |
fca1cef2 | 251 | pub async fn delete_custom_certificate() -> Result<(), Error> { |
4088d5bc WB |
252 | let cert_path = configdir!("/proxy.pem"); |
253 | // Here we fail since if this fails nothing else breaks anyway | |
254 | std::fs::remove_file(&cert_path) | |
255 | .map_err(|err| format_err!("failed to unlink {:?} - {}", cert_path, err))?; | |
256 | ||
257 | let key_path = configdir!("/proxy.key"); | |
258 | if let Err(err) = std::fs::remove_file(&key_path) { | |
259 | // Here we just log since the certificate is already gone and we'd rather try to generate | |
260 | // the self-signed certificate even if this fails: | |
261 | log::error!( | |
262 | "failed to remove certificate private key {:?} - {}", | |
263 | key_path, | |
264 | err | |
265 | ); | |
266 | } | |
267 | ||
268 | crate::config::update_self_signed_cert(true)?; | |
fca1cef2 | 269 | crate::server::reload_proxy_certificate().await?; |
4088d5bc WB |
270 | |
271 | Ok(()) | |
272 | } | |
273 | ||
274 | struct OrderedCertificate { | |
275 | certificate: hyper::body::Bytes, | |
276 | private_key_pem: Vec<u8>, | |
277 | } | |
278 | ||
279 | async fn order_certificate( | |
280 | worker: Arc<WorkerTask>, | |
281 | node_config: &NodeConfig, | |
282 | ) -> Result<Option<OrderedCertificate>, Error> { | |
283 | use proxmox_acme_rs::authorization::Status; | |
284 | use proxmox_acme_rs::order::Identifier; | |
285 | ||
286 | let domains = node_config.acme_domains().try_fold( | |
287 | Vec::<AcmeDomain>::new(), | |
288 | |mut acc, domain| -> Result<_, Error> { | |
289 | let mut domain = domain?; | |
290 | domain.domain.make_ascii_lowercase(); | |
291 | if let Some(alias) = &mut domain.alias { | |
292 | alias.make_ascii_lowercase(); | |
293 | } | |
294 | acc.push(domain); | |
295 | Ok(acc) | |
296 | }, | |
297 | )?; | |
298 | ||
299 | let get_domain_config = |domain: &str| { | |
300 | domains | |
301 | .iter() | |
302 | .find(|d| d.domain == domain) | |
303 | .ok_or_else(|| format_err!("no config for domain '{}'", domain)) | |
304 | }; | |
305 | ||
306 | if domains.is_empty() { | |
1ec0d70d | 307 | task_log!(worker, "No domains configured to be ordered from an ACME server."); |
4088d5bc WB |
308 | return Ok(None); |
309 | } | |
310 | ||
311 | let (plugins, _) = crate::config::acme::plugin::config()?; | |
312 | ||
313 | let mut acme = node_config.acme_client().await?; | |
314 | ||
1ec0d70d | 315 | task_log!(worker, "Placing ACME order"); |
4088d5bc WB |
316 | let order = acme |
317 | .new_order(domains.iter().map(|d| d.domain.to_ascii_lowercase())) | |
318 | .await?; | |
1ec0d70d | 319 | task_log!(worker, "Order URL: {}", order.location); |
4088d5bc WB |
320 | |
321 | let identifiers: Vec<String> = order | |
322 | .data | |
323 | .identifiers | |
324 | .iter() | |
325 | .map(|identifier| match identifier { | |
326 | Identifier::Dns(domain) => domain.clone(), | |
327 | }) | |
328 | .collect(); | |
329 | ||
330 | for auth_url in &order.data.authorizations { | |
1ec0d70d | 331 | task_log!(worker, "Getting authorization details from '{}'", auth_url); |
4088d5bc WB |
332 | let mut auth = acme.get_authorization(&auth_url).await?; |
333 | ||
334 | let domain = match &mut auth.identifier { | |
335 | Identifier::Dns(domain) => domain.to_ascii_lowercase(), | |
336 | }; | |
337 | ||
338 | if auth.status == Status::Valid { | |
1ec0d70d | 339 | task_log!(worker, "{} is already validated!", domain); |
4088d5bc WB |
340 | continue; |
341 | } | |
342 | ||
1ec0d70d | 343 | task_log!(worker, "The validation for {} is pending", domain); |
4088d5bc WB |
344 | let domain_config: &AcmeDomain = get_domain_config(&domain)?; |
345 | let plugin_id = domain_config.plugin.as_deref().unwrap_or("standalone"); | |
346 | let mut plugin_cfg = | |
347 | crate::acme::get_acme_plugin(&plugins, plugin_id)?.ok_or_else(|| { | |
348 | format_err!("plugin '{}' for domain '{}' not found!", plugin_id, domain) | |
349 | })?; | |
350 | ||
1ec0d70d | 351 | task_log!(worker, "Setting up validation plugin"); |
4088d5bc WB |
352 | let validation_url = plugin_cfg |
353 | .setup(&mut acme, &auth, domain_config, Arc::clone(&worker)) | |
354 | .await?; | |
355 | ||
356 | let result = request_validation(&worker, &mut acme, auth_url, validation_url).await; | |
357 | ||
358 | if let Err(err) = plugin_cfg | |
359 | .teardown(&mut acme, &auth, domain_config, Arc::clone(&worker)) | |
360 | .await | |
361 | { | |
1ec0d70d DM |
362 | task_warn!( |
363 | worker, | |
4088d5bc WB |
364 | "Failed to teardown plugin '{}' for domain '{}' - {}", |
365 | plugin_id, domain, err | |
1ec0d70d | 366 | ); |
4088d5bc WB |
367 | } |
368 | ||
369 | let _: () = result?; | |
370 | } | |
371 | ||
1ec0d70d DM |
372 | task_log!(worker, "All domains validated"); |
373 | task_log!(worker, "Creating CSR"); | |
4088d5bc WB |
374 | |
375 | let csr = proxmox_acme_rs::util::Csr::generate(&identifiers, &Default::default())?; | |
376 | let mut finalize_error_cnt = 0u8; | |
377 | let order_url = &order.location; | |
378 | let mut order; | |
379 | loop { | |
380 | use proxmox_acme_rs::order::Status; | |
381 | ||
382 | order = acme.get_order(order_url).await?; | |
383 | ||
384 | match order.status { | |
385 | Status::Pending => { | |
1ec0d70d | 386 | task_log!(worker, "still pending, trying to finalize anyway"); |
4088d5bc WB |
387 | let finalize = order |
388 | .finalize | |
389 | .as_deref() | |
390 | .ok_or_else(|| format_err!("missing 'finalize' URL in order"))?; | |
391 | if let Err(err) = acme.finalize(finalize, &csr.data).await { | |
392 | if finalize_error_cnt >= 5 { | |
393 | return Err(err.into()); | |
394 | } | |
395 | ||
396 | finalize_error_cnt += 1; | |
397 | } | |
398 | tokio::time::sleep(Duration::from_secs(5)).await; | |
399 | } | |
400 | Status::Ready => { | |
1ec0d70d | 401 | task_log!(worker, "order is ready, finalizing"); |
4088d5bc WB |
402 | let finalize = order |
403 | .finalize | |
404 | .as_deref() | |
405 | .ok_or_else(|| format_err!("missing 'finalize' URL in order"))?; | |
406 | acme.finalize(finalize, &csr.data).await?; | |
407 | tokio::time::sleep(Duration::from_secs(5)).await; | |
408 | } | |
409 | Status::Processing => { | |
1ec0d70d | 410 | task_log!(worker, "still processing, trying again in 30 seconds"); |
4088d5bc WB |
411 | tokio::time::sleep(Duration::from_secs(30)).await; |
412 | } | |
413 | Status::Valid => { | |
1ec0d70d | 414 | task_log!(worker, "valid"); |
4088d5bc WB |
415 | break; |
416 | } | |
417 | other => bail!("order status: {:?}", other), | |
418 | } | |
419 | } | |
420 | ||
1ec0d70d | 421 | task_log!(worker, "Downloading certificate"); |
4088d5bc WB |
422 | let certificate = acme |
423 | .get_certificate( | |
424 | order | |
425 | .certificate | |
426 | .as_deref() | |
427 | .ok_or_else(|| format_err!("missing certificate url in finalized order"))?, | |
428 | ) | |
429 | .await?; | |
430 | ||
431 | Ok(Some(OrderedCertificate { | |
432 | certificate, | |
433 | private_key_pem: csr.private_key_pem, | |
434 | })) | |
435 | } | |
436 | ||
437 | async fn request_validation( | |
438 | worker: &WorkerTask, | |
439 | acme: &mut AcmeClient, | |
440 | auth_url: &str, | |
441 | validation_url: &str, | |
442 | ) -> Result<(), Error> { | |
1ec0d70d | 443 | task_log!(worker, "Triggering validation"); |
4088d5bc WB |
444 | acme.request_challenge_validation(&validation_url).await?; |
445 | ||
1ec0d70d | 446 | task_log!(worker, "Sleeping for 5 seconds"); |
4088d5bc WB |
447 | tokio::time::sleep(Duration::from_secs(5)).await; |
448 | ||
449 | loop { | |
450 | use proxmox_acme_rs::authorization::Status; | |
451 | ||
452 | let auth = acme.get_authorization(&auth_url).await?; | |
453 | match auth.status { | |
454 | Status::Pending => { | |
1ec0d70d | 455 | task_log!(worker, "Status is still 'pending', trying again in 10 seconds"); |
4088d5bc WB |
456 | tokio::time::sleep(Duration::from_secs(10)).await; |
457 | } | |
458 | Status::Valid => return Ok(()), | |
459 | other => bail!( | |
460 | "validating challenge '{}' failed - status: {:?}", | |
461 | validation_url, | |
462 | other | |
463 | ), | |
464 | } | |
465 | } | |
466 | } | |
467 | ||
468 | #[api( | |
469 | input: { | |
470 | properties: { | |
471 | node: { schema: NODE_SCHEMA }, | |
472 | force: { | |
473 | description: "Force replacement of existing files.", | |
474 | type: Boolean, | |
475 | optional: true, | |
476 | default: false, | |
477 | }, | |
478 | }, | |
479 | }, | |
480 | access: { | |
481 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
482 | }, | |
483 | protected: true, | |
484 | )] | |
485 | /// Order a new ACME certificate. | |
486 | pub fn new_acme_cert(force: bool, rpcenv: &mut dyn RpcEnvironment) -> Result<String, Error> { | |
487 | spawn_certificate_worker("acme-new-cert", force, rpcenv) | |
488 | } | |
489 | ||
490 | #[api( | |
491 | input: { | |
492 | properties: { | |
493 | node: { schema: NODE_SCHEMA }, | |
494 | force: { | |
495 | description: "Force replacement of existing files.", | |
496 | type: Boolean, | |
497 | optional: true, | |
498 | default: false, | |
499 | }, | |
500 | }, | |
501 | }, | |
502 | access: { | |
503 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
504 | }, | |
505 | protected: true, | |
506 | )] | |
507 | /// Renew the current ACME certificate if it expires within 30 days (or always if the `force` | |
508 | /// parameter is set). | |
509 | pub fn renew_acme_cert(force: bool, rpcenv: &mut dyn RpcEnvironment) -> Result<String, Error> { | |
510 | if !cert_expires_soon()? && !force { | |
511 | bail!("Certificate does not expire within the next 30 days and 'force' is not set.") | |
512 | } | |
513 | ||
514 | spawn_certificate_worker("acme-renew-cert", force, rpcenv) | |
515 | } | |
516 | ||
517 | /// Check whether the current certificate expires within the next 30 days. | |
518 | pub fn cert_expires_soon() -> Result<bool, Error> { | |
519 | let cert = pem_to_cert_info(get_certificate_pem()?.as_bytes())?; | |
6ef1b649 | 520 | cert.is_expired_after_epoch(proxmox_time::epoch_i64() + 30 * 24 * 60 * 60) |
4088d5bc WB |
521 | .map_err(|err| format_err!("Failed to check certificate expiration date: {}", err)) |
522 | } | |
523 | ||
524 | fn spawn_certificate_worker( | |
525 | name: &'static str, | |
526 | force: bool, | |
527 | rpcenv: &mut dyn RpcEnvironment, | |
528 | ) -> Result<String, Error> { | |
529 | // We only have 1 certificate path in PBS which makes figuring out whether or not it is a | |
530 | // custom one too hard... We keep the parameter because the widget-toolkit may be using it... | |
531 | let _ = force; | |
532 | ||
533 | let (node_config, _digest) = crate::config::node::config()?; | |
534 | ||
049a22a3 | 535 | let auth_id = rpcenv.get_auth_id().unwrap(); |
4088d5bc WB |
536 | |
537 | WorkerTask::spawn(name, None, auth_id, true, move |worker| async move { | |
538 | if let Some(cert) = order_certificate(worker, &node_config).await? { | |
fca1cef2 WB |
539 | crate::config::set_proxy_certificate(&cert.certificate, &cert.private_key_pem)?; |
540 | crate::server::reload_proxy_certificate().await?; | |
4088d5bc WB |
541 | } |
542 | Ok(()) | |
543 | }) | |
544 | } | |
545 | ||
546 | #[api( | |
547 | input: { | |
548 | properties: { | |
549 | node: { schema: NODE_SCHEMA }, | |
550 | }, | |
551 | }, | |
552 | access: { | |
553 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
554 | }, | |
555 | protected: true, | |
556 | )] | |
557 | /// Renew the current ACME certificate if it expires within 30 days (or always if the `force` | |
558 | /// parameter is set). | |
559 | pub fn revoke_acme_cert(rpcenv: &mut dyn RpcEnvironment) -> Result<String, Error> { | |
560 | let (node_config, _digest) = crate::config::node::config()?; | |
561 | ||
562 | let cert_pem = get_certificate_pem()?; | |
563 | ||
049a22a3 | 564 | let auth_id = rpcenv.get_auth_id().unwrap(); |
4088d5bc WB |
565 | |
566 | WorkerTask::spawn( | |
567 | "acme-revoke-cert", | |
568 | None, | |
569 | auth_id, | |
570 | true, | |
571 | move |worker| async move { | |
1ec0d70d | 572 | task_log!(worker, "Loading ACME account"); |
4088d5bc | 573 | let mut acme = node_config.acme_client().await?; |
1ec0d70d | 574 | task_log!(worker, "Revoking old certificate"); |
4088d5bc | 575 | acme.revoke_certificate(cert_pem.as_bytes(), None).await?; |
1ec0d70d | 576 | task_log!(worker, "Deleting certificate and regenerating a self-signed one"); |
fca1cef2 | 577 | delete_custom_certificate().await?; |
4088d5bc WB |
578 | Ok(()) |
579 | }, | |
580 | ) | |
581 | } |