core_crypto/e2e_identity/enrollment/
mod.rs1mod crypto;
2#[cfg(test)]
3pub mod test_utils;
4
5use core_crypto_keystore::CryptoKeystoreMls as _;
6use mls_crypto_provider::MlsCryptoProvider;
7use openmls_traits::{OpenMlsCryptoProvider as _, random::OpenMlsRand as _};
8use wire_e2e_identity::{RustyE2eIdentity, prelude::E2eiAcmeAuthorization};
9use zeroize::Zeroize as _;
10
11use crate::{
12 KeystoreError, MlsError,
13 prelude::{ClientId, MlsCiphersuite},
14};
15
16#[cfg(not(target_family = "wasm"))]
17use super::refresh_token;
18use super::{EnrollmentHandle, Error, Json, Result, crypto::E2eiSignatureKeypair, id::QualifiedE2eiClientId, types};
19
20#[derive(Debug, serde::Serialize, serde::Deserialize)]
22pub struct E2eiEnrollment {
23 delegate: RustyE2eIdentity,
24 pub(crate) sign_sk: E2eiSignatureKeypair,
25 pub(super) client_id: String,
26 pub(super) display_name: String,
27 pub(super) handle: String,
28 pub(super) team: Option<String>,
29 expiry: core::time::Duration,
30 directory: Option<types::E2eiAcmeDirectory>,
31 account: Option<wire_e2e_identity::prelude::E2eiAcmeAccount>,
32 user_authz: Option<E2eiAcmeAuthorization>,
33 device_authz: Option<E2eiAcmeAuthorization>,
34 valid_order: Option<wire_e2e_identity::prelude::E2eiAcmeOrder>,
35 finalize: Option<wire_e2e_identity::prelude::E2eiAcmeFinalize>,
36 pub(super) ciphersuite: MlsCiphersuite,
37 #[cfg(not(target_family = "wasm"))]
38 pub(super) refresh_token: Option<refresh_token::RefreshToken>,
39}
40
41impl std::ops::Deref for E2eiEnrollment {
42 type Target = RustyE2eIdentity;
43
44 fn deref(&self) -> &Self::Target {
45 &self.delegate
46 }
47}
48
49impl E2eiEnrollment {
50 #[allow(clippy::too_many_arguments)]
59 pub fn try_new(
60 client_id: ClientId,
61 display_name: String,
62 handle: String,
63 team: Option<String>,
64 expiry_sec: u32,
65 backend: &MlsCryptoProvider,
66 ciphersuite: MlsCiphersuite,
67 sign_keypair: Option<E2eiSignatureKeypair>,
68 #[cfg(not(target_family = "wasm"))] refresh_token: Option<refresh_token::RefreshToken>,
69 ) -> Result<Self> {
70 let alg = ciphersuite.try_into()?;
71 let sign_sk = sign_keypair
72 .map(Ok)
73 .unwrap_or_else(|| Self::new_sign_key(ciphersuite, backend))?;
74
75 let client_id = QualifiedE2eiClientId::try_from(client_id.as_slice())?;
76 let client_id = String::try_from(client_id)?;
77 let expiry = core::time::Duration::from_secs(u64::from(expiry_sec));
78 let delegate = RustyE2eIdentity::try_new(alg, sign_sk.clone()).map_err(Error::from)?;
79 Ok(Self {
80 delegate,
81 sign_sk,
82 client_id,
83 display_name,
84 handle,
85 team,
86 expiry,
87 directory: None,
88 account: None,
89 user_authz: None,
90 device_authz: None,
91 valid_order: None,
92 finalize: None,
93 ciphersuite,
94 #[cfg(not(target_family = "wasm"))]
95 refresh_token,
96 })
97 }
98
99 pub(crate) fn ciphersuite(&self) -> &MlsCiphersuite {
100 &self.ciphersuite
101 }
102
103 pub fn directory_response(&mut self, directory: Json) -> Result<types::E2eiAcmeDirectory> {
112 let directory = serde_json::from_slice(&directory[..])?;
113 let directory: types::E2eiAcmeDirectory = self.acme_directory_response(directory)?.into();
114 self.directory = Some(directory.clone());
115 Ok(directory)
116 }
117
118 pub fn new_account_request(&self, previous_nonce: String) -> Result<Json> {
127 let directory = self
128 .directory
129 .as_ref()
130 .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?;
131 let account = self.acme_new_account_request(&directory.try_into()?, previous_nonce)?;
132 let account = serde_json::to_vec(&account)?;
133 Ok(account)
134 }
135
136 pub fn new_account_response(&mut self, account: Json) -> Result<()> {
143 let account = serde_json::from_slice(&account[..])?;
144 let account = self.acme_new_account_response(account)?;
145 self.account = Some(account);
146 Ok(())
147 }
148
149 pub fn new_order_request(&self, previous_nonce: String) -> Result<Json> {
156 let directory = self
157 .directory
158 .as_ref()
159 .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?;
160 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
161 "You must first call 'newAccountResponse()'",
162 ))?;
163 let order = self.acme_new_order_request(
164 &self.display_name,
165 &self.client_id,
166 &self.handle,
167 self.expiry,
168 &directory.try_into()?,
169 account,
170 previous_nonce,
171 )?;
172 let order = serde_json::to_vec(&order)?;
173 Ok(order)
174 }
175
176 pub fn new_order_response(&self, order: Json) -> Result<types::E2eiNewAcmeOrder> {
183 let order = serde_json::from_slice(&order[..])?;
184 self.acme_new_order_response(order)?.try_into()
185 }
186
187 pub fn new_authz_request(&self, url: String, previous_nonce: String) -> Result<Json> {
197 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
198 "You must first call 'newAccountResponse()'",
199 ))?;
200 let authz = self.acme_new_authz_request(&url.parse()?, account, previous_nonce)?;
201 let authz = serde_json::to_vec(&authz)?;
202 Ok(authz)
203 }
204
205 pub fn new_authz_response(&mut self, authz: Json) -> Result<types::E2eiNewAcmeAuthz> {
212 let authz = serde_json::from_slice(&authz[..])?;
213 let authz = self.acme_new_authz_response(authz)?;
214 match &authz {
215 E2eiAcmeAuthorization::User { .. } => self.user_authz = Some(authz.clone()),
216 E2eiAcmeAuthorization::Device { .. } => self.device_authz = Some(authz.clone()),
217 };
218 authz.try_into()
219 }
220
221 #[allow(clippy::too_many_arguments)]
235 pub fn create_dpop_token(&self, expiry_secs: u32, backend_nonce: String) -> Result<String> {
236 let expiry = core::time::Duration::from_secs(expiry_secs as u64);
237 let authz = self
238 .device_authz
239 .as_ref()
240 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
241 let challenge = match authz {
242 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
243 E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
244 };
245 Ok(self.new_dpop_token(
246 &self.client_id,
247 self.display_name.as_str(),
248 challenge,
249 backend_nonce,
250 self.handle.as_str(),
251 self.team.clone(),
252 expiry,
253 )?)
254 }
255
256 pub fn new_dpop_challenge_request(&self, access_token: String, previous_nonce: String) -> Result<Json> {
266 let authz = self
267 .device_authz
268 .as_ref()
269 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
270 let challenge = match authz {
271 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
272 E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
273 };
274 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
275 "You must first call 'newAccountResponse()'",
276 ))?;
277 let challenge = self.acme_dpop_challenge_request(access_token, challenge, account, previous_nonce)?;
278 let challenge = serde_json::to_vec(&challenge)?;
279 Ok(challenge)
280 }
281
282 pub fn new_dpop_challenge_response(&self, challenge: Json) -> Result<()> {
289 let challenge = serde_json::from_slice(&challenge[..])?;
290 Ok(self.acme_new_challenge_response(challenge)?)
291 }
292
293 pub fn new_oidc_challenge_request(
304 &mut self,
305 id_token: String,
306 #[cfg(not(target_family = "wasm"))] refresh_token: String,
307 previous_nonce: String,
308 ) -> Result<Json> {
309 #[cfg(not(target_family = "wasm"))]
310 {
311 if refresh_token.is_empty() {
312 return Err(Error::InvalidRefreshToken);
313 }
314 }
315 let authz = self
316 .user_authz
317 .as_ref()
318 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
319 let challenge = match authz {
320 E2eiAcmeAuthorization::User { challenge, .. } => challenge,
321 E2eiAcmeAuthorization::Device { .. } => return Err(Error::ImplementationError),
322 };
323 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
324 "You must first call 'newAccountResponse()'",
325 ))?;
326 let challenge = self.acme_oidc_challenge_request(id_token, challenge, account, previous_nonce)?;
327 let challenge = serde_json::to_vec(&challenge)?;
328 #[cfg(not(target_family = "wasm"))]
329 {
330 self.refresh_token.replace(refresh_token.into());
331 }
332 Ok(challenge)
333 }
334
335 pub async fn new_oidc_challenge_response(
342 &mut self,
343 #[cfg(not(target_family = "wasm"))] backend: &MlsCryptoProvider,
344 challenge: Json,
345 ) -> Result<()> {
346 let challenge = serde_json::from_slice(&challenge[..])?;
347 self.acme_new_challenge_response(challenge)?;
348
349 #[cfg(not(target_family = "wasm"))]
350 {
351 let refresh_token = self.refresh_token.take().ok_or(Error::OutOfOrderEnrollment(
356 "You must first call 'new_oidc_challenge_request()'",
357 ))?;
358 refresh_token.replace(backend).await?;
359 }
360 Ok(())
361 }
362
363 pub fn check_order_request(&self, order_url: String, previous_nonce: String) -> Result<Json> {
372 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
373 "You must first call 'newAccountResponse()'",
374 ))?;
375 let order = self.acme_check_order_request(order_url.parse()?, account, previous_nonce)?;
376 let order = serde_json::to_vec(&order)?;
377 Ok(order)
378 }
379
380 pub fn check_order_response(&mut self, order: Json) -> Result<String> {
390 let order = serde_json::from_slice(&order[..])?;
391 let valid_order = self.acme_check_order_response(order)?;
392 let finalize_url = valid_order.finalize_url.to_string();
393 self.valid_order = Some(valid_order);
394 Ok(finalize_url)
395 }
396
397 pub fn finalize_request(&mut self, previous_nonce: String) -> Result<Json> {
407 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
408 "You must first call 'newAccountResponse()'",
409 ))?;
410 let order = self.valid_order.as_ref().ok_or(Error::OutOfOrderEnrollment(
411 "You must first call 'checkOrderResponse()'",
412 ))?;
413 let finalize = self.acme_finalize_request(order, account, previous_nonce)?;
414 let finalize = serde_json::to_vec(&finalize)?;
415 Ok(finalize)
416 }
417
418 pub fn finalize_response(&mut self, finalize: Json) -> Result<String> {
428 let finalize = serde_json::from_slice(&finalize[..])?;
429 let finalize = self.acme_finalize_response(finalize)?;
430 let certificate_url = finalize.certificate_url.to_string();
431 self.finalize = Some(finalize);
432 Ok(certificate_url)
433 }
434
435 pub fn certificate_request(&mut self, previous_nonce: String) -> Result<Json> {
444 let account = self.account.take().ok_or(Error::OutOfOrderEnrollment(
445 "You must first call 'newAccountResponse()'",
446 ))?;
447 let finalize = self
448 .finalize
449 .take()
450 .ok_or(Error::OutOfOrderEnrollment("You must first call 'finalizeResponse()'"))?;
451 let certificate = self.acme_x509_certificate_request(finalize, account, previous_nonce)?;
452 let certificate = serde_json::to_vec(&certificate)?;
453 Ok(certificate)
454 }
455
456 pub(crate) async fn certificate_response(
457 &mut self,
458 certificate_chain: String,
459 env: &wire_e2e_identity::prelude::x509::revocation::PkiEnvironment,
460 ) -> Result<Vec<Vec<u8>>> {
461 let order = self.valid_order.take().ok_or(Error::OutOfOrderEnrollment(
462 "You must first call 'checkOrderResponse()'",
463 ))?;
464 let certificates = self.acme_x509_certificate_response(certificate_chain, order, Some(env))?;
465
466 self.sign_sk.zeroize();
468 self.delegate.sign_kp.zeroize();
469 self.delegate.acme_kp.zeroize();
470
471 #[cfg(not(target_family = "wasm"))]
472 self.refresh_token.zeroize();
473
474 Ok(certificates)
475 }
476
477 pub(crate) async fn stash(self, backend: &MlsCryptoProvider) -> Result<EnrollmentHandle> {
478 const HANDLE_SIZE: usize = 32;
480
481 let content = serde_json::to_vec(&self)?;
482 let handle = backend
483 .crypto()
484 .random_vec(HANDLE_SIZE)
485 .map_err(MlsError::wrap("generating random vector of bytes"))?;
486 backend
487 .key_store()
488 .save_e2ei_enrollment(&handle, &content)
489 .await
490 .map_err(KeystoreError::wrap("saving e2ei enrollment"))?;
491 Ok(handle)
492 }
493
494 pub(crate) async fn stash_pop(backend: &MlsCryptoProvider, handle: EnrollmentHandle) -> Result<Self> {
495 let content = backend
496 .key_store()
497 .pop_e2ei_enrollment(&handle)
498 .await
499 .map_err(KeystoreError::wrap("popping e2ei enrollment"))?;
500 Ok(serde_json::from_slice(&content)?)
501 }
502
503 #[cfg(not(target_family = "wasm"))]
507 pub fn get_refresh_token(&self) -> Result<&str> {
508 self.refresh_token
509 .as_ref()
510 .map(|rt| rt.as_str())
511 .ok_or(Error::OutOfOrderEnrollment(
512 "No OIDC refresh token registered yet or it has been persisted",
513 ))
514 }
515}