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