Skip to main content

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. `pub(crate)`: raw private-key material
61    /// is never exposed on the public surface (persisted via `snapshot`).
62    pub(crate) pkcs8: Vec<u8>,
63}
64
65impl std::fmt::Debug for IcacIdentity {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.debug_struct("IcacIdentity")
68            .field("cert", &"<MatterCertificate>")
69            .field("pkcs8", &"<redacted PKCS#8>")
70            .finish()
71    }
72}
73
74/// A device commissioned onto a fabric.
75///
76/// `#[non_exhaustive]`: persisted record whose shape may grow (e.g. CAT tags,
77/// typed resumption state); marking it keeps such additions non-breaking. Only
78/// constructed inside `matter-controller`.
79#[derive(Clone)]
80#[non_exhaustive]
81pub struct DeviceEntry {
82    /// The device's operational node ID on this fabric.
83    pub node_id: u64,
84    /// The device's NOC public key (SEC1 uncompressed, `0x04 || X || Y`).
85    pub peer_noc_public_key: [u8; 65],
86    /// Cached CASE resumption record (opaque bytes).
87    pub resumption_record: Option<Vec<u8>>,
88    /// Last operational address we reached the device at (a discovery hint).
89    pub last_known_addr: Option<String>,
90    /// Device vendor id (`BasicInformation` 0x0028/0x0002), captured
91    /// best-effort after commissioning. `None` if the read did not complete.
92    pub vendor_id: Option<u16>,
93    /// Device product id (`BasicInformation` 0x0028/0x0004), captured
94    /// best-effort after commissioning. `None` if the read did not complete.
95    pub product_id: Option<u16>,
96    /// Caller-supplied opaque label attached at `commission()` time. `None`
97    /// if the caller passed none.
98    pub label: Option<String>,
99}
100
101impl std::fmt::Debug for DeviceEntry {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        // `resumption_record` is a serialized CASE ResumptionRecord and carries
104        // a session shared secret — redact it (matches the redaction discipline
105        // in `FabricEntry`/`CommissionerIdentity`). `peer_noc_public_key`,
106        // `last_known_addr`, `vendor_id`, `product_id`, and `label` are not
107        // secret.
108        f.debug_struct("DeviceEntry")
109            .field("node_id", &self.node_id)
110            .field("peer_noc_public_key", &self.peer_noc_public_key)
111            .field(
112                "resumption_record",
113                &self
114                    .resumption_record
115                    .as_ref()
116                    .map(|_| "<redacted; CASE secret>"),
117            )
118            .field("last_known_addr", &self.last_known_addr)
119            .field("vendor_id", &self.vendor_id)
120            .field("product_id", &self.product_id)
121            .field("label", &self.label)
122            .finish()
123    }
124}
125
126/// The controller's own stable operational identity on a fabric.
127///
128/// Minted **once** when the fabric is created (see
129/// [`MatterController::create_fabric`](crate::controller::MatterController::create_fabric))
130/// and reused for every CASE handshake, replacing the earlier per-call NOC
131/// minting.
132///
133/// `#[non_exhaustive]`: persisted identity record that may grow; marking it
134/// keeps additions non-breaking. Only constructed inside `matter-controller`.
135#[derive(Clone)]
136#[non_exhaustive]
137pub struct CommissionerIdentity {
138    /// The commissioner's stable node ID on this fabric.
139    pub node_id: u64,
140    /// The commissioner's operational private key, PKCS#8 DER. `pub(crate)`:
141    /// raw private-key material is never exposed on the public surface.
142    pub(crate) operational_pkcs8: Vec<u8>,
143    /// The commissioner's NOC, signed by the fabric RCAC.
144    pub noc: MatterCertificate,
145}
146
147impl std::fmt::Debug for CommissionerIdentity {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("CommissionerIdentity")
150            .field("node_id", &self.node_id)
151            .field("operational_pkcs8", &"<redacted PKCS#8>")
152            .field("noc", &"<MatterCertificate>")
153            .finish()
154    }
155}
156
157/// One fabric the controller administers: trust root, IPK, the
158/// commissioner identity, and the devices commissioned onto it.
159///
160/// `#[non_exhaustive]`: persisted record that may gain fields (e.g. an ICAC
161/// tier, fabric label); marking it keeps such additions non-breaking. Only
162/// constructed inside `matter-controller`.
163#[derive(Clone)]
164#[non_exhaustive]
165pub struct FabricEntry {
166    /// Matter fabric identifier.
167    pub fabric_id: u64,
168    /// 16-byte Identity Protection Key for this fabric.
169    pub ipk: [u8; 16],
170    /// Self-signed root (RCAC) certificate.
171    pub rcac_cert: MatterCertificate,
172    /// The RCAC root signing key, PKCS#8 DER. `pub(crate)`: raw private-key
173    /// material is never exposed on the public surface.
174    pub(crate) rcac_pkcs8: Vec<u8>,
175    /// The controller's stable identity on this fabric.
176    pub commissioner: CommissionerIdentity,
177    /// Devices commissioned onto this fabric.
178    pub devices: Vec<DeviceEntry>,
179    /// Group key sets programmed on this fabric (persisted for outbound group
180    /// message encryption). Empty until the controller programs group keys.
181    pub group_keys: Vec<GroupKeySetConfig>,
182    /// The reserved **ceiling** of the outbound group message counter for this
183    /// fabric: the lowest counter value this controller may not yet have sent.
184    ///
185    /// Persisted so counters survive restarts (spec §4.6.7 prohibits counter
186    /// reuse across sessions / resets). The controller hands out counters below
187    /// the ceiling from memory and only persists when it raises the ceiling by a
188    /// block, so a restart resumes here — skipping at most a block of never-sent
189    /// values, never reusing a sent one.
190    ///
191    /// Snapshots written before block reservation stored the next counter to
192    /// send, which is the same quantity (the smallest never-sent value), so they
193    /// are read back correctly with no migration.
194    pub outbound_group_counter: u32,
195    /// ICD (Intermittently Connected Device) client registrations on this
196    /// fabric. Each holds the shared key + counter floor the check-in listener
197    /// uses to verify a registered device's Check-In messages. Empty until the
198    /// controller calls `register_icd_client`.
199    pub icd_clients: Vec<crate::icd::IcdRegistration>,
200    /// Optional per-fabric intermediate CA. `None` for a flat RCAC->NOC
201    /// fabric (the default); `Some` once the fabric adopts an ICAC tier.
202    pub icac: Option<IcacIdentity>,
203}
204
205impl std::fmt::Debug for FabricEntry {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        f.debug_struct("FabricEntry")
208            .field("fabric_id", &self.fabric_id)
209            .field("ipk", &"<redacted; 16 bytes>")
210            .field("rcac_cert", &"<MatterCertificate>")
211            .field("rcac_pkcs8", &"<redacted PKCS#8>")
212            .field("commissioner", &self.commissioner)
213            .field("devices", &self.devices)
214            .field(
215                "group_keys",
216                &format!("<{} key sets>", self.group_keys.len()),
217            )
218            .field("outbound_group_counter", &self.outbound_group_counter)
219            .field(
220                "icd_clients",
221                &format!("<{} registrations>", self.icd_clients.len()),
222            )
223            .field("icac", &self.icac)
224            .finish()
225    }
226}
227
228impl FabricEntry {
229    /// Reconstruct the RCAC root signer from the stored PKCS#8 key.
230    ///
231    /// # Errors
232    ///
233    /// Returns [`Error::Signer`] if the stored key is not valid PKCS#8.
234    pub(crate) fn rcac_signer(&self) -> Result<RingSigner, Error> {
235        RingSigner::from_pkcs8(&self.rcac_pkcs8).map_err(|e| Error::Signer(e.to_string()))
236    }
237
238    /// Reconstruct the commissioner operational signer from PKCS#8.
239    ///
240    /// # Errors
241    ///
242    /// Returns [`Error::Signer`] if the stored key is not valid PKCS#8.
243    pub(crate) fn commissioner_signer(&self) -> Result<RingSigner, Error> {
244        RingSigner::from_pkcs8(&self.commissioner.operational_pkcs8)
245            .map_err(|e| Error::Signer(e.to_string()))
246    }
247
248    /// Build a [`FabricRecord`] view (used by later sub-phases for NOC
249    /// issuance and CASE). Reconstructs the RCAC signer from PKCS#8.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`Error::Signer`] if the RCAC key cannot be reconstructed.
254    pub(crate) fn to_fabric_record(&self) -> Result<FabricRecord, Error> {
255        let signer = self.rcac_signer()?;
256        let root_public_key = signer.public_key().clone();
257        // Reconstruct the ICAC signer/cert when this fabric has an ICAC
258        // tier, so a restored fabric keeps signing NOCs (and CASE
259        // credentials, via `FabricRecord.icac_cert`) under the ICAC —
260        // matching what `create_fabric` sets up live.
261        let (icac_signer, icac_cert) = match &self.icac {
262            Some(icac) => {
263                let signer = RingSigner::from_pkcs8(&icac.pkcs8)
264                    .map_err(|e| Error::Signer(e.to_string()))?;
265                (
266                    Some(Arc::new(signer) as Arc<dyn Signer>),
267                    Some(icac.cert.clone()),
268                )
269            }
270            None => (None, None),
271        };
272        Ok(FabricRecord {
273            fabric_id: self.fabric_id,
274            root_public_key,
275            root_signer: Arc::new(signer) as Arc<dyn Signer>,
276            root_cert: self.rcac_cert.clone(),
277            icac_signer,
278            icac_cert,
279            identity_protection_key: self.ipk,
280        })
281    }
282}
283
284/// The full controller state: all administered fabrics.
285///
286/// `#[non_exhaustive]`: the persisted top-level record may gain fields (e.g.
287/// schema version, controller-wide settings); marking it keeps such additions
288/// non-breaking. Construct via [`ControllerState::new`] or
289/// [`ControllerState::default`] from outside this crate.
290#[derive(Debug, Clone, Default)]
291#[non_exhaustive]
292pub struct ControllerState {
293    /// Fabrics this controller administers.
294    pub fabrics: Vec<FabricEntry>,
295}
296
297impl ControllerState {
298    /// Construct controller state from a list of administered fabrics.
299    ///
300    /// Supported construction path now that [`ControllerState`] is
301    /// `#[non_exhaustive]`; the `fabrics` field stays directly accessible.
302    #[must_use]
303    pub fn new(fabrics: Vec<FabricEntry>) -> Self {
304        Self { fabrics }
305    }
306}
307
308#[cfg(test)]
309#[allow(clippy::unwrap_used, clippy::expect_used)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn controller_state_new_builds_from_fabrics() {
315        // `ControllerState` is `#[non_exhaustive]`; `new` is the supported
316        // construction path. An empty list yields no fabrics.
317        let state = ControllerState::new(Vec::new());
318        assert!(state.fabrics.is_empty());
319    }
320
321    #[test]
322    fn reconstructed_signer_signs_and_verifies() {
323        // A standalone RingSigner round-trips through PKCS#8 and signs.
324        let (signer, pkcs8) = RingSigner::generate().expect("generate");
325        let entry_key = pkcs8.clone();
326        let reloaded = RingSigner::from_pkcs8(&entry_key).expect("reload");
327        // Both signers share the same public key.
328        assert_eq!(
329            signer.public_key().as_bytes(),
330            reloaded.public_key().as_bytes()
331        );
332        // The reloaded signer produces a verifiable signature.
333        let msg = b"controller identity";
334        let sig_bytes = reloaded.sign_p256_sha256(msg).expect("sign");
335        // `PublicKey::verify` takes a `&matter_cert::Signature`, not a raw `[u8; 64]`.
336        let sig = matter_cert::Signature::new(sig_bytes);
337        reloaded
338            .public_key()
339            .verify(msg, &sig)
340            .expect("reloaded signature verifies");
341    }
342}