Skip to main content

rs_matter/dm/clusters/
noc.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
18//! This module contains the implementation of the Node Operational Credentials cluster and its handler.
19
20use core::cell::Cell;
21use core::mem::MaybeUninit;
22use core::num::NonZeroU8;
23
24use crate::acl::AclEntry;
25use crate::cert::CertRef;
26use crate::crypto::{CanonPkcSignature, Crypto, SigningSecretKey, PKC_CANON_PUBLIC_KEY_LEN};
27use crate::dm::clusters::acl::{emit_acl_entry_changed, ChangeTypeEnum};
28use crate::dm::clusters::adm_comm;
29use crate::dm::clusters::dev_att::DeviceAttestation;
30use crate::dm::clusters::gen_comm::GenCommHandler;
31use crate::dm::endpoints::ROOT_ENDPOINT_ID;
32use crate::dm::{ArrayAttributeRead, Cluster, Dataver, InvokeContext, ReadContext};
33use crate::error::{Error, ErrorCode};
34use crate::fabric::{Fabric, FabricPersist, MAX_FABRICS};
35use crate::tlv::{
36    Nullable, Octets, OctetsArrayBuilder, OctetsBuilder, TLVBuilder, TLVBuilderParent, TLVElement,
37    TLVTag, TLVWrite,
38};
39use crate::transport::session::{AttChallengeRef, SessionMode, ATT_CHALLENGE_LEN};
40use crate::utils::init::InitMaybeUninit;
41use crate::utils::storage::WriteBuf;
42
43pub use crate::dm::clusters::decl::operational_credentials::*;
44
45impl NodeOperationalCertStatusEnum {
46    fn map(result: Result<(), Error>) -> Result<Self, Error> {
47        match result {
48            Ok(()) => Ok(Self::OK),
49            Err(err) => match err.code() {
50                ErrorCode::NocFabricTableFull => Ok(Self::TableFull),
51                ErrorCode::NocInvalidFabricIndex => Ok(Self::InvalidFabricIndex),
52                ErrorCode::NocFabricConflict => Ok(Self::FabricConflict),
53                ErrorCode::NocLabelConflict => Ok(Self::LabelConflict),
54                ErrorCode::NocInvalidNoc => Ok(Self::InvalidNOC),
55                ErrorCode::NocInvalidPublicKey => Ok(Self::InvalidPublicKey),
56                ErrorCode::NocInvalidAdminSubject => Ok(Self::InvalidAdminSubject),
57                ErrorCode::NocMissingCsr => Ok(Self::MissingCsr),
58                // Bare `ConstraintError` from `FailSafe::check_state`
59                // (e.g. "AddNOC received twice in the same fail-safe
60                // context", spec) is reported as an
61                // IM-level status code per the spec, not as a
62                // `NodeOperationalCertStatusEnum` cluster status — let it
63                // propagate.
64                _ => Err(err),
65            },
66        }
67    }
68}
69
70/// The system implementation of a handler for the Node Operational Credentials Matter cluster.
71#[derive(Debug, Clone)]
72#[cfg_attr(feature = "defmt", derive(defmt::Format))]
73pub struct NocHandler {
74    dataver: Dataver,
75}
76
77impl NocHandler {
78    /// Creates a new instance of the `NocHandler` with the given `Dataver`.
79    pub const fn new(dataver: Dataver) -> Self {
80        Self { dataver }
81    }
82
83    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
84    pub const fn adapt(self) -> HandlerAdaptor<Self> {
85        HandlerAdaptor(self)
86    }
87
88    /// Computes the attestation signature using the provided `DeviceAttestation`
89    fn compute_attestation_signature<C: Crypto>(
90        crypto: C,
91        dev_att: &dyn DeviceAttestation,
92        attest_element: &mut WriteBuf,
93        attest_challenge: AttChallengeRef<'_>,
94        signature: &mut CanonPkcSignature,
95    ) -> Result<(), Error> {
96        let dac_key = crypto.secret_key(dev_att.dac_priv_key())?;
97
98        attest_element.copy_from_slice(attest_challenge.access())?;
99        dac_key.sign(attest_element.as_slice(), signature)?;
100
101        Ok(())
102    }
103}
104
105impl ClusterHandler for NocHandler {
106    const CLUSTER: Cluster<'static> = FULL_CLUSTER;
107
108    fn dataver(&self) -> u32 {
109        self.dataver.get()
110    }
111
112    fn dataver_changed(&self) {
113        self.dataver.changed();
114    }
115
116    fn nocs<P: TLVBuilderParent>(
117        &self,
118        ctx: impl ReadContext,
119        builder: ArrayAttributeRead<NOCStructArrayBuilder<P>, NOCStructBuilder<P>>,
120    ) -> Result<P, Error> {
121        fn read_into<P: TLVBuilderParent>(
122            fabric: &Fabric,
123            builder: NOCStructBuilder<P>,
124        ) -> Result<P, Error> {
125            builder
126                .noc(Octets::new(fabric.noc()))?
127                .icac(Nullable::new(
128                    (!fabric.icac().is_empty()).then(|| Octets::new(fabric.icac())),
129                ))?
130                .vvsc((!fabric.vvsc().is_empty()).then(|| Octets::new(fabric.vvsc())))?
131                .fabric_index(Some(fabric.fab_idx().get()))?
132                .end()
133        }
134
135        let attr = ctx.attr();
136
137        ctx.exchange().with_state(|state| {
138            let mut fabrics = state.fabrics.iter().filter(|fabric| {
139                (!attr.fab_filter || attr.fab_idx == fabric.fab_idx().get())
140                    && !fabric.root_ca().is_empty()
141            });
142
143            // Outer `fabrics` iterator already drops entries the
144            // accessor isn't allowed to see when `fab_filter` is true;
145            // the inner-loop checks that used to gate every entry on
146            // `attr.fab_idx == fabric.fab_idx().get()` were therefore
147            // hiding non-accessing fabrics from a deliberately
148            // non-fabric-filtered read. Per Matter Core spec
149            // (NOCStruct, post-1.4.2) `noc` / `icac` are no longer
150            // fabric-sensitive, so a non-fabric-filtered read MUST return
151            // every fabric's NOC.
152            match builder {
153                ArrayAttributeRead::ReadAll(mut builder) => {
154                    for fabric in fabrics {
155                        builder = read_into(fabric, builder.push()?)?;
156                    }
157
158                    builder.end()
159                }
160                ArrayAttributeRead::ReadOne(index, builder) => {
161                    if let Some(fabric) = fabrics.nth(index as _) {
162                        read_into(fabric, builder)
163                    } else {
164                        Err(ErrorCode::ConstraintError.into())
165                    }
166                }
167                ArrayAttributeRead::ReadNone(builder) => builder.end(),
168            }
169        })
170    }
171
172    fn fabrics<P: TLVBuilderParent>(
173        &self,
174        ctx: impl ReadContext,
175        builder: ArrayAttributeRead<
176            FabricDescriptorStructArrayBuilder<P>,
177            FabricDescriptorStructBuilder<P>,
178        >,
179    ) -> Result<P, Error> {
180        fn read_into<P: TLVBuilderParent>(
181            fabric: &Fabric,
182            builder: FabricDescriptorStructBuilder<P>,
183        ) -> Result<P, Error> {
184            // Empty `root_ca` might happen in the E2E tests
185            let root_ca_cert = CertRef::new(TLVElement::new(fabric.root_ca()));
186
187            builder
188                .root_public_key(Octets::new(root_ca_cert.pubkey()?))?
189                .vendor_id(fabric.vendor_id())?
190                .fabric_id(fabric.fabric_id())?
191                .node_id(fabric.node_id())?
192                .label(fabric.label())?
193                .vid_verification_statement(
194                    (!fabric.vid_verification_statement().is_empty())
195                        .then(|| Octets::new(fabric.vid_verification_statement())),
196                )?
197                .fabric_index(Some(fabric.fab_idx().get()))?
198                .end()
199        }
200
201        let attr = ctx.attr();
202
203        ctx.exchange().with_state(|state| {
204            let mut fabrics = state.fabrics.iter().filter(|fabric| {
205                (!attr.fab_filter || attr.fab_idx == fabric.fab_idx().get())
206                    && !fabric.root_ca().is_empty()
207            });
208
209            match builder {
210                ArrayAttributeRead::ReadAll(mut builder) => {
211                    for fabric in fabrics {
212                        builder = read_into(fabric, builder.push()?)?;
213                    }
214
215                    builder.end()
216                }
217                ArrayAttributeRead::ReadOne(index, builder) => {
218                    let fabric = fabrics.nth(index as _);
219
220                    if let Some(fabric) = fabric {
221                        read_into(fabric, builder)
222                    } else {
223                        Err(ErrorCode::ConstraintError.into())
224                    }
225                }
226                ArrayAttributeRead::ReadNone(builder) => builder.end(),
227            }
228        })
229    }
230
231    fn supported_fabrics(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
232        Ok(MAX_FABRICS as u8)
233    }
234
235    fn commissioned_fabrics(&self, ctx: impl ReadContext) -> Result<u8, Error> {
236        ctx.exchange()
237            .with_state(|state| Ok(state.fabrics.iter().count() as u8))
238    }
239
240    fn trusted_root_certificates<P: TLVBuilderParent>(
241        &self,
242        ctx: impl ReadContext,
243        builder: ArrayAttributeRead<OctetsArrayBuilder<P>, OctetsBuilder<P>>,
244    ) -> Result<P, Error> {
245        ctx.exchange().with_state(|state| {
246            // `TrustedRootCertificates` is a plain `list[octet_string]`, not a
247            // fabric-scoped struct list, so fabric filtering does not apply
248            // here — every committed fabric's root cert is reported.
249            let fabric_certs = state
250                .fabrics
251                .iter()
252                .filter(|fabric| !fabric.root_ca().is_empty())
253                .map(|fabric| fabric.root_ca());
254
255            // While the fail-safe is armed, an `AddTrustedRootCertificate`
256            // command stages a root certificate that is not yet bound to a
257            // fabric. Per the Matter Core spec it must still appear in the
258            // `TrustedRootCertificates` list until the fail-safe expires or
259            // the cert is consumed by `AddNOC` / `UpdateNOC` (at which point
260            // the fabric table reports it).
261            let mut certs = fabric_certs.chain(state.failsafe.pending_root_ca());
262
263            match builder {
264                ArrayAttributeRead::ReadAll(mut builder) => {
265                    for cert in certs {
266                        builder = builder.push(Octets::new(cert))?;
267                    }
268
269                    builder.end()
270                }
271                ArrayAttributeRead::ReadOne(index, builder) => {
272                    if let Some(cert) = certs.nth(index as _) {
273                        builder.set(Octets::new(cert))
274                    } else {
275                        Err(ErrorCode::ConstraintError.into())
276                    }
277                }
278                ArrayAttributeRead::ReadNone(builder) => builder.end(),
279            }
280        })
281    }
282
283    fn current_fabric_index(&self, ctx: impl ReadContext) -> Result<u8, Error> {
284        let attr = ctx.attr();
285        Ok(attr.fab_idx)
286    }
287
288    fn handle_attestation_request<P: TLVBuilderParent>(
289        &self,
290        ctx: impl InvokeContext,
291        request: AttestationRequestRequest<'_>,
292        response: AttestationResponseBuilder<P>,
293    ) -> Result<P, Error> {
294        info!("Got Attestation Request");
295
296        // Per Matter Core spec, the `AttestationNonce` field MUST
297        // be exactly 32 octets. Anything else is rejected with
298        // `INVALID_COMMAND`. TC_DA_1_2 steps 13/14 cover the >32 / <32 cases.
299        const ATTESTATION_NONCE_LEN: usize = 32;
300
301        if request.attestation_nonce()?.0.len() != ATTESTATION_NONCE_LEN {
302            return Err(ErrorCode::InvalidCommand.into());
303        }
304
305        ctx.exchange().with_state(|state| {
306            let sess = ctx.exchange().id().session(&mut state.sessions);
307
308            // Switch to raw writer for the response
309            // Necessary, as we want to take advantage of the `TLVWrite::str_cb` method
310            // to in-place compute and write the attestation response and the signature as an octet string
311            let mut parent = response.unchecked_into_parent();
312            let writer = parent.writer();
313
314            // Struct is already started
315            // writer.start_struct(&CmdDataWriter::TAG)?;
316
317            // Attestation timestamp (`AttestationElements.timestamp`,
318            // Matter Core spec) — Matter-epoch seconds.
319            // Sourced from the device's Last-Known-Good UTC Time;
320            // on a freshly-flashed device with no
321            // `SetUTCTime` yet, this is the firmware build timestamp.
322            // Read directly from the already-held `state` — re-entering
323            // `Matter::with_state` here would deadlock the inner mutex.
324            let epoch = state.rtc.utc_time().any_secs() as u32;
325
326            let mut signature = MaybeUninit::uninit();
327            let signature = signature.init_with(CanonPkcSignature::init()); // TODO MEDIUM BUFFER
328
329            writer.str_cb(&TLVTag::Context(0), |buf| {
330                let dev_att = ctx.exchange().matter().dev_att();
331
332                let mut wb = WriteBuf::new(buf);
333                wb.start_struct(&TLVTag::Anonymous)?;
334                wb.str(&TLVTag::Context(1), dev_att.cert_declaration())?;
335                wb.str(&TLVTag::Context(2), request.attestation_nonce()?.0)?;
336                wb.u32(&TLVTag::Context(3), epoch)?;
337                wb.end_container()?;
338
339                let len = wb.get_tail();
340
341                Self::compute_attestation_signature(
342                    ctx.crypto(),
343                    dev_att,
344                    &mut wb,
345                    sess.get_att_challenge().ok_or(ErrorCode::InvalidState)?,
346                    signature,
347                )?;
348
349                Ok(len)
350            })?;
351
352            writer.str(&TLVTag::Context(1), signature.access())?;
353
354            writer.end_container()?;
355
356            Ok(parent)
357        })
358    }
359
360    fn handle_certificate_chain_request<P: TLVBuilderParent>(
361        &self,
362        ctx: impl InvokeContext,
363        request: CertificateChainRequestRequest<'_>,
364        response: CertificateChainResponseBuilder<P>,
365    ) -> Result<P, Error> {
366        info!("Got Cert Chain Request");
367
368        // Switch to raw writer for the response
369        // Necessary, as we want to take advantage of the `TLVWrite::str_cb` method
370        // to emplace the attestation certificate as an octet string
371        let mut parent = response.unchecked_into_parent();
372        let writer = parent.writer();
373
374        // Struct is already started
375        // writer.start_struct(&CmdDataWriter::TAG)?;
376
377        let dev_att = ctx.exchange().matter().dev_att();
378
379        writer.str(
380            &TLVTag::Context(0),
381            match request.certificate_type()? {
382                CertificateChainTypeEnum::DACCertificate => dev_att.dac(),
383                CertificateChainTypeEnum::PAICertificate => dev_att.pai(),
384            },
385        )?;
386
387        writer.end_container()?;
388
389        Ok(parent)
390    }
391
392    fn handle_csr_request<P: TLVBuilderParent>(
393        &self,
394        ctx: impl InvokeContext,
395        request: CSRRequestRequest<'_>,
396        response: CSRResponseBuilder<P>,
397    ) -> Result<P, Error> {
398        info!("Got CSR Request");
399
400        // Per Matter Core spec, the `CSRNonce` field MUST be
401        // exactly 32 octets — anything else is rejected with
402        // `INVALID_COMMAND`. TC_DA_1_5 steps 11/12 cover the <32 / >32 cases.
403        const CSR_NONCE_LEN: usize = 32;
404
405        if request.csr_nonce()?.0.len() != CSR_NONCE_LEN {
406            return Err(ErrorCode::InvalidCommand.into());
407        }
408
409        let is_for_update_noc = request.is_for_update_noc()?.unwrap_or(false);
410
411        GenCommHandler::with_armed_failsafe(&ctx, |state, _| {
412            let sess = ctx.exchange().id().session(&mut state.sessions);
413
414            // Per Matter Core spec (`CSRRequest`),
415            // `isForUpdateNOC=true` is only valid over CASE — UpdateNOC
416            // can never run over PASE. A CSRRequest of that flavour over
417            // PASE is rejected with IM `INVALID_COMMAND` rather than
418            // bubbling up the generic auth failure as `Failure`.
419            if is_for_update_noc && !matches!(sess.get_session_mode(), SessionMode::Case { .. }) {
420                return Err(ErrorCode::InvalidCommand.into());
421            }
422
423            let secret_key = if is_for_update_noc {
424                state
425                    .failsafe
426                    .update_csr_req(ctx.crypto(), sess.get_session_mode())?
427            } else {
428                state
429                    .failsafe
430                    .add_csr_req(ctx.crypto(), sess.get_session_mode())?
431            };
432
433            // Switch to raw writer for the response
434            // Necessary, as we want to take advantage of the `TLVWrite::str_cb` method
435            // to in-place compute and write the CSR response and the signature as an octet string
436            let mut parent = response.unchecked_into_parent();
437            let writer = parent.writer();
438
439            // Struct is already started
440            // writer.start_struct(&CmdDataWriter::TAG)?;
441
442            let mut signature = MaybeUninit::uninit();
443            let signature = signature.init_with(CanonPkcSignature::init()); // TODO MEDIUM BUFFER
444
445            writer.str_cb(&TLVTag::Context(0), |buf| {
446                let mut wb = WriteBuf::new(buf);
447
448                wb.start_struct(&TLVTag::Anonymous)?;
449                wb.str_cb(&TLVTag::Context(1), |buf| {
450                    ctx.crypto()
451                        .secret_key(secret_key)?
452                        .csr(buf)
453                        .map(|slice| slice.len())
454                })?;
455                wb.str(&TLVTag::Context(2), request.csr_nonce()?.0)?;
456                wb.end_container()?;
457
458                let len = wb.get_tail();
459
460                Self::compute_attestation_signature(
461                    ctx.crypto(),
462                    ctx.exchange().matter().dev_att(),
463                    &mut wb,
464                    sess.get_att_challenge().ok_or(ErrorCode::InvalidState)?,
465                    signature,
466                )?;
467
468                Ok(len)
469            })?;
470
471            writer.str(&TLVTag::Context(1), signature.access())?;
472
473            writer.end_container()?;
474
475            Ok(parent)
476        })
477    }
478
479    fn handle_add_noc<P: TLVBuilderParent>(
480        &self,
481        ctx: impl InvokeContext,
482        request: AddNOCRequest<'_>,
483        mut response: NOCResponseBuilder<P>,
484    ) -> Result<P, Error> {
485        info!("Got Add NOC Request");
486
487        let icac = request
488            .icac_value()?
489            .as_ref()
490            .map(|icac| icac.0)
491            .filter(|icac| !icac.is_empty());
492
493        let mut added_fab_idx = None;
494        // Captured inside the closure so we can emit `AccessControlEntryChanged`
495        // for the auto-created admin ACL entry *after* the failsafe-armed
496        // closure unwinds successfully (Matter Core spec).
497        let mut admin_acl_entry: Option<AclEntry> = None;
498        // Set by the rollback scopeguard; the `LifecycleOp::FabricRemoval`
499        // broadcast happens below, once the state lock is released.
500        let rolled_back_fab_idx = Cell::new(None);
501
502        let buf = response.writer().available_space();
503
504        let status = NodeOperationalCertStatusEnum::map(GenCommHandler::with_armed_failsafe(
505            &ctx,
506            |state, mut notify_mdns| {
507                let sess = ctx.exchange().id().session(&mut state.sessions);
508
509                let fabric = state.failsafe.add_noc(
510                    ctx.crypto(),
511                    state.rtc.utc_time(),
512                    &mut state.fabrics,
513                    sess.get_session_mode(),
514                    request.admin_vendor_id()?,
515                    icac,
516                    request.noc_value()?.0,
517                    request.ipk_value()?.0,
518                    request.case_admin_subject()?,
519                    buf,
520                    &mut notify_mdns,
521                )?;
522
523                let fab_idx = fabric.fab_idx();
524                // Snapshot the freshly seeded admin ACL entry while we still
525                // hold the state lock; we'll emit the event once we're sure
526                // the fabric stays committed (i.e. no rollback happened).
527                let captured_admin_entry = fabric.acl_iter().next().cloned();
528                let succeeded = Cell::new(false);
529
530                let _fab_guard = scopeguard::guard(fab_idx, |fab_idx| {
531                    if !succeeded.get() {
532                        // Remove the fabric if we fail further down this function
533                        warn!("Removing fabric {} due to failure", fab_idx.get());
534
535                        unwrap!(state.fabrics.remove(fab_idx));
536
537                        notify_mdns();
538
539                        rolled_back_fab_idx.set(Some(fab_idx));
540                    }
541                });
542
543                if matches!(sess.get_session_mode(), SessionMode::Pase { .. }) {
544                    sess.upgrade_fabric_idx(fab_idx)?;
545                }
546
547                succeeded.set(true);
548                added_fab_idx = Some(fab_idx.get());
549                admin_acl_entry = captured_admin_entry;
550
551                Ok(())
552            },
553        ));
554
555        // Broadcast `LifecycleOp::FabricRemoval` for a fabric the rollback
556        // scopeguard just removed - on both the mapped-status and the
557        // propagated-error paths. Outside `with_state`, since the broadcast
558        // runs the handlers inline.
559        if let Some(fab_idx) = rolled_back_fab_idx.get() {
560            ctx.notify_fabric_removed(fab_idx);
561        }
562
563        let status = status?;
564
565        // AddNOC mutates NOCs, Fabrics, CommissionedFabrics, TrustedRootCerts, etc.
566        ctx.notify_own_cluster_changed();
567
568        // Emit the `AccessControlEntryChanged` event for the auto-created admin
569        // entry. AddNOC happens over PASE during commissioning, so per Matter
570        // Core spec the event has `adminNodeID = null` and
571        // `adminPasscodeID = 0`.
572        if let (Some(fab_idx), Some(entry)) = (added_fab_idx, &admin_acl_entry) {
573            emit_acl_entry_changed(
574                &ctx,
575                crate::tlv::Nullable::none(),
576                crate::tlv::Nullable::some(0u16),
577                ChangeTypeEnum::Added,
578                entry,
579                fab_idx,
580            )?;
581        }
582
583        response
584            .status_code(status)?
585            .fabric_index(added_fab_idx)?
586            .debug_text(None)?
587            .end()
588    }
589
590    fn handle_update_noc<P: TLVBuilderParent>(
591        &self,
592        ctx: impl InvokeContext,
593        request: UpdateNOCRequest<'_>,
594        mut response: NOCResponseBuilder<P>,
595    ) -> Result<P, Error> {
596        info!("Got Update NOC Request");
597
598        let icac = request
599            .icac_value()?
600            .as_ref()
601            .map(|icac| icac.0)
602            .filter(|icac| !icac.is_empty());
603
604        let buf = response.writer().available_space();
605
606        let status = NodeOperationalCertStatusEnum::map(GenCommHandler::with_armed_failsafe(
607            &ctx,
608            |state, notify_mdns| {
609                let sess = ctx.exchange().id().session(&mut state.sessions);
610
611                state.failsafe.update_noc(
612                    ctx.crypto(),
613                    state.rtc.utc_time(),
614                    &mut state.fabrics,
615                    sess.get_session_mode(),
616                    icac,
617                    request.noc_value()?.0,
618                    buf,
619                    notify_mdns,
620                )?;
621
622                Ok(())
623            },
624        ))?;
625
626        // UpdateNOC mutates the NOCs / Fabrics lists for the calling fabric
627        ctx.notify_own_cluster_changed();
628
629        response
630            .status_code(status)?
631            .fabric_index(Some(ctx.cmd().fab_idx))?
632            .debug_text(None)?
633            .end()
634    }
635
636    fn handle_update_fabric_label<P: TLVBuilderParent>(
637        &self,
638        ctx: impl InvokeContext,
639        request: UpdateFabricLabelRequest<'_>,
640        response: NOCResponseBuilder<P>,
641    ) -> Result<P, Error> {
642        info!("Got Update Fabric Label Request: {:?}", request.label());
643
644        let mut updated_fab_idx = None;
645
646        let status = NodeOperationalCertStatusEnum::map(ctx.exchange().with_state(|state| {
647            let sess = ctx.exchange().id().session(&mut state.sessions);
648
649            // `UpdateFabricLabel` is fabric-scoped and the IM access-control
650            // check (see `cluster::check_cmd_access`) already rejects calls
651            // from a session with no associated fabric (PASE pre-AddNOC) with
652            // `UnsupportedAccess`. Anything that gets here therefore has an
653            // accessing fabric — and per the spec / CHIP reference impl,
654            // that includes a PASE session whose fab_idx was upgraded by
655            // `AddNOC` (`session.upgrade_fabric_idx`). So allow Case *and*
656            // Pase as long as fab_idx > 0; the `accessing_fab_idx()` helper
657            // returns 0 only for plain-text / un-upgraded PASE.
658            let fab_idx = NonZeroU8::new(sess.get_local_fabric_idx())
659                .ok_or(ErrorCode::GennCommInvalidAuthentication)?;
660
661            let fabric = state
662                .fabrics
663                .update_label(fab_idx, request.label()?)
664                .map_err(|e| {
665                    if e.code() == ErrorCode::Invalid {
666                        ErrorCode::NocLabelConflict.into()
667                    } else {
668                        e
669                    }
670                })?;
671
672            updated_fab_idx = Some(fabric.fab_idx().get());
673
674            Ok(())
675        }))?;
676
677        // UpdateFabricLabel mutates the Fabrics list
678        ctx.notify_own_cluster_changed();
679
680        response
681            .status_code(status)?
682            .fabric_index(updated_fab_idx)?
683            .debug_text(None)?
684            .end()
685    }
686
687    fn handle_remove_fabric<P: TLVBuilderParent>(
688        &self,
689        ctx: impl InvokeContext,
690        request: RemoveFabricRequest<'_>,
691        response: NOCResponseBuilder<P>,
692    ) -> Result<P, Error> {
693        info!("Got Remove Fabric Request");
694
695        let fab_idx = NonZeroU8::new(request.fabric_index()?).ok_or(ErrorCode::ConstraintError)?;
696
697        let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
698
699        let mut persist = FabricPersist::new(ctx.kv());
700
701        let (status, opener_fabric_removed) = ctx.exchange().with_state(|state| {
702            let sess = ctx.exchange().id().session(&mut state.sessions);
703
704            if state.fabrics.remove(fab_idx).is_ok() {
705                // If our own session is running on the fabric being removed,
706                // we need to expire it rather than immediately remove it, so that
707                // the response can be sent back properly
708                let expire_sess_id =
709                    (sess.get_local_fabric_idx() == fab_idx.get()).then_some(sess.id());
710
711                // Remove all sessions related to the fabric being removed
712                // If `expire_sess_id` is Some, the session will be expired instead of removed.
713                state.sessions.remove_for_fabric(fab_idx, expire_sess_id);
714
715                // Drop any CASE session resumption records that were
716                // scoped to this fabric so a subsequent CASE handshake
717                // to any peer that used to belong to it starts fresh.
718                #[cfg(feature = "case-resumption")]
719                state.resumption.remove_for_fabric(fab_idx);
720
721                // Notify that a session was removed
722                ctx.exchange().matter().transport().notify_session_removed();
723
724                // The resumption cache was just mutated — wake the
725                // background persist task so the on-disk copy sheds
726                // the removed fabric's records too.
727                #[cfg(feature = "case-resumption")]
728                ctx.exchange()
729                    .matter()
730                    .transport()
731                    .notify_resumption_dirty();
732
733                // Notify that our mDNS records might have changed
734                notify_mdns();
735
736                // Note that since we might have removed our own session, the exchange
737                // will terminate with a "NoSession" error, but that's OK and handled properly
738
739                persist.remove(fab_idx)?;
740
741                // Matter Core spec: if the removed fabric is the
742                // one that installed the TrustedTimeSource, the device SHALL
743                // clear the attribute (and emit `MissingTrustedTimeSource`).
744                if state.rtc.trusted_time_source().map(|tts| tts.fab_idx) == Some(fab_idx) {
745                    state.rtc.set_trusted_time_source_persist(
746                        None,
747                        persist.persist_mut(),
748                        &ctx,
749                        &ctx,
750                    )?;
751                }
752
753                info!("Removed operational fabric with local index {}", fab_idx);
754
755                // If the removed fabric was the one that opened the current
756                // commissioning window, `AdminFabricIndex` (and `AdminVendorId`)
757                // transition to null and subscribers must be notified — see
758                // Matter Core spec.
759                let opener_fabric_removed = state
760                    .pase
761                    .comm_window()
762                    .and_then(|w| w.opener())
763                    .map(|opener| opener.fab_idx == fab_idx)
764                    .unwrap_or(false);
765
766                Ok::<_, Error>((NodeOperationalCertStatusEnum::OK, opener_fabric_removed))
767            } else {
768                Ok((NodeOperationalCertStatusEnum::InvalidFabricIndex, false))
769            }
770        })?;
771
772        persist.run()?;
773
774        if matches!(status, NodeOperationalCertStatusEnum::OK) {
775            // Matter Core spec: the node emits `BasicInformation::Leave` for a
776            // fabric that is about to be removed. Emitted through the generic
777            // cross-cluster path (this is the NOC handler, the event belongs
778            // to BasicInformation on EP0) - `TC_BINFO_2_2` step 5 reads it and
779            // checks its `fabricIndex` matches the fabric just removed.
780            //
781            // Best-effort: a missing diagnostic event must not fail the
782            // (already-committed) fabric removal.
783            let emitted = crate::dm::clusters::decl::basic_information::Leave::emit_for(
784                &ctx,
785                ROOT_ENDPOINT_ID,
786                |event| event.fabric_index(fab_idx.get())?.end(),
787            );
788
789            if let Err(e) = emitted {
790                warn!("Failed to emit the Leave event: {:?}", e);
791            }
792
793            // Broadcast `LifecycleOp::FabricRemoval`, so handlers owning
794            // fabric-scoped state outside the fabric table (bindings, scenes,
795            // ...) drop the removed fabric's entries. Outside `with_state`,
796            // since the broadcast runs the handlers inline.
797            ctx.notify_fabric_removed(fab_idx);
798        }
799
800        // RemoveFabric mutates NOCs, Fabrics, CommissionedFabrics, TrustedRootCerts
801        ctx.notify_own_cluster_changed();
802
803        if opener_fabric_removed {
804            ctx.notify_cluster_changed(ROOT_ENDPOINT_ID, adm_comm::FULL_CLUSTER.id);
805        }
806
807        response
808            .status_code(status)?
809            .fabric_index(Some(fab_idx.get()))?
810            .debug_text(None)?
811            .end()
812    }
813
814    fn handle_add_trusted_root_certificate(
815        &self,
816        ctx: impl InvokeContext,
817        request: AddTrustedRootCertificateRequest<'_>,
818    ) -> Result<(), Error> {
819        info!("Got Add Trusted Root Cert Request");
820
821        // Self-signature validation in `add_trusted_root_cert` re-encodes the
822        // RCAC into ASN.1 to feed it to ECDSA verify; sized to
823        // `MAX_CERT_ASN1_LEN` (the same bound `validate_certs` uses for the
824        // NOC chain).
825        // TODO XXX FIXME: LARGE BUFFER.
826        // We can avoid it if we had access to
827        // the output buffer (TX), but this is not possible yet for handler methods
828        // that are not expected to return command responses other than status result.
829        let mut buf = [0u8; crate::cert::MAX_CERT_ASN1_LEN];
830
831        GenCommHandler::with_armed_failsafe(&ctx, |state, _| {
832            let sess = ctx.exchange().id().session(&mut state.sessions);
833
834            state.failsafe.add_trusted_root_cert(
835                ctx.crypto(),
836                state.rtc.utc_time(),
837                sess.get_session_mode(),
838                request.root_ca_certificate()?.0,
839                &mut buf,
840            )
841        })
842    }
843
844    fn handle_set_vid_verification_statement(
845        &self,
846        ctx: impl InvokeContext,
847        request: SetVIDVerificationStatementRequest<'_>,
848    ) -> Result<(), Error> {
849        info!("Got Set VID Verification Statement Request");
850
851        let vendor_id = request.vendor_id()?;
852        let vvs = request.vid_verification_statement()?;
853        let vvsc = request.vvsc()?;
854
855        // Spec (`SetVIDVerificationStatement`): at
856        // least one field must be present, otherwise the command SHALL
857        // be rejected with `INVALID_COMMAND`.
858        if vendor_id.is_none() && vvs.is_none() && vvsc.is_none() {
859            return Err(ErrorCode::InvalidCommand.into());
860        }
861
862        // Per Matter Core spec, valid VendorIDs are
863        // 0x0001..=0xFFF4. 0xFFF5..=0xFFFF are reserved or test/CSA
864        // values not allowed for SetVIDVerificationStatement.
865        if let Some(vid) = vendor_id {
866            if vid == 0 || vid > 0xFFF4 {
867                return Err(ErrorCode::ConstraintError.into());
868            }
869        }
870
871        // VID Verification Statement, when present, must be either
872        // empty (clearing) or exactly `VID_VERIFICATION_STATEMENT_LEN`
873        // bytes (cluster XML: `length="85" minLength="85"`).
874        if let Some(s) = &vvs {
875            if !s.0.is_empty() && s.0.len() != crate::fabric::VID_VERIFICATION_STATEMENT_LEN {
876                return Err(ErrorCode::ConstraintError.into());
877            }
878        }
879
880        // VVSC, when present, must fit in the shared `icac_or_vvsc`
881        // slot — see `Fabric::icac_or_vvsc` (capacity `MAX_CERT_TLV_LEN`,
882        // also the spec's 400-byte ceiling on the field). An empty VVSC
883        // clears the existing one.
884        if let Some(v) = &vvsc {
885            if v.0.len() > crate::cert::MAX_CERT_TLV_LEN {
886                return Err(ErrorCode::ConstraintError.into());
887            }
888        }
889
890        let fab_idx = NonZeroU8::new(ctx.cmd().fab_idx).ok_or(ErrorCode::UnsupportedAccess)?;
891
892        let mut persist = FabricPersist::new(ctx.kv());
893
894        ctx.exchange().with_state(|state| {
895            let fabric = state.fabrics.fabric_mut(fab_idx)?;
896
897            // A VVSC may only be present on a fabric whose chain has no
898            // ICAC (Matter Core spec): the VVSC takes the
899            // ICAC's slot in the cert chain, the two are mutually
900            // exclusive. Reject any non-empty VVSC against a fabric
901            // that already carries an ICAC.
902            if let Some(v) = &vvsc {
903                if !v.0.is_empty() && !fabric.icac().is_empty() {
904                    return Err(ErrorCode::InvalidCommand.into());
905                }
906            }
907
908            fabric.set_vid_verification(
909                vendor_id,
910                vvs.as_ref().map(|s| s.0),
911                vvsc.as_ref().map(|v| v.0),
912            )?;
913
914            // Persist semantics (Matter Core spec):
915            //   * If `AddNOC` / `UpdateNOC` was already received in this
916            //     fail-safe context, the VID-verification state is part
917            //     of the pending fabric mutation. Don't persist yet —
918            //     `CommissioningComplete` will persist; a fail-safe
919            //     expiry will roll back via the usual fabric remove /
920            //     reload path.
921            //   * Otherwise (no in-flight fabric mutation), the change
922            //     is immediately persistent and SHALL NOT be reverted
923            //     even if the caller later disarms the fail-safe.
924            let part_of_pending_fabric =
925                state.failsafe.is_armed() && state.failsafe.has_pending_noc_for(fab_idx);
926            if !part_of_pending_fabric {
927                persist.store(fabric)?;
928            }
929
930            Ok(())
931        })?;
932
933        persist.run()?;
934
935        // The mutation changed `NOCs.vvsc` and / or `Fabrics.vendorID`
936        // / `.vidVerificationStatement` on this cluster.
937        ctx.notify_own_cluster_changed();
938
939        Ok(())
940    }
941
942    fn handle_sign_vid_verification_request<P: TLVBuilderParent>(
943        &self,
944        ctx: impl InvokeContext,
945        request: SignVIDVerificationRequestRequest<'_>,
946        mut response: SignVIDVerificationResponseBuilder<P>,
947    ) -> Result<P, Error> {
948        info!("Got Sign VID Verification Request");
949
950        // Spec: `FabricIndex` must be in [1..254];
951        // 0 / 255 are constraint errors.
952        let fab_idx_raw = request.fabric_index()?;
953        let fab_idx = NonZeroU8::new(fab_idx_raw)
954            .filter(|fi| fi.get() != u8::MAX)
955            .ok_or(ErrorCode::ConstraintError)?;
956
957        // `ClientChallenge` is fixed at 32 octets per cluster XML
958        // (`length="32" minLength="32"`). Anything else is a
959        // `CONSTRAINT_ERROR`.
960        let client_challenge = request.client_challenge()?.0;
961        if client_challenge.len() != VID_VERIFY_CLIENT_CHALLENGE_LEN {
962            return Err(ErrorCode::ConstraintError.into());
963        }
964
965        ctx.exchange().with_state(|state| {
966            let sess = ctx.exchange().id().session(&mut state.sessions);
967            let attestation_challenge = sess.get_att_challenge().ok_or(ErrorCode::InvalidState)?;
968            let attestation_challenge_bytes: [u8; ATT_CHALLENGE_LEN] =
969                *attestation_challenge.access();
970
971            let fabric = state
972                .fabrics
973                .get(fab_idx)
974                .ok_or(ErrorCode::ConstraintError)?;
975
976            // Build VendorFabricBindingMessage (Matter Core spec):
977            //   1B fabric_binding_version || 65B root_pub_key
978            //   || 8B fabric_id BE || 2B vendor_id BE
979            let root_ref = CertRef::new(TLVElement::new(fabric.root_ca()));
980            let root_pub_key = root_ref.pubkey()?;
981            if root_pub_key.len() != PKC_CANON_PUBLIC_KEY_LEN {
982                return Err(ErrorCode::InvalidData.into());
983            }
984
985            let fabric_id_be = fabric.fabric_id().to_be_bytes();
986            let vendor_id_be = fabric.vendor_id().to_be_bytes();
987
988            // Compute VIDVerificationTBS in a single contiguous buffer
989            // and feed it to the fabric's NOC private key.
990            //   1B fabric_binding_version || 32B client_challenge
991            //   || 32B attestation_challenge || 1B fabric_index
992            //   || vendor_fabric_binding_message (76B)
993            //   [|| vid_verification_statement (85B)]
994            //
995            // Borrow the response writer's unused tail as scratch — the TBS
996            // is consumed by `sign(...)` before any response field is
997            // written, so the bytes can safely be overwritten afterwards.
998            let tbs_buf = response.writer().available_space();
999            let mut len = 0usize;
1000
1001            tbs_buf[len] = FABRIC_BINDING_VERSION_1;
1002            len += 1;
1003            tbs_buf[len..len + client_challenge.len()].copy_from_slice(client_challenge);
1004            len += client_challenge.len();
1005            tbs_buf[len..len + attestation_challenge_bytes.len()]
1006                .copy_from_slice(&attestation_challenge_bytes);
1007            len += attestation_challenge_bytes.len();
1008            tbs_buf[len] = fab_idx.get();
1009            len += 1;
1010            // VendorFabricBindingMessage starts here.
1011            tbs_buf[len] = FABRIC_BINDING_VERSION_1;
1012            len += 1;
1013            tbs_buf[len..len + PKC_CANON_PUBLIC_KEY_LEN].copy_from_slice(root_pub_key);
1014            len += PKC_CANON_PUBLIC_KEY_LEN;
1015            tbs_buf[len..len + 8].copy_from_slice(&fabric_id_be);
1016            len += 8;
1017            tbs_buf[len..len + 2].copy_from_slice(&vendor_id_be);
1018            len += 2;
1019            // VIDVerificationStatement is appended only when set.
1020            let vvs = fabric.vid_verification_statement();
1021            if !vvs.is_empty() {
1022                tbs_buf[len..len + vvs.len()].copy_from_slice(vvs);
1023                len += vvs.len();
1024            }
1025
1026            // TODO XXX FIXME: MEDIUM BUFFER
1027            let mut signature = MaybeUninit::uninit();
1028            let signature = signature.init_with(CanonPkcSignature::init());
1029
1030            ctx.crypto()
1031                .secret_key(fabric.secret_key())?
1032                .sign(&tbs_buf[..len], signature)?;
1033
1034            response
1035                .fabric_index(fab_idx.get())?
1036                .fabric_binding_version(FABRIC_BINDING_VERSION_1)?
1037                .signature(Octets::new(signature.access()))?
1038                .end()
1039        })
1040    }
1041}
1042
1043/// Matter Core spec: `FabricBindingVersion` constant
1044/// for V1 of the `VendorFabricBindingMessage` and `VIDVerificationTBS`.
1045const FABRIC_BINDING_VERSION_1: u8 = 1;
1046
1047/// `ClientChallenge` is fixed at 32 octets (cluster XML
1048/// `length="32" minLength="32"` on `SignVIDVerificationRequest`).
1049const VID_VERIFY_CLIENT_CHALLENGE_LEN: usize = 32;