Skip to main content

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