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 /// A device acknowledged an operational request at the transport layer but
63 /// never sent the Interaction Model response, and the response deadline
64 /// elapsed.
65 ///
66 /// Distinct from [`Self::Operational`] because the cause is specific and
67 /// actionable: MRP confirmed delivery, so this is not packet loss — the
68 /// device accepted the request and did not answer it. Observed on bridges
69 /// that silently drop a read after several rapid consecutive reads on one
70 /// session.
71 ///
72 /// The request is **not** retried before this is returned: delivery was
73 /// confirmed, so re-sending could execute a non-idempotent command twice.
74 /// Deciding whether a retry is safe is the caller's.
75 ///
76 /// Tune the deadline with
77 /// [`MatterControllerBuilder::response_deadline`][crate::MatterControllerBuilder::response_deadline].
78 #[error("node {node_id:016X} acknowledged the request but sent no response within {after:?}")]
79 ResponseTimeout {
80 /// The node that failed to answer.
81 node_id: u64,
82 /// The deadline that elapsed.
83 after: std::time::Duration,
84 },
85
86 /// Attestation trust material could not be loaded.
87 #[error("attestation trust error: {0}")]
88 Trust(String),
89
90 /// The setup code (QR / manual) could not be parsed.
91 #[error("invalid setup code: {0}")]
92 SetupCode(String),
93
94 /// No attestation trust configured; commissioning cannot verify the device.
95 #[error(
96 "no attestation trust configured — commissioning cannot verify the device's \
97 attestation. Build the controller with MatterController::builder(store)\
98 .attestation_trust(AttestationTrust::from_dirs(paa_dir, cd_dir)).build(), \
99 not MatterController::open(store)"
100 )]
101 NoTrust,
102
103 /// An `AdministratorCommissioning` command returned a non-success IM status
104 /// (e.g. 0x02 Busy, 0x03 `PAKEParameterError`, 0x04 `WindowNotOpen` reported as
105 /// a cluster status). The raw IM status byte is preserved.
106 #[error("commissioning window command rejected (IM status {0:#04x})")]
107 CommissioningWindowRejected(u8),
108
109 /// Refused to remove the controller's own fabric (would sever the CASE
110 /// session and orphan persisted device state). No `force` override exists.
111 #[error("refusing to remove our own fabric (would orphan the device)")]
112 WouldRemoveSelf,
113
114 /// An `OperationalCredentials` command returned a non-success
115 /// `NodeOperationalCertStatusEnum` (e.g. 7 `InvalidFabricIndex`). Raw code preserved.
116 #[error("operational-credentials command rejected (status {0})")]
117 OperationalCredentialsRejected(u8),
118
119 /// Refused an ACL write that would strip our own administrative access
120 /// (no Administer/CASE entry covering our commissioner node id). Prevents
121 /// orphaning the device. Checked before any bytes are sent.
122 #[error("refusing ACL write: it would remove our own administrative access")]
123 AclWouldLockOut,
124
125 /// A `Groups` / `GroupKeyManagement` command returned a non-success status
126 /// (e.g. `ResourceExhausted` from `MaxGroupsPerFabric`). Raw status preserved.
127 #[error("group command rejected (status {0})")]
128 GroupCommandRejected(u8),
129
130 /// A group send (`invoke_group`) named a `key_set_id` that has not been
131 /// provisioned on the controller's fabric (no matching
132 /// [`GroupKeySetConfig`](crate::GroupKeySetConfig) in `group_keys`). Call
133 /// [`MatterController::create_group`](crate::MatterController::create_group)
134 /// first to mint and persist the key set.
135 #[error("group key set {0} is not provisioned on this fabric")]
136 GroupNotProvisioned(u16),
137
138 /// [`MatterController::create_fabric`](crate::MatterController::create_fabric)
139 /// was called with a `fabric_id` that already exists on this controller
140 /// (issue #110 — commonly hit by calling `create_fabric` unconditionally
141 /// on every startup instead of only on a fresh store). Call
142 /// [`MatterController::fabrics`](crate::MatterController::fabrics) first
143 /// to check which fabrics already exist.
144 ///
145 /// To recover: the existing fabric is already usable — just skip the
146 /// `create_fabric` call and carry on with it. If you genuinely want a
147 /// second fabric, pass a different `fabric_id`; if you want to start over,
148 /// point the controller at a fresh store. There is no API to delete a
149 /// fabric from the controller's own store, so once a `fabric_id` is in a
150 /// store, `create_fabric` refuses it for that store's lifetime.
151 /// ([`Node::remove_fabric`](crate::Node::remove_fabric) removes *our*
152 /// fabric from a **device**, not from the controller.)
153 #[error(
154 "fabric {0:#018x} already exists — call MatterController::fabrics() to check before \
155 calling create_fabric; to recover, use the existing fabric, pass a different fabric_id, \
156 or start from a fresh store"
157 )]
158 FabricAlreadyExists(u64),
159
160 /// [`FabricConfig::validity`](crate::FabricConfig::validity) names a
161 /// window that cannot work on a device (issue #111). Rejected windows:
162 ///
163 /// - `not_before` at the Matter epoch (`MatterTime(0)`, i.e.
164 /// 2000-01-01T00:00:00Z). Not a validity-policy rejection: chip's
165 /// `ChipEpochToASN1Time`
166 /// (`connectedhomeip/src/credentials/CHIPCert.cpp`) encodes epoch 0 as
167 /// `99991231235959Z` for both `notBefore` and `notAfter`, so the X.509
168 /// TBS the device rebuilds from our TLV certificate differs from the one
169 /// we signed and the **signature** check fails — surfacing as an opaque
170 /// `IM status 0x85` on `AddTrustedRootCertificate`.
171 /// - `not_before` more than a day ahead of this host's clock — usually a
172 /// millisecond timestamp passed to `MatterTime::from_unix_secs`, which
173 /// saturates to ≈ year 2136. Such a root *installs* (chip's
174 /// `ValidateChipRCAC` skips RCAC validity times) and then fails every
175 /// CASE session with `kNotYetValid`.
176 /// - An inverted or empty window (`not_after <= not_before`, excluding
177 /// `MatterTime::NO_EXPIRY`).
178 ///
179 /// The detail string names which.
180 #[error("invalid fabric validity window: {0}")]
181 InvalidFabricValidity(String),
182
183 /// The host's wall clock reads before the Matter epoch
184 /// (2000-01-01T00:00:00Z) — almost always an **unset system clock** on a
185 /// host with no RTC that has not yet reached an NTP server. Payload: the
186 /// Unix seconds actually read.
187 ///
188 /// Refused rather than used, because `MatterTime::from_unix_secs` saturates
189 /// such a reading to `MatterTime(0)`, and a certificate minted with
190 /// `notBefore == 0` cannot be installed on a device at all: chip re-encodes
191 /// epoch 0 as `99991231235959Z` when rebuilding the X.509 TBS, breaking the
192 /// signature (`ChipEpochToASN1Time`,
193 /// `connectedhomeip/src/credentials/CHIPCert.cpp` — the same root cause as
194 /// issue #111). Set the clock (or wait for time sync) and retry.
195 #[error(
196 "system clock reads {0} (before the Matter epoch, 2000-01-01T00:00:00Z) — it is probably \
197 unset; certificates minted against it cannot be installed on a device. Set the host \
198 clock or wait for time sync, then retry"
199 )]
200 SystemClockUnset(u64),
201}
202
203impl Error {
204 /// If this error is the device rejecting the supplied network-credential
205 /// *type* — e.g. Thread credentials handed to a Wi-Fi-only device
206 /// (`NetworkCommissioning::FeatureMap` lacks the needed bit) — returns
207 /// which network type the credentials required. Use this to route to a
208 /// different credential type instead of substring-matching the rendered
209 /// message.
210 ///
211 /// Returns `None` for every other error.
212 #[must_use]
213 pub fn network_feature_unsupported(&self) -> Option<matter_commissioning::NetworkKind> {
214 match self {
215 Error::Driver(matter_commissioning::driver::DriverError::Commissioning(
216 matter_commissioning::CommissioningError::NetworkFeatureUnsupported { needed },
217 )) => Some(*needed),
218 _ => None,
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 #[test]
226 fn no_trust_error_names_the_fix() {
227 let msg = crate::error::Error::NoTrust.to_string();
228 assert!(
229 msg.contains("attestation_trust"),
230 "NoTrust must name the builder fix: {msg}"
231 );
232 assert!(
233 msg.contains("from_dirs"),
234 "NoTrust must name from_dirs: {msg}"
235 );
236 }
237
238 #[test]
239 fn network_feature_unsupported_is_typed_through_the_chain() {
240 use matter_commissioning::{driver::DriverError, CommissioningError, NetworkKind};
241
242 // The nested chain a commission failure actually produces.
243 let e = crate::error::Error::Driver(DriverError::Commissioning(
244 CommissioningError::NetworkFeatureUnsupported {
245 needed: NetworkKind::Thread,
246 },
247 ));
248 assert_eq!(e.network_feature_unsupported(), Some(NetworkKind::Thread));
249
250 // The substring WeaveHome matched still renders through the chain
251 // (belt for the matter-commissioning pin's braces).
252 assert!(e
253 .to_string()
254 .contains("does not support Thread network type"));
255
256 // Unrelated errors: None.
257 assert_eq!(
258 crate::error::Error::ControllerStopped.network_feature_unsupported(),
259 None
260 );
261 let other = crate::error::Error::Driver(DriverError::Commissioning(
262 CommissioningError::CaseEstablishmentFailed,
263 ));
264 assert_eq!(other.network_feature_unsupported(), None);
265 }
266}