Skip to main content

rs_matter/
failsafe.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::num::NonZeroU8;
19
20use embassy_time::{Duration, Instant};
21
22use crate::cert::{CertRef, MAX_CERT_TLV_LEN};
23use crate::crypto::{
24    CanonAeadKeyRef, CanonPkcSecretKey, CanonPkcSecretKeyRef, Crypto, PublicKey, SecretKey,
25    SigningSecretKey, PKC_SECRET_KEY_ZEROED,
26};
27use crate::dm::clusters::net_comm::NetworksAccess;
28use crate::dm::clusters::time_sync::UtcTime;
29use crate::dm::endpoints::ROOT_ENDPOINT_ID;
30use crate::dm::{ClusterId, EndptId};
31use crate::error::{Error, ErrorCode};
32use crate::fabric::{Fabric, Fabrics};
33use crate::im::IMStatusCode;
34use crate::persist::{KvBlobStoreAccess, NETWORKS_KEY};
35use crate::sc::pase::Pase;
36use crate::tlv::TLVElement;
37use crate::transport::session::SessionMode;
38use crate::utils::bitflags::bitflags;
39use crate::utils::init::{init, Init};
40use crate::utils::storage::Vec;
41
42bitflags! {
43    #[repr(transparent)]
44    #[derive(Default)]
45    #[cfg_attr(not(feature = "defmt"), derive(Debug, Copy, Clone, Eq, PartialEq, Hash))]
46    pub struct NocFlags: u8 {
47        const ADD_CSR_REQ_RECVD = 0x01;
48        const UPDATE_CSR_REQ_RECVD = 0x02;
49        const ADD_ROOT_CERT_RECVD = 0x04;
50        const ADD_NOC_RECVD = 0x08;
51        const UPDATE_NOC_RECVD = 0x10;
52    }
53}
54
55#[derive(PartialEq)]
56pub struct ArmedCtx {
57    armed_at: Instant,
58    timeout_secs: u16,
59    fab_idx: u8,
60    flags: NocFlags,
61}
62
63#[derive(PartialEq)]
64pub enum State {
65    Idle,
66    Armed(ArmedCtx),
67}
68
69pub enum IMError {
70    Error(Error),
71    Status(IMStatusCode),
72}
73
74impl From<Error> for IMError {
75    fn from(e: Error) -> Self {
76        IMError::Error(e)
77    }
78}
79
80impl From<IMStatusCode> for IMError {
81    fn from(e: IMStatusCode) -> Self {
82        IMError::Status(e)
83    }
84}
85
86/// Default fail-safe expiry length used when the device implicitly arms the
87/// fail-safe (e.g. on PASE session establishment). Mirrors
88/// `CHIP_DEVICE_CONFIG_FAILSAFE_EXPIRY_LENGTH_SEC` from the reference SDK.
89pub const DEFAULT_FAILSAFE_EXPIRY_SECS: u16 = 60;
90
91pub struct FailSafe {
92    state: State,
93    secret_key: CanonPkcSecretKey,
94    root_ca: Vec<u8, { MAX_CERT_TLV_LEN }>,
95    breadcrumb: u64,
96}
97
98impl FailSafe {
99    #[inline(always)]
100    pub const fn new() -> Self {
101        Self {
102            state: State::Idle,
103            secret_key: PKC_SECRET_KEY_ZEROED,
104            root_ca: Vec::new(),
105            breadcrumb: 0,
106        }
107    }
108
109    pub fn init() -> impl Init<Self> {
110        init!(Self {
111            state: State::Idle,
112            secret_key <- CanonPkcSecretKey::init(),
113            root_ca <- Vec::init(),
114            breadcrumb: 0
115        })
116    }
117
118    /// Check if the fail-safe timer has expired and if so disarms and restores the state of the fabric as well as
119    /// the basic info settings.
120    ///
121    /// This should be called periodically to ensure that the fail-safe state is updated in a timely manner.
122    /// Ideally, it should also be called at the beginning of any API that requires the fail-safe to be armed to ensure that the state is up to date.
123    ///
124    /// Returns the local index of the fabric that ended up removed by the
125    /// rollback (see [`Failsafe::expire`]), if any - the caller must follow
126    /// up with a `HandlerContext::notify_fabric_removed` broadcast once the
127    /// Matter state lock is released.
128    #[allow(clippy::too_many_arguments)]
129    pub fn check_failsafe_timeout<S, N>(
130        &mut self,
131        fabrics: &mut Fabrics,
132        sessions: &mut crate::transport::session::Sessions,
133        networks: N,
134        kv: S,
135        expire_sess_id: Option<u32>,
136        mdns_notif: impl FnMut(),
137        notify_change: impl FnMut(EndptId, ClusterId),
138    ) -> Result<Option<NonZeroU8>, Error>
139    where
140        S: KvBlobStoreAccess,
141        N: NetworksAccess,
142    {
143        if let State::Armed(ctx) = &self.state {
144            let now = Instant::now();
145            if now
146                >= ctx
147                    .armed_at
148                    .saturating_add(Duration::from_secs(ctx.timeout_secs as u64))
149            {
150                // Timeout path: no caller exchange to preserve, so wipe
151                // every PASE session along with the fabric / networks
152                // rollback.
153                return self.expire(
154                    fabrics,
155                    sessions,
156                    expire_sess_id,
157                    networks,
158                    kv,
159                    mdns_notif,
160                    notify_change,
161                );
162            }
163        }
164
165        Ok(None)
166    }
167
168    /// Force the fail-safe context to expire immediately, rolling back any
169    /// fabric / network changes that the in-flight commissioning had staged
170    /// and resetting the breadcrumb to 0.
171    ///
172    /// `expire_sess_id` is the optional session ID of the exchange that
173    /// triggered the expiry — typically passed when the trigger arrived
174    /// over PASE, so the response can still be sent before the slot is
175    /// reclaimed. `None` for the timeout-driven path or when the trigger
176    /// arrived over CASE.
177    ///
178    /// Returns the local index of the fabric the rollback ended up removing,
179    /// if any: a fabric added by the in-flight `AddNOC` has no persisted copy
180    /// yet and is simply dropped, whereas a pre-existing fabric mutated by
181    /// `UpdateNOC` is resurrected from its persisted copy (and is thus NOT
182    /// reported as removed). The caller must follow up with a
183    /// `HandlerContext::notify_fabric_removed` broadcast for a reported
184    /// removal, once the Matter state lock is released.
185    #[allow(clippy::too_many_arguments)]
186    pub fn expire<S, N>(
187        &mut self,
188        fabrics: &mut Fabrics,
189        sessions: &mut crate::transport::session::Sessions,
190        expire_sess_id: Option<u32>,
191        networks: N,
192        kv: S,
193        mut mdns_notif: impl FnMut(),
194        mut notify_change: impl FnMut(EndptId, ClusterId),
195    ) -> Result<Option<NonZeroU8>, Error>
196    where
197        S: KvBlobStoreAccess,
198        N: NetworksAccess,
199    {
200        let State::Armed(ctx) = &self.state else {
201            return Ok(None);
202        };
203
204        warn!(
205            "Fail-Safe timeout expired for fabric {}, disarming",
206            ctx.fab_idx
207        );
208
209        let fab_idx_raw = ctx.fab_idx;
210        let mut removed_fabric = None;
211
212        kv.access(|mut kv, buf| {
213            if let Some(fab_idx) = NonZeroU8::new(fab_idx_raw) {
214                fabrics.remove(fab_idx)?;
215                fabrics.add_load(fab_idx.get(), &mut kv, buf)?;
216
217                removed_fabric = fabrics.get(fab_idx).is_none().then_some(fab_idx);
218            }
219
220            networks.access(|networks| {
221                let data = kv.load(NETWORKS_KEY, buf)?;
222
223                if let Some(data) = data {
224                    networks.load(data)
225                } else {
226                    networks.reset()
227                }
228            })
229        })?;
230
231        // Any PASE session that was in flight under this fail-safe is
232        // now orphaned: its commissioning attempt was rolled back, so the
233        // session has nothing to do and should not stick around to fill
234        // the session table (same leak class fixed for
235        // `CommissioningComplete`). `Sessions::remove_pase` keeps
236        // `expire_sess_id` alive (marked expired) so any in-flight
237        // response can complete.
238        sessions.remove_pase(expire_sess_id);
239
240        self.state = State::Idle;
241        self.breadcrumb = 0;
242
243        mdns_notif();
244
245        // The rollback above restores attributes visible to subscribers —
246        // `OperationalCredentials::NOCs` / `Fabrics` (including `vvsc`,
247        // `VIDVerificationStatement`, `vendorID` mutated in-failsafe by
248        // `SetVIDVerificationStatement`) and `NetworkCommissioning::Networks`
249        // — to their persisted values. Notify so any active subscriptions
250        // re-report.
251        //
252        // TODO: this only flags subscriptions for re-reporting; it does
253        // *not* bump the affected clusters' data versions. `Failsafe`
254        // has no handle to the cluster meta needed to do that. Pre-existing
255        // limitation, not introduced by the timeout-vs-force-expiry path.
256        notify_change(
257            ROOT_ENDPOINT_ID,
258            crate::dm::clusters::decl::operational_credentials::FULL_CLUSTER.id,
259        );
260        notify_change(
261            ROOT_ENDPOINT_ID,
262            crate::dm::clusters::decl::network_commissioning::FULL_CLUSTER.id,
263        );
264
265        Ok(removed_fabric)
266    }
267
268    pub fn arm(
269        &mut self,
270        timeout_secs: u16,
271        breadcrumb: u64,
272        session_mode: &SessionMode,
273        pase: &mut Pase,
274    ) -> Result<(), Error> {
275        if matches!(self.state, State::Idle) {
276            if matches!(session_mode, SessionMode::PlainText) {
277                // Only PASE and CASE sessions supported
278                return Err(ErrorCode::GennCommInvalidAuthentication.into());
279            }
280
281            if pase.comm_window().is_some() && matches!(session_mode, SessionMode::Case { .. }) {
282                // Cannot arm via CASE while there's an active window
283                return Err(ErrorCode::Busy.into());
284            }
285
286            // if pase.comm_window().is_none() && !matches!(session_mode, SessionMode::Case { .. }) {
287            //     // Cannot arm via PASE if there is no active commissioning window
288            //     return Err(ErrorCode::GennCommInvalidAuthentication.into());
289            // }
290
291            self.state = State::Armed(ArmedCtx {
292                armed_at: Instant::now(),
293                timeout_secs,
294                fab_idx: session_mode.fab_idx(),
295                flags: NocFlags::empty(),
296            });
297            self.breadcrumb = breadcrumb;
298
299            return Ok(());
300        }
301
302        // Re-arm
303
304        self.check_state(
305            session_mode,
306            NocFlags::empty(),
307            NocFlags::empty(),
308            NocFlags::empty(),
309        )?;
310
311        let State::Armed(ctx) = &mut self.state else {
312            // Impossible, as we checked for Idle above
313            unreachable!();
314        };
315
316        if timeout_secs > 0 {
317            ctx.armed_at = Instant::now();
318            ctx.timeout_secs = timeout_secs;
319            self.breadcrumb = breadcrumb;
320        } else {
321            // As per the spec, when timeout seconds is 0, we have to actually disarm
322            self.state = State::Idle;
323            self.breadcrumb = 0;
324        }
325
326        Ok(())
327    }
328
329    pub fn disarm<'a>(
330        &mut self,
331        session_mode: &SessionMode,
332        fabrics: &'a mut Fabrics,
333    ) -> Result<&'a mut Fabric, Error> {
334        if matches!(self.state, State::Idle) {
335            error!("Received Fail-Safe Disarm without it being armed");
336            return Err(ErrorCode::FailSafeRequired.into());
337        }
338
339        // Has to be a CASE session
340        let fab_idx = Self::get_case_fab_idx(session_mode)?;
341
342        self.check_state(
343            session_mode,
344            NocFlags::empty(),
345            NocFlags::empty(),
346            NocFlags::empty(),
347        )?;
348
349        let fabric = fabrics.fabric_mut(fab_idx)?;
350
351        self.state = State::Idle;
352        self.breadcrumb = 0;
353
354        Ok(fabric)
355    }
356
357    pub fn is_armed(&self) -> bool {
358        matches!(self.state, State::Armed(_))
359    }
360
361    /// Return the trusted root certificate that has been staged via
362    /// `AddTrustedRootCertificate` while the fail-safe is armed but has not
363    /// yet been bound to a fabric via `AddNOC` / `UpdateNOC`.
364    ///
365    /// Once `AddNOC` or `UpdateNOC` is processed the root certificate is
366    /// owned by the (new or updated) fabric and is reported through the
367    /// fabric table; until then it has no fabric association but the spec
368    /// still requires it to appear in the `TrustedRootCertificates` list
369    /// (Matter Core spec, NodeOperationalCredentials cluster).
370    pub fn pending_root_ca(&self) -> Option<&[u8]> {
371        let State::Armed(ctx) = &self.state else {
372            return None;
373        };
374
375        if !ctx.flags.contains(NocFlags::ADD_ROOT_CERT_RECVD) {
376            return None;
377        }
378
379        if ctx
380            .flags
381            .intersects(NocFlags::ADD_NOC_RECVD | NocFlags::UPDATE_NOC_RECVD)
382        {
383            return None;
384        }
385
386        (!self.root_ca.is_empty()).then_some(self.root_ca.as_slice())
387    }
388
389    pub fn is_armed_for(&self, caller_fab_idx: u8) -> bool {
390        match self.state {
391            State::Idle => false,
392            State::Armed(ArmedCtx { fab_idx, .. }) => fab_idx == caller_fab_idx,
393        }
394    }
395
396    /// Whether the current fail-safe context already has an in-flight
397    /// `AddNOC` or `UpdateNOC` for `caller_fab_idx`. Used by
398    /// `SetVIDVerificationStatement` to decide whether the VID-verification
399    /// mutation rides along with the pending fabric (and thus rolls back
400    /// on fail-safe expiry) or is committed to storage immediately.
401    pub fn has_pending_noc_for(&self, caller_fab_idx: NonZeroU8) -> bool {
402        let State::Armed(ctx) = &self.state else {
403            return false;
404        };
405        ctx.fab_idx == caller_fab_idx.get()
406            && ctx
407                .flags
408                .intersects(NocFlags::ADD_NOC_RECVD | NocFlags::UPDATE_NOC_RECVD)
409    }
410
411    pub fn check_armed(&self, session_mode: &SessionMode) -> Result<(), Error> {
412        self.check_state(
413            session_mode,
414            NocFlags::empty(),
415            NocFlags::empty(),
416            NocFlags::empty(),
417        )
418    }
419
420    pub fn add_trusted_root_cert<C: Crypto>(
421        &mut self,
422        crypto: C,
423        time: UtcTime,
424        session_mode: &SessionMode,
425        root_ca: &[u8],
426        buf: &mut [u8],
427    ) -> Result<(), Error> {
428        self.check_state(
429            session_mode,
430            NocFlags::empty(),
431            NocFlags::ADD_ROOT_CERT_RECVD,
432            NocFlags::ADD_ROOT_CERT_RECVD,
433        )?;
434
435        // Validate the candidate RCAC by checking its self-signature (a Matter
436        // RCAC is self-issued, so the certificate's own public key must verify
437        // the certificate's signature). Any decode or signature failure must
438        // surface as `INVALID_COMMAND` per Matter Core spec
439        // (`AddTrustedRootCertificate`), not as the generic `Failure` we'd
440        // otherwise get from `ErrorCode::InvalidSignature`.
441        {
442            let root_ref = CertRef::new(TLVElement::new(root_ca));
443            root_ref
444                .verify_chain_start(&crypto, time)
445                .finalise(buf)
446                .map_err(|_| ErrorCode::InvalidCommand)?;
447
448            // Matter spec extra: an RCAC SHALL NOT carry a
449            // `pathLenConstraint` greater than `1` — the deepest valid
450            // Matter chain is RCAC → ICAC → NOC, i.e. at most one
451            // intermediate CA below the root. Mirrors CHIP's
452            // `ValidateChipRCAC`.
453            if let Some(path_len) = root_ref
454                .basic_constraints_path_len()
455                .map_err(|_| ErrorCode::InvalidCommand)?
456            {
457                if path_len > 1 {
458                    Err(ErrorCode::InvalidCommand)?;
459                }
460            }
461        }
462
463        self.root_ca.clear();
464        self.root_ca
465            .extend_from_slice(root_ca)
466            .map_err(|_| ErrorCode::InvalidCommand)?;
467
468        self.add_flags(NocFlags::ADD_ROOT_CERT_RECVD);
469
470        Ok(())
471    }
472
473    pub fn add_csr_req<C: Crypto>(
474        &mut self,
475        crypto: C,
476        session_mode: &SessionMode,
477    ) -> Result<CanonPkcSecretKeyRef<'_>, Error> {
478        self.check_state(
479            session_mode,
480            NocFlags::empty(),
481            NocFlags::ADD_CSR_REQ_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD,
482            NocFlags::ADD_CSR_REQ_RECVD,
483        )?;
484
485        let crypto_secret_key = crypto.generate_secret_key()?;
486        crypto_secret_key.write_canon(&mut self.secret_key)?;
487
488        self.add_flags(NocFlags::ADD_CSR_REQ_RECVD);
489
490        Ok(self.secret_key.reference())
491    }
492
493    pub fn update_csr_req<C: Crypto>(
494        &mut self,
495        crypto: C,
496        session_mode: &SessionMode,
497    ) -> Result<CanonPkcSecretKeyRef<'_>, Error> {
498        // Must be a CASE session
499        Self::get_case_fab_idx(session_mode)?;
500
501        self.check_state(
502            session_mode,
503            NocFlags::empty(),
504            NocFlags::ADD_CSR_REQ_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD,
505            NocFlags::UPDATE_CSR_REQ_RECVD,
506        )?;
507
508        crypto
509            .generate_secret_key()?
510            .write_canon(&mut self.secret_key)?;
511
512        self.add_flags(NocFlags::UPDATE_CSR_REQ_RECVD);
513
514        Ok(self.secret_key.reference())
515    }
516
517    #[allow(clippy::too_many_arguments)]
518    pub fn update_noc<'a, C: Crypto>(
519        &mut self,
520        crypto: C,
521        time: UtcTime,
522        fabrics: &'a mut Fabrics,
523        session_mode: &SessionMode,
524        icac: Option<&[u8]>,
525        noc: &[u8],
526        buf: &mut [u8],
527        mut mdns_notif: impl FnMut(),
528    ) -> Result<&'a mut Fabric, Error> {
529        let fab_idx = Self::get_case_fab_idx(session_mode)?;
530
531        // `UpdateNOC` only requires the corresponding `CSRRequest` (with
532        // `isForUpdateNOC=true`) to have been processed in this fail-safe
533        // context. Per Matter Core spec it must NOT
534        // have been preceded by `AddTrustedRootCertificate`, `AddNOC`,
535        // `UpdateNOC`, or a CSRRequest of the wrong kind — those go in
536        // `absent`. `validate_certs` further down uses the *committed*
537        // root cert (`fabrics.fabric(fab_idx).root_ca()`), not anything
538        // staged via AddTrustedRootCertificate.
539        self.check_state(
540            session_mode,
541            NocFlags::UPDATE_CSR_REQ_RECVD,
542            NocFlags::ADD_ROOT_CERT_RECVD
543                | NocFlags::ADD_NOC_RECVD
544                | NocFlags::ADD_CSR_REQ_RECVD
545                | NocFlags::UPDATE_NOC_RECVD,
546            NocFlags::UPDATE_NOC_RECVD,
547        )?;
548
549        {
550            let noc_ref = CertRef::new(TLVElement::new(noc));
551            let icac_ref = icac.map(|icac| CertRef::new(TLVElement::new(icac)));
552            // `UpdateNOC` re-uses the existing fabric's root cert; it does
553            // not consume one staged via `AddTrustedRootCertificate` (the
554            // `absent` constraint above ensures none was staged).
555            let fabric_root_ca = fabrics.fabric(fab_idx)?.root_ca();
556            let root_ref = CertRef::new(TLVElement::new(fabric_root_ca));
557
558            // Validate the certs first. A chain that doesn't pass
559            // signature verification (or that doesn't chain back to the
560            // staged root) is reported as `kInvalidNOC` cluster status per
561            // Matter Core spec (`UpdateNOC`).
562            Self::validate_certs(&crypto, time, &noc_ref, icac_ref.as_ref(), &root_ref, buf)
563                .map_err(|_| ErrorCode::NocInvalidNoc)?;
564
565            // The NOC's public key must match the public key derived from
566            // the most recent `CSRRequest(isForUpdateNOC=true)` (Matter
567            // Core spec).
568            let mut csr_pubkey = crate::crypto::CanonPkcPublicKey::new();
569            crypto
570                .secret_key(self.secret_key.reference())?
571                .pub_key()?
572                .write_canon(&mut csr_pubkey)?;
573            if csr_pubkey.access().as_slice() != noc_ref.pubkey()? {
574                Err(ErrorCode::NocInvalidPublicKey)?;
575            }
576
577            // Check that the fabric ID in the NOC matches the fabric
578            // being updated. The root cert pubkey check is implicit: the
579            // chain validation above used the fabric's own root cert.
580
581            let fabric_id = noc_ref.get_fabric_id()?;
582            let fabric = fabrics.fabric(fab_idx)?;
583
584            if fabric_id != fabric.fabric_id() {
585                Err(ErrorCode::NocFabricConflict)?;
586            }
587        }
588
589        // `Fabrics::update` keeps the existing root cert in place — no
590        // need (and no reason) to copy it out of the fabric just to pass
591        // it back in.
592        let fabric = fabrics.update(
593            &crypto,
594            fab_idx,
595            self.secret_key.reference(),
596            noc,
597            icac.unwrap_or(&[]),
598        )?;
599
600        let State::Armed(ctx) = &mut self.state else {
601            // Impossible to be in any other state because otherwise
602            // check_state would have failed
603            unreachable!();
604        };
605
606        ctx.fab_idx = fabric.fab_idx().get();
607        self.add_flags(NocFlags::UPDATE_NOC_RECVD);
608
609        mdns_notif();
610
611        Ok(fabric)
612    }
613
614    #[allow(clippy::too_many_arguments)]
615    pub fn add_noc<'a, C: Crypto>(
616        &mut self,
617        crypto: C,
618        time: UtcTime,
619        fabrics: &'a mut Fabrics,
620        session_mode: &SessionMode,
621        vendor_id: u16,
622        icac: Option<&[u8]>,
623        noc: &[u8],
624        ipk: &[u8],
625        case_admin_subject: u64,
626        buf: &mut [u8],
627        mut mdns_notif: impl FnMut(),
628    ) -> Result<&'a mut Fabric, Error> {
629        self.check_state(
630            session_mode,
631            NocFlags::ADD_ROOT_CERT_RECVD | NocFlags::ADD_CSR_REQ_RECVD,
632            NocFlags::ADD_NOC_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD | NocFlags::UPDATE_NOC_RECVD,
633            NocFlags::ADD_NOC_RECVD,
634        )?;
635
636        // CaseAdminSubject must be either a valid Operational Node ID or a
637        // CASE Authenticated Tag (CAT) — Matter Core spec
638        // (`AddNOC`). Anything else (most commonly 0) is reported as
639        // `kInvalidAdminSubject` cluster status.
640        if !crate::acl::is_node(case_admin_subject) && !crate::acl::is_noc_cat(case_admin_subject) {
641            Err(ErrorCode::NocInvalidAdminSubject)?;
642        }
643
644        {
645            let noc_ref = CertRef::new(TLVElement::new(noc));
646            let icac_ref = icac.map(|icac| CertRef::new(TLVElement::new(icac)));
647            let root_ref = CertRef::new(TLVElement::new(&self.root_ca));
648
649            // Validate the certs first. A chain that doesn't pass
650            // signature verification (or that doesn't chain back to the
651            // staged root) is reported as `kInvalidNOC` cluster status per
652            // Matter Core spec (`AddNOC`).
653            Self::validate_certs(&crypto, time, &noc_ref, icac_ref.as_ref(), &root_ref, buf)
654                .map_err(|_| ErrorCode::NocInvalidNoc)?;
655
656            // The NOC's public key must match the public key derived from
657            // the most recent `CSRRequest` (Matter Core spec). The CSR's
658            // secret key is stashed in
659            // `self.secret_key` by `add_csr_req` / `update_csr_req`.
660            let mut csr_pubkey = crate::crypto::CanonPkcPublicKey::new();
661            crypto
662                .secret_key(self.secret_key.reference())?
663                .pub_key()?
664                .write_canon(&mut csr_pubkey)?;
665            if csr_pubkey.access().as_slice() != noc_ref.pubkey()? {
666                Err(ErrorCode::NocInvalidPublicKey)?;
667            }
668
669            // Check that there is no fabric with the same fabric ID and root cert pubkey
670            // as the one in the NOC, to avoid adding duplicate fabrics
671
672            let fabric_id = noc_ref.get_fabric_id()?;
673            let root_cert_pubkey = root_ref.pubkey()?;
674
675            for fabric in fabrics.iter() {
676                if fabric_id == fabric.fabric_id() {
677                    let f_root_ref = CertRef::new(TLVElement::new(fabric.root_ca()));
678                    let f_root_pubkey = f_root_ref.pubkey()?;
679
680                    if root_cert_pubkey == f_root_pubkey {
681                        // A fabric with the same ID and root cert pubkey already exists,
682                        // which means that this NOC cannot be accepted
683                        Err(ErrorCode::NocFabricConflict)?;
684                    }
685                }
686            }
687        }
688
689        let fabric = fabrics
690            .add(
691                &crypto,
692                self.secret_key.reference(),
693                &self.root_ca,
694                noc,
695                icac.unwrap_or(&[]),
696                Some(CanonAeadKeyRef::try_new(ipk)?),
697                vendor_id,
698                case_admin_subject,
699            )
700            .map_err(|e| {
701                if e.code() == ErrorCode::ResourceExhausted {
702                    ErrorCode::NocFabricTableFull.into()
703                } else {
704                    e
705                }
706            })?;
707
708        info!(
709            "Added operational fabric with local index {}",
710            fabric.fab_idx()
711        );
712
713        let State::Armed(ctx) = &mut self.state else {
714            // Impossible to be in any other state because otherwise
715            // check_state would have failed
716            unreachable!();
717        };
718
719        ctx.fab_idx = fabric.fab_idx().get();
720        self.add_flags(NocFlags::ADD_NOC_RECVD);
721
722        mdns_notif();
723
724        Ok(fabric)
725    }
726
727    pub fn breadcrumb(&self) -> u64 {
728        self.breadcrumb
729    }
730
731    pub fn set_breadcrumb(&mut self, value: u64) {
732        self.breadcrumb = value;
733    }
734
735    #[allow(clippy::too_many_arguments)]
736    fn validate_certs<C: Crypto>(
737        crypto: C,
738        time: UtcTime,
739        noc: &CertRef,
740        icac: Option<&CertRef>,
741        root: &CertRef,
742        buf: &mut [u8],
743    ) -> Result<(), Error> {
744        let mut verifier = noc.verify_chain_start(crypto, time);
745
746        if let Some(icac) = icac {
747            // If ICAC is present handle it. Reject the case where the
748            // commissioner re-uses the RCAC as the ICAC:
749            // the spec requires the ICAC to be a separate CA cert
750            // (i.e. not self-signed).
751            if icac.is_self_signed()? {
752                return Err(ErrorCode::InvalidData.into());
753            }
754            verifier = verifier.add_cert(icac, buf)?;
755        }
756
757        verifier.add_cert(root, buf)?.finalise(buf)
758    }
759
760    fn get_case_fab_idx(session_mode: &SessionMode) -> Result<NonZeroU8, Error> {
761        if let SessionMode::Case { fab_idx, .. } = session_mode {
762            Ok(*fab_idx)
763        } else {
764            // Only CASE session supported
765            Err(ErrorCode::GennCommInvalidAuthentication.into())
766        }
767    }
768
769    fn check_state(
770        &self,
771        session_mode: &SessionMode,
772        present: NocFlags,
773        absent: NocFlags,
774        op: NocFlags,
775    ) -> Result<(), Error> {
776        if let State::Armed(ctx) = &self.state {
777            if matches!(session_mode, SessionMode::PlainText) {
778                // Session is plain text
779                Err(ErrorCode::GennCommInvalidAuthentication)?;
780            }
781
782            if op == NocFlags::UPDATE_NOC_RECVD && !matches!(session_mode, SessionMode::Case { .. })
783            {
784                // Update NOC requires a CASE session
785                Err(ErrorCode::GennCommInvalidAuthentication)?;
786            }
787
788            if ctx.fab_idx != session_mode.fab_idx() {
789                // Fabric index does not match
790                Err(ErrorCode::NocInvalidFabricIndex)?;
791            }
792
793            if !ctx.flags.contains(present) {
794                // State is not what is expected for that concrete command.
795                //
796                // Disambiguate "no CSR at all" from "wrong CSR type" per
797                // Matter Core spec, `AddNOC` / `UpdateNOC`:
798                //   * No `CSRRequest` of either kind seen yet for this
799                //     fail-safe context → `kMissingCsr` cluster status.
800                //   * A CSR was issued but with the opposite
801                //     `isForUpdateNOC` flag from the command being
802                //     processed (e.g. `UpdateNOC` after a CSR for
803                //     `AddNOC`) → IM `CONSTRAINT_ERROR`.
804                let any_csr = ctx
805                    .flags
806                    .intersects(NocFlags::ADD_CSR_REQ_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD);
807                if (op == NocFlags::ADD_NOC_RECVD || op == NocFlags::UPDATE_NOC_RECVD) && !any_csr {
808                    Err(ErrorCode::NocMissingCsr)?;
809                }
810
811                Err(ErrorCode::ConstraintError)?;
812            }
813
814            if !ctx.flags.intersection(absent).is_empty() {
815                // State is not what is expected for that concrete command.
816                //
817                // Two flavours both surface as IM `CONSTRAINT_ERROR` per
818                // Matter Core spec, `AddNOC` / `UpdateNOC`:
819                //   * the same `Add`/`UpdateNOC` was already received in
820                //     this fail-safe context
821                //   * the most recent `CSRRequest` had the wrong
822                //     `isForUpdateNOC` flag for the command being
823                //     processed (e.g. UpdateNOC after an AddNOC-style CSR)
824                Err(ErrorCode::ConstraintError)?;
825            }
826        } else {
827            // Fail-safe is not armed
828            Err(ErrorCode::FailSafeRequired)?;
829        }
830
831        Ok(())
832    }
833
834    fn add_flags(&mut self, flags: NocFlags) {
835        match &mut self.state {
836            State::Armed(ctx) => ctx.flags |= flags,
837            _ => panic!("Not armed"),
838        }
839    }
840}
841
842impl Default for FailSafe {
843    fn default() -> Self {
844        Self::new()
845    }
846}