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                        p.as_proposal()
243                            .and_then(|p| p.as_remove())
244                            .map(|r| r.removed())
245                    });
246
247                    // Collect the sender indices of SelfRemoves that are part of
248                    // this commit.
249                    let self_removed_indices =
250                        self_removes_in_store.into_iter().filter_map(|self_remove| {
251                            proposals.iter().find_map(|committed_p| {
252                                committed_p.as_reference().and_then(|committed_p_ref| {
253                                    (&self_remove.proposal_ref == committed_p_ref)
254                                        .then_some(self_remove.sender)
255                                })
256                            })
257                        });
258
259                    let new_leaf_index = [leftmost_blank_index]
260                        .into_iter()
261                        .chain(former_sender_index)
262                        .chain(self_removed_indices)
263                        .min()
264                        .ok_or(ValidationError::LibraryError(LibraryError::custom(
265                            "The iterator should have at least one element.",
266                        )))?;
267
268                    TreePosition::new(group_id, new_leaf_index)
269                }
270            };
271            Some(path.into_verified(ciphersuite, crypto, tree_position)?)
272        } else {
273            None
274        };
275        Ok(Commit { proposals, path })
276    }
277}
278
279// The following `From` implementation( breaks abstraction layers and MUST
280// NOT be made available outside of tests or "test-utils".
281#[cfg(any(feature = "test-utils", test))]
282impl From<CommitIn> for Commit {
283    fn from(commit: CommitIn) -> Self {
284        Self {
285            proposals: commit.proposals.into_iter().map(Into::into).collect(),
286            path: commit.path.map(Into::into),
287        }
288    }
289}
290
291impl From<Commit> for CommitIn {
292    fn from(commit: Commit) -> Self {
293        Self {
294            proposals: commit.proposals.into_iter().map(Into::into).collect(),
295            path: commit.path.map(Into::into),
296        }
297    }
298}
299
300/// Confirmation tag field of PublicMessage. For type safety this is a wrapper
301/// around a `Mac`.
302#[derive(
303    Debug,
304    PartialEq,
305    Clone,
306    Serialize,
307    Deserialize,
308    TlsDeserialize,
309    TlsDeserializeBytes,
310    TlsSerialize,
311    TlsSize,
312)]
313pub struct ConfirmationTag(pub(crate) Mac);
314
315/// PathSecret
316///
317/// > 11.2.2. Welcoming New Members
318///
319/// ```text
320/// struct {
321///   opaque path_secret<1..255>;
322/// } PathSecret;
323/// ```
324#[derive(
325    Debug, Serialize, Deserialize, TlsSerialize, TlsDeserialize, TlsDeserializeBytes, TlsSize,
326)]
327#[cfg_attr(any(feature = "test-utils", test), derive(PartialEq, Clone))]
328pub(crate) struct PathSecret {
329    pub(crate) path_secret: Secret,
330}
331
332impl From<Secret> for PathSecret {
333    fn from(path_secret: Secret) -> Self {
334        Self { path_secret }
335    }
336}
337
338impl PathSecret {
339    /// Derives a node secret which in turn is used to derive an HpkeKeyPair.
340    pub(crate) fn derive_key_pair(
341        &self,
342        crypto: &impl OpenMlsCrypto,
343        ciphersuite: Ciphersuite,
344    ) -> Result<EncryptionKeyPair, LibraryError> {
345        let node_secret = self
346            .path_secret
347            .kdf_expand_label(crypto, ciphersuite, "node", &[], ciphersuite.hash_length())
348            .map_err(LibraryError::unexpected_crypto_error)?;
349        let HpkeKeyPair { public, private } = crypto
350            .derive_hpke_keypair(ciphersuite.hpke_config(), node_secret.as_slice())
351            .map_err(LibraryError::unexpected_crypto_error)?;
352
353        Ok((HpkePublicKey::from(public), private).into())
354    }
355
356    /// Derives a path secret.
357    pub(crate) fn derive_path_secret(
358        &self,
359        crypto: &impl OpenMlsCrypto,
360        ciphersuite: Ciphersuite,
361    ) -> Result<Self, LibraryError> {
362        let path_secret = self
363            .path_secret
364            .kdf_expand_label(crypto, ciphersuite, "path", &[], ciphersuite.hash_length())
365            .map_err(LibraryError::unexpected_crypto_error)?;
366        Ok(Self { path_secret })
367    }
368
369    /// Encrypt the path secret under the given `HpkePublicKey` using the given
370    /// `group_context`.
371    pub(crate) fn encrypt(
372        &self,
373        crypto: &impl OpenMlsCrypto,
374        ciphersuite: Ciphersuite,
375        public_key: &EncryptionKey,
376        group_context: &[u8],
377    ) -> Result<HpkeCiphertext, LibraryError> {
378        public_key.encrypt(
379            crypto,
380            ciphersuite,
381            group_context,
382            self.path_secret.as_slice(),
383        )
384    }
385
386    /// Consume the `PathSecret`, returning the internal `Secret` value.
387    pub(crate) fn secret(self) -> Secret {
388        self.path_secret
389    }
390
391    /// Decrypt a given `HpkeCiphertext` using the `private_key` and `group_context`.
392    ///
393    /// Returns the decrypted `PathSecret`. Returns an error if the decryption
394    /// was unsuccessful.
395    ///
396    /// ValSem203: Path secrets must decrypt correctly
397    pub(crate) fn decrypt(
398        crypto: &impl OpenMlsCrypto,
399        ciphersuite: Ciphersuite,
400        ciphertext: &HpkeCiphertext,
401        private_key: &EncryptionPrivateKey,
402        group_context: &[u8],
403    ) -> Result<PathSecret, PathSecretError> {
404        // ValSem203: Path secrets must decrypt correctly
405        private_key
406            .decrypt(crypto, ciphersuite, ciphertext, group_context)
407            .map(|path_secret| Self { path_secret })
408            .map_err(|e| e.into())
409    }
410}
411
412/// Path secret error
413#[derive(Error, Debug, PartialEq, Clone)]
414pub(crate) enum PathSecretError {
415    /// See [`hpke::Error`] for more details.
416    #[error(transparent)]
417    DecryptionError(#[from] hpke::Error),
418}
419
420/// GroupSecrets
421///
422/// ```c
423/// // draft-ietf-mls-protocol-17
424/// struct {
425///   opaque joiner_secret<V>;
426///   optional<PathSecret> path_secret;
427///   PreSharedKeyID psks<V>;
428/// } GroupSecrets;
429/// ```
430#[derive(Debug, TlsDeserialize, TlsDeserializeBytes, TlsSize)]
431pub(crate) struct GroupSecrets {
432    pub(crate) joiner_secret: JoinerSecret,
433    pub(crate) path_secret: Option<PathSecret>,
434    pub(crate) psks: Vec<PreSharedKeyId>,
435}
436
437#[derive(TlsSerialize, TlsSize)]
438struct EncodedGroupSecrets<'a> {
439    pub(crate) joiner_secret: &'a JoinerSecret,
440    pub(crate) path_secret: Option<&'a PathSecret>,
441    pub(crate) psks: &'a [PreSharedKeyId],
442}
443
444/// Error related to group secrets.
445#[derive(Error, Debug, PartialEq, Clone)]
446pub enum GroupSecretsError {
447    /// Decryption failed.
448    #[error("Decryption failed.")]
449    DecryptionFailed,
450    /// Malformed.
451    #[error("Malformed.")]
452    Malformed,
453}
454
455impl GroupSecrets {
456    /// Try to decrypt (and parse) a ciphertext into group secrets.
457    pub(crate) fn try_from_ciphertext(
458        skey: &HpkePrivateKey,
459        ciphertext: &HpkeCiphertext,
460        context: &[u8],
461        ciphersuite: Ciphersuite,
462        crypto: &impl OpenMlsCrypto,
463    ) -> Result<Self, GroupSecretsError> {
464        let group_secrets_plaintext =
465            hpke::decrypt_with_label(skey, "Welcome", context, ciphertext, ciphersuite, crypto)
466                .map_err(|_| GroupSecretsError::DecryptionFailed)?;
467
468        // Note: This also checks that no extraneous data was encrypted.
469        let group_secrets = GroupSecrets::tls_deserialize_exact(group_secrets_plaintext)
470            .map_err(|_| GroupSecretsError::Malformed)?;
471
472        Ok(group_secrets)
473    }
474
475    /// Create new encoded group secrets.
476    pub(crate) fn new_encoded<'a>(
477        joiner_secret: &JoinerSecret,
478        path_secret: Option<&'a PathSecret>,
479        psks: &'a [PreSharedKeyId],
480    ) -> Result<Vec<u8>, tls_codec::Error> {
481        EncodedGroupSecrets {
482            joiner_secret,
483            path_secret,
484            psks,
485        }
486        .tls_serialize_detached()
487    }
488}
489
490#[cfg(test)]
491impl GroupSecrets {
492    pub fn random_encoded(
493        ciphersuite: Ciphersuite,
494        rng: &impl OpenMlsRand,
495    ) -> Result<Vec<u8>, tls_codec::Error> {
496        let psk_id = PreSharedKeyId::new(
497            ciphersuite,
498            rng,
499            Psk::External(ExternalPsk::new(
500                rng.random_vec(ciphersuite.hash_length())
501                    .expect("Not enough randomness."),
502            )),
503        )
504        .expect("An unexpected error occurred.");
505        let psks = vec![psk_id];
506
507        GroupSecrets::new_encoded(
508            &JoinerSecret::random(ciphersuite, rng),
509            Some(&PathSecret {
510                path_secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
511            }),
512            &psks,
513        )
514    }
515}