Skip to main content

rs_matter/dm/clusters/
adm_comm.rs

1/*
2 *
3 *    Copyright (c) 2025-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 Administrative Commissioning cluster and its handler.
19
20use rand_core::RngCore;
21
22use crate::crypto::Crypto;
23use crate::dm::{Cluster, Dataver, InvokeContext, ReadContext};
24use crate::error::{Error, ErrorCode};
25use crate::sc::pase::spake2p::SPAKE2P_VERIFIER_SALT_ZEROED;
26use crate::sc::pase::{CommWindowOpener, CommWindowType};
27use crate::tlv::Nullable;
28use crate::MatterState;
29
30pub use crate::dm::clusters::decl::administrator_commissioning::*;
31use crate::transport::exchange::ExchangeId;
32use crate::transport::session::SessionMode;
33
34/// PAKE iteration count bounds (Matter Core spec).
35const MIN_PBKDF_ITERATIONS: u32 = 1000;
36const MAX_PBKDF_ITERATIONS: u32 = 100_000;
37
38/// PAKE salt length bounds (Matter Core spec).
39const MIN_PAKE_SALT_LEN: usize = 16;
40const MAX_PAKE_SALT_LEN: usize = 32;
41
42/// SPAKE2+ verifier length: W0 (32) || L (65) = 97 octets
43/// (Matter Core spec).
44const PAKE_VERIFIER_LEN: usize = 97;
45
46/// Stash an `AdministratorCommissioning` cluster-specific status on the
47/// invoke context so the IM layer surfaces it in `StatusIB.clusterStatus`,
48/// then build the matching `Failure(0x01)` error to return from the handler.
49fn cluster_status_err(ctx: &impl InvokeContext, status: StatusCode) -> Error {
50    ctx.cmd().set_cluster_status(status as u8);
51    Error::new(ErrorCode::Failure)
52}
53
54/// The system implementation of a handler for the Administrative Commissioning Matter cluster.
55#[derive(Debug, Clone)]
56#[cfg_attr(feature = "defmt", derive(defmt::Format))]
57pub struct AdminCommHandler {
58    dataver: Dataver,
59}
60
61impl AdminCommHandler {
62    /// Create a new instance of `AdminCommHandler` with the given `Dataver`.
63    pub const fn new(dataver: Dataver) -> Self {
64        Self { dataver }
65    }
66
67    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
68    pub const fn adapt(self) -> HandlerAdaptor<Self> {
69        HandlerAdaptor(self)
70    }
71
72    /// Return a `CommWindowOpener` instance for the current session
73    ///
74    /// Used when opening a new commissioning window so as to preserve the fabric index and vendor ID
75    /// of the admin fabric which opened the current commissioning window.
76    fn current_window_opener(state: &mut MatterState, id: &ExchangeId) -> Option<CommWindowOpener> {
77        let session = id.session(&mut state.sessions);
78
79        match session.get_session_mode() {
80            SessionMode::Case { fab_idx, .. } => Some(CommWindowOpener {
81                fab_idx: *fab_idx,
82                vendor_id: unwrap!(state.fabrics.get(*fab_idx)).vendor_id(),
83            }),
84            _ => None,
85        }
86    }
87}
88
89impl ClusterHandler for AdminCommHandler {
90    const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_features(Feature::BASIC.bits());
91
92    fn dataver(&self) -> u32 {
93        self.dataver.get()
94    }
95
96    fn dataver_changed(&self) {
97        self.dataver.changed();
98    }
99
100    fn window_status(&self, ctx: impl ReadContext) -> Result<CommissioningWindowStatusEnum, Error> {
101        let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
102        let notify_change = |_, _| ctx.notify_own_cluster_changed();
103
104        ctx.exchange().with_state(|state| {
105            state
106                .pase
107                .check_comm_window_timeout(notify_mdns, notify_change)?;
108
109            let comm_window = state.pase.comm_window();
110
111            let window_type = comm_window.map(|comm_window| comm_window.comm_window_type());
112
113            Ok(match window_type {
114                Some(CommWindowType::Basic) => CommissioningWindowStatusEnum::BasicWindowOpen,
115                Some(CommWindowType::Enhanced) => CommissioningWindowStatusEnum::EnhancedWindowOpen,
116                None => CommissioningWindowStatusEnum::WindowNotOpen,
117            })
118        })
119    }
120
121    fn admin_fabric_index(&self, ctx: impl ReadContext) -> Result<Nullable<u8>, Error> {
122        let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
123        let notify_change = |_, _| ctx.notify_own_cluster_changed();
124
125        ctx.exchange().with_state(|state| {
126            state
127                .pase
128                .check_comm_window_timeout(notify_mdns, notify_change)?;
129
130            let comm_window = state.pase.comm_window();
131
132            if let Some(opener) = comm_window.and_then(|comm_window| comm_window.opener()) {
133                if state.fabrics.get(opener.fab_idx).is_some() {
134                    // Fabric is still around, return its index
135                    // If it is not around - and contrary to vendor ID - we should NOT return it
136                    return Ok(Nullable::some(opener.fab_idx.get()));
137                }
138            }
139
140            Ok(Nullable::none())
141        })
142    }
143
144    fn admin_vendor_id(&self, ctx: impl ReadContext) -> Result<Nullable<u16>, Error> {
145        let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
146        let notify_change = |_, _| ctx.notify_own_cluster_changed();
147
148        ctx.exchange().with_state(|state| {
149            state
150                .pase
151                .check_comm_window_timeout(notify_mdns, notify_change)?;
152
153            let comm_window = state.pase.comm_window();
154
155            Ok(Nullable::new(
156                comm_window
157                    .and_then(|comm_window| comm_window.opener())
158                    .map(|opener| opener.vendor_id),
159            ))
160        })
161    }
162
163    fn handle_open_commissioning_window(
164        &self,
165        ctx: impl InvokeContext,
166        request: OpenCommissioningWindowRequest<'_>,
167    ) -> Result<(), Error> {
168        let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
169        let notify_change = |_, _| ctx.notify_own_cluster_changed();
170
171        // Validate the PAKE parameters up front so we can surface
172        // `PAKEParameterError` as the cluster-specific status (Matter Core
173        // spec).
174        let iterations = request.iterations()?;
175        if !(MIN_PBKDF_ITERATIONS..=MAX_PBKDF_ITERATIONS).contains(&iterations) {
176            return Err(cluster_status_err(&ctx, StatusCode::PAKEParameterError));
177        }
178
179        let salt = request.salt()?;
180        if !(MIN_PAKE_SALT_LEN..=MAX_PAKE_SALT_LEN).contains(&salt.0.len()) {
181            return Err(cluster_status_err(&ctx, StatusCode::PAKEParameterError));
182        }
183
184        let verifier = request.pake_passcode_verifier()?;
185        if verifier.0.len() != PAKE_VERIFIER_LEN {
186            return Err(cluster_status_err(&ctx, StatusCode::PAKEParameterError));
187        }
188
189        ctx.exchange().with_state(|state| {
190            state
191                .pase
192                .check_comm_window_timeout(notify_mdns, notify_change)?;
193
194            let opener = Self::current_window_opener(state, &ctx.exchange().id());
195
196            let mdns_id = ctx.crypto().rand()?.next_u64();
197
198            state
199                .pase
200                .open_comm_window(
201                    mdns_id,
202                    verifier.0.try_into()?,
203                    salt.0,
204                    iterations,
205                    request.discriminator()?,
206                    request.commissioning_timeout()?,
207                    opener,
208                    notify_mdns,
209                    notify_change,
210                )
211                .map_err(|err| map_open_window_err(&ctx, err))
212        })
213    }
214
215    fn handle_open_basic_commissioning_window(
216        &self,
217        ctx: impl InvokeContext,
218        request: OpenBasicCommissioningWindowRequest<'_>,
219    ) -> Result<(), Error> {
220        let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
221        let notify_change = |_, _| ctx.notify_own_cluster_changed();
222
223        ctx.exchange().with_state(|state| {
224            state
225                .pase
226                .check_comm_window_timeout(notify_mdns, notify_change)?;
227
228            let opener = Self::current_window_opener(state, &ctx.exchange().id());
229            let dev_comm = ctx.exchange().matter().dev_comm();
230
231            let crypto = ctx.crypto();
232            let mut rand = crypto.rand()?;
233
234            let mdns_id = rand.next_u64();
235
236            let mut salt = SPAKE2P_VERIFIER_SALT_ZEROED;
237            rand.fill_bytes(salt.access_mut());
238
239            state
240                .pase
241                .open_basic_comm_window(
242                    mdns_id,
243                    salt.access(),
244                    dev_comm.password.reference(),
245                    dev_comm.discriminator,
246                    request.commissioning_timeout()?,
247                    opener,
248                    notify_mdns,
249                    notify_change,
250                )
251                .map_err(|err| map_open_window_err(&ctx, err))
252        })
253    }
254
255    fn handle_revoke_commissioning(&self, ctx: impl InvokeContext) -> Result<(), Error> {
256        let notify_mdns = || ctx.exchange().matter().transport().notify_mdns_changed();
257        let notify_change = |_, _| ctx.notify_own_cluster_changed();
258
259        // Per Matter Core spec (and CHIP's
260        // `AdministratorCommissioningLogic::RevokeCommissioning`), revoking
261        // the commissioning window also:
262        //
263        //  1. Forces any in-flight fail-safe context to expire. Otherwise
264        //     stale state — e.g. the breadcrumb value an interrupted
265        //     commissioning attempt left behind — leaks into the next
266        //     `OpenCommissioningWindow` round and causes the commissioner to
267        //     skip past `ArmFailSafe` (it reads `breadcrumb > 0` and assumes
268        //     an in-progress commission, per the CHIP
269        //     `AutoCommissioner::GetNextCommissioningStageInternal` post-NOC
270        //     recovery path).
271        //
272        //  2. Tears down any PASE sessions that were established under that
273        //     commissioning window but never promoted to a fabric. CHIP ties
274        //     this to fail-safe expiry inside `CommissioningWindowManager`;
275        //     we do it explicitly here so accumulated dangling PASE sessions
276        //     don't confuse the commissioner across multiple rounds (see
277        //     TC_CGEN_2_4, which iterates open / commission / revoke 8x).
278        let removed_fabric = ctx.exchange().with_state(|state| {
279            // If RevokeCommissioning came in over a PASE session, don't
280            // drop it before we've sent the response — mark it as expired
281            // instead so it lingers just long enough for the in-flight
282            // exchange to complete, then can't accept new ones.
283            // `Failsafe::expire` does the actual `remove_pase` call.
284            let sess = ctx.exchange().id().session(&mut state.sessions);
285            let expire_sess_id = matches!(
286                sess.get_session_mode(),
287                crate::transport::session::SessionMode::Pase { .. }
288            )
289            .then(|| sess.id());
290
291            let removed_fabric = state.failsafe.expire(
292                &mut state.fabrics,
293                &mut state.sessions,
294                expire_sess_id,
295                ctx.networks(),
296                ctx.kv(),
297                notify_mdns,
298                notify_change,
299            )?;
300
301            ctx.exchange().matter().transport().notify_session_removed();
302
303            Ok::<_, Error>(removed_fabric)
304        })?;
305
306        // Broadcast for a fabric the expiry dropped (a not-yet-committed
307        // `AddNOC` one; an `UpdateNOC`-mutated fabric is resurrected instead
308        // and not reported). Outside `with_state`, since the broadcast runs
309        // the handlers inline.
310        if let Some(fab_idx) = removed_fabric {
311            ctx.notify_fabric_removed(fab_idx);
312        }
313
314        ctx.exchange().with_state(|state| {
315            state
316                .pase
317                .close_comm_window(notify_mdns, notify_change)
318                .map_err(|err| map_revoke_err(&ctx, err))
319        })?;
320
321        Ok(())
322    }
323}
324
325/// Map errors returned by `Pase::open_*_comm_window` to
326/// `AdministratorCommissioning` cluster-specific status codes (Matter Core
327/// spec). `Busy` is the only cluster status the open paths
328/// surface today; other transport-level errors (e.g. invalid timeout) bubble
329/// up unchanged so the IM layer turns them into the generic `InvalidCommand`.
330fn map_open_window_err(ctx: &impl InvokeContext, err: Error) -> Error {
331    if err.code() == ErrorCode::Busy {
332        cluster_status_err(ctx, StatusCode::Busy)
333    } else {
334        err
335    }
336}
337
338/// Same idea for `RevokeCommissioning`: per spec, attempting to revoke when
339/// no window is open SHALL return `WindowNotOpen`. `Pase::close_comm_window`
340/// reports this as `ErrorCode::Invalid`.
341fn map_revoke_err(ctx: &impl InvokeContext, err: Error) -> Error {
342    if matches!(err.code(), ErrorCode::Invalid) {
343        cluster_status_err(ctx, StatusCode::WindowNotOpen)
344    } else {
345        err
346    }
347}