Skip to main content

matter_commissioning/state_machine/
stage.rs

1//! `Stage` — every cursor position the state machine can occupy.
2//!
3//! Stages match `project-chip/connectedhomeip`'s `CommissioningStage`
4//! enum (translated to Rust style and trimmed to the M6.5 subset).
5//! Stages we defer past M6.5 are noted inline as `// deferred: kFoo`
6//! so future expansion is mechanical.
7
8#![forbid(unsafe_code)]
9
10/// Cursor position inside the commissioning sequence.
11///
12/// Variants are ordered top-to-bottom in transition order. The transition
13/// function lives in `next_stage` (crate-internal).
14#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum Stage {
17    /// Entry state — caller has just constructed the [`super::Commissioner`]
18    /// with a valid PASE session. No action emitted; advances on first
19    /// `poll()` to [`Self::ReadCommissioningInfo`].
20    SecurePairing,
21    /// Read `BasicCommissioningInfo`, `RegulatoryConfig`, etc. from the
22    /// `GeneralCommissioning` cluster (id `0x0030`) so the commissioner
23    /// knows `failsafe_expiry_length_seconds` before arming the failsafe.
24    ReadCommissioningInfo,
25    /// `GeneralCommissioning::ArmFailSafe` (command id `0x00`).
26    ArmFailsafe,
27    /// `GeneralCommissioning::SetRegulatoryConfig` (command id `0x02`).
28    ConfigRegulatory,
29    /// `OperationalCredentials::CertificateChainRequest` with `type=PAI`
30    /// (cluster `0x003E`, command `0x02`, type enum `0x01`).
31    SendPaiCertRequest,
32    /// `OperationalCredentials::CertificateChainRequest` with `type=DAC`
33    /// (type enum `0x02`).
34    SendDacCertRequest,
35    /// `OperationalCredentials::AttestationRequest` (command `0x00`).
36    SendAttestationRequest,
37    /// Off-wire: chain + signature + CD verification.
38    AttestationVerification,
39    /// `OperationalCredentials::CSRRequest` (command `0x04`).
40    SendOpCertSigningRequest,
41    /// Off-wire: PKCS#10 self-signature + DAC attestation + nonce echo.
42    ValidateCsr,
43    /// Off-wire: build + sign the NOC under the commissioner's RCAC.
44    GenerateNocChain,
45    /// `OperationalCredentials::AddTrustedRootCertificate` (command `0x0B`).
46    SendTrustedRootCert,
47    /// `OperationalCredentials::AddNOC` (command `0x06`).
48    SendNoc,
49    /// Read `NetworkCommissioning::FeatureMap` (attribute `0xFFFC`) and
50    /// `ConnectMaxTimeSeconds` (attribute `0x0003`) on endpoint 0.
51    /// `FeatureMap` determines whether the device supports Wi-Fi,
52    /// Ethernet, or Thread (or some combination). Branching at this
53    /// stage's response routes by the *supplied*
54    /// [`super::NetworkCredentials`] variant, cross-checked against the
55    /// device `FeatureMap`: matching credentials advance to
56    /// `NetworkSetup`, `AlreadyOnNetwork` skips to
57    /// `EvictPreviousCaseSessions`, and a credential type absent from the
58    /// `FeatureMap` fails with `NetworkFeatureUnsupported`.
59    ReadNetworkCommissioningInfo,
60    /// Network provisioning: `NetworkCommissioning::AddOrUpdateWiFiNetwork`
61    /// (cluster `0x0031` command `0x02`) for Wi-Fi credentials, or
62    /// `AddOrUpdateThreadNetwork` (command `0x03`) for a Thread dataset.
63    /// The stage is generic; the command is selected by the supplied
64    /// [`super::NetworkCredentials`] variant. Skipped for `AlreadyOnNetwork`
65    /// / Ethernet-only devices.
66    NetworkSetup,
67    /// Second `GeneralCommissioning::ArmFailSafe` (cluster `0x0030`
68    /// command `0x00`). Extends the failsafe window before
69    /// `ConnectNetwork` so the device has room to associate with the
70    /// operational network and re-discover the commissioner via mDNS.
71    /// The extension is sized from the device's `ConnectMaxTimeSeconds`
72    /// (Thread attach is slower than Wi-Fi association), falling back to
73    /// a generous default. Re-uses the existing
74    /// `Expectation::ArmFailsafeResponse`.
75    FailsafeBeforeNetworkEnable,
76    /// `NetworkCommissioning::ConnectNetwork` (cluster `0x0031`
77    /// command `0x06`). The device associates with the operational
78    /// network and (typically) returns `ConnectNetworkResponse` over
79    /// PASE before switching networks. The `network_id` is the SSID for
80    /// Wi-Fi and the Extended PAN ID for Thread.
81    NetworkEnable,
82    /// Evict any prior CASE session for this fabric/peer pair. M6.4
83    /// only supports new-fabric commissioning — no eviction needed —
84    /// so the stage advances immediately. Slot reserved for M8
85    /// multi-fabric work.
86    EvictPreviousCaseSessions,
87    /// Caller establishes a CASE session via mDNS find-operational +
88    /// SIGMA handshake (M6.6 mechanics). State machine emits
89    /// `Action::EstablishCase` and waits for `on_case_established()`.
90    FindOperationalForComplete,
91    /// `GeneralCommissioning::CommissioningComplete` (command `0x04`),
92    /// sent over the freshly-established CASE session.
93    SendComplete,
94    /// Terminal success. Emits `Action::Done(CommissionedFabric)`.
95    Cleanup,
96    /// Terminal failure. Emits `Action::Abort`.
97    Failed,
98    // deferred: kReadCommissioningInfo2 (post-NOC capability re-read)
99    // deferred: kConfigureUTCTime, kConfigureTimeZone, kConfigureDSTOffset, kConfigureDefaultNTP
100    // deferred: kAttestationRevocationCheck
101    // deferred: kJCMTrustVerification
102    // deferred: kICDGetRegistrationInfo, kICDRegistration
103    // deferred: kConfigureTCAcknowledgments
104    // deferred: kPrimaryOperationalNetworkFailed, kRemoveWiFiNetworkConfig, kRemoveThreadNetworkConfig
105}
106
107/// Happy-path successor of `current`. Returns `None` for terminal
108/// stages (`Cleanup`, `Failed`).
109///
110/// Used by [`super::Commissioner`] to advance the cursor after a stage
111/// completes successfully. Errors at any stage transition the cursor
112/// directly to [`Stage::Failed`] rather than calling this function.
113// Used by Commissioner::advance from M6.4.1 T6 onward.
114#[allow(dead_code)]
115#[allow(unreachable_pub)]
116#[must_use]
117pub fn next_stage(current: Stage) -> Option<Stage> {
118    Some(match current {
119        Stage::SecurePairing => Stage::ReadCommissioningInfo,
120        Stage::ReadCommissioningInfo => Stage::ArmFailsafe,
121        Stage::ArmFailsafe => Stage::ConfigRegulatory,
122        Stage::ConfigRegulatory => Stage::SendPaiCertRequest,
123        Stage::SendPaiCertRequest => Stage::SendDacCertRequest,
124        Stage::SendDacCertRequest => Stage::SendAttestationRequest,
125        Stage::SendAttestationRequest => Stage::AttestationVerification,
126        Stage::AttestationVerification => Stage::SendOpCertSigningRequest,
127        Stage::SendOpCertSigningRequest => Stage::ValidateCsr,
128        Stage::ValidateCsr => Stage::GenerateNocChain,
129        Stage::GenerateNocChain => Stage::SendTrustedRootCert,
130        Stage::SendTrustedRootCert => Stage::SendNoc,
131        Stage::SendNoc => Stage::ReadNetworkCommissioningInfo,
132        Stage::ReadNetworkCommissioningInfo => Stage::NetworkSetup,
133        Stage::NetworkSetup => Stage::FailsafeBeforeNetworkEnable,
134        Stage::FailsafeBeforeNetworkEnable => Stage::NetworkEnable,
135        Stage::NetworkEnable => Stage::EvictPreviousCaseSessions,
136        Stage::EvictPreviousCaseSessions => Stage::FindOperationalForComplete,
137        Stage::FindOperationalForComplete => Stage::SendComplete,
138        Stage::SendComplete => Stage::Cleanup,
139        Stage::Cleanup | Stage::Failed => return None,
140    })
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn happy_path_advances_through_all_stages() {
149        // Test-code carve-out: see CLAUDE.md.
150        #![allow(clippy::unwrap_used)]
151        let expected = [
152            Stage::SecurePairing,
153            Stage::ReadCommissioningInfo,
154            Stage::ArmFailsafe,
155            Stage::ConfigRegulatory,
156            Stage::SendPaiCertRequest,
157            Stage::SendDacCertRequest,
158            Stage::SendAttestationRequest,
159            Stage::AttestationVerification,
160            Stage::SendOpCertSigningRequest,
161            Stage::ValidateCsr,
162            Stage::GenerateNocChain,
163            Stage::SendTrustedRootCert,
164            Stage::SendNoc,
165            Stage::ReadNetworkCommissioningInfo,
166            Stage::NetworkSetup,
167            Stage::FailsafeBeforeNetworkEnable,
168            Stage::NetworkEnable,
169            Stage::EvictPreviousCaseSessions,
170            Stage::FindOperationalForComplete,
171            Stage::SendComplete,
172            Stage::Cleanup,
173        ];
174        for pair in expected.windows(2) {
175            assert_eq!(
176                next_stage(pair[0]),
177                Some(pair[1]),
178                "next_stage({:?}) should be Some({:?})",
179                pair[0],
180                pair[1],
181            );
182        }
183        assert_eq!(next_stage(Stage::Cleanup), None, "Cleanup is terminal");
184        assert_eq!(next_stage(Stage::Failed), None, "Failed is terminal");
185    }
186}