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(all(test, feature = "generate-kats"))]
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(all(test, feature = "generate-kats"))]
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 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
297// The following `From` implementation( breaks abstraction layers and MUST
298// NOT be made available outside of tests or "test-utils".
299#[cfg(any(feature = "test-utils", test))]
300impl From<CommitIn> for Commit {
301    fn from(commit: CommitIn) -> Self {
302        Self {
303            proposals: commit.proposals.into_iter().map(Into::into).collect(),
304            path: commit.path.map(Into::into),
305        }
306    }
307}
308
309impl From<Commit> for CommitIn {
310    fn from(commit: Commit) -> Self {
311        Self {
312            proposals: commit.proposals.into_iter().map(Into::into).collect(),
313            path: commit.path.map(Into::into),
314        }
315    }
316}
317
318/// Confirmation tag field of PublicMessage. For type safety this is a wrapper
319/// around a `Mac`.
320#[derive(
321    Debug,
322    PartialEq,
323    Clone,
324    Serialize,
325    Deserialize,
326    TlsDeserialize,
327    TlsDeserializeBytes,
328    TlsSerialize,
329    TlsSize,
330)]
331pub struct ConfirmationTag(pub(crate) Mac);
332
333/// PathSecret
334///
335/// > 11.2.2. Welcoming New Members
336///
337/// ```text
338/// struct {
339///   opaque path_secret<1..255>;
340/// } PathSecret;
341/// ```
342#[derive(
343    Debug, Serialize, Deserialize, TlsSerialize, TlsDeserialize, TlsDeserializeBytes, TlsSize,
344)]
345#[cfg_attr(any(feature = "test-utils", test), derive(PartialEq, Clone))]
346pub(crate) struct PathSecret {
347    pub(crate) path_secret: Secret,
348}
349
350impl From<Secret> for PathSecret {
351    fn from(path_secret: Secret) -> Self {
352        Self { path_secret }
353    }
354}
355
356impl PathSecret {
357    /// Derives a node secret which in turn is used to derive an HpkeKeyPair.
358    pub(crate) fn derive_key_pair(
359        &self,
360        crypto: &impl OpenMlsCrypto,
361        ciphersuite: Ciphersuite,
362    ) -> Result<EncryptionKeyPair, LibraryError> {
363        let node_secret = self
364            .path_secret
365            .kdf_expand_label(crypto, ciphersuite, "node", &[], ciphersuite.hash_length())
366            .map_err(LibraryError::unexpected_crypto_error)?;
367        let HpkeKeyPair { public, private } = crypto
368            .derive_hpke_keypair(ciphersuite.hpke_config(), node_secret.as_slice())
369            .map_err(LibraryError::unexpected_crypto_error)?;
370
371        Ok((HpkePublicKey::from(public), private).into())
372    }
373
374    /// Derives a path secret.
375    pub(crate) fn derive_path_secret(
376        &self,
377        crypto: &impl OpenMlsCrypto,
378        ciphersuite: Ciphersuite,
379    ) -> Result<Self, LibraryError> {
380        let path_secret = self
381            .path_secret
382            .kdf_expand_label(crypto, ciphersuite, "path", &[], ciphersuite.hash_length())
383            .map_err(LibraryError::unexpected_crypto_error)?;
384        Ok(Self { path_secret })
385    }
386
387    /// Encrypt the path secret under the given `HpkePublicKey` using the given
388    /// `group_context`.
389    pub(crate) fn encrypt(
390        &self,
391        crypto: &impl OpenMlsCrypto,
392        ciphersuite: Ciphersuite,
393        public_key: &EncryptionKey,
394        group_context: &[u8],
395    ) -> Result<HpkeCiphertext, LibraryError> {
396        public_key.encrypt(
397            crypto,
398            ciphersuite,
399            group_context,
400            self.path_secret.as_slice(),
401        )
402    }
403
404    /// Consume the `PathSecret`, returning the internal `Secret` value.
405    pub(crate) fn secret(self) -> Secret {
406        self.path_secret
407    }
408
409    /// Decrypt a given `HpkeCiphertext` using the `private_key` and `group_context`.
410    ///
411    /// Returns the decrypted `PathSecret`. Returns an error if the decryption
412    /// was unsuccessful.
413    ///
414    /// ValSem203: Path secrets must decrypt correctly
415    pub(crate) fn decrypt(
416        crypto: &impl OpenMlsCrypto,
417        ciphersuite: Ciphersuite,
418        ciphertext: &HpkeCiphertext,
419        private_key: &EncryptionPrivateKey,
420        group_context: &[u8],
421    ) -> Result<PathSecret, PathSecretError> {
422        // ValSem203: Path secrets must decrypt correctly
423        private_key
424            .decrypt(crypto, ciphersuite, ciphertext, group_context)
425            .map(|path_secret| Self { path_secret })
426            .map_err(|e| e.into())
427    }
428}
429
430/// Path secret error
431#[derive(Error, Debug, PartialEq, Clone)]
432pub(crate) enum PathSecretError {
433    /// See [`hpke::Error`] for more details.
434    #[error(transparent)]
435    DecryptionError(#[from] hpke::Error),
436}
437
438/// GroupSecrets
439///
440/// ```c
441/// // draft-ietf-mls-protocol-17
442/// struct {
443///   opaque joiner_secret<V>;
444///   optional<PathSecret> path_secret;
445///   PreSharedKeyID psks<V>;
446/// } GroupSecrets;
447/// ```
448#[derive(Debug, TlsDeserialize, TlsDeserializeBytes, TlsSize)]
449pub(crate) struct GroupSecrets {
450    pub(crate) joiner_secret: JoinerSecret,
451    pub(crate) path_secret: Option<PathSecret>,
452    pub(crate) psks: Vec<PreSharedKeyId>,
453}
454
455#[derive(TlsSerialize, TlsSize)]
456struct EncodedGroupSecrets<'a> {
457    pub(crate) joiner_secret: &'a JoinerSecret,
458    pub(crate) path_secret: Option<&'a PathSecret>,
459    pub(crate) psks: &'a [PreSharedKeyId],
460}
461
462/// Error related to group secrets.
463#[derive(Error, Debug, PartialEq, Clone)]
464pub enum GroupSecretsError {
465    /// Decryption failed.
466    #[error("Decryption failed.")]
467    DecryptionFailed,
468    /// Malformed.
469    #[error("Malformed.")]
470    Malformed,
471}
472
473impl GroupSecrets {
474    /// Try to decrypt (and parse) a ciphertext into group secrets.
475    pub(crate) fn try_from_ciphertext(
476        skey: &HpkePrivateKey,
477        ciphertext: &HpkeCiphertext,
478        context: &[u8],
479        ciphersuite: Ciphersuite,
480        crypto: &impl OpenMlsCrypto,
481    ) -> Result<Self, GroupSecretsError> {
482        let group_secrets_plaintext =
483            hpke::decrypt_with_label(skey, "Welcome", context, ciphertext, ciphersuite, crypto)
484                .map_err(|_| GroupSecretsError::DecryptionFailed)?;
485
486        // Note: This also checks that no extraneous data was encrypted.
487        let group_secrets = GroupSecrets::tls_deserialize_exact(group_secrets_plaintext)
488            .map_err(|_| GroupSecretsError::Malformed)?;
489
490        Ok(group_secrets)
491    }
492
493    /// Create new encoded group secrets.
494    pub(crate) fn new_encoded<'a>(
495        joiner_secret: &JoinerSecret,
496        path_secret: Option<&'a PathSecret>,
497        psks: &'a [PreSharedKeyId],
498    ) -> Result<Vec<u8>, tls_codec::Error> {
499        EncodedGroupSecrets {
500            joiner_secret,
501            path_secret,
502            psks,
503        }
504        .tls_serialize_detached()
505    }
506}
507
508#[cfg(all(test, feature = "generate-kats"))]
509impl GroupSecrets {
510    pub fn random_encoded(
511        ciphersuite: Ciphersuite,
512        rng: &impl OpenMlsRand,
513    ) -> Result<Vec<u8>, tls_codec::Error> {
514        let psk_id = PreSharedKeyId::new(
515            ciphersuite,
516            rng,
517            Psk::External(ExternalPsk::new(
518                rng.random_vec(ciphersuite.hash_length())
519                    .expect("Not enough randomness."),
520            )),
521        )
522        .expect("An unexpected error occurred.");
523        let psks = vec![psk_id];
524
525        GroupSecrets::new_encoded(
526            &JoinerSecret::random(ciphersuite, rng),
527            Some(&PathSecret {
528                path_secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
529            }),
530            &psks,
531        )
532    }
533}