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;
20use core::str::FromStr;
21
22use cfg_if::cfg_if;
23use heapless::String;
24
25use crate::acl::{self, AccessReq, AclEntry, AuthMode};
26use crate::cert::{CertRef, MAX_CERT_TLV_LEN};
27use crate::crypto::{
28    CanonAeadKeyRef, CanonPkcPublicKeyRef, CanonPkcSecretKey, CanonPkcSecretKeyRef, Crypto,
29    CryptoSensitive, Digest, Hash, Kdf, PKC_CANON_PUBLIC_KEY_LEN,
30};
31use crate::dm::Privilege;
32use crate::error::{Error, ErrorCode};
33use crate::group_keys::{GroupKeySet, KeySet};
34use crate::persist::{KvBlobStore, KvBlobStoreAccess, Persist, FABRIC_KEYS_START};
35use crate::tlv::{FromTLV, TLVElement, ToTLV};
36use crate::transport::network::MatterLocalService;
37use crate::utils::init::{init, Init, InitMaybeUninit, IntoFallibleInit};
38use crate::utils::storage::Vec;
39
40const COMPRESSED_FABRIC_ID_LEN: usize = 8;
41
42cfg_if! {
43    if #[cfg(feature = "max-group-keys-per-fabric-5")] {
44        /// Max number of group key sets per fabric (excluding IPK at index 0).
45        pub const MAX_GROUP_KEYS_PER_FABRIC: usize = 5;
46    } else if #[cfg(feature = "max-group-keys-per-fabric-4")] {
47        /// Max number of group key sets per fabric (excluding IPK at index 0).
48        pub const MAX_GROUP_KEYS_PER_FABRIC: usize = 4;
49    } else if #[cfg(feature = "max-group-keys-per-fabric-3")] {
50        /// Max number of group key sets per fabric (excluding IPK at index 0).
51        pub const MAX_GROUP_KEYS_PER_FABRIC: usize = 3;
52    } else if #[cfg(feature = "max-group-keys-per-fabric-2")] {
53        /// Max number of group key sets per fabric (excluding IPK at index 0).
54        pub const MAX_GROUP_KEYS_PER_FABRIC: usize = 2;
55    } else {
56        /// Max number of group key sets per fabric (excluding IPK at index 0).
57        pub const MAX_GROUP_KEYS_PER_FABRIC: usize = 0;
58    }
59}
60
61/// Max length of a group name (per Matter spec).
62pub const MAX_GROUP_NAME_LEN: usize = 16;
63
64cfg_if! {
65    if #[cfg(feature = "max-groups-per-fabric-32")] {
66        /// Max number of group key map entries per fabric.
67        pub const MAX_GROUPS_PER_FABRIC: usize = 32;
68    } else if #[cfg(feature = "max-groups-per-fabric-16")] {
69        /// Max number of group key map entries per fabric.
70        pub const MAX_GROUPS_PER_FABRIC: usize = 16;
71    } else if #[cfg(feature = "max-groups-per-fabric-12")] {
72        /// Max number of group key map entries per fabric.
73        pub const MAX_GROUPS_PER_FABRIC: usize = 12;
74    } else if #[cfg(feature = "max-groups-per-fabric-8")] {
75        /// Max number of group key map entries per fabric.
76        pub const MAX_GROUPS_PER_FABRIC: usize = 9;
77    } else if #[cfg(feature = "max-groups-per-fabric-7")] {
78        /// Max number of group key map entries per fabric.
79        pub const MAX_GROUPS_PER_FABRIC: usize = 7;
80    } else if #[cfg(feature = "max-groups-per-fabric-6")] {
81        /// Max number of group key map entries per fabric.
82        pub const MAX_GROUPS_PER_FABRIC: usize = 6;
83    } else if #[cfg(feature = "max-groups-per-fabric-5")] {
84        /// Max number of group key map entries per fabric.
85        pub const MAX_GROUPS_PER_FABRIC: usize = 5;
86    } else if #[cfg(feature = "max-groups-per-fabric-4")] {
87        /// Max number of group key map entries per fabric.
88        pub const MAX_GROUPS_PER_FABRIC: usize = 4;
89    } else {
90        /// Max number of group key map entries per fabric.
91        pub const MAX_GROUPS_PER_FABRIC: usize = 0;
92    }
93}
94
95cfg_if! {
96    if #[cfg(feature = "max-group-endpoints-per-fabric-5")] {
97        /// Max number of endpoints per group entry.
98        pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 5;
99    } else if #[cfg(feature = "max-group-endpoints-per-fabric-4")] {
100        /// Max number of endpoints per group entry.
101        pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 4;
102    } else if #[cfg(feature = "max-group-endpoints-per-fabric-3")] {
103        /// Max number of endpoints per group entry.
104        pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 3;
105    } else if #[cfg(feature = "max-group-endpoints-per-fabric-2")] {
106        /// Max number of endpoints per group entry.
107        pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 2;
108    } else if #[cfg(feature = "max-group-endpoints-per-fabric-1")] {
109        /// Max number of endpoints per group entry.
110        pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 1;
111    } else {
112        /// Max number of endpoints per group entry.
113        pub const GROUP_ENDPOINTS_PER_FABRIC: usize = 0;
114    }
115}
116
117/// A group table entry mapping a group ID to its endpoints and name.
118#[derive(Debug, FromTLV, ToTLV)]
119#[cfg_attr(feature = "defmt", derive(defmt::Format))]
120pub struct GroupEndpointMapping {
121    pub group_id: u16,
122    pub endpoints: Vec<u16, GROUP_ENDPOINTS_PER_FABRIC>,
123    pub group_name: String<MAX_GROUP_NAME_LEN>,
124}
125
126/// A stored group key map entry (maps group ID to key set).
127#[derive(Debug, Clone, Default, FromTLV, ToTLV)]
128#[cfg_attr(feature = "defmt", derive(defmt::Format))]
129pub struct GroupKeyMapping {
130    pub group_id: u16,
131    pub group_key_set_id: u16,
132}
133
134#[derive(Debug, FromTLV, ToTLV)]
135#[cfg_attr(feature = "defmt", derive(defmt::Format))]
136pub struct Groups {
137    /// Group key sets (excluding IPK which is stored in `ipk`)
138    key_sets: Vec<GroupKeySet, MAX_GROUP_KEYS_PER_FABRIC>,
139    /// Groups keyset mapping
140    key_map: Vec<GroupKeyMapping, MAX_GROUPS_PER_FABRIC>,
141    /// Group table (group ID → endpoints + name)
142    endpoint_mapping: Vec<GroupEndpointMapping, MAX_GROUPS_PER_FABRIC>,
143}
144
145impl Groups {
146    fn init() -> impl Init<Self> {
147        init!(Self {
148            key_sets <- Vec::init(),
149            key_map <- Vec::init(),
150            endpoint_mapping <- Vec::init(),
151        })
152    }
153
154    /// Return an iterator over the group key sets of the fabric
155    pub fn key_set_iter(&self) -> impl Iterator<Item = &GroupKeySet> {
156        self.key_sets.iter()
157    }
158
159    /// Find a group key set by ID
160    pub fn key_set_get(&self, id: u16) -> Option<&GroupKeySet> {
161        self.key_sets.iter().find(|e| e.group_key_set_id == id)
162    }
163
164    /// Add or update a group key set
165    pub fn key_set_add(&mut self, entry: GroupKeySet) -> Result<(), Error> {
166        if let Some(existing) = self
167            .key_sets
168            .iter_mut()
169            .find(|e| e.group_key_set_id == entry.group_key_set_id)
170        {
171            *existing = entry;
172        } else {
173            self.key_sets
174                .push(entry)
175                .map_err(|_| ErrorCode::ResourceExhausted)?;
176        }
177        Ok(())
178    }
179
180    /// Remove a group key set by ID. Returns true if found and removed.
181    pub fn key_set_remove(&mut self, id: u16) -> Result<(), Error> {
182        let before = self.key_sets.len();
183        self.key_sets.retain(|e| e.group_key_set_id != id);
184        let removed = self.key_sets.len() < before;
185
186        self.key_map_remove_by_key_set(id);
187
188        // Check if element was actually removed
189        if removed {
190            Ok(())
191        } else {
192            Err(Error::new(ErrorCode::NotFound))
193        }
194    }
195
196    pub fn key_map_add(&mut self, entry: GroupKeyMapping) -> Result<(), Error> {
197        self.key_map.push(entry).map_err(|_| ErrorCode::Failure)?;
198
199        Ok(())
200    }
201
202    /// Return an iterator over the group key map entries of the fabric
203    pub fn key_map_iter(&self) -> impl Iterator<Item = &GroupKeyMapping> {
204        self.key_map.iter()
205    }
206
207    /// Replace all group key map entries
208    pub fn key_map_replace(
209        &mut self,
210        entries: impl Iterator<Item = GroupKeyMapping>,
211    ) -> Result<(), Error> {
212        self.key_map.clear();
213        for entry in entries {
214            self.key_map
215                .push(entry)
216                .map_err(|_| ErrorCode::ResourceExhausted)?;
217        }
218        Ok(())
219    }
220
221    /// Remove group key map entries that reference a specific key set ID
222    pub fn key_map_remove_by_key_set(&mut self, key_set_id: u16) {
223        self.key_map.retain(|e| e.group_key_set_id != key_set_id);
224    }
225
226    /// Return an iterator over the group table entries
227    pub fn iter(&self) -> impl Iterator<Item = &GroupEndpointMapping> {
228        self.endpoint_mapping.iter()
229    }
230
231    /// Look up a group by ID
232    pub fn get(&self, group_id: u16) -> Option<&GroupEndpointMapping> {
233        self.endpoint_mapping
234            .iter()
235            .find(|e| e.group_id == group_id)
236    }
237
238    /// Add an endpoint to a group.
239    /// Returns true if the endpoint was already a member (name still updated per spec).
240    pub fn add(
241        &mut self,
242        endpoint_id: u16,
243        group_id: u16,
244        group_name: &str,
245    ) -> Result<bool, Error> {
246        let entry = if let Some(entry) = self
247            .endpoint_mapping
248            .iter_mut()
249            .find(|e| e.group_id == group_id)
250        {
251            entry
252        } else {
253            self.endpoint_mapping
254                .push(GroupEndpointMapping {
255                    group_id,
256                    endpoints: Vec::new(),
257                    group_name: unwrap!(String::from_str(group_name)),
258                })
259                .map_err(|_| ErrorCode::ResourceExhausted)?;
260            unwrap!(self.endpoint_mapping.last_mut())
261        };
262
263        // Update group name
264        entry.group_name.clear();
265        unwrap!(entry.group_name.push_str(group_name));
266
267        if entry.endpoints.contains(&endpoint_id) {
268            return Ok(true);
269        }
270
271        entry
272            .endpoints
273            .push(endpoint_id)
274            .map_err(|_| ErrorCode::ResourceExhausted)?;
275
276        Ok(false)
277    }
278
279    /// Remove an endpoint from a group, or from all groups if `group_id` is `None`.
280    /// Returns true if the endpoint was removed from at least one group.
281    pub fn remove(&mut self, endpoint_id: u16, group_id: Option<u16>) -> bool {
282        let mut removed = false;
283
284        for entry in self.endpoint_mapping.iter_mut() {
285            if group_id.is_some_and(|id| id != entry.group_id) {
286                continue;
287            }
288            let before = entry.endpoints.len();
289            entry.endpoints.retain(|&ep| ep != endpoint_id);
290            if entry.endpoints.len() < before {
291                removed = true;
292            }
293        }
294
295        // Remove entries with no endpoints left
296        self.endpoint_mapping.retain(|e| !e.endpoints.is_empty());
297
298        removed
299    }
300}
301
302/// Fabric type
303#[derive(Debug, ToTLV, FromTLV)]
304#[cfg_attr(feature = "defmt", derive(defmt::Format))]
305pub struct Fabric {
306    /// Fabric local index
307    fab_idx: NonZeroU8,
308    /// Fabric node ID
309    node_id: u64,
310    /// Fabric ID
311    fabric_id: u64,
312    /// Vendor ID
313    vendor_id: u16,
314    /// Compressed ID
315    compressed_fabric_id: u64,
316    /// Fabric secret key
317    secret_key: CanonPkcSecretKey,
318    /// Root CA certificate to be used when verifying the node's certificate
319    ///
320    /// Note that we deviate from the Matter spec here, in that we store the
321    /// root certificate in the Fabric type itself, rather than - as the
322    /// spec mandates - in a separate Root CA store
323    ///
324    /// This simplifies the implementation, but results in potentially multiple
325    /// copies of the same Root CA used accross multiple fabrics.
326    root_ca: Vec<u8, { MAX_CERT_TLV_LEN }>,
327    /// Either the Intermediate CA certificate (`vvsc_set == false`) or the
328    /// Vendor Verification Signing Cert (`vvsc_set == true`). The two are
329    /// mutually exclusive in the cert chain (Matter Core spec) —
330    /// a fabric with an ICAC cannot also carry a VVSC and vice
331    /// versa — so we share one buffer instead of paying for both. Empty
332    /// means neither is set; in that case `vvsc_set` is meaningless.
333    icac_or_vvsc: Vec<u8, { MAX_CERT_TLV_LEN }>,
334    /// Selector for what `icac_or_vvsc` holds: `false` for an ICAC,
335    /// `true` for a VVSC.
336    vvsc_set: bool,
337    /// Node Operational Certificate
338    noc: Vec<u8, { MAX_CERT_TLV_LEN }>,
339    /// Identity Protection Key
340    ipk: KeySet,
341    /// Fabric label; unique accross all fabrics on the device
342    label: String<32>,
343    /// Access Control List
344    acl: Vec<AclEntry, { acl::MAX_ACL_ENTRIES_PER_FABRIC }>,
345    /// Fabric group information
346    groups: Groups,
347    /// VID Verification Statement (Matter Core spec).
348    /// Either empty (not set) or exactly `VID_VERIFICATION_STATEMENT_LEN`
349    /// bytes long; the cluster XML enforces both bounds at the schema
350    /// level (`length="85" minLength="85"`).
351    vid_verification_statement: Vec<u8, VID_VERIFICATION_STATEMENT_LEN>,
352}
353
354/// Exact length of a non-empty VID Verification Statement.
355/// Matches `length="85" minLength="85"` on
356/// `OperationalCredentials::SetVIDVerificationStatement.vid_verification_statement`.
357pub const VID_VERIFICATION_STATEMENT_LEN: usize = 85;
358
359impl Fabric {
360    /// Return an in-place-initializer for a Fabric type, with the
361    /// provided Fabric Index and KeyPair
362    ///
363    /// All other fields are initialized to default values, which are NOT
364    /// valid for the operation of the fabric.
365    ///
366    /// The Fabric must be updated with the correct values before it can be
367    /// used, via `Fabric::update`.
368    fn init(fab_idx: NonZeroU8) -> impl Init<Self> {
369        init!(Self {
370            fab_idx,
371            node_id: 0,
372            fabric_id: 0,
373            vendor_id: 0,
374            compressed_fabric_id: 0,
375            secret_key <- CanonPkcSecretKey::init(),
376            root_ca <- Vec::init(),
377            icac_or_vvsc <- Vec::init(),
378            vvsc_set: false,
379            noc <- Vec::init(),
380            ipk <- KeySet::init(),
381            label: String::new(),
382            acl <- Vec::init(),
383            groups <- Groups::init(),
384            vid_verification_statement <- Vec::init(),
385        })
386    }
387
388    /// Update the fabric with the provided data so that it can operate.
389    ///
390    /// This method is supposed to be called right after `Fabric::init` or
391    /// when the NOC of the fabric needs to be updated.
392    ///
393    /// `root_ca` is `None` when called from the `UpdateNOC` flow — Matter
394    /// Core spec keeps the fabric's root cert unchanged
395    /// across `UpdateNOC`, and re-passing the existing bytes here would
396    /// require a (large) caller-side copy of `self.root_ca`. `Some(...)`
397    /// is used by the initial `AddNOC` flow, where the cert was just
398    /// staged in the fail-safe context.
399    #[allow(clippy::too_many_arguments)]
400    fn update<C: Crypto>(
401        &mut self,
402        crypto: C,
403        root_ca: Option<&[u8]>,
404        noc: &[u8],
405        icac: &[u8],
406        secret_key: CanonPkcSecretKeyRef<'_>,
407        epoch_key: Option<CanonAeadKeyRef<'_>>,
408        vendor_id: Option<u16>,
409        case_admin_subject: Option<u64>,
410    ) -> Result<(), Error> {
411        if let Some(root_ca) = root_ca {
412            self.root_ca.clear();
413            self.root_ca
414                .extend_from_slice(root_ca)
415                .map_err(|_| ErrorCode::BufferTooSmall)?;
416        }
417        // `AddNOC` / `UpdateNOC` always replace the cert chain, so any
418        // previously-staged VVSC for this fabric is implicitly cleared
419        // here — the spec doesn't allow an ICAC and a VVSC to coexist.
420        self.icac_or_vvsc.clear();
421        self.icac_or_vvsc
422            .extend_from_slice(icac)
423            .map_err(|_| ErrorCode::BufferTooSmall)?;
424        self.vvsc_set = false;
425        self.noc.clear();
426        self.noc
427            .extend_from_slice(noc)
428            .map_err(|_| ErrorCode::BufferTooSmall)?;
429
430        let root_cert = CertRef::new(TLVElement::new(self.root_ca.as_slice()));
431        let noc_cert = CertRef::new(TLVElement::new(noc));
432
433        self.node_id = noc_cert.get_node_id()?;
434        self.fabric_id = noc_cert.get_fabric_id()?;
435        self.compressed_fabric_id = Self::compute_compressed_fabric_id(
436            &crypto,
437            root_cert.pubkey()?.try_into()?,
438            self.fabric_id,
439        );
440
441        if let Some(epoch_key) = epoch_key {
442            self.ipk
443                .update(&crypto, epoch_key, &self.compressed_fabric_id)?;
444        }
445
446        if let Some(vendor_id) = vendor_id {
447            self.vendor_id = vendor_id;
448        }
449
450        if let Some(case_admin_subject) = case_admin_subject {
451            self.acl.clear();
452            self.acl.push_init(
453                AclEntry::init(None, Privilege::ADMIN, AuthMode::Case)
454                    .into_fallible()
455                    .chain(|e| {
456                        e.fab_idx = Some(self.fab_idx);
457                        e.add_subject(case_admin_subject)
458                    }),
459                || ErrorCode::ResourceExhausted.into(),
460            )?;
461        }
462
463        self.secret_key.load(secret_key);
464
465        Ok(())
466    }
467
468    pub fn mdns_service(&self) -> Option<MatterLocalService> {
469        self.mdns_service_for(self.node_id)
470    }
471
472    pub fn mdns_service_for(&self, node_id: u64) -> Option<MatterLocalService> {
473        (!self.noc.is_empty()).then_some(MatterLocalService::Commissioned {
474            compressed_fabric_id: self.compressed_fabric_id,
475            node_id,
476        })
477    }
478
479    /// Is the fabric matching the privided destination ID
480    pub fn is_dest_id<C: Crypto>(
481        &self,
482        crypto: C,
483        random: &[u8],
484        target: &[u8],
485    ) -> Result<(), Error> {
486        let mut mac = crypto.hmac(self.ipk.op_key())?;
487
488        mac.update(random)?;
489        mac.update(CertRef::new(TLVElement::new(self.root_ca())).pubkey()?)?;
490
491        mac.update(&self.fabric_id.to_le_bytes())?;
492        mac.update(&self.node_id.to_le_bytes())?;
493
494        let mut id = MaybeUninit::<Hash>::uninit(); // TODO MEDIUM BUFFER
495        let id = id.init_with(Hash::init());
496        mac.finish(id)?;
497        if id.access() == target {
498            Ok(())
499        } else {
500            Err(ErrorCode::NotFound.into())
501        }
502    }
503
504    /// Compute the destination identifier for a target node on this fabric.
505    ///
506    /// Used by the CASE initiator to build Sigma1 (spec).
507    /// destinationMessage = initiatorRandom || rootPublicKey || fabricId(LE) || nodeId(LE)
508    /// destinationIdentifier = Crypto_HMAC(key=IPK, message=destinationMessage)
509    ///
510    /// # Arguments
511    /// - `target_node_id`: The node ID of the destination (peer) node, NOT the local node.
512    pub fn compute_dest_id<C: Crypto>(
513        &self,
514        crypto: C,
515        random: &[u8],
516        target_node_id: u64,
517        out: &mut Hash,
518    ) -> Result<(), Error> {
519        let mut mac = crypto.hmac(self.ipk.op_key())?;
520
521        mac.update(random)?;
522        mac.update(CertRef::new(TLVElement::new(self.root_ca())).pubkey()?)?;
523        mac.update(&self.fabric_id.to_le_bytes())?;
524        mac.update(&target_node_id.to_le_bytes())?;
525
526        mac.finish(out)?;
527        Ok(())
528    }
529
530    /// Return the secret key of the fabric
531    pub fn secret_key(&self) -> CanonPkcSecretKeyRef<'_> {
532        self.secret_key.reference()
533    }
534
535    /// Return the fabric's node ID
536    pub fn node_id(&self) -> u64 {
537        self.node_id
538    }
539
540    /// Return the fabric's fabric ID
541    pub fn fabric_id(&self) -> u64 {
542        self.fabric_id
543    }
544
545    /// Return the fabric's local index
546    pub fn fab_idx(&self) -> NonZeroU8 {
547        self.fab_idx
548    }
549
550    /// Return the fabric's compressed fabric ID
551    pub fn compressed_fabric_id(&self) -> u64 {
552        self.compressed_fabric_id
553    }
554
555    /// Return the fabric's Vendor ID
556    pub fn vendor_id(&self) -> u16 {
557        self.vendor_id
558    }
559
560    /// Return the fabric's label
561    pub fn label(&self) -> &str {
562        &self.label
563    }
564
565    /// Return the fabric's Root CA in encoded TLV form
566    ///
567    /// Use `CertRef` to decode on the fly
568    pub fn root_ca(&self) -> &[u8] {
569        &self.root_ca
570    }
571
572    /// Return the fabric's ICAC in encoded TLV form
573    ///
574    /// Use `CertRef` to decode on the fly.
575    ///
576    /// Note that this method might return an empty slice,
577    /// which indicates that this fabric does not have an ICAC.
578    /// (The shared `icac_or_vvsc` slot may instead hold a VVSC; see
579    /// `vvsc()`.)
580    pub fn icac(&self) -> &[u8] {
581        if self.vvsc_set {
582            &[]
583        } else {
584            &self.icac_or_vvsc
585        }
586    }
587
588    /// Return the fabric's NOC
589    pub fn noc(&self) -> &[u8] {
590        &self.noc
591    }
592
593    /// Return the fabric's IPK
594    pub fn ipk(&self) -> &KeySet {
595        &self.ipk
596    }
597
598    /// Return the fabric's groups
599    pub fn groups(&self) -> &Groups {
600        &self.groups
601    }
602
603    /// Return a mutable reference to the fabric's groups
604    pub fn groups_mut(&mut self) -> &mut Groups {
605        &mut self.groups
606    }
607
608    /// Return the fabric's VVSC bytes (Matter Core spec).
609    /// Empty when `SetVIDVerificationStatement` has never been called with
610    /// a non-empty VVSC for this fabric, or when the fabric instead carries
611    /// an ICAC (see `icac()`) — VVSC and ICAC share storage and are
612    /// mutually exclusive per spec.
613    pub fn vvsc(&self) -> &[u8] {
614        if self.vvsc_set {
615            &self.icac_or_vvsc
616        } else {
617            &[]
618        }
619    }
620
621    /// Return the fabric's VID Verification Statement bytes (Matter Core
622    /// spec). Either empty (not set) or
623    /// exactly `VID_VERIFICATION_STATEMENT_LEN` bytes.
624    pub fn vid_verification_statement(&self) -> &[u8] {
625        &self.vid_verification_statement
626    }
627
628    /// Apply a `SetVIDVerificationStatement` mutation to the fabric. Each
629    /// field is `Some(slice)` for "replace with this value" (where an
630    /// empty slice clears the value), or `None` for "leave unchanged".
631    /// The caller is responsible for spec-level validation (size limits,
632    /// VVSC vs ICAC mutual exclusion, "all fields absent" → INVALID_COMMAND,
633    /// VendorID range, …); this method only enforces the storage
634    /// invariants (heapless `Vec` capacity).
635    pub fn set_vid_verification(
636        &mut self,
637        vendor_id: Option<u16>,
638        vid_verification_statement: Option<&[u8]>,
639        vvsc: Option<&[u8]>,
640    ) -> Result<(), Error> {
641        if let Some(vid) = vendor_id {
642            self.vendor_id = vid;
643        }
644
645        if let Some(vvs) = vid_verification_statement {
646            self.vid_verification_statement.clear();
647            self.vid_verification_statement
648                .extend_from_slice(vvs)
649                .map_err(|_| ErrorCode::BufferTooSmall)?;
650        }
651
652        if let Some(v) = vvsc {
653            // VVSC and ICAC share `icac_or_vvsc`. Clearing the VVSC must
654            // not stomp on an existing ICAC: per spec the
655            // two never coexist on the same fabric, so an empty-VVSC
656            // request against a fabric that holds an ICAC is a no-op
657            // here. The cluster handler still rejects a *non-empty* VVSC
658            // against such a fabric upstream.
659            if !v.is_empty() {
660                self.icac_or_vvsc.clear();
661                self.icac_or_vvsc
662                    .extend_from_slice(v)
663                    .map_err(|_| ErrorCode::BufferTooSmall)?;
664                self.vvsc_set = true;
665            } else if self.vvsc_set {
666                self.icac_or_vvsc.clear();
667                self.vvsc_set = false;
668            }
669        }
670
671        Ok(())
672    }
673
674    /// Return an iterator over the ACL entries of the fabric
675    pub fn acl_iter(&self) -> impl Iterator<Item = &AclEntry> {
676        self.acl.iter()
677    }
678
679    /// Add a new ACL entry to the fabric.
680    ///
681    /// Return the index of the added entry.
682    pub fn acl_add(&mut self, mut entry: AclEntry) -> Result<usize, Error> {
683        if entry.auth_mode() == AuthMode::Pase {
684            // Reserved for future use
685            Err(ErrorCode::ConstraintError)?;
686        }
687
688        // Overwrite the fabric index with our accessing fabric index
689        entry.fab_idx = Some(self.fab_idx);
690
691        self.acl
692            .push(entry)
693            .map_err(|_| ErrorCode::ResourceExhausted)?;
694
695        Ok(self.acl.len() - 1)
696    }
697
698    /// Add a new ACL entry to the fabric using the supplied initializer.
699    ///
700    /// Return the index of the added entry.
701    pub fn acl_add_init<I>(&mut self, init: I) -> Result<usize, Error>
702    where
703        I: Init<AclEntry, Error>,
704    {
705        // if entry.auth_mode() == AuthMode::Pase {
706        //     // Reserved for future use
707        //     Err(ErrorCode::ConstraintError)?;
708        // }
709
710        self.acl
711            .push_init(init, || ErrorCode::ResourceExhausted.into())?;
712
713        let idx = self.acl.len() - 1;
714        let entry = &mut self.acl[idx];
715
716        // Overwrite the fabric index with our accessing fabric index
717        entry.fab_idx = Some(self.fab_idx);
718
719        Ok(idx)
720    }
721
722    /// Update an existing ACL entry in the fabric
723    pub fn acl_update(&mut self, idx: usize, mut entry: AclEntry) -> Result<(), Error> {
724        if self.acl.len() <= idx {
725            return Err(ErrorCode::NotFound.into());
726        }
727
728        // Overwrite the fabric index with our accessing fabric index
729        entry.fab_idx = Some(self.fab_idx);
730
731        self.acl[idx] = entry;
732
733        Ok(())
734    }
735
736    /// Update an existing ACL entry in the fabric using the supplied initializer
737    pub fn acl_update_init<I>(&mut self, idx: usize, init: I) -> Result<(), Error>
738    where
739        I: Init<AclEntry, Error>,
740    {
741        if self.acl.len() <= idx {
742            return Err(ErrorCode::NotFound.into());
743        }
744
745        // TODO: Needs #214
746        let mut entry = MaybeUninit::uninit();
747        let entry = entry.try_init_with(init)?.clone();
748
749        self.acl[idx] = entry;
750
751        // Overwrite the fabric index with our accessing fabric index
752        self.acl[idx].fab_idx = Some(self.fab_idx);
753
754        Ok(())
755    }
756
757    /// Remove an ACL entry from the fabric
758    pub fn acl_remove(&mut self, idx: usize) -> Result<(), Error> {
759        if self.acl.len() <= idx {
760            return Err(ErrorCode::NotFound.into());
761        }
762
763        self.acl.remove(idx);
764
765        Ok(())
766    }
767
768    /// Remove all ACL entries from the fabric
769    pub fn acl_remove_all(&mut self) {
770        // pub for tests
771        self.acl.clear();
772    }
773
774    /// Check if the fabric allows the given access request
775    ///
776    /// Note that the fabric index in the access request needs to be checked before that.
777    fn allow(&self, req: &AccessReq) -> bool {
778        for e in &self.acl {
779            if e.allow(req) {
780                return true;
781            }
782        }
783
784        debug!(
785            "ACL Disallow for subjects {} fab idx {}",
786            req.accessor().subjects(),
787            req.accessor().fab_idx
788        );
789
790        false
791    }
792
793    /// Compute the compressed fabric ID
794    pub(crate) fn compute_compressed_fabric_id<C: Crypto>(
795        crypto: C,
796        root_pubkey: CanonPkcPublicKeyRef<'_>,
797        fabric_id: u64,
798    ) -> u64 {
799        const COMPRESSED_FABRIC_ID_INFO: &[u8; 16] = &[
800            0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x46, 0x61, 0x62, 0x72,
801            0x69, 0x63,
802        ];
803
804        let mut compressed_fabric_id = CryptoSensitive::<{ COMPRESSED_FABRIC_ID_LEN }>::new();
805        unwrap!(unwrap!(crypto.kdf()).expand(
806            &fabric_id.to_be_bytes(),
807            root_pubkey.split::<1, { PKC_CANON_PUBLIC_KEY_LEN - 1 }>().1,
808            COMPRESSED_FABRIC_ID_INFO,
809            &mut compressed_fabric_id,
810        ));
811
812        u64::from_be_bytes(*compressed_fabric_id.access())
813    }
814}
815
816cfg_if! {
817    if #[cfg(feature = "max-fabrics-32")] {
818        /// Max number of supported fabrics
819        pub const MAX_FABRICS: usize = 32;
820    } else if #[cfg(feature = "max-fabrics-16")] {
821        /// Max number of supported fabrics
822        pub const MAX_FABRICS: usize = 16;
823    } else if #[cfg(feature = "max-fabrics-8")] {
824        /// Max number of supported fabrics
825        pub const MAX_FABRICS: usize = 8;
826    } else if #[cfg(feature = "max-fabrics-7")] {
827        /// Max number of supported fabrics
828        pub const MAX_FABRICS: usize = 7;
829    } else if #[cfg(feature = "max-fabrics-6")] {
830        /// Max number of supported fabrics
831        pub const MAX_FABRICS: usize = 6;
832    } else { // Matter requires a minimum of 5 fabrics
833        /// Max number of supported fabrics
834        pub const MAX_FABRICS: usize = 5;
835    }
836}
837
838/// All fabrics
839pub struct Fabrics {
840    fabrics: Vec<Fabric, MAX_FABRICS>,
841}
842
843impl Default for Fabrics {
844    fn default() -> Self {
845        Self::new()
846    }
847}
848
849impl Fabrics {
850    /// Create a new Fabrics instance
851    #[inline(always)]
852    pub const fn new() -> Self {
853        Self {
854            fabrics: Vec::new(),
855        }
856    }
857
858    /// Return an in-place-initializer for a Fabrics type
859    pub fn init() -> impl Init<Self> {
860        init!(Self {
861            fabrics <- Vec::init(),
862        })
863    }
864
865    /// Remove all fabrics
866    pub fn reset(&mut self) {
867        self.fabrics.clear();
868    }
869
870    /// Remove all fabrics from the provided BLOB store as well as from memory.
871    ///
872    /// # Arguments
873    /// - `store`: the BLOB store to remove the fabrics from
874    /// - `buf`: a temporary buffer to use for removing the fabrics
875    pub fn reset_persist<S: KvBlobStore>(
876        &mut self,
877        mut store: S,
878        buf: &mut [u8],
879    ) -> Result<(), Error> {
880        self.reset();
881
882        for idx in 1..=255u8 {
883            store.remove(FABRIC_KEYS_START + idx as u16, buf)?;
884        }
885
886        info!("Removed all fabrics from storage");
887
888        Ok(())
889    }
890
891    /// Load all fabrics from the provided BLOB store
892    ///
893    /// # Arguments
894    /// - `store`: the BLOB store to load the fabrics from
895    /// - `buf`: a temporary buffer to use for loading the fabrics
896    pub fn load_persist<S: KvBlobStore>(
897        &mut self,
898        mut store: S,
899        buf: &mut [u8],
900    ) -> Result<(), Error> {
901        self.reset();
902
903        for fab_idx in 1..=255u8 {
904            self.add_load(fab_idx, &mut store, buf)?;
905        }
906
907        Ok(())
908    }
909
910    pub(crate) fn add_load<S: KvBlobStore>(
911        &mut self,
912        fab_idx: u8,
913        mut store: S,
914        buf: &mut [u8],
915    ) -> Result<(), Error> {
916        if let Some(data) = store.load(FABRIC_KEYS_START + fab_idx as u16, buf)? {
917            self.fabrics
918                .push_init(Fabric::init_from_tlv(TLVElement::new(data)), || {
919                    ErrorCode::ResourceExhausted.into()
920                })?;
921
922            let fabric = unwrap!(self.fabrics.last());
923
924            info!(
925                "Loaded fabric {} with ID {:x} from storage",
926                fabric.fab_idx(),
927                fabric.compressed_fabric_id()
928            );
929        }
930
931        Ok(())
932    }
933
934    /// Add a new fabric to the fabrics with the provided data and immediately updates it with the provided post-init updater.
935    ///
936    /// This method is unlikely to be useful outside of tests.
937    ///
938    /// If this operation succeeds, the fabric immediately becomes operational.
939    pub fn add_with_post_init<F>(&mut self, post_init: F) -> Result<&mut Fabric, Error>
940    where
941        F: FnOnce(&mut Fabric) -> Result<(), Error>,
942    {
943        let max_fab_idx = self
944            .iter()
945            .map(|fabric| fabric.fab_idx().get())
946            .max()
947            .unwrap_or(0);
948        let fab_idx = unwrap!(NonZeroU8::new(if max_fab_idx < u8::MAX - 1 {
949            // First try with the next available fabric index larger than all currently used
950            max_fab_idx + 1
951        } else {
952            // If there is already a fabric with index 254, try to find the first unused one
953            let Some(fab_idx) = (1..u8::MAX)
954                .find(|fab_idx| self.iter().all(|fabric| fabric.fab_idx().get() != *fab_idx))
955            else {
956                return Err(ErrorCode::ResourceExhausted.into());
957            };
958
959            fab_idx
960        })); // We never use 0 as a fabric index, nor u8::MAX
961
962        self.fabrics.push_init(
963            Fabric::init(fab_idx)
964                .into_fallible::<Error>()
965                .chain(post_init),
966            || ErrorCode::ResourceExhausted.into(),
967        )?;
968
969        let fabric = unwrap!(self.fabrics.last_mut());
970
971        Ok(fabric)
972    }
973
974    /// Add a new fabric to the fabrics with the provided data.
975    ///
976    /// If this operation succeeds, the fabric immediately becomes operational.
977    #[allow(clippy::too_many_arguments)]
978    pub fn add<C: Crypto>(
979        &mut self,
980        crypto: C,
981        secret_key: CanonPkcSecretKeyRef<'_>,
982        root_ca: &[u8],
983        noc: &[u8],
984        icac: &[u8],
985        epoch_key: Option<CanonAeadKeyRef<'_>>,
986        vendor_id: u16,
987        case_admin_subject: u64,
988    ) -> Result<&mut Fabric, Error> {
989        self.add_with_post_init(|fabric| {
990            fabric.update(
991                crypto,
992                Some(root_ca),
993                noc,
994                icac,
995                secret_key,
996                epoch_key,
997                Some(vendor_id),
998                Some(case_admin_subject),
999            )
1000        })
1001    }
1002
1003    /// Update an existing fabric with the provided data (usually, as a result of an `UpdateNOC` IM command).
1004    ///
1005    /// The fabric's existing root cert is preserved across this call —
1006    /// `UpdateNOC` per Matter Core spec is not allowed
1007    /// to change the root, and re-passing the bytes would force the
1008    /// caller to take a (large) heap-less copy of `Fabric::root_ca`.
1009    ///
1010    /// If this operation succeeds, the fabric immediately becomes operational.
1011    /// Note however, that the caller is expected to remove all sessions associated with the fabric, as they would
1012    /// contain invalid keys after the NOC update.
1013    pub fn update<C: Crypto>(
1014        &mut self,
1015        crypto: C,
1016        fab_idx: NonZeroU8,
1017        secret_key: CanonPkcSecretKeyRef<'_>,
1018        noc: &[u8],
1019        icac: &[u8],
1020    ) -> Result<&mut Fabric, Error> {
1021        let fabric = self.fabric_mut(fab_idx)?;
1022
1023        fabric.update(crypto, None, noc, icac, secret_key, None, None, None)?;
1024
1025        Ok(fabric)
1026    }
1027
1028    pub fn update_label(&mut self, fab_idx: NonZeroU8, label: &str) -> Result<&mut Fabric, Error> {
1029        if self.iter().any(|fabric| {
1030            fabric.fab_idx != fab_idx && !fabric.label.is_empty() && fabric.label == label
1031        }) {
1032            return Err(ErrorCode::Invalid.into());
1033        }
1034
1035        let fabric = self.fabric_mut(fab_idx)?;
1036        fabric.label.clear();
1037        fabric
1038            .label
1039            .push_str(label)
1040            .map_err(|_| ErrorCode::ConstraintError)?;
1041
1042        Ok(fabric)
1043    }
1044
1045    /// Remove a fabric from the fabrics
1046    pub fn remove(&mut self, fab_idx: NonZeroU8) -> Result<(), Error> {
1047        let _ = self.fabric(fab_idx)?;
1048
1049        self.fabrics.retain(|fabric| fabric.fab_idx != fab_idx);
1050
1051        Ok(())
1052    }
1053
1054    /// Get a fabric that matches the provided destination ID
1055    pub fn get_by_dest_id<C: Crypto>(
1056        &self,
1057        crypto: C,
1058        random: &[u8],
1059        target: &[u8],
1060    ) -> Option<&Fabric> {
1061        self.iter()
1062            .find(|fabric| fabric.is_dest_id(&crypto, random, target).is_ok())
1063    }
1064
1065    /// Get a fabric by its local index
1066    pub fn get(&self, fab_idx: NonZeroU8) -> Option<&Fabric> {
1067        self.iter().find(|fabric| fabric.fab_idx == fab_idx)
1068    }
1069
1070    /// Get a mutable fabric reference by its local index
1071    pub fn get_mut(&mut self, fab_idx: NonZeroU8) -> Option<&mut Fabric> {
1072        // pub for testing
1073        self.fabrics
1074            .iter_mut()
1075            .find(|fabric| fabric.fab_idx == fab_idx)
1076    }
1077
1078    /// Iterate over the fabrics
1079    pub fn iter(&self) -> impl Iterator<Item = &Fabric> {
1080        self.fabrics.iter()
1081    }
1082
1083    /// Get a fabric by its local index
1084    ///
1085    /// Returns an error if the fabric is not found
1086    pub fn fabric(&self, fab_idx: NonZeroU8) -> Result<&Fabric, Error> {
1087        self.get(fab_idx).ok_or(ErrorCode::NotFound.into())
1088    }
1089
1090    /// Get a mutable fabric reference by its local index
1091    ///
1092    /// Returns an error if the fabric is not found
1093    pub fn fabric_mut(&mut self, fab_idx: NonZeroU8) -> Result<&mut Fabric, Error> {
1094        self.get_mut(fab_idx).ok_or(ErrorCode::NotFound.into())
1095    }
1096
1097    /// Check if the given access request should be allowed, based on all operational fabrics
1098    /// and their ACLs
1099    pub fn allow(&self, req: &AccessReq) -> bool {
1100        // PASE Sessions with no fabric index have implicit access grant,
1101        // but only as long as the ACL list is empty
1102        //
1103        // As per the spec:
1104        // The Access Control List is able to have an initial entry added because the Access Control Privilege
1105        // Granting algorithm behaves as if, over a PASE commissioning channel during the commissioning
1106        // phase, the following implicit Access Control Entry were present on the Commissionee (but not on
1107        // the Commissioner):
1108        // Access Control Cluster: {
1109        //     ACL: [
1110        //         0: {
1111        //             // implicit entry only; does not explicitly exist!
1112        //             FabricIndex: 0, // not fabric-specific
1113        //             Privilege: Administer,
1114        //             AuthMode: PASE,
1115        //             Subjects: [],
1116        //             Targets: [] // entire node
1117        //         }
1118        //     ],
1119        //     Extension: []
1120        // }
1121        if req.accessor().auth_mode() == Some(AuthMode::Pase) {
1122            return true;
1123        }
1124
1125        let Ok(fab_idx) = req.accessor().fab_idx() else {
1126            return false;
1127        };
1128
1129        let Some(fabric) = self.get(fab_idx) else {
1130            return false;
1131        };
1132
1133        fabric.allow(req)
1134    }
1135}
1136
1137/// A utility for persisting a fabric in a `KvBlobStore` instance.
1138pub struct FabricPersist<S>(Persist<S>);
1139
1140impl<S> FabricPersist<S>
1141where
1142    S: KvBlobStoreAccess,
1143{
1144    /// Create a new `FabricPersist` with the given key-value store instance.
1145    pub const fn new(kvb: S) -> Self {
1146        Self(Persist::new(kvb))
1147    }
1148
1149    /// Return a reference to the underlying `Persist` instance.
1150    pub fn persist_mut(&mut self) -> &mut Persist<S> {
1151        &mut self.0
1152    }
1153
1154    /// Save the provided fabric in the persistent storage.
1155    pub fn store(&mut self, fabric: &Fabric) -> Result<(), Error> {
1156        self.0
1157            .store_tlv(FABRIC_KEYS_START + fabric.fab_idx().get() as u16, fabric)
1158    }
1159
1160    /// Remove the fabric with the given index from the persistent storage.
1161    pub fn remove(&mut self, fab_idx: NonZeroU8) -> Result<(), Error> {
1162        self.0.remove(FABRIC_KEYS_START + fab_idx.get() as u16)
1163    }
1164
1165    /// Call at the end when finished with everything else
1166    /// No-op for now
1167    pub fn run(self) -> Result<(), Error> {
1168        self.0.run()
1169    }
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174    use core::mem::MaybeUninit;
1175
1176    use crate::cert::gen::{CertGenerator, CertType, IssuerDN, SubjectDN, Validity};
1177    use crate::cert::MAX_CERT_TLV_AND_ASN1_LEN;
1178    use crate::crypto::test_only_crypto;
1179    use crate::crypto::{
1180        CanonAeadKeyRef, CanonPkcSecretKey, Crypto, Hash, PublicKey, SecretKey, SigningSecretKey,
1181        AEAD_CANON_KEY_LEN,
1182    };
1183    use crate::utils::init::InitMaybeUninit;
1184
1185    use super::Fabrics;
1186
1187    /// Verify that `compute_dest_id` and `is_dest_id` agree: the hash output by
1188    /// `compute_dest_id` must be accepted by `is_dest_id` on the same fabric with
1189    /// the same random nonce.
1190    ///
1191    /// Uses runtime-generated certs (via `CertGenerator`) with a real keypair
1192    /// so the fabric is in a valid state — the secret key matches the NOC's public key.
1193    #[test]
1194    fn test_compute_dest_id_matches_is_dest_id() {
1195        let crypto = test_only_crypto();
1196
1197        let fabric_id: u64 = 1;
1198        let rcac_id: u64 = 1;
1199        let node_id: u64 = 100;
1200
1201        // Generate RCAC keypair and build self-signed RCAC
1202        let rcac_secret_key = crypto.generate_secret_key().unwrap();
1203        let mut rcac_pubkey_canon = crate::crypto::CanonPkcPublicKey::new();
1204        rcac_secret_key
1205            .pub_key()
1206            .unwrap()
1207            .write_canon(&mut rcac_pubkey_canon)
1208            .unwrap();
1209
1210        let validity = Validity {
1211            not_before: 0,
1212            not_after: 0,
1213        };
1214
1215        let mut rcac_buf = [0u8; MAX_CERT_TLV_AND_ASN1_LEN];
1216        let rcac_len = CertGenerator::new(&mut rcac_buf)
1217            .generate(
1218                &crypto,
1219                CertType::Rcac,
1220                &[0x01],
1221                validity,
1222                SubjectDN {
1223                    node_id: None,
1224                    fabric_id: Some(fabric_id),
1225                    cat_ids: &[],
1226                    ca_id: Some(rcac_id),
1227                },
1228                IssuerDN {
1229                    ca_id: None,
1230                    fabric_id: None,
1231                    is_rcac: false,
1232                },
1233                rcac_pubkey_canon.reference(),
1234                None,
1235                &rcac_secret_key,
1236            )
1237            .unwrap();
1238
1239        // Generate NOC keypair and build NOC signed by RCAC
1240        let noc_secret_key = crypto.generate_secret_key().unwrap();
1241        let mut noc_pubkey_canon = crate::crypto::CanonPkcPublicKey::new();
1242        noc_secret_key
1243            .pub_key()
1244            .unwrap()
1245            .write_canon(&mut noc_pubkey_canon)
1246            .unwrap();
1247
1248        let mut noc_secret_key_canon = CanonPkcSecretKey::new();
1249        noc_secret_key
1250            .write_canon(&mut noc_secret_key_canon)
1251            .unwrap();
1252
1253        let mut noc_buf = [0u8; MAX_CERT_TLV_AND_ASN1_LEN];
1254        let noc_len = CertGenerator::new(&mut noc_buf)
1255            .generate(
1256                &crypto,
1257                CertType::Noc,
1258                &[0x02],
1259                validity,
1260                SubjectDN {
1261                    node_id: Some(node_id),
1262                    fabric_id: Some(fabric_id),
1263                    cat_ids: &[],
1264                    ca_id: None,
1265                },
1266                IssuerDN {
1267                    ca_id: Some(rcac_id),
1268                    fabric_id: Some(fabric_id),
1269                    is_rcac: true,
1270                },
1271                noc_pubkey_canon.reference(),
1272                Some(rcac_pubkey_canon.reference()),
1273                &rcac_secret_key,
1274            )
1275            .unwrap();
1276
1277        // Build fabric with real certs and matching secret key
1278        let epoch_key = [0x5a_u8; AEAD_CANON_KEY_LEN];
1279        let mut fabrics = Fabrics::new();
1280        fabrics
1281            .add(
1282                &crypto,
1283                noc_secret_key_canon.reference(),
1284                &rcac_buf[..rcac_len],
1285                &noc_buf[..noc_len],
1286                &[], // no ICAC
1287                Some(CanonAeadKeyRef::new(&epoch_key)),
1288                0x8000,
1289                node_id,
1290            )
1291            .expect("Fabrics::add should succeed");
1292
1293        let fab_idx = core::num::NonZeroU8::new(1).unwrap();
1294        let fabric = fabrics
1295            .get(fab_idx)
1296            .expect("fabric at index 1 should exist");
1297
1298        let random = [0xABu8; 32];
1299
1300        // Compute the destination ID (targeting this fabric's own node).
1301        let mut dest_id = MaybeUninit::<Hash>::uninit();
1302        let dest_id = dest_id.init_with(Hash::init());
1303        fabric
1304            .compute_dest_id(&crypto, &random, fabric.node_id(), dest_id)
1305            .expect("compute_dest_id should not fail");
1306
1307        // is_dest_id must accept the computed value.
1308        fabric
1309            .is_dest_id(&crypto, &random, dest_id.access())
1310            .expect("is_dest_id should accept hash produced by compute_dest_id");
1311    }
1312}