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 super::{EnrollmentHandle, Error, Json, Result, crypto::E2eiSignatureKeypair, id::QualifiedE2eiClientId, types};
12use crate::{ClientId, KeystoreError, MlsCiphersuite, MlsError};
13
14#[derive(Debug, serde::Serialize, serde::Deserialize)]
16pub struct E2eiEnrollment {
17 delegate: RustyE2eIdentity,
18 pub(crate) sign_sk: E2eiSignatureKeypair,
19 pub(super) client_id: String,
20 pub(super) display_name: String,
21 pub(super) handle: String,
22 pub(super) team: Option<String>,
23 expiry: core::time::Duration,
24 directory: Option<types::E2eiAcmeDirectory>,
25 account: Option<wire_e2e_identity::prelude::E2eiAcmeAccount>,
26 user_authz: Option<E2eiAcmeAuthorization>,
27 device_authz: Option<E2eiAcmeAuthorization>,
28 valid_order: Option<wire_e2e_identity::prelude::E2eiAcmeOrder>,
29 finalize: Option<wire_e2e_identity::prelude::E2eiAcmeFinalize>,
30 pub(super) ciphersuite: MlsCiphersuite,
31 has_called_new_oidc_challenge_request: bool,
32}
33
34impl std::ops::Deref for E2eiEnrollment {
35 type Target = RustyE2eIdentity;
36
37 fn deref(&self) -> &Self::Target {
38 &self.delegate
39 }
40}
41
42impl E2eiEnrollment {
43 #[allow(clippy::too_many_arguments)]
52 pub fn try_new(
53 client_id: ClientId,
54 display_name: String,
55 handle: String,
56 team: Option<String>,
57 expiry_sec: u32,
58 backend: &MlsCryptoProvider,
59 ciphersuite: MlsCiphersuite,
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, backend))?;
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 ciphersuite(&self) -> &MlsCiphersuite {
92 &self.ciphersuite
93 }
94
95 pub fn directory_response(&mut self, directory: Json) -> Result<types::E2eiAcmeDirectory> {
104 let directory = serde_json::from_slice(&directory[..])?;
105 let directory: types::E2eiAcmeDirectory = self.acme_directory_response(directory)?.into();
106 self.directory = Some(directory.clone());
107 Ok(directory)
108 }
109
110 pub fn new_account_request(&self, previous_nonce: String) -> Result<Json> {
119 let directory = self
120 .directory
121 .as_ref()
122 .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?;
123 let account = self.acme_new_account_request(&directory.try_into()?, previous_nonce)?;
124 let account = serde_json::to_vec(&account)?;
125 Ok(account)
126 }
127
128 pub fn new_account_response(&mut self, account: Json) -> Result<()> {
135 let account = serde_json::from_slice(&account[..])?;
136 let account = self.acme_new_account_response(account)?;
137 self.account = Some(account);
138 Ok(())
139 }
140
141 pub fn new_order_request(&self, previous_nonce: String) -> Result<Json> {
148 let directory = self
149 .directory
150 .as_ref()
151 .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?;
152 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
153 "You must first call 'newAccountResponse()'",
154 ))?;
155 let order = self.acme_new_order_request(
156 &self.display_name,
157 &self.client_id,
158 &self.handle,
159 self.expiry,
160 &directory.try_into()?,
161 account,
162 previous_nonce,
163 )?;
164 let order = serde_json::to_vec(&order)?;
165 Ok(order)
166 }
167
168 pub fn new_order_response(&self, order: Json) -> Result<types::E2eiNewAcmeOrder> {
175 let order = serde_json::from_slice(&order[..])?;
176 self.acme_new_order_response(order)?.try_into()
177 }
178
179 pub fn new_authz_request(&self, url: String, previous_nonce: String) -> Result<Json> {
189 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
190 "You must first call 'newAccountResponse()'",
191 ))?;
192 let authz = self.acme_new_authz_request(&url.parse()?, account, previous_nonce)?;
193 let authz = serde_json::to_vec(&authz)?;
194 Ok(authz)
195 }
196
197 pub fn new_authz_response(&mut self, authz: Json) -> Result<types::E2eiNewAcmeAuthz> {
204 let authz = serde_json::from_slice(&authz[..])?;
205 let authz = self.acme_new_authz_response(authz)?;
206 match &authz {
207 E2eiAcmeAuthorization::User { .. } => self.user_authz = Some(authz.clone()),
208 E2eiAcmeAuthorization::Device { .. } => self.device_authz = Some(authz.clone()),
209 };
210 authz.try_into()
211 }
212
213 #[allow(clippy::too_many_arguments)]
227 pub fn create_dpop_token(&self, expiry_secs: u32, backend_nonce: String) -> Result<String> {
228 let expiry = core::time::Duration::from_secs(expiry_secs as u64);
229 let authz = self
230 .device_authz
231 .as_ref()
232 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
233 let challenge = match authz {
234 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
235 E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
236 };
237 Ok(self.new_dpop_token(
238 &self.client_id,
239 self.display_name.as_str(),
240 challenge,
241 backend_nonce,
242 self.handle.as_str(),
243 self.team.clone(),
244 expiry,
245 )?)
246 }
247
248 pub fn new_dpop_challenge_request(&self, access_token: String, previous_nonce: String) -> Result<Json> {
258 let authz = self
259 .device_authz
260 .as_ref()
261 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
262 let challenge = match authz {
263 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
264 E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
265 };
266 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
267 "You must first call 'newAccountResponse()'",
268 ))?;
269 let challenge = self.acme_dpop_challenge_request(access_token, challenge, account, previous_nonce)?;
270 let challenge = serde_json::to_vec(&challenge)?;
271 Ok(challenge)
272 }
273
274 pub fn new_dpop_challenge_response(&self, challenge: Json) -> Result<()> {
281 let challenge = serde_json::from_slice(&challenge[..])?;
282 Ok(self.acme_new_challenge_response(challenge)?)
283 }
284
285 pub fn new_oidc_challenge_request(&mut self, id_token: String, previous_nonce: String) -> Result<Json> {
295 let authz = self
296 .user_authz
297 .as_ref()
298 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
299 let challenge = match authz {
300 E2eiAcmeAuthorization::User { challenge, .. } => challenge,
301 E2eiAcmeAuthorization::Device { .. } => return Err(Error::ImplementationError),
302 };
303 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
304 "You must first call 'newAccountResponse()'",
305 ))?;
306 let challenge = self.acme_oidc_challenge_request(id_token, challenge, account, previous_nonce)?;
307 let challenge = serde_json::to_vec(&challenge)?;
308
309 self.has_called_new_oidc_challenge_request = true;
310
311 Ok(challenge)
312 }
313
314 pub async fn new_oidc_challenge_response(&mut self, challenge: Json) -> Result<()> {
321 let challenge = serde_json::from_slice(&challenge[..])?;
322 self.acme_new_challenge_response(challenge)?;
323
324 if !self.has_called_new_oidc_challenge_request {
325 return Err(Error::OutOfOrderEnrollment(
326 "You must first call 'new_oidc_challenge_request()'",
327 ));
328 }
329
330 Ok(())
331 }
332
333 pub fn check_order_request(&self, order_url: String, previous_nonce: String) -> Result<Json> {
342 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
343 "You must first call 'newAccountResponse()'",
344 ))?;
345 let order = self.acme_check_order_request(order_url.parse()?, account, previous_nonce)?;
346 let order = serde_json::to_vec(&order)?;
347 Ok(order)
348 }
349
350 pub fn check_order_response(&mut self, order: Json) -> Result<String> {
360 let order = serde_json::from_slice(&order[..])?;
361 let valid_order = self.acme_check_order_response(order)?;
362 let finalize_url = valid_order.finalize_url.to_string();
363 self.valid_order = Some(valid_order);
364 Ok(finalize_url)
365 }
366
367 pub fn finalize_request(&mut self, previous_nonce: String) -> Result<Json> {
377 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
378 "You must first call 'newAccountResponse()'",
379 ))?;
380 let order = self.valid_order.as_ref().ok_or(Error::OutOfOrderEnrollment(
381 "You must first call 'checkOrderResponse()'",
382 ))?;
383 let finalize = self.acme_finalize_request(order, account, previous_nonce)?;
384 let finalize = serde_json::to_vec(&finalize)?;
385 Ok(finalize)
386 }
387
388 pub fn finalize_response(&mut self, finalize: Json) -> Result<String> {
398 let finalize = serde_json::from_slice(&finalize[..])?;
399 let finalize = self.acme_finalize_response(finalize)?;
400 let certificate_url = finalize.certificate_url.to_string();
401 self.finalize = Some(finalize);
402 Ok(certificate_url)
403 }
404
405 pub fn certificate_request(&mut self, previous_nonce: String) -> Result<Json> {
414 let account = self.account.take().ok_or(Error::OutOfOrderEnrollment(
415 "You must first call 'newAccountResponse()'",
416 ))?;
417 let finalize = self
418 .finalize
419 .take()
420 .ok_or(Error::OutOfOrderEnrollment("You must first call 'finalizeResponse()'"))?;
421 let certificate = self.acme_x509_certificate_request(finalize, account, previous_nonce)?;
422 let certificate = serde_json::to_vec(&certificate)?;
423 Ok(certificate)
424 }
425
426 pub(crate) async fn certificate_response(
427 &mut self,
428 certificate_chain: String,
429 env: &wire_e2e_identity::prelude::x509::revocation::PkiEnvironment,
430 ) -> Result<Vec<Vec<u8>>> {
431 let order = self.valid_order.take().ok_or(Error::OutOfOrderEnrollment(
432 "You must first call 'checkOrderResponse()'",
433 ))?;
434 let certificates = self.acme_x509_certificate_response(certificate_chain, order, Some(env))?;
435
436 self.sign_sk.zeroize();
438 self.delegate.sign_kp.zeroize();
439 self.delegate.acme_kp.zeroize();
440
441 Ok(certificates)
442 }
443
444 pub(crate) async fn stash(self, backend: &MlsCryptoProvider) -> Result<EnrollmentHandle> {
445 const HANDLE_SIZE: usize = 32;
447
448 let content = serde_json::to_vec(&self)?;
449 let handle = backend
450 .crypto()
451 .random_vec(HANDLE_SIZE)
452 .map_err(MlsError::wrap("generating random vector of bytes"))?;
453 backend
454 .key_store()
455 .save_e2ei_enrollment(&handle, &content)
456 .await
457 .map_err(KeystoreError::wrap("saving e2ei enrollment"))?;
458 Ok(handle)
459 }
460
461 pub(crate) async fn stash_pop(backend: &MlsCryptoProvider, handle: EnrollmentHandle) -> Result<Self> {
462 let content = backend
463 .key_store()
464 .pop_e2ei_enrollment(&handle)
465 .await
466 .map_err(KeystoreError::wrap("popping e2ei enrollment"))?;
467 Ok(serde_json::from_slice(&content)?)
468 }
469}