1use std::collections::HashSet;
2
3use rusty_jwt_tools::prelude::*;
4
5use crate::acme::{identifier::CanonicalIdentifier, prelude::*};
6
7impl RustyAcme {
9 #[allow(clippy::too_many_arguments)]
12 pub fn new_order_request(
13 display_name: &str,
14 client_id: ClientId,
15 handle: &Handle,
16 expiry: core::time::Duration,
17 directory: &AcmeDirectory,
18 account: &AcmeAccount,
19 alg: JwsAlgorithm,
20 kp: &Pem,
21 previous_nonce: String,
22 ) -> RustyAcmeResult<AcmeJws> {
23 let acct_url = account.acct_url()?;
25
26 let domain = client_id.domain.clone();
27 let handle = handle.try_to_qualified(&domain)?;
28 let device_identifier =
29 AcmeIdentifier::try_new_device(client_id, handle.clone(), display_name.to_string(), domain.clone())?;
30 let user_identifier = AcmeIdentifier::try_new_user(handle, display_name.to_string(), domain)?;
31
32 let not_before = time::OffsetDateTime::now_utc();
33 let not_after = not_before + expiry;
34 let payload = AcmeOrderRequest {
35 identifiers: vec![device_identifier, user_identifier],
36 not_before: Some(not_before),
37 not_after: Some(not_after),
38 };
39 let req = AcmeJws::new(
40 alg,
41 previous_nonce,
42 &directory.new_order,
43 Some(&acct_url),
44 Some(payload),
45 kp,
46 )?;
47 Ok(req)
48 }
49
50 pub fn new_order_response(response: serde_json::Value) -> RustyAcmeResult<AcmeOrder> {
53 let order = serde_json::from_value::<AcmeOrder>(response)?;
54 match order.status {
55 AcmeOrderStatus::Pending => {}
56 AcmeOrderStatus::Processing | AcmeOrderStatus::Valid | AcmeOrderStatus::Ready => {
57 return Err(RustyAcmeError::ClientImplementationError(
58 "an order is not supposed to be 'processing | valid | ready' at this point. \
59 You should only be using this method after account creation, not after finalize",
60 ));
61 }
62 AcmeOrderStatus::Invalid => return Err(AcmeOrderError::Invalid)?,
63 }
64 order.verify()?;
65 Ok(order)
66 }
67}
68
69impl RustyAcme {
71 pub fn check_order_request(
74 order_url: url::Url,
75 account: &AcmeAccount,
76 alg: JwsAlgorithm,
77 kp: &Pem,
78 previous_nonce: String,
79 ) -> RustyAcmeResult<AcmeJws> {
80 let acct_url = account.acct_url()?;
82
83 let payload = None::<serde_json::Value>;
85 let req = AcmeJws::new(alg, previous_nonce, &order_url, Some(&acct_url), payload, kp)?;
86 Ok(req)
87 }
88
89 pub fn check_order_response(response: serde_json::Value) -> RustyAcmeResult<AcmeOrder> {
92 let order = serde_json::from_value::<AcmeOrder>(response)?;
93 match order.status {
94 AcmeOrderStatus::Ready => {}
95 AcmeOrderStatus::Pending => {
96 return Err(RustyAcmeError::ClientImplementationError(
97 "an order is not supposed to be 'pending' at this point. \
98 It means you have forgotten to create authorizations",
99 ));
100 }
101 AcmeOrderStatus::Processing => {
102 return Err(RustyAcmeError::ClientImplementationError(
103 "an order is not supposed to be 'processing' at this point. \
104 You should not have called finalize yet ; in fact, you should only call finalize \
105 once this order turns 'ready'",
106 ));
107 }
108 AcmeOrderStatus::Valid => {
109 return Err(RustyAcmeError::ClientImplementationError(
110 "an order is not supposed to be 'valid' at this point. \
111 It means a certificate has already been delivered which defeats the purpose \
112 of using this method",
113 ));
114 }
115 AcmeOrderStatus::Invalid => return Err(AcmeOrderError::Invalid)?,
116 }
117 order.verify()?;
118 Ok(order)
119 }
120}
121
122#[derive(Debug, thiserror::Error)]
123pub enum AcmeOrderError {
124 #[error("Created order is not valid")]
126 Invalid,
127 #[error("This order 'not_before' is in future")]
129 NotYetValid,
130 #[error("This order is expired")]
132 Expired,
133 #[error("This order should only have the 2 Wire identifiers")]
135 WrongIdentifiers,
136}
137
138#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
141#[cfg_attr(test, derive(Clone))]
142#[serde(rename_all = "camelCase")]
143struct AcmeOrderRequest {
144 pub identifiers: Vec<AcmeIdentifier>,
146 #[serde(skip_serializing_if = "Option::is_none", with = "time::serde::rfc3339::option")]
149 pub not_before: Option<time::OffsetDateTime>,
150 #[serde(skip_serializing_if = "Option::is_none", with = "time::serde::rfc3339::option")]
153 pub not_after: Option<time::OffsetDateTime>,
154}
155
156#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct AcmeOrder {
161 pub status: AcmeOrderStatus,
162 pub finalize: url::Url,
163 pub identifiers: [AcmeIdentifier; 2],
164 pub authorizations: [url::Url; 2],
165 #[serde(skip_serializing_if = "Option::is_none", with = "time::serde::rfc3339::option")]
166 pub expires: Option<time::OffsetDateTime>,
167 #[serde(skip_serializing_if = "Option::is_none", with = "time::serde::rfc3339::option")]
168 pub not_before: Option<time::OffsetDateTime>,
169 #[serde(skip_serializing_if = "Option::is_none", with = "time::serde::rfc3339::option")]
170 pub not_after: Option<time::OffsetDateTime>,
171}
172
173impl AcmeOrder {
174 pub fn verify(&self) -> RustyAcmeResult<()> {
175 let [ref a, ref b] = self
176 .identifiers
177 .iter()
178 .collect::<HashSet<_>>() .iter()
180 .map(|i| i.to_wire_identifier())
181 .collect::<RustyAcmeResult<Vec<_>>>()?[..]
182 else {
183 return Err(AcmeOrderError::WrongIdentifiers)?;
184 };
185
186 let same_handle = a.handle == b.handle;
187 let same_display_name = a.display_name == b.display_name;
188 let same_domain = a.domain == b.domain;
189 if !(same_handle && same_display_name && same_domain) {
190 return Err(AcmeOrderError::WrongIdentifiers)?;
191 }
192
193 let now = time::OffsetDateTime::now_utc().unix_timestamp();
194
195 let is_expired = self
196 .expires
197 .map(time::OffsetDateTime::unix_timestamp)
198 .map(|expires| expires < now)
199 .unwrap_or_default();
200 if is_expired {
201 return Err(AcmeOrderError::Expired)?;
202 }
203
204 let is_after = self
205 .not_after
206 .map(time::OffsetDateTime::unix_timestamp)
207 .map(|not_after| not_after < now)
208 .unwrap_or_default();
209 if is_after {
210 return Err(AcmeOrderError::Expired)?;
211 }
212
213 let is_before = self
214 .not_before
215 .map(time::OffsetDateTime::unix_timestamp)
216 .map(|not_before| now < not_before)
217 .unwrap_or_default();
218 if is_before {
219 return Err(AcmeOrderError::NotYetValid)?;
220 }
221
222 Ok(())
223 }
224
225 pub fn try_get_coalesce_identifier(&self) -> RustyAcmeResult<CanonicalIdentifier> {
228 self.identifiers
229 .iter()
230 .find_map(|i| match i {
231 AcmeIdentifier::WireappDevice(_) => Some(i.to_wire_identifier()),
232 _ => None,
233 })
234 .transpose()?
235 .ok_or(RustyAcmeError::OrderError(AcmeOrderError::WrongIdentifiers))?
236 .try_into()
237 }
238
239 pub fn try_get_user_authorization(&self) -> RustyAcmeResult<AcmeAuthz> {
240 todo!()
241 }
242}
243
244#[cfg(test)]
245impl Default for AcmeOrder {
246 fn default() -> Self {
247 let now = time::OffsetDateTime::now_utc();
248 let tomorrow = now + time::Duration::days(1);
249 Self {
250 status: AcmeOrderStatus::Ready,
251 finalize: "https://acme-server/acme/order/n8LovurSfUFeeGSzD8nuGQwOUeIfSjhs/finalize"
252 .parse()
253 .unwrap(),
254 identifiers: [AcmeIdentifier::new_user(), AcmeIdentifier::new_device()],
255 authorizations: [
256 "https://acme-server/acme/wire/authz/0DpEeMVjTpOk615lIRvihqEyZLW8CsMH"
257 .parse()
258 .unwrap(),
259 "https://acme-server/acme/wire/authz/0hKeQhgRIpQKynZ8qGQo2Y0EXqEVSQ4j"
260 .parse()
261 .unwrap(),
262 ],
263 expires: Some(tomorrow),
264 not_before: Some(now),
265 not_after: Some(tomorrow),
266 }
267 }
268}
269
270#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
272#[serde(rename_all = "lowercase")]
273pub enum AcmeOrderStatus {
274 Pending,
275 Ready,
276 Processing,
277 Valid,
278 Invalid,
279}
280
281#[cfg(test)]
282pub mod tests {
283 use serde_json::json;
284 use wasm_bindgen_test::*;
285
286 use super::*;
287
288 wasm_bindgen_test_configure!(run_in_browser);
289
290 mod json {
291 use super::*;
292
293 #[test]
294 #[wasm_bindgen_test]
295 fn can_deserialize_sample_request() {
296 let rfc_sample = json!({
297 "identifiers": [
298 { "type": "wireapp-user", "value": "www.example.org" },
299 { "type": "wireapp-device", "value": "example.org" }
300 ],
301 "notBefore": "2016-01-01T00:04:00+04:00",
302 "notAfter": "2016-01-08T00:04:00+04:00"
303 });
304 assert!(serde_json::from_value::<AcmeOrderRequest>(rfc_sample).is_ok());
305 }
306
307 #[test]
308 #[wasm_bindgen_test]
309 fn can_deserialize_rfc_sample_response() {
310 let rfc_sample = json!({
311 "status": "pending",
312 "expires": "2016-01-05T14:09:07.99Z",
313 "notBefore": "2016-01-01T00:00:00Z",
314 "notAfter": "2016-01-08T00:00:00Z",
315 "identifiers": [
316 { "type": "wireapp-user", "value": "www.example.org" },
317 { "type": "wireapp-device", "value": "example.org" }
318 ],
319 "authorizations": [
320 "https://example.com/acme/authz/PAniVnsZcis",
321 "https://example.com/acme/authz/r4HqLzrSrpI"
322 ],
323 "finalize": "https://example.com/acme/order/TOlocE8rfgo/finalize"
324 });
325 assert!(serde_json::from_value::<AcmeOrderRequest>(rfc_sample).is_ok());
326 }
327 }
328
329 mod verify {
330 use super::*;
331
332 #[test]
333 #[wasm_bindgen_test]
334 fn should_succeed_when_valid() {
335 let now = time::OffsetDateTime::now_utc();
336 let tomorrow = now + time::Duration::days(1);
337 let order = AcmeOrder {
338 expires: Some(tomorrow),
339 not_before: Some(now),
340 not_after: Some(tomorrow),
341 ..Default::default()
342 };
343 assert!(order.verify().is_ok());
344 }
345
346 #[test]
347 #[wasm_bindgen_test]
348 fn should_fail_when_not_before_in_future() {
349 let tomorrow = time::OffsetDateTime::now_utc() + time::Duration::days(1);
350 let order = AcmeOrder {
351 not_before: Some(tomorrow),
352 ..Default::default()
353 };
354 assert!(matches!(
355 order.verify().unwrap_err(),
356 RustyAcmeError::OrderError(AcmeOrderError::NotYetValid)
357 ));
358 }
359
360 #[test]
361 #[wasm_bindgen_test]
362 fn should_fail_when_not_after_in_past() {
363 let yesterday = time::OffsetDateTime::now_utc() - time::Duration::days(1);
364 let order = AcmeOrder {
365 not_after: Some(yesterday),
366 ..Default::default()
367 };
368 assert!(matches!(
369 order.verify().unwrap_err(),
370 RustyAcmeError::OrderError(AcmeOrderError::Expired)
371 ));
372 }
373
374 #[test]
375 #[wasm_bindgen_test]
376 fn should_fail_when_expires_in_past() {
377 let yesterday = time::OffsetDateTime::now_utc() - time::Duration::days(1);
378 let order = AcmeOrder {
379 expires: Some(yesterday),
380 ..Default::default()
381 };
382 assert!(matches!(
383 order.verify().unwrap_err(),
384 RustyAcmeError::OrderError(AcmeOrderError::Expired)
385 ));
386 }
387
388 #[test]
389 #[wasm_bindgen_test]
390 fn should_fail_when_wrong_number_identifiers() {
391 let now = time::OffsetDateTime::now_utc();
392 let tomorrow = now + time::Duration::days(1);
393 let default_order = AcmeOrder {
394 expires: Some(tomorrow),
395 not_before: Some(now),
396 not_after: Some(tomorrow),
397 ..Default::default()
398 };
399
400 let order = AcmeOrder {
402 identifiers: [AcmeIdentifier::new_user(), AcmeIdentifier::new_user()],
403 ..default_order.clone()
404 };
405 assert!(matches!(
406 order.verify().unwrap_err(),
407 RustyAcmeError::OrderError(AcmeOrderError::WrongIdentifiers)
408 ));
409
410 let order = AcmeOrder {
412 identifiers: [AcmeIdentifier::new_device(), AcmeIdentifier::new_device()],
413 ..default_order.clone()
414 };
415 assert!(matches!(
416 order.verify().unwrap_err(),
417 RustyAcmeError::OrderError(AcmeOrderError::WrongIdentifiers)
418 ));
419 }
420 }
421
422 mod creation {
423 use super::*;
424
425 #[test]
426 #[wasm_bindgen_test]
427 fn should_succeed_when_pending() {
428 let order = AcmeOrder {
429 status: AcmeOrderStatus::Pending,
430 ..Default::default()
431 };
432 let order = serde_json::to_value(order).unwrap();
433 assert!(RustyAcme::new_order_response(order).is_ok());
434 }
435
436 #[test]
437 #[wasm_bindgen_test]
438 fn should_fail_when_not_pending() {
439 let order = AcmeOrder {
440 status: AcmeOrderStatus::Ready,
441 ..Default::default()
442 };
443 let order = serde_json::to_value(order).unwrap();
444 assert!(matches!(
445 RustyAcme::new_order_response(order).unwrap_err(),
446 RustyAcmeError::ClientImplementationError(_)
447 ));
448
449 let order = AcmeOrder {
450 status: AcmeOrderStatus::Processing,
451 ..Default::default()
452 };
453 let order = serde_json::to_value(order).unwrap();
454 assert!(matches!(
455 RustyAcme::new_order_response(order).unwrap_err(),
456 RustyAcmeError::ClientImplementationError(_)
457 ));
458
459 let order = AcmeOrder {
460 status: AcmeOrderStatus::Valid,
461 ..Default::default()
462 };
463 let order = serde_json::to_value(order).unwrap();
464 assert!(matches!(
465 RustyAcme::new_order_response(order).unwrap_err(),
466 RustyAcmeError::ClientImplementationError(_)
467 ));
468 }
469
470 #[test]
471 #[wasm_bindgen_test]
472 fn should_fail_when_invalid() {
473 let order = AcmeOrder {
474 status: AcmeOrderStatus::Invalid,
475 ..Default::default()
476 };
477 let order = serde_json::to_value(order).unwrap();
478 assert!(matches!(
479 RustyAcme::new_order_response(order).unwrap_err(),
480 RustyAcmeError::OrderError(AcmeOrderError::Invalid)
481 ));
482 }
483 }
484
485 mod check {
486 use super::*;
487
488 #[test]
489 #[wasm_bindgen_test]
490 fn should_succeed_when_ready() {
491 let order = AcmeOrder {
492 status: AcmeOrderStatus::Ready,
493 ..Default::default()
494 };
495 let order = serde_json::to_value(order).unwrap();
496 assert!(RustyAcme::check_order_response(order).is_ok());
497 }
498
499 #[test]
500 #[wasm_bindgen_test]
501 fn should_fail_when_not_pending() {
502 for status in [
503 AcmeOrderStatus::Pending,
504 AcmeOrderStatus::Processing,
505 AcmeOrderStatus::Valid,
506 ] {
507 let order = AcmeOrder {
508 status,
509 ..Default::default()
510 };
511 let order = serde_json::to_value(&order).unwrap();
512 assert!(matches!(
513 RustyAcme::check_order_response(order).unwrap_err(),
514 RustyAcmeError::ClientImplementationError(_)
515 ));
516 }
517 }
518
519 #[test]
520 #[wasm_bindgen_test]
521 fn should_fail_when_invalid() {
522 let order = AcmeOrder {
523 status: AcmeOrderStatus::Invalid,
524 ..Default::default()
525 };
526 let order = serde_json::to_value(order).unwrap();
527 assert!(matches!(
528 RustyAcme::check_order_response(order).unwrap_err(),
529 RustyAcmeError::OrderError(AcmeOrderError::Invalid)
530 ));
531 }
532 }
533}