core_crypto/e2e_identity/enrollment/
mod.rs

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