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