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
16use super::{EnrollmentHandle, Error, Json, Result, crypto::E2eiSignatureKeypair, id::QualifiedE2eiClientId, types};
17
18#[derive(Debug, serde::Serialize, serde::Deserialize)]
20pub struct E2eiEnrollment {
21 delegate: RustyE2eIdentity,
22 pub(crate) sign_sk: E2eiSignatureKeypair,
23 pub(super) client_id: String,
24 pub(super) display_name: String,
25 pub(super) handle: String,
26 pub(super) team: Option<String>,
27 expiry: core::time::Duration,
28 directory: Option<types::E2eiAcmeDirectory>,
29 account: Option<wire_e2e_identity::prelude::E2eiAcmeAccount>,
30 user_authz: Option<E2eiAcmeAuthorization>,
31 device_authz: Option<E2eiAcmeAuthorization>,
32 valid_order: Option<wire_e2e_identity::prelude::E2eiAcmeOrder>,
33 finalize: Option<wire_e2e_identity::prelude::E2eiAcmeFinalize>,
34 pub(super) ciphersuite: MlsCiphersuite,
35 has_called_new_oidc_challenge_request: bool,
36}
37
38impl std::ops::Deref for E2eiEnrollment {
39 type Target = RustyE2eIdentity;
40
41 fn deref(&self) -> &Self::Target {
42 &self.delegate
43 }
44}
45
46impl E2eiEnrollment {
47 #[allow(clippy::too_many_arguments)]
56 pub fn try_new(
57 client_id: ClientId,
58 display_name: String,
59 handle: String,
60 team: Option<String>,
61 expiry_sec: u32,
62 backend: &MlsCryptoProvider,
63 ciphersuite: MlsCiphersuite,
64 sign_keypair: Option<E2eiSignatureKeypair>,
65 has_called_new_oidc_challenge_request: bool,
66 ) -> Result<Self> {
67 let alg = ciphersuite.try_into()?;
68 let sign_sk = sign_keypair
69 .map(Ok)
70 .unwrap_or_else(|| Self::new_sign_key(ciphersuite, backend))?;
71
72 let client_id = QualifiedE2eiClientId::try_from(client_id.as_slice())?;
73 let client_id = String::try_from(client_id)?;
74 let expiry = core::time::Duration::from_secs(u64::from(expiry_sec));
75 let delegate = RustyE2eIdentity::try_new(alg, sign_sk.clone()).map_err(Error::from)?;
76 Ok(Self {
77 delegate,
78 sign_sk,
79 client_id,
80 display_name,
81 handle,
82 team,
83 expiry,
84 directory: None,
85 account: None,
86 user_authz: None,
87 device_authz: None,
88 valid_order: None,
89 finalize: None,
90 ciphersuite,
91 has_called_new_oidc_challenge_request,
92 })
93 }
94
95 pub(crate) fn ciphersuite(&self) -> &MlsCiphersuite {
96 &self.ciphersuite
97 }
98
99 pub fn directory_response(&mut self, directory: Json) -> Result<types::E2eiAcmeDirectory> {
108 let directory = serde_json::from_slice(&directory[..])?;
109 let directory: types::E2eiAcmeDirectory = self.acme_directory_response(directory)?.into();
110 self.directory = Some(directory.clone());
111 Ok(directory)
112 }
113
114 pub fn new_account_request(&self, previous_nonce: String) -> Result<Json> {
123 let directory = self
124 .directory
125 .as_ref()
126 .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?;
127 let account = self.acme_new_account_request(&directory.try_into()?, previous_nonce)?;
128 let account = serde_json::to_vec(&account)?;
129 Ok(account)
130 }
131
132 pub fn new_account_response(&mut self, account: Json) -> Result<()> {
139 let account = serde_json::from_slice(&account[..])?;
140 let account = self.acme_new_account_response(account)?;
141 self.account = Some(account);
142 Ok(())
143 }
144
145 pub fn new_order_request(&self, previous_nonce: String) -> Result<Json> {
152 let directory = self
153 .directory
154 .as_ref()
155 .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?;
156 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
157 "You must first call 'newAccountResponse()'",
158 ))?;
159 let order = self.acme_new_order_request(
160 &self.display_name,
161 &self.client_id,
162 &self.handle,
163 self.expiry,
164 &directory.try_into()?,
165 account,
166 previous_nonce,
167 )?;
168 let order = serde_json::to_vec(&order)?;
169 Ok(order)
170 }
171
172 pub fn new_order_response(&self, order: Json) -> Result<types::E2eiNewAcmeOrder> {
179 let order = serde_json::from_slice(&order[..])?;
180 self.acme_new_order_response(order)?.try_into()
181 }
182
183 pub fn new_authz_request(&self, url: String, previous_nonce: String) -> Result<Json> {
193 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
194 "You must first call 'newAccountResponse()'",
195 ))?;
196 let authz = self.acme_new_authz_request(&url.parse()?, account, previous_nonce)?;
197 let authz = serde_json::to_vec(&authz)?;
198 Ok(authz)
199 }
200
201 pub fn new_authz_response(&mut self, authz: Json) -> Result<types::E2eiNewAcmeAuthz> {
208 let authz = serde_json::from_slice(&authz[..])?;
209 let authz = self.acme_new_authz_response(authz)?;
210 match &authz {
211 E2eiAcmeAuthorization::User { .. } => self.user_authz = Some(authz.clone()),
212 E2eiAcmeAuthorization::Device { .. } => self.device_authz = Some(authz.clone()),
213 };
214 authz.try_into()
215 }
216
217 #[allow(clippy::too_many_arguments)]
231 pub fn create_dpop_token(&self, expiry_secs: u32, backend_nonce: String) -> Result<String> {
232 let expiry = core::time::Duration::from_secs(expiry_secs as u64);
233 let authz = self
234 .device_authz
235 .as_ref()
236 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
237 let challenge = match authz {
238 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
239 E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
240 };
241 Ok(self.new_dpop_token(
242 &self.client_id,
243 self.display_name.as_str(),
244 challenge,
245 backend_nonce,
246 self.handle.as_str(),
247 self.team.clone(),
248 expiry,
249 )?)
250 }
251
252 pub fn new_dpop_challenge_request(&self, access_token: String, previous_nonce: String) -> Result<Json> {
262 let authz = self
263 .device_authz
264 .as_ref()
265 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
266 let challenge = match authz {
267 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
268 E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
269 };
270 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
271 "You must first call 'newAccountResponse()'",
272 ))?;
273 let challenge = self.acme_dpop_challenge_request(access_token, challenge, account, previous_nonce)?;
274 let challenge = serde_json::to_vec(&challenge)?;
275 Ok(challenge)
276 }
277
278 pub fn new_dpop_challenge_response(&self, challenge: Json) -> Result<()> {
285 let challenge = serde_json::from_slice(&challenge[..])?;
286 Ok(self.acme_new_challenge_response(challenge)?)
287 }
288
289 pub fn new_oidc_challenge_request(&mut self, id_token: String, previous_nonce: String) -> Result<Json> {
299 let authz = self
300 .user_authz
301 .as_ref()
302 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
303 let challenge = match authz {
304 E2eiAcmeAuthorization::User { challenge, .. } => challenge,
305 E2eiAcmeAuthorization::Device { .. } => return Err(Error::ImplementationError),
306 };
307 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
308 "You must first call 'newAccountResponse()'",
309 ))?;
310 let challenge = self.acme_oidc_challenge_request(id_token, challenge, account, previous_nonce)?;
311 let challenge = serde_json::to_vec(&challenge)?;
312
313 self.has_called_new_oidc_challenge_request = true;
314
315 Ok(challenge)
316 }
317
318 pub async fn new_oidc_challenge_response(&mut self, challenge: Json) -> Result<()> {
325 let challenge = serde_json::from_slice(&challenge[..])?;
326 self.acme_new_challenge_response(challenge)?;
327
328 if !self.has_called_new_oidc_challenge_request {
329 return Err(Error::OutOfOrderEnrollment(
330 "You must first call 'new_oidc_challenge_request()'",
331 ));
332 }
333
334 Ok(())
335 }
336
337 pub fn check_order_request(&self, order_url: String, previous_nonce: String) -> Result<Json> {
346 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
347 "You must first call 'newAccountResponse()'",
348 ))?;
349 let order = self.acme_check_order_request(order_url.parse()?, account, previous_nonce)?;
350 let order = serde_json::to_vec(&order)?;
351 Ok(order)
352 }
353
354 pub fn check_order_response(&mut self, order: Json) -> Result<String> {
364 let order = serde_json::from_slice(&order[..])?;
365 let valid_order = self.acme_check_order_response(order)?;
366 let finalize_url = valid_order.finalize_url.to_string();
367 self.valid_order = Some(valid_order);
368 Ok(finalize_url)
369 }
370
371 pub fn finalize_request(&mut self, previous_nonce: String) -> Result<Json> {
381 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
382 "You must first call 'newAccountResponse()'",
383 ))?;
384 let order = self.valid_order.as_ref().ok_or(Error::OutOfOrderEnrollment(
385 "You must first call 'checkOrderResponse()'",
386 ))?;
387 let finalize = self.acme_finalize_request(order, account, previous_nonce)?;
388 let finalize = serde_json::to_vec(&finalize)?;
389 Ok(finalize)
390 }
391
392 pub fn finalize_response(&mut self, finalize: Json) -> Result<String> {
402 let finalize = serde_json::from_slice(&finalize[..])?;
403 let finalize = self.acme_finalize_response(finalize)?;
404 let certificate_url = finalize.certificate_url.to_string();
405 self.finalize = Some(finalize);
406 Ok(certificate_url)
407 }
408
409 pub fn certificate_request(&mut self, previous_nonce: String) -> Result<Json> {
418 let account = self.account.take().ok_or(Error::OutOfOrderEnrollment(
419 "You must first call 'newAccountResponse()'",
420 ))?;
421 let finalize = self
422 .finalize
423 .take()
424 .ok_or(Error::OutOfOrderEnrollment("You must first call 'finalizeResponse()'"))?;
425 let certificate = self.acme_x509_certificate_request(finalize, account, previous_nonce)?;
426 let certificate = serde_json::to_vec(&certificate)?;
427 Ok(certificate)
428 }
429
430 pub(crate) async fn certificate_response(
431 &mut self,
432 certificate_chain: String,
433 env: &wire_e2e_identity::prelude::x509::revocation::PkiEnvironment,
434 ) -> Result<Vec<Vec<u8>>> {
435 let order = self.valid_order.take().ok_or(Error::OutOfOrderEnrollment(
436 "You must first call 'checkOrderResponse()'",
437 ))?;
438 let certificates = self.acme_x509_certificate_response(certificate_chain, order, Some(env))?;
439
440 self.sign_sk.zeroize();
442 self.delegate.sign_kp.zeroize();
443 self.delegate.acme_kp.zeroize();
444
445 Ok(certificates)
446 }
447
448 pub(crate) async fn stash(self, backend: &MlsCryptoProvider) -> Result<EnrollmentHandle> {
449 const HANDLE_SIZE: usize = 32;
451
452 let content = serde_json::to_vec(&self)?;
453 let handle = backend
454 .crypto()
455 .random_vec(HANDLE_SIZE)
456 .map_err(MlsError::wrap("generating random vector of bytes"))?;
457 backend
458 .key_store()
459 .save_e2ei_enrollment(&handle, &content)
460 .await
461 .map_err(KeystoreError::wrap("saving e2ei enrollment"))?;
462 Ok(handle)
463 }
464
465 pub(crate) async fn stash_pop(backend: &MlsCryptoProvider, handle: EnrollmentHandle) -> Result<Self> {
466 let content = backend
467 .key_store()
468 .pop_e2ei_enrollment(&handle)
469 .await
470 .map_err(KeystoreError::wrap("popping e2ei enrollment"))?;
471 Ok(serde_json::from_slice(&content)?)
472 }
473}