matter_controller/state.rs
1//! In-memory controller state. These types are the *persistable* record;
2//! live signers are reconstructed from the stored PKCS#8 keys on demand.
3
4use std::sync::Arc;
5
6use matter_cert::MatterCertificate;
7use matter_commissioning::FabricRecord;
8use matter_crypto::{RingSigner, Signer};
9
10use crate::error::Error;
11
12/// The persisted material for one group key set.
13///
14/// Stored inside [`FabricEntry::group_keys`] and round-tripped through the
15/// TLV snapshot at context tags t6 (key-set array) and t7 (outbound counter).
16/// This carries only what the controller needs to *send* group-encrypted
17/// messages; a full `GroupKeySet` cluster record lives in the device, not
18/// here.
19///
20/// This is a `pub` type because callers that program group keys (e.g.
21/// higher-level fabric-management APIs) need to construct and inspect it.
22///
23/// `#[non_exhaustive]`: persisted record whose shape may grow (e.g. key policy
24/// epoch, security level flags); marking it keeps such additions non-breaking.
25/// Construct via [`GroupKeySetConfig::new`].
26#[derive(Debug, Clone, PartialEq, Eq)]
27#[non_exhaustive]
28pub struct GroupKeySetConfig {
29 /// Group Key Set ID (`GrpKeySetID`, 16-bit, spec §4.15).
30 pub key_set_id: u16,
31 /// 16-byte epoch key (`EpochKey0` / `EpochKey1` / `EpochKey2` per policy).
32 pub epoch_key: [u8; 16],
33 /// Epoch key start time in Matter epoch seconds (0 = unset / pre-operational).
34 pub epoch_start_time: u64,
35}
36
37impl GroupKeySetConfig {
38 /// Construct a [`GroupKeySetConfig`].
39 ///
40 /// Required because the struct is `#[non_exhaustive]`; external callers
41 /// cannot use struct-literal syntax.
42 #[must_use]
43 pub fn new(key_set_id: u16, epoch_key: [u8; 16], epoch_start_time: u64) -> Self {
44 Self {
45 key_set_id,
46 epoch_key,
47 epoch_start_time,
48 }
49 }
50}
51
52/// A per-fabric intermediate CA (ICAC): the issued ICAC certificate plus the
53/// PKCS#8 private key that signs NOCs under it. `None` for a flat RCAC->NOC
54/// fabric (the default).
55#[derive(Clone)]
56#[non_exhaustive]
57pub struct IcacIdentity {
58 /// The RCAC-signed ICAC certificate.
59 pub cert: MatterCertificate,
60 /// The ICAC signing key, PKCS#8 DER.
61 pub pkcs8: Vec<u8>,
62}
63
64impl std::fmt::Debug for IcacIdentity {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("IcacIdentity")
67 .field("cert", &"<MatterCertificate>")
68 .field("pkcs8", &"<redacted PKCS#8>")
69 .finish()
70 }
71}
72
73/// A device commissioned onto a fabric.
74///
75/// `#[non_exhaustive]`: persisted record whose shape may grow (e.g. CAT tags,
76/// typed resumption state); marking it keeps such additions non-breaking. Only
77/// constructed inside `matter-controller`.
78#[derive(Clone)]
79#[non_exhaustive]
80pub struct DeviceEntry {
81 /// The device's operational node ID on this fabric.
82 pub node_id: u64,
83 /// The device's NOC public key (SEC1 uncompressed, `0x04 || X || Y`).
84 pub peer_noc_public_key: [u8; 65],
85 /// Cached CASE resumption record (opaque bytes; typed in M8.2).
86 pub resumption_record: Option<Vec<u8>>,
87 /// Last operational address we reached the device at (a discovery hint).
88 pub last_known_addr: Option<String>,
89 /// Device vendor id (`BasicInformation` 0x0028/0x0002), captured
90 /// best-effort after commissioning. `None` if the read did not complete.
91 pub vendor_id: Option<u16>,
92 /// Device product id (`BasicInformation` 0x0028/0x0004), captured
93 /// best-effort after commissioning. `None` if the read did not complete.
94 pub product_id: Option<u16>,
95 /// Caller-supplied opaque label attached at `commission()` time. `None`
96 /// if the caller passed none.
97 pub label: Option<String>,
98}
99
100impl std::fmt::Debug for DeviceEntry {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 // `resumption_record` is a serialized CASE ResumptionRecord and carries
103 // a session shared secret — redact it (matches the redaction discipline
104 // in `FabricEntry`/`CommissionerIdentity`). `peer_noc_public_key`,
105 // `last_known_addr`, `vendor_id`, `product_id`, and `label` are not
106 // secret.
107 f.debug_struct("DeviceEntry")
108 .field("node_id", &self.node_id)
109 .field("peer_noc_public_key", &self.peer_noc_public_key)
110 .field(
111 "resumption_record",
112 &self
113 .resumption_record
114 .as_ref()
115 .map(|_| "<redacted; CASE secret>"),
116 )
117 .field("last_known_addr", &self.last_known_addr)
118 .field("vendor_id", &self.vendor_id)
119 .field("product_id", &self.product_id)
120 .field("label", &self.label)
121 .finish()
122 }
123}
124
125/// The controller's own stable operational identity on a fabric.
126///
127/// Minted **once** when the fabric is created (see
128/// [`crate::fabric::create_fabric`]) and reused for every CASE handshake,
129/// replacing M6.6.4's per-call NOC minting.
130///
131/// `#[non_exhaustive]`: persisted identity record that may grow; marking it
132/// keeps additions non-breaking. Only constructed inside `matter-controller`.
133#[derive(Clone)]
134#[non_exhaustive]
135pub struct CommissionerIdentity {
136 /// The commissioner's stable node ID on this fabric.
137 pub node_id: u64,
138 /// The commissioner's operational private key, PKCS#8 DER.
139 pub operational_pkcs8: Vec<u8>,
140 /// The commissioner's NOC, signed by the fabric RCAC.
141 pub noc: MatterCertificate,
142}
143
144impl std::fmt::Debug for CommissionerIdentity {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 f.debug_struct("CommissionerIdentity")
147 .field("node_id", &self.node_id)
148 .field("operational_pkcs8", &"<redacted PKCS#8>")
149 .field("noc", &"<MatterCertificate>")
150 .finish()
151 }
152}
153
154/// One fabric the controller administers: trust root, IPK, the
155/// commissioner identity, and the devices commissioned onto it.
156///
157/// `#[non_exhaustive]`: persisted record that may gain fields (e.g. an ICAC
158/// tier, fabric label); marking it keeps such additions non-breaking. Only
159/// constructed inside `matter-controller`.
160#[derive(Clone)]
161#[non_exhaustive]
162pub struct FabricEntry {
163 /// Matter fabric identifier.
164 pub fabric_id: u64,
165 /// 16-byte Identity Protection Key for this fabric.
166 pub ipk: [u8; 16],
167 /// Self-signed root (RCAC) certificate.
168 pub rcac_cert: MatterCertificate,
169 /// The RCAC root signing key, PKCS#8 DER.
170 pub rcac_pkcs8: Vec<u8>,
171 /// The controller's stable identity on this fabric.
172 pub commissioner: CommissionerIdentity,
173 /// Devices commissioned onto this fabric.
174 pub devices: Vec<DeviceEntry>,
175 /// Group key sets programmed on this fabric (persisted for outbound group
176 /// message encryption). Empty until the controller programs group keys.
177 pub group_keys: Vec<GroupKeySetConfig>,
178 /// The outbound group message counter for this fabric.
179 ///
180 /// Monotonically incremented each time the controller sends a group
181 /// message. Persisted so the counter survives restarts (spec §4.6.7
182 /// prohibits counter reuse across sessions / resets).
183 pub outbound_group_counter: u32,
184 /// ICD (Intermittently Connected Device) client registrations on this
185 /// fabric. Each holds the shared key + counter floor the check-in listener
186 /// uses to verify a registered device's Check-In messages. Empty until the
187 /// controller calls `register_icd_client`.
188 pub icd_clients: Vec<crate::icd::IcdRegistration>,
189 /// Optional per-fabric intermediate CA. `None` for a flat RCAC->NOC
190 /// fabric (the default); `Some` once the fabric adopts an ICAC tier.
191 pub icac: Option<IcacIdentity>,
192}
193
194impl std::fmt::Debug for FabricEntry {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct("FabricEntry")
197 .field("fabric_id", &self.fabric_id)
198 .field("ipk", &"<redacted; 16 bytes>")
199 .field("rcac_cert", &"<MatterCertificate>")
200 .field("rcac_pkcs8", &"<redacted PKCS#8>")
201 .field("commissioner", &self.commissioner)
202 .field("devices", &self.devices)
203 .field(
204 "group_keys",
205 &format!("<{} key sets>", self.group_keys.len()),
206 )
207 .field("outbound_group_counter", &self.outbound_group_counter)
208 .field(
209 "icd_clients",
210 &format!("<{} registrations>", self.icd_clients.len()),
211 )
212 .field("icac", &self.icac)
213 .finish()
214 }
215}
216
217impl FabricEntry {
218 /// Reconstruct the RCAC root signer from the stored PKCS#8 key.
219 ///
220 /// # Errors
221 ///
222 /// Returns [`Error::Signer`] if the stored key is not valid PKCS#8.
223 pub fn rcac_signer(&self) -> Result<RingSigner, Error> {
224 RingSigner::from_pkcs8(&self.rcac_pkcs8).map_err(|e| Error::Signer(e.to_string()))
225 }
226
227 /// Reconstruct the commissioner operational signer from PKCS#8.
228 ///
229 /// # Errors
230 ///
231 /// Returns [`Error::Signer`] if the stored key is not valid PKCS#8.
232 pub fn commissioner_signer(&self) -> Result<RingSigner, Error> {
233 RingSigner::from_pkcs8(&self.commissioner.operational_pkcs8)
234 .map_err(|e| Error::Signer(e.to_string()))
235 }
236
237 /// Build a [`FabricRecord`] view (used by later sub-phases for NOC
238 /// issuance and CASE). Reconstructs the RCAC signer from PKCS#8.
239 ///
240 /// # Errors
241 ///
242 /// Returns [`Error::Signer`] if the RCAC key cannot be reconstructed.
243 pub fn to_fabric_record(&self) -> Result<FabricRecord, Error> {
244 let signer = self.rcac_signer()?;
245 let root_public_key = signer.public_key().clone();
246 // Reconstruct the ICAC signer/cert when this fabric has an ICAC
247 // tier, so a restored fabric keeps signing NOCs (and CASE
248 // credentials, via `FabricRecord.icac_cert`) under the ICAC —
249 // matching what `create_fabric` sets up live.
250 let (icac_signer, icac_cert) = match &self.icac {
251 Some(icac) => {
252 let signer = RingSigner::from_pkcs8(&icac.pkcs8)
253 .map_err(|e| Error::Signer(e.to_string()))?;
254 (
255 Some(Arc::new(signer) as Arc<dyn Signer>),
256 Some(icac.cert.clone()),
257 )
258 }
259 None => (None, None),
260 };
261 Ok(FabricRecord {
262 fabric_id: self.fabric_id,
263 root_public_key,
264 root_signer: Arc::new(signer) as Arc<dyn Signer>,
265 root_cert: self.rcac_cert.clone(),
266 icac_signer,
267 icac_cert,
268 identity_protection_key: self.ipk,
269 })
270 }
271}
272
273/// The full controller state: all administered fabrics.
274///
275/// `#[non_exhaustive]`: the persisted top-level record may gain fields (e.g.
276/// schema version, controller-wide settings); marking it keeps such additions
277/// non-breaking. Construct via [`ControllerState::new`] or
278/// [`ControllerState::default`] from outside this crate.
279#[derive(Debug, Clone, Default)]
280#[non_exhaustive]
281pub struct ControllerState {
282 /// Fabrics this controller administers.
283 pub fabrics: Vec<FabricEntry>,
284}
285
286impl ControllerState {
287 /// Construct controller state from a list of administered fabrics.
288 ///
289 /// Supported construction path now that [`ControllerState`] is
290 /// `#[non_exhaustive]`; the `fabrics` field stays directly accessible.
291 #[must_use]
292 pub fn new(fabrics: Vec<FabricEntry>) -> Self {
293 Self { fabrics }
294 }
295}
296
297#[cfg(test)]
298#[allow(clippy::unwrap_used, clippy::expect_used)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn controller_state_new_builds_from_fabrics() {
304 // `ControllerState` is `#[non_exhaustive]`; `new` is the supported
305 // construction path. An empty list yields no fabrics.
306 let state = ControllerState::new(Vec::new());
307 assert!(state.fabrics.is_empty());
308 }
309
310 #[test]
311 fn reconstructed_signer_signs_and_verifies() {
312 // A standalone RingSigner round-trips through PKCS#8 and signs.
313 let (signer, pkcs8) = RingSigner::generate().expect("generate");
314 let entry_key = pkcs8.clone();
315 let reloaded = RingSigner::from_pkcs8(&entry_key).expect("reload");
316 // Both signers share the same public key.
317 assert_eq!(
318 signer.public_key().as_bytes(),
319 reloaded.public_key().as_bytes()
320 );
321 // The reloaded signer produces a verifiable signature.
322 let msg = b"controller identity";
323 let sig_bytes = reloaded.sign_p256_sha256(msg).expect("sign");
324 // `PublicKey::verify` takes a `&matter_cert::Signature`, not a raw `[u8; 64]`.
325 let sig = matter_cert::Signature::new(sig_bytes);
326 reloaded
327 .public_key()
328 .verify(msg, &sig)
329 .expect("reloaded signature verifies");
330 }
331}