core_crypto/mls/session/
e2e_identity.rs

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