From b32ff7cce1df7c7647c41773afe200e23d0ade46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 10:26:02 +0200 Subject: [PATCH 01/28] chore: crypto: remove old x509 impl from TransactionContext It's time. With CC 10, credentials are handled in an entirely different way. --- .../e2e_identity/init_certificates.rs | 5 - .../transaction_context/e2e_identity/mod.rs | 218 ------------------ 2 files changed, 223 deletions(-) diff --git a/crypto/src/transaction_context/e2e_identity/init_certificates.rs b/crypto/src/transaction_context/e2e_identity/init_certificates.rs index 8fc6056c81..9961f68e69 100644 --- a/crypto/src/transaction_context/e2e_identity/init_certificates.rs +++ b/crypto/src/transaction_context/e2e_identity/init_certificates.rs @@ -82,11 +82,6 @@ impl TransactionContext { self.e2ei_register_intermediate_ca(inter_ca).await } - pub(crate) async fn e2ei_register_intermediate_ca_der(&self, cert_der: &[u8]) -> Result { - let inter_ca = x509_cert::Certificate::from_der(cert_der)?; - self.e2ei_register_intermediate_ca(inter_ca).await - } - async fn e2ei_register_intermediate_ca( &self, inter_ca: x509_cert::Certificate, diff --git a/crypto/src/transaction_context/e2e_identity/mod.rs b/crypto/src/transaction_context/e2e_identity/mod.rs index ae3b6acb03..34b89f9ab4 100644 --- a/crypto/src/transaction_context/e2e_identity/mod.rs +++ b/crypto/src/transaction_context/e2e_identity/mod.rs @@ -5,222 +5,4 @@ pub mod enabled; mod error; mod init_certificates; -use std::{collections::HashSet, sync::Arc}; - pub use error::{Error, Result}; -use openmls_traits::types::SignatureScheme; -use wire_e2e_identity::{NewCrlDistributionPoints, x509_check::extract_crl_uris}; - -use super::TransactionContext; -use crate::{ - CertificateBundle, Ciphersuite, ClientId, Credential, CredentialRef, E2eiEnrollment, MlsTransport, RecursiveError, - RustCrypto, - mls::credential::{crl::get_new_crl_distribution_points, x509::CertificatePrivateKey}, -}; - -fn get_sign_key_for_mls(enrollment: &E2eiEnrollment) -> Result> { - let sk = match enrollment.ciphersuite().signature_algorithm() { - SignatureScheme::ECDSA_SECP256R1_SHA256 | SignatureScheme::ECDSA_SECP384R1_SHA384 => { - enrollment.sign_sk.to_vec() - } - SignatureScheme::ECDSA_SECP521R1_SHA512 => RustCrypto::normalize_p521_secret_key(&enrollment.sign_sk).to_vec(), - SignatureScheme::ED25519 => RustCrypto::normalize_ed25519_key(enrollment.sign_sk.as_slice()) - .map_err(RecursiveError::e2e_identity("normalizing ed25519 key"))? - .to_bytes() - .to_vec(), - SignatureScheme::ED448 => { - return Err(wire_e2e_identity::E2eIdentityError::NotSupported) - .map_err(RecursiveError::e2e_identity("normalizing ed25519 key"))?; - } - }; - Ok(sk) -} - -impl TransactionContext { - /// Creates an enrollment instance with private key material you can use in order to fetch - /// a new x509 certificate from the acme server. - /// - /// # Parameters - /// * `client_id` - client identifier e.g. `b7ac11a4-8f01-4527-af88-1c30885a7931:6add501bacd1d90e@example.com` - /// * `display_name` - human readable name displayed in the application e.g. `Smith, Alice M (QA)` - /// * `handle` - user handle e.g. `alice.smith.qa@example.com` - /// * `expiry_sec` - generated x509 certificate expiry in seconds - pub async fn e2ei_new_enrollment( - &self, - client_id: ClientId, - display_name: String, - handle: String, - team: Option, - expiry_sec: u32, - ciphersuite: Ciphersuite, - ) -> Result { - let client_id = wire_e2e_identity::legacy::id::ClientId::from(client_id.0); - E2eiEnrollment::try_new::( - client_id, - display_name, - handle, - team, - expiry_sec, - ciphersuite.into(), - false, // fresh install so no refresh token registered yet - crate::mls_provider::CRYPTO.as_ref(), - ) - .map_err(RecursiveError::e2e_identity("creating new enrollment")) - .map_err(Into::into) - } - - /// Saves a new X509 credential. Requires first having enrolled a new X509 certificate - /// with [TransactionContext::e2ei_new_enrollment]. - /// - /// # Expected actions to perform after this function (in this order) - /// 1. Set the credential to the return value of this function for each conversation via - /// [crate::mls::conversation::ConversationGuard::set_credential_by_ref] - /// 2. Generate new key packages with [Self::generate_keypackage] - /// 3. Use these to replace the stale ones the in the backend - /// 4. Delete the old credentials and keypackages locally using [Self::remove_credential] - pub async fn save_x509_credential( - &self, - enrollment: &mut E2eiEnrollment, - certificate_chain: String, - ) -> Result<(CredentialRef, NewCrlDistributionPoints)> { - let sk = get_sign_key_for_mls(enrollment)?; - let ciphersuite = *enrollment.ciphersuite(); - let signature_scheme = ciphersuite.signature_algorithm(); - - let pki_environment = self - .pki_environment() - .await - .map_err(RecursiveError::transaction("getting pki environment"))?; - let certificate_chain = enrollment - .certificate_response( - certificate_chain, - pki_environment - .mls_pki_env_provider() - .borrow() - .await - .as_ref() - .ok_or(Error::PkiEnvironmentUnset)?, - ) - .await - .map_err(RecursiveError::e2e_identity("getting certificate response"))?; - - let private_key = CertificatePrivateKey::new(sk); - - let crl_new_distribution_points = self.extract_dp_on_init(&certificate_chain[..]).await?; - - let cert_bundle = CertificateBundle { - certificate_chain, - private_key, - signature_scheme, - }; - - let credential = Credential::x509(ciphersuite.into(), cert_bundle).map_err(RecursiveError::mls_credential( - "creating new x509 credential from certificate bundle in save_x509_credential", - ))?; - - let credential_ref = self - .add_credential(credential) - .await - .map_err(RecursiveError::transaction( - "saving and adding credential in save_x509_credential", - ))?; - - Ok((credential_ref, crl_new_distribution_points)) - } - /// Parses the ACME server response from the endpoint fetching x509 certificates and uses it - /// to initialize the MLS client with a certificate - pub async fn e2ei_mls_init_only( - &self, - enrollment: &mut E2eiEnrollment, - certificate_chain: String, - transport: Arc, - ) -> Result<(CredentialRef, NewCrlDistributionPoints)> { - let pki_environment = self - .pki_environment() - .await - .map_err(RecursiveError::transaction("getting pki environment"))?; - - let sk = get_sign_key_for_mls(enrollment)?; - let ciphersuite = *enrollment.ciphersuite(); - let certificate_chain = enrollment - .certificate_response( - certificate_chain, - pki_environment - .mls_pki_env_provider() - .borrow() - .await - .as_ref() - .ok_or(Error::PkiEnvironmentUnset)?, - ) - .await - .map_err(RecursiveError::e2e_identity("getting certificate response"))?; - - let crl_new_distribution_points = self.extract_dp_on_init(&certificate_chain[..]).await?; - - let private_key = CertificatePrivateKey::new(sk); - - let cert_bundle = CertificateBundle { - certificate_chain, - private_key, - signature_scheme: ciphersuite.signature_algorithm(), - }; - - let mut credential = Credential::x509(ciphersuite.into(), cert_bundle.clone()).map_err( - RecursiveError::mls_credential("creating credential from certificate bundle in e2ei_mls_init_only"), - )?; - let database = &self - .database() - .await - .map_err(RecursiveError::transaction("Getting database from transaction context"))?; - let credential_ref = credential.save(database).await.map_err(RecursiveError::mls_credential( - "saving credential in e2ei_mls_init_only", - ))?; - let session_id = cert_bundle.get_client_id().map_err(RecursiveError::mls_credential( - "Getting session id from certificate bundle", - ))?; - self.mls_init(session_id, transport) - .await - .map_err(RecursiveError::transaction("initializing mls"))?; - Ok((credential_ref, crl_new_distribution_points)) - } - - /// When x509 new credentials are registered this extracts the new CRL Distribution Point from the end entity - /// certificate and all the intermediates - async fn extract_dp_on_init(&self, certificate_chain: &[Vec]) -> Result { - use x509_cert::der::Decode as _; - - // Own intermediates are not provided by smallstep in the /federation endpoint so we got to intercept them here, - // at issuance - let size = certificate_chain.len(); - let mut crl_new_distribution_points = HashSet::new(); - if size > 1 { - for int in certificate_chain.iter().skip(1).rev() { - let mut crl_dp = self - .e2ei_register_intermediate_ca_der(int) - .await - .map_err(RecursiveError::transaction("registering intermediate ca der"))?; - if let Some(crl_dp) = crl_dp.take() { - crl_new_distribution_points.extend(crl_dp); - } - } - } - - let ee = certificate_chain.first().ok_or(Error::InvalidCertificateChain)?; - let ee = x509_cert::Certificate::from_der(ee) - .map_err(crate::mls::credential::Error::DecodeX509) - .map_err(RecursiveError::mls_credential("decoding x509 credential"))?; - let mut ee_crl_dp = extract_crl_uris(&ee).map_err(RecursiveError::e2e_identity("extracting crl urls"))?; - if let Some(crl_dp) = ee_crl_dp.take() { - crl_new_distribution_points.extend(crl_dp); - } - - let database = self - .database() - .await - .map_err(RecursiveError::transaction("getting database"))?; - get_new_crl_distribution_points(&database, crl_new_distribution_points) - .await - .map_err(RecursiveError::mls_credential("getting new crl distribution points")) - .map_err(Into::into) - } -} From ac7df5da922c33225109ea77ec9ee1b99bf0b010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 10:47:26 +0200 Subject: [PATCH 02/28] chore: crypto: remove unused import --- crypto/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 8f3fdbdfcc..efe2ccf4fa 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -41,10 +41,7 @@ pub use openmls::{ group_info::VerifiableGroupInfo, }, }; -use wire_e2e_identity::{ - legacy::{E2eiEnrollment, device_status::DeviceStatus}, - pki_env::PkiEnvironment, -}; +use wire_e2e_identity::{legacy::device_status::DeviceStatus, pki_env::PkiEnvironment}; pub use crate::{ build_metadata::{BUILD_METADATA, BuildMetadata}, From 37c67bb8d13c0091ff3be2795a380f5222a966be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 10:52:36 +0200 Subject: [PATCH 03/28] chore: crypto: silence clippy --- crypto/src/mls_provider/crypto_provider.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crypto/src/mls_provider/crypto_provider.rs b/crypto/src/mls_provider/crypto_provider.rs index 004b248e8e..eec09ae134 100644 --- a/crypto/src/mls_provider/crypto_provider.rs +++ b/crypto/src/mls_provider/crypto_provider.rs @@ -65,6 +65,9 @@ impl RustCrypto { Ok(()) } + // TODO: remove this expect(unused) once we start using it again or we drop + // it completely (see WPB-23594) + #[expect(unused)] pub(crate) fn normalize_p521_secret_key(sk: &[u8]) -> zeroize::Zeroizing<[u8; 66]> { normalize_p521_secret_key(sk) } From 827a6f109278652d86245726357a87dea5d185e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 16:45:41 +0200 Subject: [PATCH 04/28] chore: e2e-identity: move acme/identity to under acquisition The code there does not really have to do with acme at all. It contains functionality required by leaf certificate verification, and keeping it there would make error handling more complex. So just move it to the acquisition module, where it belongs. --- .../src/{acme/identity/mod.rs => acquisition/identity.rs} | 0 e2e-identity/src/{acme/identity => acquisition}/thumbprint.rs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename e2e-identity/src/{acme/identity/mod.rs => acquisition/identity.rs} (100%) rename e2e-identity/src/{acme/identity => acquisition}/thumbprint.rs (100%) diff --git a/e2e-identity/src/acme/identity/mod.rs b/e2e-identity/src/acquisition/identity.rs similarity index 100% rename from e2e-identity/src/acme/identity/mod.rs rename to e2e-identity/src/acquisition/identity.rs diff --git a/e2e-identity/src/acme/identity/thumbprint.rs b/e2e-identity/src/acquisition/thumbprint.rs similarity index 100% rename from e2e-identity/src/acme/identity/thumbprint.rs rename to e2e-identity/src/acquisition/thumbprint.rs From 5c671312a807b4769c9e5e9ace33b0dbb534f036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 16:45:41 +0200 Subject: [PATCH 05/28] chore: e2e-identity: move CertificateError to the acquisition module This is where those errors belong and where they will be returned from. --- e2e-identity/src/acme/error.rs | 41 --------------------------- e2e-identity/src/acquisition/error.rs | 41 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/e2e-identity/src/acme/error.rs b/e2e-identity/src/acme/error.rs index 33514d03f2..7a417eeb46 100644 --- a/e2e-identity/src/acme/error.rs +++ b/e2e-identity/src/acme/error.rs @@ -61,45 +61,4 @@ pub enum RustyAcmeError { /// UTF-8 parsing error #[error(transparent)] Utf8(#[from] std::str::Utf8Error), - /// Invalid/incomplete certificate - #[error(transparent)] - InvalidCertificate(#[from] CertificateError), -} - -/// Given x509 certificate is invalid and does not follow Wire's format -#[derive(Debug, thiserror::Error)] -pub enum CertificateError { - /// ClientId does not match expected one - #[error("ClientId does not match expected one")] - ClientIdMismatch, - /// Display name does not match expected one - #[error("Display name does not match expected one")] - DisplayNameMismatch, - /// Handle does not match expected one - #[error("Handle does not match expected one")] - HandleMismatch, - /// Domain does not match expected one - #[error("Domain does not match expected one")] - DomainMismatch, - /// DisplayName is missing from the certificate - #[error("DisplayName is missing from the certificate")] - MissingDisplayName, - /// Handle is missing from the certificate - #[error("Handle is missing from the certificate")] - MissingHandle, - /// Domain is missing from the certificate - #[error("Domain is missing from the certificate")] - MissingDomain, - /// ClientId is missing from the certificate - #[error("ClientId is missing from the certificate")] - MissingClientId, - /// X509 lacks required standard fields - #[error("X509 lacks required standard fields")] - InvalidFormat, - /// Advertised public key does not match algorithm - #[error("Advertised public key does not match algorithm")] - InvalidPublicKey, - /// Advertised public key is not supported - #[error("Advertised public key is not supported")] - UnsupportedPublicKey, } diff --git a/e2e-identity/src/acquisition/error.rs b/e2e-identity/src/acquisition/error.rs index c9cdee101b..2eb4cbeddd 100644 --- a/e2e-identity/src/acquisition/error.rs +++ b/e2e-identity/src/acquisition/error.rs @@ -14,4 +14,45 @@ pub enum Error { Acme(#[from] crate::acme::RustyAcmeError), #[error(transparent)] RustyJwtError(#[from] RustyJwtError), + /// Invalid/incomplete certificate + #[error(transparent)] + InvalidCertificate(#[from] CertificateError), +} + +/// Given x509 certificate is invalid and does not follow Wire's format +#[derive(Debug, thiserror::Error)] +pub enum CertificateError { + /// ClientId does not match expected one + #[error("ClientId does not match expected one")] + ClientIdMismatch, + /// Display name does not match expected one + #[error("Display name does not match expected one")] + DisplayNameMismatch, + /// Handle does not match expected one + #[error("Handle does not match expected one")] + HandleMismatch, + /// Domain does not match expected one + #[error("Domain does not match expected one")] + DomainMismatch, + /// DisplayName is missing from the certificate + #[error("DisplayName is missing from the certificate")] + MissingDisplayName, + /// Handle is missing from the certificate + #[error("Handle is missing from the certificate")] + MissingHandle, + /// Domain is missing from the certificate + #[error("Domain is missing from the certificate")] + MissingDomain, + /// ClientId is missing from the certificate + #[error("ClientId is missing from the certificate")] + MissingClientId, + /// X509 lacks required standard fields + #[error("X509 lacks required standard fields")] + InvalidFormat, + /// Advertised public key does not match algorithm + #[error("Advertised public key does not match algorithm")] + InvalidPublicKey, + /// Advertised public key is not supported + #[error("Advertised public key is not supported")] + UnsupportedPublicKey, } From c587fb0542b0e87b039148e7db7e60cfd12788b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 16:45:41 +0200 Subject: [PATCH 06/28] chore: e2e-identity: adjust imports after the move --- e2e-identity/src/acme/mod.rs | 2 -- e2e-identity/src/acquisition/identity.rs | 7 +++---- e2e-identity/src/acquisition/mod.rs | 3 +++ e2e-identity/src/acquisition/thumbprint.rs | 5 ++++- e2e-identity/src/lib.rs | 4 +--- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/e2e-identity/src/acme/mod.rs b/e2e-identity/src/acme/mod.rs index 926cf288ca..42ba5ca8b8 100644 --- a/e2e-identity/src/acme/mod.rs +++ b/e2e-identity/src/acme/mod.rs @@ -6,7 +6,6 @@ mod directory; mod error; mod finalize; mod identifier; -mod identity; mod jws; mod order; @@ -17,7 +16,6 @@ pub use directory::AcmeDirectory; pub use error::{RustyAcmeError, RustyAcmeResult}; pub use finalize::AcmeFinalize; pub use identifier::{AcmeIdentifier, WireIdentifier}; -pub use identity::{WireIdentity, WireIdentityReader, thumbprint::compute_raw_key_thumbprint}; pub use jws::AcmeJws; pub use order::AcmeOrder; diff --git a/e2e-identity/src/acquisition/identity.rs b/e2e-identity/src/acquisition/identity.rs index 934189e0bb..1339a0806a 100644 --- a/e2e-identity/src/acquisition/identity.rs +++ b/e2e-identity/src/acquisition/identity.rs @@ -2,12 +2,11 @@ use rusty_jwt_tools::prelude::{ClientId, HashAlgorithm, QualifiedHandle}; use x509_cert::der::Decode as _; use crate::{ - acme::{RustyAcmeResult, error::CertificateError}, + acme::RustyAcmeResult, + acquisition::{error::CertificateError, thumbprint::try_compute_jwk_canonicalized_thumbprint}, x509_check::{IdentityStatus, revocation::PkiEnvironment}, }; -pub(crate) mod thumbprint; - #[derive(Debug, Clone)] pub struct WireIdentity { pub client_id: String, @@ -41,7 +40,7 @@ impl WireIdentityReader for x509_cert::Certificate { let (client_id, handle) = try_extract_san(&self.tbs_certificate)?; let (display_name, domain) = try_extract_subject(&self.tbs_certificate)?; let status = IdentityStatus::from_cert(self, env); - let thumbprint = thumbprint::try_compute_jwk_canonicalized_thumbprint(&self.tbs_certificate, hash_alg)?; + let thumbprint = try_compute_jwk_canonicalized_thumbprint(&self.tbs_certificate, hash_alg)?; Ok(WireIdentity { client_id, diff --git a/e2e-identity/src/acquisition/mod.rs b/e2e-identity/src/acquisition/mod.rs index 4be09d5907..7b71a3ab23 100644 --- a/e2e-identity/src/acquisition/mod.rs +++ b/e2e-identity/src/acquisition/mod.rs @@ -17,6 +17,9 @@ mod error; mod initial; mod oidc_challenge; +mod identity; +mod thumbprint; + #[derive(Debug)] pub struct X509CredentialConfiguration { pub acme_url: String, diff --git a/e2e-identity/src/acquisition/thumbprint.rs b/e2e-identity/src/acquisition/thumbprint.rs index 74f02939e1..de12fae134 100644 --- a/e2e-identity/src/acquisition/thumbprint.rs +++ b/e2e-identity/src/acquisition/thumbprint.rs @@ -5,7 +5,10 @@ use rusty_jwt_tools::{ }; use x509_cert::spki::SubjectPublicKeyInfoOwned; -use crate::acme::{RustyAcmeError, RustyAcmeResult, error::CertificateError}; +use crate::{ + acme::{RustyAcmeError, RustyAcmeResult}, + acquisition::error::CertificateError, +}; /// Used to compute the MLS thumbprint of a Basic Credential pub fn compute_raw_key_thumbprint( diff --git a/e2e-identity/src/lib.rs b/e2e-identity/src/lib.rs index 74de8ac028..db515ea487 100644 --- a/e2e-identity/src/lib.rs +++ b/e2e-identity/src/lib.rs @@ -136,9 +136,7 @@ pub mod pki_env; pub mod utils; pub mod x509_check; -pub use acme::{ - AcmeDirectory, RustyAcme, RustyAcmeError, WireIdentity, WireIdentityReader, compute_raw_key_thumbprint, -}; +pub use acme::{AcmeDirectory, RustyAcme, RustyAcmeError}; pub use acquisition::X509CredentialAcquisition; pub use e2e_identity::RustyE2eIdentity; pub use error::{E2eIdentityError, E2eIdentityResult}; From a52fd7f72d861d565d92d11b2b5966ce98761ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 16:45:41 +0200 Subject: [PATCH 07/28] chore: e2e-identity: add CertificateError::X509Check variant This is for errors coming from x509_check. --- e2e-identity/src/acquisition/error.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/e2e-identity/src/acquisition/error.rs b/e2e-identity/src/acquisition/error.rs index 2eb4cbeddd..198416034c 100644 --- a/e2e-identity/src/acquisition/error.rs +++ b/e2e-identity/src/acquisition/error.rs @@ -55,4 +55,7 @@ pub enum CertificateError { /// Advertised public key is not supported #[error("Advertised public key is not supported")] UnsupportedPublicKey, + /// X509Check error + #[error("transparent")] + X509Check(#[from] crate::x509_check::RustyX509CheckError), } From 0b9ea06f4a19efe0f0cb1acd6270bc9f708b3ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 16:45:41 +0200 Subject: [PATCH 08/28] chore: e2e-identity: use correct error types --- e2e-identity/src/acquisition/thumbprint.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/e2e-identity/src/acquisition/thumbprint.rs b/e2e-identity/src/acquisition/thumbprint.rs index de12fae134..aad71484e6 100644 --- a/e2e-identity/src/acquisition/thumbprint.rs +++ b/e2e-identity/src/acquisition/thumbprint.rs @@ -5,10 +5,7 @@ use rusty_jwt_tools::{ }; use x509_cert::spki::SubjectPublicKeyInfoOwned; -use crate::{ - acme::{RustyAcmeError, RustyAcmeResult}, - acquisition::error::CertificateError, -}; +use crate::{acme::RustyAcmeResult, acquisition::error::CertificateError}; /// Used to compute the MLS thumbprint of a Basic Credential pub fn compute_raw_key_thumbprint( @@ -36,7 +33,7 @@ pub(crate) fn try_compute_jwk_canonicalized_thumbprint( Ok(thumbprint.kid) } -fn try_into_jwk(spki: &SubjectPublicKeyInfoOwned) -> RustyAcmeResult { +fn try_into_jwk(spki: &SubjectPublicKeyInfoOwned) -> Result { use const_oid::db::{ rfc5912::{ID_EC_PUBLIC_KEY, SECP_256_R_1, SECP_384_R_1, SECP_521_R_1}, rfc8410::{ID_ED_448, ID_ED_25519}, @@ -49,9 +46,7 @@ fn try_into_jwk(spki: &SubjectPublicKeyInfoOwned) -> RustyAcmeResult { match (spki.algorithm.oid, params) { (ID_ED_25519, None) => Ok(Ed25519PublicKey::from_bytes(spki.subject_public_key.raw_bytes())?.try_into_jwk()?), - (ID_ED_448, None) => Err(RustyAcmeError::InvalidCertificate( - CertificateError::UnsupportedPublicKey, - )), + (ID_ED_448, None) => Err(CertificateError::UnsupportedPublicKey), (ID_EC_PUBLIC_KEY, Some(SECP_256_R_1)) => { Ok(ES256PublicKey::from_bytes(spki.subject_public_key.raw_bytes())?.try_into_jwk()?) } @@ -61,8 +56,6 @@ fn try_into_jwk(spki: &SubjectPublicKeyInfoOwned) -> RustyAcmeResult { (ID_EC_PUBLIC_KEY, Some(SECP_521_R_1)) => { Ok(ES512PublicKey::from_bytes(spki.subject_public_key.raw_bytes())?.try_into_jwk()?) } - _ => Err(RustyAcmeError::InvalidCertificate( - CertificateError::UnsupportedPublicKey, - )), + _ => Err(CertificateError::UnsupportedPublicKey), } } From 2926ec09030c441801982c241f812bb5ace4666f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 16:45:41 +0200 Subject: [PATCH 09/28] chore: e2e-identity: PKI environment is not optional when validating certs --- e2e-identity/src/x509_check/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e-identity/src/x509_check/mod.rs b/e2e-identity/src/x509_check/mod.rs index b962e85b01..d17610d4f7 100644 --- a/e2e-identity/src/x509_check/mod.rs +++ b/e2e-identity/src/x509_check/mod.rs @@ -61,9 +61,9 @@ pub enum IdentityStatus { } impl IdentityStatus { - pub fn from_cert(cert: &x509_cert::Certificate, env: Option<&PkiEnvironment>) -> Self { - match env.map(|e| e.validate_cert_and_revocation(cert)) { - Some(Err(RustyX509CheckError::CertValError(certval::Error::PathValidation(e)))) => match e { + pub fn from_cert(cert: &x509_cert::Certificate, env: &PkiEnvironment) -> Self { + match env.validate_cert_and_revocation(cert) { + Err(RustyX509CheckError::CertValError(certval::Error::PathValidation(e))) => match e { PathValidationStatus::InvalidNotAfterDate => IdentityStatus::Expired, PathValidationStatus::CertificateRevoked | PathValidationStatus::CertificateRevokedEndEntity From 3f2639da5714ca035f596c769a3d4f0bc5a4e0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 16:45:41 +0200 Subject: [PATCH 10/28] chore: e2e-identity: PKI environment is not optional when extracting identity --- e2e-identity/src/acquisition/identity.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e-identity/src/acquisition/identity.rs b/e2e-identity/src/acquisition/identity.rs index 1339a0806a..8f00b17332 100644 --- a/e2e-identity/src/acquisition/identity.rs +++ b/e2e-identity/src/acquisition/identity.rs @@ -23,7 +23,7 @@ pub struct WireIdentity { pub trait WireIdentityReader { /// Verifies a proof of identity, may it be a x509 certificate (or a Verifiable Presentation (later)). /// We do not verify anything else e.g. expiry, it is left to MLS implementation - fn extract_identity(&self, env: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult; + fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> RustyAcmeResult; /// returns the 'Not Before' claim which usually matches the creation timestamp fn extract_created_at(&self) -> RustyAcmeResult; @@ -33,7 +33,7 @@ pub trait WireIdentityReader { } impl WireIdentityReader for x509_cert::Certificate { - fn extract_identity(&self, env: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult { + fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> RustyAcmeResult { let serial_number = hex::encode(self.tbs_certificate.serial_number.as_bytes()); let not_before = self.tbs_certificate.validity.not_before.to_unix_duration().as_secs(); let not_after = self.tbs_certificate.validity.not_after.to_unix_duration().as_secs(); @@ -70,7 +70,7 @@ impl WireIdentityReader for x509_cert::Certificate { } impl WireIdentityReader for &[u8] { - fn extract_identity(&self, env: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult { + fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> RustyAcmeResult { x509_cert::Certificate::from_der(self)?.extract_identity(env, hash_alg) } @@ -84,7 +84,7 @@ impl WireIdentityReader for &[u8] { } impl WireIdentityReader for Vec { - fn extract_identity(&self, env: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult { + fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> RustyAcmeResult { self.as_slice().extract_identity(env, hash_alg) } From 9fa3e070ac0d3f257ad060f08967f98756b6ab86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 10:45:21 +0200 Subject: [PATCH 11/28] chore: e2e-identity: extend CertificateError with more variants It is not ideal that we're still depending on AcmeError, but the identity module needs it and while the error handling is a mess, we don't really want to rework it right now. --- e2e-identity/src/acquisition/error.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/e2e-identity/src/acquisition/error.rs b/e2e-identity/src/acquisition/error.rs index 198416034c..e43fd06645 100644 --- a/e2e-identity/src/acquisition/error.rs +++ b/e2e-identity/src/acquisition/error.rs @@ -22,6 +22,9 @@ pub enum Error { /// Given x509 certificate is invalid and does not follow Wire's format #[derive(Debug, thiserror::Error)] pub enum CertificateError { + /// Client ID is not in a valid format + #[error("Client ID is not in a valid format")] + InvalidClientId, /// ClientId does not match expected one #[error("ClientId does not match expected one")] ClientIdMismatch, @@ -58,4 +61,19 @@ pub enum CertificateError { /// X509Check error #[error("transparent")] X509Check(#[from] crate::x509_check::RustyX509CheckError), + /// DER error + #[error(transparent)] + Der(#[from] spki::der::Error), + /// UTF-8 error + #[error(transparent)] + Utf8(#[from] std::str::Utf8Error), + /// ACME error + #[error(transparent)] + Acme(#[from] crate::acme::RustyAcmeError), + /// JWT error + #[error(transparent)] + RustyJwtError(#[from] RustyJwtError), + /// jwt-simple error + #[error(transparent)] + JwtSimple(#[from] jwt_simple::Error), } From f93991087da03ad91ccd63df46fb1fe9d0006d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 10:48:59 +0200 Subject: [PATCH 12/28] chore: e2e-identity: identity: use CertificateError instead of RustyAcmeError --- e2e-identity/src/acquisition/identity.rs | 35 ++++++++++++------------ 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/e2e-identity/src/acquisition/identity.rs b/e2e-identity/src/acquisition/identity.rs index 8f00b17332..c3ac5402a2 100644 --- a/e2e-identity/src/acquisition/identity.rs +++ b/e2e-identity/src/acquisition/identity.rs @@ -2,11 +2,12 @@ use rusty_jwt_tools::prelude::{ClientId, HashAlgorithm, QualifiedHandle}; use x509_cert::der::Decode as _; use crate::{ - acme::RustyAcmeResult, acquisition::{error::CertificateError, thumbprint::try_compute_jwk_canonicalized_thumbprint}, x509_check::{IdentityStatus, revocation::PkiEnvironment}, }; +type Result = std::result::Result; + #[derive(Debug, Clone)] pub struct WireIdentity { pub client_id: String, @@ -23,17 +24,17 @@ pub struct WireIdentity { pub trait WireIdentityReader { /// Verifies a proof of identity, may it be a x509 certificate (or a Verifiable Presentation (later)). /// We do not verify anything else e.g. expiry, it is left to MLS implementation - fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> RustyAcmeResult; + fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> Result; /// returns the 'Not Before' claim which usually matches the creation timestamp - fn extract_created_at(&self) -> RustyAcmeResult; + fn extract_created_at(&self) -> Result; /// returns the 'Subject Public Key Info' claim - fn extract_public_key(&self) -> RustyAcmeResult>; + fn extract_public_key(&self) -> Result>; } impl WireIdentityReader for x509_cert::Certificate { - fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> RustyAcmeResult { + fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> Result { let serial_number = hex::encode(self.tbs_certificate.serial_number.as_bytes()); let not_before = self.tbs_certificate.validity.not_before.to_unix_duration().as_secs(); let not_after = self.tbs_certificate.validity.not_after.to_unix_duration().as_secs(); @@ -55,11 +56,11 @@ impl WireIdentityReader for x509_cert::Certificate { }) } - fn extract_created_at(&self) -> RustyAcmeResult { + fn extract_created_at(&self) -> Result { Ok(self.tbs_certificate.validity.not_before.to_unix_duration().as_secs()) } - fn extract_public_key(&self) -> RustyAcmeResult> { + fn extract_public_key(&self) -> Result> { Ok(self .tbs_certificate .subject_public_key_info @@ -70,39 +71,39 @@ impl WireIdentityReader for x509_cert::Certificate { } impl WireIdentityReader for &[u8] { - fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> RustyAcmeResult { + fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> Result { x509_cert::Certificate::from_der(self)?.extract_identity(env, hash_alg) } - fn extract_created_at(&self) -> RustyAcmeResult { + fn extract_created_at(&self) -> Result { x509_cert::Certificate::from_der(self)?.extract_created_at() } - fn extract_public_key(&self) -> RustyAcmeResult> { + fn extract_public_key(&self) -> Result> { x509_cert::Certificate::from_der(self)?.extract_public_key() } } impl WireIdentityReader for Vec { - fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> RustyAcmeResult { + fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> Result { self.as_slice().extract_identity(env, hash_alg) } - fn extract_created_at(&self) -> RustyAcmeResult { + fn extract_created_at(&self) -> Result { self.as_slice().extract_created_at() } - fn extract_public_key(&self) -> RustyAcmeResult> { + fn extract_public_key(&self) -> Result> { self.as_slice().extract_public_key() } } -fn try_extract_subject(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(String, String)> { +fn try_extract_subject(cert: &x509_cert::TbsCertificate) -> Result<(String, String)> { let mut display_name = None; let mut domain = None; let mut subjects = cert.subject.0.iter().flat_map(|n| n.0.iter()); - subjects.try_for_each(|s| -> RustyAcmeResult<()> { + subjects.try_for_each(|s| -> Result<()> { match s.oid { const_oid::db::rfc4519::ORGANIZATION_NAME => { domain = Some(std::str::from_utf8(s.value.value())?); @@ -121,7 +122,7 @@ fn try_extract_subject(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(Str } /// extract Subject Alternative Name to pick client-id & display name -fn try_extract_san(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(String, QualifiedHandle)> { +fn try_extract_san(cert: &x509_cert::TbsCertificate) -> Result<(String, QualifiedHandle)> { let extensions = cert.extensions.as_ref().ok_or(CertificateError::InvalidFormat)?; let san = extensions @@ -141,7 +142,7 @@ fn try_extract_san(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(String, x509_cert::ext::pkix::name::GeneralName::UniformResourceIdentifier(ia5_str) => Some(ia5_str.as_str()), _ => None, }) - .try_for_each(|name| -> RustyAcmeResult<()> { + .try_for_each(|name| -> Result<()> { // since both ClientId & handle are in the SAN we first try to parse the element as // a ClientId (since it's the most characterizable) and else fallback to a handle if let Ok(cid) = ClientId::try_from_uri(name) { From deb605c26a999bfb52bbbbdf227271efca8f2ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 10:48:59 +0200 Subject: [PATCH 13/28] chore: e2e-identity: thumbprint: use CertificateError instead of RustyAcmeError --- e2e-identity/src/acquisition/thumbprint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-identity/src/acquisition/thumbprint.rs b/e2e-identity/src/acquisition/thumbprint.rs index aad71484e6..17d8c9055e 100644 --- a/e2e-identity/src/acquisition/thumbprint.rs +++ b/e2e-identity/src/acquisition/thumbprint.rs @@ -27,7 +27,7 @@ pub fn compute_raw_key_thumbprint( pub(crate) fn try_compute_jwk_canonicalized_thumbprint( cert: &x509_cert::TbsCertificate, hash_alg: HashAlgorithm, -) -> RustyAcmeResult { +) -> Result { let jwk = try_into_jwk(&cert.subject_public_key_info)?; let thumbprint = JwkThumbprint::generate(&jwk, hash_alg)?; Ok(thumbprint.kid) From 9038231cf48fba6fad21568f85a7150e9049f1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 10:57:52 +0200 Subject: [PATCH 14/28] chore: e2e-identity: extend X509CredentialConfiguration with a domain field This is going to be used in certificate checks. --- e2e-identity/src/acquisition/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e-identity/src/acquisition/mod.rs b/e2e-identity/src/acquisition/mod.rs index 7b71a3ab23..cc71f9168c 100644 --- a/e2e-identity/src/acquisition/mod.rs +++ b/e2e-identity/src/acquisition/mod.rs @@ -29,6 +29,7 @@ pub struct X509CredentialConfiguration { pub display_name: String, pub client_id: ClientId, pub handle: String, + pub domain: String, pub team: Option, pub validity_period: std::time::Duration, } From 8a0f02978f8619eef447786d8153c94779bceb73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 10:18:47 +0200 Subject: [PATCH 15/28] chore: e2e-identity: move certificate checks out of the acme module They really belong in the acquisition and we want to make it obvious that they're being performed as part of the acquisition process, rather than them being implied. --- e2e-identity/src/acme/certificate.rs | 105 ++----------------------- e2e-identity/src/acquisition/checks.rs | 78 ++++++++++++++++++ e2e-identity/src/acquisition/mod.rs | 1 + 3 files changed, 87 insertions(+), 97 deletions(-) create mode 100644 e2e-identity/src/acquisition/checks.rs diff --git a/e2e-identity/src/acme/certificate.rs b/e2e-identity/src/acme/certificate.rs index 386e9ffaa1..73f3bac66b 100644 --- a/e2e-identity/src/acme/certificate.rs +++ b/e2e-identity/src/acme/certificate.rs @@ -1,13 +1,7 @@ -use rusty_jwt_tools::prelude::{ClientId, HashAlgorithm, JwsAlgorithm, Pem}; -use x509_cert::{Certificate, anchor::TrustAnchorChoice}; +use rusty_jwt_tools::prelude::{JwsAlgorithm, Pem}; +use x509_cert::{Certificate, der::Decode as _}; -use crate::{ - acme::{ - AcmeAccount, AcmeFinalize, AcmeJws, AcmeOrder, RustyAcme, RustyAcmeError, RustyAcmeResult, WireIdentityReader, - error::CertificateError, identifier::CanonicalIdentifier, - }, - x509_check::revocation::{PkiEnvironment, PkiEnvironmentParams}, -}; +use crate::acme::{AcmeAccount, AcmeFinalize, AcmeJws, AcmeOrder, RustyAcme, RustyAcmeResult}; impl RustyAcme { /// For fetching the generated certificate @@ -29,96 +23,13 @@ impl RustyAcme { } /// see [RFC 8555 Section 7.4.2](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2) - pub fn certificate_response( - response: String, - order: AcmeOrder, - hash_alg: HashAlgorithm, - env: Option<&PkiEnvironment>, - ) -> RustyAcmeResult>> { + pub fn certificate_response(response: String, order: AcmeOrder) -> RustyAcmeResult> { order.verify()?; let pems: Vec = pem::parse_many(response)?; - let intermediates = Self::extract_intermediates(&pems)?; - let env = env.and_then(|env| { - let trust_anchors = env.get_trust_anchors().unwrap_or_default(); - let trust_roots: Vec = trust_anchors.iter().map(|f| f.decoded_ta.clone()).collect(); - PkiEnvironment::init(PkiEnvironmentParams { - trust_roots: trust_roots.as_slice(), - intermediates: intermediates.as_slice(), - crls: &[], - time_of_interest: None, - }) - .ok() - }); - - pems.into_iter() - .enumerate() - .try_fold(vec![], move |mut acc, (i, cert_pem)| -> RustyAcmeResult>> { - // see https://datatracker.ietf.org/doc/html/rfc8555#section-11.4 - if cert_pem.tag() != "CERTIFICATE" { - return Err(RustyAcmeError::SmallstepImplementationError( - "Something other than x509 certificates was returned by the ACME server", - )); - } - use x509_cert::der::Decode as _; - let cert = x509_cert::Certificate::from_der(cert_pem.contents())?; - - PkiEnvironment::extract_ski_aki_from_cert(&cert)?; - - // only verify that leaf has the right identity fields - if i == 0 { - Self::verify_leaf_certificate(cert, &order.try_get_coalesce_identifier()?, hash_alg, env.as_ref())?; - } - acc.push(cert_pem.contents().to_vec()); - Ok(acc) - }) - } - - fn extract_intermediates(pems: &[pem::Pem]) -> RustyAcmeResult> { - use x509_cert::der::Decode as _; - pems.iter() - .skip(1) - .try_fold(vec![], |mut acc, pem| -> RustyAcmeResult> { - let cert = x509_cert::Certificate::from_der(pem.contents())?; - acc.push(cert); - Ok(acc) - }) - } - - /// Ensure that the generated certificate matches our expectations (i.e. that the acme server is configured the - /// right way) We verify that the fields in the certificate match the ones in the ACME order - fn verify_leaf_certificate( - cert: Certificate, - identifier: &CanonicalIdentifier, - hash_alg: HashAlgorithm, - env: Option<&PkiEnvironment>, - ) -> RustyAcmeResult<()> { - if let Some(env) = env { - env.validate_cert(&cert)?; - } - - // TODO: verify that cert is signed by enrollment.sign_kp - let cert_identity = cert.extract_identity(env, hash_alg)?; - - let invalid_client_id = - ClientId::try_from_qualified(&cert_identity.client_id)? != ClientId::try_from_uri(&identifier.client_id)?; - if invalid_client_id { - return Err(CertificateError::ClientIdMismatch)?; - } - - let invalid_display_name = cert_identity.display_name != identifier.display_name; - if invalid_display_name { - return Err(CertificateError::DisplayNameMismatch)?; - } - - let invalid_handle = cert_identity.handle != identifier.handle; - if invalid_handle { - return Err(CertificateError::HandleMismatch)?; - } - - let invalid_domain = cert_identity.domain != identifier.domain; - if invalid_domain { - return Err(CertificateError::DomainMismatch)?; + let mut certs = Vec::with_capacity(pems.len()); + for pem in pems { + certs.push(Certificate::from_der(pem.contents())?); } - Ok(()) + Ok(certs) } } diff --git a/e2e-identity/src/acquisition/checks.rs b/e2e-identity/src/acquisition/checks.rs new file mode 100644 index 0000000000..8f6fa7df02 --- /dev/null +++ b/e2e-identity/src/acquisition/checks.rs @@ -0,0 +1,78 @@ +use rusty_jwt_tools::prelude::{ClientId, Handle}; +use x509_cert::{Certificate, anchor::TrustAnchorChoice}; + +use super::X509CredentialConfiguration; +use crate::{ + acquisition::{error::CertificateError, identity::WireIdentityReader as _}, + pki_env::PkiEnvironment, + x509_check::revocation::{PkiEnvironment as RjtPkiEnvironment, PkiEnvironmentParams}, +}; + +pub(crate) async fn verify_cert_chain( + config: &X509CredentialConfiguration, + pki_env: &PkiEnvironment, + certs: &[Certificate], +) -> Result<(), CertificateError> { + let leaf = &certs[0]; + let intermediates = &certs[1..]; + + // TODO: this is ridiculous, once we have the "outer" PKI env, we should + // be certain that there is also the "inner", RjtPkiEnvironment one. This + // should be simplified once we drop RjtPkiEnvironment. + let trust_roots: Vec = match *pki_env.mls_pki_env_provider().borrow().await { + Some(ref pki_env) => { + let trust_anchors = pki_env.get_trust_anchors().unwrap_or_default(); + trust_anchors.iter().map(|f| f.decoded_ta.clone()).collect() + } + None => vec![], + }; + + let env = RjtPkiEnvironment::init(PkiEnvironmentParams { + trust_roots: trust_roots.as_slice(), + intermediates, + crls: &[], + time_of_interest: None, + })?; + + verify_leaf_certificate(config, &env, leaf)?; + + // see https://datatracker.ietf.org/doc/html/rfc8555#section-11.4 + RjtPkiEnvironment::extract_ski_aki_from_cert(leaf)?; + + Ok(()) +} + +/// Ensure that the generated certificate matches our expectations, i.e. that the fields in the +/// certificate match configuration values. +fn verify_leaf_certificate( + config: &X509CredentialConfiguration, + pki_env: &RjtPkiEnvironment, + cert: &Certificate, +) -> Result<(), CertificateError> { + pki_env.validate_cert(cert)?; + + // TODO: verify that cert is signed by enrollment.sign_kp + let cert_identity = cert.extract_identity(pki_env, config.hash_alg)?; + + let cert_id = + ClientId::try_from_qualified(&cert_identity.client_id).map_err(|_| CertificateError::InvalidClientId)?; + if cert_id != config.client_id { + return Err(CertificateError::ClientIdMismatch); + } + + if cert_identity.display_name != config.display_name { + return Err(CertificateError::DisplayNameMismatch); + } + + let handle = Handle::from(config.handle.as_ref()) + .try_to_qualified(&config.domain) + .expect("X509 configuration handle and domain must be valid"); + if cert_identity.handle != handle { + return Err(CertificateError::HandleMismatch); + } + + if cert_identity.domain != config.domain { + return Err(CertificateError::DomainMismatch); + } + Ok(()) +} diff --git a/e2e-identity/src/acquisition/mod.rs b/e2e-identity/src/acquisition/mod.rs index cc71f9168c..9ed90184b9 100644 --- a/e2e-identity/src/acquisition/mod.rs +++ b/e2e-identity/src/acquisition/mod.rs @@ -12,6 +12,7 @@ use crate::{ }, }; +mod checks; mod dpop_challenge; mod error; mod initial; From f352eca93921981dffd8466978d2323ec4c263ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 1 Apr 2026 11:35:29 +0200 Subject: [PATCH 16/28] chore: e2e-identity: hook the new verification code into the acquisition --- e2e-identity/src/acquisition/oidc_challenge.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/e2e-identity/src/acquisition/oidc_challenge.rs b/e2e-identity/src/acquisition/oidc_challenge.rs index 6c9830e48c..9ae383698a 100644 --- a/e2e-identity/src/acquisition/oidc_challenge.rs +++ b/e2e-identity/src/acquisition/oidc_challenge.rs @@ -1,4 +1,5 @@ use rusty_jwt_tools::{jwk_thumbprint::JwkThumbprint, prelude::Pem}; +use x509_cert::Certificate; use super::{Result, X509CredentialAcquisition, states}; use crate::{ @@ -12,7 +13,7 @@ impl X509CredentialAcquisition { /// Returns (signing keypair in PEM format, certificate chain). /// The first certificate in the chain is the end-entity certificate, /// i.e. the one certifying the public portion of the signing keypair. - pub async fn complete_oidc_challenge(self) -> Result<(Pem, Vec>)> { + pub async fn complete_oidc_challenge(self) -> Result<(Pem, Vec)> { let hooks = self.pki_env.hooks(); // Complete the OIDC challenge. @@ -68,7 +69,10 @@ impl X509CredentialAcquisition { .http_request(HttpMethod::Post, finalize.certificate.to_string(), headers, body) .await?; let response = String::from_utf8(response.body).map_err(|e| RustyAcmeError::from(e.utf8_error()))?; - let certificates = RustyAcme::certificate_response(response, self.data.order, self.config.hash_alg, None)?; + let certificates = RustyAcme::certificate_response(response, self.data.order)?; + + super::checks::verify_cert_chain(&self.config, &self.pki_env, &certificates).await?; + Ok((self.sign_kp, certificates)) } } From 334457728f425b54db1c0ee02144ffd72cc05562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 11:17:32 +0200 Subject: [PATCH 17/28] chore: keystore-dump: the old enrollment is going away --- keystore-dump/src/main.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/keystore-dump/src/main.rs b/keystore-dump/src/main.rs index 87dd057213..aa45d55ebd 100644 --- a/keystore-dump/src/main.rs +++ b/keystore-dump/src/main.rs @@ -27,7 +27,6 @@ async fn main() -> anyhow::Result<()> { }; use openmls::prelude::TlsDeserializeTrait; use serde::ser::{SerializeMap, Serializer}; - use wire_e2e_identity::legacy::E2eiEnrollment; let args = Args::parse(); @@ -101,14 +100,6 @@ async fn main() -> anyhow::Result<()> { .collect::>()?; json_map.serialize_entry("mls_keypackages", &keypackages)?; - let e2ei_enrollments: Vec = keystore - .load_all::() - .await? - .into_iter() - .map(|enrollment| serde_json::from_slice::(&enrollment.content)) - .collect::>()?; - json_map.serialize_entry("e2ei_enrollments", &e2ei_enrollments)?; - let pgroups: Vec = keystore .load_all::() .await? From b1e53138bcc74118e57794b78b56b2ad07c11567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 11:17:32 +0200 Subject: [PATCH 18/28] chore: e2e-identity: remove the old enrollment type And a bunch of other legacy code. --- e2e-identity/src/legacy/crypto.rs | 31 --- e2e-identity/src/legacy/mod.rs | 446 ------------------------------ e2e-identity/src/legacy/types.rs | 175 ------------ 3 files changed, 652 deletions(-) delete mode 100644 e2e-identity/src/legacy/crypto.rs delete mode 100644 e2e-identity/src/legacy/types.rs diff --git a/e2e-identity/src/legacy/crypto.rs b/e2e-identity/src/legacy/crypto.rs deleted file mode 100644 index 37dcf8737a..0000000000 --- a/e2e-identity/src/legacy/crypto.rs +++ /dev/null @@ -1,31 +0,0 @@ -use openmls_traits::types::{Ciphersuite, SignatureScheme}; -use zeroize::Zeroize; - -use crate::{ - JwsAlgorithm, - error::{E2eIdentityError, E2eIdentityResult}, - pki::PkiKeypair, -}; - -pub(crate) fn ciphersuite_to_jws_algo(cs: Ciphersuite) -> E2eIdentityResult { - match cs { - Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - | Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519 => Ok(JwsAlgorithm::Ed25519), - Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256 => Ok(JwsAlgorithm::P256), - Ciphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384 => Ok(JwsAlgorithm::P384), - Ciphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521 => Ok(JwsAlgorithm::P521), - Ciphersuite::MLS_256_DHKEMX448_AES256GCM_SHA512_Ed448 - | Ciphersuite::MLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448 => Err(E2eIdentityError::NotSupported), - } -} - -#[derive(Debug, serde::Serialize, serde::Deserialize, Zeroize, derive_more::From, derive_more::Deref)] -#[zeroize(drop)] -pub struct E2eiSignatureKeypair(Vec); - -impl E2eiSignatureKeypair { - pub fn try_new(sc: SignatureScheme, sk: Vec) -> E2eIdentityResult { - let keypair = PkiKeypair::new(sc, sk)?; - Ok(Self(keypair.signing_key_bytes())) - } -} diff --git a/e2e-identity/src/legacy/mod.rs b/e2e-identity/src/legacy/mod.rs index 4e6f6ae754..cb621525ee 100644 --- a/e2e-identity/src/legacy/mod.rs +++ b/e2e-identity/src/legacy/mod.rs @@ -1,20 +1,8 @@ -use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite}; -use zeroize::Zeroize as _; - -use crate::{E2eiAcmeAuthorization, RustyE2eIdentity}; - -pub mod crypto; pub mod device_status; pub mod id; -pub mod types; - -use crypto::{E2eiSignatureKeypair, ciphersuite_to_jws_algo}; -use id::{ClientId, QualifiedE2eiClientId}; use super::error::{E2eIdentityError as Error, E2eIdentityResult as Result}; -type Json = Vec; - #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Supporting struct for CRL registration result pub struct CrlRegistration { @@ -23,437 +11,3 @@ pub struct CrlRegistration { /// Optional expiration timestamp pub expiration: Option, } - -/// Wire end to end identity solution for fetching a x509 certificate which identifies a client. -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct E2eiEnrollment { - delegate: RustyE2eIdentity, - pub sign_sk: E2eiSignatureKeypair, - pub(super) client_id: String, - pub(super) display_name: String, - pub(super) handle: String, - pub(super) team: Option, - expiry: core::time::Duration, - directory: Option, - account: Option, - user_authz: Option, - device_authz: Option, - valid_order: Option, - finalize: Option, - pub(super) ciphersuite: Ciphersuite, - has_called_new_oidc_challenge_request: bool, -} - -impl std::ops::Deref for E2eiEnrollment { - type Target = RustyE2eIdentity; - - fn deref(&self) -> &Self::Target { - &self.delegate - } -} - -impl E2eiEnrollment { - /// Builds an instance holding private key material. This instance has to be used in the whole - /// enrollment process then dropped to clear secret key material. - /// - /// # Parameters - /// * `client_id` - client identifier e.g. `b7ac11a4-8f01-4527-af88-1c30885a7931:6add501bacd1d90e@example.com` - /// * `display_name` - human readable name displayed in the application e.g. `Smith, Alice M (QA)` - /// * `handle` - user handle e.g. `alice.smith.qa@example.com` - /// * `expiry_sec` - generated x509 certificate expiry in seconds - #[allow(clippy::too_many_arguments)] - pub fn try_new( - client_id: ClientId, - display_name: String, - handle: String, - team: Option, - expiry_sec: u32, - ciphersuite: Ciphersuite, - has_called_new_oidc_challenge_request: bool, - crypto: &impl OpenMlsCrypto, - ) -> Result { - let alg = ciphersuite_to_jws_algo(ciphersuite)?; - let sign_sk = Self::new_sign_key(crypto, ciphersuite)?; - - let client_id = QualifiedE2eiClientId::try_from(client_id.as_slice())?; - let client_id = String::try_from(client_id)?; - let expiry = core::time::Duration::from_secs(u64::from(expiry_sec)); - let delegate = RustyE2eIdentity::try_new(alg, sign_sk.clone())?; - Ok(Self { - delegate, - sign_sk, - client_id, - display_name, - handle, - team, - expiry, - directory: None, - account: None, - user_authz: None, - device_authz: None, - valid_order: None, - finalize: None, - ciphersuite, - has_called_new_oidc_challenge_request, - }) - } - - pub(crate) fn new_sign_key(crypto: &impl OpenMlsCrypto, ciphersuite: Ciphersuite) -> Result { - let (sk, _) = crypto.signature_key_gen(ciphersuite.signature_algorithm())?; - E2eiSignatureKeypair::try_new(ciphersuite.signature_algorithm(), sk) - } - - pub fn ciphersuite(&self) -> &Ciphersuite { - &self.ciphersuite - } - - /// Parses the response from `GET /acme/{provisioner-name}/directory`. - /// Use this [types::E2eiAcmeDirectory] in the next step to fetch the first nonce from the acme server. Use - /// [types::E2eiAcmeDirectory.new_nonce]. - /// - /// See [RFC 8555 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1) - /// - /// # Parameters - /// * `directory` - http response body - pub fn directory_response(&mut self, directory: Json) -> Result { - let directory = serde_json::from_slice(&directory[..])?; - let directory: types::E2eiAcmeDirectory = self.acme_directory_response(directory)?.into(); - self.directory = Some(directory.clone()); - Ok(directory) - } - - /// For creating a new acme account. This returns a signed JWS-alike request body to send to - /// `POST /acme/{provisioner-name}/new-account`. - /// - /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3). - /// - /// # Parameters - /// * `directory` - you got from [Self::directory_response] - /// * `previous_nonce` - you got from calling `HEAD {directory.new_nonce}` - pub fn new_account_request(&self, previous_nonce: String) -> Result { - let directory = self - .directory - .as_ref() - .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?; - let account = self.acme_new_account_request(&directory.try_into()?, previous_nonce)?; - let account = serde_json::to_vec(&account)?; - Ok(account) - } - - /// Parses the response from `POST /acme/{provisioner-name}/new-account`. - /// - /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3). - /// - /// # Parameters - /// * `account` - http response body - pub fn new_account_response(&mut self, account: Json) -> Result<()> { - let account = serde_json::from_slice(&account[..])?; - let account = self.acme_new_account_response(account)?; - self.account = Some(account); - Ok(()) - } - - /// Creates a new acme order for the handle (userId + display name) and the clientId. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/new-account` - pub fn new_order_request(&self, previous_nonce: String) -> Result { - let directory = self - .directory - .as_ref() - .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?; - let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment( - "You must first call 'newAccountResponse()'", - ))?; - let order = self.acme_new_order_request( - &self.display_name, - &self.client_id, - &self.handle, - self.expiry, - &directory.try_into()?, - account, - previous_nonce, - )?; - let order = serde_json::to_vec(&order)?; - Ok(order) - } - - /// Parses the response from `POST /acme/{provisioner-name}/new-order`. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `new_order` - http response body - pub fn new_order_response(&self, order: Json) -> Result { - let order = serde_json::from_slice(&order[..])?; - self.acme_new_order_response(order)?.try_into() - } - - /// Creates a new authorization request. - /// - /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5). - /// - /// # Parameters - /// * `url` - one of the URL in new order's authorizations (from [Self::new_order_response]) - /// * `account` - you got from [Self::new_account_response] - /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/new-order` (or from the - /// previous to this method if you are creating the second authorization) - pub fn new_authz_request(&self, url: String, previous_nonce: String) -> Result { - let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment( - "You must first call 'newAccountResponse()'", - ))?; - let authz = self.acme_new_authz_request(&url.parse()?, account, previous_nonce)?; - let authz = serde_json::to_vec(&authz)?; - Ok(authz) - } - - /// Parses the response from `POST /acme/{provisioner-name}/authz/{authz-id}` - /// - /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5). - /// - /// # Parameters - /// * `new_authz` - http response body - pub fn new_authz_response(&mut self, authz: Json) -> Result { - let authz = serde_json::from_slice(&authz[..])?; - let authz = self.acme_new_authz_response(authz)?; - match &authz { - E2eiAcmeAuthorization::User { .. } => self.user_authz = Some(authz.clone()), - E2eiAcmeAuthorization::Device { .. } => self.device_authz = Some(authz.clone()), - }; - authz.try_into() - } - - /// Generates a new client Dpop JWT token. It demonstrates proof of possession of the nonces - /// (from wire-server & acme server) and will be verified by the acme server when verifying the - /// challenge (in order to deliver a certificate). - /// - /// Then send it to - /// [`POST /clients/{id}/access-token`](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token) - /// on wire-server. - /// - /// # Parameters - /// * `expiry_secs` - of the client Dpop JWT. This should be equal to the grace period set in Team Management - /// * `backend_nonce` - you get by calling `GET /clients/token/nonce` on wire-server. See endpoint [definition](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/get_clients__client__nonce) - /// * `expiry` - token expiry - #[allow(clippy::too_many_arguments)] - pub fn create_dpop_token(&self, expiry_secs: u32, backend_nonce: String) -> Result { - let expiry = core::time::Duration::from_secs(expiry_secs as u64); - let authz = self - .device_authz - .as_ref() - .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?; - let challenge = match authz { - E2eiAcmeAuthorization::Device { challenge, .. } => challenge, - E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError), - }; - self.new_dpop_token( - &self.client_id, - self.display_name.as_str(), - challenge, - backend_nonce, - self.handle.as_str(), - self.team.clone(), - expiry, - ) - } - - /// Creates a new challenge request. - /// - /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1). - /// - /// # Parameters - /// * `access_token` - returned by wire-server from [this endpoint](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token) - /// * `dpop_challenge` - you found after [Self::new_authz_response] - /// * `account` - you got from [Self::new_account_response] - /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/authz/{authz-id}` - pub fn new_dpop_challenge_request(&self, access_token: String, previous_nonce: String) -> Result { - let authz = self - .device_authz - .as_ref() - .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?; - let challenge = match authz { - E2eiAcmeAuthorization::Device { challenge, .. } => challenge, - E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError), - }; - let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment( - "You must first call 'newAccountResponse()'", - ))?; - let challenge = self.acme_dpop_challenge_request(access_token, challenge, account, previous_nonce)?; - let challenge = serde_json::to_vec(&challenge)?; - Ok(challenge) - } - - /// Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}` for the DPoP challenge - /// - /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1). - /// - /// # Parameters - /// * `challenge` - http response body - pub fn new_dpop_challenge_response(&self, challenge: Json) -> Result<()> { - let challenge = serde_json::from_slice(&challenge[..])?; - self.acme_new_challenge_response(challenge) - } - - /// Creates a new challenge request. - /// - /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1). - /// - /// # Parameters - /// * `id_token` - you get back from Identity Provider - /// * `oidc_challenge` - you found after [Self::new_authz_response] - /// * `account` - you got from [Self::new_account_response] - /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/authz/{authz-id}` - pub fn new_oidc_challenge_request(&mut self, id_token: String, previous_nonce: String) -> Result { - let authz = self - .user_authz - .as_ref() - .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?; - let challenge = match authz { - E2eiAcmeAuthorization::User { challenge, .. } => challenge, - E2eiAcmeAuthorization::Device { .. } => return Err(Error::ImplementationError), - }; - let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment( - "You must first call 'newAccountResponse()'", - ))?; - let challenge = self.acme_oidc_challenge_request(id_token, challenge, account, previous_nonce)?; - let challenge = serde_json::to_vec(&challenge)?; - - self.has_called_new_oidc_challenge_request = true; - - Ok(challenge) - } - - /// Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}` for the OIDC challenge - /// - /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1). - /// - /// # Parameters - /// * `challenge` - http response body - pub fn new_oidc_challenge_response(&mut self, challenge: Json) -> Result<()> { - let challenge = serde_json::from_slice(&challenge[..])?; - self.acme_new_challenge_response(challenge)?; - - if !self.has_called_new_oidc_challenge_request { - return Err(Error::OutOfOrderEnrollment( - "You must first call 'new_oidc_challenge_request()'", - )); - } - - Ok(()) - } - - /// Verifies that the previous challenge has been completed. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `order_url` - `location` header from http response you got from [Self::new_order_response] - /// * `account` - you got from [Self::new_account_response] - /// * `previous_nonce` - `replay-nonce` response header from `POST - /// /acme/{provisioner-name}/challenge/{challenge-id}` - pub fn check_order_request(&self, order_url: String, previous_nonce: String) -> Result { - let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment( - "You must first call 'newAccountResponse()'", - ))?; - let order = self.acme_check_order_request(order_url.parse()?, account, previous_nonce)?; - let order = serde_json::to_vec(&order)?; - Ok(order) - } - - /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}`. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `order` - http response body - /// - /// # Returns - /// The finalize url to use with [Self::finalize_request] - pub fn check_order_response(&mut self, order: Json) -> Result { - let order = serde_json::from_slice(&order[..])?; - let valid_order = self.acme_check_order_response(order)?; - let finalize_url = valid_order.finalize_url.to_string(); - self.valid_order = Some(valid_order); - Ok(finalize_url) - } - - /// Final step before fetching the certificate. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `domains` - you want to generate a certificate for e.g. `["wire.com"]` - /// * `order` - you got from [Self::check_order_response] - /// * `account` - you got from [Self::new_account_response] - /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/order/{order-id}` - pub fn finalize_request(&mut self, previous_nonce: String) -> Result { - let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment( - "You must first call 'newAccountResponse()'", - ))?; - let order = self.valid_order.as_ref().ok_or(Error::OutOfOrderEnrollment( - "You must first call 'checkOrderResponse()'", - ))?; - let finalize = self.acme_finalize_request(order, account, previous_nonce)?; - let finalize = serde_json::to_vec(&finalize)?; - Ok(finalize) - } - - /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}/finalize`. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `finalize` - http response body - /// - /// # Returns - /// The certificate url to use with [Self::certificate_request] - pub fn finalize_response(&mut self, finalize: Json) -> Result { - let finalize = serde_json::from_slice(&finalize[..])?; - let finalize = self.acme_finalize_response(finalize)?; - let certificate_url = finalize.certificate_url.to_string(); - self.finalize = Some(finalize); - Ok(certificate_url) - } - - /// Creates a request for finally fetching the x509 certificate. - /// - /// See [RFC 8555 Section 7.4.2](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2). - /// - /// # Parameters - /// * `finalize` - you got from [Self::finalize_response] - /// * `account` - you got from [Self::new_account_response] - /// * `previous_nonce` - `replay-nonce` response header from `POST - /// /acme/{provisioner-name}/order/{order-id}/finalize` - pub fn certificate_request(&mut self, previous_nonce: String) -> Result { - let account = self.account.take().ok_or(Error::OutOfOrderEnrollment( - "You must first call 'newAccountResponse()'", - ))?; - let finalize = self - .finalize - .take() - .ok_or(Error::OutOfOrderEnrollment("You must first call 'finalizeResponse()'"))?; - let certificate = self.acme_x509_certificate_request(finalize, account, previous_nonce)?; - let certificate = serde_json::to_vec(&certificate)?; - Ok(certificate) - } - - pub async fn certificate_response( - &mut self, - certificate_chain: String, - env: &crate::x509_check::revocation::PkiEnvironment, - ) -> Result>> { - let order = self.valid_order.take().ok_or(Error::OutOfOrderEnrollment( - "You must first call 'checkOrderResponse()'", - ))?; - let certificates = self.acme_x509_certificate_response(certificate_chain, order, Some(env))?; - - // zeroize the private material - self.sign_sk.zeroize(); - self.delegate.sign_kp.zeroize(); - self.delegate.acme_kp.zeroize(); - - Ok(certificates) - } -} diff --git a/e2e-identity/src/legacy/types.rs b/e2e-identity/src/legacy/types.rs deleted file mode 100644 index 285160165b..0000000000 --- a/e2e-identity/src/legacy/types.rs +++ /dev/null @@ -1,175 +0,0 @@ -//! We only expose byte arrays through the FFI so we do all the conversions here - -use crate::error::{E2eIdentityError as Error, E2eIdentityResult as Result}; - -/// See [RFC 8555 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1) -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct E2eiAcmeDirectory { - /// For fetching a new nonce used in [crate::legacy::E2eiEnrollment::new_account_request] - pub new_nonce: String, - /// URL to call with [crate::legacy::E2eiEnrollment::new_account_request] - pub new_account: String, - /// URL to call with [crate::legacy::E2eiEnrollment::new_order_request] - pub new_order: String, - /// Not yet used - pub revoke_cert: String, -} - -impl From for E2eiAcmeDirectory { - fn from(directory: crate::AcmeDirectory) -> Self { - Self { - new_nonce: directory.new_nonce.to_string(), - new_account: directory.new_account.to_string(), - new_order: directory.new_order.to_string(), - revoke_cert: directory.revoke_cert.to_string(), - } - } -} - -impl TryFrom<&E2eiAcmeDirectory> for crate::AcmeDirectory { - type Error = Error; - - fn try_from(directory: &E2eiAcmeDirectory) -> Result { - Ok(Self { - new_nonce: directory.new_nonce.parse()?, - new_account: directory.new_account.parse()?, - new_order: directory.new_order.parse()?, - revoke_cert: directory.revoke_cert.parse()?, - }) - } -} - -/// Result of an order creation -/// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4) -#[derive(Debug, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct E2eiNewAcmeOrder { - /// Opaque raw json value - pub delegate: super::Json, - /// Authorizations to create with [crate::legacy::E2eiEnrollment::new_authz_request] - pub authorizations: Vec, -} - -impl TryFrom for E2eiNewAcmeOrder { - type Error = Error; - - fn try_from(new_order: crate::E2eiNewAcmeOrder) -> Result { - Ok(Self { - authorizations: new_order.authorizations.iter().map(url::Url::to_string).collect(), - delegate: serde_json::to_vec(&new_order.delegate)?, - }) - } -} - -impl TryFrom for crate::E2eiNewAcmeOrder { - type Error = Error; - - fn try_from(new_order: E2eiNewAcmeOrder) -> Result { - let authorizations = new_order - .authorizations - .iter() - .map(|a| a.parse().map_err(Error::UrlError)) - .collect::>>()? - .try_into() - .map_err(|_| Error::ImplementationError)?; - - Ok(Self { - authorizations, - delegate: serde_json::to_value(new_order.delegate)?, - }) - } -} - -/// Result of an authorization creation -/// see [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5) -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct E2eiNewAcmeAuthz { - /// DNS entry associated with those challenge - pub identifier: String, - /// ACME challenge + ACME key thumbprint - pub keyauth: Option, - /// ACME Challenge - pub challenge: E2eiAcmeChallenge, -} - -impl TryFrom for E2eiNewAcmeAuthz { - type Error = Error; - - fn try_from(authz: crate::E2eiAcmeAuthorization) -> Result { - Ok(match authz { - crate::E2eiAcmeAuthorization::User { - identifier, - keyauth, - challenge, - } => Self { - identifier, - keyauth: Some(keyauth), - challenge: challenge.try_into()?, - }, - crate::E2eiAcmeAuthorization::Device { identifier, challenge } => Self { - identifier, - keyauth: None, - challenge: challenge.try_into()?, - }, - }) - } -} - -impl TryFrom<&E2eiNewAcmeAuthz> for crate::E2eiAcmeAuthorization { - type Error = Error; - - fn try_from(authz: &E2eiNewAcmeAuthz) -> Result { - Ok(match &authz.keyauth { - None => Self::Device { - identifier: authz.identifier.clone(), - challenge: (&authz.challenge).try_into()?, - }, - Some(keyauth) => Self::User { - identifier: authz.identifier.clone(), - keyauth: keyauth.clone(), - challenge: (&authz.challenge).try_into()?, - }, - }) - } -} - -/// For creating a challenge -/// see [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1) -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct E2eiAcmeChallenge { - /// Opaque raw json value - pub delegate: super::Json, - /// URL to call for the acme server to complete the challenge - pub url: String, - /// Non-standard, Wire specific claim. Indicates the consumer from where it should get the challenge - /// proof. Either from wire-server "/access-token" endpoint in case of a DPoP challenge, or from - /// an OAuth token endpoint for an OIDC challenge - pub target: String, -} - -impl TryFrom for E2eiAcmeChallenge { - type Error = Error; - - fn try_from(chall: crate::E2eiAcmeChallenge) -> Result { - Ok(Self { - delegate: serde_json::to_vec(&chall.delegate)?, - url: chall.url.to_string(), - target: chall.target.to_string(), - }) - } -} - -impl TryFrom<&E2eiAcmeChallenge> for crate::E2eiAcmeChallenge { - type Error = Error; - - fn try_from(chall: &E2eiAcmeChallenge) -> Result { - Ok(Self { - delegate: serde_json::from_slice(&chall.delegate[..])?, - url: chall.url.parse()?, - target: chall.target.parse()?, - }) - } -} From 6e79a759dbbf0c50708fb0a3376567f48b6d5a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 11:17:32 +0200 Subject: [PATCH 19/28] chore: e2e-identity: remove RustyE2eIdentity and related code It is now unused. --- e2e-identity/src/e2e_identity.rs | 448 ------------------------------- e2e-identity/src/lib.rs | 2 - 2 files changed, 450 deletions(-) delete mode 100644 e2e-identity/src/e2e_identity.rs diff --git a/e2e-identity/src/e2e_identity.rs b/e2e-identity/src/e2e_identity.rs deleted file mode 100644 index 5a59286c8a..0000000000 --- a/e2e-identity/src/e2e_identity.rs +++ /dev/null @@ -1,448 +0,0 @@ -use jwt_simple::prelude::{ES256KeyPair, ES384KeyPair, ES512KeyPair, Ed25519KeyPair, Jwk}; -use rusty_jwt_tools::{ - jwk::TryIntoJwk, - jwk_thumbprint::JwkThumbprint, - prelude::{ClientId, Dpop, Handle, HashAlgorithm, Htm, JwsAlgorithm, Pem, RustyJwtTools}, -}; -use zeroize::Zeroize as _; - -use crate::{ - acme::{AcmeChallenge, AcmeDirectory, AcmeIdentifier, RustyAcme}, - error::E2eIdentityResult, - types::{ - E2eiAcmeAccount, E2eiAcmeAuthorization, E2eiAcmeChallenge, E2eiAcmeFinalize, E2eiAcmeOrder, E2eiNewAcmeOrder, - Json, - }, - x509_check::revocation::PkiEnvironment, -}; - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct RustyE2eIdentity { - pub sign_alg: JwsAlgorithm, - pub sign_kp: Pem, - pub hash_alg: HashAlgorithm, - pub acme_kp: Pem, - pub acme_jwk: Jwk, -} - -/// Enrollment flow. -impl RustyE2eIdentity { - /// Builds an instance holding private key material. This instance has to be used in the whole - /// enrollment process then dropped to clear secret key material. - /// - /// # Parameters - /// * `sign_alg` - Signature algorithm (only Ed25519 for now) - /// * `raw_sign_key` - Raw signature key as bytes - pub fn try_new(sign_alg: JwsAlgorithm, mut raw_sign_key: Vec) -> E2eIdentityResult { - let sign_kp = match sign_alg { - JwsAlgorithm::Ed25519 => Ed25519KeyPair::from_bytes(&raw_sign_key[..])?.to_pem(), - JwsAlgorithm::P256 => ES256KeyPair::from_bytes(&raw_sign_key[..])?.to_pem()?, - JwsAlgorithm::P384 => ES384KeyPair::from_bytes(&raw_sign_key[..])?.to_pem()?, - JwsAlgorithm::P521 => ES512KeyPair::from_bytes(&raw_sign_key[..])?.to_pem()?, - }; - let (acme_kp, acme_jwk) = match sign_alg { - JwsAlgorithm::Ed25519 => { - let kp = Ed25519KeyPair::generate(); - (kp.to_pem().into(), kp.public_key().try_into_jwk()?) - } - JwsAlgorithm::P256 => { - let kp = ES256KeyPair::generate(); - (kp.to_pem()?.into(), kp.public_key().try_into_jwk()?) - } - JwsAlgorithm::P384 => { - let kp = ES384KeyPair::generate(); - (kp.to_pem()?.into(), kp.public_key().try_into_jwk()?) - } - JwsAlgorithm::P521 => { - let kp = ES512KeyPair::generate(); - (kp.to_pem()?.into(), kp.public_key().try_into_jwk()?) - } - }; - // drop the private immediately since it already has been copied - raw_sign_key.zeroize(); - Ok(Self { - sign_alg, - sign_kp: sign_kp.into(), - hash_alg: HashAlgorithm::from(sign_alg), - acme_kp, - acme_jwk, - }) - } - - /// Parses the response from `GET /acme/{provisioner-name}/directory`. - /// Use this [AcmeDirectory] in the next step to fetch the first nonce from the acme server. Use - /// [AcmeDirectory::new_nonce]. - /// - /// See [RFC 8555 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1) - /// - /// # Parameters - /// * `directory` - http response body - pub fn acme_directory_response(&self, directory: Json) -> E2eIdentityResult { - let directory = RustyAcme::acme_directory_response(directory)?; - Ok(directory) - } - - /// For creating a new acme account. This returns a signed JWS-alike request body to send to - /// `POST /acme/{provisioner-name}/new-account`. - /// - /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3). - /// - /// # Parameters - /// * `directory` - you got from [Self::acme_directory_response] - /// * `previous_nonce` - you got from calling `HEAD {directory.new_nonce}` - pub fn acme_new_account_request( - &self, - directory: &AcmeDirectory, - previous_nonce: String, - ) -> E2eIdentityResult { - let acct_req = RustyAcme::new_account_request(directory, self.sign_alg, &self.acme_kp, previous_nonce)?; - Ok(serde_json::to_value(acct_req)?) - } - - /// Parses the response from `POST /acme/{provisioner-name}/new-account`. - /// - /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3). - /// - /// # Parameters - /// * `account` - http response body - pub fn acme_new_account_response(&self, account: Json) -> E2eIdentityResult { - RustyAcme::new_account_response(account)?.try_into() - } - - /// Creates a new acme order for the handle (userId + display name) and the clientId. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `display_name` - human readable name displayed in the application e.g. `Smith, Alice M (QA)` - /// * `domain` - DNS name of owning backend e.g. `example.com` - /// * `client_id` - client identifier with user b64Url encoded & clientId hex encoded e.g. - /// `NDUyMGUyMmY2YjA3NGU3NjkyZjE1NjJjZTAwMmQ2NTQ/6add501bacd1d90e@example.com` - /// * `handle` - user handle e.g. `alice.smith.qa@example.com` - /// * `expiry` - x509 generated certificate expiry - /// * `directory` - you got from [Self::acme_directory_response] - /// * `account` - you got from [Self::acme_new_account_response] - /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/new-account` - #[allow(clippy::too_many_arguments)] - pub fn acme_new_order_request( - &self, - display_name: &str, - client_id: &str, - handle: &str, - expiry: core::time::Duration, - directory: &AcmeDirectory, - account: &E2eiAcmeAccount, - previous_nonce: String, - ) -> E2eIdentityResult { - let account = account.clone().try_into()?; - let client_id = ClientId::try_from_qualified(client_id)?; - let order_req = RustyAcme::new_order_request( - display_name, - client_id, - &handle.into(), - expiry, - directory, - &account, - self.sign_alg, - &self.acme_kp, - previous_nonce, - )?; - Ok(serde_json::to_value(order_req)?) - } - - /// Parses the response from `POST /acme/{provisioner-name}/new-order`. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `new_order` - http response body - pub fn acme_new_order_response(&self, new_order: Json) -> E2eIdentityResult { - let new_order = RustyAcme::new_order_response(new_order)?; - let json_new_order = serde_json::to_vec(&new_order)?.into(); - Ok(E2eiNewAcmeOrder { - delegate: json_new_order, - authorizations: new_order.authorizations, - }) - } - - /// Creates a new authorization request. - /// - /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5). - /// - /// # Parameters - /// * `url` - one of the URL in new order's authorizations (from [Self::acme_new_order_response]) - /// * `account` - you got from [Self::acme_new_account_response] - /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/new-order` (or from the - /// previous to this method if you are creating the second authorization) - pub fn acme_new_authz_request( - &self, - url: &url::Url, - account: &E2eiAcmeAccount, - previous_nonce: String, - ) -> E2eIdentityResult { - let account = account.clone().try_into()?; - let authz_req = RustyAcme::new_authz_request(url, &account, self.sign_alg, &self.acme_kp, previous_nonce)?; - Ok(serde_json::to_value(authz_req)?) - } - - /// Parses the response from `POST /acme/{provisioner-name}/authz/{authz-id}` - /// - /// You then have to map the challenge from this authorization object. The `client_id_challenge` - /// will be the one with the `client_id_host` (you supplied to [Self::acme_new_order_request]) identifier, - /// the other will be your `handle_challenge`. - /// - /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5). - /// - /// # Parameters - /// * `new_authz` - http response body - pub fn acme_new_authz_response(&self, new_authz: Json) -> E2eIdentityResult { - let authz = serde_json::from_value(new_authz)?; - let authz = RustyAcme::new_authz_response(authz)?; - - let [challenge] = authz.challenges; - Ok(match authz.identifier { - AcmeIdentifier::WireappUser(_) => { - let thumbprint = JwkThumbprint::generate(&self.acme_jwk, self.hash_alg)?.kid; - let oidc_chall_token = &challenge.token; - let keyauth = format!("{oidc_chall_token}.{thumbprint}"); - E2eiAcmeAuthorization::User { - identifier: authz.identifier.to_json()?, - challenge: challenge.try_into()?, - keyauth, - } - } - AcmeIdentifier::WireappDevice(_) => E2eiAcmeAuthorization::Device { - identifier: authz.identifier.to_json()?, - challenge: challenge.try_into()?, - }, - }) - } - - /// Generates a new client Dpop JWT token. It demonstrates proof of possession of the nonces - /// (from wire-server & acme server) and will be verified by the acme server when verifying the - /// challenge (in order to deliver a certificate). - /// - /// Then send it to - /// [`POST /clients/{id}/access-token`](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token) - /// on wire-server. - /// - /// # Parameters - /// * `access_token_url` - backend endpoint where this token will be sent. Should be [this one](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token) - /// * `client_id` - client identifier with user b64Url encoded & clientId hex encoded e.g. - /// `NDUyMGUyMmY2YjA3NGU3NjkyZjE1NjJjZTAwMmQ2NTQ:6add501bacd1d90e@example.com` - /// * `dpop_challenge` - you found after [Self::acme_new_authz_response] - /// * `backend_nonce` - you get by calling `GET /clients/token/nonce` on wire-server. - /// * `handle` - user handle e.g. `alice.smith.qa@example.com` See endpoint [definition](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/get_clients__client__nonce) - /// * `expiry` - token expiry - #[allow(clippy::too_many_arguments)] - pub fn new_dpop_token( - &self, - client_id: &str, - display_name: &str, - dpop_challenge: &E2eiAcmeChallenge, - backend_nonce: String, - handle: &str, - team: Option, - expiry: core::time::Duration, - ) -> E2eIdentityResult { - let dpop_chall: AcmeChallenge = dpop_challenge.clone().try_into()?; - let audience = dpop_chall.url; - let client_id = ClientId::try_from_qualified(client_id)?; - let handle = Handle::from(handle).try_to_qualified(&client_id.domain)?; - let dpop = Dpop { - htm: Htm::Post, - htu: dpop_challenge.target.clone().into(), - challenge: dpop_chall.token.into(), - handle, - team: team.into(), - display_name: display_name.to_string(), - extra_claims: None, - }; - Ok(RustyJwtTools::generate_dpop_token( - dpop, - &client_id, - backend_nonce.into(), - audience, - expiry, - self.sign_alg, - &self.acme_kp, - )?) - } - - /// Creates a new challenge request. - /// - /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1). - /// - /// # Parameters - /// * `access_token` - returned by wire-server from [this endpoint](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token) - /// * `dpop_challenge` - you found after [Self::acme_new_authz_response] - /// * `account` - you got from [Self::acme_new_account_response] - /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/authz/{authz-id}` - pub fn acme_dpop_challenge_request( - &self, - access_token: String, - dpop_challenge: &E2eiAcmeChallenge, - account: &E2eiAcmeAccount, - previous_nonce: String, - ) -> E2eIdentityResult { - let account = account.clone().try_into()?; - let dpop_challenge: AcmeChallenge = dpop_challenge.clone().try_into()?; - let new_challenge_req = RustyAcme::dpop_chall_request( - access_token, - dpop_challenge, - &account, - self.sign_alg, - &self.acme_kp, - previous_nonce, - )?; - Ok(serde_json::to_value(new_challenge_req)?) - } - - /// Creates a new challenge request. - /// - /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1). - /// - /// # Parameters - /// * `id_token` - returned by Identity Provider - /// * `oidc_challenge` - you found after [Self::acme_new_authz_response] - /// * `account` - you got from [Self::acme_new_account_response] - /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/authz/{authz-id}` - pub fn acme_oidc_challenge_request( - &self, - id_token: String, - oidc_challenge: &E2eiAcmeChallenge, - account: &E2eiAcmeAccount, - previous_nonce: String, - ) -> E2eIdentityResult { - let account = account.clone().try_into()?; - let oidc_chall: AcmeChallenge = oidc_challenge.clone().try_into()?; - let new_challenge_req = RustyAcme::oidc_chall_request( - id_token, - &oidc_chall, - &account, - self.sign_alg, - &self.acme_kp, - previous_nonce, - )?; - Ok(serde_json::to_value(new_challenge_req)?) - } - - /// Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}`. - /// - /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1). - /// - /// # Parameters - /// * `challenge` - http response body - pub fn acme_new_challenge_response(&self, challenge: Json) -> E2eIdentityResult<()> { - let challenge = serde_json::from_value(challenge)?; - RustyAcme::new_chall_response(challenge)?; - Ok(()) - } - - /// Verifies that the previous challenge has been completed. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `order_url` - "location" header from http response you got from [Self::acme_new_order_response] - /// * `account` - you got from [Self::acme_new_account_response] - /// * `previous_nonce` - "replay-nonce" response header from `POST - /// /acme/{provisioner-name}/challenge/{challenge-id}` - pub fn acme_check_order_request( - &self, - order_url: url::Url, - account: &E2eiAcmeAccount, - previous_nonce: String, - ) -> E2eIdentityResult { - let account = account.clone().try_into()?; - let check_order_req = - RustyAcme::check_order_request(order_url, &account, self.sign_alg, &self.acme_kp, previous_nonce)?; - Ok(serde_json::to_value(check_order_req)?) - } - - /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}`. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `order` - http response body - pub fn acme_check_order_response(&self, order: Json) -> E2eIdentityResult { - RustyAcme::check_order_response(order)?.try_into() - } - - /// Final step before fetching the certificate. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `domains` - domains you want to generate a certificate for e.g. `["wire.com"]` - /// * `order` - you got from [Self::acme_check_order_response] - /// * `account` - you got from [Self::acme_new_account_response] - /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/order/{order-id}` - pub fn acme_finalize_request( - &self, - order: &E2eiAcmeOrder, - account: &E2eiAcmeAccount, - previous_nonce: String, - ) -> E2eIdentityResult { - let order = order.clone().try_into()?; - let account = account.clone().try_into()?; - let finalize_req = RustyAcme::finalize_req( - &order, - &account, - self.sign_alg, - &self.acme_kp, - &self.sign_kp, - previous_nonce, - )?; - Ok(serde_json::to_value(finalize_req)?) - } - - /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}/finalize`. - /// - /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4). - /// - /// # Parameters - /// * `finalize` - http response body - pub fn acme_finalize_response(&self, finalize: Json) -> E2eIdentityResult { - RustyAcme::finalize_response(finalize)?.try_into() - } - - /// Creates a request for finally fetching the x509 certificate. - /// - /// See [RFC 8555 Section 7.4.2](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2). - /// - /// # Parameters - /// * `domains` - domains you want to generate a certificate for e.g. `["wire.com"]` - /// * `order` - you got from [Self::acme_check_order_response] - /// * `account` - you got from [Self::acme_new_account_response] - /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/order/{order-id}` - pub fn acme_x509_certificate_request( - &self, - finalize: E2eiAcmeFinalize, - account: E2eiAcmeAccount, - previous_nonce: String, - ) -> E2eIdentityResult { - let finalize = finalize.try_into()?; - let account = account.try_into()?; - let certificate_req = - RustyAcme::certificate_req(&finalize, &account, self.sign_alg, &self.acme_kp, previous_nonce)?; - Ok(serde_json::to_value(certificate_req)?) - } - - /// Parses the response from `POST /acme/{provisioner-name}/certificate/{certificate-id}`. - /// - /// See [RFC 8555 Section 7.4.2](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2) - /// - /// # Parameters - /// * `response` - http string response body - pub fn acme_x509_certificate_response( - &self, - response: String, - order: E2eiAcmeOrder, - env: Option<&PkiEnvironment>, - ) -> E2eIdentityResult>> { - let order = order.try_into()?; - Ok(RustyAcme::certificate_response(response, order, self.hash_alg, env)?) - } -} diff --git a/e2e-identity/src/lib.rs b/e2e-identity/src/lib.rs index db515ea487..900f73d1fb 100644 --- a/e2e-identity/src/lib.rs +++ b/e2e-identity/src/lib.rs @@ -125,7 +125,6 @@ //! - [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) pub mod acquisition; -mod e2e_identity; mod error; mod types; @@ -138,7 +137,6 @@ pub mod x509_check; pub use acme::{AcmeDirectory, RustyAcme, RustyAcmeError}; pub use acquisition::X509CredentialAcquisition; -pub use e2e_identity::RustyE2eIdentity; pub use error::{E2eIdentityError, E2eIdentityResult}; pub use pki_env::{NewCrlDistributionPoints, PkiEnvironmentProvider}; #[cfg(feature = "builder")] From c83df46a01c52cedf70f791d143138901f9a5d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 11:17:32 +0200 Subject: [PATCH 20/28] chore: e2e-identity: expose WireIdentityReader It is used in crypto. Ideally it would be only used internally in e2e-identity, but we're not at that point yet. --- e2e-identity/src/acquisition/mod.rs | 2 +- e2e-identity/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e-identity/src/acquisition/mod.rs b/e2e-identity/src/acquisition/mod.rs index 9ed90184b9..89e5b0b604 100644 --- a/e2e-identity/src/acquisition/mod.rs +++ b/e2e-identity/src/acquisition/mod.rs @@ -18,7 +18,7 @@ mod error; mod initial; mod oidc_challenge; -mod identity; +pub mod identity; mod thumbprint; #[derive(Debug)] diff --git a/e2e-identity/src/lib.rs b/e2e-identity/src/lib.rs index 900f73d1fb..913c382ef7 100644 --- a/e2e-identity/src/lib.rs +++ b/e2e-identity/src/lib.rs @@ -136,7 +136,7 @@ pub mod utils; pub mod x509_check; pub use acme::{AcmeDirectory, RustyAcme, RustyAcmeError}; -pub use acquisition::X509CredentialAcquisition; +pub use acquisition::{X509CredentialAcquisition, identity::WireIdentityReader}; pub use error::{E2eIdentityError, E2eIdentityResult}; pub use pki_env::{NewCrlDistributionPoints, PkiEnvironmentProvider}; #[cfg(feature = "builder")] From 5fff9ba138a951fceab297da74fa6cbd2d56359a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 11:17:32 +0200 Subject: [PATCH 21/28] chore: e2e-identity: expose compute_raw_key_thumbprint --- e2e-identity/src/acquisition/mod.rs | 2 +- e2e-identity/src/lib.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/e2e-identity/src/acquisition/mod.rs b/e2e-identity/src/acquisition/mod.rs index 89e5b0b604..ed0f0a5baa 100644 --- a/e2e-identity/src/acquisition/mod.rs +++ b/e2e-identity/src/acquisition/mod.rs @@ -19,7 +19,7 @@ mod initial; mod oidc_challenge; pub mod identity; -mod thumbprint; +pub mod thumbprint; #[derive(Debug)] pub struct X509CredentialConfiguration { diff --git a/e2e-identity/src/lib.rs b/e2e-identity/src/lib.rs index 913c382ef7..6dfaf9d47c 100644 --- a/e2e-identity/src/lib.rs +++ b/e2e-identity/src/lib.rs @@ -136,7 +136,11 @@ pub mod utils; pub mod x509_check; pub use acme::{AcmeDirectory, RustyAcme, RustyAcmeError}; -pub use acquisition::{X509CredentialAcquisition, identity::WireIdentityReader}; +pub use acquisition::{ + X509CredentialAcquisition, + identity::{WireIdentity, WireIdentityReader}, + thumbprint::compute_raw_key_thumbprint, +}; pub use error::{E2eIdentityError, E2eIdentityResult}; pub use pki_env::{NewCrlDistributionPoints, PkiEnvironmentProvider}; #[cfg(feature = "builder")] From cbb0cea302294be3ebefa3154387856b594ecaa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 11:17:32 +0200 Subject: [PATCH 22/28] test: e2e-identity: make rand_client_id take a domain parameter --- e2e-identity/tests/utils/mod.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/e2e-identity/tests/utils/mod.rs b/e2e-identity/tests/utils/mod.rs index 0a424cd1f4..e8da40a9aa 100644 --- a/e2e-identity/tests/utils/mod.rs +++ b/e2e-identity/tests/utils/mod.rs @@ -31,13 +31,8 @@ pub(crate) fn rand_base64_str(size: usize) -> String { base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(rand_str(size)) } -pub(crate) fn rand_client_id() -> ClientId { - ClientId::try_new( - uuid::Uuid::new_v4().to_string(), - rand::random::(), - "wire.localhost", - ) - .unwrap() +pub(crate) fn rand_client_id(domain: &str) -> ClientId { + ClientId::try_new(uuid::Uuid::new_v4().to_string(), rand::random::(), domain).unwrap() } pub(crate) fn scrap_login(html: String) -> String { From c3ee4372493a4717764684e20840e512554c9273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 11:17:32 +0200 Subject: [PATCH 23/28] test: e2e-identity: specify the domain centrally And add it to the credential configuration. --- e2e-identity/tests/e2e.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/e2e-identity/tests/e2e.rs b/e2e-identity/tests/e2e.rs index d6a322ea9f..ad29f545cf 100644 --- a/e2e-identity/tests/e2e.rs +++ b/e2e-identity/tests/e2e.rs @@ -156,13 +156,14 @@ async fn prepare_pki_env_and_config( .to_pem(), }; + let domain = "wire.localhost".to_string(); let ca_cfg = CaCfg { sign_key: wire_server_pubkey.to_string(), issuer, audience: "wireapp".to_string(), discovery_base_url, dpop_target_uri: Some(dpop_target_uri), - domain: "wire.localhost".to_string(), + domain: domain.clone(), host: format!("{}.stepca", rand_str(6).to_lowercase()), }; @@ -177,7 +178,7 @@ async fn prepare_pki_env_and_config( ctx_store_http_client(&dns_mappings); - let client_id = rand_client_id(); + let client_id = rand_client_id(&domain); let device_id = format!("{:x}", client_id.device_id); let config = X509CredentialConfiguration { @@ -188,6 +189,7 @@ async fn prepare_pki_env_and_config( display_name: "Alice Smith".into(), handle: "alice_wire".into(), client_id: client_id.clone(), + domain: domain.clone(), team: Some("team".into()), validity_period: std::time::Duration::from_hours(1), }; @@ -196,7 +198,7 @@ async fn prepare_pki_env_and_config( "backend-kp": wire_server_keypair, "hash-alg": config.hash_alg.to_string(), "wire-server-uri": format!("{wire_server_uri}/clients/{}/access-token", device_id), - "handle": Handle::from(config.handle.clone()).try_to_qualified(&client_id.domain).unwrap(), + "handle": Handle::from(config.handle.clone()).try_to_qualified(&domain).unwrap(), "display_name": config.display_name, "team": config.team.as_ref().unwrap(), }); From e044886ff9e139a1c9ad73f32606988fde0c5054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 11:17:32 +0200 Subject: [PATCH 24/28] test: e2e-identity: add PKI environment to identity tests It is no longer optional. --- e2e-identity/src/acquisition/identity.rs | 28 +++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/e2e-identity/src/acquisition/identity.rs b/e2e-identity/src/acquisition/identity.rs index c3ac5402a2..da1eb58de3 100644 --- a/e2e-identity/src/acquisition/identity.rs +++ b/e2e-identity/src/acquisition/identity.rs @@ -160,6 +160,7 @@ fn try_extract_san(cert: &x509_cert::TbsCertificate) -> Result<(String, Qualifie #[cfg(test)] mod tests { + use rstest::rstest; use wasm_bindgen_test::*; use super::*; @@ -197,13 +198,20 @@ EoOKgYLyKNa24aewZZObydD+k0hFs4iKddICIQDf70uv+h0tHw/WNf15mZ8NGkJm OfqfZA1YMtN5NLz/AA== -----END CERTIFICATE-----"#; - #[test] + #[rstest::fixture] + fn pki_env() -> PkiEnvironment { + let mut env = PkiEnvironment::init(Default::default()).unwrap(); + env.refresh_time_of_interest().unwrap(); + env + } + + #[rstest] #[wasm_bindgen_test] - fn should_find_claims_in_x509() { + fn should_find_claims_in_x509(pki_env: PkiEnvironment) { let cert_der = pem::parse(CERT).unwrap(); let identity = cert_der .contents() - .extract_identity(None, HashAlgorithm::SHA256) + .extract_identity(&pki_env, HashAlgorithm::SHA256) .unwrap(); assert_eq!(&identity.client_id, "obakjPOHQ2CkNb0rOrNM3A:ba54e8ace8b4c90d@wire.com"); @@ -234,13 +242,13 @@ OfqfZA1YMtN5NLz/AA== ); } - #[test] + #[rstest] #[wasm_bindgen_test] - fn should_have_valid_status() { + fn should_have_valid_status(pki_env: PkiEnvironment) { let cert_der = pem::parse(CERT).unwrap(); let identity = cert_der .contents() - .extract_identity(None, HashAlgorithm::SHA256) + .extract_identity(&pki_env, HashAlgorithm::SHA256) .unwrap(); assert_eq!(&identity.status, &IdentityStatus::Valid); @@ -249,18 +257,18 @@ OfqfZA1YMtN5NLz/AA== env.refresh_time_of_interest().unwrap(); let identity = cert_der .contents() - .extract_identity(Some(&env), HashAlgorithm::SHA256) + .extract_identity(&env, HashAlgorithm::SHA256) .unwrap(); assert_eq!(&identity.status, &IdentityStatus::Expired); } - #[test] + #[rstest] #[wasm_bindgen_test] - fn should_have_thumbprint() { + fn should_have_thumbprint(pki_env: PkiEnvironment) { let cert_der = pem::parse(CERT).unwrap(); let identity = cert_der .contents() - .extract_identity(None, HashAlgorithm::SHA256) + .extract_identity(&pki_env, HashAlgorithm::SHA256) .unwrap(); assert!(!identity.thumbprint.is_empty()); } From 76bcb3e133df97d79013f038ee3ec0ee3c175d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 2 Apr 2026 11:17:32 +0200 Subject: [PATCH 25/28] test: e2e-identity: we can't get a valid status because we don't have a trust anchor In this case, the status is going to be revoked, which is wrong. So just test for expiration of the second certificate. --- e2e-identity/src/acquisition/identity.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/e2e-identity/src/acquisition/identity.rs b/e2e-identity/src/acquisition/identity.rs index da1eb58de3..21ede3c7ac 100644 --- a/e2e-identity/src/acquisition/identity.rs +++ b/e2e-identity/src/acquisition/identity.rs @@ -164,7 +164,6 @@ mod tests { use wasm_bindgen_test::*; use super::*; - use crate::x509_check::revocation::PkiEnvironmentParams; wasm_bindgen_test_configure!(run_in_browser); @@ -244,20 +243,11 @@ OfqfZA1YMtN5NLz/AA== #[rstest] #[wasm_bindgen_test] - fn should_have_valid_status(pki_env: PkiEnvironment) { - let cert_der = pem::parse(CERT).unwrap(); - let identity = cert_der - .contents() - .extract_identity(&pki_env, HashAlgorithm::SHA256) - .unwrap(); - assert_eq!(&identity.status, &IdentityStatus::Valid); - + fn should_have_revoked_status(pki_env: PkiEnvironment) { let cert_der = pem::parse(CERT_EXPIRED).unwrap(); - let mut env = PkiEnvironment::init(PkiEnvironmentParams::default()).unwrap(); - env.refresh_time_of_interest().unwrap(); let identity = cert_der .contents() - .extract_identity(&env, HashAlgorithm::SHA256) + .extract_identity(&pki_env, HashAlgorithm::SHA256) .unwrap(); assert_eq!(&identity.status, &IdentityStatus::Expired); } From 8c2b787a871306bd3f08ffdb5a78dfca778c63b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Tue, 7 Apr 2026 11:46:06 +0200 Subject: [PATCH 26/28] chore: crypto: add MissingPKIEnvironment variant to the credential error type This is going to be used in cases where an operation that requires a PKI environment is attempted, but no PKI environment is set. --- crypto/src/mls/credential/error.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crypto/src/mls/credential/error.rs b/crypto/src/mls/credential/error.rs index 4bfdbf794a..4c661cd89b 100644 --- a/crypto/src/mls/credential/error.rs +++ b/crypto/src/mls/credential/error.rs @@ -15,6 +15,8 @@ pub enum Error { InvalidIdentity, #[error("No credential for the given public key ({0:?}) was found in this database")] CredentialNotFound(SignaturePublicKey), + #[error("missing PKI environment")] + MissingPKIEnvironment, /// Unsupported credential type. /// /// Supported credential types: From 58b87803f171d0fbdef92b2c6fdf0f6b1267132c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Tue, 7 Apr 2026 11:49:05 +0200 Subject: [PATCH 27/28] chore: crypto: provide PKI environment to extract_identity If we're dealing with x509 credentials, the PKI must be provided, otherwise extract_identity will return an error. Previously this was not the case. It is unclear whether we even need WireIdentity in its current form, and, ideally, we should consider reworking it, but for now this should suffice. The whole thing around accessing the "inner" PKI environment is quite inelegant, due to a bunch of async locks. --- crypto/src/mls/conversation/own_commit.rs | 4 ++- .../mls/conversation/pending_conversation.rs | 19 ++++++++-- crypto/src/mls/credential/ext.rs | 1 + crypto/src/mls/credential/x509.rs | 8 +++-- crypto/src/mls/mod.rs | 3 +- crypto/src/mls/session/e2e_identity.rs | 10 +++--- crypto/src/mls/session/identifier.rs | 7 ++-- crypto/src/proteus.rs | 3 +- crypto/src/test_utils/context.rs | 13 +++++-- crypto/src/test_utils/mod.rs | 36 +++++++++++++------ .../src/test_utils/test_conversation/mod.rs | 11 ++++-- 11 files changed, 85 insertions(+), 30 deletions(-) diff --git a/crypto/src/mls/conversation/own_commit.rs b/crypto/src/mls/conversation/own_commit.rs index be3a6d18d0..25be463712 100644 --- a/crypto/src/mls/conversation/own_commit.rs +++ b/crypto/src/mls/conversation/own_commit.rs @@ -2,6 +2,7 @@ use core_crypto_keystore::Database; use openmls::prelude::{ ConfirmationTag, ContentType, CredentialWithKey, FramedContentBodyIn, MlsMessageIn, MlsMessageInBody, Sender, }; +use openmls_traits::OpenMlsCryptoProvider as _; use super::{Error, Result}; use crate::{ @@ -98,8 +99,9 @@ impl MlsConversation { credential: own_leaf.credential().clone(), signature_key: own_leaf.signature_key().clone(), }; + let provider = client.crypto_provider.authentication_service(); let identity = own_leaf_credential_with_key - .extract_identity(self.ciphersuite(), None) + .extract_identity(self.ciphersuite(), provider.borrow().await.as_ref()) .map_err(RecursiveError::mls_credential("extracting identity"))?; Ok(MlsConversationDecryptMessage { diff --git a/crypto/src/mls/conversation/pending_conversation.rs b/crypto/src/mls/conversation/pending_conversation.rs index 752c62214a..dbf7bc0094 100644 --- a/crypto/src/mls/conversation/pending_conversation.rs +++ b/crypto/src/mls/conversation/pending_conversation.rs @@ -184,9 +184,22 @@ impl PendingConversation { credential: own_leaf.credential().clone(), signature_key: own_leaf.signature_key().clone(), }; - let identity = own_leaf_credential_with_key - .extract_identity(conversation.ciphersuite(), None) - .map_err(RecursiveError::mls_credential("extracting identity"))?; + let pki_env = self + .context + .pki_environment_option() + .await + .map_err(RecursiveError::transaction("getting PKI environment"))?; + let identity = match pki_env { + Some(pki_env) => { + let provider = pki_env.mls_pki_env_provider(); + own_leaf_credential_with_key + .extract_identity(conversation.ciphersuite(), provider.borrow().await.as_ref()) + .map_err(RecursiveError::mls_credential("extracting identity"))? + } + None => own_leaf_credential_with_key + .extract_identity(conversation.ciphersuite(), None) + .map_err(RecursiveError::mls_credential("extracting identity"))?, + }; Ok(MlsConversationDecryptMessage { app_msg: None, diff --git a/crypto/src/mls/credential/ext.rs b/crypto/src/mls/credential/ext.rs index 63945fb109..c775f29037 100644 --- a/crypto/src/mls/credential/ext.rs +++ b/crypto/src/mls/credential/ext.rs @@ -115,6 +115,7 @@ impl CredentialExt for openmls::prelude::Certificate { cs: Ciphersuite, env: Option<&wire_e2e_identity::x509_check::revocation::PkiEnvironment>, ) -> Result { + let env = env.ok_or_else(|| Error::MissingPKIEnvironment)?; let leaf = self.certificates.first().ok_or(Error::InvalidIdentity)?; let leaf = leaf.as_slice(); use wire_e2e_identity::WireIdentityReader as _; diff --git a/crypto/src/mls/credential/x509.rs b/crypto/src/mls/credential/x509.rs index a69cb4319f..05377828ac 100644 --- a/crypto/src/mls/credential/x509.rs +++ b/crypto/src/mls/credential/x509.rs @@ -69,7 +69,11 @@ impl fmt::Debug for CertificateBundle { impl CertificateBundle { /// Reads the client_id from the leaf certificate - pub fn get_client_id(&self) -> Result { + pub fn get_client_id( + &self, + env: Option<&wire_e2e_identity::x509_check::revocation::PkiEnvironment>, + ) -> Result { + let env = env.ok_or_else(|| Error::MissingPKIEnvironment)?; let leaf = self.certificate_chain.first().ok_or(Error::InvalidIdentity)?; let hash_alg = match self.signature_scheme { @@ -79,7 +83,7 @@ impl CertificateBundle { }; let identity = leaf - .extract_identity(None, hash_alg) + .extract_identity(env, hash_alg) .map_err(|_| Error::InvalidIdentity)?; use wire_e2e_identity::legacy::id as legacy_id; diff --git a/crypto/src/mls/mod.rs b/crypto/src/mls/mod.rs index c55c6696c7..9995f5d73f 100644 --- a/crypto/src/mls/mod.rs +++ b/crypto/src/mls/mod.rs @@ -117,8 +117,9 @@ mod tests { CertificateBundle::rand_identifier(&session_id, &[x509_test_chain.find_local_intermediate_ca()]) } }; + let provider = cc.get_pki_environment().await.unwrap().mls_pki_env_provider(); let session_id = identifier - .get_id() + .get_id(provider.borrow().await.as_ref()) .expect("get session_id from identifier") .into_owned(); context diff --git a/crypto/src/mls/session/e2e_identity.rs b/crypto/src/mls/session/e2e_identity.rs index ed05b903d3..624c51cda0 100644 --- a/crypto/src/mls/session/e2e_identity.rs +++ b/crypto/src/mls/session/e2e_identity.rs @@ -119,6 +119,11 @@ impl Session { _credential_type: CredentialType, env: Option<&wire_e2e_identity::x509_check::revocation::PkiEnvironment>, ) -> E2eiConversationState { + let env = match env { + Some(e) => e, + None => return E2eiConversationState::NotEnabled, + }; + let mut is_e2ei = false; let mut state = E2eiConversationState::Verified; @@ -138,10 +143,7 @@ impl Session { use openmls_x509_credential::X509Ext as _; let is_time_valid = cert.is_time_valid().unwrap_or(false); let is_time_invalid = !is_time_valid; - let is_revoked_or_invalid = env - .map(|e| e.validate_cert_and_revocation(&cert).is_err()) - .unwrap_or(false); - + let is_revoked_or_invalid = env.validate_cert_and_revocation(&cert).is_err(); let is_invalid = invalid_identity || is_time_invalid || is_revoked_or_invalid; if is_invalid { state = E2eiConversationState::NotVerified; diff --git a/crypto/src/mls/session/identifier.rs b/crypto/src/mls/session/identifier.rs index 3c7d0d66a7..3ce21c96d6 100644 --- a/crypto/src/mls/session/identifier.rs +++ b/crypto/src/mls/session/identifier.rs @@ -19,7 +19,10 @@ pub enum ClientIdentifier { impl ClientIdentifier { /// Extract the unique [ClientId] from an identifier. Use with parsimony as, in case of a x509 /// certificate this leads to parsing the certificate - pub fn get_id(&self) -> Result> { + pub fn get_id( + &self, + env: Option<&wire_e2e_identity::x509_check::revocation::PkiEnvironment>, + ) -> Result> { match self { ClientIdentifier::Basic(id) => Ok(std::borrow::Cow::Borrowed(id)), ClientIdentifier::X509(certs) => { @@ -28,7 +31,7 @@ impl ClientIdentifier { // that's not a getter's job let cert = certs.values().next().ok_or(Error::NoX509CertificateBundle)?; let id = cert - .get_client_id() + .get_client_id(env) .map_err(RecursiveError::mls_credential("getting client id"))?; Ok(std::borrow::Cow::Owned(id)) } diff --git a/crypto/src/proteus.rs b/crypto/src/proteus.rs index f1951fb212..62ed7dd015 100644 --- a/crypto/src/proteus.rs +++ b/crypto/src/proteus.rs @@ -645,8 +645,9 @@ mod tests { CertificateBundle::rand_identifier(&session_id, &[x509_test_chain.find_local_intermediate_ca()]) } }; + let provider = cc.get_pki_environment().await.unwrap().mls_pki_env_provider(); let session_id = identifier - .get_id() + .get_id(provider.borrow().await.as_ref()) .expect("Getting session id from identifier") .into_owned(); transaction.mls_init(session_id, transport).await.unwrap(); diff --git a/crypto/src/test_utils/context.rs b/crypto/src/test_utils/context.rs index e991d0db78..67f4411276 100644 --- a/crypto/src/test_utils/context.rs +++ b/crypto/src/test_utils/context.rs @@ -230,7 +230,14 @@ impl SessionContext { if let openmls::prelude::MlsCredentialType::X509(certificate) = &expected_credential.mls_credential().mls_credential() { - let mls_identity = certificate.extract_identity(case.ciphersuite(), None).unwrap(); + let session = self.session().await; + let provider = session.crypto_provider; + let guard = provider.authentication_service().borrow().await; + let env = match guard.as_ref() { + Some(env) => env, + None => unreachable!("PKI environment must be set"), + }; + let mls_identity = certificate.extract_identity(case.ciphersuite(), Some(env)).unwrap(); let mls_client_id = mls_identity.client_id.as_bytes(); let decrypted_identity = &decrypted.identity; @@ -238,7 +245,7 @@ impl SessionContext { let leaf: Vec = certificate.certificates.first().unwrap().clone().into(); let identity = leaf .as_slice() - .extract_identity(None, case.ciphersuite().e2ei_hash_alg()) + .extract_identity(env, case.ciphersuite().e2ei_hash_alg()) .unwrap(); let identity = WireIdentity::try_from((identity, leaf.as_slice())).unwrap(); @@ -263,7 +270,7 @@ impl SessionContext { ); let chain = x509_cert::Certificate::load_pem_chain(decrypted_x509_identity.certificate.as_bytes()).unwrap(); let leaf = chain.first().unwrap(); - let cert_identity = leaf.extract_identity(None, case.ciphersuite().e2ei_hash_alg()).unwrap(); + let cert_identity = leaf.extract_identity(env, case.ciphersuite().e2ei_hash_alg()).unwrap(); let cert_identity = WireIdentity::try_from((cert_identity, leaf.to_der().unwrap().as_slice())).unwrap(); assert_eq!(cert_identity.client_id, identity.client_id); diff --git a/crypto/src/test_utils/mod.rs b/crypto/src/test_utils/mod.rs index 4da6ebff75..ac3ec00a3b 100644 --- a/crypto/src/test_utils/mod.rs +++ b/crypto/src/test_utils/mod.rs @@ -18,6 +18,7 @@ use std::{collections::HashMap, sync::Arc}; use async_lock::RwLock; use openmls::framing::MlsMessageOut; +use openmls_traits::OpenMlsCryptoProvider as _; pub use openmls_traits::types::SignatureScheme; use wire_e2e_identity::pki_env::PkiEnvironment; @@ -118,18 +119,8 @@ impl SessionContext { .unwrap(); let core_crypto = CoreCrypto::new(db.clone()); - let transaction = core_crypto.new_transaction().await.unwrap(); - let session_id = identifier - .get_id() - .map_err(RecursiveError::mls_client("getting client id"))? - .into_owned(); - transaction - .mls_init(session_id, context.transport.clone()) - .await - .map_err(RecursiveError::transaction("mls init"))?; - // Setup the X509 PKI environment if let Some(chain) = chain.as_ref() { let dummy_hooks = Arc::new(DummyPkiEnvironmentHooks); @@ -143,6 +134,25 @@ impl SessionContext { chain.register_with_central(&transaction).await; } + let pki_env = core_crypto.get_pki_environment().await; + let session_id = match pki_env { + Some(pki_env) => { + let provider = pki_env.mls_pki_env_provider(); + identifier + .get_id(provider.borrow().await.as_ref()) + .map_err(RecursiveError::mls_client("getting client id"))? + .into_owned() + } + None => identifier + .get_id(None) + .map_err(RecursiveError::mls_client("getting client id"))? + .into_owned(), + }; + transaction + .mls_init(session_id, context.transport.clone()) + .await + .map_err(RecursiveError::transaction("mls init"))?; + let session = transaction.session().await.unwrap(); let credential = Credential::from_identifier(&identifier, context.ciphersuite()) @@ -306,7 +316,11 @@ impl SessionContext { CredentialType::X509 => { let signer = signer.expect("Missing intermediate CA"); let cert = CertificateBundle::rand(&session_id, signer); - let session_id = cert.get_client_id().expect("Getting client id from certificate bundle"); + let session = self.session.read().await; + let guard = session.crypto_provider.authentication_service().borrow().await; + let session_id = cert + .get_client_id(guard.as_ref()) + .expect("Getting client id from certificate bundle"); let credential = Credential::x509(case.ciphersuite(), cert).expect("creating x509 credential"); diff --git a/crypto/src/test_utils/test_conversation/mod.rs b/crypto/src/test_utils/test_conversation/mod.rs index a9b036e0a1..46d88b3d8f 100644 --- a/crypto/src/test_utils/test_conversation/mod.rs +++ b/crypto/src/test_utils/test_conversation/mod.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use openmls::{group::QueuedProposal, prelude::group_info::VerifiableGroupInfo}; +use openmls_traits::OpenMlsCryptoProvider as _; use super::{CredentialType, MessageExt as _, MlsTransportTestExt, SessionContext, TestContext, TestError}; use crate::{ @@ -385,7 +386,11 @@ impl<'a> TestConversation<'a> { let credential = credential_ref.load(&database).await.unwrap(); let mls_credential_with_key = credential.to_mls_credential_with_key(); let ciphersuite = self.case.ciphersuite(); - let local_identity = mls_credential_with_key.extract_identity(ciphersuite, None).unwrap(); + let session = self.actor().session().await; + let provider = session.crypto_provider.authentication_service(); + let local_identity = mls_credential_with_key + .extract_identity(ciphersuite, provider.borrow().await.as_ref()) + .unwrap(); assert_eq!(&local_identity.client_id.as_bytes(), &cid.0); assert_eq!( @@ -404,7 +409,9 @@ impl<'a> TestConversation<'a> { }; assert_eq!(credential.credential.identity(), &cid.0); - let keystore_identity = credential.extract_identity(ciphersuite, None).unwrap(); + let keystore_identity = credential + .extract_identity(ciphersuite, provider.borrow().await.as_ref()) + .unwrap(); assert_eq!( keystore_identity.x509_identity.as_ref().unwrap().display_name, new_display_name From 5ca78c7902d48fa33570c94aa419f8c09d3434c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Wed, 8 Apr 2026 17:04:42 +0200 Subject: [PATCH 28/28] chore: crypto: remove get_new_crl_distribution_points It's unused. --- crypto/src/mls/credential/crl.rs | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/crypto/src/mls/credential/crl.rs b/crypto/src/mls/credential/crl.rs index 258bd0f25f..8df9426add 100644 --- a/crypto/src/mls/credential/crl.rs +++ b/crypto/src/mls/credential/crl.rs @@ -1,14 +1,13 @@ use std::collections::HashSet; -use core_crypto_keystore::{entities::E2eiCrl, traits::FetchFromDatabase}; use openmls::{ group::MlsGroup, prelude::{Certificate, MlsCredentialType}, }; -use wire_e2e_identity::{NewCrlDistributionPoints, x509_check::extract_crl_uris}; +use wire_e2e_identity::x509_check::extract_crl_uris; use super::{Error, Result}; -use crate::{KeystoreError, RecursiveError}; +use crate::RecursiveError; #[derive( Debug, @@ -52,24 +51,6 @@ pub(crate) fn extract_dp(cert: &Certificate) -> Result { }) } -pub(crate) async fn get_new_crl_distribution_points( - database: &impl FetchFromDatabase, - mut crl_dps: HashSet, -) -> Result { - if crl_dps.is_empty() { - return Ok(None.into()); - } - - let stored_crls = database - .load_all::() - .await - .map_err(KeystoreError::wrap("finding all e2e crl"))?; - let stored_crl_dps: HashSet<&str> = stored_crls.iter().map(|crl| crl.distribution_point.as_str()).collect(); - crl_dps.retain(|dp| !stored_crl_dps.contains(&dp.as_str())); - - Ok(Some(crl_dps).into()) -} - impl CrlUris { fn new() -> Self { HashSet::new().into()