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<Self, 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                match resumption_psk.usage {
378                    ResumptionPskUsage::Application => {}
379                    ResumptionPskUsage::Reinit => {
380                        return Err(PskError::UsageMismatch {
381                            allowed: vec![ResumptionPskUsage::Application],
382                            got: resumption_psk.usage,
383                        });
384                    }
385                    ResumptionPskUsage::Branch => {
386                        // We can't check anything in here since we need more
387                        // information about the commit. We do this check
388                        // on the outside.
389                    }
390                }
391            }
392            Psk::External(_) => {}
393            #[cfg(feature = "extensions-draft")]
394            Psk::Application(_) => {}
395        };
396
397        // ValSem401
398        // https://validation.openmls.tech/#valn0803
399        {
400            let expected_nonce_length = ciphersuite.hash_length();
401            let got_nonce_length = self.psk_nonce().len();
402
403            if expected_nonce_length != got_nonce_length {
404                return Err(PskError::NonceLengthMismatch {
405                    expected: expected_nonce_length,
406                    got: got_nonce_length,
407                });
408            }
409        }
410
411        Ok(self)
412    }
413
414    pub(crate) fn validate_in_welcome(
415        psk_ids: &[PreSharedKeyId],
416        ciphersuite: Ciphersuite,
417    ) -> Result<(), PskError> {
418        let mut contains_branch_psk = false;
419        let mut contains_reinit_psk = false;
420        for id in psk_ids {
421            // https://validation.openmls.tech/#valn1401
422            match id.psk() {
423                Psk::Resumption(resumption_psk) => match resumption_psk.usage {
424                    ResumptionPskUsage::Application => {
425                        return Err(PskError::UsageMismatch {
426                            allowed: vec![ResumptionPskUsage::Reinit, ResumptionPskUsage::Branch],
427                            got: resumption_psk.usage,
428                        });
429                    }
430                    ResumptionPskUsage::Reinit => {
431                        if contains_reinit_psk {
432                            return Err(PskError::UsageDuplicate {
433                                usage: ResumptionPskUsage::Reinit,
434                            });
435                        }
436                        if contains_branch_psk {
437                            return Err(PskError::UsageConflict {
438                                first: ResumptionPskUsage::Reinit,
439                                second: ResumptionPskUsage::Branch,
440                            });
441                        }
442                        contains_reinit_psk = true;
443                    }
444                    ResumptionPskUsage::Branch => {
445                        if contains_branch_psk {
446                            return Err(PskError::UsageDuplicate {
447                                usage: ResumptionPskUsage::Branch,
448                            });
449                        }
450                        if contains_reinit_psk {
451                            return Err(PskError::UsageConflict {
452                                first: ResumptionPskUsage::Branch,
453                                second: ResumptionPskUsage::Reinit,
454                            });
455                        }
456                        contains_branch_psk = true;
457                    }
458                },
459                Psk::External(_) => {}
460                #[cfg(feature = "extensions-draft")]
461                Psk::Application(_) => {}
462            };
463
464            {
465                let expected_nonce_length = ciphersuite.hash_length();
466                let got_nonce_length = id.psk_nonce().len();
467
468                if expected_nonce_length != got_nonce_length {
469                    return Err(PskError::NonceLengthMismatch {
470                        expected: expected_nonce_length,
471                        got: got_nonce_length,
472                    });
473                }
474            }
475        }
476        Ok(())
477    }
478}
479
480#[cfg(test)]
481impl PreSharedKeyId {
482    pub(crate) fn new_with_nonce(psk: Psk, psk_nonce: Vec<u8>) -> Self {
483        Self {
484            psk,
485            psk_nonce: psk_nonce.into(),
486        }
487    }
488}
489
490/// `PskLabel` is used in the final concatentation of PSKs before they are
491/// injected in the key schedule.
492///
493/// ```c
494/// // draft-ietf-mls-protocol-19
495/// struct {
496///     PreSharedKeyID id;
497///     uint16 index;
498///     uint16 count;
499/// } PSKLabel;
500/// ```
501#[derive(TlsSerialize, TlsSize)]
502pub(crate) struct PskLabel<'a> {
503    pub(crate) id: &'a PreSharedKeyId,
504    pub(crate) index: u16,
505    pub(crate) count: u16,
506}
507
508impl<'a> PskLabel<'a> {
509    /// Create a new `PskLabel`
510    fn new(id: &'a PreSharedKeyId, index: u16, count: u16) -> Self {
511        Self { id, index, count }
512    }
513}
514
515/// This contains the `psk-secret` calculated from the PSKs contained in a
516/// Commit or a PreSharedKey proposal.
517#[derive(Clone)]
518pub struct PskSecret {
519    secret: Secret,
520}
521
522impl PskSecret {
523    /// Create a new `PskSecret` from PSK IDs and PSKs
524    ///
525    /// ```text
526    /// psk_extracted_[i] = KDF.Extract(0, psk_[i])
527    /// psk_input_[i] = ExpandWithLabel(psk_extracted_[i], "derived psk", PSKLabel, KDF.Nh)
528    ///
529    /// psk_secret_[0] = 0
530    /// psk_secret_[i] = KDF.Extract(psk_input[i-1], psk_secret_[i-1])
531    /// psk_secret     = psk_secret[n]
532    /// ```
533    pub(crate) fn new(
534        crypto: &impl OpenMlsCrypto,
535        ciphersuite: Ciphersuite,
536        psks: Vec<(impl Borrow<PreSharedKeyId>, Secret)>,
537    ) -> Result<Self, PskError> {
538        // Check that we don't have too many PSKs
539        let num_psks = u16::try_from(psks.len()).map_err(|_| PskError::TooManyKeys)?;
540
541        // Following comments are from `draft-ietf-mls-protocol-19`.
542        //
543        // psk_secret_[0] = 0
544        let mut psk_secret = Secret::zero(ciphersuite);
545
546        for (index, (psk_id, psk)) in psks.into_iter().enumerate() {
547            // psk_extracted_[i] = KDF.Extract(0, psk_[i])
548            let psk_extracted = {
549                let zero_secret = Secret::zero(ciphersuite);
550                zero_secret
551                    .hkdf_extract(crypto, ciphersuite, &psk)
552                    .map_err(LibraryError::unexpected_crypto_error)?
553            };
554
555            // psk_input_[i] = ExpandWithLabel( psk_extracted_[i], "derived psk", PSKLabel, KDF.Nh)
556            let psk_input = {
557                let psk_label = PskLabel::new(psk_id.borrow(), index as u16, num_psks)
558                    .tls_serialize_detached()
559                    .map_err(LibraryError::missing_bound_check)?;
560
561                psk_extracted
562                    .kdf_expand_label(
563                        crypto,
564                        ciphersuite,
565                        "derived psk",
566                        &psk_label,
567                        ciphersuite.hash_length(),
568                    )
569                    .map_err(LibraryError::unexpected_crypto_error)?
570            };
571
572            // psk_secret_[i] = KDF.Extract(psk_input_[i-1], psk_secret_[i-1])
573            psk_secret = psk_input
574                .hkdf_extract(crypto, ciphersuite, &psk_secret)
575                .map_err(LibraryError::unexpected_crypto_error)?;
576        }
577
578        Ok(Self { secret: psk_secret })
579    }
580
581    /// Return the inner secret
582    pub(crate) fn secret(&self) -> &Secret {
583        &self.secret
584    }
585
586    #[cfg(any(feature = "test-utils", feature = "crypto-debug", test))]
587    pub(crate) fn as_slice(&self) -> &[u8] {
588        self.secret.as_slice()
589    }
590}
591
592#[cfg(any(feature = "test-utils", test))]
593impl From<Secret> for PskSecret {
594    fn from(secret: Secret) -> Self {
595        Self { secret }
596    }
597}
598
599/// Load PSKs from storage
600pub(crate) fn load_psks<'p, Storage: StorageProvider>(
601    storage: &Storage,
602    resumption_psk_store: &ResumptionPskStore,
603    psk_ids: &'p [PreSharedKeyId],
604) -> Result<Vec<(&'p PreSharedKeyId, Secret)>, PskError> {
605    let mut psk_bundles = Vec::new();
606
607    for psk_id in psk_ids.iter() {
608        log_crypto!(trace, "PSK store {:?}", resumption_psk_store);
609
610        match &psk_id.psk {
611            Psk::Resumption(resumption) => {
612                let psk_epoch = match resumption.usage() {
613                    // Application and Reinit PSKs are looked up by their own epoch.
614                    ResumptionPskUsage::Application | ResumptionPskUsage::Reinit => {
615                        resumption.psk_epoch()
616                    }
617                    // The branch PSK is not in this group's resumption store: it
618                    // comes from the parent group and is injected at the sentinel
619                    // epoch 0 (see `CommitBuilder::branch` and
620                    // `ProcessedWelcome::new_from_welcome_inner`).
621                    ResumptionPskUsage::Branch => 0.into(),
622                };
623                if let Some(psk_bundle) = resumption_psk_store.get(psk_epoch) {
624                    psk_bundles.push((psk_id, psk_bundle.secret.clone()));
625                } else {
626                    return Err(PskError::KeyNotFound);
627                }
628            }
629            Psk::External(_) => {
630                let psk_bundle: Option<PskBundle> = storage
631                    .psk(psk_id.psk())
632                    .map_err(|_| PskError::KeyNotFound)?;
633                if let Some(psk_bundle) = psk_bundle {
634                    psk_bundles.push((psk_id, psk_bundle.secret));
635                } else {
636                    return Err(PskError::KeyNotFound);
637                }
638            }
639            #[cfg(feature = "extensions-draft")]
640            Psk::Application(_) => {
641                let psk_bundle: Option<PskBundle> = storage
642                    .psk(psk_id.psk())
643                    .map_err(|_| PskError::KeyNotFound)?;
644                if let Some(psk_bundle) = psk_bundle {
645                    psk_bundles.push((psk_id, psk_bundle.secret));
646                } else {
647                    return Err(PskError::KeyNotFound);
648                }
649            }
650        }
651    }
652
653    Ok(psk_bundles)
654}
655
656/// This module contains a store that can hold a rollover list of resumption PSKs.
657pub mod store {
658    use serde::{Deserialize, Serialize};
659
660    use crate::{group::GroupEpoch, schedule::ResumptionPskSecret};
661
662    /// Resumption PSK store.
663    ///
664    /// This is where the resumption PSKs are kept in a rollover list.
665    #[derive(Debug, Serialize, Deserialize)]
666    #[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
667    pub(crate) struct ResumptionPskStore {
668        max_number_of_secrets: usize,
669        resumption_psk: Vec<(GroupEpoch, ResumptionPskSecret)>,
670        cursor: usize,
671    }
672
673    impl ResumptionPskStore {
674        /// Creates a new store with a given maximum size of `number_of_secrets`.
675        pub(crate) fn new(max_number_of_secrets: usize) -> Self {
676            Self {
677                max_number_of_secrets,
678                resumption_psk: vec![],
679                cursor: 0,
680            }
681        }
682
683        /// Clear all PSKs.
684        pub(crate) fn clear(&mut self) {
685            self.resumption_psk = vec![];
686            self.cursor = 0;
687        }
688
689        /// Adds a new entry to the store.
690        pub(crate) fn add(&mut self, epoch: GroupEpoch, resumption_psk: ResumptionPskSecret) {
691            if self.max_number_of_secrets == 0 {
692                return;
693            }
694            let item = (epoch, resumption_psk);
695            if self.resumption_psk.len() < self.max_number_of_secrets {
696                self.resumption_psk.push(item);
697                self.cursor += 1;
698            } else {
699                self.cursor += 1;
700                self.cursor %= self.resumption_psk.len();
701                self.resumption_psk[self.cursor] = item;
702            }
703        }
704
705        /// Searches an entry for a given epoch number and if found, returns the
706        /// corresponding resumption psk.
707        pub(crate) fn get(&self, epoch: GroupEpoch) -> Option<&ResumptionPskSecret> {
708            self.resumption_psk
709                .iter()
710                .find(|&(e, _s)| e == &epoch)
711                .map(|(_e, s)| s)
712        }
713    }
714
715    #[cfg(test)]
716    impl ResumptionPskStore {
717        pub(crate) fn cursor(&self) -> usize {
718            self.cursor
719        }
720    }
721}