core_crypto/mls/session/
e2e_identity.rs

1use openmls::{
2    prelude::{Credential, Node, SignatureScheme, group_info::VerifiableGroupInfo},
3    treesync::RatchetTree,
4};
5use wire_e2e_identity::prelude::WireIdentityReader as _;
6
7use crate::{
8    MlsError, RecursiveError,
9    e2e_identity::E2eiDumpedPkiEnv,
10    mls::session::CredentialExt as _,
11    prelude::{E2eiConversationState, MlsCiphersuite, MlsCredentialType},
12};
13use openmls_traits::OpenMlsCryptoProvider as _;
14
15use super::{Error, Result, Session};
16
17impl Session {
18    /// Returns whether the E2EI PKI environment is setup (i.e. Root CA, Intermediates, CRLs)
19    pub async fn e2ei_is_pki_env_setup(&self) -> bool {
20        self.crypto_provider.is_pki_env_setup().await
21    }
22
23    /// Dumps the PKI environment as PEM
24    pub async fn e2ei_dump_pki_env(&self) -> Result<Option<E2eiDumpedPkiEnv>> {
25        if !self.e2ei_is_pki_env_setup().await {
26            return Ok(None);
27        }
28        let pki_env_lock = self.crypto_provider.authentication_service().borrow().await;
29        let Some(pki_env) = &*pki_env_lock else {
30            return Ok(None);
31        };
32        E2eiDumpedPkiEnv::from_pki_env(pki_env)
33            .await
34            .map_err(RecursiveError::e2e_identity("dumping pki env"))
35            .map_err(Into::into)
36    }
37
38    /// Returns true when end-to-end-identity is enabled for the given SignatureScheme
39    pub async fn e2ei_is_enabled(&self, signature_scheme: SignatureScheme) -> Result<bool> {
40        let x509_result = self
41            .find_most_recent_credential_bundle(signature_scheme, MlsCredentialType::X509)
42            .await;
43        match x509_result {
44            Err(Error::CredentialNotFound(MlsCredentialType::X509)) => {
45                self.find_most_recent_credential_bundle(signature_scheme, MlsCredentialType::Basic)
46                    .await?;
47                Ok(false)
48            }
49            Err(e) => Err(e),
50            Ok(_) => Ok(true),
51        }
52    }
53
54    /// Verifies a Group state before joining it
55    pub async fn e2ei_verify_group_state(&self, group_info: VerifiableGroupInfo) -> Result<E2eiConversationState> {
56        self.crypto_provider
57            .authentication_service()
58            .refresh_time_of_interest()
59            .await;
60
61        let cs = group_info.ciphersuite().into();
62
63        let is_sender = true; // verify the ratchet tree as sender to turn on hardened verification
64        let Ok(rt) = group_info.take_ratchet_tree(&self.crypto_provider, is_sender).await else {
65            return Ok(E2eiConversationState::NotVerified);
66        };
67
68        let credentials = rt.iter().filter_map(|n| match n {
69            Some(Node::LeafNode(ln)) => Some(ln.credential()),
70            _ => None,
71        });
72
73        Ok(Self::compute_conversation_state(
74            cs,
75            credentials,
76            MlsCredentialType::X509,
77            self.crypto_provider.authentication_service().borrow().await.as_ref(),
78        )
79        .await)
80    }
81
82    /// Gets the e2ei conversation state from a `GroupInfo`. Useful to check if the group has e2ei
83    /// turned on or not before joining it.
84    pub async fn get_credential_in_use(
85        &self,
86        group_info: VerifiableGroupInfo,
87        credential_type: MlsCredentialType,
88    ) -> Result<E2eiConversationState> {
89        let cs = group_info.ciphersuite().into();
90        // Not verifying the supplied the GroupInfo here could let attackers lure the clients about
91        // the e2ei state of a conversation and as a consequence degrade this conversation for all
92        // participants once joining it.
93        // This 👇 verifies the GroupInfo and the RatchetTree btw
94        let rt = group_info
95            .take_ratchet_tree(&self.crypto_provider, false)
96            .await
97            .map_err(MlsError::wrap("taking ratchet tree"))?;
98        Self::get_credential_in_use_in_ratchet_tree(
99            cs,
100            rt,
101            credential_type,
102            self.crypto_provider.authentication_service().borrow().await.as_ref(),
103        )
104        .await
105    }
106    pub(crate) async fn get_credential_in_use_in_ratchet_tree(
107        ciphersuite: MlsCiphersuite,
108        ratchet_tree: RatchetTree,
109        credential_type: MlsCredentialType,
110        env: Option<&wire_e2e_identity::prelude::x509::revocation::PkiEnvironment>,
111    ) -> Result<E2eiConversationState> {
112        let credentials = ratchet_tree.iter().filter_map(|n| match n {
113            Some(Node::LeafNode(ln)) => Some(ln.credential()),
114            _ => None,
115        });
116        Ok(Self::compute_conversation_state(ciphersuite, credentials, credential_type, env).await)
117    }
118
119    /// _credential_type will be used in the future to get the usage of VC Credentials, even Basics one.
120    /// Right now though, we do not need anything other than X509 so let's keep things simple.
121    pub(crate) async fn compute_conversation_state<'a>(
122        ciphersuite: MlsCiphersuite,
123        credentials: impl Iterator<Item = &'a Credential>,
124        _credential_type: MlsCredentialType,
125        env: Option<&wire_e2e_identity::prelude::x509::revocation::PkiEnvironment>,
126    ) -> E2eiConversationState {
127        let mut is_e2ei = false;
128        let mut state = E2eiConversationState::Verified;
129
130        for credential in credentials {
131            let Ok(Some(cert)) = credential.parse_leaf_cert() else {
132                state = E2eiConversationState::NotVerified;
133                if is_e2ei {
134                    break;
135                }
136                continue;
137            };
138
139            is_e2ei = true;
140
141            let invalid_identity = cert.extract_identity(env, ciphersuite.e2ei_hash_alg()).is_err();
142
143            use openmls_x509_credential::X509Ext as _;
144            let is_time_valid = cert.is_time_valid().unwrap_or(false);
145            let is_time_invalid = !is_time_valid;
146            let is_revoked_or_invalid = env
147                .map(|e| e.validate_cert_and_revocation(&cert).is_err())
148                .unwrap_or(false);
149
150            let is_invalid = invalid_identity || is_time_invalid || is_revoked_or_invalid;
151            if is_invalid {
152                state = E2eiConversationState::NotVerified;
153                break;
154            }
155        }
156
157        if is_e2ei {
158            state
159        } else {
160            E2eiConversationState::NotEnabled
161        }
162    }
163}