Skip to main content

matter_controller/
error.rs

1//! Error type for `matter-controller`.
2
3use crate::store::StoreError;
4
5/// Errors surfaced by the controller's persistence and identity layer.
6///
7/// `#[non_exhaustive]` so later sub-phases can add networked variants
8/// (e.g. `SessionLost`, `DeviceUnreachable`) without a breaking change.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum Error {
12    /// The backing [`ControllerStore`](crate::store::ControllerStore) failed.
13    #[error("store error: {0}")]
14    Store(#[from] StoreError),
15
16    /// TLV encode/decode of the snapshot blob failed.
17    #[error("TLV codec error: {0}")]
18    Codec(#[from] matter_codec::Error),
19
20    /// A certificate failed to parse or serialize.
21    #[error("certificate error: {0}")]
22    Cert(#[from] matter_cert::Error),
23
24    /// NOC/RCAC issuance failed.
25    #[error("NOC issuance error: {0}")]
26    Noc(#[from] matter_commissioning::NocError),
27
28    /// A signing key could not be generated or reconstructed.
29    #[error("signer error: {0}")]
30    Signer(String),
31
32    /// The persisted snapshot was structurally invalid or an unknown version.
33    #[error("malformed snapshot: {0}")]
34    Snapshot(String),
35
36    /// CASE session establishment failed, or a driver operation errored.
37    #[error("driver error: {0}")]
38    Driver(#[from] matter_commissioning::driver::DriverError),
39
40    /// A transport / session-manager (framing, MRP) operation failed.
41    #[error("transport error: {0}")]
42    Transport(#[from] matter_transport::Error),
43
44    /// No fabric exists, or the requested node/fabric is not addressable.
45    #[error("not commissioned: {0}")]
46    NotCommissioned(String),
47
48    /// The owning controller task has stopped (channel closed).
49    #[error("controller task is no longer running")]
50    ControllerStopped,
51
52    /// An Interaction-Model request/response failed to build or parse.
53    #[error("interaction model error: {0}")]
54    InteractionModel(#[from] matter_interaction::ImError),
55
56    /// An operational-path failure with a human-readable detail — a key
57    /// derivation (operational IPK / compressed fabric id), a transport/session
58    /// send or decode, a request timeout, or a subscription liveness timeout.
59    #[error("operational error: {0}")]
60    Operational(String),
61
62    /// Attestation trust material could not be loaded.
63    #[error("attestation trust error: {0}")]
64    Trust(String),
65
66    /// The setup code (QR / manual) could not be parsed.
67    #[error("invalid setup code: {0}")]
68    SetupCode(String),
69
70    /// No attestation trust configured; commissioning cannot verify the device.
71    #[error(
72        "no attestation trust configured — commissioning cannot verify the device's \
73         attestation. Build the controller with MatterController::builder(store)\
74         .attestation_trust(AttestationTrust::from_dirs(paa_dir, cd_dir)).build(), \
75         not MatterController::open(store)"
76    )]
77    NoTrust,
78
79    /// An `AdministratorCommissioning` command returned a non-success IM status
80    /// (e.g. 0x02 Busy, 0x03 `PAKEParameterError`, 0x04 `WindowNotOpen` reported as
81    /// a cluster status). The raw IM status byte is preserved.
82    #[error("commissioning window command rejected (IM status {0:#04x})")]
83    CommissioningWindowRejected(u8),
84
85    /// Refused to remove the controller's own fabric (would sever the CASE
86    /// session and orphan persisted device state). No `force` override exists.
87    #[error("refusing to remove our own fabric (would orphan the device)")]
88    WouldRemoveSelf,
89
90    /// An `OperationalCredentials` command returned a non-success
91    /// `NodeOperationalCertStatusEnum` (e.g. 7 `InvalidFabricIndex`). Raw code preserved.
92    #[error("operational-credentials command rejected (status {0})")]
93    OperationalCredentialsRejected(u8),
94
95    /// Refused an ACL write that would strip our own administrative access
96    /// (no Administer/CASE entry covering our commissioner node id). Prevents
97    /// orphaning the device. Checked before any bytes are sent.
98    #[error("refusing ACL write: it would remove our own administrative access")]
99    AclWouldLockOut,
100
101    /// A `Groups` / `GroupKeyManagement` command returned a non-success status
102    /// (e.g. `ResourceExhausted` from `MaxGroupsPerFabric`). Raw status preserved.
103    #[error("group command rejected (status {0})")]
104    GroupCommandRejected(u8),
105
106    /// A group send (`invoke_group`) named a `key_set_id` that has not been
107    /// provisioned on the controller's fabric (no matching
108    /// [`GroupKeySetConfig`](crate::GroupKeySetConfig) in `group_keys`). Call
109    /// [`MatterController::create_group`](crate::MatterController::create_group)
110    /// first to mint and persist the key set.
111    #[error("group key set {0} is not provisioned on this fabric")]
112    GroupNotProvisioned(u16),
113
114    /// [`MatterController::create_fabric`](crate::MatterController::create_fabric)
115    /// was called with a `fabric_id` that already exists on this controller
116    /// (issue #110 — commonly hit by calling `create_fabric` unconditionally
117    /// on every startup instead of only on a fresh store). Call
118    /// [`MatterController::fabrics`](crate::MatterController::fabrics) first
119    /// to check which fabrics already exist.
120    ///
121    /// To recover: the existing fabric is already usable — just skip the
122    /// `create_fabric` call and carry on with it. If you genuinely want a
123    /// second fabric, pass a different `fabric_id`; if you want to start over,
124    /// point the controller at a fresh store. There is no API to delete a
125    /// fabric from the controller's own store, so once a `fabric_id` is in a
126    /// store, `create_fabric` refuses it for that store's lifetime.
127    /// ([`Node::remove_fabric`](crate::Node::remove_fabric) removes *our*
128    /// fabric from a **device**, not from the controller.)
129    #[error(
130        "fabric {0:#018x} already exists — call MatterController::fabrics() to check before \
131         calling create_fabric; to recover, use the existing fabric, pass a different fabric_id, \
132         or start from a fresh store"
133    )]
134    FabricAlreadyExists(u64),
135
136    /// [`FabricConfig::validity`](crate::FabricConfig::validity) names a
137    /// window that cannot work on a device (issue #111). Rejected windows:
138    ///
139    /// - `not_before` at the Matter epoch (`MatterTime(0)`, i.e.
140    ///   2000-01-01T00:00:00Z). Not a validity-policy rejection: chip's
141    ///   `ChipEpochToASN1Time`
142    ///   (`connectedhomeip/src/credentials/CHIPCert.cpp`) encodes epoch 0 as
143    ///   `99991231235959Z` for both `notBefore` and `notAfter`, so the X.509
144    ///   TBS the device rebuilds from our TLV certificate differs from the one
145    ///   we signed and the **signature** check fails — surfacing as an opaque
146    ///   `IM status 0x85` on `AddTrustedRootCertificate`.
147    /// - `not_before` more than a day ahead of this host's clock — usually a
148    ///   millisecond timestamp passed to `MatterTime::from_unix_secs`, which
149    ///   saturates to ≈ year 2136. Such a root *installs* (chip's
150    ///   `ValidateChipRCAC` skips RCAC validity times) and then fails every
151    ///   CASE session with `kNotYetValid`.
152    /// - An inverted or empty window (`not_after <= not_before`, excluding
153    ///   `MatterTime::NO_EXPIRY`).
154    ///
155    /// The detail string names which.
156    #[error("invalid fabric validity window: {0}")]
157    InvalidFabricValidity(String),
158
159    /// The host's wall clock reads before the Matter epoch
160    /// (2000-01-01T00:00:00Z) — almost always an **unset system clock** on a
161    /// host with no RTC that has not yet reached an NTP server. Payload: the
162    /// Unix seconds actually read.
163    ///
164    /// Refused rather than used, because `MatterTime::from_unix_secs` saturates
165    /// such a reading to `MatterTime(0)`, and a certificate minted with
166    /// `notBefore == 0` cannot be installed on a device at all: chip re-encodes
167    /// epoch 0 as `99991231235959Z` when rebuilding the X.509 TBS, breaking the
168    /// signature (`ChipEpochToASN1Time`,
169    /// `connectedhomeip/src/credentials/CHIPCert.cpp` — the same root cause as
170    /// issue #111). Set the clock (or wait for time sync) and retry.
171    #[error(
172        "system clock reads {0} (before the Matter epoch, 2000-01-01T00:00:00Z) — it is probably \
173         unset; certificates minted against it cannot be installed on a device. Set the host \
174         clock or wait for time sync, then retry"
175    )]
176    SystemClockUnset(u64),
177}
178
179impl Error {
180    /// If this error is the device rejecting the supplied network-credential
181    /// *type* — e.g. Thread credentials handed to a Wi-Fi-only device
182    /// (`NetworkCommissioning::FeatureMap` lacks the needed bit) — returns
183    /// which network type the credentials required. Use this to route to a
184    /// different credential type instead of substring-matching the rendered
185    /// message.
186    ///
187    /// Returns `None` for every other error.
188    #[must_use]
189    pub fn network_feature_unsupported(&self) -> Option<matter_commissioning::NetworkKind> {
190        match self {
191            Error::Driver(matter_commissioning::driver::DriverError::Commissioning(
192                matter_commissioning::CommissioningError::NetworkFeatureUnsupported { needed },
193            )) => Some(*needed),
194            _ => None,
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    #[test]
202    fn no_trust_error_names_the_fix() {
203        let msg = crate::error::Error::NoTrust.to_string();
204        assert!(
205            msg.contains("attestation_trust"),
206            "NoTrust must name the builder fix: {msg}"
207        );
208        assert!(
209            msg.contains("from_dirs"),
210            "NoTrust must name from_dirs: {msg}"
211        );
212    }
213
214    #[test]
215    fn network_feature_unsupported_is_typed_through_the_chain() {
216        use matter_commissioning::{driver::DriverError, CommissioningError, NetworkKind};
217
218        // The nested chain a commission failure actually produces.
219        let e = crate::error::Error::Driver(DriverError::Commissioning(
220            CommissioningError::NetworkFeatureUnsupported {
221                needed: NetworkKind::Thread,
222            },
223        ));
224        assert_eq!(e.network_feature_unsupported(), Some(NetworkKind::Thread));
225
226        // The substring WeaveHome matched still renders through the chain
227        // (belt for the matter-commissioning pin's braces).
228        assert!(e
229            .to_string()
230            .contains("does not support Thread network type"));
231
232        // Unrelated errors: None.
233        assert_eq!(
234            crate::error::Error::ControllerStopped.network_feature_unsupported(),
235            None
236        );
237        let other = crate::error::Error::Driver(DriverError::Commissioning(
238            CommissioningError::CaseEstablishmentFailed,
239        ));
240        assert_eq!(other.network_feature_unsupported(), None);
241    }
242}