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_csa_test_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                // ICAC slot is `None` in M6.4: only RCAC -> NOC chains
663                // are issued. ICAC support is M6.3.x / M8 work.
664                let payload = encode_add_noc(
665                    &noc_tlv,
666                    None,
667                    &self.ipk_epoch_key,
668                    self.case_admin_subject,
669                    self.admin_vendor_id,
670                );
671                self.awaiting = Some(Expectation::NocResponse);
672                Ok(Action::Invoke {
673                    session: SessionContext::Pase,
674                    endpoint: 0,
675                    cluster: 0x003E,
676                    command: 0x06,
677                    payload,
678                    expect: Expectation::NocResponse,
679                })
680            }
681            Stage::ReadNetworkCommissioningInfo => {
682                self.awaiting = Some(Expectation::NetworkCommissioningInfo);
683                Ok(Action::ReadAttribute {
684                    session: SessionContext::Pase,
685                    endpoint: 0,
686                    cluster: crate::clusters::network_commissioning::CLUSTER_ID,
687                    attributes: &[
688                        crate::clusters::network_commissioning::attribute_id::FEATURE_MAP,
689                        // ConnectMaxTimeSeconds (spec §11.9.5.4) — sizes the
690                        // FailsafeBeforeNetworkEnable extension (D7). Thread
691                        // attach is slower than Wi-Fi association.
692                        crate::clusters::network_commissioning::attribute_id::CONNECT_MAX_TIME_SECONDS,
693                    ],
694                    expect: Expectation::NetworkCommissioningInfo,
695                })
696            }
697            Stage::NetworkSetup => {
698                use crate::clusters::network_commissioning as nc;
699                // Select the provisioning command by the supplied
700                // credential type. The FeatureMap-cross-check at
701                // `Expectation::NetworkCommissioningInfo` guarantees the
702                // device actually supports this network type before we
703                // reach here, so `AlreadyOnNetwork` never lands in this
704                // arm — treat it as an out-of-order state, not a silent
705                // skip.
706                let breadcrumb = self.next_breadcrumb();
707                let (command, payload) = match &self.network {
708                    NetworkCredentials::WiFi(creds) => (
709                        nc::command_id::ADD_OR_UPDATE_WIFI_NETWORK,
710                        nc::encode_add_or_update_wifi_network(
711                            &creds.ssid,
712                            &creds.credentials,
713                            breadcrumb,
714                        ),
715                    ),
716                    NetworkCredentials::Thread(dataset) => (
717                        nc::command_id::ADD_OR_UPDATE_THREAD_NETWORK,
718                        nc::encode_add_or_update_thread_network(dataset.as_bytes(), breadcrumb),
719                    ),
720                    NetworkCredentials::AlreadyOnNetwork => {
721                        return Err(CommissioningError::OutOfOrderResponse(self.stage));
722                    }
723                };
724                self.awaiting = Some(Expectation::NetworkConfigResponse);
725                Ok(Action::Invoke {
726                    session: SessionContext::Pase,
727                    endpoint: 0,
728                    cluster: nc::CLUSTER_ID,
729                    command,
730                    payload,
731                    expect: Expectation::NetworkConfigResponse,
732                })
733            }
734            Stage::NetworkEnable => {
735                use crate::clusters::network_commissioning as nc;
736                // `ConnectNetwork` takes an opaque `network_id`: the SSID
737                // for Wi-Fi, the Extended PAN ID for Thread (spec §11.9.6.6).
738                let breadcrumb = self.next_breadcrumb();
739                let payload = match &self.network {
740                    NetworkCredentials::WiFi(creds) => {
741                        nc::encode_connect_network(&creds.ssid, breadcrumb)
742                    }
743                    NetworkCredentials::Thread(dataset) => {
744                        nc::encode_connect_network(&dataset.ext_pan_id(), breadcrumb)
745                    }
746                    NetworkCredentials::AlreadyOnNetwork => {
747                        return Err(CommissioningError::OutOfOrderResponse(self.stage));
748                    }
749                };
750                self.awaiting = Some(Expectation::ConnectNetworkResponse);
751                Ok(Action::Invoke {
752                    session: SessionContext::Pase,
753                    endpoint: 0,
754                    cluster: nc::CLUSTER_ID,
755                    command: nc::command_id::CONNECT_NETWORK,
756                    payload,
757                    expect: Expectation::ConnectNetworkResponse,
758                })
759            }
760            Stage::EvictPreviousCaseSessions => {
761                // New-fabric commissioning has no prior CASE session
762                // to evict. M8 multi-fabric work will emit
763                // Action::EvictCase here.
764                self.advance(Stage::FindOperationalForComplete);
765                self.dispatch_stage()
766            }
767            Stage::FindOperationalForComplete => {
768                self.awaiting_case_session = true;
769                Ok(Action::EstablishCase {
770                    fabric_id: self.fabric.fabric_id,
771                    peer_node_id: self.assigned_node_id,
772                })
773            }
774            Stage::SendComplete => {
775                let payload = gc::encode_commissioning_complete();
776                self.awaiting = Some(Expectation::CommissioningCompleteResponse);
777                Ok(Action::Invoke {
778                    session: SessionContext::Case,
779                    endpoint: 0,
780                    cluster: gc::CLUSTER_ID,
781                    command: gc::command_id::COMMISSIONING_COMPLETE,
782                    payload,
783                    expect: Expectation::CommissioningCompleteResponse,
784                })
785            }
786            Stage::Cleanup => {
787                let public_key = self
788                    .issued_noc_public_key
789                    .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
790                Ok(Action::Done(crate::state_machine::CommissionedFabric {
791                    fabric: self.fabric.clone(),
792                    peer_node_id: self.assigned_node_id,
793                    peer_root_public_key: public_key,
794                    terminated_at: Stage::Cleanup,
795                }))
796            }
797            Stage::Failed => {
798                // Subsequent poll() after a failure surfaces the Abort.
799                // The state machine stays in Failed.
800                self.awaiting = None;
801                let reason = self
802                    .last_failure
803                    .clone()
804                    .unwrap_or_else(|| "commissioning aborted".to_string());
805                Ok(Action::Abort {
806                    send_disarm_failsafe: true,
807                    reason,
808                })
809            } // Every `Stage` variant has its own arm above. `Stage` is
810              // `#[non_exhaustive]` for cross-crate consumers, but within
811              // this crate the match is exhaustive — no `_ =>` arm needed.
812        }
813    }
814
815    /// Feed a response payload back into the state machine.
816    ///
817    /// `expect` MUST match the [`Expectation`] from the last `poll()`'s
818    /// emitted `Action`.
819    ///
820    /// # Errors
821    ///
822    /// - [`CommissioningError::OutOfOrderResponse`] if the state machine
823    ///   isn't currently waiting for a response.
824    /// - [`CommissioningError::UnexpectedResponseKind`] if `expect`
825    ///   doesn't match the last `Action`'s `Expectation`. The cursor
826    ///   does not advance.
827    /// - [`CommissioningError::MalformedResponse`] if `payload` fails
828    ///   to decode at the cluster-command level.
829    /// - [`CommissioningError::DeviceImStatus`] if the device returned a
830    ///   non-OK Interaction Model status.
831    ///
832    /// Any error other than `OutOfOrderResponse` and
833    /// `UnexpectedResponseKind` transitions the cursor to
834    /// [`Stage::Failed`]; the next `poll()` call emits
835    /// [`Action::Abort`] with a rendered summary.
836    #[cfg_attr(feature = "tracing", instrument(skip(self, payload), fields(stage = ?self.stage, expectation = ?expect)))]
837    pub fn on_response(
838        &mut self,
839        expect: Expectation,
840        payload: &[u8],
841    ) -> Result<(), CommissioningError> {
842        if expect == Expectation::CaseFailed {
843            // CaseFailed bypasses the awaiting check — the caller
844            // signals failure of the EstablishCase action explicitly,
845            // and EstablishCase tracks readiness via
846            // `awaiting_case_session`, not `awaiting`.
847            if !self.awaiting_case_session {
848                return Err(CommissioningError::OutOfOrderResponse(self.stage));
849            }
850            self.awaiting_case_session = false;
851            self.stage = Stage::Failed;
852            self.awaiting = None;
853            self.pending_action = None;
854            self.last_failure = Some(CommissioningError::CaseEstablishmentFailed.to_string());
855            return Err(CommissioningError::CaseEstablishmentFailed);
856        }
857        let Some(awaiting) = self.awaiting else {
858            return Err(CommissioningError::OutOfOrderResponse(self.stage));
859        };
860        if awaiting != expect {
861            return Err(CommissioningError::UnexpectedResponseKind {
862                expected: awaiting,
863                got: expect,
864            });
865        }
866        match self.handle_response(expect, payload) {
867            Ok(()) => Ok(()),
868            Err(err) => {
869                self.last_failure = Some(err.to_string());
870                self.stage = Stage::Failed;
871                self.awaiting = None;
872                self.pending_action = None;
873                Err(err)
874            }
875        }
876    }
877
878    /// Signal that CASE establishment (mDNS find-operational + the
879    /// SIGMA-I handshake — both M6.6 mechanics, owned by the driver)
880    /// has succeeded. The state machine advances from
881    /// [`Stage::FindOperationalForComplete`] to [`Stage::SendComplete`].
882    ///
883    /// # Errors
884    ///
885    /// Returns [`CommissioningError::OutOfOrderResponse`] if the state
886    /// machine isn't currently awaiting CASE establishment (i.e., the
887    /// cursor is not at `FindOperationalForComplete` or the
888    /// `EstablishCase` action hasn't been emitted yet).
889    #[cfg_attr(feature = "tracing", instrument(skip(self)))]
890    pub fn on_case_established(&mut self) -> Result<(), CommissioningError> {
891        if !self.awaiting_case_session {
892            return Err(CommissioningError::OutOfOrderResponse(self.stage));
893        }
894        self.awaiting_case_session = false;
895        self.advance(Stage::SendComplete);
896        Ok(())
897    }
898
899    #[allow(clippy::too_many_lines)]
900    fn handle_response(
901        &mut self,
902        expect: Expectation,
903        payload: &[u8],
904    ) -> Result<(), CommissioningError> {
905        use crate::clusters::general_commissioning as gc;
906        match expect {
907            Expectation::CommissioningInfo => {
908                Self::assert_tlv_well_formed(self.stage, payload)?;
909                // Best-effort: scan the response for a BasicCommissioningInfo
910                // struct and update failsafe_expiry_seconds. Malformed or
911                // missing → keep the M6.4 fallback (60s) silently.
912                if let Some(info) = gc::decode_basic_commissioning_info(payload) {
913                    if info.failsafe_expiry_length_seconds > 0 {
914                        self.failsafe_expiry_seconds = info.failsafe_expiry_length_seconds;
915                    }
916                }
917                self.advance(Stage::ArmFailsafe);
918                Ok(())
919            }
920            Expectation::ArmFailsafeResponse => {
921                let resp = gc::decode_arm_fail_safe_response(payload)?;
922                if resp.error_code != 0 {
923                    return Err(CommissioningError::DeviceImStatus {
924                        stage: self.stage,
925                        im_status: u16::from(resp.error_code),
926                    });
927                }
928                let next = match self.stage {
929                    Stage::ArmFailsafe => Stage::ConfigRegulatory,
930                    Stage::FailsafeBeforeNetworkEnable => Stage::NetworkEnable,
931                    other => {
932                        return Err(CommissioningError::OutOfOrderResponse(other));
933                    }
934                };
935                self.advance(next);
936                Ok(())
937            }
938            Expectation::SetRegulatoryConfigResponse => {
939                let resp = gc::decode_set_regulatory_config_response(payload)?;
940                if resp.error_code != 0 {
941                    return Err(CommissioningError::DeviceImStatus {
942                        stage: Stage::ConfigRegulatory,
943                        im_status: u16::from(resp.error_code),
944                    });
945                }
946                self.advance(Stage::SendPaiCertRequest);
947                Ok(())
948            }
949            Expectation::PaiCertChainResponse => {
950                let resp = crate::noc::decode_certificate_chain_response(payload)?;
951                self.pai_der = Some(resp.certificate);
952                self.advance(Stage::SendDacCertRequest);
953                Ok(())
954            }
955            Expectation::DacCertChainResponse => {
956                let resp = crate::noc::decode_certificate_chain_response(payload)?;
957                self.dac_der = Some(resp.certificate);
958                self.advance(Stage::SendAttestationRequest);
959                Ok(())
960            }
961            Expectation::AttestationResponse => {
962                let resp = crate::noc::decode_attestation_response(payload)?;
963                self.attestation_response = Some(resp);
964                self.advance(Stage::AttestationVerification);
965                Ok(())
966            }
967            Expectation::CsrResponse => {
968                let resp = crate::noc::decode_csr_response(payload)?;
969                self.csr_response = Some(resp);
970                self.advance(Stage::ValidateCsr);
971                Ok(())
972            }
973            Expectation::AddTrustedRootResponse => {
974                // `AddTrustedRootCertificate` has no typed response —
975                // success is a status-only ack at the Interaction Model
976                // layer. The caller surfaces the IM status as a 1-byte
977                // payload: `0x00` = success, anything else = error.
978                if payload.first() != Some(&0u8) {
979                    return Err(CommissioningError::DeviceImStatus {
980                        stage: Stage::SendTrustedRootCert,
981                        im_status: u16::from(payload.first().copied().unwrap_or(0xFF)),
982                    });
983                }
984                self.advance(Stage::SendNoc);
985                Ok(())
986            }
987            Expectation::NocResponse => {
988                let resp = crate::noc::decode_noc_response(payload)?;
989                if resp.status != 0 {
990                    return Err(CommissioningError::DeviceImStatus {
991                        stage: Stage::SendNoc,
992                        im_status: u16::from(resp.status),
993                    });
994                }
995                self.advance(Stage::ReadNetworkCommissioningInfo);
996                Ok(())
997            }
998            Expectation::NetworkCommissioningInfo => {
999                use crate::clusters::network_commissioning as nc;
1000                use crate::state_machine::NetworkKind;
1001                let features = nc::decode_feature_map(payload)?;
1002                // A FeatureMap with no recognised interface bit is
1003                // malformed regardless of the supplied credentials — a
1004                // NetworkCommissioning cluster always exposes at least one
1005                // of Wi-Fi / Thread / Ethernet.
1006                if features.is_empty() {
1007                    return Err(CommissioningError::MalformedResponse(
1008                        Stage::ReadNetworkCommissioningInfo,
1009                    ));
1010                }
1011                // Route by the *supplied* credential type, cross-checked
1012                // against the device FeatureMap. This resolves the
1013                // dual-stack ordering ambiguity (a Wi-Fi+Thread device is
1014                // provisioned per the caller's chosen credential type, not
1015                // by feature-bit order) and rejects a mismatch (credential
1016                // type absent from the FeatureMap) instead of silently
1017                // skipping provisioning.
1018                match &self.network {
1019                    NetworkCredentials::WiFi(_) => {
1020                        if !features.contains(nc::NetworkCommissioningFeature::WIFI) {
1021                            return Err(CommissioningError::NetworkFeatureUnsupported {
1022                                needed: NetworkKind::WiFi,
1023                            });
1024                        }
1025                        self.advance(Stage::NetworkSetup);
1026                    }
1027                    NetworkCredentials::Thread(_) => {
1028                        if !features.contains(nc::NetworkCommissioningFeature::THREAD) {
1029                            return Err(CommissioningError::NetworkFeatureUnsupported {
1030                                needed: NetworkKind::Thread,
1031                            });
1032                        }
1033                        self.advance(Stage::NetworkSetup);
1034                    }
1035                    NetworkCredentials::AlreadyOnNetwork => {
1036                        // No credentials to provision: the device is
1037                        // already reachable on its operational network (IP
1038                        // commissioning reached it there — e.g. a
1039                        // second-fabric commission of an already-provisioned
1040                        // device, or an Ethernet-only device). Mirror
1041                        // chip's AutoCommissioner: skip the network
1042                        // sub-cursor entirely (observed necessary on a real
1043                        // device: Tapo P110M, M6.6.5 validation).
1044                        self.advance(Stage::EvictPreviousCaseSessions);
1045                    }
1046                }
1047                Ok(())
1048            }
1049            Expectation::NetworkConfigResponse => {
1050                use crate::clusters::network_commissioning as nc;
1051                let resp = nc::decode_network_config_response(Stage::NetworkSetup, payload)?;
1052                if resp.networking_status != 0 {
1053                    return Err(CommissioningError::NetworkRejected {
1054                        stage: Stage::NetworkSetup,
1055                        networking_status: resp.networking_status,
1056                        debug_text: resp.debug_text,
1057                        remediation_hint: nc::remediation_for(resp.networking_status),
1058                    });
1059                }
1060                self.advance(Stage::FailsafeBeforeNetworkEnable);
1061                Ok(())
1062            }
1063            Expectation::ConnectNetworkResponse => {
1064                use crate::clusters::network_commissioning as nc;
1065                let resp = nc::decode_connect_network_response(Stage::NetworkEnable, payload)?;
1066                if resp.networking_status != 0 {
1067                    return Err(CommissioningError::NetworkRejected {
1068                        stage: Stage::NetworkEnable,
1069                        networking_status: resp.networking_status,
1070                        debug_text: resp.debug_text,
1071                        remediation_hint: nc::remediation_for(resp.networking_status),
1072                    });
1073                }
1074                self.advance(Stage::EvictPreviousCaseSessions);
1075                Ok(())
1076            }
1077            Expectation::CommissioningCompleteResponse => {
1078                let (error_code, _debug) =
1079                    gc::decode_commissioning_error_response(Stage::SendComplete, payload)?;
1080                if error_code != 0 {
1081                    return Err(CommissioningError::DeviceImStatus {
1082                        stage: Stage::SendComplete,
1083                        im_status: u16::from(error_code),
1084                    });
1085                }
1086                self.advance(Stage::Cleanup);
1087                Ok(())
1088            }
1089            // `Expectation::CaseFailed` is handled by `on_response`'s
1090            // pre-awaiting fast path and never reaches handle_response.
1091            _ => Err(CommissioningError::OutOfOrderResponse(self.stage)),
1092        }
1093    }
1094
1095    fn next_breadcrumb(&mut self) -> u64 {
1096        let b = self.breadcrumb_counter;
1097        self.breadcrumb_counter = b.saturating_add(1);
1098        b
1099    }
1100
1101    fn advance(&mut self, next: Stage) {
1102        self.stage = next;
1103        self.awaiting = None;
1104        self.pending_action = None;
1105    }
1106
1107    /// Off-wire attestation verification chain (M6.4.2 T21).
1108    ///
1109    /// Consumes the PAI/DAC DER + `AttestationResponse` + nonce captured
1110    /// by [`Stage::SendPaiCertRequest`] / [`Stage::SendDacCertRequest`]
1111    /// / [`Stage::SendAttestationRequest`] and runs M6.2's verifier
1112    /// chain end-to-end:
1113    ///
1114    /// 1. Parse PAI/DAC DER.
1115    /// 2. `verify_chain` — webpki path validation + Matter VID/PID
1116    ///    overlay (M6.2.2).
1117    /// 3. `verify_attestation_response` — ECDSA signature over
1118    ///    `attestation_elements || attestation_challenge` (M6.2.3).
1119    /// 4. `extract_attestation_elements_fields` — pull the
1120    ///    `attestation_nonce` echo + CD bytes out of the TLV blob.
1121    /// 5. Confirm the device echoed the nonce we sent.
1122    /// 6. `verify_certification_declaration` — verify the CSA-signed CD
1123    ///    embedded in `attestation_elements` against
1124    ///    [`crate::attestation::CdSigningRoots`] and confirm the
1125    ///    declared VID/PID match what the DAC subject claimed.
1126    fn run_attestation_verification(&mut self) -> Result<(), CommissioningError> {
1127        use crate::attestation::{
1128            extract_attestation_elements_fields, verify_attestation_response, verify_chain,
1129            AttestationError, Dac, Pai,
1130        };
1131
1132        let pai_der = self
1133            .pai_der
1134            .as_ref()
1135            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1136        let dac_der = self
1137            .dac_der
1138            .as_ref()
1139            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1140        let response = self
1141            .attestation_response
1142            .as_ref()
1143            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1144        let expected_nonce = self
1145            .attestation_nonce
1146            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1147
1148        // 1. Parse chain certs.
1149        let pai = Pai::from_der(pai_der)?;
1150        let dac = Dac::from_der(dac_der)?;
1151        #[cfg(feature = "tracing")]
1152        tracing::debug!(
1153            dac_der = %crate::hexdump::hex(dac_der),
1154            pai_der = %crate::hexdump::hex(pai_der),
1155            "verifying attestation chain"
1156        );
1157
1158        // 2. Chain validation (M6.2.2 — webpki path validation + VID/PID overlay).
1159        //    The returned `ChainVerification` carries the VID/PID that
1160        //    both webpki and the Matter overlay agreed on; we re-use
1161        //    those for the CD check below so a single source of truth
1162        //    drives both the chain validation and the CD VID/PID
1163        //    equality check.
1164        let chain = verify_chain(&dac, &pai, &self.paa_trust_store, self.now)?;
1165
1166        // 3. AttestationResponse signature (M6.2.3).
1167        verify_attestation_response(response, &self.pase_attestation_challenge, dac.public_key())?;
1168
1169        // 4. Extract attestation_elements fields: CD bytes (M6.4.3 will verify),
1170        //    nonce echo, timestamp.
1171        let fields = extract_attestation_elements_fields(&response.attestation_elements)?;
1172        if fields.attestation_nonce != expected_nonce {
1173            return Err(CommissioningError::Attestation(
1174                AttestationError::ResponseElementsMalformed,
1175            ));
1176        }
1177
1178        // 5. CD verification — verify the device's declared VID/PID
1179        //    against the CSA-signed Certification Declaration extracted
1180        //    from `attestation_elements`.
1181        #[cfg(feature = "tracing")]
1182        tracing::debug!(
1183            cd_cms = %crate::hexdump::hex(&fields.certification_declaration),
1184            "verifying certification declaration"
1185        );
1186        crate::attestation::verify_certification_declaration(
1187            &fields.certification_declaration,
1188            chain.vendor_id,
1189            chain.product_id,
1190            &self.cd_signing_roots,
1191        )?;
1192        Ok(())
1193    }
1194
1195    /// Off-wire CSR verification (M6.4.4 `Stage::ValidateCsr`).
1196    ///
1197    /// Consumes the `CsrResponse` captured by `Stage::SendOpCertSigningRequest`
1198    /// plus the DAC DER captured earlier by `Stage::SendDacCertRequest`,
1199    /// and runs M6.3's `verify_csr_response` three-check atomic gate:
1200    ///
1201    /// 1. PKCS#10 self-signature on the embedded CSR.
1202    /// 2. The device's `CSRNonce` echo equals the commissioner-issued nonce.
1203    /// 3. The DAC's attestation signature over
1204    ///    `nocsr_elements || attestation_challenge`.
1205    fn run_validate_csr(&mut self) -> Result<(), CommissioningError> {
1206        use crate::attestation::Dac;
1207        use crate::noc::verify_csr_response;
1208
1209        let resp = self
1210            .csr_response
1211            .as_ref()
1212            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1213        let dac_der = self
1214            .dac_der
1215            .as_ref()
1216            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1217        let csr_nonce = self
1218            .csr_nonce
1219            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1220
1221        let dac = Dac::from_der(dac_der)?;
1222        let verified = verify_csr_response(
1223            &resp.nocsr_elements,
1224            &resp.attestation_signature,
1225            &csr_nonce,
1226            &self.pase_attestation_challenge,
1227            dac.public_key(),
1228        )?;
1229        self.verified_csr = Some(verified);
1230        Ok(())
1231    }
1232
1233    /// Off-wire NOC issuance (M6.4.4 `Stage::GenerateNocChain`).
1234    ///
1235    /// Consumes the [`crate::noc::VerifiedCsr`] populated by
1236    /// [`Self::run_validate_csr`] and mints a NOC signed by the fabric's
1237    /// RCAC via M6.3's `issue_noc`.
1238    ///
1239    /// Validity window: M6.4 uses `(self.now, MatterTime::NO_EXPIRY)` —
1240    /// the same convention `issue_noc`'s own unit test uses. M8 may
1241    /// tighten this to a bounded operational-cert lifetime per Matter
1242    /// Core Spec §6.4 once persistence + rotation policy lands.
1243    ///
1244    /// CATs (CASE Authenticated Tags) are empty in M6.4; tag-based
1245    /// access control comes later.
1246    fn run_generate_noc_chain(&mut self) -> Result<(), CommissioningError> {
1247        use crate::noc::issue_noc;
1248
1249        let verified = self
1250            .verified_csr
1251            .as_ref()
1252            .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1253        let noc = issue_noc(
1254            &self.fabric,
1255            verified,
1256            self.assigned_node_id,
1257            &[],
1258            (self.now, MatterTime::NO_EXPIRY),
1259            self.rng.as_ref(),
1260        )?;
1261        // Cache the NOC public key (the same bytes the verified CSR
1262        // committed to) for later use — currently only consumed by
1263        // M6.4.5's PASE -> CASE handoff in `CommissionedFabric`. Stored
1264        // here so the SendNoc stage doesn't have to re-derive it.
1265        self.issued_noc_public_key = Some(*verified.public_key.as_bytes());
1266        self.issued_noc = Some(noc);
1267        Ok(())
1268    }
1269
1270    fn assert_tlv_well_formed(stage: Stage, payload: &[u8]) -> Result<(), CommissioningError> {
1271        use matter_codec::{ContainerKind, Element, Tag, TlvReader};
1272        let mut reader = TlvReader::new(payload);
1273        match reader
1274            .next()
1275            .map_err(|_| CommissioningError::MalformedResponse(stage))?
1276        {
1277            Some(Element::ContainerStart {
1278                tag: Tag::Anonymous,
1279                kind: ContainerKind::Structure,
1280            }) => {}
1281            _ => return Err(CommissioningError::MalformedResponse(stage)),
1282        }
1283        // Walk to ContainerEnd; ignore contents for M6.4.1.
1284        loop {
1285            match reader
1286                .next()
1287                .map_err(|_| CommissioningError::MalformedResponse(stage))?
1288            {
1289                None => return Err(CommissioningError::MalformedResponse(stage)),
1290                Some(Element::ContainerEnd) => return Ok(()),
1291                Some(_) => {}
1292            }
1293        }
1294    }
1295}
1296
1297/// Test-only state seeds for [`Commissioner::position_at_stage_for_test`].
1298///
1299/// Each field is `None` by default — the caller opts in to each seed
1300/// explicitly. **Never use in production.**
1301#[cfg(feature = "__test_shortcuts")]
1302#[derive(Default, Debug, Clone, Copy)]
1303pub struct TestStateSeeds {
1304    /// Override `issued_noc_public_key` (normally populated when the
1305    /// state machine actually issues a NOC). Set to a synthetic SEC1-
1306    /// uncompressed P-256 byte pattern (e.g. `[0xCC; 65]`) when
1307    /// fast-forwarding past the NOC-issuance stages.
1308    pub synthetic_noc_pubkey: Option<[u8; 65]>,
1309}
1310
1311#[cfg(test)]
1312mod tests {
1313    // Test-code carve-out: see CLAUDE.md.
1314    #![allow(clippy::unwrap_used, clippy::expect_used)]
1315
1316    use super::*;
1317    use crate::attestation::CdSigningRoots;
1318    use crate::noc::{FabricRecord, NocRng, SystemNocRng};
1319    use crate::setup::{
1320        CommissioningFlow, DiscoveryCapabilities, Discriminator, Passcode, SetupPayload,
1321    };
1322    use crate::state_machine::{Action, Expectation};
1323    use crate::PaaTrustStore;
1324    use matter_cert::time::MatterTime;
1325    use matter_crypto::{RingSigner, Signer};
1326    use std::sync::Arc;
1327
1328    fn make_setup_payload() -> SetupPayload {
1329        SetupPayload {
1330            version: 0,
1331            vendor_id: Some(0xFFF1),
1332            product_id: Some(0x8000),
1333            commissioning_flow: CommissioningFlow::Standard,
1334            discovery_capabilities: DiscoveryCapabilities::ON_NETWORK,
1335            discriminator: Discriminator::new(0x0F00).expect("valid discriminator"),
1336            passcode: Passcode::new(20_202_021).expect("valid passcode"),
1337        }
1338    }
1339
1340    fn make_fabric_record() -> FabricRecord {
1341        let (signer, _pkcs8) = RingSigner::generate().unwrap();
1342        let signer: Arc<dyn Signer> = Arc::new(signer);
1343        FabricRecord::new_root_only(
1344            /* fabric_id */ 0x0000_0000_0000_0001,
1345            signer,
1346            /* not_before */ MatterTime::from_unix_secs(1_704_067_200),
1347            /* not_after */ MatterTime::from_unix_secs(1_735_689_600),
1348            /* rcac_id */ 42,
1349            &SystemNocRng,
1350        )
1351        .unwrap()
1352    }
1353
1354    fn base_config<'a>(
1355        fabric: &'a FabricRecord,
1356        setup: &'a SetupPayload,
1357        paa: &'a PaaTrustStore,
1358        cd: &'a crate::attestation::CdSigningRoots,
1359        rng: Arc<dyn NocRng>,
1360    ) -> CommissionerConfig<'a> {
1361        CommissionerConfig {
1362            pase_attestation_challenge: [0u8; 16],
1363            fabric,
1364            setup_payload: setup,
1365            paa_trust_store: paa,
1366            cd_signing_roots: cd,
1367            commissioner_node_id: 0x1,
1368            assigned_node_id: 0x2,
1369            ipk_epoch_key: [0x42_u8; 16],
1370            case_admin_subject: 0x1,
1371            admin_vendor_id: 0xFFF1,
1372            now: MatterTime::from_unix_secs(1_704_067_200),
1373            rng,
1374            network: NetworkCredentials::AlreadyOnNetwork,
1375        }
1376    }
1377
1378    #[test]
1379    fn new_rejects_zero_commissioner_node_id() {
1380        let fabric = make_fabric_record();
1381        let setup = make_setup_payload();
1382        let paa = PaaTrustStore::with_csa_test_roots();
1383        let cd = CdSigningRoots::with_csa_test_roots();
1384        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1385        let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1386        cfg.commissioner_node_id = 0;
1387        // Cannot use `expect_err`: `Commissioner` does not impl Debug
1388        // because `FabricRecord` (a stored field) is not Debug.
1389        let Err(err) = Commissioner::new(cfg) else {
1390            panic!("zero commissioner_node_id should fail");
1391        };
1392        assert!(
1393            matches!(err, CommissioningError::InvalidConfig(_)),
1394            "got {err:?}"
1395        );
1396    }
1397
1398    #[test]
1399    fn new_rejects_zero_assigned_node_id() {
1400        let fabric = make_fabric_record();
1401        let setup = make_setup_payload();
1402        let paa = PaaTrustStore::with_csa_test_roots();
1403        let cd = CdSigningRoots::with_csa_test_roots();
1404        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1405        let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1406        cfg.assigned_node_id = 0;
1407        let Err(err) = Commissioner::new(cfg) else {
1408            panic!("zero assigned_node_id should fail");
1409        };
1410        assert!(
1411            matches!(err, CommissioningError::InvalidConfig(_)),
1412            "got {err:?}"
1413        );
1414    }
1415
1416    #[test]
1417    fn new_rejects_equal_commissioner_and_assigned_ids() {
1418        let fabric = make_fabric_record();
1419        let setup = make_setup_payload();
1420        let paa = PaaTrustStore::with_csa_test_roots();
1421        let cd = CdSigningRoots::with_csa_test_roots();
1422        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1423        let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1424        cfg.commissioner_node_id = 0x42;
1425        cfg.assigned_node_id = 0x42;
1426        let Err(err) = Commissioner::new(cfg) else {
1427            panic!("equal IDs should fail");
1428        };
1429        assert!(
1430            matches!(err, CommissioningError::InvalidConfig(_)),
1431            "got {err:?}"
1432        );
1433    }
1434
1435    #[test]
1436    fn new_rejects_zero_ipk_epoch_key() {
1437        let fabric = make_fabric_record();
1438        let setup = make_setup_payload();
1439        let paa = PaaTrustStore::with_csa_test_roots();
1440        let cd = CdSigningRoots::with_csa_test_roots();
1441        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1442        let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1443        cfg.ipk_epoch_key = [0u8; 16];
1444        let Err(err) = Commissioner::new(cfg) else {
1445            panic!("zero IPK should fail");
1446        };
1447        assert!(
1448            matches!(err, CommissioningError::InvalidConfig(_)),
1449            "got {err:?}"
1450        );
1451    }
1452
1453    #[test]
1454    fn new_returns_secure_pairing_stage() {
1455        let fabric = make_fabric_record();
1456        let setup = make_setup_payload();
1457        let paa = PaaTrustStore::with_csa_test_roots();
1458        let cd = CdSigningRoots::with_csa_test_roots();
1459        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1460        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1461        let sm = Commissioner::new(cfg).expect("valid config should construct");
1462        assert_eq!(sm.stage(), Stage::SecurePairing);
1463    }
1464
1465    #[test]
1466    fn poll_from_secure_pairing_emits_read_commissioning_info() {
1467        let fabric = make_fabric_record();
1468        let setup = make_setup_payload();
1469        let paa = PaaTrustStore::with_csa_test_roots();
1470        let cd = CdSigningRoots::with_csa_test_roots();
1471        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1472        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1473        let mut sm = Commissioner::new(cfg).expect("valid config");
1474        let act = sm.poll().expect("poll succeeds");
1475        match act {
1476            Action::ReadAttribute {
1477                session,
1478                endpoint,
1479                cluster,
1480                attributes,
1481                expect,
1482            } => {
1483                assert_eq!(session, crate::state_machine::SessionContext::Pase);
1484                assert_eq!(endpoint, 0);
1485                assert_eq!(cluster, 0x0030);
1486                assert_eq!(expect, Expectation::CommissioningInfo);
1487                assert!(!attributes.is_empty());
1488            }
1489            other => panic!("expected ReadAttribute, got {other:?}"),
1490        }
1491        assert_eq!(sm.stage(), Stage::ReadCommissioningInfo);
1492    }
1493
1494    #[test]
1495    fn poll_is_idempotent_between_responses() {
1496        let fabric = make_fabric_record();
1497        let setup = make_setup_payload();
1498        let paa = PaaTrustStore::with_csa_test_roots();
1499        let cd = CdSigningRoots::with_csa_test_roots();
1500        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1501        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1502        let mut sm = Commissioner::new(cfg).expect("valid config");
1503        let act1 = sm.poll().expect("first poll");
1504        let act2 = sm.poll().expect("second poll");
1505        match (act1, act2) {
1506            (
1507                Action::ReadAttribute {
1508                    cluster: c1,
1509                    expect: e1,
1510                    ..
1511                },
1512                Action::ReadAttribute {
1513                    cluster: c2,
1514                    expect: e2,
1515                    ..
1516                },
1517            ) => {
1518                assert_eq!(c1, c2);
1519                assert_eq!(e1, e2);
1520            }
1521            other => panic!("idempotent poll returned different variants: {other:?}"),
1522        }
1523    }
1524
1525    #[test]
1526    fn full_happy_path_through_config_regulatory_lands_on_send_pai_cert_request() {
1527        let fabric = make_fabric_record();
1528        let setup = make_setup_payload();
1529        let paa = PaaTrustStore::with_csa_test_roots();
1530        let cd = CdSigningRoots::with_csa_test_roots();
1531        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1532        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1533        let mut sm = Commissioner::new(cfg).expect("valid config");
1534
1535        // SecurePairing → ReadCommissioningInfo
1536        let _ = sm.poll().expect("poll #1");
1537        let canned_info = encode_read_commissioning_info_response();
1538        sm.on_response(Expectation::CommissioningInfo, &canned_info)
1539            .expect("commissioning info accepted");
1540        assert_eq!(sm.stage(), Stage::ArmFailsafe);
1541
1542        // ArmFailsafe
1543        let _ = sm.poll().expect("poll #2");
1544        sm.on_response(
1545            Expectation::ArmFailsafeResponse,
1546            &[0x15, 0x24, 0x00, 0x00, 0x18],
1547        )
1548        .expect("arm failsafe ok");
1549        assert_eq!(sm.stage(), Stage::ConfigRegulatory);
1550
1551        // ConfigRegulatory
1552        let _ = sm.poll().expect("poll #3");
1553        sm.on_response(
1554            Expectation::SetRegulatoryConfigResponse,
1555            &[0x15, 0x24, 0x00, 0x00, 0x18],
1556        )
1557        .expect("config regulatory ok");
1558        assert_eq!(sm.stage(), Stage::SendPaiCertRequest);
1559
1560        // M6.4.2: SendPaiCertRequest now actually emits an Invoke.
1561        match sm.poll().expect("poll #4") {
1562            Action::Invoke {
1563                cluster,
1564                command,
1565                expect,
1566                ..
1567            } => {
1568                assert_eq!(cluster, 0x003E);
1569                assert_eq!(command, 0x02);
1570                assert_eq!(expect, Expectation::PaiCertChainResponse);
1571            }
1572            other => panic!("expected Invoke, got {other:?}"),
1573        }
1574    }
1575
1576    #[test]
1577    fn arm_failsafe_busy_response_aborts_with_device_im_status() {
1578        let fabric = make_fabric_record();
1579        let setup = make_setup_payload();
1580        let paa = PaaTrustStore::with_csa_test_roots();
1581        let cd = CdSigningRoots::with_csa_test_roots();
1582        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1583        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1584        let mut sm = Commissioner::new(cfg).expect("valid config");
1585        let _ = sm.poll().expect("poll info");
1586        sm.on_response(
1587            Expectation::CommissioningInfo,
1588            &encode_read_commissioning_info_response(),
1589        )
1590        .expect("commissioning info ok");
1591        let _ = sm.poll().expect("poll arm failsafe");
1592        // Device returns BusyWithOtherAdmin: error_code = 4 (spec §11.10.5.1).
1593        let err = sm
1594            .on_response(
1595                Expectation::ArmFailsafeResponse,
1596                &[0x15, 0x24, 0x00, 0x04, 0x18],
1597            )
1598            .expect_err("busy should fail");
1599        assert!(matches!(
1600            err,
1601            CommissioningError::DeviceImStatus {
1602                stage: Stage::ArmFailsafe,
1603                im_status: 4,
1604            }
1605        ));
1606        assert_eq!(sm.stage(), Stage::Failed);
1607        match sm.poll().expect("abort emission") {
1608            Action::Abort {
1609                send_disarm_failsafe,
1610                reason,
1611            } => {
1612                assert!(send_disarm_failsafe);
1613                assert!(reason.contains("ArmFailsafe"), "reason was {reason}");
1614                assert!(reason.contains("0x4"), "reason was {reason}");
1615            }
1616            other => panic!("expected Abort, got {other:?}"),
1617        }
1618    }
1619
1620    #[test]
1621    fn out_of_order_response_returns_error_without_advancing() {
1622        let fabric = make_fabric_record();
1623        let setup = make_setup_payload();
1624        let paa = PaaTrustStore::with_csa_test_roots();
1625        let cd = CdSigningRoots::with_csa_test_roots();
1626        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1627        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1628        let mut sm = Commissioner::new(cfg).expect("valid config");
1629        // No poll called — state machine isn't waiting on anything.
1630        let err = sm
1631            .on_response(Expectation::ArmFailsafeResponse, &[])
1632            .expect_err("should reject out-of-order");
1633        assert!(matches!(err, CommissioningError::OutOfOrderResponse(_)));
1634        assert_eq!(sm.stage(), Stage::SecurePairing);
1635    }
1636
1637    #[test]
1638    fn wrong_expectation_returns_unexpected_response_kind() {
1639        let fabric = make_fabric_record();
1640        let setup = make_setup_payload();
1641        let paa = PaaTrustStore::with_csa_test_roots();
1642        let cd = CdSigningRoots::with_csa_test_roots();
1643        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1644        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1645        let mut sm = Commissioner::new(cfg).expect("valid config");
1646        let _ = sm.poll().expect("poll");
1647        let err = sm
1648            .on_response(Expectation::ArmFailsafeResponse, &[])
1649            .expect_err("wrong kind should fail");
1650        assert!(matches!(
1651            err,
1652            CommissioningError::UnexpectedResponseKind {
1653                expected: Expectation::CommissioningInfo,
1654                got: Expectation::ArmFailsafeResponse,
1655            }
1656        ));
1657        // Wrong-kind does NOT advance the cursor.
1658        assert_eq!(sm.stage(), Stage::ReadCommissioningInfo);
1659    }
1660
1661    fn encode_read_commissioning_info_response() -> Vec<u8> {
1662        // Minimal well-formed anonymous struct. M6.4.1 doesn't parse
1663        // individual attributes yet.
1664        vec![0x15, 0x18]
1665    }
1666
1667    // --- M6.4.2 T18-T21: attestation flow tests ---
1668
1669    fn drive_to_send_pai_cert_request(sm: &mut Commissioner) {
1670        let _ = sm.poll().expect("poll info");
1671        sm.on_response(Expectation::CommissioningInfo, &[0x15, 0x18])
1672            .expect("info ok");
1673        let _ = sm.poll().expect("poll arm failsafe");
1674        sm.on_response(
1675            Expectation::ArmFailsafeResponse,
1676            &[0x15, 0x24, 0x00, 0x00, 0x18],
1677        )
1678        .expect("arm ok");
1679        let _ = sm.poll().expect("poll config regulatory");
1680        sm.on_response(
1681            Expectation::SetRegulatoryConfigResponse,
1682            &[0x15, 0x24, 0x00, 0x00, 0x18],
1683        )
1684        .expect("regulatory ok");
1685    }
1686
1687    fn synthetic_cert_chain_response(cert: &[u8]) -> Vec<u8> {
1688        use matter_codec::{Tag, TlvWriter};
1689        let mut buf = Vec::new();
1690        let mut w = TlvWriter::new(&mut buf);
1691        w.start_structure(Tag::Anonymous).expect("infallible");
1692        w.put_bytes(Tag::Context(0), cert).expect("infallible");
1693        w.end_container().expect("infallible");
1694        buf
1695    }
1696
1697    fn nonce_from_attestation_invoke(act: &Action) -> [u8; 32] {
1698        match act {
1699            Action::Invoke { payload, .. } => {
1700                use matter_codec::{Element, Tag, TlvReader, Value};
1701                let mut r = TlvReader::new(payload);
1702                let _ = r.next().expect("reader").expect("anon-struct-start");
1703                loop {
1704                    match r.next().expect("reader") {
1705                        Some(Element::Scalar {
1706                            tag: Tag::Context(0),
1707                            value: Value::Bytes(b),
1708                        }) => {
1709                            return b.as_slice().try_into().expect("32 bytes");
1710                        }
1711                        Some(_) => {}
1712                        None => panic!("no nonce found"),
1713                    }
1714                }
1715            }
1716            other => panic!("expected Invoke, got {other:?}"),
1717        }
1718    }
1719
1720    #[test]
1721    fn poll_at_send_pai_emits_certificate_chain_request_pai() {
1722        let fabric = make_fabric_record();
1723        let setup = make_setup_payload();
1724        let paa = PaaTrustStore::with_csa_test_roots();
1725        let cd = CdSigningRoots::with_csa_test_roots();
1726        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1727        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1728        let mut sm = Commissioner::new(cfg).expect("valid config");
1729        drive_to_send_pai_cert_request(&mut sm);
1730        assert_eq!(sm.stage(), Stage::SendPaiCertRequest);
1731        match sm.poll().expect("poll PAI") {
1732            Action::Invoke {
1733                cluster,
1734                command,
1735                expect,
1736                payload,
1737                ..
1738            } => {
1739                assert_eq!(cluster, 0x003E);
1740                assert_eq!(command, 0x02);
1741                assert_eq!(expect, Expectation::PaiCertChainResponse);
1742                // CertificateChainTypeEnum (spec §11.18.5.2): 2 = PAI.
1743                assert_eq!(payload, vec![0x15, 0x24, 0x00, 0x02, 0x18]);
1744            }
1745            other => panic!("expected Invoke, got {other:?}"),
1746        }
1747    }
1748
1749    #[test]
1750    fn poll_at_send_dac_emits_certificate_chain_request_dac() {
1751        let fabric = make_fabric_record();
1752        let setup = make_setup_payload();
1753        let paa = PaaTrustStore::with_csa_test_roots();
1754        let cd = CdSigningRoots::with_csa_test_roots();
1755        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1756        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1757        let mut sm = Commissioner::new(cfg).expect("valid config");
1758        drive_to_send_pai_cert_request(&mut sm);
1759        let _ = sm.poll().expect("poll PAI");
1760        let pai_response = synthetic_cert_chain_response(&[0xAA, 0xBB, 0xCC]);
1761        sm.on_response(Expectation::PaiCertChainResponse, &pai_response)
1762            .expect("PAI accepted");
1763        assert_eq!(sm.stage(), Stage::SendDacCertRequest);
1764        match sm.poll().expect("poll DAC") {
1765            Action::Invoke {
1766                cluster,
1767                command,
1768                expect,
1769                payload,
1770                ..
1771            } => {
1772                assert_eq!(cluster, 0x003E);
1773                assert_eq!(command, 0x02);
1774                assert_eq!(expect, Expectation::DacCertChainResponse);
1775                // CertificateChainTypeEnum (spec §11.18.5.2): 1 = DAC.
1776                assert_eq!(payload, vec![0x15, 0x24, 0x00, 0x01, 0x18]);
1777            }
1778            other => panic!("expected Invoke, got {other:?}"),
1779        }
1780    }
1781
1782    #[test]
1783    fn send_attestation_request_uses_fresh_random_nonce_each_time() {
1784        let fabric = make_fabric_record();
1785        let setup = make_setup_payload();
1786        let paa = PaaTrustStore::with_csa_test_roots();
1787        let cd = CdSigningRoots::with_csa_test_roots();
1788
1789        let rng_a: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1790        let cfg_a = base_config(&fabric, &setup, &paa, &cd, rng_a);
1791        let mut sm_a = Commissioner::new(cfg_a).expect("valid config");
1792        drive_to_send_pai_cert_request(&mut sm_a);
1793        let _ = sm_a.poll().expect("poll PAI a");
1794        let pai_response = synthetic_cert_chain_response(&[0xAA]);
1795        sm_a.on_response(Expectation::PaiCertChainResponse, &pai_response)
1796            .expect("ok");
1797        let _ = sm_a.poll().expect("poll DAC a");
1798        let dac_response = synthetic_cert_chain_response(&[0xBB]);
1799        sm_a.on_response(Expectation::DacCertChainResponse, &dac_response)
1800            .expect("ok");
1801        let nonce_a = nonce_from_attestation_invoke(&sm_a.poll().expect("poll att a"));
1802
1803        let rng_b: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1804        let cfg_b = base_config(&fabric, &setup, &paa, &cd, rng_b);
1805        let mut sm_b = Commissioner::new(cfg_b).expect("valid config");
1806        drive_to_send_pai_cert_request(&mut sm_b);
1807        let _ = sm_b.poll().expect("poll PAI b");
1808        sm_b.on_response(Expectation::PaiCertChainResponse, &pai_response)
1809            .expect("ok");
1810        let _ = sm_b.poll().expect("poll DAC b");
1811        sm_b.on_response(Expectation::DacCertChainResponse, &dac_response)
1812            .expect("ok");
1813        let nonce_b = nonce_from_attestation_invoke(&sm_b.poll().expect("poll att b"));
1814
1815        assert_ne!(
1816            nonce_a, nonce_b,
1817            "two independent runs should use different random nonces"
1818        );
1819    }
1820
1821    // --- M6.4.4 T35-T40: CSR + NOC issuance flow tests ---
1822
1823    /// Extract the 32-byte `CSRNonce` from a `CSRRequest` Invoke payload.
1824    /// Mirrors `nonce_from_attestation_invoke` — same TLV shape, both
1825    /// pull the bytes at context tag 0 inside the anonymous outer struct.
1826    fn nonce_from_csr_invoke(act: &Action) -> [u8; 32] {
1827        match act {
1828            Action::Invoke { payload, .. } => {
1829                use matter_codec::{Element, Tag, TlvReader, Value};
1830                let mut r = TlvReader::new(payload);
1831                let _ = r.next().expect("reader").expect("anon-struct-start");
1832                loop {
1833                    match r.next().expect("reader") {
1834                        Some(Element::Scalar {
1835                            tag: Tag::Context(0),
1836                            value: Value::Bytes(b),
1837                        }) => {
1838                            return b.as_slice().try_into().expect("32 bytes");
1839                        }
1840                        Some(_) => {}
1841                        None => panic!("no nonce found"),
1842                    }
1843                }
1844            }
1845            other => panic!("expected Invoke, got {other:?}"),
1846        }
1847    }
1848
1849    /// Glass-box test: jumps the cursor straight to
1850    /// `Stage::SendOpCertSigningRequest` (bypassing PAI/DAC/Att, which
1851    /// would otherwise demand real fixtures M6.4.2's verifier accepts)
1852    /// and checks the emitted `CSRRequest` Invoke's nonce randomness
1853    /// across two independent commissioner instances. The full
1854    /// integration drive ships in T41 with real matter.js fixtures.
1855    #[test]
1856    fn send_op_cert_signing_request_emits_csr_with_random_nonce() {
1857        let fabric = make_fabric_record();
1858        let setup = make_setup_payload();
1859        let paa = PaaTrustStore::with_csa_test_roots();
1860        let cd = CdSigningRoots::with_csa_test_roots();
1861
1862        let rng_a: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1863        let cfg_a = base_config(&fabric, &setup, &paa, &cd, rng_a);
1864        let mut sm_a = Commissioner::new(cfg_a).expect("valid config");
1865        // Jump the cursor + plant the prerequisite DAC slot. Glass-box
1866        // crate-private access is fine inside the in-module `tests`
1867        // submodule.
1868        sm_a.stage = Stage::SendOpCertSigningRequest;
1869        sm_a.dac_der = Some(vec![0xAA, 0xBB]);
1870        let act_a = sm_a.poll().expect("poll csr a");
1871        let nonce_a = nonce_from_csr_invoke(&act_a);
1872
1873        let rng_b: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1874        let cfg_b = base_config(&fabric, &setup, &paa, &cd, rng_b);
1875        let mut sm_b = Commissioner::new(cfg_b).expect("valid config");
1876        sm_b.stage = Stage::SendOpCertSigningRequest;
1877        sm_b.dac_der = Some(vec![0xAA, 0xBB]);
1878        let act_b = sm_b.poll().expect("poll csr b");
1879        let nonce_b = nonce_from_csr_invoke(&act_b);
1880
1881        assert_ne!(
1882            nonce_a, nonce_b,
1883            "two independent runs should use different CSR nonces"
1884        );
1885
1886        match act_a {
1887            Action::Invoke {
1888                cluster,
1889                command,
1890                expect,
1891                ..
1892            } => {
1893                assert_eq!(cluster, 0x003E);
1894                assert_eq!(command, 0x04);
1895                assert_eq!(expect, Expectation::CsrResponse);
1896            }
1897            other => panic!("expected Invoke, got {other:?}"),
1898        }
1899    }
1900
1901    /// Glass-box test: with the CSR + NOC artefacts pre-populated and
1902    /// the cursor placed at `Stage::SendNoc`, `poll()` must emit an
1903    /// `AddNOC` Invoke targeting cluster `0x003E` / command `0x06`.
1904    /// Then drive the synthetic `NOCResponse { status: 0 }` through
1905    /// `on_response` and assert the cursor lands on
1906    /// `Stage::ReadNetworkCommissioningInfo`.
1907    #[test]
1908    fn drive_through_send_noc_with_synthetic_noc_response() {
1909        use matter_cert::{
1910            BasicConstraints, DistinguishedName, DnAttribute, Extensions, MatterCertificate,
1911            PublicKey,
1912        };
1913
1914        let fabric = make_fabric_record();
1915        let setup = make_setup_payload();
1916        let paa = PaaTrustStore::with_csa_test_roots();
1917        let cd = CdSigningRoots::with_csa_test_roots();
1918        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1919        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1920        let mut sm = Commissioner::new(cfg).expect("valid config");
1921
1922        // Skip past attestation/CSR machinery — we want to test the
1923        // SendNoc dispatch arm + the NOC response handler in isolation.
1924        // Plant a structurally-valid synthetic NOC the AddNOC encoder
1925        // can re-serialise. (The device side wouldn't accept it, but
1926        // we're not talking to a device — we feed a canned response.)
1927        let mut key_bytes = [0u8; 65];
1928        key_bytes[0] = 0x04;
1929        let synthetic_noc = MatterCertificate::builder()
1930            .serial(vec![1, 2, 3])
1931            .issuer(fabric.root_cert.subject().clone())
1932            .subject(DistinguishedName::new(vec![
1933                DnAttribute::FabricId(fabric.fabric_id),
1934                DnAttribute::NodeId(0x2),
1935            ]))
1936            .validity(
1937                MatterTime::from_unix_secs(1_704_067_200),
1938                MatterTime::NO_EXPIRY,
1939            )
1940            .public_key(PublicKey::new(key_bytes).expect("valid sec1 prefix"))
1941            .extensions(
1942                Extensions::builder()
1943                    .basic_constraints(Some(BasicConstraints::new(false, None)))
1944                    .build(),
1945            )
1946            .build_unsigned()
1947            .expect("builder")
1948            .assemble([0u8; 64]);
1949
1950        sm.stage = Stage::SendNoc;
1951        sm.issued_noc = Some(synthetic_noc);
1952        sm.issued_noc_public_key = Some(key_bytes);
1953
1954        match sm.poll().expect("poll SendNoc") {
1955            Action::Invoke {
1956                cluster,
1957                command,
1958                expect,
1959                ..
1960            } => {
1961                assert_eq!(cluster, 0x003E);
1962                assert_eq!(command, 0x06);
1963                assert_eq!(expect, Expectation::NocResponse);
1964            }
1965            other => panic!("expected Invoke, got {other:?}"),
1966        }
1967
1968        // Synthetic NOCResponse: anonymous struct with status=0 + fabric_index=1.
1969        let mut noc_response = Vec::new();
1970        {
1971            use matter_codec::{Tag, TlvWriter};
1972            let mut w = TlvWriter::new(&mut noc_response);
1973            w.start_structure(Tag::Anonymous).expect("infallible");
1974            w.put_uint(Tag::Context(0), 0).expect("infallible"); // status = OK
1975            w.put_uint(Tag::Context(1), 1).expect("infallible"); // fabric_index = 1
1976            w.end_container().expect("infallible");
1977        }
1978        sm.on_response(Expectation::NocResponse, &noc_response)
1979            .expect("NocResponse accepted");
1980        assert_eq!(sm.stage(), Stage::ReadNetworkCommissioningInfo);
1981    }
1982
1983    /// Glass-box test: a non-zero NOC status surfaces as
1984    /// `CommissioningError::DeviceImStatus { stage: SendNoc, ... }`
1985    /// and transitions the cursor to `Failed`.
1986    #[test]
1987    fn send_noc_failure_status_aborts_with_device_im_status() {
1988        use matter_cert::{
1989            BasicConstraints, DistinguishedName, DnAttribute, Extensions, MatterCertificate,
1990            PublicKey,
1991        };
1992
1993        let fabric = make_fabric_record();
1994        let setup = make_setup_payload();
1995        let paa = PaaTrustStore::with_csa_test_roots();
1996        let cd = CdSigningRoots::with_csa_test_roots();
1997        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1998        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1999        let mut sm = Commissioner::new(cfg).expect("valid config");
2000
2001        let mut key_bytes = [0u8; 65];
2002        key_bytes[0] = 0x04;
2003        let synthetic_noc = MatterCertificate::builder()
2004            .serial(vec![9])
2005            .issuer(fabric.root_cert.subject().clone())
2006            .subject(DistinguishedName::new(vec![
2007                DnAttribute::FabricId(fabric.fabric_id),
2008                DnAttribute::NodeId(0x2),
2009            ]))
2010            .validity(
2011                MatterTime::from_unix_secs(1_704_067_200),
2012                MatterTime::NO_EXPIRY,
2013            )
2014            .public_key(PublicKey::new(key_bytes).expect("valid sec1 prefix"))
2015            .extensions(
2016                Extensions::builder()
2017                    .basic_constraints(Some(BasicConstraints::new(false, None)))
2018                    .build(),
2019            )
2020            .build_unsigned()
2021            .expect("builder")
2022            .assemble([0u8; 64]);
2023
2024        sm.stage = Stage::SendNoc;
2025        sm.issued_noc = Some(synthetic_noc);
2026        sm.issued_noc_public_key = Some(key_bytes);
2027
2028        let _ = sm.poll().expect("poll SendNoc");
2029
2030        // status = 9 (InvalidNOC, spec §11.18.6.1).
2031        let mut bad_response = Vec::new();
2032        {
2033            use matter_codec::{Tag, TlvWriter};
2034            let mut w = TlvWriter::new(&mut bad_response);
2035            w.start_structure(Tag::Anonymous).expect("infallible");
2036            w.put_uint(Tag::Context(0), 9).expect("infallible");
2037            w.end_container().expect("infallible");
2038        }
2039        let err = sm
2040            .on_response(Expectation::NocResponse, &bad_response)
2041            .expect_err("non-zero NOC status should fail");
2042        assert!(matches!(
2043            err,
2044            CommissioningError::DeviceImStatus {
2045                stage: Stage::SendNoc,
2046                im_status: 9,
2047            }
2048        ));
2049        assert_eq!(sm.stage(), Stage::Failed);
2050    }
2051
2052    /// Glass-box test: `SendTrustedRootCert` emits an `AddTrustedRootCertificate`
2053    /// Invoke whose payload TLV starts with anonymous-struct + context-0
2054    /// octet-string carrying the RCAC TLV bytes. A subsequent `[0x00]`
2055    /// status-ack advances the cursor to `Stage::SendNoc`.
2056    #[test]
2057    fn send_trusted_root_cert_emits_invoke_and_status_ack_advances() {
2058        let fabric = make_fabric_record();
2059        let setup = make_setup_payload();
2060        let paa = PaaTrustStore::with_csa_test_roots();
2061        let cd = CdSigningRoots::with_csa_test_roots();
2062        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2063        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2064        let mut sm = Commissioner::new(cfg).expect("valid config");
2065
2066        sm.stage = Stage::SendTrustedRootCert;
2067
2068        match sm.poll().expect("poll SendTrustedRootCert") {
2069            Action::Invoke {
2070                cluster,
2071                command,
2072                expect,
2073                payload,
2074                ..
2075            } => {
2076                assert_eq!(cluster, 0x003E);
2077                assert_eq!(command, 0x0B);
2078                assert_eq!(expect, Expectation::AddTrustedRootResponse);
2079                // Sanity: payload is at least the anonymous-struct
2080                // wrapper + a non-trivial octet-string of RCAC TLV.
2081                assert!(payload.len() > 16, "RCAC TLV too short: {}", payload.len());
2082                assert_eq!(payload[0], 0x15); // anonymous struct start
2083            }
2084            other => panic!("expected Invoke, got {other:?}"),
2085        }
2086
2087        // Status-ack of 0x00 (success) advances to SendNoc.
2088        sm.on_response(Expectation::AddTrustedRootResponse, &[0x00])
2089            .expect("status-ack accepted");
2090        assert_eq!(sm.stage(), Stage::SendNoc);
2091    }
2092
2093    // --- M6.4.5 T44-T47: PASE -> CASE handoff + CommissioningComplete tests ---
2094
2095    #[test]
2096    fn find_operational_for_complete_emits_establish_case() {
2097        let fabric = make_fabric_record();
2098        let setup = make_setup_payload();
2099        let paa = PaaTrustStore::with_csa_test_roots();
2100        let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2101        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2102        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2103        let mut sm = Commissioner::new(cfg).expect("valid config");
2104        sm.stage = Stage::FindOperationalForComplete;
2105        match sm.poll().expect("poll establish case") {
2106            Action::EstablishCase {
2107                fabric_id,
2108                peer_node_id,
2109            } => {
2110                assert_eq!(fabric_id, fabric.fabric_id);
2111                assert_eq!(peer_node_id, 0x2); // matches base_config's assigned_node_id
2112            }
2113            other => panic!("expected EstablishCase, got {other:?}"),
2114        }
2115        assert!(sm.awaiting_case_session);
2116    }
2117
2118    #[test]
2119    fn on_case_established_advances_to_send_complete() {
2120        let fabric = make_fabric_record();
2121        let setup = make_setup_payload();
2122        let paa = PaaTrustStore::with_csa_test_roots();
2123        let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2124        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2125        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2126        let mut sm = Commissioner::new(cfg).expect("valid config");
2127        sm.stage = Stage::FindOperationalForComplete;
2128        let _ = sm.poll().expect("emit EstablishCase");
2129        sm.on_case_established().expect("case established");
2130        assert_eq!(sm.stage(), Stage::SendComplete);
2131        assert!(!sm.awaiting_case_session);
2132    }
2133
2134    #[test]
2135    fn on_case_established_without_pending_emits_out_of_order() {
2136        let fabric = make_fabric_record();
2137        let setup = make_setup_payload();
2138        let paa = PaaTrustStore::with_csa_test_roots();
2139        let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2140        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2141        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2142        let mut sm = Commissioner::new(cfg).expect("valid config");
2143        let err = sm.on_case_established().expect_err("no pending establish");
2144        assert!(matches!(err, CommissioningError::OutOfOrderResponse(_)));
2145    }
2146
2147    #[test]
2148    fn send_complete_emits_invoke_over_case_session() {
2149        let fabric = make_fabric_record();
2150        let setup = make_setup_payload();
2151        let paa = PaaTrustStore::with_csa_test_roots();
2152        let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2153        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2154        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2155        let mut sm = Commissioner::new(cfg).expect("valid config");
2156        sm.stage = Stage::SendComplete;
2157        match sm.poll().expect("poll send complete") {
2158            Action::Invoke {
2159                session,
2160                cluster,
2161                command,
2162                expect,
2163                payload,
2164                ..
2165            } => {
2166                assert_eq!(session, crate::state_machine::SessionContext::Case);
2167                assert_eq!(cluster, 0x0030);
2168                assert_eq!(command, 0x04); // CommissioningComplete
2169                assert_eq!(expect, Expectation::CommissioningCompleteResponse);
2170                // CommissioningComplete carries no payload fields — empty struct.
2171                assert_eq!(payload, vec![0x15, 0x18]);
2172            }
2173            other => panic!("expected Invoke, got {other:?}"),
2174        }
2175    }
2176
2177    #[test]
2178    fn send_complete_success_advances_to_cleanup() {
2179        let fabric = make_fabric_record();
2180        let setup = make_setup_payload();
2181        let paa = PaaTrustStore::with_csa_test_roots();
2182        let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2183        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2184        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2185        let mut sm = Commissioner::new(cfg).expect("valid config");
2186        sm.stage = Stage::SendComplete;
2187        let _ = sm.poll().expect("emit invoke");
2188        sm.on_response(
2189            Expectation::CommissioningCompleteResponse,
2190            &[0x15, 0x24, 0x00, 0x00, 0x18], // error_code = 0
2191        )
2192        .expect("complete ok");
2193        assert_eq!(sm.stage(), Stage::Cleanup);
2194    }
2195
2196    #[test]
2197    fn cleanup_emits_done_with_noc_public_key() {
2198        let fabric = make_fabric_record();
2199        let setup = make_setup_payload();
2200        let paa = PaaTrustStore::with_csa_test_roots();
2201        let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2202        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2203        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2204        let mut sm = Commissioner::new(cfg).expect("valid config");
2205        sm.stage = Stage::Cleanup;
2206        sm.issued_noc_public_key = Some([0xCA; 65]);
2207        match sm.poll().expect("poll cleanup") {
2208            Action::Done(cf) => {
2209                assert_eq!(cf.peer_node_id, 0x2);
2210                assert_eq!(cf.peer_root_public_key, [0xCA; 65]);
2211                assert_eq!(cf.terminated_at, Stage::Cleanup);
2212                assert_eq!(cf.fabric.fabric_id, fabric.fabric_id);
2213            }
2214            other => panic!("expected Done, got {other:?}"),
2215        }
2216    }
2217
2218    // --- M6.4.5 T49: CaseFailed negative coverage ---
2219
2220    #[test]
2221    fn case_failed_response_aborts_with_case_establishment_failed() {
2222        let fabric = make_fabric_record();
2223        let setup = make_setup_payload();
2224        let paa = PaaTrustStore::with_csa_test_roots();
2225        let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2226        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2227        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2228        let mut sm = Commissioner::new(cfg).expect("valid config");
2229
2230        // Glass-box: jump to FindOperationalForComplete (skipping the
2231        // attestation + CSR + NOC stages that need real fixtures).
2232        sm.stage = Stage::FindOperationalForComplete;
2233        let _ = sm.poll().expect("emit EstablishCase");
2234        assert!(sm.awaiting_case_session);
2235
2236        // Caller signals CASE establishment failure.
2237        let err = sm
2238            .on_response(Expectation::CaseFailed, &[])
2239            .expect_err("CaseFailed should error");
2240        assert!(matches!(err, CommissioningError::CaseEstablishmentFailed));
2241        assert_eq!(sm.stage(), Stage::Failed);
2242        assert!(!sm.awaiting_case_session);
2243
2244        // Subsequent poll emits Action::Abort with send_disarm_failsafe=true.
2245        match sm.poll().expect("emit abort") {
2246            Action::Abort {
2247                send_disarm_failsafe,
2248                reason,
2249            } => {
2250                assert!(send_disarm_failsafe);
2251                assert!(
2252                    reason.contains("CASE"),
2253                    "abort reason should mention CASE: {reason}"
2254                );
2255            }
2256            other => panic!("expected Abort, got {other:?}"),
2257        }
2258    }
2259
2260    #[test]
2261    fn case_failed_when_not_awaiting_returns_out_of_order() {
2262        let fabric = make_fabric_record();
2263        let setup = make_setup_payload();
2264        let paa = PaaTrustStore::with_csa_test_roots();
2265        let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2266        let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2267        let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2268        let mut sm = Commissioner::new(cfg).expect("valid config");
2269        let err = sm
2270            .on_response(Expectation::CaseFailed, &[])
2271            .expect_err("CaseFailed without pending should error");
2272        assert!(matches!(err, CommissioningError::OutOfOrderResponse(_)));
2273    }
2274
2275    /// Returns a fully-populated, valid [`CommissionerConfig`] for use in
2276    /// unit tests that only need to mutate one field. All held references
2277    /// are leaked so the config is `'static`; acceptable for test code.
2278    fn sample_valid_config() -> CommissionerConfig<'static> {
2279        use std::sync::OnceLock;
2280        static FABRIC: OnceLock<FabricRecord> = OnceLock::new();
2281        static SETUP: OnceLock<SetupPayload> = OnceLock::new();
2282        static PAA: OnceLock<PaaTrustStore> = OnceLock::new();
2283        static CD: OnceLock<crate::attestation::CdSigningRoots> = OnceLock::new();
2284
2285        let fabric = FABRIC.get_or_init(make_fabric_record);
2286        let setup = SETUP.get_or_init(make_setup_payload);
2287        let paa = PAA.get_or_init(PaaTrustStore::with_csa_test_roots);
2288        let cd = CD.get_or_init(crate::attestation::CdSigningRoots::with_csa_test_roots);
2289        let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
2290
2291        CommissionerConfig {
2292            pase_attestation_challenge: [0u8; 16],
2293            fabric,
2294            setup_payload: setup,
2295            paa_trust_store: paa,
2296            cd_signing_roots: cd,
2297            commissioner_node_id: 0x1,
2298            assigned_node_id: 0x2,
2299            ipk_epoch_key: [0x42_u8; 16],
2300            case_admin_subject: 0x1,
2301            admin_vendor_id: 0xFFF1,
2302            now: MatterTime::from_unix_secs(1_704_067_200),
2303            rng,
2304            network: NetworkCredentials::AlreadyOnNetwork,
2305        }
2306    }
2307
2308    #[test]
2309    fn empty_ssid_is_rejected() {
2310        let mut config = sample_valid_config();
2311        config.network = NetworkCredentials::WiFi(WiFiCredentials {
2312            ssid: vec![],
2313            credentials: vec![],
2314        });
2315        let Err(err) = Commissioner::new(config) else {
2316            panic!("empty ssid should fail");
2317        };
2318        assert!(
2319            matches!(err, CommissioningError::InvalidConfig(m) if m.contains("ssid")),
2320            "got {err:?}",
2321        );
2322    }
2323
2324    #[test]
2325    fn oversize_ssid_is_rejected() {
2326        let mut config = sample_valid_config();
2327        config.network = NetworkCredentials::WiFi(WiFiCredentials {
2328            ssid: vec![b'a'; 33],
2329            credentials: vec![],
2330        });
2331        let Err(err) = Commissioner::new(config) else {
2332            panic!("33-byte ssid should fail");
2333        };
2334        assert!(
2335            matches!(err, CommissioningError::InvalidConfig(m) if m.contains("≤32")),
2336            "got {err:?}",
2337        );
2338    }
2339
2340    #[test]
2341    fn oversize_credentials_is_rejected() {
2342        let mut config = sample_valid_config();
2343        config.network = NetworkCredentials::WiFi(WiFiCredentials {
2344            ssid: b"matter".to_vec(),
2345            credentials: vec![0u8; 65],
2346        });
2347        let Err(err) = Commissioner::new(config) else {
2348            panic!("65-byte credentials should fail");
2349        };
2350        assert!(
2351            matches!(err, CommissioningError::InvalidConfig(m) if m.contains("≤64")),
2352            "got {err:?}",
2353        );
2354    }
2355
2356    #[test]
2357    fn wifi_credentials_none_is_accepted() {
2358        let mut config = sample_valid_config();
2359        config.network = NetworkCredentials::AlreadyOnNetwork;
2360        Commissioner::new(config).expect("AlreadyOnNetwork should pass validation");
2361    }
2362
2363    #[test]
2364    fn network_credentials_thread_variant_accepted() {
2365        // Minimal well-formed dataset: a single Extended PAN ID TLV
2366        // (type 0x02, length 8). ThreadDataset::new self-validates.
2367        let ds =
2368            crate::thread_dataset::ThreadDataset::new(vec![0x02, 0x08, 0, 0, 0, 0, 0, 0, 0, 0])
2369                .expect("minimal ext-pan-id dataset is valid");
2370        let mut config = sample_valid_config();
2371        config.network = NetworkCredentials::Thread(ds);
2372        let c = Commissioner::new(config).expect("Thread network should pass validation");
2373        assert!(matches!(c.network(), NetworkCredentials::Thread(_)));
2374    }
2375
2376    #[test]
2377    fn wifi_credentials_debug_redacts_passphrase() {
2378        let creds = WiFiCredentials {
2379            ssid: b"matter".to_vec(),
2380            credentials: b"hunter22".to_vec(),
2381        };
2382        let rendered = format!("{creds:?}");
2383        assert!(
2384            !rendered.contains("hunter22"),
2385            "Debug must not contain credentials bytes: {rendered}",
2386        );
2387        assert!(rendered.contains("redacted"), "got {rendered}");
2388        assert!(
2389            rendered.contains('8'),
2390            "credentials length should appear: {rendered}"
2391        );
2392        assert!(
2393            rendered.contains('6'),
2394            "ssid length should appear: {rendered}"
2395        );
2396    }
2397
2398    // --- M6.5.2 T14: failsafe expiry derivation from BasicCommissioningInfo ---
2399
2400    #[test]
2401    fn failsafe_expiry_derives_from_basic_commissioning_info() {
2402        let mut sm = Commissioner::new(sample_valid_config()).expect("valid config");
2403        // Advance to ReadCommissioningInfo and feed a BasicCommissioningInfo with 120s.
2404        let _initial = sm.poll().expect("initial poll");
2405        let response = vec![
2406            0x15, 0x25, 0x00, 0x78, 0x00, // u16 = 120
2407            0x18,
2408        ];
2409        sm.on_response(Expectation::CommissioningInfo, &response)
2410            .expect("commissioning info accepted");
2411        // Now ArmFailsafe should emit with expiry=120.
2412        let action = sm.poll().expect("arm-failsafe poll");
2413        match action {
2414            Action::Invoke {
2415                payload,
2416                cluster,
2417                command,
2418                ..
2419            } => {
2420                assert_eq!(cluster, 0x0030);
2421                assert_eq!(command, 0x00);
2422                // ArmFailSafe payload byte for expiry: TLV-encoded u8/u16 at context tag 0.
2423                // For value 120 the smallest-width encoding is u8 = 0x24 0x00 0x78.
2424                assert!(
2425                    payload.windows(3).any(|w| w == [0x24, 0x00, 0x78]),
2426                    "ArmFailSafe payload should carry expiry=120: {payload:02x?}",
2427                );
2428            }
2429            other => panic!("expected Invoke, got {other:?}"),
2430        }
2431    }
2432
2433    #[test]
2434    fn failsafe_expiry_falls_back_to_60_on_empty_basic_commissioning_info() {
2435        let mut sm = Commissioner::new(sample_valid_config()).expect("valid config");
2436        let _initial = sm.poll().expect("initial poll");
2437        // Feed a well-formed empty struct — decode_basic_commissioning_info
2438        // returns None when the failsafe field is missing, so the M6.4
2439        // fallback of 60s applies.
2440        sm.on_response(Expectation::CommissioningInfo, &[0x15, 0x18])
2441            .expect("empty struct accepted");
2442        let action = sm.poll().expect("arm-failsafe poll");
2443        if let Action::Invoke { payload, .. } = action {
2444            // 60 = 0x3C, anonymous struct with context-tag-0 u8.
2445            assert!(
2446                payload.windows(3).any(|w| w == [0x24, 0x00, 0x3C]),
2447                "ArmFailSafe payload should carry expiry=60 fallback: {payload:02x?}",
2448            );
2449        }
2450    }
2451
2452    // --- M6.5.2 T15: breadcrumb monotonicity ---
2453
2454    #[test]
2455    fn breadcrumb_increases_across_commands() {
2456        fn extract_uint_at_tag(payload: &[u8], tag_num: u8) -> Option<u64> {
2457            use matter_codec::{Element, Tag, TlvReader, Value};
2458            let mut reader = TlvReader::new(payload);
2459            while let Ok(Some(elem)) = reader.next() {
2460                if let Element::Scalar {
2461                    tag: Tag::Context(t),
2462                    value: Value::Uint(v),
2463                } = elem
2464                {
2465                    if t == tag_num {
2466                        return Some(v);
2467                    }
2468                }
2469            }
2470            None
2471        }
2472
2473        let mut sm = Commissioner::new(sample_valid_config()).expect("valid config");
2474        let mut breadcrumbs: Vec<u64> = Vec::new();
2475
2476        // Poll #1: ReadCommissioningInfo (no breadcrumb).
2477        let _ = sm.poll().expect("read commissioning info");
2478        sm.on_response(Expectation::CommissioningInfo, &[0x15, 0x18])
2479            .expect("info accepted");
2480
2481        // Poll #2: ArmFailsafe — first breadcrumb-bearing command.
2482        let action = sm.poll().expect("arm-failsafe");
2483        if let Action::Invoke { payload, .. } = action {
2484            if let Some(b) = extract_uint_at_tag(&payload, 1) {
2485                breadcrumbs.push(b);
2486            }
2487        }
2488        sm.on_response(
2489            Expectation::ArmFailsafeResponse,
2490            &[0x15, 0x24, 0x00, 0x00, 0x18],
2491        )
2492        .expect("arm-failsafe ok");
2493
2494        // Poll #3: SetRegulatoryConfig — second breadcrumb-bearing command.
2495        let action = sm.poll().expect("set-regulatory");
2496        if let Action::Invoke { payload, .. } = action {
2497            if let Some(b) = extract_uint_at_tag(&payload, 2) {
2498                breadcrumbs.push(b);
2499            }
2500        }
2501
2502        assert_eq!(
2503            breadcrumbs.len(),
2504            2,
2505            "should have extracted exactly two breadcrumbs, got {breadcrumbs:?}",
2506        );
2507        assert!(
2508            breadcrumbs[0] < breadcrumbs[1],
2509            "breadcrumbs should be strictly increasing: {breadcrumbs:?}",
2510        );
2511    }
2512}