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