Skip to main content

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, 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
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/// see [RFC 8555 Section 7.1.6](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.6)
270#[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            // homogeneous identifiers
400            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            // homogeneous identifiers
410            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}