Skip to main content

core_crypto/mls/session/
e2e_identity.rs

1use futures_util::TryFutureExt;
2use openmls::{
3    prelude::{Credential, Node, group_info::VerifiableGroupInfo},
4    treesync::RatchetTree,
5};
6use openmls_traits::OpenMlsCryptoProvider as _;
7use wire_e2e_identity::WireIdentityReader as _;
8
9use super::{Result, Session};
10use crate::{
11    CipherSuite, CredentialFindFilters, CredentialRef, CredentialType, E2eiConversationState, OpenMlsError,
12    RecursiveError,
13    mls::{credential::ext::CredentialExt as _, session::Error},
14    mls_provider::AuthenticationService,
15};
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    /// Returns true if end-to-end-identity is enabled for the given cipher suite.
24    ///
25    /// This is determined by checking for existence of credentials for the given cipher suite:
26    /// If there are x509 (and optionally basic) credentials -> Ok(true)
27    /// If there are no x509 but basic credentials -> Ok(false)
28    /// If there are no credentials for the given cipher suite -> Err(CredentialNotFound)
29    pub async fn e2ei_is_enabled(&self, cipher_suite: CipherSuite) -> Result<bool> {
30        let credentials = CredentialRef::find(
31            &self.database,
32            CredentialFindFilters::builder().cipher_suite(cipher_suite).build(),
33        )
34        .map_err(RecursiveError::mls_credential_ref("finding credentials with filters"))
35        .await?;
36
37        let x509_credential_exists = credentials
38            .iter()
39            .any(|credential| credential.r#type() == CredentialType::X509);
40        if x509_credential_exists {
41            return Ok(true);
42        }
43
44        if !credentials.is_empty() {
45            return Ok(false);
46        }
47
48        Err(Error::NoCredentialWithType(CredentialType::Basic))
49    }
50
51    /// Verifies a Group state before joining it
52    pub async fn e2ei_verify_group_state(&self, group_info: VerifiableGroupInfo) -> Result<E2eiConversationState> {
53        let cs = group_info.ciphersuite().into();
54
55        let is_sender = true; // verify the ratchet tree as sender to turn on hardened verification
56        let Ok(rt) = group_info.take_ratchet_tree(&self.crypto_provider, is_sender).await else {
57            return Ok(E2eiConversationState::NotVerified);
58        };
59
60        let credentials = rt.iter().filter_map(|n| match n {
61            Some(Node::LeafNode(ln)) => Some(ln.credential()),
62            _ => None,
63        });
64
65        Ok(Self::compute_conversation_state(
66            cs,
67            credentials,
68            CredentialType::X509,
69            self.crypto_provider.authentication_service(),
70        )
71        .await)
72    }
73
74    /// Gets the e2ei conversation state from a `GroupInfo`. Useful to check if the group has e2ei
75    /// turned on or not before joining it.
76    pub async fn get_credential_in_use(
77        &self,
78        group_info: VerifiableGroupInfo,
79        credential_type: CredentialType,
80    ) -> Result<E2eiConversationState> {
81        let cs = group_info.ciphersuite().into();
82        // Not verifying the supplied the GroupInfo here could let attackers lure the clients about
83        // the e2ei state of a conversation and as a consequence degrade this conversation for all
84        // participants once joining it.
85        // This 👇 verifies the GroupInfo and the RatchetTree btw
86        let rt = group_info
87            .take_ratchet_tree(&self.crypto_provider, false)
88            .await
89            .map_err(OpenMlsError::wrap("taking ratchet tree"))?;
90        Self::get_credential_in_use_in_ratchet_tree(
91            cs,
92            rt,
93            credential_type,
94            self.crypto_provider.authentication_service(),
95        )
96        .await
97    }
98    pub(crate) async fn get_credential_in_use_in_ratchet_tree(
99        cipher_suite: CipherSuite,
100        ratchet_tree: RatchetTree,
101        credential_type: CredentialType,
102        auth_service: &AuthenticationService,
103    ) -> Result<E2eiConversationState> {
104        let credentials = ratchet_tree.iter().filter_map(|n| match n {
105            Some(Node::LeafNode(ln)) => Some(ln.credential()),
106            _ => None,
107        });
108        Ok(Self::compute_conversation_state(cipher_suite, credentials, credential_type, auth_service).await)
109    }
110
111    /// _credential_type will be used in the future to get the usage of VC Credentials, even Basics one.
112    /// Right now though, we do not need anything other than X509 so let's keep things simple.
113    pub(crate) async fn compute_conversation_state<'a>(
114        cipher_suite: CipherSuite,
115        credentials: impl Iterator<Item = &'a Credential>,
116        _credential_type: CredentialType,
117        auth_service: &AuthenticationService,
118    ) -> E2eiConversationState {
119        let pki_env = auth_service.pki_env().await;
120        let Some(env) = pki_env else {
121            return E2eiConversationState::NotEnabled;
122        };
123
124        let mut is_e2ei = false;
125        let mut state = E2eiConversationState::Verified;
126
127        for credential in credentials {
128            let Ok(Some(cert)) = credential.parse_leaf_cert() else {
129                state = E2eiConversationState::NotVerified;
130                if is_e2ei {
131                    break;
132                }
133                continue;
134            };
135
136            is_e2ei = true;
137
138            let invalid_identity = cert.extract_identity(&env, cipher_suite.e2ei_hash_alg()).await.is_err();
139
140            use openmls_x509_credential::X509Ext as _;
141            let is_time_valid = cert.is_time_valid().unwrap_or(false);
142            let is_time_invalid = !is_time_valid;
143            let is_revoked_or_invalid = env.validate_cert(&cert).await.is_err();
144            let is_invalid = invalid_identity || is_time_invalid || is_revoked_or_invalid;
145            if is_invalid {
146                state = E2eiConversationState::NotVerified;
147                break;
148            }
149        }
150
151        if is_e2ei {
152            state
153        } else {
154            E2eiConversationState::NotEnabled
155        }
156    }
157}