Skip to main content

openmls/test_utils/frankenstein/
crypto.rs

1use openmls_traits::{
2    crypto::OpenMlsCrypto,
3    signatures::Signer,
4    storage::{self, CURRENT_VERSION},
5    types::Ciphersuite,
6    OpenMlsProvider,
7};
8use thiserror::Error;
9use tls_codec::{Deserialize, Serialize, SerializeBytes, TlsSerialize, TlsSize, VLBytes};
10
11use crate::{
12    ciphersuite::{hash_ref::HashReference, hpke, AeadKey, AeadNonce, Mac, Secret},
13    error::LibraryError,
14    group::{CreateCommitError, WelcomeError, WelcomeKeyMaterial},
15    key_packages::{KeyPackage, KeyPackageBundle},
16    messages::{
17        group_info::{GroupInfo, GroupInfoTBS, VerifiableGroupInfo},
18        ConfirmationTag, GroupSecrets, GroupSecretsError, PathSecret, Welcome,
19    },
20    schedule::{
21        errors::{KeyScheduleError, PskError},
22        psk::{load_psks, store::ResumptionPskStore, PreSharedKeyId, PskSecret},
23        JoinerSecret, KeySchedule, Psk, WelcomeSecret,
24    },
25    storage::StorageProvider,
26    test_utils::frankenstein::{
27        group_info::FrankenGroupInfo, key_package, FrankenEncryptedGroupSecrets,
28        FrankenGroupSecrets, FrankenKeyPackage, FrankenPathSecret, FrankenWelcome,
29    },
30};
31
32use super::FrankenAuthenticatedContentTbm;
33
34/// Computes a valid membership tag for the provided content.
35pub fn compute_membership_tag(
36    crypto: &impl OpenMlsCrypto,
37    ciphersuite: Ciphersuite,
38    membership_key: &[u8],
39    auth_content_tbm: &FrankenAuthenticatedContentTbm,
40) -> VLBytes {
41    let serialized_auth_content_tbm = &auth_content_tbm.tls_serialize_detached().unwrap();
42    crypto
43        .hmac(
44            ciphersuite.hash_algorithm(),
45            membership_key,              // Extract salt is HMAC key
46            serialized_auth_content_tbm, // Extract ikm is HMAC message
47        )
48        .unwrap()
49        .as_slice()
50        .into()
51}
52
53/// Implements the "sign with label" function of the spec.
54pub fn sign_with_label(signer: &impl Signer, label: &[u8], msg: &[u8]) -> Vec<u8> {
55    let data = FrankenSignContent::new(label, msg)
56        .tls_serialize_detached()
57        .unwrap();
58    signer.sign(&data).unwrap()
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, TlsSerialize, TlsSize)]
62pub struct FrankenSignContent<'a> {
63    label: Vec<u8>,
64    content: &'a [u8],
65}
66
67impl<'a> FrankenSignContent<'a> {
68    pub fn new(label: &[u8], content: &'a [u8]) -> Self {
69        let mut tagged_label = b"MLS 1.0 ".to_vec();
70        tagged_label.extend_from_slice(label);
71
72        Self {
73            label: tagged_label,
74            content,
75        }
76    }
77}
78
79impl FrankenWelcome {
80    /// Decrypts a [`FrankenWelcome`] using the first group secret decryptable by `storage`.
81    /// Performs no semantic checks.
82    ///
83    /// This method is meant for tests that need to manipulate the encrypted contents.
84    ///
85    /// The implementation is roughly inspired by `mls_group::creation::decrypt_group_secrets`, without validity checks.
86    #[openmls_traits::maybe_async]
87    pub async fn open<Crypto, Storage>(
88        &self,
89        crypto: &Crypto,
90        storage: &Storage,
91    ) -> Result<(Ciphersuite, FrankenGroupSecrets, FrankenGroupInfo), WelcomeError<Storage::Error>>
92    where
93        Crypto: OpenMlsCrypto,
94        Storage: StorageProvider,
95    {
96        let ciphersuite = Ciphersuite::try_from(self.cipher_suite)
97            .expect("cannot open welcome with invalid ciphersuite");
98        crypto
99            .supports(ciphersuite)
100            .map_err(|_| WelcomeError::UnsupportedCiphersuite(ciphersuite))?;
101
102        // Find and decrypt GroupSecrets addressed to us
103        let (key_material, egs) = self.find_decryptable_secret(storage).await?;
104        let group_secrets_plaintext = hpke::decrypt_with_label(
105            key_material.init_private_key(),
106            "Welcome",
107            self.encrypted_group_info.as_slice(),
108            &egs.encrypted_group_secrets.clone().into(),
109            ciphersuite,
110            crypto,
111        )
112        .map_err(|_| GroupSecretsError::DecryptionFailed)?;
113        let group_secrets = FrankenGroupSecrets::tls_deserialize_exact(group_secrets_plaintext)
114            .map_err(|_| GroupSecretsError::Malformed)?;
115
116        // Derive keys for GroupInfo
117        let (welcome_key, welcome_nonce) = group_secrets.welcome_keys(
118            ciphersuite,
119            group_secrets
120                .psk_secret(ciphersuite, crypto, storage)
121                .await?,
122            crypto,
123        )?;
124
125        // Decrypt GroupInfo
126        let group_info: GroupInfo = VerifiableGroupInfo::try_from_ciphertext(
127            &welcome_key,
128            &welcome_nonce,
129            self.encrypted_group_info.as_slice(),
130            &[],
131            crypto,
132        )?
133        .into();
134
135        Ok((ciphersuite, group_secrets, group_info.into()))
136    }
137
138    /// Signs and encrypts a new [`FrankenWelcome`] message from components.
139    /// Performs no semantic checks.
140    ///
141    /// This method is meant for tests that need to manipulate the encrypted contents.
142    #[openmls_traits::maybe_async]
143    pub async fn seal<Crypto, Storage>(
144        ciphersuite: Ciphersuite,
145        group_secrets: &FrankenGroupSecrets,
146        group_info: &FrankenGroupInfo,
147        signer: &impl Signer,
148        invited_members: &[(FrankenKeyPackage, Option<FrankenPathSecret>)],
149        crypto: &Crypto,
150        storage: &Storage,
151    ) -> Result<Self, CreateCommitError>
152    where
153        Crypto: OpenMlsCrypto,
154        Storage: StorageProvider,
155    {
156        // Sign updated group info
157        let mut group_info = group_info.clone();
158        group_info.resign(signer);
159
160        // Seal GroupInfo
161        let psk_secret = group_secrets
162            .psk_secret(ciphersuite, crypto, storage)
163            .await?;
164        let (welcome_key, welcome_nonce) =
165            group_secrets.welcome_keys(ciphersuite, psk_secret, crypto)?;
166        let encrypted_group_info = welcome_key
167            .aead_seal(
168                crypto,
169                group_info
170                    .tls_serialize_detached()
171                    .map_err(LibraryError::missing_bound_check)?
172                    .as_slice(),
173                &[],
174                &welcome_nonce,
175            )
176            .map_err(LibraryError::unexpected_crypto_error)?;
177
178        // Encrypt GroupSecrets for each invited member
179        let psk_ids = group_secrets.psk_ids();
180        let joiner_secret = group_secrets.joiner_secret.clone().into();
181        let encrypted_group_secrets: Vec<FrankenEncryptedGroupSecrets> = invited_members
182            .iter()
183            .map(|invited_member| {
184                let (invited_key_package, path_secret) = invited_member;
185
186                let path_secret = path_secret.as_ref().map(|path_secret| PathSecret {
187                    path_secret: Secret::from_slice(path_secret.as_slice()),
188                });
189                let encoded_group_secrets =
190                    GroupSecrets::new_encoded(&joiner_secret, path_secret.as_ref(), &psk_ids)
191                        .unwrap();
192
193                let key_package: KeyPackage = invited_key_package.clone().into();
194                let ciphertext = hpke::encrypt_with_label(
195                    key_package.hpke_init_key().as_slice(),
196                    "Welcome",
197                    encrypted_group_info.as_slice(),
198                    &encoded_group_secrets,
199                    key_package.ciphersuite(),
200                    crypto,
201                )
202                .unwrap();
203
204                FrankenEncryptedGroupSecrets {
205                    new_member: key_package.hash_ref(crypto).unwrap().as_slice().into(),
206                    encrypted_group_secrets: ciphertext.into(),
207                }
208            })
209            .collect();
210
211        // create Welcome message
212        Ok(Self {
213            cipher_suite: ciphersuite.into(),
214            secrets: encrypted_group_secrets,
215            encrypted_group_info: encrypted_group_info.into(),
216        })
217    }
218
219    /// Finds the first `EncryptedGroupSecret` in this `Welcome` that corresponds to a `KeyPackageBundle` in `storage`.
220    ///
221    /// Combines [`crate::group::mls_group::creation::keys_for_welcome`] and [`Welcome::find_encrypted_group_secret`]
222    #[openmls_traits::maybe_async]
223    pub(crate) async fn find_decryptable_secret<Storage>(
224        &self,
225        storage: &Storage,
226    ) -> Result<(WelcomeKeyMaterial, &FrankenEncryptedGroupSecrets), WelcomeError<Storage::Error>>
227    where
228        Storage: StorageProvider,
229    {
230        // Store all secrets for reasonable tests
231        for egs in self.secrets.as_slice() {
232            let hash_ref = &HashReference::from_slice(egs.new_member.as_slice());
233            if let Some(key_package_bundle) = storage
234                .key_package(hash_ref)
235                .await
236                .map_err(WelcomeError::StorageError)?
237            {
238                return Ok((
239                    WelcomeKeyMaterial::with_key_package_bundle(key_package_bundle),
240                    egs,
241                ));
242            }
243        }
244        Err(WelcomeError::NoMatchingKeyPackage)
245    }
246
247    /// Create a FrankenWelcome from `self` with updated, sealed content.
248    ///
249    /// Equivalent to using [`Self::open`] and [`Self::seal`].
250    #[openmls_traits::maybe_async]
251    pub async fn with_sealed_update<F, InvitedProvider>(
252        &self,
253        signer: &impl Signer,
254        signer_provider: &impl OpenMlsProvider,
255        invited_key_package: &KeyPackage,
256        invited_provider: &InvitedProvider,
257        f: F,
258    ) -> Self
259    where
260        F: FnOnce(&mut Ciphersuite, &mut FrankenGroupSecrets, &mut FrankenGroupInfo),
261        InvitedProvider: OpenMlsProvider,
262    {
263        let (mut ciphersuite, mut group_secrets, mut group_info) = self
264            .open(invited_provider.crypto(), invited_provider.storage())
265            .await
266            .expect("failed to open FrankenWelcome");
267
268        f(&mut ciphersuite, &mut group_secrets, &mut group_info);
269
270        FrankenWelcome::seal(
271            ciphersuite,
272            &group_secrets,
273            &group_info,
274            signer,
275            &[(
276                invited_key_package.clone().into(),
277                group_secrets.path_secret.clone(),
278            )],
279            signer_provider.crypto(),
280            signer_provider.storage(),
281        )
282        .await
283        .expect("failed to seal FrankenWelcome")
284    }
285}
286
287impl FrankenGroupSecrets {
288    pub fn psk_ids(&self) -> Vec<PreSharedKeyId> {
289        self.psks.iter().clone().map(Into::into).collect()
290    }
291
292    #[openmls_traits::maybe_async]
293    pub async fn psk_secret<Crypto, Storage>(
294        &self,
295        ciphersuite: Ciphersuite,
296        crypto: &Crypto,
297        storage: &Storage,
298    ) -> Result<PskSecret, PskError>
299    where
300        Crypto: OpenMlsCrypto,
301        Storage: StorageProvider,
302    {
303        let psk_ids = self.psk_ids();
304        let psks = load_psks(
305            storage,
306            // No resumption keys for now
307            &ResumptionPskStore::new(0),
308            psk_ids.as_slice(),
309        )
310        .await?;
311        let psk_secret = PskSecret::new(crypto, ciphersuite, psks)?;
312        Ok(psk_secret)
313    }
314
315    pub(crate) fn welcome_keys<Crypto>(
316        &self,
317        ciphersuite: Ciphersuite,
318        psk_secret: PskSecret,
319        crypto: &Crypto,
320    ) -> Result<(AeadKey, AeadNonce), LibraryError>
321    where
322        Crypto: OpenMlsCrypto,
323    {
324        let joiner_secret = self.joiner_secret.clone().into();
325        let key_schedule = KeySchedule::init(ciphersuite, crypto, &joiner_secret, psk_secret)
326            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
327        let welcome_secret = key_schedule.welcome(crypto, ciphersuite).unwrap();
328        let (welcome_key, welcome_nonce) = welcome_secret
329            .derive_welcome_key_nonce(crypto, ciphersuite)
330            .map_err(LibraryError::unexpected_crypto_error)?;
331        Ok((welcome_key, welcome_nonce))
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::{super::FrankenExtension, *};
338    use crate::group::{
339        mls_group::tests_and_kats::utils::{setup_alice_group, setup_client},
340        StagedWelcome,
341    };
342
343    #[openmls_test::openmls_test]
344    fn test_reseal_franken_group_info() {
345        // create Welcome message and GroupInfo
346        let alice_provider = &Provider::default();
347        let bob_provider = &Provider::default();
348        let (mut alice_group, _alice_credential, alice_signer, _alice_pk) =
349            setup_alice_group(ciphersuite, alice_provider);
350
351        let (_bob_credential, bob_kpb, _bob_signer, _bob_pk) =
352            setup_client("Bob", ciphersuite, bob_provider);
353        let message_bundle = alice_group
354            .commit_builder()
355            .propose_adds([bob_kpb.key_package.clone()])
356            .load_psks(alice_provider.storage())
357            .unwrap()
358            .use_ratchet_tree_extension(true)
359            .build(
360                alice_provider.rand(),
361                alice_provider.crypto(),
362                &alice_signer,
363                |_proposal| true,
364            )
365            .unwrap()
366            .stage_commit(alice_provider)
367            .unwrap();
368
369        // Open, modify and re-seal Welcome
370        let welcome: FrankenWelcome = message_bundle.welcome().unwrap().clone().into();
371        let opened = welcome.open(bob_provider.crypto(), bob_provider.storage());
372        let (_ciphersuite, group_secrets, mut group_info) = opened.unwrap();
373
374        // Change the GroupInfo but keep it valid.
375        // Clients ignore unknown extensions (https://www.rfc-editor.org/rfc/rfc9420.html#section-13.4-5.3)
376        group_info.extensions.push(FrankenExtension::Unknown(
377            0xf000,
378            b"Test extension modifying group_info".into(),
379        ));
380
381        let resealed = FrankenWelcome::seal(
382            ciphersuite,
383            &group_secrets,
384            &group_info,
385            &alice_signer,
386            &[(
387                bob_kpb.key_package.clone().into(),
388                group_secrets.path_secret.clone(),
389            )],
390            alice_provider.crypto(),
391            alice_provider.storage(),
392        );
393
394        // validate Welcome message
395        let staged_welcome = StagedWelcome::new_from_welcome(
396            bob_provider,
397            alice_group.configuration(),
398            resealed.unwrap().into(),
399            None,
400        )
401        .expect("expected valid join from valid welcome");
402
403        let _bob_group = staged_welcome
404            .into_group(bob_provider)
405            .expect("expected valid group from join");
406    }
407
408    #[openmls_test::openmls_test]
409    fn test_reseal_helper_franken_group_info() {
410        // create Welcome message and GroupInfo
411        let alice_provider = &Provider::default();
412        let bob_provider = &Provider::default();
413        let (mut alice_group, _alice_credential, alice_signer, _alice_pk) =
414            setup_alice_group(ciphersuite, alice_provider);
415
416        let (_bob_credential, bob_kpb, _bob_signer, _bob_pk) =
417            setup_client("Bob", ciphersuite, bob_provider);
418        let message_bundle = alice_group
419            .commit_builder()
420            .propose_adds([bob_kpb.key_package.clone()])
421            .load_psks(alice_provider.storage())
422            .unwrap()
423            .use_ratchet_tree_extension(true)
424            .build(
425                alice_provider.rand(),
426                alice_provider.crypto(),
427                &alice_signer,
428                |_proposal| true,
429            )
430            .unwrap()
431            .stage_commit(alice_provider)
432            .unwrap();
433
434        // Open, modify and re-seal Welcome
435        let welcome: FrankenWelcome = message_bundle.welcome().unwrap().clone().into();
436        let welcome = welcome.with_sealed_update(
437            &alice_signer,
438            alice_provider,
439            &bob_kpb.key_package,
440            bob_provider,
441            // Change the GroupInfo but keep it valid.
442            // Clients ignore unknown extensions (https://www.rfc-editor.org/rfc/rfc9420.html#section-13.4-5.3)
443            |_, _, group_info| {
444                group_info.extensions.push(FrankenExtension::Unknown(
445                    0xf000,
446                    b"Test extension modifying group_info".into(),
447                ));
448            },
449        );
450
451        // validate Welcome message
452        let staged_welcome = StagedWelcome::new_from_welcome(
453            bob_provider,
454            alice_group.configuration(),
455            welcome.into(),
456            None,
457        )
458        .expect("expected valid join from valid welcome");
459
460        let _bob_group = staged_welcome
461            .into_group(bob_provider)
462            .expect("expected valid group from join");
463    }
464}