core_crypto/e2e_identity/enrollment/
mod.rs1use 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#[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 #[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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 self.sign_sk.zeroize();
459 self.delegate.sign_kp.zeroize();
460 self.delegate.acme_kp.zeroize();
461
462 Ok(certificates)
463 }
464}