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