1use std::collections::HashSet;
2
3use rusty_jwt_tools::prelude::{ClientId, Handle, JwsAlgorithm, Pem};
4
5use crate::acme::{
6 AcmeAccount, AcmeAuthz, 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 pub fn try_get_user_authorization(&self) -> RustyAcmeResult<AcmeAuthz> {
243 todo!()
244 }
245}
246
247#[cfg(test)]
248impl Default for AcmeOrder {
249 fn default() -> Self {
250 let now = time::OffsetDateTime::now_utc();
251 let tomorrow = now + time::Duration::days(1);
252 Self {
253 status: AcmeOrderStatus::Ready,
254 finalize: "https://acme-server/acme/order/n8LovurSfUFeeGSzD8nuGQwOUeIfSjhs/finalize"
255 .parse()
256 .unwrap(),
257 identifiers: [AcmeIdentifier::new_user(), AcmeIdentifier::new_device()],
258 authorizations: [
259 "https://acme-server/acme/wire/authz/0DpEeMVjTpOk615lIRvihqEyZLW8CsMH"
260 .parse()
261 .unwrap(),
262 "https://acme-server/acme/wire/authz/0hKeQhgRIpQKynZ8qGQo2Y0EXqEVSQ4j"
263 .parse()
264 .unwrap(),
265 ],
266 expires: Some(tomorrow),
267 not_before: Some(now),
268 not_after: Some(tomorrow),
269 }
270 }
271}
272
273#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
275#[serde(rename_all = "lowercase")]
276pub enum AcmeOrderStatus {
277 Pending,
278 Ready,
279 Processing,
280 Valid,
281 Invalid,
282}
283
284#[cfg(test)]
285mod tests {
286 use serde_json::json;
287 use wasm_bindgen_test::*;
288
289 use super::*;
290
291 wasm_bindgen_test_configure!(run_in_browser);
292
293 mod json {
294 use super::*;
295
296 #[test]
297 #[wasm_bindgen_test]
298 fn can_deserialize_sample_request() {
299 let rfc_sample = json!({
300 "identifiers": [
301 { "type": "wireapp-user", "value": "www.example.org" },
302 { "type": "wireapp-device", "value": "example.org" }
303 ],
304 "notBefore": "2016-01-01T00:04:00+04:00",
305 "notAfter": "2016-01-08T00:04:00+04:00"
306 });
307 assert!(serde_json::from_value::<AcmeOrderRequest>(rfc_sample).is_ok());
308 }
309
310 #[test]
311 #[wasm_bindgen_test]
312 fn can_deserialize_rfc_sample_response() {
313 let rfc_sample = json!({
314 "status": "pending",
315 "expires": "2016-01-05T14:09:07.99Z",
316 "notBefore": "2016-01-01T00:00:00Z",
317 "notAfter": "2016-01-08T00:00:00Z",
318 "identifiers": [
319 { "type": "wireapp-user", "value": "www.example.org" },
320 { "type": "wireapp-device", "value": "example.org" }
321 ],
322 "authorizations": [
323 "https://example.com/acme/authz/PAniVnsZcis",
324 "https://example.com/acme/authz/r4HqLzrSrpI"
325 ],
326 "finalize": "https://example.com/acme/order/TOlocE8rfgo/finalize"
327 });
328 assert!(serde_json::from_value::<AcmeOrderRequest>(rfc_sample).is_ok());
329 }
330 }
331
332 mod verify {
333 use super::*;
334
335 #[test]
336 #[wasm_bindgen_test]
337 fn should_succeed_when_valid() {
338 let now = time::OffsetDateTime::now_utc();
339 let tomorrow = now + time::Duration::days(1);
340 let order = AcmeOrder {
341 expires: Some(tomorrow),
342 not_before: Some(now),
343 not_after: Some(tomorrow),
344 ..Default::default()
345 };
346 assert!(order.verify().is_ok());
347 }
348
349 #[test]
350 #[wasm_bindgen_test]
351 fn should_fail_when_not_before_in_future() {
352 let tomorrow = time::OffsetDateTime::now_utc() + time::Duration::days(1);
353 let order = AcmeOrder {
354 not_before: Some(tomorrow),
355 ..Default::default()
356 };
357 assert!(matches!(
358 order.verify().unwrap_err(),
359 RustyAcmeError::OrderError(AcmeOrderError::NotYetValid)
360 ));
361 }
362
363 #[test]
364 #[wasm_bindgen_test]
365 fn should_fail_when_not_after_in_past() {
366 let yesterday = time::OffsetDateTime::now_utc() - time::Duration::days(1);
367 let order = AcmeOrder {
368 not_after: Some(yesterday),
369 ..Default::default()
370 };
371 assert!(matches!(
372 order.verify().unwrap_err(),
373 RustyAcmeError::OrderError(AcmeOrderError::Expired)
374 ));
375 }
376
377 #[test]
378 #[wasm_bindgen_test]
379 fn should_fail_when_expires_in_past() {
380 let yesterday = time::OffsetDateTime::now_utc() - time::Duration::days(1);
381 let order = AcmeOrder {
382 expires: Some(yesterday),
383 ..Default::default()
384 };
385 assert!(matches!(
386 order.verify().unwrap_err(),
387 RustyAcmeError::OrderError(AcmeOrderError::Expired)
388 ));
389 }
390
391 #[test]
392 #[wasm_bindgen_test]
393 fn should_fail_when_wrong_number_identifiers() {
394 let now = time::OffsetDateTime::now_utc();
395 let tomorrow = now + time::Duration::days(1);
396 let default_order = AcmeOrder {
397 expires: Some(tomorrow),
398 not_before: Some(now),
399 not_after: Some(tomorrow),
400 ..Default::default()
401 };
402
403 let order = AcmeOrder {
405 identifiers: [AcmeIdentifier::new_user(), AcmeIdentifier::new_user()],
406 ..default_order.clone()
407 };
408 assert!(matches!(
409 order.verify().unwrap_err(),
410 RustyAcmeError::OrderError(AcmeOrderError::WrongIdentifiers)
411 ));
412
413 let order = AcmeOrder {
415 identifiers: [AcmeIdentifier::new_device(), AcmeIdentifier::new_device()],
416 ..default_order.clone()
417 };
418 assert!(matches!(
419 order.verify().unwrap_err(),
420 RustyAcmeError::OrderError(AcmeOrderError::WrongIdentifiers)
421 ));
422 }
423 }
424
425 mod creation {
426 use super::*;
427
428 #[test]
429 #[wasm_bindgen_test]
430 fn should_succeed_when_pending() {
431 let order = AcmeOrder {
432 status: AcmeOrderStatus::Pending,
433 ..Default::default()
434 };
435 let order = serde_json::to_value(order).unwrap();
436 assert!(RustyAcme::new_order_response(order).is_ok());
437 }
438
439 #[test]
440 #[wasm_bindgen_test]
441 fn should_fail_when_not_pending() {
442 let order = AcmeOrder {
443 status: AcmeOrderStatus::Ready,
444 ..Default::default()
445 };
446 let order = serde_json::to_value(order).unwrap();
447 assert!(matches!(
448 RustyAcme::new_order_response(order).unwrap_err(),
449 RustyAcmeError::ClientImplementationError(_)
450 ));
451
452 let order = AcmeOrder {
453 status: AcmeOrderStatus::Processing,
454 ..Default::default()
455 };
456 let order = serde_json::to_value(order).unwrap();
457 assert!(matches!(
458 RustyAcme::new_order_response(order).unwrap_err(),
459 RustyAcmeError::ClientImplementationError(_)
460 ));
461
462 let order = AcmeOrder {
463 status: AcmeOrderStatus::Valid,
464 ..Default::default()
465 };
466 let order = serde_json::to_value(order).unwrap();
467 assert!(matches!(
468 RustyAcme::new_order_response(order).unwrap_err(),
469 RustyAcmeError::ClientImplementationError(_)
470 ));
471 }
472
473 #[test]
474 #[wasm_bindgen_test]
475 fn should_fail_when_invalid() {
476 let order = AcmeOrder {
477 status: AcmeOrderStatus::Invalid,
478 ..Default::default()
479 };
480 let order = serde_json::to_value(order).unwrap();
481 assert!(matches!(
482 RustyAcme::new_order_response(order).unwrap_err(),
483 RustyAcmeError::OrderError(AcmeOrderError::Invalid)
484 ));
485 }
486 }
487
488 mod check {
489 use super::*;
490
491 #[test]
492 #[wasm_bindgen_test]
493 fn should_succeed_when_ready() {
494 let order = AcmeOrder {
495 status: AcmeOrderStatus::Ready,
496 ..Default::default()
497 };
498 let order = serde_json::to_value(order).unwrap();
499 assert!(RustyAcme::check_order_response(order).is_ok());
500 }
501
502 #[test]
503 #[wasm_bindgen_test]
504 fn should_fail_when_not_pending() {
505 for status in [
506 AcmeOrderStatus::Pending,
507 AcmeOrderStatus::Processing,
508 AcmeOrderStatus::Valid,
509 ] {
510 let order = AcmeOrder {
511 status,
512 ..Default::default()
513 };
514 let order = serde_json::to_value(&order).unwrap();
515 assert!(matches!(
516 RustyAcme::check_order_response(order).unwrap_err(),
517 RustyAcmeError::ClientImplementationError(_)
518 ));
519 }
520 }
521
522 #[test]
523 #[wasm_bindgen_test]
524 fn should_fail_when_invalid() {
525 let order = AcmeOrder {
526 status: AcmeOrderStatus::Invalid,
527 ..Default::default()
528 };
529 let order = serde_json::to_value(order).unwrap();
530 assert!(matches!(
531 RustyAcme::check_order_response(order).unwrap_err(),
532 RustyAcmeError::OrderError(AcmeOrderError::Invalid)
533 ));
534 }
535 }
536}