Skip to main content

matter_commissioning/state_machine/
commissioner.rs

1//! `Commissioner` — the state-machine cursor.
2
3#![forbid(unsafe_code)]
4
5use std::sync::Arc;
6
7use matter_cert::time::MatterTime;
8
9use crate::attestation::PaaTrustStore;
10use crate::noc::{FabricRecord, NocRng};
11use crate::setup::SetupPayload;
12use crate::state_machine::action::{Action, Expectation};
13use crate::state_machine::error::CommissioningError;
14use crate::state_machine::stage::Stage;
15
16#[cfg(feature = "tracing")]
17use tracing::instrument;
18
19/// Fallback failsafe extension (seconds) applied at
20/// `Stage::FailsafeBeforeNetworkEnable` when the device did not report a
21/// usable `ConnectMaxTimeSeconds`. Chosen generously (Thread attach +
22/// SRP registration is slower than Wi-Fi association); the C1 Wi-Fi path
23/// adopts it harmlessly. Matter Core Spec §11.9.5.4 defines the attribute
24/// but does not mandate a minimum, so a conservative default is safest.
25pub(crate) const DEFAULT_CONNECT_MAX_TIME_SECONDS: u16 = 90;
26
27/// Wi-Fi station credentials supplied to `AddOrUpdateWiFiNetwork`.
28///
29/// `ssid` must be 1–32 bytes (Matter Core Spec §11.9 constraints).
30/// `credentials` must be 0–64 bytes — empty means open network, ≤64
31/// bytes covers WPA2/WPA3 PSK lengths.
32///
33/// `Debug` is hand-written to redact `credentials` (renders only the
34/// length). `Clone` is derived. Validation runs in
35/// `Commissioner::new` (M6.5.2 Task 13).
36#[derive(Clone, PartialEq, Eq)]
37pub struct WiFiCredentials {
38    /// SSID bytes, 1–32 bytes.
39    pub ssid: Vec<u8>,
40    /// Pre-shared key / passphrase bytes, 0–64 bytes. Empty means
41    /// open network.
42    pub credentials: Vec<u8>,
43}
44
45impl core::fmt::Debug for WiFiCredentials {
46    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47        f.debug_struct("WiFiCredentials")
48            .field("ssid", &format_args!("<{} bytes>", self.ssid.len()))
49            .field(
50                "credentials",
51                &format_args!("<redacted, {} bytes>", self.credentials.len()),
52            )
53            .finish()
54    }
55}
56
57/// Operational-network credentials for the commissionee, selecting which
58/// network-provisioning sub-cursor the state machine runs after `AddNOC`.
59///
60/// Mirrors chip's `AutoCommissioner`: network provisioning runs only for
61/// the concrete network type whose credentials are supplied.
62///
63/// - [`NetworkCredentials::WiFi`] provisions Wi-Fi via
64///   `AddOrUpdateWiFiNetwork` + `ConnectNetwork`.
65/// - [`NetworkCredentials::Thread`] provisions Thread from an operational
66///   dataset via `AddOrUpdateThreadNetwork` + `ConnectNetwork` (the
67///   Extended PAN ID is the `ConnectNetwork` `network_id`); the dataset is
68///   self-validated at [`ThreadDataset`](crate::ThreadDataset)
69///   construction.
70/// - [`NetworkCredentials::AlreadyOnNetwork`] skips network provisioning
71///   entirely — correct both for Ethernet-only devices and for devices
72///   already reachable on their operational network (the usual
73///   IP-commissioning case, e.g. a second-fabric commission).
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub enum NetworkCredentials {
76    /// Provision Wi-Fi using the supplied station credentials.
77    WiFi(WiFiCredentials),
78    /// Provision Thread using the supplied operational dataset.
79    Thread(crate::thread_dataset::ThreadDataset),
80    /// Device is already on an operational network; skip provisioning.
81    AlreadyOnNetwork,
82}
83
84/// Configuration passed to [`Commissioner::new`].
85///
86/// All fields are by-reference where possible so the state machine
87/// can share long-lived caller-owned resources (the fabric record, the
88/// trust store, the setup payload) without copying.
89///
90/// **Not `#[non_exhaustive]`** — callers build this as a struct literal
91/// with all public fields populated. Adding a field is a breaking change,
92/// accepted for a pre-1.0 unpublished crate. `#[non_exhaustive]` stays on
93/// [`Action`], [`Expectation`], [`Stage`], and [`CommissioningError`] —
94/// those are read by callers, not constructed by them.
95pub struct CommissionerConfig<'a> {
96    /// 16-byte attestation challenge derived from the active PASE
97    /// session. Matter Core Spec §3.6.4: bytes `[32..48]` of the
98    /// 48-byte PASE session key blob (exposed as
99    /// `PaseSessionKeys::attestation_key`).
100    pub pase_attestation_challenge: [u8; 16],
101    /// The commissioner's fabric record (RCAC keypair + signer + IPK).
102    /// Constructed via [`FabricRecord::new_root_only`] from M6.3.
103    pub fabric: &'a FabricRecord,
104    /// The setup payload parsed from QR or manual code (M6.1). Used
105    /// to cross-check VID/PID against the DAC's subject during
106    /// attestation verification.
107    pub setup_payload: &'a SetupPayload,
108    /// Trusted PAA roots for attestation chain validation (M6.2).
109    pub paa_trust_store: &'a PaaTrustStore,
110    /// Trusted CSA Certification Declaration signing roots (M6.4.3).
111    /// Tests can use `CdSigningRoots::with_example_device_roots()`; production
112    /// callers supply CSA-published roots via `CdSigningRoots::from_pem`.
113    pub cd_signing_roots: &'a crate::attestation::CdSigningRoots,
114    /// The commissioner's own operational node ID on this fabric.
115    /// Must be non-zero.
116    pub commissioner_node_id: u64,
117    /// The operational node ID being assigned to the device on this
118    /// fabric. Must be non-zero and distinct from
119    /// `commissioner_node_id`.
120    pub assigned_node_id: u64,
121    /// 16-byte Identity Protection Key (IPK) epoch key for `AddNOC`.
122    /// Matter Core Spec §4.15.2. Must not be all-zero (rejected by
123    /// the device-side `AddNOC` handler).
124    pub ipk_epoch_key: [u8; 16],
125    /// CASE admin subject for `AddNOC` (typically the commissioner's
126    /// own operational node ID).
127    pub case_admin_subject: u64,
128    /// Admin vendor ID for `AddNOC`.
129    pub admin_vendor_id: u16,
130    /// Wall-clock time at construction. Used for NOC + RCAC validity
131    /// windows and for chain verification's `not_before` / `not_after`
132    /// checks.
133    pub now: MatterTime,
134    /// RNG for nonces (`CSRNonce`, `AttestationNonce`) and NOC serials.
135    pub rng: Arc<dyn NocRng>,
136    /// Operational-network credentials for the commissionee.
137    ///
138    /// [`NetworkCredentials::AlreadyOnNetwork`] skips the network
139    /// sub-cursor entirely, mirroring chip's `AutoCommissioner`: network
140    /// provisioning runs ONLY when concrete credentials are supplied. It is
141    /// correct both for Ethernet-only devices and for devices already
142    /// reachable on their operational network (the usual case for IP
143    /// commissioning, e.g. a second-fabric commission). Supplying
144    /// [`NetworkCredentials::WiFi`] or [`NetworkCredentials::Thread`]
145    /// forces provisioning via `Stage::NetworkSetup`.
146    pub network: NetworkCredentials,
147}
148
149/// The commissioning state machine cursor.
150///
151/// One `Commissioner` per in-flight commissioning. `Send` but `!Sync`.
152/// See module docs in [`crate::state_machine`] for the driver-loop
153/// example.
154// `commissioner_node_id` mirrors Matter Core Spec terminology
155// (commissioner node ID vs. assigned node ID). Renaming to satisfy
156// the lint would obscure the spec mapping.
157#[allow(clippy::struct_field_names)]
158pub struct Commissioner {
159    stage: Stage,
160
161    // Configuration captured at construction time. Storage slots for
162    // M6.4.2+ (`pai_der`, `dac_der`, `attestation_response`, CSR /
163    // NOC artefacts, the CASE-awaiting flag, etc.) are added in
164    // later tasks as the corresponding stages land — keeping the
165    // struct minimal here avoids per-task churn on the field list.
166    #[allow(dead_code)] // Used by attestation/CSR verification in M6.4.2+.
167    pase_attestation_challenge: [u8; 16],
168    #[allow(dead_code)] // Used by NOC issuance + chain validation in M6.4.4.
169    fabric: FabricRecord,
170    #[allow(dead_code)] // Used by chain validation in M6.4.2.
171    paa_trust_store: PaaTrustStore,
172    cd_signing_roots: crate::attestation::CdSigningRoots,
173    #[allow(dead_code)] // Used by VID/PID cross-check in M6.4.2.
174    setup_payload: SetupPayload,
175    #[allow(dead_code)] // Used by NOC subject in M6.4.4.
176    commissioner_node_id: u64,
177    #[allow(dead_code)] // Used by NOC subject in M6.4.4.
178    assigned_node_id: u64,
179    #[allow(dead_code)] // Used by AddNOC payload in M6.4.4.
180    ipk_epoch_key: [u8; 16],
181    #[allow(dead_code)] // Used by AddNOC payload in M6.4.4.
182    case_admin_subject: u64,
183    #[allow(dead_code)] // Used by AddNOC payload in M6.4.4.
184    admin_vendor_id: u16,
185    #[allow(dead_code)] // Used by cert validity windows in M6.4.2 + M6.4.4.
186    now: MatterTime,
187    #[allow(dead_code)] // Used for nonce generation in M6.4.2 + M6.4.4.
188    rng: Arc<dyn NocRng>,
189
190    // Attestation slots — populated by SendPaiCertRequest /
191    // SendDacCertRequest / SendAttestationRequest, consumed by
192    // AttestationVerification (M6.4.2 T18-T21).
193    pai_der: Option<Vec<u8>>,
194    dac_der: Option<Vec<u8>>,
195    attestation_nonce: Option<[u8; 32]>,
196    attestation_response: Option<crate::attestation::AttestationResponse>,
197
198    // CSR + NOC slots — populated by SendOpCertSigningRequest /
199    // ValidateCsr / GenerateNocChain, consumed by SendTrustedRootCert
200    // and SendNoc (M6.4.4 T35-T40).
201    csr_nonce: Option<[u8; 32]>,
202    csr_response: Option<crate::noc::CsrResponse>,
203    verified_csr: Option<crate::noc::VerifiedCsr>,
204    issued_noc: Option<matter_cert::MatterCertificate>,
205    issued_noc_public_key: Option<[u8; 65]>,
206
207    /// Operational-network credentials captured from config at
208    /// construction; consumed by the network-provisioning sub-cursor
209    /// (`Stage::NetworkSetup`, `AddOrUpdateWiFiNetwork` for Wi-Fi or
210    /// `AddOrUpdateThreadNetwork` for Thread).
211    /// [`NetworkCredentials::AlreadyOnNetwork`] skips provisioning.
212    network: NetworkCredentials,
213
214    /// Maximum failsafe expiry the device accepts, in seconds.
215    /// Initialised to 60 (the M6.4 fallback) and updated from
216    /// `BasicCommissioningInfo::failsafe_expiry_length_seconds` once
217    /// the `Expectation::CommissioningInfo` response arrives. The first
218    /// `Stage::ArmFailsafe` consumes this.
219    failsafe_expiry_seconds: u16,
220
221    /// Device-declared `ConnectMaxTimeSeconds` (`NetworkCommissioning`
222    /// attribute `0x0003`), captured from the
223    /// `Expectation::NetworkCommissioningInfo` read. `0` means unread /
224    /// absent, in which case [`Self::network_enable_failsafe_seconds`]
225    /// falls back to [`DEFAULT_CONNECT_MAX_TIME_SECONDS`]. Sizes the
226    /// `Stage::FailsafeBeforeNetworkEnable` failsafe extension so Thread
227    /// attach (slower than Wi-Fi association) has room to complete before
228    /// the failsafe expires.
229    connect_max_time_seconds: u16,
230
231    /// Monotonically-increasing breadcrumb attached to every
232    /// breadcrumb-bearing cluster command. Matter Core Spec §11.10
233    /// uses breadcrumb so an interrupted commissioning can be resumed
234    /// from the last acknowledged step. Initialised to `1` in
235    /// `Commissioner::new`; incremented after every breadcrumb emit.
236    breadcrumb_counter: u64,
237
238    /// `true` after [`Stage::FindOperationalForComplete`] emits
239    /// `Action::EstablishCase`; cleared by
240    /// [`Commissioner::on_case_established`] (success) or by
241    /// `on_response(Expectation::CaseFailed, _)` (failure).
242    awaiting_case_session: bool,
243
244    /// The Expectation the state machine last emitted with `poll()`.
245    /// `None` while not waiting for a response (terminal stages, or
246    /// pre-poll).
247    awaiting: Option<Expectation>,
248
249    /// Cached pending Action so repeated `poll()` calls between
250    /// `on_response`s are idempotent. Cleared when the cursor advances.
251    pending_action: Option<Action>,
252
253    /// Rendered summary of why the state machine entered `Failed`,
254    /// stashed by `on_response`'s error path and read by the
255    /// `Stage::Failed` arm of `dispatch_stage` so `Action::Abort.reason`
256    /// surfaces the real failure (not a hard-coded placeholder).
257    last_failure: Option<String>,
258}
259
260impl Commissioner {
261    /// Construct a new commissioner from a validated config.
262    ///
263    /// # Errors
264    ///
265    /// Returns [`CommissioningError::InvalidConfig`] if any field fails
266    /// basic validation: zero `commissioner_node_id`, zero
267    /// `assigned_node_id`, `commissioner_node_id == assigned_node_id`,
268    /// or all-zero `ipk_epoch_key`.
269    pub fn new(cfg: CommissionerConfig<'_>) -> Result<Self, CommissioningError> {
270        if cfg.commissioner_node_id == 0 {
271            return Err(CommissioningError::InvalidConfig(
272                "commissioner_node_id must be non-zero",
273            ));
274        }
275        if cfg.assigned_node_id == 0 {
276            return Err(CommissioningError::InvalidConfig(
277                "assigned_node_id must be non-zero",
278            ));
279        }
280        if cfg.assigned_node_id == cfg.commissioner_node_id {
281            return Err(CommissioningError::InvalidConfig(
282                "assigned_node_id must differ from commissioner_node_id",
283            ));
284        }
285        if cfg.ipk_epoch_key == [0u8; 16] {
286            return Err(CommissioningError::InvalidConfig(
287                "ipk_epoch_key must not be all-zero",
288            ));
289        }
290        // Only Wi-Fi credentials carry length bounds here; `Thread`
291        // datasets are self-validated at `ThreadDataset` construction and
292        // `AlreadyOnNetwork` carries no data to check.
293        if let NetworkCredentials::WiFi(creds) = &cfg.network {
294            if creds.ssid.is_empty() {
295                return Err(CommissioningError::InvalidConfig(
296                    "network: Wi-Fi ssid must not be empty",
297                ));
298            }
299            if creds.ssid.len() > 32 {
300                return Err(CommissioningError::InvalidConfig(
301                    "network: Wi-Fi ssid must be ≤32 bytes",
302                ));
303            }
304            if creds.credentials.len() > 64 {
305                return Err(CommissioningError::InvalidConfig(
306                    "network: Wi-Fi credentials must be ≤64 bytes",
307                ));
308            }
309        }
310        Ok(Self {
311            stage: Stage::SecurePairing,
312            pase_attestation_challenge: cfg.pase_attestation_challenge,
313            fabric: cfg.fabric.clone(),
314            paa_trust_store: cfg.paa_trust_store.clone(),
315            cd_signing_roots: cfg.cd_signing_roots.clone(),
316            setup_payload: cfg.setup_payload.clone(),
317            commissioner_node_id: cfg.commissioner_node_id,
318            assigned_node_id: cfg.assigned_node_id,
319            ipk_epoch_key: cfg.ipk_epoch_key,
320            case_admin_subject: cfg.case_admin_subject,
321            admin_vendor_id: cfg.admin_vendor_id,
322            now: cfg.now,
323            rng: cfg.rng,
324            pai_der: None,
325            dac_der: None,
326            attestation_nonce: None,
327            attestation_response: None,
328            csr_nonce: None,
329            csr_response: None,
330            verified_csr: None,
331            issued_noc: None,
332            issued_noc_public_key: None,
333            network: cfg.network,
334            failsafe_expiry_seconds: 60,
335            connect_max_time_seconds: 0,
336            breadcrumb_counter: 1,
337            awaiting_case_session: false,
338            awaiting: None,
339            pending_action: None,
340            last_failure: None,
341        })
342    }
343
344    /// Current cursor position. Useful for logging + tests.
345    #[must_use]
346    pub fn stage(&self) -> Stage {
347        self.stage
348    }
349
350    /// The operational-network credentials captured at construction.
351    /// Read by tests and by the network-provisioning dispatch/routing
352    /// (Task 5 consumes this to select the Thread sub-cursor).
353    #[allow(dead_code)] // Consumed by tests now; by Thread routing in Task 5.
354    pub(crate) fn network(&self) -> &NetworkCredentials {
355        &self.network
356    }
357
358    /// **Test-only.** Jumps the cursor to `stage` and applies any opt-in
359    /// seeds in `seeds`. Consumes `self` and returns the repositioned
360    /// `Commissioner`.
361    ///
362    /// Use this in integration tests when a real M6.4 attestation +
363    /// NOC-issuance flow is not yet available (the M6.4.6 real-fixture
364    /// e2e driver is still operator-touch deferred — see
365    /// `TODO-1.0.md`). Never use in production code.
366    ///
367    /// Behind the `__test_shortcuts` feature flag.
368    #[cfg(feature = "__test_shortcuts")]
369    #[must_use]
370    pub fn position_at_stage_for_test(mut self, stage: Stage, seeds: TestStateSeeds) -> Self {
371        self.stage = stage;
372        if let Some(pk) = seeds.synthetic_noc_pubkey {
373            self.issued_noc_public_key = Some(pk);
374        }
375        self
376    }
377
378    /// Drive the state machine forward.
379    ///
380    /// Returns the next [`Action`] the caller must perform. Idempotent:
381    /// calling `poll` twice without an intervening `on_response` returns
382    /// the same `Action`.
383    ///
384    /// # Errors
385    ///
386    /// Returns the typed error that caused a transition into
387    /// [`Stage::Failed`] — when this happens, the cursor advances to
388    /// `Failed` and the next `poll()` call emits an
389    /// [`Action::Abort`] with a rendered summary of the failure.
390    #[cfg_attr(feature = "tracing", instrument(skip(self), fields(stage = ?self.stage)))]
391    pub fn poll(&mut self) -> Result<Action, CommissioningError> {
392        if let Some(act) = self.pending_action.clone() {
393            return Ok(act);
394        }
395        let action = self.dispatch_stage()?;
396        self.pending_action = Some(action.clone());
397        Ok(action)
398    }
399
400    /// Failsafe extension (seconds) for `Stage::FailsafeBeforeNetworkEnable`.
401    ///
402    /// Uses the device-reported `ConnectMaxTimeSeconds` when non-zero,
403    /// else [`DEFAULT_CONNECT_MAX_TIME_SECONDS`]. Sized to give the device
404    /// room to associate with the operational network (Thread attach is
405    /// slower than Wi-Fi association) before the failsafe expires.
406    fn network_enable_failsafe_seconds(&self) -> u16 {
407        if self.connect_max_time_seconds > 0 {
408            self.connect_max_time_seconds
409        } else {
410            DEFAULT_CONNECT_MAX_TIME_SECONDS
411        }
412    }
413
414    /// Record the device's `ConnectMaxTimeSeconds` (`NetworkCommissioning`
415    /// attribute `0x0003`), read alongside the `FeatureMap` at
416    /// `Stage::ReadNetworkCommissioningInfo`. Consumed by
417    /// [`Self::network_enable_failsafe_seconds`] to size the
418    /// `FailsafeBeforeNetworkEnable` extension. Called by the driver's
419    /// read-dispatch after the `FeatureMap` response is applied; a `0`
420    /// value (unread/absent) leaves the default in force.
421    pub(crate) fn set_connect_max_time_seconds(&mut self, seconds: u16) {
422        self.connect_max_time_seconds = seconds;
423    }
424
425    /// The device-reported `ConnectMaxTimeSeconds`, or `0` if the device
426    /// hasn't reported it yet (default before
427    /// `Stage::ReadNetworkCommissioningInfo` completes, or the device
428    /// reported `0`).
429    ///
430    /// Consumed by the driver ([`crate::driver::commission`]) to size the
431    /// BLE-path `ConnectNetwork` response deadline from the same value that
432    /// [`Self::network_enable_failsafe_seconds`] uses for the failsafe
433    /// extension (spec D7: both must be sized from `ConnectMaxTimeSeconds`).
434    #[must_use]
435    pub(crate) fn connect_max_time_seconds(&self) -> u16 {
436        self.connect_max_time_seconds
437    }
438
439    /// Helper: emit an `ArmFailsafe` action at the current stage.
440    ///
441    /// Used by both `Stage::ArmFailsafe` and
442    /// `Stage::FailsafeBeforeNetworkEnable`, which share identical action
443    /// logic. The failsafe expiry differs: the first arm uses the
444    /// device's `failsafe_expiry_length_seconds`; the pre-`ConnectNetwork`
445    /// extension uses [`Self::network_enable_failsafe_seconds`].
446    fn arm_failsafe_action(&mut self) -> Action {
447        use crate::clusters::general_commissioning as gc;
448        use crate::state_machine::action::SessionContext;
449        let expiry_seconds = if self.stage == Stage::FailsafeBeforeNetworkEnable {
450            self.network_enable_failsafe_seconds()
451        } else {
452            self.failsafe_expiry_seconds
453        };
454        let breadcrumb = self.next_breadcrumb();
455        let payload = gc::encode_arm_fail_safe(expiry_seconds, breadcrumb);
456        self.awaiting = Some(Expectation::ArmFailsafeResponse);
457        Action::Invoke {
458            session: SessionContext::Pase,
459            endpoint: 0,
460            cluster: gc::CLUSTER_ID,
461            command: gc::command_id::ARM_FAIL_SAFE,
462            payload,
463            expect: Expectation::ArmFailsafeResponse,
464        }
465    }
466
467    /// Compute the next [`Action`] for the current [`Stage`].
468    ///
469    /// Called by [`Self::poll`] only when there is no cached
470    /// `pending_action`. Walks `Stage::SecurePairing` forward to the
471    /// first wire stage by self-recursion; stages past
472    /// `Stage::ConfigRegulatory` short-circuit to `Stage::Failed` until
473    /// M6.4.2+ tasks land.
474    // Lint carve-out: the per-stage arms each carry their own
475    // payload-shape comments, so collapsing them into smaller helpers
476    // would obscure the cluster-command mapping the function
477    // documents. Each new stage adds a small fixed arm.
478    #[allow(clippy::too_many_lines)]
479    fn dispatch_stage(&mut self) -> Result<Action, CommissioningError> {
480        use crate::clusters::general_commissioning as gc;
481        use crate::state_machine::action::SessionContext;
482        match self.stage {
483            Stage::SecurePairing => {
484                // Entry → first wire stage. Advance and re-dispatch.
485                self.stage = Stage::ReadCommissioningInfo;
486                self.dispatch_stage()
487            }
488            Stage::ReadCommissioningInfo => {
489                self.awaiting = Some(Expectation::CommissioningInfo);
490                Ok(Action::ReadAttribute {
491                    session: SessionContext::Pase,
492                    endpoint: 0,
493                    cluster: gc::CLUSTER_ID,
494                    attributes: &[
495                        // GeneralCommissioning attribute ids per spec §11.10.6
496                        // (confirmed against a real device's report).
497                        0x0000, // Breadcrumb
498                        0x0001, // BasicCommissioningInfo (failsafe_expiry_length_seconds, …)
499                        0x0002, // RegulatoryConfig
500                        0x0004, // SupportsConcurrentConnection
501                    ],
502                    expect: Expectation::CommissioningInfo,
503                })
504            }
505            Stage::ArmFailsafe | Stage::FailsafeBeforeNetworkEnable => {
506                Ok(self.arm_failsafe_action())
507            }
508            Stage::ConfigRegulatory => {
509                let breadcrumb = self.next_breadcrumb();
510                // FIXME(temp, uncommitted): hardcoding IndoorOutdoor(2) exceeds
511                // stricter devices' LocationCapability → SetRegulatoryConfig
512                // returns ValueOutsideRange (errorCode 1). The chip-faithful fix
513                // is to read attr 0x03 (LocationCapability) in ReadCommissioningInfo
514                // and echo it here. Indoor(0) is the safe universal value (accepted
515                // by Indoor-only AND IndoorOutdoor devices) — minimal unblock.
516                let payload = gc::encode_set_regulatory_config(
517                    gc::RegulatoryLocation::Indoor,
518                    "XX",
519                    breadcrumb,
520                );
521                self.awaiting = Some(Expectation::SetRegulatoryConfigResponse);
522                Ok(Action::Invoke {
523                    session: SessionContext::Pase,
524                    endpoint: 0,
525                    cluster: gc::CLUSTER_ID,
526                    command: gc::command_id::SET_REGULATORY_CONFIG,
527                    payload,
528                    expect: Expectation::SetRegulatoryConfigResponse,
529                })
530            }
531            Stage::SendPaiCertRequest => {
532                use crate::noc::{encode_certificate_chain_request, CertChainType};
533                let payload = encode_certificate_chain_request(CertChainType::Pai);
534                self.awaiting = Some(Expectation::PaiCertChainResponse);
535                Ok(Action::Invoke {
536                    session: SessionContext::Pase,
537                    endpoint: 0,
538                    cluster: 0x003E,
539                    command: 0x02,
540                    payload,
541                    expect: Expectation::PaiCertChainResponse,
542                })
543            }
544            Stage::SendDacCertRequest => {
545                use crate::noc::{encode_certificate_chain_request, CertChainType};
546                let payload = encode_certificate_chain_request(CertChainType::Dac);
547                self.awaiting = Some(Expectation::DacCertChainResponse);
548                Ok(Action::Invoke {
549                    session: SessionContext::Pase,
550                    endpoint: 0,
551                    cluster: 0x003E,
552                    command: 0x02,
553                    payload,
554                    expect: Expectation::DacCertChainResponse,
555                })
556            }
557            Stage::SendAttestationRequest => {
558                use crate::noc::encode_attestation_request;
559                let mut nonce = [0u8; 32];
560                self.rng
561                    .fill(&mut nonce)
562                    .map_err(CommissioningError::from)?;
563                let payload = encode_attestation_request(&nonce);
564                self.attestation_nonce = Some(nonce);
565                self.awaiting = Some(Expectation::AttestationResponse);
566                Ok(Action::Invoke {
567                    session: SessionContext::Pase,
568                    endpoint: 0,
569                    cluster: 0x003E,
570                    command: 0x00,
571                    payload,
572                    expect: Expectation::AttestationResponse,
573                })
574            }
575            Stage::AttestationVerification => {
576                match self.run_attestation_verification() {
577                    Ok(()) => {
578                        self.advance(Stage::SendOpCertSigningRequest);
579                        self.dispatch_stage()
580                    }
581                    Err(err) => {
582                        // Poll-time failure: align with the contract documented
583                        // on `poll()` — cursor advances to `Failed`, the next
584                        // `poll()` emits `Action::Abort` with a rendered reason.
585                        self.last_failure = Some(err.to_string());
586                        self.stage = Stage::Failed;
587                        self.awaiting = None;
588                        self.pending_action = None;
589                        Err(err)
590                    }
591                }
592            }
593            Stage::SendOpCertSigningRequest => {
594                use crate::noc::encode_csr_request;
595                let mut nonce = [0u8; 32];
596                self.rng
597                    .fill(&mut nonce)
598                    .map_err(CommissioningError::from)?;
599                // Spec §11.18.5.5 `CSRRequest`. `is_for_update_noc` is
600                // hard-coded false: M6.4 only commissions new fabrics.
601                let payload = encode_csr_request(&nonce, false);
602                self.csr_nonce = Some(nonce);
603                self.awaiting = Some(Expectation::CsrResponse);
604                Ok(Action::Invoke {
605                    session: SessionContext::Pase,
606                    endpoint: 0,
607                    cluster: 0x003E,
608                    command: 0x04,
609                    payload,
610                    expect: Expectation::CsrResponse,
611                })
612            }
613            Stage::ValidateCsr => {
614                // Off-wire: M6.3's three-check verify_csr_response gate.
615                self.run_validate_csr()?;
616                self.advance(Stage::GenerateNocChain);
617                self.dispatch_stage()
618            }
619            Stage::GenerateNocChain => {
620                // Off-wire: build + sign the NOC under the fabric's RCAC.
621                self.run_generate_noc_chain()?;
622                self.advance(Stage::SendTrustedRootCert);
623                self.dispatch_stage()
624            }
625            Stage::SendTrustedRootCert => {
626                use crate::noc::encode_add_trusted_root;
627                // RCAC is already TLV-serialisable via matter-cert's
628                // `to_tlv`. Surfaces as NocError::CertBuild on the rare
629                // re-serialisation failure path (codec / extension shape
630                // regression). Sanity: `FabricRecord::new_root_only` round-
631                // tripped the cert through `verify_signed_by` at
632                // construction, so the bytes are well-formed by here.
633                let rcac_tlv =
634                    self.fabric.root_cert.to_tlv().map_err(|e| {
635                        CommissioningError::from(crate::noc::NocError::CertBuild(e))
636                    })?;
637                #[cfg(feature = "tracing")]
638                tracing::debug!(
639                    rcac_tlv = %crate::hexdump::hex(&rcac_tlv),
640                    "sending AddTrustedRootCertificate"
641                );
642                let payload = encode_add_trusted_root(&rcac_tlv);
643                self.awaiting = Some(Expectation::AddTrustedRootResponse);
644                Ok(Action::Invoke {
645                    session: SessionContext::Pase,
646                    endpoint: 0,
647                    cluster: 0x003E,
648                    command: 0x0B,
649                    payload,
650                    expect: Expectation::AddTrustedRootResponse,
651                })
652            }
653            Stage::SendNoc => {
654                use crate::noc::encode_add_noc;
655                let noc = self
656                    .issued_noc
657                    .as_ref()
658                    .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
659                let noc_tlv = noc
660                    .to_tlv()
661                    .map_err(|e| CommissioningError::from(crate::noc::NocError::CertBuild(e)))?;
662                // If this fabric runs a 3-tier RCAC -> ICAC -> NOC chain, the
663                // device NOC was signed under the ICAC (see
664                // `run_generate_noc_chain` -> `issue_noc`, which signs under
665                // `fabric.icac_signer` whenever the fabric carries one). The
666                // device therefore needs the ICAC certificate to assemble and
667                // validate the chain, so we transmit it in AddNOC's optional
668                // ICACValue field (spec §11.18.5.9 field 1). Flat RCAC -> NOC
669                // fabrics carry no ICAC and the field is omitted, leaving the
670                // M6.3 wire bytes unchanged.
671                let icac_tlv = match self.fabric.icac_cert.as_ref() {
672                    Some(icac) => Some(icac.to_tlv().map_err(|e| {
673                        CommissioningError::from(crate::noc::NocError::CertBuild(e))
674                    })?),
675                    None => None,
676                };
677                let payload = encode_add_noc(
678                    &noc_tlv,
679                    icac_tlv.as_deref(),
680                    &self.ipk_epoch_key,
681                    self.case_admin_subject,
682                    self.admin_vendor_id,
683                );
684                self.awaiting = Some(Expectation::NocResponse);
685                Ok(Action::Invoke {
686                    session: SessionContext::Pase,
687                    endpoint: 0,
688                    cluster: 0x003E,
689                    command: 0x06,
690                    payload,
691                    expect: Expectation::NocResponse,
692                })
693            }
694            Stage::ReadNetworkCommissioningInfo => {
695                self.awaiting = Some(Expectation::NetworkCommissioningInfo);
696                Ok(Action::ReadAttribute {
697                    session: SessionContext::Pase,
698                    endpoint: 0,
699                    cluster: crate::clusters::network_commissioning::CLUSTER_ID,
700                    attributes: &[
701                        crate::clusters::network_commissioning::attribute_id::FEATURE_MAP,
702                        // ConnectMaxTimeSeconds (spec §11.9.5.4) — sizes the
703                        // FailsafeBeforeNetworkEnable extension (D7). Thread
704                        // attach is slower than Wi-Fi association.
705                        crate::clusters::network_commissioning::attribute_id::CONNECT_MAX_TIME_SECONDS,
706                    ],
707                    expect: Expectation::NetworkCommissioningInfo,
708                })
709            }
710            Stage::NetworkSetup => {
711                use crate::clusters::network_commissioning as nc;
712                // Select the provisioning command by the supplied
713                // credential type. The FeatureMap-cross-check at
714                // `Expectation::NetworkCommissioningInfo` guarantees the
715                // device actually supports this network type before we
716                // reach here, so `AlreadyOnNetwork` never lands in this
717                // arm — treat it as an out-of-order state, not a silent
718                // skip.
719                let breadcrumb = self.next_breadcrumb();
720                let (command, payload) = match &self.network {
721                    NetworkCredentials::WiFi(creds) => (
722                        nc::command_id::ADD_OR_UPDATE_WIFI_NETWORK,
723                        nc::encode_add_or_update_wifi_network(
724                            &creds.ssid,
725                            &creds.credentials,
726                            breadcrumb,
727                        ),
728                    ),
729                    NetworkCredentials::Thread(dataset) => (
730                        nc::command_id::ADD_OR_UPDATE_THREAD_NETWORK,
731                        nc::encode_add_or_update_thread_network(dataset.as_bytes(), breadcrumb),
732                    ),
733                    NetworkCredentials::AlreadyOnNetwork => {
734                        return Err(CommissioningError::OutOfOrderResponse(self.stage));
735                    }
736                };
737                self.awaiting = Some(Expectation::NetworkConfigResponse);
738                Ok(Action::Invoke {
739                    session: SessionContext::Pase,
740                    endpoint: 0,
741                    cluster: nc::CLUSTER_ID,
742                    command,
743                    payload,
744                    expect: Expectation::NetworkConfigResponse,
745                })
746            }
747            Stage::NetworkEnable => {
748                use crate::clusters::network_commissioning as nc;
749                // `ConnectNetwork` takes an opaque `network_id`: the SSID
750                // for Wi-Fi, the Extended PAN ID for Thread (spec §11.9.6.6).
751                let breadcrumb = self.next_breadcrumb();
752                let payload = match &self.network {
753                    NetworkCredentials::WiFi(creds) => {
754                        nc::encode_connect_network(&creds.ssid, breadcrumb)
755                    }
756                    NetworkCredentials::Thread(dataset) => {
757                        nc::encode_connect_network(&dataset.ext_pan_id(), breadcrumb)
758                    }
759                    NetworkCredentials::AlreadyOnNetwork => {
760                        return Err(CommissioningError::OutOfOrderResponse(self.stage));
761                    }
762                };
763                self.awaiting = Some(Expectation::ConnectNetworkResponse);
764                Ok(Action::Invoke {
765                    session: SessionContext::Pase,
766                    endpoint: 0,
767                    cluster: nc::CLUSTER_ID,
768                    command: nc::command_id::CONNECT_NETWORK,
769                    payload,
770                    expect: Expectation::ConnectNetworkResponse,
771                })
772            }
773            Stage::EvictPreviousCaseSessions => {
774                // New-fabric commissioning has no prior CASE session
775                // to evict. M8 multi-fabric work will emit
776                // Action::EvictCase here.
777                self.advance(Stage::FindOperationalForComplete);
778                self.dispatch_stage()
779            }
780            Stage::FindOperationalForComplete => {
781                self.awaiting_case_session = true;
782                Ok(Action::EstablishCase {
783                    fabric_id: self.fabric.fabric_id,
784                    peer_node_id: self.assigned_node_id,
785                })
786            }
787            Stage::SendComplete => {
788                let payload = gc::encode_commissioning_complete();
789                self.awaiting = Some(Expectation::CommissioningCompleteResponse);
790                Ok(Action::Invoke {
791                    session: SessionContext::Case,
792                    endpoint: 0,
793                    cluster: gc::CLUSTER_ID,
794                    command: gc::command_id::COMMISSIONING_COMPLETE,
795                    payload,
796                    expect: Expectation::CommissioningCompleteResponse,
797                })
798            }
799            Stage::Cleanup => {
800                let public_key = self
801                    .issued_noc_public_key
802                    .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
803                Ok(Action::Done(crate::state_machine::CommissionedFabric {
804                    fabric: self.fabric.clone(),
805                    peer_node_id: self.assigned_node_id,
806                    peer_root_public_key: public_key,
807                    terminated_at: Stage::Cleanup,
808                }))
809            }
810            Stage::Failed => {
811                // Subsequent poll() after a failure surfaces the Abort.
812                // The state machine stays in Failed.
813                self.awaiting = None;
814                let reason = self
815                    .last_failure
816                    .clone()
817                    .unwrap_or_else(|| "commissioning aborted".to_string());
818                Ok(Action::Abort {
819                    send_disarm_failsafe: true,
820                    reason,
821                })
822            } // Every `Stage` variant has its own arm above. `Stage` is
823              // `#[non_exhaustive]` for cross-crate consumers, but within
824              // this crate the match is exhaustive — no `_ =>` arm needed.
825        }
826    }
827
828    /// Feed a response payload back into the state machine.
829    ///
830    /// `expect` MUST match the [`Expectation`] from the last `poll()`'s
831    /// emitted `Action`.
832    ///
833    /// # Errors
834    ///
835    /// - [`CommissioningError::OutOfOrderResponse`] if the state machine
836    ///   isn't currently waiting for a response.
837    /// - [`CommissioningError::UnexpectedResponseKind`] if `expect`
838    ///   doesn't match the last `Action`'s `Expectation`. The cursor
839    ///   does not advance.
840    /// - [`CommissioningError::MalformedResponse`] if `payload` fails
841    ///   to decode at the cluster-command level.
842    /// - [`CommissioningError::DeviceImStatus`] if the device returned a
843    ///   non-OK Interaction Model status.
844    ///
845    /// Any error other than `OutOfOrderResponse` and
846    /// `UnexpectedResponseKind` transitions the cursor to
847    /// [`Stage::Failed`]; the next `poll()` call emits
848    /// [`Action::Abort`] with a rendered summary.
849    #[cfg_attr(feature = "tracing", instrument(skip(self, payload), fields(stage = ?self.stage, expectation = ?expect)))]
850    pub fn on_response(
851        &mut self,
852        expect: Expectation,
853        payload: &[u8],
854    ) -> Result<(), CommissioningError> {
855        if expect == Expectation::CaseFailed {
856            // CaseFailed bypasses the awaiting check — the caller
857            // signals failure of the EstablishCase action explicitly,
858            // and EstablishCase tracks readiness via
859            // `awaiting_case_session`, not `awaiting`.
860            if !self.awaiting_case_session {
861                return Err(CommissioningError::OutOfOrderResponse(self.stage));
862            }
863            self.awaiting_case_session = false;
864            self.stage = Stage::Failed;
865            self.awaiting = None;
866            self.pending_action = None;
867            self.last_failure = Some(CommissioningError::CaseEstablishmentFailed.to_string());
868            return Err(CommissioningError::CaseEstablishmentFailed);
869        }
870        let Some(awaiting) = self.awaiting else {
871            return Err(CommissioningError::OutOfOrderResponse(self.stage));
872        };
873        if awaiting != expect {
874            return Err(CommissioningError::UnexpectedResponseKind {
875                expected: awaiting,
876                got: expect,
877            });
878        }
879        match self.handle_response(expect, payload) {
880            Ok(()) => Ok(()),
881            Err(err) => {
882                self.last_failure = Some(err.to_string());
883                self.stage = Stage::Failed;
884                self.awaiting = None;
885                self.pending_action = None;
886                Err(err)
887            }
888        }
889    }
890
891    /// Signal that CASE establishment (mDNS find-operational + the
892    /// SIGMA-I handshake — both M6.6 mechanics, owned by the driver)
893    /// has succeeded. The state machine advances from
894    /// [`Stage::FindOperationalForComplete`] to [`Stage::SendComplete`].
895    ///
896    /// # Errors
897    ///
898    /// Returns [`CommissioningError::OutOfOrderResponse`] if the state
899    /// machine isn't currently awaiting CASE establishment (i.e., the
900    /// cursor is not at `FindOperationalForComplete` or the
901    /// `EstablishCase` action hasn't been emitted yet).
902    #[cfg_attr(feature = "tracing", instrument(skip(self)))]
903    pub fn on_case_established(&mut self) -> Result<(), CommissioningError> {
904        if !self.awaiting_case_session {
905            return Err(CommissioningError::OutOfOrderResponse(self.stage));
906        }
907        self.awaiting_case_session = false;
908        self.advance(Stage::SendComplete);
909        Ok(())
910    }
911
912    #[allow(clippy::too_many_lines)]
913    fn handle_response(
914        &mut self,
915        expect: Expectation,
916        payload: &[u8],
917    ) -> Result<(), CommissioningError> {
918        use crate::clusters::general_commissioning as gc;
919        match expect {
920            Expectation::CommissioningInfo => {
921                Self::assert_tlv_well_formed(self.stage, payload)?;
922                // Best-effort: scan the response for a BasicCommissioningInfo
923                // struct and update failsafe_expiry_seconds. Malformed or
924                // missing → keep the M6.4 fallback (60s) silently.
925                if let Some(info) = gc::decode_basic_commissioning_info(payload) {
926                    if info.failsafe_expiry_length_seconds > 0 {
927                        self.failsafe_expiry_seconds = info.failsafe_expiry_length_seconds;
928                    }
929                    // Cap against the device's hard cumulative failsafe limit
930                    // (Matter §11.10.5.1): an `ArmFailSafe` whose expiry exceeds
931                    // `MaxCumulativeFailsafeSeconds` is guaranteed to be rejected
932                    // (BoundsExceeded), so never round-trip such a value.
933                    if info.max_cumulative_failsafe_seconds > 0 {
934                        self.failsafe_expiry_seconds = self
935                            .failsafe_expiry_seconds
936                            .min(info.max_cumulative_failsafe_seconds);
937                    }
938                }
939                self.advance(Stage::ArmFailsafe);
940                Ok(())
941            }
942            Expectation::ArmFailsafeResponse => {
943                let resp = gc::decode_arm_fail_safe_response(payload)?;
944                if resp.error_code != 0 {
945                    return Err(CommissioningError::DeviceImStatus {
946                        stage: self.stage,
947                        im_status: u16::from(resp.error_code),
948                    });
949                }
950                let next = match self.stage {
951                    Stage::ArmFailsafe => Stage::ConfigRegulatory,
952                    Stage::FailsafeBeforeNetworkEnable => Stage::NetworkEnable,
953                    other => {
954                        return Err(CommissioningError::OutOfOrderResponse(other));
955                    }
956                };
957                self.advance(next);
958                Ok(())
959            }
960            Expectation::SetRegulatoryConfigResponse => {
961                let resp = gc::decode_set_regulatory_config_response(payload)?;
962                if resp.error_code != 0 {
963                    return Err(CommissioningError::DeviceImStatus {
964                        stage: Stage::ConfigRegulatory,
965                        im_status: u16::from(resp.error_code),
966                    });
967                }
968                self.advance(Stage::SendPaiCertRequest);
969                Ok(())
970            }
971            Expectation::PaiCertChainResponse => {
972                let resp = crate::noc::decode_certificate_chain_response(payload)?;
973                self.pai_der = Some(resp.certificate);
974                self.advance(Stage::SendDacCertRequest);
975                Ok(())
976            }
977            Expectation::DacCertChainResponse => {
978                let resp = crate::noc::decode_certificate_chain_response(payload)?;
979                self.dac_der = Some(resp.certificate);
980                self.advance(Stage::SendAttestationRequest);
981                Ok(())
982            }
983            Expectation::AttestationResponse => {
984                let resp = crate::noc::decode_attestation_response(payload)?;
985                self.attestation_response = Some(resp);
986                self.advance(Stage::AttestationVerification);
987                Ok(())
988            }
989            Expectation::CsrResponse => {
990                let resp = crate::noc::decode_csr_response(payload)?;
991                self.csr_response = Some(resp);
992                self.advance(Stage::ValidateCsr);
993                Ok(())
994            }
995            Expectation::AddTrustedRootResponse => {
996                // `AddTrustedRootCertificate` has no typed response —
997                // success is a status-only ack at the Interaction Model
998                // layer. The caller surfaces the IM status as a 1-byte
999                // payload: `0x00` = success, anything else = error.
1000                if payload.first() != Some(&0u8) {
1001                    return Err(CommissioningError::DeviceImStatus {
1002                        stage: Stage::SendTrustedRootCert,
1003                        im_status: u16::from(payload.first().copied().unwrap_or(0xFF)),
1004                    });
1005                }
1006                self.advance(Stage::SendNoc);
1007                Ok(())
1008            }
1009            Expectation::NocResponse => {
1010                let resp = crate::noc::decode_noc_response(payload)?;
1011                if resp.status != 0 {
1012                    return Err(CommissioningError::DeviceImStatus {
1013                        stage: Stage::SendNoc,
1014                        im_status: u16::from(resp.status),
1015                    });
1016                }
1017                self.advance(Stage::ReadNetworkCommissioningInfo);
1018                Ok(())
1019            }
1020            Expectation::NetworkCommissioningInfo => {
1021                use crate::clusters::network_commissioning as nc;
1022                use crate::state_machine::NetworkKind;
1023                let features = nc::decode_feature_map(payload)?;
1024                // A FeatureMap with no recognised interface bit is
1025                // malformed regardless of the supplied credentials — a
1026                // NetworkCommissioning cluster always exposes at least one
1027                // of Wi-Fi / Thread / Ethernet.
1028                if features.is_empty() {
1029                    return Err(CommissioningError::MalformedResponse(
1030                        Stage::ReadNetworkCommissioningInfo,
1031                    ));
1032                }
1033                // Route by the *supplied* credential type, cross-checked
1034                // against the device FeatureMap. This resolves the
1035                // dual-stack ordering ambiguity (a Wi-Fi+Thread device is
1036                // provisioned per the caller's chosen credential type, not
1037                // by feature-bit order) and rejects a mismatch (credential
1038                // type absent from the FeatureMap) instead of silently
1039                // skipping provisioning.
1040                match &self.network {
1041                    NetworkCredentials::WiFi(_) => {
1042                        if !features.contains(nc::NetworkCommissioningFeature::WIFI) {
1043                            return Err(CommissioningError::NetworkFeatureUnsupported {
1044                                needed: NetworkKind::WiFi,
1045                            });
1046                        }
1047                        self.advance(Stage::NetworkSetup);
1048                    }
1049                    NetworkCredentials::Thread(_) => {
1050                        if !features.contains(nc::NetworkCommissioningFeature::THREAD) {
1051                            return Err(CommissioningError::NetworkFeatureUnsupported {
1052                                needed: NetworkKind::Thread,
1053                            });
1054                        }
1055                        self.advance(Stage::NetworkSetup);
1056                    }
1057                    NetworkCredentials::AlreadyOnNetwork => {
1058                        // No credentials to provision: the device is
1059                        // already reachable on its operational network (IP
1060                        // commissioning reached it there — e.g. a
1061                        // second-fabric commission of an already-provisioned
1062                        // device, or an Ethernet-only device). Mirror
1063                        // chip's AutoCommissioner: skip the network
1064                        // sub-cursor entirely (observed necessary on a real
1065                        // device: Tapo P110M, M6.6.5 validation).
1066                        self.advance(Stage::EvictPreviousCaseSessions);
1067                    }
1068                }
1069                Ok(())
1070            }
1071            Expectation::NetworkConfigResponse => {
1072                use crate::clusters::network_commissioning as nc;
1073                let resp = nc::decode_network_config_response(Stage::NetworkSetup, payload)?;
1074                if resp.networking_status != 0 {
1075                    return Err(CommissioningError::NetworkRejected {
1076                        stage: Stage::NetworkSetup,
1077                        networking_status: resp.networking_status,
1078                        debug_text: resp.debug_text,
1079                        remediation_hint: nc::remediation_for(resp.networking_status),
1080                    });
1081                }
1082                self.advance(Stage::FailsafeBeforeNetworkEnable);
1083                Ok(())
1084            }
1085            Expectation::ConnectNetworkResponse => {
1086                use crate::clusters::network_commissioning as nc;
1087                let resp = nc::decode_connect_network_response(Stage::NetworkEnable, payload)?;
1088                if resp.networking_status != 0 {
1089                    return Err(CommissioningError::NetworkRejected {
1090                        stage: Stage::NetworkEnable,
1091                        networking_status: resp.networking_status,
1092                        debug_text: resp.debug_text,
1093                        remediation_hint: nc::remediation_for(resp.networking_status),
1094                    });
1095                }
1096                self.advance(Stage::EvictPreviousCaseSessions);
1097                Ok(())
1098            }
1099            Expectation::CommissioningCompleteResponse => {
1100                let (error_code, _debug) =
1101                    gc::decode_commissioning_error_response(Stage::SendComplete, payload)?;
1102                if error_code != 0 {
1103                    return Err(CommissioningError::DeviceImStatus {
1104                        stage: Stage::SendComplete,
1105                        im_status: u16::from(error_code),
1106                    });
1107                }
1108                self.advance(Stage::Cleanup);
1109                Ok(())
1110            }
1111            // `Expectation::CaseFailed` is handled by `on_response`'s
1112            // pre-awaiting fast path and never reaches handle_response.
1113            _ => Err(CommissioningError::OutOfOrderResponse(self.stage)),
1114        }
1115    }
1116
1117    fn next_breadcrumb(&mut self) -> u64 {
1118        let b = self.breadcrumb_counter;
1119        self.breadcrumb_counter = b.saturating_add(1);
1120        b
1121    }
1122
1123    fn advance(&mut self, next: Stage) {
1124        self.stage = next;
1125        self.awaiting = None;
1126        self.pending_action = None;
1127    }
1128
1129    /// Off-wire attestation verification chain (M6.4.2 T21).
1130    ///
1131    /// Consumes the PAI/DAC DER + `AttestationResponse` + nonce captured
1132    /// by [`Stage::SendPaiCertRequest`] / [`Stage::SendDacCertRequest`]
1133    /// / [`Stage::SendAttestationRequest`] and runs M6.2's verifier
1134    /// chain end-to-end:
1135    ///
1136    /// 1. Parse PAI/DAC DER.
1137    /// 2. `verify_chain` — webpki path validation + Matter VID/PID
1138    ///    overlay (M6.2.2).
1139    /// 3. `verify_attestation_response` — ECDSA signature over
1140    ///    `attestation_elements || attestation_challenge` (M6.2.3).
1141    /// 4. `extract_attestation_elements_fields` — pull the
1142    ///    `attestation_nonce` echo + CD bytes out of the TLV blob.
1143    /// 5. Confirm the device echoed the nonce we sent.
1144    /// 6. `verify_certification_declaration` — verify the CSA-signed CD
1145    ///    embedded in `attestation_elements` against
1146    ///    [`crate::attestation::CdSigningRoots`] and confirm the
1147    ///    declared VID/PID match what the DAC subject claimed.
1148    fn run_attestation_verification(&mut self) -> Result<(), CommissioningError> {
1149        use crate::attestation::profile::{verify_attestation_cert_format, CertRole};
1150        use crate::attestation::{
1151            extract_attestation_elements_fields, verify_attestation_response, verify_chain,
1152            AttestationError, Dac, Pai,
1153        };
1154
1155        let pai_der = self
1156            .pai_der
1157            .as_ref()
1158            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1159        let dac_der = self
1160            .dac_der
1161            .as_ref()
1162            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1163        let response = self
1164            .attestation_response
1165            .as_ref()
1166            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1167        let expected_nonce = self
1168            .attestation_nonce
1169            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1170
1171        // 1. Parse chain certs.
1172        let pai = Pai::from_der(pai_der)?;
1173        let dac = Dac::from_der(dac_der)?;
1174        #[cfg(feature = "tracing")]
1175        tracing::debug!(
1176            dac_der = %crate::hexdump::hex(dac_der),
1177            pai_der = %crate::hexdump::hex(pai_der),
1178            "verifying attestation chain"
1179        );
1180
1181        // 1b. Certificate format profile (Matter §6.2.2) — mirrors chip's
1182        //     `VerifyAttestationCertificateFormat`. `rustls-webpki` ignores
1183        //     the KeyUsage extension and never requires SKID/AKID, so a
1184        //     counterfeit DAC carrying `keyUsage = keyCertSign` (a signing
1185        //     key posing as a device leaf) would pass path validation. We
1186        //     enforce the DAC/PAI X.509 profile ourselves, before trusting
1187        //     the chain. PAI first, then DAC — chip's order.
1188        verify_attestation_cert_format(pai.der(), CertRole::Pai)?;
1189        verify_attestation_cert_format(dac.der(), CertRole::Dac)?;
1190
1191        // 2. Chain validation (M6.2.2 — webpki path validation + VID/PID overlay).
1192        //    The returned `ChainVerification` carries the VID/PID that
1193        //    both webpki and the Matter overlay agreed on; we re-use
1194        //    those for the CD check below so a single source of truth
1195        //    drives both the chain validation and the CD VID/PID
1196        //    equality check.
1197        let chain = verify_chain(&dac, &pai, &self.paa_trust_store, self.now)?;
1198
1199        // 3. AttestationResponse signature (M6.2.3).
1200        verify_attestation_response(response, &self.pase_attestation_challenge, dac.public_key())?;
1201
1202        // 4. Extract attestation_elements fields: CD bytes (M6.4.3 will verify),
1203        //    nonce echo, timestamp.
1204        let fields = extract_attestation_elements_fields(&response.attestation_elements)?;
1205        if fields.attestation_nonce != expected_nonce {
1206            return Err(CommissioningError::Attestation(
1207                AttestationError::ResponseElementsMalformed,
1208            ));
1209        }
1210
1211        // 5. CD verification — verify the device's declared VID/PID
1212        //    against the CSA-signed Certification Declaration extracted
1213        //    from `attestation_elements`.
1214        #[cfg(feature = "tracing")]
1215        tracing::debug!(
1216            cd_cms = %crate::hexdump::hex(&fields.certification_declaration),
1217            "verifying certification declaration"
1218        );
1219        crate::attestation::verify_certification_declaration_with_paa(
1220            &fields.certification_declaration,
1221            chain.vendor_id,
1222            chain.product_id,
1223            &self.cd_signing_roots,
1224            chain.paa_skid.as_deref(),
1225        )?;
1226        Ok(())
1227    }
1228
1229    /// Off-wire CSR verification (M6.4.4 `Stage::ValidateCsr`).
1230    ///
1231    /// Consumes the `CsrResponse` captured by `Stage::SendOpCertSigningRequest`
1232    /// plus the DAC DER captured earlier by `Stage::SendDacCertRequest`,
1233    /// and runs M6.3's `verify_csr_response` three-check atomic gate:
1234    ///
1235    /// 1. PKCS#10 self-signature on the embedded CSR.
1236    /// 2. The device's `CSRNonce` echo equals the commissioner-issued nonce.
1237    /// 3. The DAC's attestation signature over
1238    ///    `nocsr_elements || attestation_challenge`.
1239    fn run_validate_csr(&mut self) -> Result<(), CommissioningError> {
1240        use crate::attestation::Dac;
1241        use crate::noc::verify_csr_response;
1242
1243        let resp = self
1244            .csr_response
1245            .as_ref()
1246            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1247        let dac_der = self
1248            .dac_der
1249            .as_ref()
1250            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1251        let csr_nonce = self
1252            .csr_nonce
1253            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1254
1255        let dac = Dac::from_der(dac_der)?;
1256        let verified = verify_csr_response(
1257            &resp.nocsr_elements,
1258            &resp.attestation_signature,
1259            &csr_nonce,
1260            &self.pase_attestation_challenge,
1261            dac.public_key(),
1262        )?;
1263        self.verified_csr = Some(verified);
1264        Ok(())
1265    }
1266
1267    /// Off-wire NOC issuance (M6.4.4 `Stage::GenerateNocChain`).
1268    ///
1269    /// Consumes the [`crate::noc::VerifiedCsr`] populated by
1270    /// [`Self::run_validate_csr`] and mints a NOC signed by the fabric's
1271    /// RCAC via M6.3's `issue_noc`.
1272    ///
1273    /// Validity window: M6.4 uses `(self.now, MatterTime::NO_EXPIRY)` —
1274    /// the same convention `issue_noc`'s own unit test uses. M8 may
1275    /// tighten this to a bounded operational-cert lifetime per Matter
1276    /// Core Spec §6.4 once persistence + rotation policy lands.
1277    ///
1278    /// CATs (CASE Authenticated Tags) are empty in M6.4; tag-based
1279    /// access control comes later.
1280    fn run_generate_noc_chain(&mut self) -> Result<(), CommissioningError> {
1281        use crate::noc::issue_noc;
1282
1283        let verified = self
1284            .verified_csr
1285            .as_ref()
1286            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1287        let noc = issue_noc(
1288            &self.fabric,
1289            verified,
1290            self.assigned_node_id,
1291            &[],
1292            (self.now, MatterTime::NO_EXPIRY),
1293            self.rng.as_ref(),
1294        )?;
1295        // Cache the NOC public key (the same bytes the verified CSR
1296        // committed to) for later use — currently only consumed by
1297        // M6.4.5's PASE -> CASE handoff in `CommissionedFabric`. Stored
1298        // here so the SendNoc stage doesn't have to re-derive it.
1299        self.issued_noc_public_key = Some(*verified.public_key.as_bytes());
1300        self.issued_noc = Some(noc);
1301        Ok(())
1302    }
1303
1304    fn assert_tlv_well_formed(stage: Stage, payload: &[u8]) -> Result<(), CommissioningError> {
1305        use matter_codec::{ContainerKind, Element, Tag, TlvReader};
1306        let mut reader = TlvReader::new(payload);
1307        match reader
1308            .next()
1309            .map_err(|_| CommissioningError::MalformedResponse(stage))?
1310        {
1311            Some(Element::ContainerStart {
1312                tag: Tag::Anonymous,
1313                kind: ContainerKind::Structure,
1314            }) => {}
1315            _ => return Err(CommissioningError::MalformedResponse(stage)),
1316        }
1317        // Walk to ContainerEnd; ignore contents for M6.4.1.
1318        loop {
1319            match reader
1320                .next()
1321                .map_err(|_| CommissioningError::MalformedResponse(stage))?
1322            {
1323                None => return Err(CommissioningError::MalformedResponse(stage)),
1324                Some(Element::ContainerEnd) => return Ok(()),
1325                Some(_) => {}
1326            }
1327        }
1328    }
1329}
1330
1331/// Test-only state seeds for [`Commissioner::position_at_stage_for_test`].
1332///
1333/// Each field is `None` by default — the caller opts in to each seed
1334/// explicitly. **Never use in production.**
1335#[cfg(feature = "__test_shortcuts")]
1336#[derive(Default, Debug, Clone, Copy)]
1337pub struct TestStateSeeds {
1338    /// Override `issued_noc_public_key` (normally populated when the
1339    /// state machine actually issues a NOC). Set to a synthetic SEC1-
1340    /// uncompressed P-256 byte pattern (e.g. `[0xCC; 65]`) when
1341    /// fast-forwarding past the NOC-issuance stages.
1342    pub synthetic_noc_pubkey: Option<[u8; 65]>,
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347    // Test-code carve-out: see CLAUDE.md.
1348    #![allow(clippy::unwrap_used, clippy::expect_used)]
1349
1350    use super::*;
1351    use crate::attestation::CdSigningRoots;
1352    use crate::noc::{FabricRecord, NocRng, SystemNocRng};
1353    use crate::setup::{
1354        CommissioningFlow, DiscoveryCapabilities, Discriminator, Passcode, SetupPayload,
1355    };
1356    use crate::state_machine::{Action, Expectation};
1357    use crate::PaaTrustStore;
1358    use matter_cert::time::MatterTime;
1359    use matter_crypto::{RingSigner, Signer};
1360    use std::sync::Arc;
1361
1362    fn make_setup_payload() -> SetupPayload {
1363        SetupPayload {
1364            version: 0,
1365            vendor_id: Some(0xFFF1),
1366            product_id: Some(0x8000),
1367            commissioning_flow: CommissioningFlow::Standard,
1368            discovery_capabilities: DiscoveryCapabilities::ON_NETWORK,
1369            discriminator: Discriminator::new(0x0F00).expect("valid discriminator"),
1370            passcode: Passcode::new(20_202_021).expect("valid passcode"),
1371        }
1372    }
1373
1374    fn make_fabric_record() -> FabricRecord {
1375        let (signer, _pkcs8) = RingSigner::generate().unwrap();
1376        let signer: Arc<dyn Signer> = Arc::new(signer);
1377        FabricRecord::new_root_only(
1378            /* fabric_id */ 0x0000_0000_0000_0001,
1379            signer,
1380            /* not_before */ MatterTime::from_unix_secs(1_704_067_200),
1381            /* not_after */ MatterTime::from_unix_secs(1_735_689_600),
1382            /* rcac_id */ 42,
1383            &SystemNocRng,
1384        )
1385        .unwrap()
1386    }
1387
1388    /// A fabric that issues a 3-tier RCAC -> ICAC -> NOC chain: the RCAC from
1389    /// [`make_fabric_record`] with a freshly-minted ICAC attached (so
1390    /// `issue_noc` signs the device NOC under the ICAC and `SendNoc` must
1391    /// transmit the ICAC alongside it).
1392    fn make_fabric_record_with_icac() -> FabricRecord {
1393        let mut fabric = make_fabric_record();
1394        let (icac_signer, _pkcs8) = RingSigner::generate().unwrap();
1395        let icac_public_key =
1396            matter_cert::PublicKey::from_slice(icac_signer.public_key().as_bytes()).unwrap();
1397        let icac = crate::noc::issue_icac(
1398            &fabric,
1399            /* icac_id */ 7,
1400            &icac_public_key,
1401            (
1402                MatterTime::from_unix_secs(1_704_067_200),
1403                MatterTime::NO_EXPIRY,
1404            ),
1405            &SystemNocRng,
1406        )
1407        .unwrap();
1408        fabric.icac_signer = Some(Arc::new(icac_signer));
1409        fabric.icac_cert = Some(icac);
1410        fabric
1411    }
1412
1413    /// True iff the encoded `AddNOC` payload carries an `ICACValue` (field 1,
1414    /// spec §11.18.5.9). Scans the top-level struct members for a
1415    /// context-1 element; every `AddNOC` field is a scalar/byte-string, so
1416    /// no descent past the outer container is needed.
1417    fn add_noc_payload_has_icac(payload: &[u8]) -> bool {
1418        use matter_codec::{Element, Tag, TlvReader};
1419        let mut r = TlvReader::new(payload);
1420        match r.next().unwrap() {
1421            Some(Element::ContainerStart { .. }) => {}
1422            other => panic!("expected an AddNOC struct, got {other:?}"),
1423        }
1424        while let Some(el) = r.next().unwrap() {
1425            match el {
1426                Element::ContainerEnd => break,
1427                Element::Scalar {
1428                    tag: Tag::Context(1),
1429                    ..
1430                } => return true,
1431                _ => {}
1432            }
1433        }
1434        false
1435    }
1436
1437    #[test]
1438    fn send_noc_includes_icac_for_three_tier_fabric() {
1439        let fabric = make_fabric_record_with_icac();
1440        let setup = make_setup_payload();
1441        let paa = PaaTrustStore::with_example_device_roots();
1442        let cd = CdSigningRoots::with_example_device_roots();
1443        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1444        let mut sm =
1445            Commissioner::new(base_config(&fabric, &setup, &paa, &cd, rng)).expect("valid config");
1446        // `SendNoc` reads `issued_noc` + `fabric.icac_cert`; the NOC bytes are
1447        // irrelevant to whether the ICAC is attached, so stub the NOC and jump
1448        // straight to the stage under test.
1449        sm.issued_noc = Some(fabric.root_cert.clone());
1450        sm.stage = Stage::SendNoc;
1451        match sm.dispatch_stage().expect("SendNoc dispatch") {
1452            Action::Invoke {
1453                cluster,
1454                command,
1455                payload,
1456                ..
1457            } => {
1458                assert_eq!(cluster, 0x003E);
1459                assert_eq!(command, 0x06);
1460                assert!(
1461                    add_noc_payload_has_icac(&payload),
1462                    "AddNOC must carry the ICAC (ctx1) when the fabric runs a 3-tier chain"
1463                );
1464            }
1465            other => panic!("expected Invoke, got {other:?}"),
1466        }
1467    }
1468
1469    #[test]
1470    fn send_noc_omits_icac_for_flat_fabric() {
1471        let fabric = make_fabric_record();
1472        let setup = make_setup_payload();
1473        let paa = PaaTrustStore::with_example_device_roots();
1474        let cd = CdSigningRoots::with_example_device_roots();
1475        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1476        let mut sm =
1477            Commissioner::new(base_config(&fabric, &setup, &paa, &cd, rng)).expect("valid config");
1478        sm.issued_noc = Some(fabric.root_cert.clone());
1479        sm.stage = Stage::SendNoc;
1480        match sm.dispatch_stage().expect("SendNoc dispatch") {
1481            Action::Invoke { payload, .. } => {
1482                assert!(
1483                    !add_noc_payload_has_icac(&payload),
1484                    "flat RCAC->NOC AddNOC must omit the ICAC field"
1485                );
1486            }
1487            other => panic!("expected Invoke, got {other:?}"),
1488        }
1489    }
1490
1491    fn base_config<'a>(
1492        fabric: &'a FabricRecord,
1493        setup: &'a SetupPayload,
1494        paa: &'a PaaTrustStore,
1495        cd: &'a crate::attestation::CdSigningRoots,
1496        rng: Arc<dyn NocRng>,
1497    ) -> CommissionerConfig<'a> {
1498        CommissionerConfig {
1499            pase_attestation_challenge: [0u8; 16],
1500            fabric,
1501            setup_payload: setup,
1502            paa_trust_store: paa,
1503            cd_signing_roots: cd,
1504            commissioner_node_id: 0x1,
1505            assigned_node_id: 0x2,
1506            ipk_epoch_key: [0x42_u8; 16],
1507            case_admin_subject: 0x1,
1508            admin_vendor_id: 0xFFF1,
1509            now: MatterTime::from_unix_secs(1_704_067_200),
1510            rng,
1511            network: NetworkCredentials::AlreadyOnNetwork,
1512        }
1513    }
1514
1515    #[test]
1516    fn new_rejects_zero_commissioner_node_id() {
1517        let fabric = make_fabric_record();
1518        let setup = make_setup_payload();
1519        let paa = PaaTrustStore::with_example_device_roots();
1520        let cd = CdSigningRoots::with_example_device_roots();
1521        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1522        let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1523        cfg.commissioner_node_id = 0;
1524        // Cannot use `expect_err`: `Commissioner` does not impl Debug
1525        // because `FabricRecord` (a stored field) is not Debug.
1526        let Err(err) = Commissioner::new(cfg) else {
1527            panic!("zero commissioner_node_id should fail");
1528        };
1529        assert!(
1530            matches!(err, CommissioningError::InvalidConfig(_)),
1531            "got {err:?}"
1532        );
1533    }
1534
1535    #[test]
1536    fn new_rejects_zero_assigned_node_id() {
1537        let fabric = make_fabric_record();
1538        let setup = make_setup_payload();
1539        let paa = PaaTrustStore::with_example_device_roots();
1540        let cd = CdSigningRoots::with_example_device_roots();
1541        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1542        let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1543        cfg.assigned_node_id = 0;
1544        let Err(err) = Commissioner::new(cfg) else {
1545            panic!("zero assigned_node_id should fail");
1546        };
1547        assert!(
1548            matches!(err, CommissioningError::InvalidConfig(_)),
1549            "got {err:?}"
1550        );
1551    }
1552
1553    #[test]
1554    fn new_rejects_equal_commissioner_and_assigned_ids() {
1555        let fabric = make_fabric_record();
1556        let setup = make_setup_payload();
1557        let paa = PaaTrustStore::with_example_device_roots();
1558        let cd = CdSigningRoots::with_example_device_roots();
1559        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1560        let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1561        cfg.commissioner_node_id = 0x42;
1562        cfg.assigned_node_id = 0x42;
1563        let Err(err) = Commissioner::new(cfg) else {
1564            panic!("equal IDs should fail");
1565        };
1566        assert!(
1567            matches!(err, CommissioningError::InvalidConfig(_)),
1568            "got {err:?}"
1569        );
1570    }
1571
1572    #[test]
1573    fn new_rejects_zero_ipk_epoch_key() {
1574        let fabric = make_fabric_record();
1575        let setup = make_setup_payload();
1576        let paa = PaaTrustStore::with_example_device_roots();
1577        let cd = CdSigningRoots::with_example_device_roots();
1578        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1579        let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1580        cfg.ipk_epoch_key = [0u8; 16];
1581        let Err(err) = Commissioner::new(cfg) else {
1582            panic!("zero IPK should fail");
1583        };
1584        assert!(
1585            matches!(err, CommissioningError::InvalidConfig(_)),
1586            "got {err:?}"
1587        );
1588    }
1589
1590    #[test]
1591    fn new_returns_secure_pairing_stage() {
1592        let fabric = make_fabric_record();
1593        let setup = make_setup_payload();
1594        let paa = PaaTrustStore::with_example_device_roots();
1595        let cd = CdSigningRoots::with_example_device_roots();
1596        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1597        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1598        let sm = Commissioner::new(cfg).expect("valid config should construct");
1599        assert_eq!(sm.stage(), Stage::SecurePairing);
1600    }
1601
1602    #[test]
1603    fn poll_from_secure_pairing_emits_read_commissioning_info() {
1604        let fabric = make_fabric_record();
1605        let setup = make_setup_payload();
1606        let paa = PaaTrustStore::with_example_device_roots();
1607        let cd = CdSigningRoots::with_example_device_roots();
1608        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1609        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1610        let mut sm = Commissioner::new(cfg).expect("valid config");
1611        let act = sm.poll().expect("poll succeeds");
1612        match act {
1613            Action::ReadAttribute {
1614                session,
1615                endpoint,
1616                cluster,
1617                attributes,
1618                expect,
1619            } => {
1620                assert_eq!(session, crate::state_machine::SessionContext::Pase);
1621                assert_eq!(endpoint, 0);
1622                assert_eq!(cluster, 0x0030);
1623                assert_eq!(expect, Expectation::CommissioningInfo);
1624                assert!(!attributes.is_empty());
1625            }
1626            other => panic!("expected ReadAttribute, got {other:?}"),
1627        }
1628        assert_eq!(sm.stage(), Stage::ReadCommissioningInfo);
1629    }
1630
1631    #[test]
1632    fn poll_is_idempotent_between_responses() {
1633        let fabric = make_fabric_record();
1634        let setup = make_setup_payload();
1635        let paa = PaaTrustStore::with_example_device_roots();
1636        let cd = CdSigningRoots::with_example_device_roots();
1637        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1638        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1639        let mut sm = Commissioner::new(cfg).expect("valid config");
1640        let act1 = sm.poll().expect("first poll");
1641        let act2 = sm.poll().expect("second poll");
1642        match (act1, act2) {
1643            (
1644                Action::ReadAttribute {
1645                    cluster: c1,
1646                    expect: e1,
1647                    ..
1648                },
1649                Action::ReadAttribute {
1650                    cluster: c2,
1651                    expect: e2,
1652                    ..
1653                },
1654            ) => {
1655                assert_eq!(c1, c2);
1656                assert_eq!(e1, e2);
1657            }
1658            other => panic!("idempotent poll returned different variants: {other:?}"),
1659        }
1660    }
1661
1662    #[test]
1663    fn full_happy_path_through_config_regulatory_lands_on_send_pai_cert_request() {
1664        let fabric = make_fabric_record();
1665        let setup = make_setup_payload();
1666        let paa = PaaTrustStore::with_example_device_roots();
1667        let cd = CdSigningRoots::with_example_device_roots();
1668        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1669        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1670        let mut sm = Commissioner::new(cfg).expect("valid config");
1671
1672        // SecurePairing → ReadCommissioningInfo
1673        let _ = sm.poll().expect("poll #1");
1674        let canned_info = encode_read_commissioning_info_response();
1675        sm.on_response(Expectation::CommissioningInfo, &canned_info)
1676            .expect("commissioning info accepted");
1677        assert_eq!(sm.stage(), Stage::ArmFailsafe);
1678
1679        // ArmFailsafe
1680        let _ = sm.poll().expect("poll #2");
1681        sm.on_response(
1682            Expectation::ArmFailsafeResponse,
1683            &[0x15, 0x24, 0x00, 0x00, 0x18],
1684        )
1685        .expect("arm failsafe ok");
1686        assert_eq!(sm.stage(), Stage::ConfigRegulatory);
1687
1688        // ConfigRegulatory
1689        let _ = sm.poll().expect("poll #3");
1690        sm.on_response(
1691            Expectation::SetRegulatoryConfigResponse,
1692            &[0x15, 0x24, 0x00, 0x00, 0x18],
1693        )
1694        .expect("config regulatory ok");
1695        assert_eq!(sm.stage(), Stage::SendPaiCertRequest);
1696
1697        // M6.4.2: SendPaiCertRequest now actually emits an Invoke.
1698        match sm.poll().expect("poll #4") {
1699            Action::Invoke {
1700                cluster,
1701                command,
1702                expect,
1703                ..
1704            } => {
1705                assert_eq!(cluster, 0x003E);
1706                assert_eq!(command, 0x02);
1707                assert_eq!(expect, Expectation::PaiCertChainResponse);
1708            }
1709            other => panic!("expected Invoke, got {other:?}"),
1710        }
1711    }
1712
1713    #[test]
1714    fn arm_failsafe_busy_response_aborts_with_device_im_status() {
1715        let fabric = make_fabric_record();
1716        let setup = make_setup_payload();
1717        let paa = PaaTrustStore::with_example_device_roots();
1718        let cd = CdSigningRoots::with_example_device_roots();
1719        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1720        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1721        let mut sm = Commissioner::new(cfg).expect("valid config");
1722        let _ = sm.poll().expect("poll info");
1723        sm.on_response(
1724            Expectation::CommissioningInfo,
1725            &encode_read_commissioning_info_response(),
1726        )
1727        .expect("commissioning info ok");
1728        let _ = sm.poll().expect("poll arm failsafe");
1729        // Device returns BusyWithOtherAdmin: error_code = 4 (spec §11.10.5.1).
1730        let err = sm
1731            .on_response(
1732                Expectation::ArmFailsafeResponse,
1733                &[0x15, 0x24, 0x00, 0x04, 0x18],
1734            )
1735            .expect_err("busy should fail");
1736        assert!(matches!(
1737            err,
1738            CommissioningError::DeviceImStatus {
1739                stage: Stage::ArmFailsafe,
1740                im_status: 4,
1741            }
1742        ));
1743        assert_eq!(sm.stage(), Stage::Failed);
1744        match sm.poll().expect("abort emission") {
1745            Action::Abort {
1746                send_disarm_failsafe,
1747                reason,
1748            } => {
1749                assert!(send_disarm_failsafe);
1750                assert!(reason.contains("ArmFailsafe"), "reason was {reason}");
1751                assert!(reason.contains("0x4"), "reason was {reason}");
1752            }
1753            other => panic!("expected Abort, got {other:?}"),
1754        }
1755    }
1756
1757    #[test]
1758    fn out_of_order_response_returns_error_without_advancing() {
1759        let fabric = make_fabric_record();
1760        let setup = make_setup_payload();
1761        let paa = PaaTrustStore::with_example_device_roots();
1762        let cd = CdSigningRoots::with_example_device_roots();
1763        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1764        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1765        let mut sm = Commissioner::new(cfg).expect("valid config");
1766        // No poll called — state machine isn't waiting on anything.
1767        let err = sm
1768            .on_response(Expectation::ArmFailsafeResponse, &[])
1769            .expect_err("should reject out-of-order");
1770        assert!(matches!(err, CommissioningError::OutOfOrderResponse(_)));
1771        assert_eq!(sm.stage(), Stage::SecurePairing);
1772    }
1773
1774    #[test]
1775    fn wrong_expectation_returns_unexpected_response_kind() {
1776        let fabric = make_fabric_record();
1777        let setup = make_setup_payload();
1778        let paa = PaaTrustStore::with_example_device_roots();
1779        let cd = CdSigningRoots::with_example_device_roots();
1780        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1781        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1782        let mut sm = Commissioner::new(cfg).expect("valid config");
1783        let _ = sm.poll().expect("poll");
1784        let err = sm
1785            .on_response(Expectation::ArmFailsafeResponse, &[])
1786            .expect_err("wrong kind should fail");
1787        assert!(matches!(
1788            err,
1789            CommissioningError::UnexpectedResponseKind {
1790                expected: Expectation::CommissioningInfo,
1791                got: Expectation::ArmFailsafeResponse,
1792            }
1793        ));
1794        // Wrong-kind does NOT advance the cursor.
1795        assert_eq!(sm.stage(), Stage::ReadCommissioningInfo);
1796    }
1797
1798    fn encode_read_commissioning_info_response() -> Vec<u8> {
1799        // Minimal well-formed anonymous struct. M6.4.1 doesn't parse
1800        // individual attributes yet.
1801        vec![0x15, 0x18]
1802    }
1803
1804    // --- M6.4.2 T18-T21: attestation flow tests ---
1805
1806    fn drive_to_send_pai_cert_request(sm: &mut Commissioner) {
1807        let _ = sm.poll().expect("poll info");
1808        sm.on_response(Expectation::CommissioningInfo, &[0x15, 0x18])
1809            .expect("info ok");
1810        let _ = sm.poll().expect("poll arm failsafe");
1811        sm.on_response(
1812            Expectation::ArmFailsafeResponse,
1813            &[0x15, 0x24, 0x00, 0x00, 0x18],
1814        )
1815        .expect("arm ok");
1816        let _ = sm.poll().expect("poll config regulatory");
1817        sm.on_response(
1818            Expectation::SetRegulatoryConfigResponse,
1819            &[0x15, 0x24, 0x00, 0x00, 0x18],
1820        )
1821        .expect("regulatory ok");
1822    }
1823
1824    fn synthetic_cert_chain_response(cert: &[u8]) -> Vec<u8> {
1825        use matter_codec::{Tag, TlvWriter};
1826        let mut buf = Vec::new();
1827        let mut w = TlvWriter::new(&mut buf);
1828        w.start_structure(Tag::Anonymous).expect("infallible");
1829        w.put_bytes(Tag::Context(0), cert).expect("infallible");
1830        w.end_container().expect("infallible");
1831        buf
1832    }
1833
1834    fn nonce_from_attestation_invoke(act: &Action) -> [u8; 32] {
1835        match act {
1836            Action::Invoke { payload, .. } => {
1837                use matter_codec::{Element, Tag, TlvReader, Value};
1838                let mut r = TlvReader::new(payload);
1839                let _ = r.next().expect("reader").expect("anon-struct-start");
1840                loop {
1841                    match r.next().expect("reader") {
1842                        Some(Element::Scalar {
1843                            tag: Tag::Context(0),
1844                            value: Value::Bytes(b),
1845                        }) => {
1846                            return b.as_slice().try_into().expect("32 bytes");
1847                        }
1848                        Some(_) => {}
1849                        None => panic!("no nonce found"),
1850                    }
1851                }
1852            }
1853            other => panic!("expected Invoke, got {other:?}"),
1854        }
1855    }
1856
1857    #[test]
1858    fn poll_at_send_pai_emits_certificate_chain_request_pai() {
1859        let fabric = make_fabric_record();
1860        let setup = make_setup_payload();
1861        let paa = PaaTrustStore::with_example_device_roots();
1862        let cd = CdSigningRoots::with_example_device_roots();
1863        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1864        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1865        let mut sm = Commissioner::new(cfg).expect("valid config");
1866        drive_to_send_pai_cert_request(&mut sm);
1867        assert_eq!(sm.stage(), Stage::SendPaiCertRequest);
1868        match sm.poll().expect("poll PAI") {
1869            Action::Invoke {
1870                cluster,
1871                command,
1872                expect,
1873                payload,
1874                ..
1875            } => {
1876                assert_eq!(cluster, 0x003E);
1877                assert_eq!(command, 0x02);
1878                assert_eq!(expect, Expectation::PaiCertChainResponse);
1879                // CertificateChainTypeEnum (spec §11.18.5.2): 2 = PAI.
1880                assert_eq!(payload, vec![0x15, 0x24, 0x00, 0x02, 0x18]);
1881            }
1882            other => panic!("expected Invoke, got {other:?}"),
1883        }
1884    }
1885
1886    #[test]
1887    fn poll_at_send_dac_emits_certificate_chain_request_dac() {
1888        let fabric = make_fabric_record();
1889        let setup = make_setup_payload();
1890        let paa = PaaTrustStore::with_example_device_roots();
1891        let cd = CdSigningRoots::with_example_device_roots();
1892        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1893        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1894        let mut sm = Commissioner::new(cfg).expect("valid config");
1895        drive_to_send_pai_cert_request(&mut sm);
1896        let _ = sm.poll().expect("poll PAI");
1897        let pai_response = synthetic_cert_chain_response(&[0xAA, 0xBB, 0xCC]);
1898        sm.on_response(Expectation::PaiCertChainResponse, &pai_response)
1899            .expect("PAI accepted");
1900        assert_eq!(sm.stage(), Stage::SendDacCertRequest);
1901        match sm.poll().expect("poll DAC") {
1902            Action::Invoke {
1903                cluster,
1904                command,
1905                expect,
1906                payload,
1907                ..
1908            } => {
1909                assert_eq!(cluster, 0x003E);
1910                assert_eq!(command, 0x02);
1911                assert_eq!(expect, Expectation::DacCertChainResponse);
1912                // CertificateChainTypeEnum (spec §11.18.5.2): 1 = DAC.
1913                assert_eq!(payload, vec![0x15, 0x24, 0x00, 0x01, 0x18]);
1914            }
1915            other => panic!("expected Invoke, got {other:?}"),
1916        }
1917    }
1918
1919    #[test]
1920    fn send_attestation_request_uses_fresh_random_nonce_each_time() {
1921        let fabric = make_fabric_record();
1922        let setup = make_setup_payload();
1923        let paa = PaaTrustStore::with_example_device_roots();
1924        let cd = CdSigningRoots::with_example_device_roots();
1925
1926        let rng_a: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1927        let cfg_a = base_config(&fabric, &setup, &paa, &cd, rng_a);
1928        let mut sm_a = Commissioner::new(cfg_a).expect("valid config");
1929        drive_to_send_pai_cert_request(&mut sm_a);
1930        let _ = sm_a.poll().expect("poll PAI a");
1931        let pai_response = synthetic_cert_chain_response(&[0xAA]);
1932        sm_a.on_response(Expectation::PaiCertChainResponse, &pai_response)
1933            .expect("ok");
1934        let _ = sm_a.poll().expect("poll DAC a");
1935        let dac_response = synthetic_cert_chain_response(&[0xBB]);
1936        sm_a.on_response(Expectation::DacCertChainResponse, &dac_response)
1937            .expect("ok");
1938        let nonce_a = nonce_from_attestation_invoke(&sm_a.poll().expect("poll att a"));
1939
1940        let rng_b: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1941        let cfg_b = base_config(&fabric, &setup, &paa, &cd, rng_b);
1942        let mut sm_b = Commissioner::new(cfg_b).expect("valid config");
1943        drive_to_send_pai_cert_request(&mut sm_b);
1944        let _ = sm_b.poll().expect("poll PAI b");
1945        sm_b.on_response(Expectation::PaiCertChainResponse, &pai_response)
1946            .expect("ok");
1947        let _ = sm_b.poll().expect("poll DAC b");
1948        sm_b.on_response(Expectation::DacCertChainResponse, &dac_response)
1949            .expect("ok");
1950        let nonce_b = nonce_from_attestation_invoke(&sm_b.poll().expect("poll att b"));
1951
1952        assert_ne!(
1953            nonce_a, nonce_b,
1954            "two independent runs should use different random nonces"
1955        );
1956    }
1957
1958    // --- M6.4.4 T35-T40: CSR + NOC issuance flow tests ---
1959
1960    /// Extract the 32-byte `CSRNonce` from a `CSRRequest` Invoke payload.
1961    /// Mirrors `nonce_from_attestation_invoke` — same TLV shape, both
1962    /// pull the bytes at context tag 0 inside the anonymous outer struct.
1963    fn nonce_from_csr_invoke(act: &Action) -> [u8; 32] {
1964        match act {
1965            Action::Invoke { payload, .. } => {
1966                use matter_codec::{Element, Tag, TlvReader, Value};
1967                let mut r = TlvReader::new(payload);
1968                let _ = r.next().expect("reader").expect("anon-struct-start");
1969                loop {
1970                    match r.next().expect("reader") {
1971                        Some(Element::Scalar {
1972                            tag: Tag::Context(0),
1973                            value: Value::Bytes(b),
1974                        }) => {
1975                            return b.as_slice().try_into().expect("32 bytes");
1976                        }
1977                        Some(_) => {}
1978                        None => panic!("no nonce found"),
1979                    }
1980                }
1981            }
1982            other => panic!("expected Invoke, got {other:?}"),
1983        }
1984    }
1985
1986    /// Glass-box test: jumps the cursor straight to
1987    /// `Stage::SendOpCertSigningRequest` (bypassing PAI/DAC/Att, which
1988    /// would otherwise demand real fixtures M6.4.2's verifier accepts)
1989    /// and checks the emitted `CSRRequest` Invoke's nonce randomness
1990    /// across two independent commissioner instances. The full
1991    /// integration drive ships in T41 with real matter.js fixtures.
1992    #[test]
1993    fn send_op_cert_signing_request_emits_csr_with_random_nonce() {
1994        let fabric = make_fabric_record();
1995        let setup = make_setup_payload();
1996        let paa = PaaTrustStore::with_example_device_roots();
1997        let cd = CdSigningRoots::with_example_device_roots();
1998
1999        let rng_a: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2000        let cfg_a = base_config(&fabric, &setup, &paa, &cd, rng_a);
2001        let mut sm_a = Commissioner::new(cfg_a).expect("valid config");
2002        // Jump the cursor + plant the prerequisite DAC slot. Glass-box
2003        // crate-private access is fine inside the in-module `tests`
2004        // submodule.
2005        sm_a.stage = Stage::SendOpCertSigningRequest;
2006        sm_a.dac_der = Some(vec![0xAA, 0xBB]);
2007        let act_a = sm_a.poll().expect("poll csr a");
2008        let nonce_a = nonce_from_csr_invoke(&act_a);
2009
2010        let rng_b: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2011        let cfg_b = base_config(&fabric, &setup, &paa, &cd, rng_b);
2012        let mut sm_b = Commissioner::new(cfg_b).expect("valid config");
2013        sm_b.stage = Stage::SendOpCertSigningRequest;
2014        sm_b.dac_der = Some(vec![0xAA, 0xBB]);
2015        let act_b = sm_b.poll().expect("poll csr b");
2016        let nonce_b = nonce_from_csr_invoke(&act_b);
2017
2018        assert_ne!(
2019            nonce_a, nonce_b,
2020            "two independent runs should use different CSR nonces"
2021        );
2022
2023        match act_a {
2024            Action::Invoke {
2025                cluster,
2026                command,
2027                expect,
2028                ..
2029            } => {
2030                assert_eq!(cluster, 0x003E);
2031                assert_eq!(command, 0x04);
2032                assert_eq!(expect, Expectation::CsrResponse);
2033            }
2034            other => panic!("expected Invoke, got {other:?}"),
2035        }
2036    }
2037
2038    /// Glass-box test: with the CSR + NOC artefacts pre-populated and
2039    /// the cursor placed at `Stage::SendNoc`, `poll()` must emit an
2040    /// `AddNOC` Invoke targeting cluster `0x003E` / command `0x06`.
2041    /// Then drive the synthetic `NOCResponse { status: 0 }` through
2042    /// `on_response` and assert the cursor lands on
2043    /// `Stage::ReadNetworkCommissioningInfo`.
2044    #[test]
2045    fn drive_through_send_noc_with_synthetic_noc_response() {
2046        use matter_cert::{
2047            BasicConstraints, DistinguishedName, DnAttribute, Extensions, MatterCertificate,
2048            PublicKey,
2049        };
2050
2051        let fabric = make_fabric_record();
2052        let setup = make_setup_payload();
2053        let paa = PaaTrustStore::with_example_device_roots();
2054        let cd = CdSigningRoots::with_example_device_roots();
2055        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2056        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2057        let mut sm = Commissioner::new(cfg).expect("valid config");
2058
2059        // Skip past attestation/CSR machinery — we want to test the
2060        // SendNoc dispatch arm + the NOC response handler in isolation.
2061        // Plant a structurally-valid synthetic NOC the AddNOC encoder
2062        // can re-serialise. (The device side wouldn't accept it, but
2063        // we're not talking to a device — we feed a canned response.)
2064        let mut key_bytes = [0u8; 65];
2065        key_bytes[0] = 0x04;
2066        let synthetic_noc = MatterCertificate::builder()
2067            .serial(vec![1, 2, 3])
2068            .issuer(fabric.root_cert.subject().clone())
2069            .subject(DistinguishedName::new(vec![
2070                DnAttribute::FabricId(fabric.fabric_id),
2071                DnAttribute::NodeId(0x2),
2072            ]))
2073            .validity(
2074                MatterTime::from_unix_secs(1_704_067_200),
2075                MatterTime::NO_EXPIRY,
2076            )
2077            .public_key(PublicKey::new(key_bytes).expect("valid sec1 prefix"))
2078            .extensions(
2079                Extensions::builder()
2080                    .basic_constraints(Some(BasicConstraints::new(false, None)))
2081                    .build(),
2082            )
2083            .build_unsigned()
2084            .expect("builder")
2085            .assemble([0u8; 64]);
2086
2087        sm.stage = Stage::SendNoc;
2088        sm.issued_noc = Some(synthetic_noc);
2089        sm.issued_noc_public_key = Some(key_bytes);
2090
2091        match sm.poll().expect("poll SendNoc") {
2092            Action::Invoke {
2093                cluster,
2094                command,
2095                expect,
2096                ..
2097            } => {
2098                assert_eq!(cluster, 0x003E);
2099                assert_eq!(command, 0x06);
2100                assert_eq!(expect, Expectation::NocResponse);
2101            }
2102            other => panic!("expected Invoke, got {other:?}"),
2103        }
2104
2105        // Synthetic NOCResponse: anonymous struct with status=0 + fabric_index=1.
2106        let mut noc_response = Vec::new();
2107        {
2108            use matter_codec::{Tag, TlvWriter};
2109            let mut w = TlvWriter::new(&mut noc_response);
2110            w.start_structure(Tag::Anonymous).expect("infallible");
2111            w.put_uint(Tag::Context(0), 0).expect("infallible"); // status = OK
2112            w.put_uint(Tag::Context(1), 1).expect("infallible"); // fabric_index = 1
2113            w.end_container().expect("infallible");
2114        }
2115        sm.on_response(Expectation::NocResponse, &noc_response)
2116            .expect("NocResponse accepted");
2117        assert_eq!(sm.stage(), Stage::ReadNetworkCommissioningInfo);
2118    }
2119
2120    /// Glass-box test: a non-zero NOC status surfaces as
2121    /// `CommissioningError::DeviceImStatus { stage: SendNoc, ... }`
2122    /// and transitions the cursor to `Failed`.
2123    #[test]
2124    fn send_noc_failure_status_aborts_with_device_im_status() {
2125        use matter_cert::{
2126            BasicConstraints, DistinguishedName, DnAttribute, Extensions, MatterCertificate,
2127            PublicKey,
2128        };
2129
2130        let fabric = make_fabric_record();
2131        let setup = make_setup_payload();
2132        let paa = PaaTrustStore::with_example_device_roots();
2133        let cd = CdSigningRoots::with_example_device_roots();
2134        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2135        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2136        let mut sm = Commissioner::new(cfg).expect("valid config");
2137
2138        let mut key_bytes = [0u8; 65];
2139        key_bytes[0] = 0x04;
2140        let synthetic_noc = MatterCertificate::builder()
2141            .serial(vec![9])
2142            .issuer(fabric.root_cert.subject().clone())
2143            .subject(DistinguishedName::new(vec![
2144                DnAttribute::FabricId(fabric.fabric_id),
2145                DnAttribute::NodeId(0x2),
2146            ]))
2147            .validity(
2148                MatterTime::from_unix_secs(1_704_067_200),
2149                MatterTime::NO_EXPIRY,
2150            )
2151            .public_key(PublicKey::new(key_bytes).expect("valid sec1 prefix"))
2152            .extensions(
2153                Extensions::builder()
2154                    .basic_constraints(Some(BasicConstraints::new(false, None)))
2155                    .build(),
2156            )
2157            .build_unsigned()
2158            .expect("builder")
2159            .assemble([0u8; 64]);
2160
2161        sm.stage = Stage::SendNoc;
2162        sm.issued_noc = Some(synthetic_noc);
2163        sm.issued_noc_public_key = Some(key_bytes);
2164
2165        let _ = sm.poll().expect("poll SendNoc");
2166
2167        // status = 9 (InvalidNOC, spec §11.18.6.1).
2168        let mut bad_response = Vec::new();
2169        {
2170            use matter_codec::{Tag, TlvWriter};
2171            let mut w = TlvWriter::new(&mut bad_response);
2172            w.start_structure(Tag::Anonymous).expect("infallible");
2173            w.put_uint(Tag::Context(0), 9).expect("infallible");
2174            w.end_container().expect("infallible");
2175        }
2176        let err = sm
2177            .on_response(Expectation::NocResponse, &bad_response)
2178            .expect_err("non-zero NOC status should fail");
2179        assert!(matches!(
2180            err,
2181            CommissioningError::DeviceImStatus {
2182                stage: Stage::SendNoc,
2183                im_status: 9,
2184            }
2185        ));
2186        assert_eq!(sm.stage(), Stage::Failed);
2187    }
2188
2189    /// Glass-box test: `SendTrustedRootCert` emits an `AddTrustedRootCertificate`
2190    /// Invoke whose payload TLV starts with anonymous-struct + context-0
2191    /// octet-string carrying the RCAC TLV bytes. A subsequent `[0x00]`
2192    /// status-ack advances the cursor to `Stage::SendNoc`.
2193    #[test]
2194    fn send_trusted_root_cert_emits_invoke_and_status_ack_advances() {
2195        let fabric = make_fabric_record();
2196        let setup = make_setup_payload();
2197        let paa = PaaTrustStore::with_example_device_roots();
2198        let cd = CdSigningRoots::with_example_device_roots();
2199        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2200        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2201        let mut sm = Commissioner::new(cfg).expect("valid config");
2202
2203        sm.stage = Stage::SendTrustedRootCert;
2204
2205        match sm.poll().expect("poll SendTrustedRootCert") {
2206            Action::Invoke {
2207                cluster,
2208                command,
2209                expect,
2210                payload,
2211                ..
2212            } => {
2213                assert_eq!(cluster, 0x003E);
2214                assert_eq!(command, 0x0B);
2215                assert_eq!(expect, Expectation::AddTrustedRootResponse);
2216                // Sanity: payload is at least the anonymous-struct
2217                // wrapper + a non-trivial octet-string of RCAC TLV.
2218                assert!(payload.len() > 16, "RCAC TLV too short: {}", payload.len());
2219                assert_eq!(payload[0], 0x15); // anonymous struct start
2220            }
2221            other => panic!("expected Invoke, got {other:?}"),
2222        }
2223
2224        // Status-ack of 0x00 (success) advances to SendNoc.
2225        sm.on_response(Expectation::AddTrustedRootResponse, &[0x00])
2226            .expect("status-ack accepted");
2227        assert_eq!(sm.stage(), Stage::SendNoc);
2228    }
2229
2230    // --- M6.4.5 T44-T47: PASE -> CASE handoff + CommissioningComplete tests ---
2231
2232    #[test]
2233    fn find_operational_for_complete_emits_establish_case() {
2234        let fabric = make_fabric_record();
2235        let setup = make_setup_payload();
2236        let paa = PaaTrustStore::with_example_device_roots();
2237        let cd = crate::attestation::CdSigningRoots::with_example_device_roots();
2238        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2239        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2240        let mut sm = Commissioner::new(cfg).expect("valid config");
2241        sm.stage = Stage::FindOperationalForComplete;
2242        match sm.poll().expect("poll establish case") {
2243            Action::EstablishCase {
2244                fabric_id,
2245                peer_node_id,
2246            } => {
2247                assert_eq!(fabric_id, fabric.fabric_id);
2248                assert_eq!(peer_node_id, 0x2); // matches base_config's assigned_node_id
2249            }
2250            other => panic!("expected EstablishCase, got {other:?}"),
2251        }
2252        assert!(sm.awaiting_case_session);
2253    }
2254
2255    #[test]
2256    fn on_case_established_advances_to_send_complete() {
2257        let fabric = make_fabric_record();
2258        let setup = make_setup_payload();
2259        let paa = PaaTrustStore::with_example_device_roots();
2260        let cd = crate::attestation::CdSigningRoots::with_example_device_roots();
2261        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2262        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2263        let mut sm = Commissioner::new(cfg).expect("valid config");
2264        sm.stage = Stage::FindOperationalForComplete;
2265        let _ = sm.poll().expect("emit EstablishCase");
2266        sm.on_case_established().expect("case established");
2267        assert_eq!(sm.stage(), Stage::SendComplete);
2268        assert!(!sm.awaiting_case_session);
2269    }
2270
2271    #[test]
2272    fn on_case_established_without_pending_emits_out_of_order() {
2273        let fabric = make_fabric_record();
2274        let setup = make_setup_payload();
2275        let paa = PaaTrustStore::with_example_device_roots();
2276        let cd = crate::attestation::CdSigningRoots::with_example_device_roots();
2277        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2278        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2279        let mut sm = Commissioner::new(cfg).expect("valid config");
2280        let err = sm.on_case_established().expect_err("no pending establish");
2281        assert!(matches!(err, CommissioningError::OutOfOrderResponse(_)));
2282    }
2283
2284    #[test]
2285    fn send_complete_emits_invoke_over_case_session() {
2286        let fabric = make_fabric_record();
2287        let setup = make_setup_payload();
2288        let paa = PaaTrustStore::with_example_device_roots();
2289        let cd = crate::attestation::CdSigningRoots::with_example_device_roots();
2290        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2291        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2292        let mut sm = Commissioner::new(cfg).expect("valid config");
2293        sm.stage = Stage::SendComplete;
2294        match sm.poll().expect("poll send complete") {
2295            Action::Invoke {
2296                session,
2297                cluster,
2298                command,
2299                expect,
2300                payload,
2301                ..
2302            } => {
2303                assert_eq!(session, crate::state_machine::SessionContext::Case);
2304                assert_eq!(cluster, 0x0030);
2305                assert_eq!(command, 0x04); // CommissioningComplete
2306                assert_eq!(expect, Expectation::CommissioningCompleteResponse);
2307                // CommissioningComplete carries no payload fields — empty struct.
2308                assert_eq!(payload, vec![0x15, 0x18]);
2309            }
2310            other => panic!("expected Invoke, got {other:?}"),
2311        }
2312    }
2313
2314    #[test]
2315    fn send_complete_success_advances_to_cleanup() {
2316        let fabric = make_fabric_record();
2317        let setup = make_setup_payload();
2318        let paa = PaaTrustStore::with_example_device_roots();
2319        let cd = crate::attestation::CdSigningRoots::with_example_device_roots();
2320        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2321        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2322        let mut sm = Commissioner::new(cfg).expect("valid config");
2323        sm.stage = Stage::SendComplete;
2324        let _ = sm.poll().expect("emit invoke");
2325        sm.on_response(
2326            Expectation::CommissioningCompleteResponse,
2327            &[0x15, 0x24, 0x00, 0x00, 0x18], // error_code = 0
2328        )
2329        .expect("complete ok");
2330        assert_eq!(sm.stage(), Stage::Cleanup);
2331    }
2332
2333    #[test]
2334    fn cleanup_emits_done_with_noc_public_key() {
2335        let fabric = make_fabric_record();
2336        let setup = make_setup_payload();
2337        let paa = PaaTrustStore::with_example_device_roots();
2338        let cd = crate::attestation::CdSigningRoots::with_example_device_roots();
2339        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2340        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2341        let mut sm = Commissioner::new(cfg).expect("valid config");
2342        sm.stage = Stage::Cleanup;
2343        sm.issued_noc_public_key = Some([0xCA; 65]);
2344        match sm.poll().expect("poll cleanup") {
2345            Action::Done(cf) => {
2346                assert_eq!(cf.peer_node_id, 0x2);
2347                assert_eq!(cf.peer_root_public_key, [0xCA; 65]);
2348                assert_eq!(cf.terminated_at, Stage::Cleanup);
2349                assert_eq!(cf.fabric.fabric_id, fabric.fabric_id);
2350            }
2351            other => panic!("expected Done, got {other:?}"),
2352        }
2353    }
2354
2355    // --- M6.4.5 T49: CaseFailed negative coverage ---
2356
2357    #[test]
2358    fn case_failed_response_aborts_with_case_establishment_failed() {
2359        let fabric = make_fabric_record();
2360        let setup = make_setup_payload();
2361        let paa = PaaTrustStore::with_example_device_roots();
2362        let cd = crate::attestation::CdSigningRoots::with_example_device_roots();
2363        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2364        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2365        let mut sm = Commissioner::new(cfg).expect("valid config");
2366
2367        // Glass-box: jump to FindOperationalForComplete (skipping the
2368        // attestation + CSR + NOC stages that need real fixtures).
2369        sm.stage = Stage::FindOperationalForComplete;
2370        let _ = sm.poll().expect("emit EstablishCase");
2371        assert!(sm.awaiting_case_session);
2372
2373        // Caller signals CASE establishment failure.
2374        let err = sm
2375            .on_response(Expectation::CaseFailed, &[])
2376            .expect_err("CaseFailed should error");
2377        assert!(matches!(err, CommissioningError::CaseEstablishmentFailed));
2378        assert_eq!(sm.stage(), Stage::Failed);
2379        assert!(!sm.awaiting_case_session);
2380
2381        // Subsequent poll emits Action::Abort with send_disarm_failsafe=true.
2382        match sm.poll().expect("emit abort") {
2383            Action::Abort {
2384                send_disarm_failsafe,
2385                reason,
2386            } => {
2387                assert!(send_disarm_failsafe);
2388                assert!(
2389                    reason.contains("CASE"),
2390                    "abort reason should mention CASE: {reason}"
2391                );
2392            }
2393            other => panic!("expected Abort, got {other:?}"),
2394        }
2395    }
2396
2397    #[test]
2398    fn case_failed_when_not_awaiting_returns_out_of_order() {
2399        let fabric = make_fabric_record();
2400        let setup = make_setup_payload();
2401        let paa = PaaTrustStore::with_example_device_roots();
2402        let cd = crate::attestation::CdSigningRoots::with_example_device_roots();
2403        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2404        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2405        let mut sm = Commissioner::new(cfg).expect("valid config");
2406        let err = sm
2407            .on_response(Expectation::CaseFailed, &[])
2408            .expect_err("CaseFailed without pending should error");
2409        assert!(matches!(err, CommissioningError::OutOfOrderResponse(_)));
2410    }
2411
2412    /// Returns a fully-populated, valid [`CommissionerConfig`] for use in
2413    /// unit tests that only need to mutate one field. All held references
2414    /// are leaked so the config is `'static`; acceptable for test code.
2415    fn sample_valid_config() -> CommissionerConfig<'static> {
2416        use std::sync::OnceLock;
2417        static FABRIC: OnceLock<FabricRecord> = OnceLock::new();
2418        static SETUP: OnceLock<SetupPayload> = OnceLock::new();
2419        static PAA: OnceLock<PaaTrustStore> = OnceLock::new();
2420        static CD: OnceLock<crate::attestation::CdSigningRoots> = OnceLock::new();
2421
2422        let fabric = FABRIC.get_or_init(make_fabric_record);
2423        let setup = SETUP.get_or_init(make_setup_payload);
2424        let paa = PAA.get_or_init(PaaTrustStore::with_example_device_roots);
2425        let cd = CD.get_or_init(crate::attestation::CdSigningRoots::with_example_device_roots);
2426        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
2427
2428        CommissionerConfig {
2429            pase_attestation_challenge: [0u8; 16],
2430            fabric,
2431            setup_payload: setup,
2432            paa_trust_store: paa,
2433            cd_signing_roots: cd,
2434            commissioner_node_id: 0x1,
2435            assigned_node_id: 0x2,
2436            ipk_epoch_key: [0x42_u8; 16],
2437            case_admin_subject: 0x1,
2438            admin_vendor_id: 0xFFF1,
2439            now: MatterTime::from_unix_secs(1_704_067_200),
2440            rng,
2441            network: NetworkCredentials::AlreadyOnNetwork,
2442        }
2443    }
2444
2445    #[test]
2446    fn empty_ssid_is_rejected() {
2447        let mut config = sample_valid_config();
2448        config.network = NetworkCredentials::WiFi(WiFiCredentials {
2449            ssid: vec![],
2450            credentials: vec![],
2451        });
2452        let Err(err) = Commissioner::new(config) else {
2453            panic!("empty ssid should fail");
2454        };
2455        assert!(
2456            matches!(err, CommissioningError::InvalidConfig(m) if m.contains("ssid")),
2457            "got {err:?}",
2458        );
2459    }
2460
2461    #[test]
2462    fn oversize_ssid_is_rejected() {
2463        let mut config = sample_valid_config();
2464        config.network = NetworkCredentials::WiFi(WiFiCredentials {
2465            ssid: vec![b'a'; 33],
2466            credentials: vec![],
2467        });
2468        let Err(err) = Commissioner::new(config) else {
2469            panic!("33-byte ssid should fail");
2470        };
2471        assert!(
2472            matches!(err, CommissioningError::InvalidConfig(m) if m.contains("≤32")),
2473            "got {err:?}",
2474        );
2475    }
2476
2477    #[test]
2478    fn oversize_credentials_is_rejected() {
2479        let mut config = sample_valid_config();
2480        config.network = NetworkCredentials::WiFi(WiFiCredentials {
2481            ssid: b"matter".to_vec(),
2482            credentials: vec![0u8; 65],
2483        });
2484        let Err(err) = Commissioner::new(config) else {
2485            panic!("65-byte credentials should fail");
2486        };
2487        assert!(
2488            matches!(err, CommissioningError::InvalidConfig(m) if m.contains("≤64")),
2489            "got {err:?}",
2490        );
2491    }
2492
2493    #[test]
2494    fn wifi_credentials_none_is_accepted() {
2495        let mut config = sample_valid_config();
2496        config.network = NetworkCredentials::AlreadyOnNetwork;
2497        Commissioner::new(config).expect("AlreadyOnNetwork should pass validation");
2498    }
2499
2500    #[test]
2501    fn network_credentials_thread_variant_accepted() {
2502        // Minimal well-formed dataset: a single Extended PAN ID TLV
2503        // (type 0x02, length 8). ThreadDataset::new self-validates.
2504        let ds =
2505            crate::thread_dataset::ThreadDataset::new(vec![0x02, 0x08, 0, 0, 0, 0, 0, 0, 0, 0])
2506                .expect("minimal ext-pan-id dataset is valid");
2507        let mut config = sample_valid_config();
2508        config.network = NetworkCredentials::Thread(ds);
2509        let c = Commissioner::new(config).expect("Thread network should pass validation");
2510        assert!(matches!(c.network(), NetworkCredentials::Thread(_)));
2511    }
2512
2513    #[test]
2514    fn wifi_credentials_debug_redacts_passphrase() {
2515        let creds = WiFiCredentials {
2516            ssid: b"matter".to_vec(),
2517            credentials: b"hunter22".to_vec(),
2518        };
2519        let rendered = format!("{creds:?}");
2520        assert!(
2521            !rendered.contains("hunter22"),
2522            "Debug must not contain credentials bytes: {rendered}",
2523        );
2524        assert!(rendered.contains("redacted"), "got {rendered}");
2525        assert!(
2526            rendered.contains('8'),
2527            "credentials length should appear: {rendered}"
2528        );
2529        assert!(
2530            rendered.contains('6'),
2531            "ssid length should appear: {rendered}"
2532        );
2533    }
2534
2535    // --- M6.5.2 T14: failsafe expiry derivation from BasicCommissioningInfo ---
2536
2537    #[test]
2538    fn failsafe_expiry_derives_from_basic_commissioning_info() {
2539        let mut sm = Commissioner::new(sample_valid_config()).expect("valid config");
2540        // Advance to ReadCommissioningInfo and feed a BasicCommissioningInfo with 120s.
2541        let _initial = sm.poll().expect("initial poll");
2542        let response = vec![
2543            0x15, 0x25, 0x00, 0x78, 0x00, // u16 = 120
2544            0x18,
2545        ];
2546        sm.on_response(Expectation::CommissioningInfo, &response)
2547            .expect("commissioning info accepted");
2548        // Now ArmFailsafe should emit with expiry=120.
2549        let action = sm.poll().expect("arm-failsafe poll");
2550        match action {
2551            Action::Invoke {
2552                payload,
2553                cluster,
2554                command,
2555                ..
2556            } => {
2557                assert_eq!(cluster, 0x0030);
2558                assert_eq!(command, 0x00);
2559                // ArmFailSafe payload byte for expiry: TLV-encoded u8/u16 at context tag 0.
2560                // For value 120 the smallest-width encoding is u8 = 0x24 0x00 0x78.
2561                assert!(
2562                    payload.windows(3).any(|w| w == [0x24, 0x00, 0x78]),
2563                    "ArmFailSafe payload should carry expiry=120: {payload:02x?}",
2564                );
2565            }
2566            other => panic!("expected Invoke, got {other:?}"),
2567        }
2568    }
2569
2570    #[test]
2571    fn failsafe_expiry_capped_at_max_cumulative_failsafe_seconds() {
2572        let mut sm = Commissioner::new(sample_valid_config()).expect("valid config");
2573        let _initial = sm.poll().expect("initial poll");
2574        // BasicCommissioningInfo: failsafe_expiry_length=120 (tag 0),
2575        // max_cumulative=90 (tag 1). Our requested expiry must cap at 90 so the
2576        // device never rejects the ArmFailSafe with BoundsExceeded.
2577        let response = vec![
2578            0x15, //
2579            0x25, 0x00, 0x78, 0x00, // tag0 u16 = 120
2580            0x25, 0x01, 0x5A, 0x00, // tag1 u16 = 90 (max cumulative)
2581            0x18,
2582        ];
2583        sm.on_response(Expectation::CommissioningInfo, &response)
2584            .expect("commissioning info accepted");
2585        let action = sm.poll().expect("arm-failsafe poll");
2586        match action {
2587            Action::Invoke { payload, .. } => {
2588                assert!(
2589                    payload.windows(3).any(|w| w == [0x24, 0x00, 0x5A]),
2590                    "ArmFailSafe expiry must be capped to 90: {payload:02x?}",
2591                );
2592                assert!(
2593                    !payload.windows(3).any(|w| w == [0x24, 0x00, 0x78]),
2594                    "the uncapped 120 must NOT be sent: {payload:02x?}",
2595                );
2596            }
2597            other => panic!("expected Invoke, got {other:?}"),
2598        }
2599    }
2600
2601    #[test]
2602    fn failsafe_expiry_falls_back_to_60_on_empty_basic_commissioning_info() {
2603        let mut sm = Commissioner::new(sample_valid_config()).expect("valid config");
2604        let _initial = sm.poll().expect("initial poll");
2605        // Feed a well-formed empty struct — decode_basic_commissioning_info
2606        // returns None when the failsafe field is missing, so the M6.4
2607        // fallback of 60s applies.
2608        sm.on_response(Expectation::CommissioningInfo, &[0x15, 0x18])
2609            .expect("empty struct accepted");
2610        let action = sm.poll().expect("arm-failsafe poll");
2611        if let Action::Invoke { payload, .. } = action {
2612            // 60 = 0x3C, anonymous struct with context-tag-0 u8.
2613            assert!(
2614                payload.windows(3).any(|w| w == [0x24, 0x00, 0x3C]),
2615                "ArmFailSafe payload should carry expiry=60 fallback: {payload:02x?}",
2616            );
2617        }
2618    }
2619
2620    // --- M6.5.2 T15: breadcrumb monotonicity ---
2621
2622    #[test]
2623    fn breadcrumb_increases_across_commands() {
2624        fn extract_uint_at_tag(payload: &[u8], tag_num: u8) -> Option<u64> {
2625            use matter_codec::{Element, Tag, TlvReader, Value};
2626            let mut reader = TlvReader::new(payload);
2627            while let Ok(Some(elem)) = reader.next() {
2628                if let Element::Scalar {
2629                    tag: Tag::Context(t),
2630                    value: Value::Uint(v),
2631                } = elem
2632                {
2633                    if t == tag_num {
2634                        return Some(v);
2635                    }
2636                }
2637            }
2638            None
2639        }
2640
2641        let mut sm = Commissioner::new(sample_valid_config()).expect("valid config");
2642        let mut breadcrumbs: Vec<u64> = Vec::new();
2643
2644        // Poll #1: ReadCommissioningInfo (no breadcrumb).
2645        let _ = sm.poll().expect("read commissioning info");
2646        sm.on_response(Expectation::CommissioningInfo, &[0x15, 0x18])
2647            .expect("info accepted");
2648
2649        // Poll #2: ArmFailsafe — first breadcrumb-bearing command.
2650        let action = sm.poll().expect("arm-failsafe");
2651        if let Action::Invoke { payload, .. } = action {
2652            if let Some(b) = extract_uint_at_tag(&payload, 1) {
2653                breadcrumbs.push(b);
2654            }
2655        }
2656        sm.on_response(
2657            Expectation::ArmFailsafeResponse,
2658            &[0x15, 0x24, 0x00, 0x00, 0x18],
2659        )
2660        .expect("arm-failsafe ok");
2661
2662        // Poll #3: SetRegulatoryConfig — second breadcrumb-bearing command.
2663        let action = sm.poll().expect("set-regulatory");
2664        if let Action::Invoke { payload, .. } = action {
2665            if let Some(b) = extract_uint_at_tag(&payload, 2) {
2666                breadcrumbs.push(b);
2667            }
2668        }
2669
2670        assert_eq!(
2671            breadcrumbs.len(),
2672            2,
2673            "should have extracted exactly two breadcrumbs, got {breadcrumbs:?}",
2674        );
2675        assert!(
2676            breadcrumbs[0] < breadcrumbs[1],
2677            "breadcrumbs should be strictly increasing: {breadcrumbs:?}",
2678        );
2679    }
2680}