wire_e2e_identity/acme/
order.rs

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
10// Order creation
11impl RustyAcme {
12    /// create a new order
13    /// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
14    #[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        // Extract the account URL from previous response which created a new account
27        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    /// parse response from order creation
54    /// [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4)
55    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
72// Long poll order until ready
73impl RustyAcme {
74    /// check an order status until it becomes ready
75    /// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4)
76    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        // Extract the account URL from previous response which created a new account
84        let acct_url = account.acct_url()?;
85
86        // No payload required for authz
87        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    /// parse response from order check
93    /// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4)
94    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    /// step-ca flagged this order as invalid
128    #[error("Created order is not valid")]
129    Invalid,
130    /// This order 'not_before' is in future
131    #[error("This order 'not_before' is in future")]
132    NotYetValid,
133    /// This order is expired
134    #[error("This order is expired")]
135    Expired,
136    /// This order should only have the 2 Wire identifiers
137    #[error("This order should only have the 2 Wire identifiers")]
138    WrongIdentifiers,
139}
140
141/// For creating an order
142/// see https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4
143#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
144#[cfg_attr(test, derive(Clone))]
145#[serde(rename_all = "camelCase")]
146struct AcmeOrderRequest {
147    /// An array of identifier objects that the client wishes to submit an order for
148    pub identifiers: Vec<AcmeIdentifier>,
149    /// The requested value of the notBefore field in the certificate, in the date format defined
150    /// in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339)
151    #[serde(skip_serializing_if = "Option::is_none", with = "time::serde::rfc3339::option")]
152    pub not_before: Option<time::OffsetDateTime>,
153    /// The requested value of the notAfter field in the certificate, in the date format defined in
154    /// [RFC3339](https://www.rfc-editor.org/rfc/rfc3339)
155    #[serde(skip_serializing_if = "Option::is_none", with = "time::serde::rfc3339::option")]
156    pub not_after: Option<time::OffsetDateTime>,
157}
158
159/// Result of an order creation
160/// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4)
161#[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<_>>() // ensures uniqueness
182            .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    /// A Wire Order has 2 identifiers. For simplification purposes, since they share most of their fields together we
229    /// merge them to access the fields
230    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/// see [RFC 8555 Section 7.1.6](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.6)
274#[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            // homogeneous identifiers
404            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            // homogeneous identifiers
414            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}