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::{Ciphersuite, ClientId, KeystoreError, 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: Ciphersuite,
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: 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, 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) -> &Ciphersuite {
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)]
226 pub fn create_dpop_token(&self, expiry_secs: u32, backend_nonce: String) -> Result<String> {
227 let expiry = core::time::Duration::from_secs(expiry_secs as u64);
228 let authz = self
229 .device_authz
230 .as_ref()
231 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
232 let challenge = match authz {
233 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
234 E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
235 };
236 Ok(self.new_dpop_token(
237 &self.client_id,
238 self.display_name.as_str(),
239 challenge,
240 backend_nonce,
241 self.handle.as_str(),
242 self.team.clone(),
243 expiry,
244 )?)
245 }
246
247 pub fn new_dpop_challenge_request(&self, access_token: String, previous_nonce: String) -> Result<Json> {
257 let authz = self
258 .device_authz
259 .as_ref()
260 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
261 let challenge = match authz {
262 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
263 E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
264 };
265 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
266 "You must first call 'newAccountResponse()'",
267 ))?;
268 let challenge = self.acme_dpop_challenge_request(access_token, challenge, account, previous_nonce)?;
269 let challenge = serde_json::to_vec(&challenge)?;
270 Ok(challenge)
271 }
272
273 pub fn new_dpop_challenge_response(&self, challenge: Json) -> Result<()> {
280 let challenge = serde_json::from_slice(&challenge[..])?;
281 Ok(self.acme_new_challenge_response(challenge)?)
282 }
283
284 pub fn new_oidc_challenge_request(&mut self, id_token: String, previous_nonce: String) -> Result<Json> {
294 let authz = self
295 .user_authz
296 .as_ref()
297 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
298 let challenge = match authz {
299 E2eiAcmeAuthorization::User { challenge, .. } => challenge,
300 E2eiAcmeAuthorization::Device { .. } => return Err(Error::ImplementationError),
301 };
302 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
303 "You must first call 'newAccountResponse()'",
304 ))?;
305 let challenge = self.acme_oidc_challenge_request(id_token, challenge, account, previous_nonce)?;
306 let challenge = serde_json::to_vec(&challenge)?;
307
308 self.has_called_new_oidc_challenge_request = true;
309
310 Ok(challenge)
311 }
312
313 pub fn new_oidc_challenge_response(&mut self, challenge: Json) -> Result<()> {
320 let challenge = serde_json::from_slice(&challenge[..])?;
321 self.acme_new_challenge_response(challenge)?;
322
323 if !self.has_called_new_oidc_challenge_request {
324 return Err(Error::OutOfOrderEnrollment(
325 "You must first call 'new_oidc_challenge_request()'",
326 ));
327 }
328
329 Ok(())
330 }
331
332 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> {
415 let account = self.account.take().ok_or(Error::OutOfOrderEnrollment(
416 "You must first call 'newAccountResponse()'",
417 ))?;
418 let finalize = self
419 .finalize
420 .take()
421 .ok_or(Error::OutOfOrderEnrollment("You must first call 'finalizeResponse()'"))?;
422 let certificate = self.acme_x509_certificate_request(finalize, account, previous_nonce)?;
423 let certificate = serde_json::to_vec(&certificate)?;
424 Ok(certificate)
425 }
426
427 pub(crate) async fn certificate_response(
428 &mut self,
429 certificate_chain: String,
430 env: &wire_e2e_identity::prelude::x509::revocation::PkiEnvironment,
431 ) -> Result<Vec<Vec<u8>>> {
432 let order = self.valid_order.take().ok_or(Error::OutOfOrderEnrollment(
433 "You must first call 'checkOrderResponse()'",
434 ))?;
435 let certificates = self.acme_x509_certificate_response(certificate_chain, order, Some(env))?;
436
437 self.sign_sk.zeroize();
439 self.delegate.sign_kp.zeroize();
440 self.delegate.acme_kp.zeroize();
441
442 Ok(certificates)
443 }
444
445 pub(crate) async fn stash(self, backend: &MlsCryptoProvider) -> Result<EnrollmentHandle> {
446 const HANDLE_SIZE: usize = 32;
448
449 let content = serde_json::to_vec(&self)?;
450 let handle = backend
451 .crypto()
452 .random_vec(HANDLE_SIZE)
453 .map_err(MlsError::wrap("generating random vector of bytes"))?;
454 backend
455 .key_store()
456 .save_e2ei_enrollment(&handle, &content)
457 .await
458 .map_err(KeystoreError::wrap("saving e2ei enrollment"))?;
459 Ok(handle)
460 }
461
462 pub(crate) async fn stash_pop(backend: &MlsCryptoProvider, handle: EnrollmentHandle) -> Result<Self> {
463 let content = backend
464 .key_store()
465 .pop_e2ei_enrollment(&handle)
466 .await
467 .map_err(KeystoreError::wrap("popping e2ei enrollment"))?;
468 Ok(serde_json::from_slice(&content)?)
469 }
470}