wire_e2e_identity/acme/
order.rs

1use std::collections::HashSet;
2
3use rusty_jwt_tools::prelude::*;
4
5use crate::acme::{identifier::CanonicalIdentifier, prelude::*};
6
7// Order creation
8impl RustyAcme {
9    /// create a new order
10    /// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
11    #[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        // Extract the account URL from previous response which created a new account
24        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    /// parse response from order creation
51    /// [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4)
52    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
69// Long poll order until ready
70impl RustyAcme {
71    /// check an order status until it becomes ready
72    /// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4)
73    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        // Extract the account URL from previous response which created a new account
81        let acct_url = account.acct_url()?;
82
83        // No payload required for authz
84        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    /// parse response from order check
90    /// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4)
91    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    /// step-ca flagged this order as invalid
125    #[error("Created order is not valid")]
126    Invalid,
127    /// This order 'not_before' is in future
128    #[error("This order 'not_before' is in future")]
129    NotYetValid,
130    /// This order is expired
131    #[error("This order is expired")]
132    Expired,
133    /// This order should only have the 2 Wire identifiers
134    #[error("This order should only have the 2 Wire identifiers")]
135    WrongIdentifiers,
136}
137
138/// For creating an order
139/// see https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4
140#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
141#[cfg_attr(test, derive(Clone))]
142#[serde(rename_all = "camelCase")]
143struct AcmeOrderRequest {
144    /// An array of identifier objects that the client wishes to submit an order for
145    pub identifiers: Vec<AcmeIdentifier>,
146    /// The requested value of the notBefore field in the certificate, in the date format defined
147    /// in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339)
148    #[serde(skip_serializing_if = "Option::is_none", with = "time::serde::rfc3339::option")]
149    pub not_before: Option<time::OffsetDateTime>,
150    /// The requested value of the notAfter field in the certificate, in the date format defined in
151    /// [RFC3339](https://www.rfc-editor.org/rfc/rfc3339)
152    #[serde(skip_serializing_if = "Option::is_none", with = "time::serde::rfc3339::option")]
153    pub not_after: Option<time::OffsetDateTime>,
154}
155
156/// Result of an order creation
157/// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4)
158#[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<_>>() // ensures uniqueness
179            .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    /// A Wire Order has 2 identifiers. For simplification purposes, since they share most of their fields together we
226    /// merge them to access the fields
227    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/// see [RFC 8555 Section 7.1.6](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.6)
271#[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            // homogeneous identifiers
401            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            // homogeneous identifiers
411            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}