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