openmls/messages/
mod.rs

1//! # Messages
2//!
3//! This module defines types and logic for Commit and Welcome messages, as well
4//! as Proposals and group info used in External Commits.
5
6use hash_ref::HashReference;
7use openmls_traits::{
8    crypto::OpenMlsCrypto,
9    types::{Ciphersuite, HpkeCiphertext, HpkeKeyPair},
10};
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13use tls_codec::{Deserialize as TlsDeserializeTrait, Serialize as TlsSerializeTrait, *};
14
15#[cfg(test)]
16use crate::schedule::psk::{ExternalPsk, Psk};
17use crate::{
18    ciphersuite::{hash_ref::KeyPackageRef, *},
19    credentials::CredentialWithKey,
20    error::LibraryError,
21    framing::SenderContext,
22    group::errors::ValidationError,
23    schedule::{psk::PreSharedKeyId, JoinerSecret},
24    treesync::{
25        node::{
26            encryption_keys::{EncryptionKey, EncryptionKeyPair, EncryptionPrivateKey},
27            leaf_node::TreePosition,
28        },
29        treekem::{UpdatePath, UpdatePathIn},
30    },
31    versions::ProtocolVersion,
32};
33#[cfg(test)]
34use openmls_traits::random::OpenMlsRand;
35
36pub(crate) mod codec;
37pub mod external_proposals;
38pub mod group_info;
39pub mod proposals;
40pub mod proposals_in;
41
42#[cfg(test)]
43mod tests;
44
45use self::{proposals::*, proposals_in::ProposalOrRefIn};
46
47/// Welcome message
48///
49/// This message is generated when a new member is added to a group.
50/// The invited member can use this message to join the group using
51/// [`StagedWelcome::new_from_welcome()`](crate::group::mls_group::StagedWelcome::new_from_welcome()).
52///
53/// ```c
54/// // draft-ietf-mls-protocol-17
55/// struct {
56///   CipherSuite cipher_suite;
57///   EncryptedGroupSecrets secrets<V>;
58///   opaque encrypted_group_info<V>;
59/// } Welcome;
60/// ```
61#[derive(
62    Clone, Debug, Eq, PartialEq, TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize,
63)]
64pub struct Welcome {
65    cipher_suite: Ciphersuite,
66    secrets: Vec<EncryptedGroupSecrets>,
67    encrypted_group_info: VLBytes,
68}
69
70impl Welcome {
71    /// Create a new welcome message from the provided data.
72    /// Note that secrets and the encrypted group info are consumed.
73    pub(crate) fn new(
74        cipher_suite: Ciphersuite,
75        secrets: Vec<EncryptedGroupSecrets>,
76        encrypted_group_info: Vec<u8>,
77    ) -> Self {
78        Self {
79            cipher_suite,
80            secrets,
81            encrypted_group_info: encrypted_group_info.into(),
82        }
83    }
84
85    pub(crate) fn find_encrypted_group_secret(
86        &self,
87        hash_ref: HashReference,
88    ) -> Option<&EncryptedGroupSecrets> {
89        self.secrets()
90            .iter()
91            .find(|egs| hash_ref == egs.new_member())
92    }
93
94    /// Returns a reference to the ciphersuite in this Welcome message.
95    pub(crate) fn ciphersuite(&self) -> Ciphersuite {
96        self.cipher_suite
97    }
98
99    /// Returns a reference to the encrypted group secrets in this Welcome message.
100    pub fn secrets(&self) -> &[EncryptedGroupSecrets] {
101        self.secrets.as_slice()
102    }
103
104    /// Returns a reference to the encrypted group info.
105    pub(crate) fn encrypted_group_info(&self) -> &[u8] {
106        self.encrypted_group_info.as_slice()
107    }
108
109    /// Set the welcome's encrypted group info.
110    #[cfg(test)]
111    pub fn set_encrypted_group_info(&mut self, encrypted_group_info: Vec<u8>) {
112        self.encrypted_group_info = encrypted_group_info.into();
113    }
114}
115
116/// EncryptedGroupSecrets
117///
118/// This is part of a [`Welcome`] message. It can be used to correlate the correct secrets with each new member.
119#[derive(
120    Clone, Debug, Eq, PartialEq, TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize,
121)]
122pub struct EncryptedGroupSecrets {
123    /// Key package reference of the new member
124    new_member: KeyPackageRef,
125    /// Ciphertext of the encrypted group secret
126    encrypted_group_secrets: HpkeCiphertext,
127}
128
129impl EncryptedGroupSecrets {
130    /// Build a new [`EncryptedGroupSecrets`].
131    pub fn new(new_member: KeyPackageRef, encrypted_group_secrets: HpkeCiphertext) -> Self {
132        Self {
133            new_member,
134            encrypted_group_secrets,
135        }
136    }
137
138    /// Returns the encrypted group secrets' new [`KeyPackageRef`].
139    pub fn new_member(&self) -> KeyPackageRef {
140        self.new_member.clone()
141    }
142
143    /// Returns a reference to the encrypted group secrets' encrypted group secrets.
144    pub(crate) fn encrypted_group_secrets(&self) -> &HpkeCiphertext {
145        &self.encrypted_group_secrets
146    }
147}
148
149// Crate-only types
150
151/// Commit.
152///
153/// A Commit message initiates a new epoch for the group,
154/// based on a collection of Proposals. It instructs group
155/// members to update their representation of the state of
156/// the group by applying the proposals and advancing the
157/// key schedule.
158///
159/// ```c
160/// // draft-ietf-mls-protocol-16
161///
162/// struct {
163///     ProposalOrRef proposals<V>;
164///     optional<UpdatePath> path;
165/// } Commit;
166/// ```
167#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
168pub(crate) struct Commit {
169    pub(crate) proposals: Vec<ProposalOrRef>,
170    pub(crate) path: Option<UpdatePath>,
171}
172
173impl Commit {
174    /// Returns `true` if the commit contains an update path. `false` otherwise.
175    #[cfg(test)]
176    pub fn has_path(&self) -> bool {
177        self.path.is_some()
178    }
179
180    /// Returns the update path of the Commit if it has one.
181    #[cfg(test)]
182    pub(crate) fn path(&self) -> &Option<UpdatePath> {
183        &self.path
184    }
185}
186
187#[derive(
188    Debug,
189    PartialEq,
190    Clone,
191    Serialize,
192    Deserialize,
193    TlsDeserialize,
194    TlsDeserializeBytes,
195    TlsSerialize,
196    TlsSize,
197)]
198pub(crate) struct CommitIn {
199    proposals: Vec<ProposalOrRefIn>,
200    path: Option<UpdatePathIn>,
201}
202
203impl CommitIn {
204    pub(crate) fn unverified_credential(&self) -> Option<CredentialWithKey> {
205        self.path.as_ref().map(|p| {
206            let credential = p.leaf_node().credential().clone();
207            let pk = p.leaf_node().signature_key().clone();
208            CredentialWithKey {
209                credential,
210                signature_key: pk,
211            }
212        })
213    }
214
215    /// Returns a [`Commit`] after successful validation.
216    pub(crate) fn validate(
217        self,
218        ciphersuite: Ciphersuite,
219        crypto: &impl OpenMlsCrypto,
220        sender_context: SenderContext,
221        protocol_version: ProtocolVersion,
222    ) -> Result<Commit, ValidationError> {
223        let proposals = self
224            .proposals
225            .into_iter()
226            .map(|p| p.validate(crypto, ciphersuite, protocol_version))
227            .collect::<Result<Vec<_>, _>>()?;
228
229        let path = if let Some(path) = self.path {
230            let tree_position = match sender_context {
231                SenderContext::Member((group_id, leaf_index)) => {
232                    TreePosition::new(group_id, leaf_index)
233                }
234                SenderContext::ExternalCommit {
235                    group_id,
236                    leftmost_blank_index,
237                    self_removes_in_store,
238                } => {
239                    // We need to determine if it is a a resync or a join.
240                    // Find the first remove proposal and extract the leaf index.
241                    let former_sender_index = proposals.iter().find_map(|p| {
242                        let ProposalOrRef::Proposal(Proposal::Remove(r)) = p else {
243                            return None;
244                        };
245                        Some(r.removed())
246                    });
247
248                    // Collect the sender indices of SelfRemoves that are part of
249                    // this commit.
250                    let self_removed_indices =
251                        self_removes_in_store.into_iter().filter_map(|self_remove| {
252                            proposals.iter().find_map(|committed_p| {
253                                let ProposalOrRef::Reference(committed_p_ref) = committed_p else {
254                                    return None;
255                                };
256                                (self_remove.proposal_ref == *committed_p_ref)
257                                    .then_some(self_remove.sender)
258                            })
259                        });
260
261                    let new_leaf_index = [leftmost_blank_index]
262                        .into_iter()
263                        .chain(former_sender_index)
264                        .chain(self_removed_indices)
265                        .min()
266                        .ok_or(ValidationError::LibraryError(LibraryError::custom(
267                            "The iterator should have at least one element.",
268                        )))?;
269
270                    TreePosition::new(group_id, new_leaf_index)
271                }
272            };
273            Some(path.into_verified(ciphersuite, crypto, tree_position)?)
274        } else {
275            None
276        };
277        Ok(Commit { proposals, path })
278    }
279}
280
281// The following `From` implementation( breaks abstraction layers and MUST
282// NOT be made available outside of tests or "test-utils".
283#[cfg(any(feature = "test-utils", test))]
284impl From<CommitIn> for Commit {
285    fn from(commit: CommitIn) -> Self {
286        Self {
287            proposals: commit.proposals.into_iter().map(Into::into).collect(),
288            path: commit.path.map(Into::into),
289        }
290    }
291}
292
293impl From<Commit> for CommitIn {
294    fn from(commit: Commit) -> Self {
295        Self {
296            proposals: commit.proposals.into_iter().map(Into::into).collect(),
297            path: commit.path.map(Into::into),
298        }
299    }
300}
301
302/// Confirmation tag field of PublicMessage. For type safety this is a wrapper
303/// around a `Mac`.
304#[derive(
305    Debug,
306    PartialEq,
307    Clone,
308    Serialize,
309    Deserialize,
310    TlsDeserialize,
311    TlsDeserializeBytes,
312    TlsSerialize,
313    TlsSize,
314)]
315pub struct ConfirmationTag(pub(crate) Mac);
316
317/// PathSecret
318///
319/// > 11.2.2. Welcoming New Members
320///
321/// ```text
322/// struct {
323///   opaque path_secret<1..255>;
324/// } PathSecret;
325/// ```
326#[derive(
327    Debug, Serialize, Deserialize, TlsSerialize, TlsDeserialize, TlsDeserializeBytes, TlsSize,
328)]
329#[cfg_attr(any(feature = "test-utils", test), derive(PartialEq, Clone))]
330pub(crate) struct PathSecret {
331    pub(crate) path_secret: Secret,
332}
333
334impl From<Secret> for PathSecret {
335    fn from(path_secret: Secret) -> Self {
336        Self { path_secret }
337    }
338}
339
340impl PathSecret {
341    /// Derives a node secret which in turn is used to derive an HpkeKeyPair.
342    pub(crate) fn derive_key_pair(
343        &self,
344        crypto: &impl OpenMlsCrypto,
345        ciphersuite: Ciphersuite,
346    ) -> Result<EncryptionKeyPair, LibraryError> {
347        let node_secret = self
348            .path_secret
349            .kdf_expand_label(crypto, ciphersuite, "node", &[], ciphersuite.hash_length())
350            .map_err(LibraryError::unexpected_crypto_error)?;
351        let HpkeKeyPair { public, private } = crypto
352            .derive_hpke_keypair(ciphersuite.hpke_config(), node_secret.as_slice())
353            .map_err(LibraryError::unexpected_crypto_error)?;
354
355        Ok((HpkePublicKey::from(public), private).into())
356    }
357
358    /// Derives a path secret.
359    pub(crate) fn derive_path_secret(
360        &self,
361        crypto: &impl OpenMlsCrypto,
362        ciphersuite: Ciphersuite,
363    ) -> Result<Self, LibraryError> {
364        let path_secret = self
365            .path_secret
366            .kdf_expand_label(crypto, ciphersuite, "path", &[], ciphersuite.hash_length())
367            .map_err(LibraryError::unexpected_crypto_error)?;
368        Ok(Self { path_secret })
369    }
370
371    /// Encrypt the path secret under the given `HpkePublicKey` using the given
372    /// `group_context`.
373    pub(crate) fn encrypt(
374        &self,
375        crypto: &impl OpenMlsCrypto,
376        ciphersuite: Ciphersuite,
377        public_key: &EncryptionKey,
378        group_context: &[u8],
379    ) -> Result<HpkeCiphertext, LibraryError> {
380        public_key.encrypt(
381            crypto,
382            ciphersuite,
383            group_context,
384            self.path_secret.as_slice(),
385        )
386    }
387
388    /// Consume the `PathSecret`, returning the internal `Secret` value.
389    pub(crate) fn secret(self) -> Secret {
390        self.path_secret
391    }
392
393    /// Decrypt a given `HpkeCiphertext` using the `private_key` and `group_context`.
394    ///
395    /// Returns the decrypted `PathSecret`. Returns an error if the decryption
396    /// was unsuccessful.
397    ///
398    /// ValSem203: Path secrets must decrypt correctly
399    pub(crate) fn decrypt(
400        crypto: &impl OpenMlsCrypto,
401        ciphersuite: Ciphersuite,
402        ciphertext: &HpkeCiphertext,
403        private_key: &EncryptionPrivateKey,
404        group_context: &[u8],
405    ) -> Result<PathSecret, PathSecretError> {
406        // ValSem203: Path secrets must decrypt correctly
407        private_key
408            .decrypt(crypto, ciphersuite, ciphertext, group_context)
409            .map(|path_secret| Self { path_secret })
410            .map_err(|e| e.into())
411    }
412}
413
414/// Path secret error
415#[derive(Error, Debug, PartialEq, Clone)]
416pub(crate) enum PathSecretError {
417    /// See [`hpke::Error`] for more details.
418    #[error(transparent)]
419    DecryptionError(#[from] hpke::Error),
420}
421
422/// GroupSecrets
423///
424/// ```c
425/// // draft-ietf-mls-protocol-17
426/// struct {
427///   opaque joiner_secret<V>;
428///   optional<PathSecret> path_secret;
429///   PreSharedKeyID psks<V>;
430/// } GroupSecrets;
431/// ```
432#[derive(Debug, TlsDeserialize, TlsDeserializeBytes, TlsSize)]
433pub(crate) struct GroupSecrets {
434    pub(crate) joiner_secret: JoinerSecret,
435    pub(crate) path_secret: Option<PathSecret>,
436    pub(crate) psks: Vec<PreSharedKeyId>,
437}
438
439#[derive(TlsSerialize, TlsSize)]
440struct EncodedGroupSecrets<'a> {
441    pub(crate) joiner_secret: &'a JoinerSecret,
442    pub(crate) path_secret: Option<&'a PathSecret>,
443    pub(crate) psks: &'a [PreSharedKeyId],
444}
445
446/// Error related to group secrets.
447#[derive(Error, Debug, PartialEq, Clone)]
448pub enum GroupSecretsError {
449    /// Decryption failed.
450    #[error("Decryption failed.")]
451    DecryptionFailed,
452    /// Malformed.
453    #[error("Malformed.")]
454    Malformed,
455}
456
457impl GroupSecrets {
458    /// Try to decrypt (and parse) a ciphertext into group secrets.
459    pub(crate) fn try_from_ciphertext(
460        skey: &HpkePrivateKey,
461        ciphertext: &HpkeCiphertext,
462        context: &[u8],
463        ciphersuite: Ciphersuite,
464        crypto: &impl OpenMlsCrypto,
465    ) -> Result<Self, GroupSecretsError> {
466        let group_secrets_plaintext =
467            hpke::decrypt_with_label(skey, "Welcome", context, ciphertext, ciphersuite, crypto)
468                .map_err(|_| GroupSecretsError::DecryptionFailed)?;
469
470        // Note: This also checks that no extraneous data was encrypted.
471        let group_secrets = GroupSecrets::tls_deserialize_exact(group_secrets_plaintext)
472            .map_err(|_| GroupSecretsError::Malformed)?;
473
474        Ok(group_secrets)
475    }
476
477    /// Create new encoded group secrets.
478    pub(crate) fn new_encoded<'a>(
479        joiner_secret: &JoinerSecret,
480        path_secret: Option<&'a PathSecret>,
481        psks: &'a [PreSharedKeyId],
482    ) -> Result<Vec<u8>, tls_codec::Error> {
483        EncodedGroupSecrets {
484            joiner_secret,
485            path_secret,
486            psks,
487        }
488        .tls_serialize_detached()
489    }
490}
491
492#[cfg(test)]
493impl GroupSecrets {
494    pub fn random_encoded(
495        ciphersuite: Ciphersuite,
496        rng: &impl OpenMlsRand,
497    ) -> Result<Vec<u8>, tls_codec::Error> {
498        let psk_id = PreSharedKeyId::new(
499            ciphersuite,
500            rng,
501            Psk::External(ExternalPsk::new(
502                rng.random_vec(ciphersuite.hash_length())
503                    .expect("Not enough randomness."),
504            )),
505        )
506        .expect("An unexpected error occurred.");
507        let psks = vec![psk_id];
508
509        GroupSecrets::new_encoded(
510            &JoinerSecret::random(ciphersuite, rng),
511            Some(&PathSecret {
512                path_secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
513            }),
514            &psks,
515        )
516    }
517}