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