Skip to main content

openmls/schedule/
psk.rs

1//! # Preshared keys.
2
3use std::borrow::Borrow;
4
5use openmls_traits::{random::OpenMlsRand, storage::StorageProvider as StorageProviderTrait};
6use serde::{Deserialize, Serialize};
7use tls_codec::{Serialize as TlsSerializeTrait, VLBytes};
8
9use super::*;
10#[cfg(feature = "extensions-draft")]
11use crate::component::ComponentId;
12use crate::{
13    group::{GroupEpoch, GroupId},
14    schedule::psk::store::ResumptionPskStore,
15    storage::{OpenMlsProvider, StorageProvider},
16};
17
18/// Resumption PSK usage.
19///
20/// ```c
21/// // draft-ietf-mls-protocol-19
22/// enum {
23///   reserved(0),
24///   application(1),
25///   reinit(2),
26///   branch(3),
27///   (255)
28/// } ResumptionPSKUsage;
29/// ```
30#[derive(
31    Clone,
32    Copy,
33    Debug,
34    PartialEq,
35    Eq,
36    PartialOrd,
37    Ord,
38    Hash,
39    Deserialize,
40    Serialize,
41    TlsDeserialize,
42    TlsDeserializeBytes,
43    TlsSerialize,
44    TlsSize,
45)]
46#[repr(u8)]
47pub enum ResumptionPskUsage {
48    /// Application.
49    Application = 1,
50    /// Resumption PSK used for group reinitialization.
51    ///
52    /// Note: "Resumption PSKs with usage `reinit` MUST NOT be used in other contexts (than reinitialization)."
53    Reinit = 2,
54    /// Resumption PSK used for subgroup branching.
55    ///
56    /// Note: "Resumption PSKs with usage `branch` MUST NOT be used in other contexts (than subgroup branching)."
57    Branch = 3,
58}
59
60/// External PSK.
61#[derive(
62    Debug,
63    PartialEq,
64    Eq,
65    PartialOrd,
66    Ord,
67    Clone,
68    Hash,
69    Deserialize,
70    Serialize,
71    TlsDeserialize,
72    TlsDeserializeBytes,
73    TlsSerialize,
74    TlsSize,
75)]
76pub struct ExternalPsk {
77    psk_id: VLBytes,
78}
79
80impl ExternalPsk {
81    /// Create a new `ExternalPsk` from a PSK ID
82    pub fn new(psk_id: Vec<u8>) -> Self {
83        Self {
84            psk_id: psk_id.into(),
85        }
86    }
87
88    /// Return the PSK ID
89    pub fn psk_id(&self) -> &[u8] {
90        self.psk_id.as_slice()
91    }
92}
93
94/// Contains the secret part of the PSK as well as the
95/// public part that is used as a marker for injection into the key schedule.
96#[derive(Serialize, Deserialize, TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize)]
97pub(crate) struct PskBundle {
98    secret: Secret,
99}
100
101/// Resumption PSK.
102#[derive(
103    Clone,
104    Debug,
105    PartialEq,
106    Eq,
107    PartialOrd,
108    Ord,
109    Deserialize,
110    Serialize,
111    TlsDeserialize,
112    TlsDeserializeBytes,
113    TlsSerialize,
114    TlsSize,
115    Hash,
116)]
117pub struct ResumptionPsk {
118    pub(crate) usage: ResumptionPskUsage,
119    pub(crate) psk_group_id: GroupId,
120    pub(crate) psk_epoch: GroupEpoch,
121}
122
123impl ResumptionPsk {
124    /// Create a new `ResumptionPsk`
125    pub fn new(usage: ResumptionPskUsage, psk_group_id: GroupId, psk_epoch: GroupEpoch) -> Self {
126        Self {
127            usage,
128            psk_group_id,
129            psk_epoch,
130        }
131    }
132
133    /// Return the usage
134    pub fn usage(&self) -> ResumptionPskUsage {
135        self.usage
136    }
137
138    /// Return the `GroupId`
139    pub fn psk_group_id(&self) -> &GroupId {
140        &self.psk_group_id
141    }
142
143    /// Return the `GroupEpoch`
144    pub fn psk_epoch(&self) -> GroupEpoch {
145        self.psk_epoch
146    }
147}
148
149/// Application PSK according to the [draft-ietf-mls-extensions]
150///
151/// [draft-ietf-mls-protocol]: https://datatracker.ietf.org/doc/html/draft-ietf-mls-extensions-09#name-pre-shared-keys-psks
152#[cfg(feature = "extensions-draft")]
153#[derive(
154    Clone,
155    Debug,
156    PartialEq,
157    Eq,
158    PartialOrd,
159    Ord,
160    Deserialize,
161    Serialize,
162    TlsDeserialize,
163    TlsDeserializeBytes,
164    TlsSerialize,
165    TlsSize,
166    Hash,
167)]
168pub struct ApplicationPsk {
169    pub(crate) component_id: ComponentId,
170    pub(crate) psk_id: VLBytes,
171}
172
173#[cfg(feature = "extensions-draft")]
174impl ApplicationPsk {
175    /// Create a new `ApplicationPsk`
176    pub fn new(component_id: ComponentId, psk_id: VLBytes) -> Self {
177        Self {
178            component_id,
179            psk_id,
180        }
181    }
182
183    /// Return the `ComponentId`
184    pub fn component_id(&self) -> ComponentId {
185        self.component_id
186    }
187
188    /// Return the `psk_id`
189    pub fn psk_id(&self) -> &[u8] {
190        self.psk_id.as_slice()
191    }
192}
193
194/// The different PSK types.
195#[derive(
196    Clone,
197    Debug,
198    PartialEq,
199    Eq,
200    PartialOrd,
201    Ord,
202    Deserialize,
203    Serialize,
204    TlsDeserialize,
205    TlsDeserializeBytes,
206    TlsSerialize,
207    TlsSize,
208    Hash,
209)]
210#[repr(u8)]
211pub enum Psk {
212    /// An external PSK provided by the application.
213    #[tls_codec(discriminant = 1)]
214    External(ExternalPsk),
215    /// A resumption PSK derived from the MLS key schedule.
216    #[tls_codec(discriminant = 2)]
217    Resumption(ResumptionPsk),
218    #[cfg(feature = "extensions-draft")]
219    /// An application component PSK.
220    #[tls_codec(discriminant = 3)]
221    Application(ApplicationPsk),
222}
223
224/// ```c
225/// // RFC 9420 and draft-ietf-mls-extensions-09
226/// enum {
227///   reserved(0),
228///   external(1),
229///   resumption(2),
230///   // draft-ietf-mls-extensions-09
231///   application(3),
232///   (255)
233/// } PSKType;
234/// ```
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
236#[repr(u8)]
237pub enum PskType {
238    /// An external PSK.
239    External = 1,
240    /// A resumption PSK.
241    Resumption = 2,
242    #[cfg(feature = "extensions-draft")]
243    /// An application component PSK.
244    Application = 3,
245}
246
247/// A `PreSharedKeyID` is used to uniquely identify the PSKs that get injected
248/// in the key schedule.
249///
250/// ```c
251/// // draft-ietf-mls-protocol-19
252/// struct {
253///   PSKType psktype;
254///   select (PreSharedKeyID.psktype) {
255///     case external:
256///       opaque psk_id<V>;
257///
258///     case resumption:
259///       ResumptionPSKUsage usage;
260///       opaque psk_group_id<V>;
261///       uint64 psk_epoch;
262///   };
263///   opaque psk_nonce<V>;
264/// } PreSharedKeyID;
265/// ```
266#[derive(
267    Clone,
268    Debug,
269    PartialEq,
270    Eq,
271    PartialOrd,
272    Ord,
273    Deserialize,
274    Serialize,
275    TlsDeserialize,
276    TlsDeserializeBytes,
277    TlsSerialize,
278    TlsSize,
279    Hash,
280)]
281pub struct PreSharedKeyId {
282    pub(crate) psk: Psk,
283    pub(crate) psk_nonce: VLBytes,
284}
285
286impl PreSharedKeyId {
287    /// Construct a `PreSharedKeyID` with a random nonce.
288    pub fn new(
289        ciphersuite: Ciphersuite,
290        rand: &impl OpenMlsRand,
291        psk: Psk,
292    ) -> Result<Self, CryptoError> {
293        let psk_nonce = rand
294            .random_vec(ciphersuite.hash_length())
295            .map_err(|_| CryptoError::InsufficientRandomness)?
296            .into();
297
298        Ok(Self { psk, psk_nonce })
299    }
300
301    /// Construct an external `PreSharedKeyID`.
302    pub fn external(psk_id: Vec<u8>, psk_nonce: Vec<u8>) -> Self {
303        let psk = Psk::External(ExternalPsk::new(psk_id));
304
305        Self {
306            psk,
307            psk_nonce: psk_nonce.into(),
308        }
309    }
310
311    /// Construct a resumption `PreSharedKeyID`.
312    pub fn resumption(
313        usage: ResumptionPskUsage,
314        psk_group_id: GroupId,
315        psk_epoch: GroupEpoch,
316        psk_nonce: Vec<u8>,
317    ) -> Self {
318        let psk = Psk::Resumption(ResumptionPsk::new(usage, psk_group_id, psk_epoch));
319
320        Self {
321            psk,
322            psk_nonce: psk_nonce.into(),
323        }
324    }
325
326    /// Construct an application `PreSharedKeyID`.
327    #[cfg(feature = "extensions-draft")]
328    pub fn application(component_id: ComponentId, psk_id: Vec<u8>, psk_nonce: Vec<u8>) -> Self {
329        let psk = Psk::Application(ApplicationPsk::new(component_id, psk_id.into()));
330
331        Self {
332            psk,
333            psk_nonce: psk_nonce.into(),
334        }
335    }
336
337    /// Return the PSK.
338    pub fn psk(&self) -> &Psk {
339        &self.psk
340    }
341
342    /// Return the PSK nonce.
343    pub fn psk_nonce(&self) -> &[u8] {
344        self.psk_nonce.as_slice()
345    }
346
347    // ----- Key Store -----------------------------------------------------------------------------
348
349    /// Save this `PreSharedKeyId` in the keystore.
350    ///
351    /// Note: The nonce is not saved as it must be unique for each time it's being applied.
352    pub fn store<Provider: OpenMlsProvider>(
353        &self,
354        provider: &Provider,
355        psk: &[u8],
356    ) -> Result<(), PskError> {
357        let psk_bundle = {
358            let secret = Secret::from_slice(psk);
359
360            PskBundle { secret }
361        };
362
363        provider
364            .storage()
365            .write_psk(&self.psk, &psk_bundle)
366            .map_err(|_| PskError::Storage)
367    }
368
369    // ----- Validation ----------------------------------------------------------------------------
370
371    pub(crate) fn validate_in_proposal(self, ciphersuite: Ciphersuite) -> Result<(), PskError> {
372        // ValSem402
373        match self.psk() {
374            Psk::Resumption(resumption_psk) => {
375                // https://validation.openmls.tech/#valn0801
376                // https://validation.openmls.tech/#valn0802
377                if resumption_psk.usage != ResumptionPskUsage::Application {
378                    return Err(PskError::UsageMismatch {
379                        allowed: vec![ResumptionPskUsage::Application],
380                        got: resumption_psk.usage,
381                    });
382                }
383            }
384            Psk::External(_) => {}
385            #[cfg(feature = "extensions-draft")]
386            Psk::Application(_) => {}
387        };
388
389        // ValSem401
390        // https://validation.openmls.tech/#valn0803
391        {
392            let expected_nonce_length = ciphersuite.hash_length();
393            let got_nonce_length = self.psk_nonce().len();
394
395            if expected_nonce_length != got_nonce_length {
396                return Err(PskError::NonceLengthMismatch {
397                    expected: expected_nonce_length,
398                    got: got_nonce_length,
399                });
400            }
401        }
402
403        Ok(())
404    }
405
406    pub(crate) fn validate_in_welcome(
407        psk_ids: &[PreSharedKeyId],
408        ciphersuite: Ciphersuite,
409    ) -> Result<(), PskError> {
410        let mut contains_branch_psk = false;
411        let mut contains_reinit_psk = false;
412        for id in psk_ids {
413            // https://validation.openmls.tech/#valn1401
414            match id.psk() {
415                Psk::Resumption(resumption_psk) => match resumption_psk.usage {
416                    ResumptionPskUsage::Application => {
417                        return Err(PskError::UsageMismatch {
418                            allowed: vec![ResumptionPskUsage::Reinit, ResumptionPskUsage::Branch],
419                            got: resumption_psk.usage,
420                        });
421                    }
422                    ResumptionPskUsage::Reinit => {
423                        if contains_reinit_psk {
424                            return Err(PskError::UsageDuplicate {
425                                usage: ResumptionPskUsage::Reinit,
426                            });
427                        }
428                        if contains_branch_psk {
429                            return Err(PskError::UsageConflict {
430                                first: ResumptionPskUsage::Reinit,
431                                second: ResumptionPskUsage::Branch,
432                            });
433                        }
434                        contains_reinit_psk = true;
435                    }
436                    ResumptionPskUsage::Branch => {
437                        if contains_branch_psk {
438                            return Err(PskError::UsageDuplicate {
439                                usage: ResumptionPskUsage::Branch,
440                            });
441                        }
442                        if contains_reinit_psk {
443                            return Err(PskError::UsageConflict {
444                                first: ResumptionPskUsage::Branch,
445                                second: ResumptionPskUsage::Reinit,
446                            });
447                        }
448                        contains_branch_psk = true;
449                    }
450                },
451                Psk::External(_) => {}
452                #[cfg(feature = "extensions-draft")]
453                Psk::Application(_) => {}
454            };
455
456            {
457                let expected_nonce_length = ciphersuite.hash_length();
458                let got_nonce_length = id.psk_nonce().len();
459
460                if expected_nonce_length != got_nonce_length {
461                    return Err(PskError::NonceLengthMismatch {
462                        expected: expected_nonce_length,
463                        got: got_nonce_length,
464                    });
465                }
466            }
467        }
468        Ok(())
469    }
470}
471
472#[cfg(test)]
473impl PreSharedKeyId {
474    pub(crate) fn new_with_nonce(psk: Psk, psk_nonce: Vec<u8>) -> Self {
475        Self {
476            psk,
477            psk_nonce: psk_nonce.into(),
478        }
479    }
480}
481
482/// `PskLabel` is used in the final concatentation of PSKs before they are
483/// injected in the key schedule.
484///
485/// ```c
486/// // draft-ietf-mls-protocol-19
487/// struct {
488///     PreSharedKeyID id;
489///     uint16 index;
490///     uint16 count;
491/// } PSKLabel;
492/// ```
493#[derive(TlsSerialize, TlsSize)]
494pub(crate) struct PskLabel<'a> {
495    pub(crate) id: &'a PreSharedKeyId,
496    pub(crate) index: u16,
497    pub(crate) count: u16,
498}
499
500impl<'a> PskLabel<'a> {
501    /// Create a new `PskLabel`
502    fn new(id: &'a PreSharedKeyId, index: u16, count: u16) -> Self {
503        Self { id, index, count }
504    }
505}
506
507/// This contains the `psk-secret` calculated from the PSKs contained in a
508/// Commit or a PreSharedKey proposal.
509#[derive(Clone)]
510pub struct PskSecret {
511    secret: Secret,
512}
513
514impl PskSecret {
515    /// Create a new `PskSecret` from PSK IDs and PSKs
516    ///
517    /// ```text
518    /// psk_extracted_[i] = KDF.Extract(0, psk_[i])
519    /// psk_input_[i] = ExpandWithLabel(psk_extracted_[i], "derived psk", PSKLabel, KDF.Nh)
520    ///
521    /// psk_secret_[0] = 0
522    /// psk_secret_[i] = KDF.Extract(psk_input[i-1], psk_secret_[i-1])
523    /// psk_secret     = psk_secret[n]
524    /// ```
525    pub(crate) fn new(
526        crypto: &impl OpenMlsCrypto,
527        ciphersuite: Ciphersuite,
528        psks: Vec<(impl Borrow<PreSharedKeyId>, Secret)>,
529    ) -> Result<Self, PskError> {
530        // Check that we don't have too many PSKs
531        let num_psks = u16::try_from(psks.len()).map_err(|_| PskError::TooManyKeys)?;
532
533        // Following comments are from `draft-ietf-mls-protocol-19`.
534        //
535        // psk_secret_[0] = 0
536        let mut psk_secret = Secret::zero(ciphersuite);
537
538        for (index, (psk_id, psk)) in psks.into_iter().enumerate() {
539            // psk_extracted_[i] = KDF.Extract(0, psk_[i])
540            let psk_extracted = {
541                let zero_secret = Secret::zero(ciphersuite);
542                zero_secret
543                    .hkdf_extract(crypto, ciphersuite, &psk)
544                    .map_err(LibraryError::unexpected_crypto_error)?
545            };
546
547            // psk_input_[i] = ExpandWithLabel( psk_extracted_[i], "derived psk", PSKLabel, KDF.Nh)
548            let psk_input = {
549                let psk_label = PskLabel::new(psk_id.borrow(), index as u16, num_psks)
550                    .tls_serialize_detached()
551                    .map_err(LibraryError::missing_bound_check)?;
552
553                psk_extracted
554                    .kdf_expand_label(
555                        crypto,
556                        ciphersuite,
557                        "derived psk",
558                        &psk_label,
559                        ciphersuite.hash_length(),
560                    )
561                    .map_err(LibraryError::unexpected_crypto_error)?
562            };
563
564            // psk_secret_[i] = KDF.Extract(psk_input_[i-1], psk_secret_[i-1])
565            psk_secret = psk_input
566                .hkdf_extract(crypto, ciphersuite, &psk_secret)
567                .map_err(LibraryError::unexpected_crypto_error)?;
568        }
569
570        Ok(Self { secret: psk_secret })
571    }
572
573    /// Return the inner secret
574    pub(crate) fn secret(&self) -> &Secret {
575        &self.secret
576    }
577
578    #[cfg(any(feature = "test-utils", feature = "crypto-debug", test))]
579    pub(crate) fn as_slice(&self) -> &[u8] {
580        self.secret.as_slice()
581    }
582}
583
584#[cfg(any(feature = "test-utils", test))]
585impl From<Secret> for PskSecret {
586    fn from(secret: Secret) -> Self {
587        Self { secret }
588    }
589}
590
591pub(crate) fn load_psks<'p, Storage: StorageProvider>(
592    storage: &Storage,
593    resumption_psk_store: &ResumptionPskStore,
594    psk_ids: &'p [PreSharedKeyId],
595) -> Result<Vec<(&'p PreSharedKeyId, Secret)>, PskError> {
596    let mut psk_bundles = Vec::new();
597
598    for psk_id in psk_ids.iter() {
599        log_crypto!(trace, "PSK store {:?}", resumption_psk_store);
600
601        match &psk_id.psk {
602            Psk::Resumption(resumption) => {
603                if let Some(psk_bundle) = resumption_psk_store.get(resumption.psk_epoch()) {
604                    psk_bundles.push((psk_id, psk_bundle.secret.clone()));
605                } else {
606                    return Err(PskError::KeyNotFound);
607                }
608            }
609            Psk::External(_) => {
610                let psk_bundle: Option<PskBundle> = storage
611                    .psk(psk_id.psk())
612                    .map_err(|_| PskError::KeyNotFound)?;
613                if let Some(psk_bundle) = psk_bundle {
614                    psk_bundles.push((psk_id, psk_bundle.secret));
615                } else {
616                    return Err(PskError::KeyNotFound);
617                }
618            }
619            #[cfg(feature = "extensions-draft")]
620            Psk::Application(_) => {
621                let psk_bundle: Option<PskBundle> = storage
622                    .psk(psk_id.psk())
623                    .map_err(|_| PskError::KeyNotFound)?;
624                if let Some(psk_bundle) = psk_bundle {
625                    psk_bundles.push((psk_id, psk_bundle.secret));
626                } else {
627                    return Err(PskError::KeyNotFound);
628                }
629            }
630        }
631    }
632
633    Ok(psk_bundles)
634}
635
636/// This module contains a store that can hold a rollover list of resumption PSKs.
637pub mod store {
638    use serde::{Deserialize, Serialize};
639
640    use crate::{group::GroupEpoch, schedule::ResumptionPskSecret};
641
642    /// Resumption PSK store.
643    ///
644    /// This is where the resumption PSKs are kept in a rollover list.
645    #[derive(Debug, Serialize, Deserialize)]
646    #[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
647    pub(crate) struct ResumptionPskStore {
648        max_number_of_secrets: usize,
649        resumption_psk: Vec<(GroupEpoch, ResumptionPskSecret)>,
650        cursor: usize,
651    }
652
653    impl ResumptionPskStore {
654        /// Creates a new store with a given maximum size of `number_of_secrets`.
655        pub(crate) fn new(max_number_of_secrets: usize) -> Self {
656            Self {
657                max_number_of_secrets,
658                resumption_psk: vec![],
659                cursor: 0,
660            }
661        }
662
663        /// Adds a new entry to the store.
664        pub(crate) fn add(&mut self, epoch: GroupEpoch, resumption_psk: ResumptionPskSecret) {
665            if self.max_number_of_secrets == 0 {
666                return;
667            }
668            let item = (epoch, resumption_psk);
669            if self.resumption_psk.len() < self.max_number_of_secrets {
670                self.resumption_psk.push(item);
671                self.cursor += 1;
672            } else {
673                self.cursor += 1;
674                self.cursor %= self.resumption_psk.len();
675                self.resumption_psk[self.cursor] = item;
676            }
677        }
678
679        /// Searches an entry for a given epoch number and if found, returns the
680        /// corresponding resumption psk.
681        pub(crate) fn get(&self, epoch: GroupEpoch) -> Option<&ResumptionPskSecret> {
682            self.resumption_psk
683                .iter()
684                .find(|&(e, _s)| e == &epoch)
685                .map(|(_e, s)| s)
686        }
687    }
688
689    #[cfg(test)]
690    impl ResumptionPskStore {
691        pub(crate) fn cursor(&self) -> usize {
692            self.cursor
693        }
694    }
695}