1use crate::errors::{AuthError, Result};
33use base64::Engine;
34use ring::rand::SystemRandom;
35use ring::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair, KeyPair};
36use serde::{Deserialize, Serialize};
37use sha2::{Digest, Sha256};
38use std::collections::HashMap;
39use std::sync::Arc;
40use tokio::sync::RwLock;
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AcmeConfig {
47 pub directory_url: String,
49 pub contact_emails: Vec<String>,
51 pub agree_to_tos: bool,
53 pub timeout_secs: u64,
55}
56
57impl Default for AcmeConfig {
58 fn default() -> Self {
59 Self {
60 directory_url: "https://acme-staging-v02.api.letsencrypt.org/directory".to_string(),
61 contact_emails: Vec::new(),
62 agree_to_tos: false,
63 timeout_secs: 30,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct AcmeDirectory {
74 pub new_nonce: String,
75 pub new_account: String,
76 pub new_order: String,
77 #[serde(default)]
78 pub new_authz: Option<String>,
79 #[serde(default)]
80 pub revoke_cert: Option<String>,
81 #[serde(default)]
82 pub key_change: Option<String>,
83 #[serde(default)]
84 pub meta: Option<AcmeDirectoryMeta>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct AcmeDirectoryMeta {
90 #[serde(default)]
91 pub terms_of_service: Option<String>,
92 #[serde(default)]
93 pub website: Option<String>,
94 #[serde(default)]
95 pub caa_identities: Vec<String>,
96 #[serde(default)]
97 pub external_account_required: bool,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "lowercase")]
105pub enum AccountStatus {
106 Valid,
107 Deactivated,
108 Revoked,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct AcmeAccount {
115 pub status: AccountStatus,
116 #[serde(default)]
117 pub contact: Vec<String>,
118 #[serde(default)]
119 pub terms_of_service_agreed: bool,
120 #[serde(default)]
121 pub orders: Option<String>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "lowercase")]
129pub enum OrderStatus {
130 Pending,
131 Ready,
132 Processing,
133 Valid,
134 Invalid,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct AcmeIdentifier {
140 #[serde(rename = "type")]
141 pub id_type: String,
142 pub value: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct AcmeOrder {
149 pub status: OrderStatus,
150 #[serde(default)]
151 pub expires: Option<String>,
152 pub identifiers: Vec<AcmeIdentifier>,
153 #[serde(default)]
154 pub not_before: Option<String>,
155 #[serde(default)]
156 pub not_after: Option<String>,
157 pub authorizations: Vec<String>,
158 pub finalize: String,
159 #[serde(default)]
160 pub certificate: Option<String>,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "lowercase")]
168pub enum AuthorizationStatus {
169 Pending,
170 Valid,
171 Invalid,
172 Deactivated,
173 Expired,
174 Revoked,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct AcmeAuthorization {
180 pub identifier: AcmeIdentifier,
181 pub status: AuthorizationStatus,
182 #[serde(default)]
183 pub expires: Option<String>,
184 pub challenges: Vec<AcmeChallenge>,
185 #[serde(default)]
186 pub wildcard: bool,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub enum ChallengeType {
194 #[serde(rename = "http-01")]
195 Http01,
196 #[serde(rename = "dns-01")]
197 Dns01,
198 #[serde(rename = "tls-alpn-01")]
199 TlsAlpn01,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "lowercase")]
205pub enum ChallengeStatus {
206 Pending,
207 Processing,
208 Valid,
209 Invalid,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct AcmeChallenge {
215 #[serde(rename = "type")]
216 pub challenge_type: ChallengeType,
217 pub url: String,
218 pub status: ChallengeStatus,
219 pub token: String,
220 #[serde(default)]
221 pub validated: Option<String>,
222 #[serde(default)]
223 pub error: Option<serde_json::Value>,
224}
225
226fn jwk_thumbprint_p256(public_key: &[u8]) -> String {
230 if public_key.len() != 65 || public_key[0] != 0x04 {
232 return String::new();
233 }
234 let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
235 let x = b64.encode(&public_key[1..33]);
236 let y = b64.encode(&public_key[33..65]);
237
238 let thumbprint_input = format!(r#"{{"crv":"P-256","kty":"EC","x":"{x}","y":"{y}"}}"#);
240 let digest = Sha256::digest(thumbprint_input.as_bytes());
241 b64.encode(digest)
242}
243
244fn build_p256_jwk(public_key: &[u8]) -> serde_json::Value {
246 let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
247 if public_key.len() == 65 && public_key[0] == 0x04 {
248 serde_json::json!({
249 "kty": "EC",
250 "crv": "P-256",
251 "x": b64.encode(&public_key[1..33]),
252 "y": b64.encode(&public_key[33..65]),
253 })
254 } else {
255 serde_json::json!({})
256 }
257}
258
259fn create_jws(
264 key_pair: &EcdsaKeyPair,
265 url: &str,
266 nonce: &str,
267 payload: &str,
268 kid: Option<&str>,
269) -> Result<serde_json::Value> {
270 let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
271
272 let mut header = serde_json::json!({
273 "alg": "ES256",
274 "nonce": nonce,
275 "url": url,
276 });
277
278 if let Some(kid_url) = kid {
279 header["kid"] = serde_json::json!(kid_url);
280 } else {
281 header["jwk"] = build_p256_jwk(key_pair.public_key().as_ref());
282 }
283
284 let protected = b64.encode(header.to_string().as_bytes());
285 let payload_b64 = if payload.is_empty() {
286 String::new() } else {
288 b64.encode(payload.as_bytes())
289 };
290
291 let signing_input = format!("{protected}.{payload_b64}");
292 let rng = SystemRandom::new();
293 let signature = key_pair
294 .sign(&rng, signing_input.as_bytes())
295 .map_err(|_| AuthError::internal("ACME JWS signing failed"))?;
296
297 Ok(serde_json::json!({
298 "protected": protected,
299 "payload": payload_b64,
300 "signature": b64.encode(signature.as_ref()),
301 }))
302}
303
304pub struct AcmeClient {
308 config: AcmeConfig,
309 http: reqwest::Client,
310 key_pair: EcdsaKeyPair,
311 directory: AcmeDirectory,
312 account_url: Arc<RwLock<Option<String>>>,
313 nonce: Arc<RwLock<Option<String>>>,
314}
315
316impl AcmeClient {
317 pub async fn new(config: AcmeConfig) -> Result<Self> {
319 let http = reqwest::Client::builder()
320 .timeout(std::time::Duration::from_secs(config.timeout_secs))
321 .build()
322 .map_err(|e| AuthError::internal(&format!("HTTP client init failed: {e}")))?;
323
324 let rng = SystemRandom::new();
326 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
327 .map_err(|_| AuthError::internal("Failed to generate ACME account key"))?;
328 let key_pair =
329 EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
330 .map_err(|_| AuthError::internal("Failed to parse generated PKCS#8 key"))?;
331
332 let resp = http
334 .get(&config.directory_url)
335 .send()
336 .await
337 .map_err(|e| AuthError::internal(&format!("ACME directory fetch failed: {e}")))?;
338
339 let directory: AcmeDirectory = resp
340 .json()
341 .await
342 .map_err(|e| AuthError::internal(&format!("Invalid ACME directory response: {e}")))?;
343
344 Ok(Self {
345 config,
346 http,
347 key_pair,
348 directory,
349 account_url: Arc::new(RwLock::new(None)),
350 nonce: Arc::new(RwLock::new(None)),
351 })
352 }
353
354 pub fn from_parts(
356 config: AcmeConfig,
357 key_pair: EcdsaKeyPair,
358 directory: AcmeDirectory,
359 ) -> Self {
360 Self {
361 http: reqwest::Client::new(),
362 config,
363 key_pair,
364 directory,
365 account_url: Arc::new(RwLock::new(None)),
366 nonce: Arc::new(RwLock::new(None)),
367 }
368 }
369
370 pub fn directory(&self) -> &AcmeDirectory {
372 &self.directory
373 }
374
375 async fn fetch_nonce(&self) -> Result<String> {
377 let resp = self
378 .http
379 .head(&self.directory.new_nonce)
380 .send()
381 .await
382 .map_err(|e| AuthError::internal(&format!("Nonce fetch failed: {e}")))?;
383
384 let nonce = resp
385 .headers()
386 .get("replay-nonce")
387 .and_then(|v| v.to_str().ok())
388 .ok_or_else(|| AuthError::internal("ACME server did not return Replay-Nonce header"))?
389 .to_string();
390
391 *self.nonce.write().await = Some(nonce.clone());
392 Ok(nonce)
393 }
394
395 async fn get_nonce(&self) -> Result<String> {
397 let current = self.nonce.read().await.clone();
398 match current {
399 Some(n) => {
400 *self.nonce.write().await = None;
402 Ok(n)
403 }
404 None => self.fetch_nonce().await,
405 }
406 }
407
408 async fn signed_request(&self, url: &str, payload: &str) -> Result<reqwest::Response> {
410 let nonce = self.get_nonce().await?;
411 let kid = self.account_url.read().await.clone();
412 let jws = create_jws(&self.key_pair, url, &nonce, payload, kid.as_deref())?;
413
414 let resp = self
415 .http
416 .post(url)
417 .header("Content-Type", "application/jose+json")
418 .json(&jws)
419 .send()
420 .await
421 .map_err(|e| AuthError::internal(&format!("ACME request failed: {e}")))?;
422
423 if let Some(new_nonce) = resp
425 .headers()
426 .get("replay-nonce")
427 .and_then(|v| v.to_str().ok())
428 {
429 *self.nonce.write().await = Some(new_nonce.to_string());
430 }
431
432 Ok(resp)
433 }
434
435 pub async fn register_account(&self) -> Result<AcmeAccount> {
437 let contacts: Vec<String> = self
438 .config
439 .contact_emails
440 .iter()
441 .map(|e| format!("mailto:{e}"))
442 .collect();
443
444 let payload = serde_json::json!({
445 "termsOfServiceAgreed": self.config.agree_to_tos,
446 "contact": contacts,
447 });
448
449 let resp = self
450 .signed_request(&self.directory.new_account, &payload.to_string())
451 .await?;
452
453 if let Some(location) = resp.headers().get("location").and_then(|v| v.to_str().ok()) {
455 *self.account_url.write().await = Some(location.to_string());
456 }
457
458 let account: AcmeAccount = resp
459 .json()
460 .await
461 .map_err(|e| AuthError::internal(&format!("Invalid account response: {e}")))?;
462
463 Ok(account)
464 }
465
466 pub async fn create_order(&self, domains: &[&str]) -> Result<AcmeOrder> {
468 if domains.is_empty() {
469 return Err(AuthError::validation(
470 "At least one domain is required for an ACME order",
471 ));
472 }
473
474 let identifiers: Vec<AcmeIdentifier> = domains
475 .iter()
476 .map(|d| AcmeIdentifier {
477 id_type: "dns".to_string(),
478 value: d.to_string(),
479 })
480 .collect();
481
482 let payload = serde_json::json!({
483 "identifiers": identifiers,
484 });
485
486 let resp = self
487 .signed_request(&self.directory.new_order, &payload.to_string())
488 .await?;
489
490 let order: AcmeOrder = resp
491 .json()
492 .await
493 .map_err(|e| AuthError::internal(&format!("Invalid order response: {e}")))?;
494
495 Ok(order)
496 }
497
498 pub async fn get_authorization(&self, authz_url: &str) -> Result<AcmeAuthorization> {
500 let resp = self.signed_request(authz_url, "").await?;
501 let authz: AcmeAuthorization = resp
502 .json()
503 .await
504 .map_err(|e| AuthError::internal(&format!("Invalid authorization response: {e}")))?;
505 Ok(authz)
506 }
507
508 pub fn key_authorization(&self, token: &str) -> String {
512 let thumbprint = jwk_thumbprint_p256(self.key_pair.public_key().as_ref());
513 format!("{token}.{thumbprint}")
514 }
515
516 pub fn dns01_record_value(&self, token: &str) -> String {
520 let key_authz = self.key_authorization(token);
521 let digest = Sha256::digest(key_authz.as_bytes());
522 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest)
523 }
524
525 pub async fn respond_to_challenge(&self, challenge_url: &str) -> Result<AcmeChallenge> {
527 let resp = self.signed_request(challenge_url, "{}").await?;
528 let challenge: AcmeChallenge = resp
529 .json()
530 .await
531 .map_err(|e| AuthError::internal(&format!("Challenge response error: {e}")))?;
532 Ok(challenge)
533 }
534
535 pub async fn finalize_order(&self, finalize_url: &str, csr_der: &[u8]) -> Result<AcmeOrder> {
537 let csr_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(csr_der);
538 let payload = serde_json::json!({
539 "csr": csr_b64,
540 });
541
542 let resp = self
543 .signed_request(finalize_url, &payload.to_string())
544 .await?;
545
546 let order: AcmeOrder = resp
547 .json()
548 .await
549 .map_err(|e| AuthError::internal(&format!("Finalize response error: {e}")))?;
550
551 Ok(order)
552 }
553
554 pub async fn download_certificate(&self, cert_url: &str) -> Result<String> {
556 let resp = self.signed_request(cert_url, "").await?;
557 let pem = resp
558 .text()
559 .await
560 .map_err(|e| AuthError::internal(&format!("Certificate download error: {e}")))?;
561 Ok(pem)
562 }
563
564 pub fn account_thumbprint(&self) -> String {
566 jwk_thumbprint_p256(self.key_pair.public_key().as_ref())
567 }
568
569 pub async fn revoke_certificate(&self, cert_der: &[u8], reason: Option<u8>) -> Result<()> {
574 let revoke_url =
575 self.directory.revoke_cert.as_ref().ok_or_else(|| {
576 AuthError::config("ACME directory does not provide a revokeCert URL")
577 })?;
578
579 let cert_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(cert_der);
580 let mut payload = serde_json::json!({
581 "certificate": cert_b64,
582 });
583 if let Some(r) = reason {
584 payload["reason"] = serde_json::json!(r);
585 }
586
587 let resp = self
588 .signed_request(revoke_url, &payload.to_string())
589 .await?;
590 let status = resp.status().as_u16();
591 if status == 200 {
592 Ok(())
593 } else {
594 let body = resp.text().await.unwrap_or_default();
595 Err(AuthError::internal(&format!(
596 "Certificate revocation failed (HTTP {status}): {body}"
597 )))
598 }
599 }
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
606pub struct CertificateRecord {
607 pub domains: Vec<String>,
609 pub pem_chain: String,
611 pub issued_at: u64,
613 pub expires_at: u64,
615 pub renew_at: u64,
618 pub order_url: Option<String>,
620}
621
622impl CertificateRecord {
623 pub fn new(
628 domains: Vec<String>,
629 pem_chain: String,
630 issued_at: u64,
631 expires_at: u64,
632 renew_before_secs: u64,
633 ) -> Self {
634 let renew_at = expires_at.saturating_sub(renew_before_secs);
635 Self {
636 domains,
637 pem_chain,
638 issued_at,
639 expires_at,
640 renew_at,
641 order_url: None,
642 }
643 }
644
645 pub fn is_expired(&self) -> bool {
647 let now = std::time::SystemTime::now()
648 .duration_since(std::time::UNIX_EPOCH)
649 .unwrap_or_default()
650 .as_secs();
651 now >= self.expires_at
652 }
653
654 pub fn needs_renewal(&self) -> bool {
656 let now = std::time::SystemTime::now()
657 .duration_since(std::time::UNIX_EPOCH)
658 .unwrap_or_default()
659 .as_secs();
660 now >= self.renew_at
661 }
662
663 pub fn remaining_secs(&self) -> u64 {
665 let now = std::time::SystemTime::now()
666 .duration_since(std::time::UNIX_EPOCH)
667 .unwrap_or_default()
668 .as_secs();
669 self.expires_at.saturating_sub(now)
670 }
671}
672
673pub struct CertificateTracker {
678 records: Arc<RwLock<HashMap<String, CertificateRecord>>>,
679 renew_before_secs: u64,
681}
682
683impl CertificateTracker {
684 pub fn new() -> Self {
686 Self {
687 records: Arc::new(RwLock::new(HashMap::new())),
688 renew_before_secs: 30 * 24 * 3600, }
690 }
691
692 pub fn with_renew_window(renew_before_secs: u64) -> Self {
694 Self {
695 records: Arc::new(RwLock::new(HashMap::new())),
696 renew_before_secs,
697 }
698 }
699
700 pub async fn track(
702 &self,
703 domains: Vec<String>,
704 pem_chain: String,
705 issued_at: u64,
706 expires_at: u64,
707 ) -> String {
708 let key = domains.first().cloned().unwrap_or_default();
709 let record = CertificateRecord::new(
710 domains,
711 pem_chain,
712 issued_at,
713 expires_at,
714 self.renew_before_secs,
715 );
716 self.records.write().await.insert(key.clone(), record);
717 key
718 }
719
720 pub async fn get(&self, domain: &str) -> Option<CertificateRecord> {
722 self.records.read().await.get(domain).cloned()
723 }
724
725 pub async fn due_for_renewal(&self) -> Vec<String> {
727 self.records
728 .read()
729 .await
730 .iter()
731 .filter(|(_, r)| r.needs_renewal())
732 .map(|(k, _)| k.clone())
733 .collect()
734 }
735
736 pub async fn expired(&self) -> Vec<String> {
738 self.records
739 .read()
740 .await
741 .iter()
742 .filter(|(_, r)| r.is_expired())
743 .map(|(k, _)| k.clone())
744 .collect()
745 }
746
747 pub async fn remove_expired(&self) -> usize {
749 let mut records = self.records.write().await;
750 let before = records.len();
751 records.retain(|_, r| !r.is_expired());
752 before - records.len()
753 }
754
755 pub async fn remove(&self, domain: &str) -> bool {
757 self.records.write().await.remove(domain).is_some()
758 }
759
760 pub async fn count(&self) -> usize {
762 self.records.read().await.len()
763 }
764}
765
766impl Default for CertificateTracker {
767 fn default() -> Self {
768 Self::new()
769 }
770}
771
772#[derive(Debug, Clone, Default)]
779pub struct Http01ChallengeStore {
780 challenges: Arc<RwLock<HashMap<String, String>>>,
781}
782
783impl Http01ChallengeStore {
784 pub fn new() -> Self {
785 Self::default()
786 }
787
788 pub async fn add(&self, token: String, key_authorization: String) {
790 self.challenges
791 .write()
792 .await
793 .insert(token, key_authorization);
794 }
795
796 pub async fn get(&self, token: &str) -> Option<String> {
798 self.challenges.read().await.get(token).cloned()
799 }
800
801 pub async fn remove(&self, token: &str) {
803 self.challenges.write().await.remove(token);
804 }
805
806 pub async fn count(&self) -> usize {
808 self.challenges.read().await.len()
809 }
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 #[test]
819 fn test_config_defaults() {
820 let config = AcmeConfig::default();
821 assert!(config.directory_url.contains("staging"));
822 assert!(!config.agree_to_tos);
823 assert!(config.contact_emails.is_empty());
824 assert_eq!(config.timeout_secs, 30);
825 }
826
827 #[test]
830 fn test_jwk_thumbprint_p256_format() {
831 let mut key = vec![0x04];
833 key.extend_from_slice(&[0xAA; 32]); key.extend_from_slice(&[0xBB; 32]); let thumbprint = jwk_thumbprint_p256(&key);
837 assert!(!thumbprint.is_empty());
838
839 assert_eq!(thumbprint.len(), 43);
841 }
842
843 #[test]
844 fn test_jwk_thumbprint_deterministic() {
845 let mut key = vec![0x04];
846 key.extend_from_slice(&[0x11; 32]);
847 key.extend_from_slice(&[0x22; 32]);
848
849 let t1 = jwk_thumbprint_p256(&key);
850 let t2 = jwk_thumbprint_p256(&key);
851 assert_eq!(t1, t2);
852 }
853
854 #[test]
855 fn test_jwk_thumbprint_different_keys() {
856 let mut k1 = vec![0x04];
857 k1.extend_from_slice(&[0x01; 32]);
858 k1.extend_from_slice(&[0x02; 32]);
859
860 let mut k2 = vec![0x04];
861 k2.extend_from_slice(&[0x03; 32]);
862 k2.extend_from_slice(&[0x04; 32]);
863
864 assert_ne!(jwk_thumbprint_p256(&k1), jwk_thumbprint_p256(&k2));
865 }
866
867 #[test]
868 fn test_jwk_thumbprint_invalid_key() {
869 assert_eq!(jwk_thumbprint_p256(&[0x00; 10]), "");
870 assert_eq!(jwk_thumbprint_p256(&[]), "");
871 }
872
873 #[test]
876 fn test_build_p256_jwk() {
877 let mut key = vec![0x04];
878 key.extend_from_slice(&[0xCC; 32]);
879 key.extend_from_slice(&[0xDD; 32]);
880
881 let jwk = build_p256_jwk(&key);
882 assert_eq!(jwk["kty"], "EC");
883 assert_eq!(jwk["crv"], "P-256");
884 assert!(jwk["x"].as_str().is_some());
885 assert!(jwk["y"].as_str().is_some());
886 }
887
888 #[test]
889 fn test_build_p256_jwk_invalid() {
890 let jwk = build_p256_jwk(&[0x00; 10]);
891 assert!(jwk.as_object().unwrap().is_empty());
892 }
893
894 #[test]
897 fn test_create_jws_with_jwk() {
898 let rng = SystemRandom::new();
899 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
900 let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
901 .unwrap();
902
903 let jws = create_jws(&kp, "https://example.com/new-acct", "nonce1", "{}", None).unwrap();
904 assert!(jws.get("protected").is_some());
905 assert!(jws.get("payload").is_some());
906 assert!(jws.get("signature").is_some());
907 }
908
909 #[test]
910 fn test_create_jws_with_kid() {
911 let rng = SystemRandom::new();
912 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
913 let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
914 .unwrap();
915
916 let jws = create_jws(
917 &kp,
918 "https://example.com/order",
919 "nonce2",
920 "{}",
921 Some("https://example.com/acct/1"),
922 )
923 .unwrap();
924
925 let protected = jws["protected"].as_str().unwrap();
927 let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
928 .decode(protected)
929 .unwrap();
930 let header: serde_json::Value = serde_json::from_slice(&decoded).unwrap();
931 assert_eq!(header["kid"], "https://example.com/acct/1");
932 assert!(header.get("jwk").is_none());
933 }
934
935 #[test]
936 fn test_create_jws_post_as_get() {
937 let rng = SystemRandom::new();
938 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
939 let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
940 .unwrap();
941
942 let jws = create_jws(&kp, "https://example.com/authz", "nonce3", "", None).unwrap();
943 assert_eq!(jws["payload"].as_str().unwrap(), "");
945 }
946
947 #[test]
950 fn test_key_authorization_format() {
951 let rng = SystemRandom::new();
952 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
953 let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
954 .unwrap();
955
956 let dir = AcmeDirectory {
957 new_nonce: String::new(),
958 new_account: String::new(),
959 new_order: String::new(),
960 new_authz: None,
961 revoke_cert: None,
962 key_change: None,
963 meta: None,
964 };
965
966 let client = AcmeClient::from_parts(AcmeConfig::default(), kp, dir);
967 let key_authz = client.key_authorization("test-token");
968 assert!(key_authz.starts_with("test-token."));
969 assert!(key_authz.len() > 20); }
971
972 #[test]
973 fn test_dns01_record_value() {
974 let rng = SystemRandom::new();
975 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
976 let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
977 .unwrap();
978
979 let dir = AcmeDirectory {
980 new_nonce: String::new(),
981 new_account: String::new(),
982 new_order: String::new(),
983 new_authz: None,
984 revoke_cert: None,
985 key_change: None,
986 meta: None,
987 };
988
989 let client = AcmeClient::from_parts(AcmeConfig::default(), kp, dir);
990 let value = client.dns01_record_value("dns-token");
991 assert_eq!(value.len(), 43);
993 }
994
995 #[tokio::test]
998 async fn test_http01_challenge_store() {
999 let store = Http01ChallengeStore::new();
1000 assert_eq!(store.count().await, 0);
1001
1002 store.add("token1".to_string(), "authz1".to_string()).await;
1003 store.add("token2".to_string(), "authz2".to_string()).await;
1004 assert_eq!(store.count().await, 2);
1005
1006 assert_eq!(store.get("token1").await, Some("authz1".to_string()));
1007 assert_eq!(store.get("token2").await, Some("authz2".to_string()));
1008 assert_eq!(store.get("missing").await, None);
1009
1010 store.remove("token1").await;
1011 assert_eq!(store.count().await, 1);
1012 assert_eq!(store.get("token1").await, None);
1013 }
1014
1015 #[test]
1018 fn test_order_status_serialization() {
1019 assert_eq!(
1020 serde_json::to_string(&OrderStatus::Pending).unwrap(),
1021 r#""pending""#
1022 );
1023 assert_eq!(
1024 serde_json::to_string(&OrderStatus::Ready).unwrap(),
1025 r#""ready""#
1026 );
1027 }
1028
1029 #[test]
1030 fn test_challenge_type_serialization() {
1031 assert_eq!(
1032 serde_json::to_string(&ChallengeType::Http01).unwrap(),
1033 r#""http-01""#
1034 );
1035 assert_eq!(
1036 serde_json::to_string(&ChallengeType::Dns01).unwrap(),
1037 r#""dns-01""#
1038 );
1039 }
1040
1041 #[test]
1042 fn test_acme_identifier() {
1043 let id = AcmeIdentifier {
1044 id_type: "dns".to_string(),
1045 value: "example.com".to_string(),
1046 };
1047 let json = serde_json::to_value(&id).unwrap();
1048 assert_eq!(json["type"], "dns");
1049 assert_eq!(json["value"], "example.com");
1050 }
1051
1052 #[test]
1053 fn test_acme_directory_deserialization() {
1054 let json = r#"{
1055 "newNonce": "https://acme.example/nonce",
1056 "newAccount": "https://acme.example/account",
1057 "newOrder": "https://acme.example/order",
1058 "revokeCert": "https://acme.example/revoke",
1059 "meta": {
1060 "termsOfService": "https://acme.example/tos",
1061 "externalAccountRequired": false
1062 }
1063 }"#;
1064 let dir: AcmeDirectory = serde_json::from_str(json).unwrap();
1065 assert_eq!(dir.new_nonce, "https://acme.example/nonce");
1066 assert_eq!(dir.new_account, "https://acme.example/account");
1067 assert!(dir.meta.is_some());
1068 assert!(!dir.meta.unwrap().external_account_required);
1069 }
1070
1071 #[test]
1072 fn test_acme_authorization_deserialization() {
1073 let json = r#"{
1074 "identifier": {"type": "dns", "value": "example.com"},
1075 "status": "pending",
1076 "challenges": [
1077 {
1078 "type": "http-01",
1079 "url": "https://acme.example/chall/1",
1080 "status": "pending",
1081 "token": "abc123"
1082 }
1083 ]
1084 }"#;
1085 let authz: AcmeAuthorization = serde_json::from_str(json).unwrap();
1086 assert_eq!(authz.status, AuthorizationStatus::Pending);
1087 assert_eq!(authz.challenges.len(), 1);
1088 assert_eq!(authz.challenges[0].challenge_type, ChallengeType::Http01);
1089 assert_eq!(authz.challenges[0].token, "abc123");
1090 }
1091
1092 #[test]
1093 fn test_account_thumbprint() {
1094 let rng = SystemRandom::new();
1095 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
1096 let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
1097 .unwrap();
1098
1099 let dir = AcmeDirectory {
1100 new_nonce: String::new(),
1101 new_account: String::new(),
1102 new_order: String::new(),
1103 new_authz: None,
1104 revoke_cert: None,
1105 key_change: None,
1106 meta: None,
1107 };
1108
1109 let client = AcmeClient::from_parts(AcmeConfig::default(), kp, dir);
1110 let tp = client.account_thumbprint();
1111 assert_eq!(tp.len(), 43);
1112
1113 assert_eq!(tp, client.account_thumbprint());
1115 }
1116
1117 #[test]
1120 fn test_certificate_record_not_expired() {
1121 let now = std::time::SystemTime::now()
1122 .duration_since(std::time::UNIX_EPOCH)
1123 .unwrap()
1124 .as_secs();
1125 let record = CertificateRecord::new(
1126 vec!["example.com".to_string()],
1127 "PEM".to_string(),
1128 now,
1129 now + 90 * 24 * 3600, 30 * 24 * 3600, );
1132 assert!(!record.is_expired());
1133 assert!(!record.needs_renewal());
1134 assert!(record.remaining_secs() > 0);
1135 }
1136
1137 #[test]
1138 fn test_certificate_record_expired() {
1139 let record = CertificateRecord::new(
1140 vec!["old.com".to_string()],
1141 "PEM".to_string(),
1142 1000,
1143 2000, 300,
1145 );
1146 assert!(record.is_expired());
1147 assert!(record.needs_renewal());
1148 assert_eq!(record.remaining_secs(), 0);
1149 }
1150
1151 #[test]
1152 fn test_certificate_record_needs_renewal() {
1153 let now = std::time::SystemTime::now()
1154 .duration_since(std::time::UNIX_EPOCH)
1155 .unwrap()
1156 .as_secs();
1157 let record = CertificateRecord::new(
1159 vec!["renew.com".to_string()],
1160 "PEM".to_string(),
1161 now - 80 * 24 * 3600,
1162 now + 10 * 24 * 3600,
1163 30 * 24 * 3600,
1164 );
1165 assert!(!record.is_expired());
1166 assert!(record.needs_renewal());
1167 }
1168
1169 #[tokio::test]
1172 async fn test_certificate_tracker_track_and_get() {
1173 let tracker = CertificateTracker::new();
1174 let now = std::time::SystemTime::now()
1175 .duration_since(std::time::UNIX_EPOCH)
1176 .unwrap()
1177 .as_secs();
1178 let key = tracker
1179 .track(
1180 vec!["example.com".to_string()],
1181 "-----BEGIN CERT-----".to_string(),
1182 now,
1183 now + 90 * 24 * 3600,
1184 )
1185 .await;
1186 assert_eq!(key, "example.com");
1187 assert_eq!(tracker.count().await, 1);
1188
1189 let record = tracker.get("example.com").await.unwrap();
1190 assert_eq!(record.domains, vec!["example.com"]);
1191 }
1192
1193 #[tokio::test]
1194 async fn test_certificate_tracker_due_for_renewal() {
1195 let tracker = CertificateTracker::with_renew_window(30 * 24 * 3600);
1196 let now = std::time::SystemTime::now()
1197 .duration_since(std::time::UNIX_EPOCH)
1198 .unwrap()
1199 .as_secs();
1200
1201 tracker
1203 .track(
1204 vec!["renew-me.com".to_string()],
1205 "PEM".to_string(),
1206 now - 80 * 24 * 3600,
1207 now + 10 * 24 * 3600,
1208 )
1209 .await;
1210
1211 tracker
1213 .track(
1214 vec!["still-good.com".to_string()],
1215 "PEM".to_string(),
1216 now,
1217 now + 60 * 24 * 3600,
1218 )
1219 .await;
1220
1221 let due = tracker.due_for_renewal().await;
1222 assert_eq!(due.len(), 1);
1223 assert!(due.contains(&"renew-me.com".to_string()));
1224 }
1225
1226 #[tokio::test]
1227 async fn test_certificate_tracker_expired() {
1228 let tracker = CertificateTracker::new();
1229 tracker
1230 .track(
1231 vec!["expired.com".to_string()],
1232 "PEM".to_string(),
1233 1000,
1234 2000,
1235 )
1236 .await;
1237 let expired = tracker.expired().await;
1238 assert_eq!(expired, vec!["expired.com"]);
1239 }
1240
1241 #[tokio::test]
1242 async fn test_certificate_tracker_remove_expired() {
1243 let tracker = CertificateTracker::new();
1244 let now = std::time::SystemTime::now()
1245 .duration_since(std::time::UNIX_EPOCH)
1246 .unwrap()
1247 .as_secs();
1248
1249 tracker
1250 .track(vec!["old.com".to_string()], "PEM".to_string(), 100, 200)
1251 .await;
1252 tracker
1253 .track(
1254 vec!["fresh.com".to_string()],
1255 "PEM".to_string(),
1256 now,
1257 now + 9999,
1258 )
1259 .await;
1260
1261 assert_eq!(tracker.count().await, 2);
1262 let removed = tracker.remove_expired().await;
1263 assert_eq!(removed, 1);
1264 assert_eq!(tracker.count().await, 1);
1265 assert!(tracker.get("fresh.com").await.is_some());
1266 }
1267
1268 #[tokio::test]
1269 async fn test_certificate_tracker_remove() {
1270 let tracker = CertificateTracker::new();
1271 let now = std::time::SystemTime::now()
1272 .duration_since(std::time::UNIX_EPOCH)
1273 .unwrap()
1274 .as_secs();
1275 tracker
1276 .track(
1277 vec!["rm.com".to_string()],
1278 "PEM".to_string(),
1279 now,
1280 now + 9999,
1281 )
1282 .await;
1283 assert!(tracker.remove("rm.com").await);
1284 assert!(!tracker.remove("rm.com").await);
1285 assert_eq!(tracker.count().await, 0);
1286 }
1287}