1use super::challenge::TlsAlpnSolver;
9use super::client::{AcmeClient, PendingChallenge};
10use super::{leaf_dns_sans, load_certified_key, now_unix, parse_cert_meta, AcmeError, StoredCert};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum KeyType {
14 EcdsaP256,
15 EcdsaP384,
16}
17
18impl KeyType {
19 pub fn parse(s: &str) -> Result<Self, AcmeError> {
20 match s {
21 "ecdsa-p256" => Ok(KeyType::EcdsaP256),
22 "ecdsa-p384" => Ok(KeyType::EcdsaP384),
23 other => Err(AcmeError::Config(format!(
24 "key_type '{other}' is not supported (ecdsa-p256 | ecdsa-p384)"
25 ))),
26 }
27 }
28
29 fn alg(self) -> &'static rcgen::SignatureAlgorithm {
30 match self {
31 KeyType::EcdsaP256 => &rcgen::PKCS_ECDSA_P256_SHA256,
32 KeyType::EcdsaP384 => &rcgen::PKCS_ECDSA_P384_SHA384,
33 }
34 }
35}
36
37pub fn generate_key(kt: KeyType) -> Result<rcgen::KeyPair, AcmeError> {
39 rcgen::KeyPair::generate_for(kt.alg()).map_err(|e| AcmeError::Crypto(e.to_string()))
40}
41
42pub fn build_csr(domains: &[String], key: &rcgen::KeyPair) -> Result<Vec<u8>, AcmeError> {
44 let mut params = rcgen::CertificateParams::new(domains.to_vec())
45 .map_err(|e| AcmeError::Crypto(e.to_string()))?;
46 if let Some(first) = domains.first() {
47 params
48 .distinguished_name
49 .push(rcgen::DnType::CommonName, first.as_str());
50 }
51 let csr = params
52 .serialize_request(key)
53 .map_err(|e| AcmeError::Crypto(e.to_string()))?;
54 Ok(csr.der().as_ref().to_vec())
55}
56
57const NOT_BEFORE_SKEW_SECS: i64 = 300;
59
60pub fn verify_chain(
63 chain_pem: &str,
64 key: &rcgen::KeyPair,
65 domains: &[String],
66 now: i64,
67) -> Result<(), AcmeError> {
68 let (_, leaf) = load_certified_key(chain_pem, &key.serialize_pem())?;
73 let meta = parse_cert_meta(&leaf)?;
74 if meta.not_after <= now {
75 return Err(AcmeError::Certificate(format!(
76 "issued certificate is already expired (not_after={})",
77 meta.not_after
78 )));
79 }
80 if meta.not_before > now + NOT_BEFORE_SKEW_SECS {
81 return Err(AcmeError::Certificate(format!(
82 "issued certificate is not yet valid (not_before={})",
83 meta.not_before
84 )));
85 }
86 let sans = leaf_dns_sans(&leaf)?;
87 for d in domains {
88 if !sans.iter().any(|s| s == &d.to_ascii_lowercase()) {
89 return Err(AcmeError::Certificate(format!(
90 "issued certificate lacks SAN {d} (has {sans:?})"
91 )));
92 }
93 }
94 Ok(())
95}
96
97pub async fn issue(
100 client: &dyn AcmeClient,
101 solver: &TlsAlpnSolver,
102 domains: &[String],
103 key_type: KeyType,
104) -> Result<StoredCert, AcmeError> {
105 let mut registered: Vec<String> = Vec::new();
106 let result = run(client, solver, domains, key_type, &mut registered).await;
107 for domain in ®istered {
108 if let Err(e) = solver.clear(domain).await {
109 tracing::warn!("acme: failed to clear challenge for {domain}: {e}");
110 }
111 }
112 result
113}
114
115async fn run(
116 client: &dyn AcmeClient,
117 solver: &TlsAlpnSolver,
118 domains: &[String],
119 key_type: KeyType,
120 registered: &mut Vec<String>,
121) -> Result<StoredCert, AcmeError> {
122 let key = generate_key(key_type)?;
123 let mut order = client.new_order(domains).await?;
124 let pending: Vec<PendingChallenge> = order.pending_challenges().await?;
125 let requested: std::collections::HashSet<String> =
131 domains.iter().map(|d| d.to_ascii_lowercase()).collect();
132 for p in &pending {
133 if !requested.contains(&p.domain.to_ascii_lowercase()) {
134 return Err(AcmeError::Protocol(format!(
135 "CA returned an authorization for an unrequested identifier '{}'",
136 p.domain
137 )));
138 }
139 }
140 for p in &pending {
141 solver.register(&p.domain, &p.key_auth).await?;
142 registered.push(p.domain.clone());
143 }
144 for p in &pending {
145 order.mark_ready(&p.domain).await?;
146 }
147 order.wait_ready().await?;
148 let csr = build_csr(domains, &key)?;
149 let chain_pem = order.finalize(&csr).await?;
150 let now = now_unix();
151 verify_chain(&chain_pem, &key, domains, now)?;
152 Ok(StoredCert {
153 chain_pem,
154 key_pem: key.serialize_pem(),
155 issued_at: now,
156 })
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::acme::client::mock::{MockAcmeClient, MockBehavior, MockStep};
163 use crate::acme::storage::fs::FsCertStorage;
164 use std::sync::Arc;
165
166 fn solver(tag: &str) -> Arc<TlsAlpnSolver> {
167 let d = std::env::temp_dir().join(format!("fb_acme_order_{}_{}", tag, std::process::id()));
168 let _ = std::fs::remove_dir_all(&d);
169 TlsAlpnSolver::new(Arc::new(FsCertStorage::new(d)))
170 }
171
172 fn doms() -> Vec<String> {
173 vec!["a.example.com".to_string(), "b.example.com".to_string()]
174 }
175
176 #[test]
177 fn key_type_parse_and_generate() {
178 assert_eq!(KeyType::parse("ecdsa-p256").unwrap(), KeyType::EcdsaP256);
179 assert_eq!(KeyType::parse("ecdsa-p384").unwrap(), KeyType::EcdsaP384);
180 assert!(KeyType::parse("rsa-2048").is_err());
181 let k = generate_key(KeyType::EcdsaP384).unwrap();
182 assert!(k.is_compatible(&rcgen::PKCS_ECDSA_P384_SHA384));
183 let csr = build_csr(&doms(), &k).unwrap();
184 assert!(!csr.is_empty());
185 }
186
187 #[tokio::test]
188 async fn issue_happy_path_returns_verified_cert_and_clears_challenges() {
189 let client = MockAcmeClient::new(MockBehavior::default());
190 let solver = solver("happy");
191 let stored = issue(&client, &solver, &doms(), KeyType::EcdsaP256)
192 .await
193 .unwrap();
194 let (_, leaf) =
195 crate::acme::load_certified_key(&stored.chain_pem, &stored.key_pem).unwrap();
196 let mut sans = crate::acme::leaf_dns_sans(&leaf).unwrap();
197 sans.sort();
198 assert_eq!(sans, doms());
199 assert!(stored.issued_at > 0);
200 assert!(
201 solver.cached_domains().is_empty(),
202 "challenges cleared after success"
203 );
204 assert_eq!(client.orders(), 1);
205 }
206
207 #[tokio::test]
208 async fn issue_rejects_chain_for_a_foreign_key_and_clears_challenges() {
209 let client = MockAcmeClient::new(MockBehavior {
210 wrong_key_chain: true,
211 ..Default::default()
212 });
213 let solver = solver("wrongkey");
214 let err = issue(&client, &solver, &doms(), KeyType::EcdsaP256)
215 .await
216 .unwrap_err();
217 assert!(matches!(err, AcmeError::Certificate(_)), "{err}");
218 assert!(solver.cached_domains().is_empty());
219 }
220
221 #[tokio::test]
222 async fn issue_surfaces_ca_failures_and_clears_challenges() {
223 for step in [MockStep::NewOrder, MockStep::WaitReady, MockStep::Finalize] {
224 let client = MockAcmeClient::new(MockBehavior {
225 fail_step: Some(step),
226 ..Default::default()
227 });
228 let solver = solver(&format!("fail{step:?}"));
229 let err = issue(&client, &solver, &doms(), KeyType::EcdsaP256)
230 .await
231 .unwrap_err();
232 assert!(matches!(err, AcmeError::Protocol(_)), "{step:?}: {err}");
233 assert!(
234 solver.cached_domains().is_empty(),
235 "{step:?}: challenges must be cleared"
236 );
237 }
238 }
239
240 #[tokio::test]
241 async fn issue_rejects_an_authorization_for_an_unrequested_identifier() {
242 let client = MockAcmeClient::new(MockBehavior {
243 extra_pending_domain: Some("../../evil".into()),
244 ..Default::default()
245 });
246 let solver = solver("bogusident");
247 let err = issue(&client, &solver, &doms(), KeyType::EcdsaP256)
248 .await
249 .unwrap_err();
250 assert!(
251 matches!(&err, AcmeError::Protocol(m) if m.contains("unrequested identifier")),
252 "{err}"
253 );
254 assert!(
255 solver.cached_domains().is_empty(),
256 "nothing is registered — not even the legitimate domains — before the whole set is validated"
257 );
258 }
259
260 #[test]
261 fn verify_chain_checks_sans_validity_and_key() {
262 let key = generate_key(KeyType::EcdsaP256).unwrap();
263 let mut params = rcgen::CertificateParams::new(vec!["a.example.com".to_string()]).unwrap();
264 let now = time::OffsetDateTime::now_utc();
265 params.not_before = now - time::Duration::hours(1);
266 params.not_after = now + time::Duration::days(30);
267 let cert = params.self_signed(&key).unwrap();
268 let pem = cert.pem();
269 let t = crate::acme::now_unix();
270 verify_chain(&pem, &key, &["a.example.com".to_string()], t).unwrap();
271 assert!(verify_chain(&pem, &key, &doms(), t).is_err());
273 assert!(verify_chain(&pem, &key, &["a.example.com".to_string()], t + 31 * 86_400).is_err());
275 let other = generate_key(KeyType::EcdsaP256).unwrap();
278 assert!(verify_chain(&pem, &other, &["a.example.com".to_string()], t).is_err());
279 }
280}