wire_e2e_identity/legacy/
mod.rs

1use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
2use zeroize::Zeroize as _;
3
4use crate::{E2eiAcmeAuthorization, RustyE2eIdentity};
5
6pub mod crypto;
7pub mod device_status;
8pub mod id;
9pub mod types;
10
11use crypto::{E2eiSignatureKeypair, ciphersuite_to_jws_algo};
12use id::{ClientId, QualifiedE2eiClientId};
13
14use super::error::{E2eIdentityError as Error, E2eIdentityResult as Result};
15
16type Json = Vec<u8>;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19/// Supporting struct for CRL registration result
20pub struct CrlRegistration {
21    /// Whether this CRL modifies the old CRL (i.e. has a different revocated cert list)
22    pub dirty: bool,
23    /// Optional expiration timestamp
24    pub expiration: Option<u64>,
25}
26
27/// Wire end to end identity solution for fetching a x509 certificate which identifies a client.
28#[derive(Debug, serde::Serialize, serde::Deserialize)]
29pub struct E2eiEnrollment {
30    delegate: RustyE2eIdentity,
31    pub sign_sk: E2eiSignatureKeypair,
32    pub(super) client_id: String,
33    pub(super) display_name: String,
34    pub(super) handle: String,
35    pub(super) team: Option<String>,
36    expiry: core::time::Duration,
37    directory: Option<types::E2eiAcmeDirectory>,
38    account: Option<crate::E2eiAcmeAccount>,
39    user_authz: Option<E2eiAcmeAuthorization>,
40    device_authz: Option<E2eiAcmeAuthorization>,
41    valid_order: Option<crate::E2eiAcmeOrder>,
42    finalize: Option<crate::E2eiAcmeFinalize>,
43    pub(super) ciphersuite: Ciphersuite,
44    has_called_new_oidc_challenge_request: bool,
45}
46
47impl std::ops::Deref for E2eiEnrollment {
48    type Target = RustyE2eIdentity;
49
50    fn deref(&self) -> &Self::Target {
51        &self.delegate
52    }
53}
54
55impl E2eiEnrollment {
56    /// Builds an instance holding private key material. This instance has to be used in the whole
57    /// enrollment process then dropped to clear secret key material.
58    ///
59    /// # Parameters
60    /// * `client_id` - client identifier e.g. `b7ac11a4-8f01-4527-af88-1c30885a7931:6add501bacd1d90e@example.com`
61    /// * `display_name` - human readable name displayed in the application e.g. `Smith, Alice M (QA)`
62    /// * `handle` - user handle e.g. `alice.smith.qa@example.com`
63    /// * `expiry_sec` - generated x509 certificate expiry in seconds
64    #[allow(clippy::too_many_arguments)]
65    pub fn try_new<T>(
66        client_id: ClientId,
67        display_name: String,
68        handle: String,
69        team: Option<String>,
70        expiry_sec: u32,
71        ciphersuite: Ciphersuite,
72        has_called_new_oidc_challenge_request: bool,
73        crypto: &impl OpenMlsCrypto,
74    ) -> Result<Self> {
75        let alg = ciphersuite_to_jws_algo(ciphersuite)?;
76        let sign_sk = Self::new_sign_key(crypto, ciphersuite)?;
77
78        let client_id = QualifiedE2eiClientId::try_from(client_id.as_slice())?;
79        let client_id = String::try_from(client_id)?;
80        let expiry = core::time::Duration::from_secs(u64::from(expiry_sec));
81        let delegate = RustyE2eIdentity::try_new(alg, sign_sk.clone())?;
82        Ok(Self {
83            delegate,
84            sign_sk,
85            client_id,
86            display_name,
87            handle,
88            team,
89            expiry,
90            directory: None,
91            account: None,
92            user_authz: None,
93            device_authz: None,
94            valid_order: None,
95            finalize: None,
96            ciphersuite,
97            has_called_new_oidc_challenge_request,
98        })
99    }
100
101    pub(crate) fn new_sign_key(crypto: &impl OpenMlsCrypto, ciphersuite: Ciphersuite) -> Result<E2eiSignatureKeypair> {
102        let (sk, _) = crypto.signature_key_gen(ciphersuite.signature_algorithm())?;
103        E2eiSignatureKeypair::try_new(ciphersuite.signature_algorithm(), sk)
104    }
105
106    pub fn ciphersuite(&self) -> &Ciphersuite {
107        &self.ciphersuite
108    }
109
110    /// Parses the response from `GET /acme/{provisioner-name}/directory`.
111    /// Use this [types::E2eiAcmeDirectory] in the next step to fetch the first nonce from the acme server. Use
112    /// [types::E2eiAcmeDirectory.new_nonce].
113    ///
114    /// See [RFC 8555 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1)
115    ///
116    /// # Parameters
117    /// * `directory` - http response body
118    pub fn directory_response(&mut self, directory: Json) -> Result<types::E2eiAcmeDirectory> {
119        let directory = serde_json::from_slice(&directory[..])?;
120        let directory: types::E2eiAcmeDirectory = self.acme_directory_response(directory)?.into();
121        self.directory = Some(directory.clone());
122        Ok(directory)
123    }
124
125    /// For creating a new acme account. This returns a signed JWS-alike request body to send to
126    /// `POST /acme/{provisioner-name}/new-account`.
127    ///
128    /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3).
129    ///
130    /// # Parameters
131    /// * `directory` - you got from [Self::directory_response]
132    /// * `previous_nonce` - you got from calling `HEAD {directory.new_nonce}`
133    pub fn new_account_request(&self, previous_nonce: String) -> Result<Json> {
134        let directory = self
135            .directory
136            .as_ref()
137            .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?;
138        let account = self.acme_new_account_request(&directory.try_into()?, previous_nonce)?;
139        let account = serde_json::to_vec(&account)?;
140        Ok(account)
141    }
142
143    /// Parses the response from `POST /acme/{provisioner-name}/new-account`.
144    ///
145    /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3).
146    ///
147    /// # Parameters
148    /// * `account` - http response body
149    pub fn new_account_response(&mut self, account: Json) -> Result<()> {
150        let account = serde_json::from_slice(&account[..])?;
151        let account = self.acme_new_account_response(account)?;
152        self.account = Some(account);
153        Ok(())
154    }
155
156    /// Creates a new acme order for the handle (userId + display name) and the clientId.
157    ///
158    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
159    ///
160    /// # Parameters
161    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/new-account`
162    pub fn new_order_request(&self, previous_nonce: String) -> Result<Json> {
163        let directory = self
164            .directory
165            .as_ref()
166            .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?;
167        let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
168            "You must first call 'newAccountResponse()'",
169        ))?;
170        let order = self.acme_new_order_request(
171            &self.display_name,
172            &self.client_id,
173            &self.handle,
174            self.expiry,
175            &directory.try_into()?,
176            account,
177            previous_nonce,
178        )?;
179        let order = serde_json::to_vec(&order)?;
180        Ok(order)
181    }
182
183    /// Parses the response from `POST /acme/{provisioner-name}/new-order`.
184    ///
185    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
186    ///
187    /// # Parameters
188    /// * `new_order` - http response body
189    pub fn new_order_response(&self, order: Json) -> Result<types::E2eiNewAcmeOrder> {
190        let order = serde_json::from_slice(&order[..])?;
191        self.acme_new_order_response(order)?.try_into()
192    }
193
194    /// Creates a new authorization request.
195    ///
196    /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5).
197    ///
198    /// # Parameters
199    /// * `url` - one of the URL in new order's authorizations (from [Self::new_order_response])
200    /// * `account` - you got from [Self::new_account_response]
201    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/new-order` (or from the
202    ///   previous to this method if you are creating the second authorization)
203    pub fn new_authz_request(&self, url: String, previous_nonce: String) -> Result<Json> {
204        let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
205            "You must first call 'newAccountResponse()'",
206        ))?;
207        let authz = self.acme_new_authz_request(&url.parse()?, account, previous_nonce)?;
208        let authz = serde_json::to_vec(&authz)?;
209        Ok(authz)
210    }
211
212    /// Parses the response from `POST /acme/{provisioner-name}/authz/{authz-id}`
213    ///
214    /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5).
215    ///
216    /// # Parameters
217    /// * `new_authz` - http response body
218    pub fn new_authz_response(&mut self, authz: Json) -> Result<types::E2eiNewAcmeAuthz> {
219        let authz = serde_json::from_slice(&authz[..])?;
220        let authz = self.acme_new_authz_response(authz)?;
221        match &authz {
222            E2eiAcmeAuthorization::User { .. } => self.user_authz = Some(authz.clone()),
223            E2eiAcmeAuthorization::Device { .. } => self.device_authz = Some(authz.clone()),
224        };
225        authz.try_into()
226    }
227
228    /// Generates a new client Dpop JWT token. It demonstrates proof of possession of the nonces
229    /// (from wire-server & acme server) and will be verified by the acme server when verifying the
230    /// challenge (in order to deliver a certificate).
231    ///
232    /// Then send it to
233    /// [`POST /clients/{id}/access-token`](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token)
234    /// on wire-server.
235    ///
236    /// # Parameters
237    /// * `expiry_secs` - of the client Dpop JWT. This should be equal to the grace period set in Team Management
238    /// * `backend_nonce` - you get by calling `GET /clients/token/nonce` on wire-server. See endpoint [definition](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/get_clients__client__nonce)
239    /// * `expiry` - token expiry
240    #[allow(clippy::too_many_arguments)]
241    pub fn create_dpop_token(&self, expiry_secs: u32, backend_nonce: String) -> Result<String> {
242        let expiry = core::time::Duration::from_secs(expiry_secs as u64);
243        let authz = self
244            .device_authz
245            .as_ref()
246            .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
247        let challenge = match authz {
248            E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
249            E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
250        };
251        self.new_dpop_token(
252            &self.client_id,
253            self.display_name.as_str(),
254            challenge,
255            backend_nonce,
256            self.handle.as_str(),
257            self.team.clone(),
258            expiry,
259        )
260    }
261
262    /// Creates a new challenge request.
263    ///
264    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
265    ///
266    /// # Parameters
267    /// * `access_token` - returned by wire-server from [this endpoint](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token)
268    /// * `dpop_challenge` - you found after [Self::new_authz_response]
269    /// * `account` - you got from [Self::new_account_response]
270    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/authz/{authz-id}`
271    pub fn new_dpop_challenge_request(&self, access_token: String, previous_nonce: String) -> Result<Json> {
272        let authz = self
273            .device_authz
274            .as_ref()
275            .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
276        let challenge = match authz {
277            E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
278            E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
279        };
280        let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
281            "You must first call 'newAccountResponse()'",
282        ))?;
283        let challenge = self.acme_dpop_challenge_request(access_token, challenge, account, previous_nonce)?;
284        let challenge = serde_json::to_vec(&challenge)?;
285        Ok(challenge)
286    }
287
288    /// Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}` for the DPoP challenge
289    ///
290    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
291    ///
292    /// # Parameters
293    /// * `challenge` - http response body
294    pub fn new_dpop_challenge_response(&self, challenge: Json) -> Result<()> {
295        let challenge = serde_json::from_slice(&challenge[..])?;
296        self.acme_new_challenge_response(challenge)
297    }
298
299    /// Creates a new challenge request.
300    ///
301    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
302    ///
303    /// # Parameters
304    /// * `id_token` - you get back from Identity Provider
305    /// * `oidc_challenge` - you found after [Self::new_authz_response]
306    /// * `account` - you got from [Self::new_account_response]
307    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/authz/{authz-id}`
308    pub fn new_oidc_challenge_request(&mut self, id_token: String, previous_nonce: String) -> Result<Json> {
309        let authz = self
310            .user_authz
311            .as_ref()
312            .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
313        let challenge = match authz {
314            E2eiAcmeAuthorization::User { challenge, .. } => challenge,
315            E2eiAcmeAuthorization::Device { .. } => return Err(Error::ImplementationError),
316        };
317        let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
318            "You must first call 'newAccountResponse()'",
319        ))?;
320        let challenge = self.acme_oidc_challenge_request(id_token, challenge, account, previous_nonce)?;
321        let challenge = serde_json::to_vec(&challenge)?;
322
323        self.has_called_new_oidc_challenge_request = true;
324
325        Ok(challenge)
326    }
327
328    /// Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}` for the OIDC challenge
329    ///
330    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
331    ///
332    /// # Parameters
333    /// * `challenge` - http response body
334    pub fn new_oidc_challenge_response(&mut self, challenge: Json) -> Result<()> {
335        let challenge = serde_json::from_slice(&challenge[..])?;
336        self.acme_new_challenge_response(challenge)?;
337
338        if !self.has_called_new_oidc_challenge_request {
339            return Err(Error::OutOfOrderEnrollment(
340                "You must first call 'new_oidc_challenge_request()'",
341            ));
342        }
343
344        Ok(())
345    }
346
347    /// Verifies that the previous challenge has been completed.
348    ///
349    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
350    ///
351    /// # Parameters
352    /// * `order_url` - `location` header from http response you got from [Self::new_order_response]
353    /// * `account` - you got from [Self::new_account_response]
354    /// * `previous_nonce` - `replay-nonce` response header from `POST
355    ///   /acme/{provisioner-name}/challenge/{challenge-id}`
356    pub fn check_order_request(&self, order_url: String, previous_nonce: String) -> Result<Json> {
357        let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
358            "You must first call 'newAccountResponse()'",
359        ))?;
360        let order = self.acme_check_order_request(order_url.parse()?, account, previous_nonce)?;
361        let order = serde_json::to_vec(&order)?;
362        Ok(order)
363    }
364
365    /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}`.
366    ///
367    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
368    ///
369    /// # Parameters
370    /// * `order` - http response body
371    ///
372    /// # Returns
373    /// The finalize url to use with [Self::finalize_request]
374    pub fn check_order_response(&mut self, order: Json) -> Result<String> {
375        let order = serde_json::from_slice(&order[..])?;
376        let valid_order = self.acme_check_order_response(order)?;
377        let finalize_url = valid_order.finalize_url.to_string();
378        self.valid_order = Some(valid_order);
379        Ok(finalize_url)
380    }
381
382    /// Final step before fetching the certificate.
383    ///
384    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
385    ///
386    /// # Parameters
387    /// * `domains` - you want to generate a certificate for e.g. `["wire.com"]`
388    /// * `order` - you got from [Self::check_order_response]
389    /// * `account` - you got from [Self::new_account_response]
390    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/order/{order-id}`
391    pub fn finalize_request(&mut self, previous_nonce: String) -> Result<Json> {
392        let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
393            "You must first call 'newAccountResponse()'",
394        ))?;
395        let order = self.valid_order.as_ref().ok_or(Error::OutOfOrderEnrollment(
396            "You must first call 'checkOrderResponse()'",
397        ))?;
398        let finalize = self.acme_finalize_request(order, account, previous_nonce)?;
399        let finalize = serde_json::to_vec(&finalize)?;
400        Ok(finalize)
401    }
402
403    /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}/finalize`.
404    ///
405    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
406    ///
407    /// # Parameters
408    /// * `finalize` - http response body
409    ///
410    /// # Returns
411    /// The certificate url to use with [Self::certificate_request]
412    pub fn finalize_response(&mut self, finalize: Json) -> Result<String> {
413        let finalize = serde_json::from_slice(&finalize[..])?;
414        let finalize = self.acme_finalize_response(finalize)?;
415        let certificate_url = finalize.certificate_url.to_string();
416        self.finalize = Some(finalize);
417        Ok(certificate_url)
418    }
419
420    /// Creates a request for finally fetching the x509 certificate.
421    ///
422    /// See [RFC 8555 Section 7.4.2](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2).
423    ///
424    /// # Parameters
425    /// * `finalize` - you got from [Self::finalize_response]
426    /// * `account` - you got from [Self::new_account_response]
427    /// * `previous_nonce` - `replay-nonce` response header from `POST
428    ///   /acme/{provisioner-name}/order/{order-id}/finalize`
429    pub fn certificate_request(&mut self, previous_nonce: String) -> Result<Json> {
430        let account = self.account.take().ok_or(Error::OutOfOrderEnrollment(
431            "You must first call 'newAccountResponse()'",
432        ))?;
433        let finalize = self
434            .finalize
435            .take()
436            .ok_or(Error::OutOfOrderEnrollment("You must first call 'finalizeResponse()'"))?;
437        let certificate = self.acme_x509_certificate_request(finalize, account, previous_nonce)?;
438        let certificate = serde_json::to_vec(&certificate)?;
439        Ok(certificate)
440    }
441
442    pub async fn certificate_response(
443        &mut self,
444        certificate_chain: String,
445        env: &crate::x509_check::revocation::PkiEnvironment,
446    ) -> Result<Vec<Vec<u8>>> {
447        let order = self.valid_order.take().ok_or(Error::OutOfOrderEnrollment(
448            "You must first call 'checkOrderResponse()'",
449        ))?;
450        let certificates = self.acme_x509_certificate_response(certificate_chain, order, Some(env))?;
451
452        // zeroize the private material
453        self.sign_sk.zeroize();
454        self.delegate.sign_kp.zeroize();
455        self.delegate.acme_kp.zeroize();
456
457        Ok(certificates)
458    }
459}