Skip to main content

rs_matter/
fabric.rs

1/*
2 *
3 *    Copyright (c) 2022-2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18use core::mem::MaybeUninit;
19use core::num::NonZeroU8;
20
21use cfg_if::cfg_if;
22use heapless::String;
23
24use crate::acl::{self, AccessReq, AclEntry, AuthMode};
25use crate::cert::{CertRef, MAX_CERT_TLV_LEN};
26use crate::crypto::{
27    CanonAeadKeyRef, CanonPkcPublicKeyRef, CanonPkcSecretKey, CanonPkcSecretKeyRef, Crypto,
28    CryptoSensitive, Digest, Hash, Kdf, PKC_CANON_PUBLIC_KEY_LEN,
29};
30use crate::dm::Privilege;
31use crate::error::{Error, ErrorCode};
32use crate::group_keys::KeySet;
33use crate::persist::{KvBlobStore, KvBlobStoreAccess, Persist, FABRIC_KEYS_START};
34#[cfg(feature = "groups")]
35use crate::tlv::Skippable;
36use crate::tlv::{FromTLV, TLVElement, ToTLV};
37use crate::transport::network::MatterLocalService;
38use crate::utils::init::{init, Init, InitMaybeUninit, IntoFallibleInit};
39use crate::utils::storage::Vec;
40
41const COMPRESSED_FABRIC_ID_LEN: usize = 8;
42
43/// All multicast-group fabric state: the group key sets, the group→keyset
44/// mapping, and the group table. Gated as one inline module so the whole block
45/// (consts, TLV structs and the `Groups` container) is compiled out with a
46/// single `#[cfg]` when the `groups` feature is off, and re-exported so the rest
47/// of `fabric` refers to these items unqualified.
48#[cfg(feature = "groups")]
49mod groups {
50    use core::str::FromStr;
51
52    use cfg_if::cfg_if;
53
54    use heapless::String;
55
56    use crate::dm::clusters::decl::groupcast::MulticastAddrPolicyEnum;
57    use crate::error::{Error, ErrorCode};
58    use crate::group_keys::GroupKeySet;
59    use crate::tlv::{FromTLV, ToTLV};
60    use crate::utils::init::{init, Init, InitDefault};
61    use crate::utils::storage::Vec;
62
63    cfg_if! {
64        if #[cfg(feature = "max-group-keys-per-fabric-5")] {
65            /// Max number of group key sets per fabric (excluding IPK at index 0).
66            pub const MAX_GROUP_KEYS_PER_FABRIC: usize = 5;
67        } else if #[cfg(feature = "max-group-keys-per-fabric-4")] {
68            /// Max number of group key sets per fabric (excluding IPK at index 0).
69            pub const MAX_GROUP_KEYS_PER_FABRIC: usize = 4;
70        } else if #[cfg(feature = "max-group-keys-per-fabric-3")] {
71            /// Max number of group key sets per fabric (excluding IPK at index 0).
72            pub const MAX_GROUP_KEYS_PER_FABRIC: usize = 3;
73        } else if #[cfg(feature = "max-group-keys-per-fabric-2")] {
74            /// Max number of group key sets per fabric (excluding IPK at index 0).
75            pub const MAX_GROUP_KEYS_PER_FABRIC: usize = 2;
76        } else { // Matter requires a minimum of 3 group key sets per fabric
77            /// Max number of group key sets per fabric (excluding IPK at index 0).
78            pub const MAX_GROUP_KEYS_PER_FABRIC: usize = 3;
79        }
80    }
81
82    /// Max length of a group name (per Matter spec).
83    pub const MAX_GROUP_NAME_LEN: usize = 16;
84
85    cfg_if! {
86        if #[cfg(feature = "max-groups-per-fabric-32")] {
87            /// Max number of group key map entries per fabric.
88            pub const MAX_GROUPS_PER_FABRIC: usize = 32;
89        } else if #[cfg(feature = "max-groups-per-fabric-16")] {
90            /// Max number of group key map entries per fabric.
91            pub const MAX_GROUPS_PER_FABRIC: usize = 16;
92        } else if #[cfg(feature = "max-groups-per-fabric-12")] {
93            /// Max number of group key map entries per fabric.
94            pub const MAX_GROUPS_PER_FABRIC: usize = 12;
95        } else if #[cfg(feature = "max-groups-per-fabric-8")] {
96            /// Max number of group key map entries per fabric.
97            pub const MAX_GROUPS_PER_FABRIC: usize = 9;
98        } else if #[cfg(feature = "max-groups-per-fabric-7")] {
99            /// Max number of group key map entries per fabric.
100            pub const MAX_GROUPS_PER_FABRIC: usize = 7;
101        } else if #[cfg(feature = "max-groups-per-fabric-6")] {
102            /// Max number of group key map entries per fabric.
103            pub const MAX_GROUPS_PER_FABRIC: usize = 6;
104        } else if #[cfg(feature = "max-groups-per-fabric-5")] {
105            /// Max number of group key map entries per fabric.
106            pub const MAX_GROUPS_PER_FABRIC: usize = 5;
107        } else if #[cfg(feature = "max-groups-per-fabric-4")] {
108            /// Max number of group key map entries per fabric.
109            pub const MAX_GROUPS_PER_FABRIC: usize = 4;
110        } else { // Matter requires a minimum of 4 group table entries per fabric
111            /// Max number of group key map entries per fabric.
112            pub const MAX_GROUPS_PER_FABRIC: usize = 4;
113        }
114    }
115
116    cfg_if! {
117        if #[cfg(feature = "max-group-endpoints-per-fabric-5")] {
118            /// Max number of endpoints per group entry.
119            pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 5;
120        } else if #[cfg(feature = "max-group-endpoints-per-fabric-4")] {
121            /// Max number of endpoints per group entry.
122            pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 4;
123        } else if #[cfg(feature = "max-group-endpoints-per-fabric-3")] {
124            /// Max number of endpoints per group entry.
125            pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 3;
126        } else if #[cfg(feature = "max-group-endpoints-per-fabric-2")] {
127            /// Max number of endpoints per group entry.
128            pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 2;
129        } else if #[cfg(feature = "max-group-endpoints-per-fabric-1")] {
130            /// Max number of endpoints per group entry.
131            pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 1;
132        } else { // Default: 3 endpoints per group entry
133            /// Max number of endpoints per group entry.
134            pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 3;
135        }
136    }
137
138    /// A group table entry mapping a group ID to its endpoints and name.
139    #[derive(Debug, FromTLV, ToTLV)]
140    #[cfg_attr(feature = "defmt", derive(defmt::Format))]
141    pub struct GroupEndpointMapping {
142        pub group_id: u16,
143        pub endpoints: Vec<u16, GROUP_ENDPOINTS_PER_FABRIC>,
144        pub group_name: String<MAX_GROUP_NAME_LEN>,
145        /// Whether the (Groupcast-managed) group has auxiliary ACL entries
146        /// generated for its endpoints - see the Groupcast cluster's
147        /// `ConfigureAuxiliaryACL` command and the `AuxiliaryACL` attribute
148        /// of the Access Control cluster.
149        ///
150        /// `None` (in blobs persisted before the field existed) means `false`.
151        pub has_aux_acl: Option<bool>,
152        /// The multicast-address policy of the group, when it is managed by
153        /// the Groupcast cluster.
154        ///
155        /// `None` means the group was created via the legacy Groups cluster,
156        /// which behaves like the `PerGroup` policy (such nodes join the
157        /// fabric+group-scoped multicast address) - the `PerGroup` policy
158        /// exists precisely for interop with them.
159        pub mcast_policy: Option<MulticastAddrPolicyEnum>,
160    }
161
162    impl GroupEndpointMapping {
163        /// Whether the group has auxiliary ACL entries generated for its
164        /// endpoints.
165        pub fn has_aux_acl(&self) -> bool {
166            self.has_aux_acl.unwrap_or(false)
167        }
168
169        /// The effective multicast-address policy of the group (legacy
170        /// Groups-cluster entries behave as `PerGroup`).
171        pub fn effective_mcast_policy(&self) -> MulticastAddrPolicyEnum {
172            self.mcast_policy
173                .unwrap_or(MulticastAddrPolicyEnum::PerGroup)
174        }
175
176        /// Whether the group is managed by the Groupcast cluster (as opposed
177        /// to the legacy Groups cluster).
178        pub fn groupcast_managed(&self) -> bool {
179            self.mcast_policy.is_some()
180        }
181    }
182
183    /// A stored group key map entry (maps group ID to key set).
184    #[derive(Debug, Clone, Default, FromTLV, ToTLV)]
185    #[cfg_attr(feature = "defmt", derive(defmt::Format))]
186    pub struct GroupKeyMapping {
187        pub group_id: u16,
188        pub group_key_set_id: u16,
189    }
190
191    #[derive(Debug, FromTLV, ToTLV)]
192    #[cfg_attr(feature = "defmt", derive(defmt::Format))]
193    pub struct Groups {
194        /// Group key sets (excluding IPK which is stored in `ipk`)
195        key_sets: Vec<GroupKeySet, MAX_GROUP_KEYS_PER_FABRIC>,
196        /// Groups keyset mapping
197        key_map: Vec<GroupKeyMapping, MAX_GROUPS_PER_FABRIC>,
198        /// Group table (group ID → endpoints + name)
199        endpoint_mapping: Vec<GroupEndpointMapping, MAX_GROUPS_PER_FABRIC>,
200    }
201
202    impl Groups {
203        pub(crate) const fn new() -> Self {
204            Self {
205                key_sets: Vec::new(),
206                key_map: Vec::new(),
207                endpoint_mapping: Vec::new(),
208            }
209        }
210
211        pub(crate) fn init() -> impl Init<Self> {
212            init!(Self {
213                key_sets <- Vec::init(),
214                key_map <- Vec::init(),
215                endpoint_mapping <- Vec::init(),
216            })
217        }
218
219        /// Return an iterator over the group key sets of the fabric
220        pub fn key_set_iter(&self) -> impl Iterator<Item = &GroupKeySet> {
221            self.key_sets.iter()
222        }
223
224        /// Find a group key set by ID
225        pub fn key_set_get(&self, id: u16) -> Option<&GroupKeySet> {
226            self.key_sets.iter().find(|e| e.group_key_set_id == id)
227        }
228
229        /// Add or update a group key set
230        pub fn key_set_add(&mut self, entry: GroupKeySet) -> Result<(), Error> {
231            if let Some(existing) = self
232                .key_sets
233                .iter_mut()
234                .find(|e| e.group_key_set_id == entry.group_key_set_id)
235            {
236                *existing = entry;
237            } else {
238                self.key_sets
239                    .push(entry)
240                    .map_err(|_| ErrorCode::ResourceExhausted)?;
241            }
242            Ok(())
243        }
244
245        /// Remove a group key set by ID. Returns true if found and removed.
246        pub fn key_set_remove(&mut self, id: u16) -> Result<(), Error> {
247            let before = self.key_sets.len();
248            self.key_sets.retain(|e| e.group_key_set_id != id);
249            let removed = self.key_sets.len() < before;
250
251            self.key_map_remove_by_key_set(id);
252
253            // Check if element was actually removed
254            if removed {
255                Ok(())
256            } else {
257                Err(Error::new(ErrorCode::NotFound))
258            }
259        }
260
261        pub fn key_map_add(&mut self, entry: GroupKeyMapping) -> Result<(), Error> {
262            self.key_map.push(entry).map_err(|_| ErrorCode::Failure)?;
263
264            Ok(())
265        }
266
267        /// Return an iterator over the group key map entries of the fabric
268        pub fn key_map_iter(&self) -> impl Iterator<Item = &GroupKeyMapping> {
269            self.key_map.iter()
270        }
271
272        /// Replace all group key map entries
273        pub fn key_map_replace(
274            &mut self,
275            entries: impl Iterator<Item = GroupKeyMapping>,
276        ) -> Result<(), Error> {
277            self.key_map.clear();
278            for entry in entries {
279                self.key_map
280                    .push(entry)
281                    .map_err(|_| ErrorCode::ResourceExhausted)?;
282            }
283            Ok(())
284        }
285
286        /// Remove group key map entries that reference a specific key set ID
287        pub fn key_map_remove_by_key_set(&mut self, key_set_id: u16) {
288            self.key_map.retain(|e| e.group_key_set_id != key_set_id);
289        }
290
291        /// Return an iterator over the group table entries
292        pub fn iter(&self) -> impl Iterator<Item = &GroupEndpointMapping> {
293            self.endpoint_mapping.iter()
294        }
295
296        /// Look up a group by ID
297        pub fn get(&self, group_id: u16) -> Option<&GroupEndpointMapping> {
298            self.endpoint_mapping
299                .iter()
300                .find(|e| e.group_id == group_id)
301        }
302
303        /// Look up a group by ID, mutably
304        pub fn get_mut(&mut self, group_id: u16) -> Option<&mut GroupEndpointMapping> {
305            self.endpoint_mapping
306                .iter_mut()
307                .find(|e| e.group_id == group_id)
308        }
309
310        /// Add an endpoint to a group.
311        /// Returns true if the endpoint was already a member (name still updated per spec).
312        pub fn add(
313            &mut self,
314            endpoint_id: u16,
315            group_id: u16,
316            group_name: &str,
317        ) -> Result<bool, Error> {
318            let entry = if let Some(entry) = self
319                .endpoint_mapping
320                .iter_mut()
321                .find(|e| e.group_id == group_id)
322            {
323                entry
324            } else {
325                self.endpoint_mapping
326                    .push(GroupEndpointMapping {
327                        group_id,
328                        endpoints: Vec::new(),
329                        group_name: String::from_str(group_name)
330                            .map_err(|_| ErrorCode::ConstraintError)?,
331                        has_aux_acl: None,
332                        mcast_policy: None,
333                    })
334                    .map_err(|_| ErrorCode::ResourceExhausted)?;
335                unwrap!(self.endpoint_mapping.last_mut())
336            };
337
338            // Update group name
339            entry.group_name.clear();
340            entry
341                .group_name
342                .push_str(group_name)
343                .map_err(|_| ErrorCode::ConstraintError)?;
344
345            if entry.endpoints.contains(&endpoint_id) {
346                return Ok(true);
347            }
348
349            entry
350                .endpoints
351                .push(endpoint_id)
352                .map_err(|_| ErrorCode::ResourceExhausted)?;
353
354            Ok(false)
355        }
356
357        /// Remove an endpoint from a group, or from all groups if `group_id` is `None`.
358        /// Returns true if the endpoint was removed from at least one group.
359        pub fn remove(&mut self, endpoint_id: u16, group_id: Option<u16>) -> bool {
360            let mut removed = false;
361
362            for entry in self.endpoint_mapping.iter_mut() {
363                if group_id.is_some_and(|id| id != entry.group_id) {
364                    continue;
365                }
366                let before = entry.endpoints.len();
367                entry.endpoints.retain(|&ep| ep != endpoint_id);
368                if entry.endpoints.len() < before {
369                    removed = true;
370                }
371            }
372
373            // Remove entries with no endpoints left - except Groupcast-managed
374            // ones, which may legitimately exist with no endpoints (a
375            // sender-only membership); the Groupcast cluster removes those
376            // explicitly via its `LeaveGroup` command.
377            self.endpoint_mapping
378                .retain(|e| !e.endpoints.is_empty() || e.groupcast_managed());
379
380            removed
381        }
382
383        /// Join endpoints to a group on behalf of the Groupcast cluster,
384        /// creating the membership if it does not exist.
385        ///
386        /// - `endpoints`: the endpoints to add (may be empty for a
387        ///   sender-only membership); duplicates are omitted;
388        /// - `replace`: when `true`, the given endpoints replace the
389        ///   existing list instead of being appended;
390        /// - `mcast_policy`: the multicast-address policy; applied on
391        ///   creation, or updated when `Some` on an existing membership.
392        ///
393        /// Errors with `ResourceExhausted` when the membership or endpoint
394        /// capacity is exceeded; the membership is left unchanged in that
395        /// case, except that a possibly-performed `replace` clearing is
396        /// rolled back by restoring nothing (the caller re-checks capacity
397        /// upfront via [`Self::group_count`] and the endpoint capacity).
398        pub fn groupcast_join(
399            &mut self,
400            group_id: u16,
401            endpoints: &[u16],
402            replace: bool,
403            mcast_policy: Option<MulticastAddrPolicyEnum>,
404        ) -> Result<(), Error> {
405            let entry = if let Some(entry) = self
406                .endpoint_mapping
407                .iter_mut()
408                .find(|e| e.group_id == group_id)
409            {
410                entry
411            } else {
412                self.endpoint_mapping
413                    .push(GroupEndpointMapping {
414                        group_id,
415                        endpoints: Vec::new(),
416                        group_name: String::new(),
417                        has_aux_acl: Some(false),
418                        mcast_policy: Some(
419                            mcast_policy.unwrap_or(MulticastAddrPolicyEnum::IanaAddr),
420                        ),
421                    })
422                    .map_err(|_| ErrorCode::ResourceExhausted)?;
423                unwrap!(self.endpoint_mapping.last_mut())
424            };
425
426            // Joining via Groupcast upgrades a legacy entry to
427            // Groupcast-managed (the default policy matches the legacy
428            // behavior)
429            if entry.mcast_policy.is_none() {
430                entry.mcast_policy = Some(MulticastAddrPolicyEnum::PerGroup);
431            }
432
433            if let Some(mcast_policy) = mcast_policy {
434                entry.mcast_policy = Some(mcast_policy);
435            }
436
437            if replace {
438                entry.endpoints.clear();
439            }
440
441            for endpoint in endpoints {
442                if !entry.endpoints.contains(endpoint) {
443                    entry
444                        .endpoints
445                        .push(*endpoint)
446                        .map_err(|_| ErrorCode::ResourceExhausted)?;
447                }
448            }
449
450            Ok(())
451        }
452
453        /// Remove a whole group membership. Returns `true` if it existed.
454        pub fn groupcast_remove(&mut self, group_id: u16) -> bool {
455            let before = self.endpoint_mapping.len();
456            self.endpoint_mapping.retain(|e| e.group_id != group_id);
457
458            before != self.endpoint_mapping.len()
459        }
460
461        /// Set the `has_aux_acl` flag of a group membership.
462        /// Returns `true` if the flag changed.
463        pub fn set_has_aux_acl(&mut self, group_id: u16, has_aux_acl: bool) -> bool {
464            let Some(entry) = self
465                .endpoint_mapping
466                .iter_mut()
467                .find(|e| e.group_id == group_id)
468            else {
469                return false;
470            };
471
472            let changed = entry.has_aux_acl() != has_aux_acl;
473            entry.has_aux_acl = Some(has_aux_acl);
474
475            changed
476        }
477
478        /// The number of group memberships of this fabric.
479        pub fn group_count(&self) -> usize {
480            self.endpoint_mapping.len()
481        }
482
483        /// Look up the key set ID mapped to a group, if any.
484        pub fn key_map_get(&self, group_id: u16) -> Option<u16> {
485            self.key_map
486                .iter()
487                .find(|e| e.group_id == group_id)
488                .map(|e| e.group_key_set_id)
489        }
490
491        /// Map a group to a key set, replacing any previous mapping of that
492        /// group.
493        pub fn key_map_set_group(&mut self, group_id: u16, key_set_id: u16) -> Result<(), Error> {
494            if let Some(entry) = self.key_map.iter_mut().find(|e| e.group_id == group_id) {
495                entry.group_key_set_id = key_set_id;
496                return Ok(());
497            }
498
499            self.key_map
500                .push(GroupKeyMapping {
501                    group_id,
502                    group_key_set_id: key_set_id,
503                })
504                .map_err(|_| ErrorCode::ResourceExhausted.into())
505        }
506
507        /// Remove all key-set mappings of the given group.
508        pub fn key_map_remove_group(&mut self, group_id: u16) {
509            self.key_map.retain(|e| e.group_id != group_id);
510        }
511    }
512
513    impl Default for Groups {
514        fn default() -> Self {
515            Self::new()
516        }
517    }
518
519    impl InitDefault for Groups {
520        fn init_default() -> impl Init<Self> {
521            Self::init()
522        }
523    }
524}
525
526#[cfg(feature = "groups")]
527pub use groups::*;
528
529/// Fabric type
530#[derive(Debug, ToTLV, FromTLV)]
531#[cfg_attr(feature = "defmt", derive(defmt::Format))]
532pub struct Fabric {
533    /// Fabric local index
534    fab_idx: NonZeroU8,
535    /// Fabric node ID
536    node_id: u64,
537    /// Fabric ID
538    fabric_id: u64,
539    /// Vendor ID
540    vendor_id: u16,
541    /// Compressed ID
542    compressed_fabric_id: u64,
543    /// Fabric secret key
544    secret_key: CanonPkcSecretKey,
545    /// Root CA certificate to be used when verifying the node's certificate
546    ///
547    /// Note that we deviate from the Matter spec here, in that we store the
548    /// root certificate in the Fabric type itself, rather than - as the
549    /// spec mandates - in a separate Root CA store
550    ///
551    /// This simplifies the implementation, but results in potentially multiple
552    /// copies of the same Root CA used accross multiple fabrics.
553    root_ca: Vec<u8, { MAX_CERT_TLV_LEN }>,
554    /// Either the Intermediate CA certificate (`vvsc_set == false`) or the
555    /// Vendor Verification Signing Cert (`vvsc_set == true`). The two are
556    /// mutually exclusive in the cert chain (Matter Core spec) —
557    /// a fabric with an ICAC cannot also carry a VVSC and vice
558    /// versa — so we share one buffer instead of paying for both. Empty
559    /// means neither is set; in that case `vvsc_set` is meaningless.
560    icac_or_vvsc: Vec<u8, { MAX_CERT_TLV_LEN }>,
561    /// Selector for what `icac_or_vvsc` holds: `false` for an ICAC,
562    /// `true` for a VVSC.
563    vvsc_set: bool,
564    /// Node Operational Certificate
565    noc: Vec<u8, { MAX_CERT_TLV_LEN }>,
566    /// Identity Protection Key
567    ipk: KeySet,
568    /// Fabric label; unique accross all fabrics on the device
569    label: String<32>,
570    /// Access Control List
571    acl: Vec<AclEntry, { acl::MAX_ACL_ENTRIES_PER_FABRIC }>,
572    /// Fabric group information.
573    #[cfg(feature = "groups")]
574    #[tagval(13)]
575    groups: Skippable<Groups>,
576    /// VID Verification Statement (Matter Core spec).
577    /// Either empty (not set) or exactly `VID_VERIFICATION_STATEMENT_LEN`
578    /// bytes long; the cluster XML enforces both bounds at the schema
579    /// level (`length="85" minLength="85"`).
580    #[tagval(14)]
581    vid_verification_statement: Vec<u8, VID_VERIFICATION_STATEMENT_LEN>,
582}
583
584/// Exact length of a non-empty VID Verification Statement.
585/// Matches `length="85" minLength="85"` on
586/// `OperationalCredentials::SetVIDVerificationStatement.vid_verification_statement`.
587pub const VID_VERIFICATION_STATEMENT_LEN: usize = 85;
588
589impl Fabric {
590    /// Return an in-place-initializer for a Fabric type, with the
591    /// provided Fabric Index and KeyPair
592    ///
593    /// All other fields are initialized to default values, which are NOT
594    /// valid for the operation of the fabric.
595    ///
596    /// The Fabric must be updated with the correct values before it can be
597    /// used, via `Fabric::update`.
598    fn init(fab_idx: NonZeroU8) -> impl Init<Self> {
599        // NOTE: the `init!` macro does not accept `#[cfg]` on its field entries,
600        // so the `groups` field (present only under the `groups` feature) forces
601        // two variants of the initializer that differ solely by that last field.
602        #[cfg(feature = "groups")]
603        let r = init!(Self {
604            fab_idx,
605            node_id: 0,
606            fabric_id: 0,
607            vendor_id: 0,
608            compressed_fabric_id: 0,
609            secret_key <- CanonPkcSecretKey::init(),
610            root_ca <- Vec::init(),
611            icac_or_vvsc <- Vec::init(),
612            vvsc_set: false,
613            noc <- Vec::init(),
614            ipk <- KeySet::init(),
615            label: String::new(),
616            acl <- Vec::init(),
617            vid_verification_statement <- Vec::init(),
618            groups <- Skippable::init_default(),
619        });
620
621        #[cfg(not(feature = "groups"))]
622        let r = init!(Self {
623            fab_idx,
624            node_id: 0,
625            fabric_id: 0,
626            vendor_id: 0,
627            compressed_fabric_id: 0,
628            secret_key <- CanonPkcSecretKey::init(),
629            root_ca <- Vec::init(),
630            icac_or_vvsc <- Vec::init(),
631            vvsc_set: false,
632            noc <- Vec::init(),
633            ipk <- KeySet::init(),
634            label: String::new(),
635            acl <- Vec::init(),
636            vid_verification_statement <- Vec::init(),
637        });
638        r
639    }
640
641    /// Update the fabric with the provided data so that it can operate.
642    ///
643    /// This method is supposed to be called right after `Fabric::init` or
644    /// when the NOC of the fabric needs to be updated.
645    ///
646    /// `root_ca` is `None` when called from the `UpdateNOC` flow — Matter
647    /// Core spec keeps the fabric's root cert unchanged
648    /// across `UpdateNOC`, and re-passing the existing bytes here would
649    /// require a (large) caller-side copy of `self.root_ca`. `Some(...)`
650    /// is used by the initial `AddNOC` flow, where the cert was just
651    /// staged in the fail-safe context.
652    #[allow(clippy::too_many_arguments)]
653    fn update<C: Crypto>(
654        &mut self,
655        crypto: C,
656        root_ca: Option<&[u8]>,
657        noc: &[u8],
658        icac: &[u8],
659        secret_key: CanonPkcSecretKeyRef<'_>,
660        epoch_key: Option<CanonAeadKeyRef<'_>>,
661        vendor_id: Option<u16>,
662        case_admin_subject: Option<u64>,
663    ) -> Result<(), Error> {
664        if let Some(root_ca) = root_ca {
665            self.root_ca.clear();
666            self.root_ca
667                .extend_from_slice(root_ca)
668                .map_err(|_| ErrorCode::BufferTooSmall)?;
669        }
670        // `AddNOC` / `UpdateNOC` always replace the cert chain, so any
671        // previously-staged VVSC for this fabric is implicitly cleared
672        // here — the spec doesn't allow an ICAC and a VVSC to coexist.
673        self.icac_or_vvsc.clear();
674        self.icac_or_vvsc
675            .extend_from_slice(icac)
676            .map_err(|_| ErrorCode::BufferTooSmall)?;
677        self.vvsc_set = false;
678        self.noc.clear();
679        self.noc
680            .extend_from_slice(noc)
681            .map_err(|_| ErrorCode::BufferTooSmall)?;
682
683        let root_cert = CertRef::new(TLVElement::new(self.root_ca.as_slice()));
684        let noc_cert = CertRef::new(TLVElement::new(noc));
685
686        self.node_id = noc_cert.get_node_id()?;
687        self.fabric_id = noc_cert.get_fabric_id()?;
688        self.compressed_fabric_id = Self::compute_compressed_fabric_id(
689            &crypto,
690            root_cert.pubkey()?.try_into()?,
691            self.fabric_id,
692        );
693
694        if let Some(epoch_key) = epoch_key {
695            self.ipk
696                .update(&crypto, epoch_key, &self.compressed_fabric_id)?;
697        }
698
699        if let Some(vendor_id) = vendor_id {
700            self.vendor_id = vendor_id;
701        }
702
703        if let Some(case_admin_subject) = case_admin_subject {
704            self.acl.clear();
705            self.acl.push_init(
706                AclEntry::init(None, Privilege::ADMIN, AuthMode::Case)
707                    .into_fallible()
708                    .chain(|e| {
709                        e.fab_idx = Some(self.fab_idx);
710                        e.add_subject(case_admin_subject)
711                    }),
712                || ErrorCode::ResourceExhausted.into(),
713            )?;
714        }
715
716        self.secret_key.load(secret_key);
717
718        Ok(())
719    }
720
721    pub fn mdns_service(&self) -> Option<MatterLocalService> {
722        self.mdns_service_for(self.node_id)
723    }
724
725    pub fn mdns_service_for(&self, node_id: u64) -> Option<MatterLocalService> {
726        (!self.noc.is_empty()).then_some(MatterLocalService::Commissioned {
727            compressed_fabric_id: self.compressed_fabric_id,
728            node_id,
729        })
730    }
731
732    /// Is the fabric matching the privided destination ID
733    pub fn is_dest_id<C: Crypto>(
734        &self,
735        crypto: C,
736        random: &[u8],
737        target: &[u8],
738    ) -> Result<(), Error> {
739        let mut mac = crypto.hmac(self.ipk.op_key())?;
740
741        mac.update(random)?;
742        mac.update(CertRef::new(TLVElement::new(self.root_ca())).pubkey()?)?;
743
744        mac.update(&self.fabric_id.to_le_bytes())?;
745        mac.update(&self.node_id.to_le_bytes())?;
746
747        let mut id = MaybeUninit::<Hash>::uninit(); // TODO MEDIUM BUFFER
748        let id = id.init_with(Hash::init());
749        mac.finish(id)?;
750        if id.access() == target {
751            Ok(())
752        } else {
753            Err(ErrorCode::NotFound.into())
754        }
755    }
756
757    /// Compute the destination identifier for a target node on this fabric.
758    ///
759    /// Used by the CASE initiator to build Sigma1 (spec).
760    /// destinationMessage = initiatorRandom || rootPublicKey || fabricId(LE) || nodeId(LE)
761    /// destinationIdentifier = Crypto_HMAC(key=IPK, message=destinationMessage)
762    ///
763    /// # Arguments
764    /// - `target_node_id`: The node ID of the destination (peer) node, NOT the local node.
765    pub fn compute_dest_id<C: Crypto>(
766        &self,
767        crypto: C,
768        random: &[u8],
769        target_node_id: u64,
770        out: &mut Hash,
771    ) -> Result<(), Error> {
772        let mut mac = crypto.hmac(self.ipk.op_key())?;
773
774        mac.update(random)?;
775        mac.update(CertRef::new(TLVElement::new(self.root_ca())).pubkey()?)?;
776        mac.update(&self.fabric_id.to_le_bytes())?;
777        mac.update(&target_node_id.to_le_bytes())?;
778
779        mac.finish(out)?;
780        Ok(())
781    }
782
783    /// Return the secret key of the fabric
784    pub fn secret_key(&self) -> CanonPkcSecretKeyRef<'_> {
785        self.secret_key.reference()
786    }
787
788    /// Return the fabric's node ID
789    pub fn node_id(&self) -> u64 {
790        self.node_id
791    }
792
793    /// Return the fabric's fabric ID
794    pub fn fabric_id(&self) -> u64 {
795        self.fabric_id
796    }
797
798    /// Return the fabric's local index
799    pub fn fab_idx(&self) -> NonZeroU8 {
800        self.fab_idx
801    }
802
803    /// Return the fabric's compressed fabric ID
804    pub fn compressed_fabric_id(&self) -> u64 {
805        self.compressed_fabric_id
806    }
807
808    /// Return the fabric's Vendor ID
809    pub fn vendor_id(&self) -> u16 {
810        self.vendor_id
811    }
812
813    /// Return the fabric's label
814    pub fn label(&self) -> &str {
815        &self.label
816    }
817
818    /// Return the fabric's Root CA in encoded TLV form
819    ///
820    /// Use `CertRef` to decode on the fly
821    pub fn root_ca(&self) -> &[u8] {
822        &self.root_ca
823    }
824
825    /// Return the fabric's ICAC in encoded TLV form
826    ///
827    /// Use `CertRef` to decode on the fly.
828    ///
829    /// Note that this method might return an empty slice,
830    /// which indicates that this fabric does not have an ICAC.
831    /// (The shared `icac_or_vvsc` slot may instead hold a VVSC; see
832    /// `vvsc()`.)
833    pub fn icac(&self) -> &[u8] {
834        if self.vvsc_set {
835            &[]
836        } else {
837            &self.icac_or_vvsc
838        }
839    }
840
841    /// Return the fabric's NOC
842    pub fn noc(&self) -> &[u8] {
843        &self.noc
844    }
845
846    /// Return the fabric's IPK
847    pub fn ipk(&self) -> &KeySet {
848        &self.ipk
849    }
850
851    /// Return the fabric's groups, or an empty group state if this fabric was
852    /// persisted before the `groups` field existed (see [`Fabric::groups`]).
853    #[cfg(feature = "groups")]
854    pub fn groups(&self) -> &Groups {
855        self.groups.value()
856    }
857
858    /// Return a mutable reference to the fabric's groups, materializing empty
859    /// group state on first access if it was absent.
860    #[cfg(feature = "groups")]
861    pub fn groups_mut(&mut self) -> &mut Groups {
862        self.groups.value_mut()
863    }
864
865    /// Return the fabric's VVSC bytes (Matter Core spec).
866    /// Empty when `SetVIDVerificationStatement` has never been called with
867    /// a non-empty VVSC for this fabric, or when the fabric instead carries
868    /// an ICAC (see `icac()`) — VVSC and ICAC share storage and are
869    /// mutually exclusive per spec.
870    pub fn vvsc(&self) -> &[u8] {
871        if self.vvsc_set {
872            &self.icac_or_vvsc
873        } else {
874            &[]
875        }
876    }
877
878    /// Return the fabric's VID Verification Statement bytes (Matter Core
879    /// spec). Either empty (not set) or
880    /// exactly `VID_VERIFICATION_STATEMENT_LEN` bytes.
881    pub fn vid_verification_statement(&self) -> &[u8] {
882        &self.vid_verification_statement
883    }
884
885    /// Apply a `SetVIDVerificationStatement` mutation to the fabric. Each
886    /// field is `Some(slice)` for "replace with this value" (where an
887    /// empty slice clears the value), or `None` for "leave unchanged".
888    /// The caller is responsible for spec-level validation (size limits,
889    /// VVSC vs ICAC mutual exclusion, "all fields absent" → INVALID_COMMAND,
890    /// VendorID range, …); this method only enforces the storage
891    /// invariants (heapless `Vec` capacity).
892    pub fn set_vid_verification(
893        &mut self,
894        vendor_id: Option<u16>,
895        vid_verification_statement: Option<&[u8]>,
896        vvsc: Option<&[u8]>,
897    ) -> Result<(), Error> {
898        if let Some(vid) = vendor_id {
899            self.vendor_id = vid;
900        }
901
902        if let Some(vvs) = vid_verification_statement {
903            self.vid_verification_statement.clear();
904            self.vid_verification_statement
905                .extend_from_slice(vvs)
906                .map_err(|_| ErrorCode::BufferTooSmall)?;
907        }
908
909        if let Some(v) = vvsc {
910            // VVSC and ICAC share `icac_or_vvsc`. Clearing the VVSC must
911            // not stomp on an existing ICAC: per spec the
912            // two never coexist on the same fabric, so an empty-VVSC
913            // request against a fabric that holds an ICAC is a no-op
914            // here. The cluster handler still rejects a *non-empty* VVSC
915            // against such a fabric upstream.
916            if !v.is_empty() {
917                self.icac_or_vvsc.clear();
918                self.icac_or_vvsc
919                    .extend_from_slice(v)
920                    .map_err(|_| ErrorCode::BufferTooSmall)?;
921                self.vvsc_set = true;
922            } else if self.vvsc_set {
923                self.icac_or_vvsc.clear();
924                self.vvsc_set = false;
925            }
926        }
927
928        Ok(())
929    }
930
931    /// Return an iterator over the ACL entries of the fabric
932    pub fn acl_iter(&self) -> impl Iterator<Item = &AclEntry> {
933        self.acl.iter()
934    }
935
936    /// Add a new ACL entry to the fabric.
937    ///
938    /// Return the index of the added entry.
939    pub fn acl_add(&mut self, mut entry: AclEntry) -> Result<usize, Error> {
940        if entry.auth_mode() == AuthMode::Pase {
941            // Reserved for future use
942            Err(ErrorCode::ConstraintError)?;
943        }
944
945        // Overwrite the fabric index with our accessing fabric index
946        entry.fab_idx = Some(self.fab_idx);
947
948        self.acl
949            .push(entry)
950            .map_err(|_| ErrorCode::ResourceExhausted)?;
951
952        Ok(self.acl.len() - 1)
953    }
954
955    /// Add a new ACL entry to the fabric using the supplied initializer.
956    ///
957    /// Return the index of the added entry.
958    pub fn acl_add_init<I>(&mut self, init: I) -> Result<usize, Error>
959    where
960        I: Init<AclEntry, Error>,
961    {
962        // if entry.auth_mode() == AuthMode::Pase {
963        //     // Reserved for future use
964        //     Err(ErrorCode::ConstraintError)?;
965        // }
966
967        self.acl
968            .push_init(init, || ErrorCode::ResourceExhausted.into())?;
969
970        let idx = self.acl.len() - 1;
971        let entry = &mut self.acl[idx];
972
973        // Overwrite the fabric index with our accessing fabric index
974        entry.fab_idx = Some(self.fab_idx);
975
976        Ok(idx)
977    }
978
979    /// Update an existing ACL entry in the fabric
980    pub fn acl_update(&mut self, idx: usize, mut entry: AclEntry) -> Result<(), Error> {
981        if self.acl.len() <= idx {
982            return Err(ErrorCode::NotFound.into());
983        }
984
985        // Overwrite the fabric index with our accessing fabric index
986        entry.fab_idx = Some(self.fab_idx);
987
988        self.acl[idx] = entry;
989
990        Ok(())
991    }
992
993    /// Update an existing ACL entry in the fabric using the supplied initializer
994    pub fn acl_update_init<I>(&mut self, idx: usize, init: I) -> Result<(), Error>
995    where
996        I: Init<AclEntry, Error>,
997    {
998        if self.acl.len() <= idx {
999            return Err(ErrorCode::NotFound.into());
1000        }
1001
1002        // TODO: Needs #214
1003        let mut entry = MaybeUninit::uninit();
1004        let entry = entry.try_init_with(init)?.clone();
1005
1006        self.acl[idx] = entry;
1007
1008        // Overwrite the fabric index with our accessing fabric index
1009        self.acl[idx].fab_idx = Some(self.fab_idx);
1010
1011        Ok(())
1012    }
1013
1014    /// Remove an ACL entry from the fabric
1015    pub fn acl_remove(&mut self, idx: usize) -> Result<(), Error> {
1016        if self.acl.len() <= idx {
1017            return Err(ErrorCode::NotFound.into());
1018        }
1019
1020        self.acl.remove(idx);
1021
1022        Ok(())
1023    }
1024
1025    /// Remove all ACL entries from the fabric
1026    pub fn acl_remove_all(&mut self) {
1027        // pub for tests
1028        self.acl.clear();
1029    }
1030
1031    /// Check if the fabric allows the given access request
1032    ///
1033    /// Note that the fabric index in the access request needs to be checked before that.
1034    /// `aux_acl_enabled` conveys whether the node advertises the Access Control
1035    /// cluster's `AUXILIARY` feature - see `AclEntry::allow`.
1036    fn allow(&self, req: &AccessReq, aux_acl_enabled: bool) -> bool {
1037        for e in &self.acl {
1038            if e.allow(req, aux_acl_enabled) {
1039                return true;
1040            }
1041        }
1042
1043        debug!(
1044            "ACL Disallow for subjects {} fab idx {}",
1045            req.accessor().subjects(),
1046            req.accessor().fab_idx
1047        );
1048
1049        false
1050    }
1051
1052    /// Compute the compressed fabric ID
1053    pub(crate) fn compute_compressed_fabric_id<C: Crypto>(
1054        crypto: C,
1055        root_pubkey: CanonPkcPublicKeyRef<'_>,
1056        fabric_id: u64,
1057    ) -> u64 {
1058        const COMPRESSED_FABRIC_ID_INFO: &[u8; 16] = &[
1059            0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x46, 0x61, 0x62, 0x72,
1060            0x69, 0x63,
1061        ];
1062
1063        let mut compressed_fabric_id = CryptoSensitive::<{ COMPRESSED_FABRIC_ID_LEN }>::new();
1064        unwrap!(unwrap!(crypto.kdf()).expand(
1065            &fabric_id.to_be_bytes(),
1066            root_pubkey.split::<1, { PKC_CANON_PUBLIC_KEY_LEN - 1 }>().1,
1067            COMPRESSED_FABRIC_ID_INFO,
1068            &mut compressed_fabric_id,
1069        ));
1070
1071        u64::from_be_bytes(*compressed_fabric_id.access())
1072    }
1073}
1074
1075cfg_if! {
1076    if #[cfg(feature = "max-fabrics-32")] {
1077        /// Max number of supported fabrics
1078        pub const MAX_FABRICS: usize = 32;
1079    } else if #[cfg(feature = "max-fabrics-16")] {
1080        /// Max number of supported fabrics
1081        pub const MAX_FABRICS: usize = 16;
1082    } else if #[cfg(feature = "max-fabrics-8")] {
1083        /// Max number of supported fabrics
1084        pub const MAX_FABRICS: usize = 8;
1085    } else if #[cfg(feature = "max-fabrics-7")] {
1086        /// Max number of supported fabrics
1087        pub const MAX_FABRICS: usize = 7;
1088    } else if #[cfg(feature = "max-fabrics-6")] {
1089        /// Max number of supported fabrics
1090        pub const MAX_FABRICS: usize = 6;
1091    } else { // Matter requires a minimum of 5 fabrics
1092        /// Max number of supported fabrics
1093        pub const MAX_FABRICS: usize = 5;
1094    }
1095}
1096
1097/// All fabrics
1098pub struct Fabrics {
1099    fabrics: Vec<Fabric, MAX_FABRICS>,
1100}
1101
1102impl Default for Fabrics {
1103    fn default() -> Self {
1104        Self::new()
1105    }
1106}
1107
1108impl Fabrics {
1109    /// Create a new Fabrics instance
1110    #[inline(always)]
1111    pub const fn new() -> Self {
1112        Self {
1113            fabrics: Vec::new(),
1114        }
1115    }
1116
1117    /// Return an in-place-initializer for a Fabrics type
1118    pub fn init() -> impl Init<Self> {
1119        init!(Self {
1120            fabrics <- Vec::init(),
1121        })
1122    }
1123
1124    /// Remove all fabrics
1125    pub fn reset(&mut self) {
1126        self.fabrics.clear();
1127    }
1128
1129    /// Remove all fabrics from the provided BLOB store as well as from memory.
1130    ///
1131    /// # Arguments
1132    /// - `store`: the BLOB store to remove the fabrics from
1133    /// - `buf`: a temporary buffer to use for removing the fabrics
1134    pub fn reset_persist<S: KvBlobStore>(
1135        &mut self,
1136        mut store: S,
1137        buf: &mut [u8],
1138    ) -> Result<(), Error> {
1139        self.reset();
1140
1141        for idx in 1..=255u8 {
1142            store.remove(FABRIC_KEYS_START + idx as u16, buf)?;
1143        }
1144
1145        info!("Removed all fabrics from storage");
1146
1147        Ok(())
1148    }
1149
1150    /// Load all fabrics from the provided BLOB store
1151    ///
1152    /// # Arguments
1153    /// - `store`: the BLOB store to load the fabrics from
1154    /// - `buf`: a temporary buffer to use for loading the fabrics
1155    pub fn load_persist<S: KvBlobStore>(
1156        &mut self,
1157        mut store: S,
1158        buf: &mut [u8],
1159    ) -> Result<(), Error> {
1160        self.reset();
1161
1162        for fab_idx in 1..=255u8 {
1163            self.add_load(fab_idx, &mut store, buf)?;
1164        }
1165
1166        Ok(())
1167    }
1168
1169    pub(crate) fn add_load<S: KvBlobStore>(
1170        &mut self,
1171        fab_idx: u8,
1172        mut store: S,
1173        buf: &mut [u8],
1174    ) -> Result<(), Error> {
1175        if let Some(data) = store.load(FABRIC_KEYS_START + fab_idx as u16, buf)? {
1176            self.fabrics
1177                .push_init(Fabric::init_from_tlv(TLVElement::new(data)), || {
1178                    ErrorCode::ResourceExhausted.into()
1179                })?;
1180
1181            let fabric = unwrap!(self.fabrics.last());
1182
1183            info!(
1184                "Loaded fabric {} with ID {:x} from storage",
1185                fabric.fab_idx(),
1186                fabric.compressed_fabric_id()
1187            );
1188        }
1189
1190        Ok(())
1191    }
1192
1193    /// Add a new fabric to the fabrics with the provided data and immediately updates it with the provided post-init updater.
1194    ///
1195    /// This method is unlikely to be useful outside of tests.
1196    ///
1197    /// If this operation succeeds, the fabric immediately becomes operational.
1198    pub fn add_with_post_init<F>(&mut self, post_init: F) -> Result<&mut Fabric, Error>
1199    where
1200        F: FnOnce(&mut Fabric) -> Result<(), Error>,
1201    {
1202        let max_fab_idx = self
1203            .iter()
1204            .map(|fabric| fabric.fab_idx().get())
1205            .max()
1206            .unwrap_or(0);
1207        let fab_idx = unwrap!(NonZeroU8::new(if max_fab_idx < u8::MAX - 1 {
1208            // First try with the next available fabric index larger than all currently used
1209            max_fab_idx + 1
1210        } else {
1211            // If there is already a fabric with index 254, try to find the first unused one
1212            let Some(fab_idx) = (1..u8::MAX)
1213                .find(|fab_idx| self.iter().all(|fabric| fabric.fab_idx().get() != *fab_idx))
1214            else {
1215                return Err(ErrorCode::ResourceExhausted.into());
1216            };
1217
1218            fab_idx
1219        })); // We never use 0 as a fabric index, nor u8::MAX
1220
1221        self.fabrics.push_init(
1222            Fabric::init(fab_idx)
1223                .into_fallible::<Error>()
1224                .chain(post_init),
1225            || ErrorCode::ResourceExhausted.into(),
1226        )?;
1227
1228        let fabric = unwrap!(self.fabrics.last_mut());
1229
1230        Ok(fabric)
1231    }
1232
1233    /// Add a new fabric to the fabrics with the provided data.
1234    ///
1235    /// If this operation succeeds, the fabric immediately becomes operational.
1236    #[allow(clippy::too_many_arguments)]
1237    pub fn add<C: Crypto>(
1238        &mut self,
1239        crypto: C,
1240        secret_key: CanonPkcSecretKeyRef<'_>,
1241        root_ca: &[u8],
1242        noc: &[u8],
1243        icac: &[u8],
1244        epoch_key: Option<CanonAeadKeyRef<'_>>,
1245        vendor_id: u16,
1246        case_admin_subject: u64,
1247    ) -> Result<&mut Fabric, Error> {
1248        self.add_with_post_init(|fabric| {
1249            fabric.update(
1250                crypto,
1251                Some(root_ca),
1252                noc,
1253                icac,
1254                secret_key,
1255                epoch_key,
1256                Some(vendor_id),
1257                Some(case_admin_subject),
1258            )
1259        })
1260    }
1261
1262    /// Update an existing fabric with the provided data (usually, as a result of an `UpdateNOC` IM command).
1263    ///
1264    /// The fabric's existing root cert is preserved across this call —
1265    /// `UpdateNOC` per Matter Core spec is not allowed
1266    /// to change the root, and re-passing the bytes would force the
1267    /// caller to take a (large) heap-less copy of `Fabric::root_ca`.
1268    ///
1269    /// If this operation succeeds, the fabric immediately becomes operational.
1270    /// Note however, that the caller is expected to remove all sessions associated with the fabric, as they would
1271    /// contain invalid keys after the NOC update.
1272    pub fn update<C: Crypto>(
1273        &mut self,
1274        crypto: C,
1275        fab_idx: NonZeroU8,
1276        secret_key: CanonPkcSecretKeyRef<'_>,
1277        noc: &[u8],
1278        icac: &[u8],
1279    ) -> Result<&mut Fabric, Error> {
1280        let fabric = self.fabric_mut(fab_idx)?;
1281
1282        fabric.update(crypto, None, noc, icac, secret_key, None, None, None)?;
1283
1284        Ok(fabric)
1285    }
1286
1287    pub fn update_label(&mut self, fab_idx: NonZeroU8, label: &str) -> Result<&mut Fabric, Error> {
1288        if self.iter().any(|fabric| {
1289            fabric.fab_idx != fab_idx && !fabric.label.is_empty() && fabric.label == label
1290        }) {
1291            return Err(ErrorCode::Invalid.into());
1292        }
1293
1294        let fabric = self.fabric_mut(fab_idx)?;
1295        fabric.label.clear();
1296        fabric
1297            .label
1298            .push_str(label)
1299            .map_err(|_| ErrorCode::ConstraintError)?;
1300
1301        Ok(fabric)
1302    }
1303
1304    /// Remove a fabric from the fabrics
1305    pub fn remove(&mut self, fab_idx: NonZeroU8) -> Result<(), Error> {
1306        let _ = self.fabric(fab_idx)?;
1307
1308        self.fabrics.retain(|fabric| fabric.fab_idx != fab_idx);
1309
1310        Ok(())
1311    }
1312
1313    /// Get a fabric that matches the provided destination ID
1314    pub fn get_by_dest_id<C: Crypto>(
1315        &self,
1316        crypto: C,
1317        random: &[u8],
1318        target: &[u8],
1319    ) -> Option<&Fabric> {
1320        self.iter()
1321            .find(|fabric| fabric.is_dest_id(&crypto, random, target).is_ok())
1322    }
1323
1324    /// Get a fabric by its local index
1325    pub fn get(&self, fab_idx: NonZeroU8) -> Option<&Fabric> {
1326        self.iter().find(|fabric| fabric.fab_idx == fab_idx)
1327    }
1328
1329    /// Get a mutable fabric reference by its local index
1330    pub fn get_mut(&mut self, fab_idx: NonZeroU8) -> Option<&mut Fabric> {
1331        // pub for testing
1332        self.fabrics
1333            .iter_mut()
1334            .find(|fabric| fabric.fab_idx == fab_idx)
1335    }
1336
1337    /// Iterate over the fabrics
1338    pub fn iter(&self) -> impl Iterator<Item = &Fabric> {
1339        self.fabrics.iter()
1340    }
1341
1342    /// Get a fabric by its local index
1343    ///
1344    /// Returns an error if the fabric is not found
1345    pub fn fabric(&self, fab_idx: NonZeroU8) -> Result<&Fabric, Error> {
1346        self.get(fab_idx).ok_or(ErrorCode::NotFound.into())
1347    }
1348
1349    /// Get a mutable fabric reference by its local index
1350    ///
1351    /// Returns an error if the fabric is not found
1352    pub fn fabric_mut(&mut self, fab_idx: NonZeroU8) -> Result<&mut Fabric, Error> {
1353        self.get_mut(fab_idx).ok_or(ErrorCode::NotFound.into())
1354    }
1355
1356    /// Check if the given access request should be allowed, based on all operational fabrics
1357    /// and their ACLs
1358    ///
1359    /// `aux_acl_enabled` conveys whether the node advertises the Access Control
1360    /// cluster's `AUXILIARY` feature - see `AclEntry::allow`.
1361    pub fn allow(&self, req: &AccessReq, aux_acl_enabled: bool) -> bool {
1362        // PASE Sessions with no fabric index have implicit access grant,
1363        // but only as long as the ACL list is empty
1364        //
1365        // As per the spec:
1366        // The Access Control List is able to have an initial entry added because the Access Control Privilege
1367        // Granting algorithm behaves as if, over a PASE commissioning channel during the commissioning
1368        // phase, the following implicit Access Control Entry were present on the Commissionee (but not on
1369        // the Commissioner):
1370        // Access Control Cluster: {
1371        //     ACL: [
1372        //         0: {
1373        //             // implicit entry only; does not explicitly exist!
1374        //             FabricIndex: 0, // not fabric-specific
1375        //             Privilege: Administer,
1376        //             AuthMode: PASE,
1377        //             Subjects: [],
1378        //             Targets: [] // entire node
1379        //         }
1380        //     ],
1381        //     Extension: []
1382        // }
1383        if req.accessor().auth_mode() == Some(AuthMode::Pase) {
1384            return true;
1385        }
1386
1387        let Ok(fab_idx) = req.accessor().fab_idx() else {
1388            return false;
1389        };
1390
1391        let Some(fabric) = self.get(fab_idx) else {
1392            return false;
1393        };
1394
1395        fabric.allow(req, aux_acl_enabled)
1396    }
1397}
1398
1399/// A utility for persisting a fabric in a `KvBlobStore` instance.
1400pub struct FabricPersist<S>(Persist<S>);
1401
1402impl<S> FabricPersist<S>
1403where
1404    S: KvBlobStoreAccess,
1405{
1406    /// Create a new `FabricPersist` with the given key-value store instance.
1407    pub const fn new(kvb: S) -> Self {
1408        Self(Persist::new(kvb))
1409    }
1410
1411    /// Return a reference to the underlying `Persist` instance.
1412    pub fn persist_mut(&mut self) -> &mut Persist<S> {
1413        &mut self.0
1414    }
1415
1416    /// Save the provided fabric in the persistent storage.
1417    pub fn store(&mut self, fabric: &Fabric) -> Result<(), Error> {
1418        self.0
1419            .store_tlv(FABRIC_KEYS_START + fabric.fab_idx().get() as u16, fabric)
1420    }
1421
1422    /// Remove the fabric with the given index from the persistent storage.
1423    pub fn remove(&mut self, fab_idx: NonZeroU8) -> Result<(), Error> {
1424        self.0.remove(FABRIC_KEYS_START + fab_idx.get() as u16)
1425    }
1426
1427    /// Call at the end when finished with everything else
1428    /// No-op for now
1429    pub fn run(self) -> Result<(), Error> {
1430        self.0.run()
1431    }
1432}
1433
1434#[cfg(test)]
1435mod tests {
1436    use core::mem::MaybeUninit;
1437
1438    use crate::cert::gen::{CertGenerator, CertType, IssuerDN, SubjectDN, Validity};
1439    use crate::cert::MAX_CERT_TLV_AND_ASN1_LEN;
1440    use crate::crypto::test_only_crypto;
1441    use crate::crypto::{
1442        CanonAeadKeyRef, CanonPkcSecretKey, Crypto, Hash, PublicKey, SecretKey, SigningSecretKey,
1443        AEAD_CANON_KEY_LEN,
1444    };
1445    use crate::utils::init::InitMaybeUninit;
1446
1447    use core::num::NonZeroU8;
1448
1449    use super::{Fabric, Fabrics};
1450
1451    /// Lock the on-disk TLV tag layout of the fields whose position is sensitive
1452    /// to the `groups` feature. A released `rs-matter` persists `groups` at
1453    /// context tag 13 and `vid_verification_statement` at 14; gating `groups` in
1454    /// or out must not move either. Serializing an (empty) `Fabric` and checking
1455    /// the raw tags catches any future reorder that would silently corrupt
1456    /// existing persisted fabrics.
1457    #[test]
1458    fn fabric_tlv_tag_layout_is_stable() {
1459        use crate::tlv::{TLVElement, TLVTag, ToTLV};
1460        use crate::utils::init::InitMaybeUninit;
1461        use crate::utils::storage::WriteBuf;
1462
1463        let mut fabric = core::mem::MaybeUninit::<Fabric>::uninit();
1464        let fabric = fabric.init_with(Fabric::init(unwrap!(NonZeroU8::new(1))));
1465
1466        let mut buf = [0u8; 512];
1467        let mut wb = WriteBuf::new(&mut buf);
1468        fabric.to_tlv(&TLVTag::Anonymous, &mut wb).unwrap();
1469        let len = wb.get_tail();
1470
1471        let root = TLVElement::new(&buf[..len]).structure().unwrap();
1472
1473        // `find_ctx` returns an EMPTY element (not an error) when the tag is
1474        // absent, so presence is `!is_empty()` and absence is `is_empty()`.
1475
1476        // `acl` (the last always-present field before the sensitive pair) is at 12.
1477        assert!(
1478            !root.find_ctx(12).unwrap().is_empty(),
1479            "acl must stay at TLV tag 12"
1480        );
1481
1482        // `vid_verification_statement` must always be at context tag 14.
1483        assert!(
1484            !root.find_ctx(14).unwrap().is_empty(),
1485            "vid_verification_statement must stay at TLV tag 14"
1486        );
1487
1488        // With `groups` compiled in it must be at tag 13; compiled out, tag 13 is
1489        // simply absent (and a reader defaults it).
1490        #[cfg(feature = "groups")]
1491        assert!(
1492            !root.find_ctx(13).unwrap().is_empty(),
1493            "groups must be at TLV tag 13 when compiled in"
1494        );
1495        #[cfg(not(feature = "groups"))]
1496        assert!(
1497            root.find_ctx(13).unwrap().is_empty(),
1498            "no field should occupy tag 13 when groups is compiled out"
1499        );
1500    }
1501
1502    /// Verify that `compute_dest_id` and `is_dest_id` agree: the hash output by
1503    /// `compute_dest_id` must be accepted by `is_dest_id` on the same fabric with
1504    /// the same random nonce.
1505    ///
1506    /// Uses runtime-generated certs (via `CertGenerator`) with a real keypair
1507    /// so the fabric is in a valid state — the secret key matches the NOC's public key.
1508    #[test]
1509    fn test_compute_dest_id_matches_is_dest_id() {
1510        let crypto = test_only_crypto();
1511
1512        let fabric_id: u64 = 1;
1513        let rcac_id: u64 = 1;
1514        let node_id: u64 = 100;
1515
1516        // Generate RCAC keypair and build self-signed RCAC
1517        let rcac_secret_key = crypto.generate_secret_key().unwrap();
1518        let mut rcac_pubkey_canon = crate::crypto::CanonPkcPublicKey::new();
1519        rcac_secret_key
1520            .pub_key()
1521            .unwrap()
1522            .write_canon(&mut rcac_pubkey_canon)
1523            .unwrap();
1524
1525        let validity = Validity {
1526            not_before: 0,
1527            not_after: 0,
1528        };
1529
1530        let mut rcac_buf = [0u8; MAX_CERT_TLV_AND_ASN1_LEN];
1531        let rcac_len = CertGenerator::new(&mut rcac_buf)
1532            .generate(
1533                &crypto,
1534                CertType::Rcac,
1535                &[0x01],
1536                validity,
1537                SubjectDN {
1538                    node_id: None,
1539                    fabric_id: Some(fabric_id),
1540                    cat_ids: &[],
1541                    ca_id: Some(rcac_id),
1542                },
1543                IssuerDN {
1544                    ca_id: None,
1545                    fabric_id: None,
1546                    is_rcac: false,
1547                },
1548                rcac_pubkey_canon.reference(),
1549                None,
1550                &rcac_secret_key,
1551            )
1552            .unwrap();
1553
1554        // Generate NOC keypair and build NOC signed by RCAC
1555        let noc_secret_key = crypto.generate_secret_key().unwrap();
1556        let mut noc_pubkey_canon = crate::crypto::CanonPkcPublicKey::new();
1557        noc_secret_key
1558            .pub_key()
1559            .unwrap()
1560            .write_canon(&mut noc_pubkey_canon)
1561            .unwrap();
1562
1563        let mut noc_secret_key_canon = CanonPkcSecretKey::new();
1564        noc_secret_key
1565            .write_canon(&mut noc_secret_key_canon)
1566            .unwrap();
1567
1568        let mut noc_buf = [0u8; MAX_CERT_TLV_AND_ASN1_LEN];
1569        let noc_len = CertGenerator::new(&mut noc_buf)
1570            .generate(
1571                &crypto,
1572                CertType::Noc,
1573                &[0x02],
1574                validity,
1575                SubjectDN {
1576                    node_id: Some(node_id),
1577                    fabric_id: Some(fabric_id),
1578                    cat_ids: &[],
1579                    ca_id: None,
1580                },
1581                IssuerDN {
1582                    ca_id: Some(rcac_id),
1583                    fabric_id: Some(fabric_id),
1584                    is_rcac: true,
1585                },
1586                noc_pubkey_canon.reference(),
1587                Some(rcac_pubkey_canon.reference()),
1588                &rcac_secret_key,
1589            )
1590            .unwrap();
1591
1592        // Build fabric with real certs and matching secret key
1593        let epoch_key = [0x5a_u8; AEAD_CANON_KEY_LEN];
1594        let mut fabrics = Fabrics::new();
1595        fabrics
1596            .add(
1597                &crypto,
1598                noc_secret_key_canon.reference(),
1599                &rcac_buf[..rcac_len],
1600                &noc_buf[..noc_len],
1601                &[], // no ICAC
1602                Some(CanonAeadKeyRef::new(&epoch_key)),
1603                0x8000,
1604                node_id,
1605            )
1606            .expect("Fabrics::add should succeed");
1607
1608        let fab_idx = core::num::NonZeroU8::new(1).unwrap();
1609        let fabric = fabrics
1610            .get(fab_idx)
1611            .expect("fabric at index 1 should exist");
1612
1613        let random = [0xABu8; 32];
1614
1615        // Compute the destination ID (targeting this fabric's own node).
1616        let mut dest_id = MaybeUninit::<Hash>::uninit();
1617        let dest_id = dest_id.init_with(Hash::init());
1618        fabric
1619            .compute_dest_id(&crypto, &random, fabric.node_id(), dest_id)
1620            .expect("compute_dest_id should not fail");
1621
1622        // is_dest_id must accept the computed value.
1623        fabric
1624            .is_dest_id(&crypto, &random, dest_id.access())
1625            .expect("is_dest_id should accept hash produced by compute_dest_id");
1626    }
1627}