diff --git a/Cargo.lock b/Cargo.lock index ad3efb51b0..b95cc0cb07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,7 +773,7 @@ dependencies = [ [[package]] name = "certval" version = "0.1.4" -source = "git+https://github.com/wireapp/rust-pki.git?branch=wire%2Fstable#b08b3f97fd75131b366b0bbcc42811a21887e602" +source = "git+https://github.com/wireapp/rust-pki.git?branch=wire%2Fstable#9fed8a806f02367a44eeb13d8b72b1a1dac347b8" dependencies = [ "cfg-if", "ciborium", @@ -4248,7 +4248,7 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "pkiprocmacros" version = "0.1.2" -source = "git+https://github.com/wireapp/rust-pki.git?branch=wire%2Fstable#b08b3f97fd75131b366b0bbcc42811a21887e602" +source = "git+https://github.com/wireapp/rust-pki.git?branch=wire%2Fstable#9fed8a806f02367a44eeb13d8b72b1a1dac347b8" dependencies = [ "proc-macro2", "quote", 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}, 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/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() 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: 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/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) } 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 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) - } -} 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/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/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/checks.rs b/e2e-identity/src/acquisition/checks.rs new file mode 100644 index 0000000000..422c6832a0 --- /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(); + 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/error.rs b/e2e-identity/src/acquisition/error.rs index c9cdee101b..e43fd06645 100644 --- a/e2e-identity/src/acquisition/error.rs +++ b/e2e-identity/src/acquisition/error.rs @@ -14,4 +14,66 @@ 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 { + /// 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, + /// 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, + /// 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), } diff --git a/e2e-identity/src/acme/identity/mod.rs b/e2e-identity/src/acquisition/identity.rs similarity index 78% rename from e2e-identity/src/acme/identity/mod.rs rename to e2e-identity/src/acquisition/identity.rs index 934189e0bb..21ede3c7ac 100644 --- a/e2e-identity/src/acme/identity/mod.rs +++ b/e2e-identity/src/acquisition/identity.rs @@ -2,11 +2,11 @@ use rusty_jwt_tools::prelude::{ClientId, HashAlgorithm, QualifiedHandle}; use x509_cert::der::Decode as _; use crate::{ - acme::{RustyAcmeResult, error::CertificateError}, + acquisition::{error::CertificateError, thumbprint::try_compute_jwk_canonicalized_thumbprint}, x509_check::{IdentityStatus, revocation::PkiEnvironment}, }; -pub(crate) mod thumbprint; +type Result = std::result::Result; #[derive(Debug, Clone)] pub struct WireIdentity { @@ -24,24 +24,24 @@ 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) -> 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: Option<&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(); 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, @@ -56,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 @@ -71,39 +71,39 @@ 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) -> 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: Option<&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())?); @@ -122,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 @@ -142,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) { @@ -160,10 +160,10 @@ fn try_extract_san(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(String, #[cfg(test)] mod tests { + use rstest::rstest; use wasm_bindgen_test::*; use super::*; - use crate::x509_check::revocation::PkiEnvironmentParams; wasm_bindgen_test_configure!(run_in_browser); @@ -197,13 +197,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,33 +241,24 @@ OfqfZA1YMtN5NLz/AA== ); } - #[test] + #[rstest] #[wasm_bindgen_test] - fn should_have_valid_status() { - let cert_der = pem::parse(CERT).unwrap(); - let identity = cert_der - .contents() - .extract_identity(None, 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(Some(&env), HashAlgorithm::SHA256) + .extract_identity(&pki_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()); } diff --git a/e2e-identity/src/acquisition/mod.rs b/e2e-identity/src/acquisition/mod.rs index 4be09d5907..ed0f0a5baa 100644 --- a/e2e-identity/src/acquisition/mod.rs +++ b/e2e-identity/src/acquisition/mod.rs @@ -12,11 +12,15 @@ use crate::{ }, }; +mod checks; mod dpop_challenge; mod error; mod initial; mod oidc_challenge; +pub mod identity; +pub mod thumbprint; + #[derive(Debug)] pub struct X509CredentialConfiguration { pub acme_url: String, @@ -26,6 +30,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, } 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)) } } diff --git a/e2e-identity/src/acme/identity/thumbprint.rs b/e2e-identity/src/acquisition/thumbprint.rs similarity index 83% rename from e2e-identity/src/acme/identity/thumbprint.rs rename to e2e-identity/src/acquisition/thumbprint.rs index 74f02939e1..17d8c9055e 100644 --- a/e2e-identity/src/acme/identity/thumbprint.rs +++ b/e2e-identity/src/acquisition/thumbprint.rs @@ -5,7 +5,7 @@ use rusty_jwt_tools::{ }; use x509_cert::spki::SubjectPublicKeyInfoOwned; -use crate::acme::{RustyAcmeError, RustyAcmeResult, 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( @@ -27,13 +27,13 @@ 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) } -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}, @@ -46,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()?) } @@ -58,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), } } 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/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()?, - }) - } -} diff --git a/e2e-identity/src/lib.rs b/e2e-identity/src/lib.rs index 74de8ac028..6dfaf9d47c 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; @@ -136,11 +135,12 @@ 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, + identity::{WireIdentity, WireIdentityReader}, + thumbprint::compute_raw_key_thumbprint, }; -pub use acquisition::X509CredentialAcquisition; -pub use e2e_identity::RustyE2eIdentity; pub use error::{E2eIdentityError, E2eIdentityResult}; pub use pki_env::{NewCrlDistributionPoints, PkiEnvironmentProvider}; #[cfg(feature = "builder")] diff --git a/e2e-identity/src/pki_env/mod.rs b/e2e-identity/src/pki_env/mod.rs index 4fa6870bd2..105d397b96 100644 --- a/e2e-identity/src/pki_env/mod.rs +++ b/e2e-identity/src/pki_env/mod.rs @@ -6,13 +6,17 @@ pub mod hooks; use std::{collections::HashSet, sync::Arc}; use async_lock::{RwLock, RwLockReadGuard}; +use certval::{CertVector as _, TaSource}; use core_crypto_keystore::{ connection::Database, entities::{E2eiAcmeCA, E2eiCrl, E2eiIntermediateCert}, traits::FetchFromDatabase, }; use openmls_traits::authentication_service::{CredentialAuthenticationStatus, CredentialRef}; -use x509_cert::{Certificate, der::Decode as _}; +use x509_cert::{ + Certificate, + der::{Decode as _, Encode as _}, +}; use crate::{ pki_env::hooks::PkiEnvironmentHooks, @@ -42,6 +46,8 @@ pub enum Error { X509CertDerError(#[from] x509_cert::der::Error), #[error(transparent)] KeystoreError(#[from] core_crypto_keystore::CryptoKeystoreError), + #[error("certval error: {0}")] + Certval(certval::Error), } /// New Certificate Revocation List distribution points. @@ -65,15 +71,13 @@ impl IntoIterator for NewCrlDistributionPoints { } } -async fn restore_pki_env(data_provider: &impl FetchFromDatabase) -> Result> { +async fn restore_pki_env(data_provider: &impl FetchFromDatabase) -> Result { let mut trust_roots = vec![]; - let Ok(Some(ta_raw)) = data_provider.get_unique::().await else { - return Ok(None); - }; - - trust_roots.push( - x509_cert::Certificate::from_der(&ta_raw.content).map(x509_cert::anchor::TrustAnchorChoice::Certificate)?, - ); + if let Ok(Some(ta_raw)) = data_provider.get_unique::().await { + trust_roots.push( + x509_cert::Certificate::from_der(&ta_raw.content).map(x509_cert::anchor::TrustAnchorChoice::Certificate)?, + ); + } let intermediates = data_provider .load_all::() @@ -96,7 +100,7 @@ async fn restore_pki_env(data_provider: &impl FetchFromDatabase) -> Result, database: Database) -> Result { - let mls_pki_env_provider = restore_pki_env(&database) - .await? - .map(PkiEnvironmentProvider::from) - .unwrap_or_default(); + let mls_pki_env_provider = PkiEnvironmentProvider::from(restore_pki_env(&database).await?); Ok(Self { hooks, database, @@ -136,9 +137,8 @@ impl PkiEnvironment { } pub async fn update_pki_environment_provider(&self) -> Result<()> { - if let Some(rjt_pki_environment) = restore_pki_env(&self.database).await? { - self.mls_pki_env_provider.update_env(Some(rjt_pki_environment)).await; - } + let rjt_pki_environment = restore_pki_env(&self.database).await?; + self.mls_pki_env_provider.update_env(Some(rjt_pki_environment)).await; Ok(()) } @@ -160,6 +160,20 @@ impl PkiEnvironment { let trust_anchor = x509_cert::Certificate::from_der(&trust_anchor.content)?; Ok(trust_anchor) } + + pub async fn add_trust_anchor(&mut self, name: &str, cert: Certificate) -> Result<()> { + let mut guard = self.mls_pki_env_provider.0.write().await; + let pki_env = guard.as_mut().expect("inner PKI environment must be set"); + + let mut trust_anchors = TaSource::new(); + trust_anchors.push(certval::CertFile { + filename: name.to_owned(), + bytes: cert.to_der()?, + }); + trust_anchors.initialize().map_err(Error::Certval)?; + pki_env.add_trust_anchor_source(Box::new(trust_anchors)); + Ok(()) + } } #[derive(Debug, Clone, Default)] 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 diff --git a/e2e-identity/tests/e2e.rs b/e2e-identity/tests/e2e.rs index d6a322ea9f..cf7f2bd372 100644 --- a/e2e-identity/tests/e2e.rs +++ b/e2e-identity/tests/e2e.rs @@ -38,6 +38,7 @@ use utils::{ stepca::CaCfg, }; use wire_e2e_identity::{X509CredentialAcquisition, acquisition::X509CredentialConfiguration, pki_env::PkiEnvironment}; +use x509_cert::der::DecodePem as _; #[path = "utils/mod.rs"] mod utils; @@ -156,18 +157,20 @@ 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()), }; let acme = utils::stepca::start_acme_server(&ca_cfg).await; let acme_url = acme.socket.to_string(); + let acme_cert = x509_cert::Certificate::from_pem(acme.ca_cert.to_string()).unwrap(); // configure DNS mappings let mut dns_mappings = HashMap::::new(); @@ -177,7 +180,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 +191,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 +200,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(), }); @@ -215,7 +219,9 @@ async fn prepare_pki_env_and_config( .await .unwrap(); - let pki_env = PkiEnvironment::new(hooks, db).await.unwrap(); + let mut pki_env = PkiEnvironment::new(hooks, db).await.unwrap(); + pki_env.update_pki_environment_provider().await.unwrap(); + pki_env.add_trust_anchor("step-ca-root", acme_cert).await.unwrap(); (pki_env, config) } diff --git a/e2e-identity/tests/utils/hooks.rs b/e2e-identity/tests/utils/hooks.rs index 97b44aa027..6069ccd082 100644 --- a/e2e-identity/tests/utils/hooks.rs +++ b/e2e-identity/tests/utils/hooks.rs @@ -30,10 +30,8 @@ impl PkiEnvironmentHooks for TestPkiEnvironmentHooks { mut headers: Vec, body: Vec, ) -> Result { - let client = default_http_client() - .add_root_certificate(self.acme.ca_cert.clone()) - .build() - .unwrap(); + let cert = reqwest::tls::Certificate::from_pem(self.acme.ca_cert.to_string().as_ref()).unwrap(); + let client = default_http_client().add_root_certificate(cert).build().unwrap(); let headers: HashMap = HashMap::from_iter(headers.drain(..).map(|h| (h.name, h.value))); let headers = reqwest::header::HeaderMap::try_from(&headers).unwrap(); let req = match method { 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 { diff --git a/e2e-identity/tests/utils/stepca.rs b/e2e-identity/tests/utils/stepca.rs index 9f9ae4fc66..ab46d8713f 100644 --- a/e2e-identity/tests/utils/stepca.rs +++ b/e2e-identity/tests/utils/stepca.rs @@ -13,7 +13,7 @@ use crate::utils::{NETWORK, SHM, rand_str}; #[derive(Debug)] pub(crate) struct AcmeServer { pub uri: String, - pub ca_cert: reqwest::Certificate, + pub ca_cert: pem::Pem, pub node: ContainerAsync, pub socket: SocketAddr, } @@ -281,9 +281,9 @@ pub(crate) async fn start_acme_server(ca_cfg: &CaCfg) -> AcmeServer { } } -fn ca_cert(host_volume: &Path) -> reqwest::Certificate { +fn ca_cert(host_volume: &Path) -> pem::Pem { // we need to call step-ca over https so we need to fetch its self-signed CA let ca_cert = host_volume.join("certs").join("root_ca.crt"); - let ca_pem = std::fs::read(ca_cert).unwrap(); - reqwest::tls::Certificate::from_pem(ca_pem.as_slice()).expect("Smallstep issued an invalid certificate") + let bytes = std::fs::read(ca_cert).unwrap(); + pem::parse(bytes).expect("step-ca issued a valid certificate") } 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?