core_crypto/mls/session/
e2e_identity.rs

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