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 device commissioned onto a fabric.
53///
54/// `#[non_exhaustive]`: persisted record whose shape may grow (e.g. CAT tags,
55/// typed resumption state); marking it keeps such additions non-breaking. Only
56/// constructed inside `matter-controller`.
57#[derive(Clone)]
58#[non_exhaustive]
59pub struct DeviceEntry {
60    /// The device's operational node ID on this fabric.
61    pub node_id: u64,
62    /// The device's NOC public key (SEC1 uncompressed, `0x04 || X || Y`).
63    pub peer_noc_public_key: [u8; 65],
64    /// Cached CASE resumption record (opaque bytes; typed in M8.2).
65    pub resumption_record: Option<Vec<u8>>,
66    /// Last operational address we reached the device at (a discovery hint).
67    pub last_known_addr: Option<String>,
68}
69
70impl std::fmt::Debug for DeviceEntry {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        // `resumption_record` is a serialized CASE ResumptionRecord and carries
73        // a session shared secret — redact it (matches the redaction discipline
74        // in `FabricEntry`/`CommissionerIdentity`). `peer_noc_public_key` and
75        // `last_known_addr` are not secret.
76        f.debug_struct("DeviceEntry")
77            .field("node_id", &self.node_id)
78            .field("peer_noc_public_key", &self.peer_noc_public_key)
79            .field(
80                "resumption_record",
81                &self
82                    .resumption_record
83                    .as_ref()
84                    .map(|_| "<redacted; CASE secret>"),
85            )
86            .field("last_known_addr", &self.last_known_addr)
87            .finish()
88    }
89}
90
91/// The controller's own stable operational identity on a fabric.
92///
93/// Minted **once** when the fabric is created (see
94/// [`crate::fabric::create_fabric`]) and reused for every CASE handshake,
95/// replacing M6.6.4's per-call NOC minting.
96///
97/// `#[non_exhaustive]`: persisted identity record that may grow; marking it
98/// keeps additions non-breaking. Only constructed inside `matter-controller`.
99#[derive(Clone)]
100#[non_exhaustive]
101pub struct CommissionerIdentity {
102    /// The commissioner's stable node ID on this fabric.
103    pub node_id: u64,
104    /// The commissioner's operational private key, PKCS#8 DER.
105    pub operational_pkcs8: Vec<u8>,
106    /// The commissioner's NOC, signed by the fabric RCAC.
107    pub noc: MatterCertificate,
108}
109
110impl std::fmt::Debug for CommissionerIdentity {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.debug_struct("CommissionerIdentity")
113            .field("node_id", &self.node_id)
114            .field("operational_pkcs8", &"<redacted PKCS#8>")
115            .field("noc", &"<MatterCertificate>")
116            .finish()
117    }
118}
119
120/// One fabric the controller administers: trust root, IPK, the
121/// commissioner identity, and the devices commissioned onto it.
122///
123/// `#[non_exhaustive]`: persisted record that may gain fields (e.g. an ICAC
124/// tier, fabric label); marking it keeps such additions non-breaking. Only
125/// constructed inside `matter-controller`.
126#[derive(Clone)]
127#[non_exhaustive]
128pub struct FabricEntry {
129    /// Matter fabric identifier.
130    pub fabric_id: u64,
131    /// 16-byte Identity Protection Key for this fabric.
132    pub ipk: [u8; 16],
133    /// Self-signed root (RCAC) certificate.
134    pub rcac_cert: MatterCertificate,
135    /// The RCAC root signing key, PKCS#8 DER.
136    pub rcac_pkcs8: Vec<u8>,
137    /// The controller's stable identity on this fabric.
138    pub commissioner: CommissionerIdentity,
139    /// Devices commissioned onto this fabric.
140    pub devices: Vec<DeviceEntry>,
141    /// Group key sets programmed on this fabric (persisted for outbound group
142    /// message encryption). Empty until the controller programs group keys.
143    pub group_keys: Vec<GroupKeySetConfig>,
144    /// The outbound group message counter for this fabric.
145    ///
146    /// Monotonically incremented each time the controller sends a group
147    /// message. Persisted so the counter survives restarts (spec §4.6.7
148    /// prohibits counter reuse across sessions / resets).
149    pub outbound_group_counter: u32,
150    /// ICD (Intermittently Connected Device) client registrations on this
151    /// fabric. Each holds the shared key + counter floor the check-in listener
152    /// uses to verify a registered device's Check-In messages. Empty until the
153    /// controller calls `register_icd_client`.
154    pub icd_clients: Vec<crate::icd::IcdRegistration>,
155}
156
157impl std::fmt::Debug for FabricEntry {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("FabricEntry")
160            .field("fabric_id", &self.fabric_id)
161            .field("ipk", &"<redacted; 16 bytes>")
162            .field("rcac_cert", &"<MatterCertificate>")
163            .field("rcac_pkcs8", &"<redacted PKCS#8>")
164            .field("commissioner", &self.commissioner)
165            .field("devices", &self.devices)
166            .field(
167                "group_keys",
168                &format!("<{} key sets>", self.group_keys.len()),
169            )
170            .field("outbound_group_counter", &self.outbound_group_counter)
171            .field(
172                "icd_clients",
173                &format!("<{} registrations>", self.icd_clients.len()),
174            )
175            .finish()
176    }
177}
178
179impl FabricEntry {
180    /// Reconstruct the RCAC root signer from the stored PKCS#8 key.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`Error::Signer`] if the stored key is not valid PKCS#8.
185    pub fn rcac_signer(&self) -> Result<RingSigner, Error> {
186        RingSigner::from_pkcs8(&self.rcac_pkcs8).map_err(|e| Error::Signer(e.to_string()))
187    }
188
189    /// Reconstruct the commissioner operational signer from PKCS#8.
190    ///
191    /// # Errors
192    ///
193    /// Returns [`Error::Signer`] if the stored key is not valid PKCS#8.
194    pub fn commissioner_signer(&self) -> Result<RingSigner, Error> {
195        RingSigner::from_pkcs8(&self.commissioner.operational_pkcs8)
196            .map_err(|e| Error::Signer(e.to_string()))
197    }
198
199    /// Build a [`FabricRecord`] view (used by later sub-phases for NOC
200    /// issuance and CASE). Reconstructs the RCAC signer from PKCS#8.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`Error::Signer`] if the RCAC key cannot be reconstructed.
205    pub fn to_fabric_record(&self) -> Result<FabricRecord, Error> {
206        let signer = self.rcac_signer()?;
207        let root_public_key = signer.public_key().clone();
208        Ok(FabricRecord {
209            fabric_id: self.fabric_id,
210            root_public_key,
211            root_signer: Arc::new(signer) as Arc<dyn Signer>,
212            root_cert: self.rcac_cert.clone(),
213            icac_signer: None,
214            icac_cert: None,
215            identity_protection_key: self.ipk,
216        })
217    }
218}
219
220/// The full controller state: all administered fabrics.
221///
222/// `#[non_exhaustive]`: the persisted top-level record may gain fields (e.g.
223/// schema version, controller-wide settings); marking it keeps such additions
224/// non-breaking. Construct via [`ControllerState::new`] or
225/// [`ControllerState::default`] from outside this crate.
226#[derive(Debug, Clone, Default)]
227#[non_exhaustive]
228pub struct ControllerState {
229    /// Fabrics this controller administers.
230    pub fabrics: Vec<FabricEntry>,
231}
232
233impl ControllerState {
234    /// Construct controller state from a list of administered fabrics.
235    ///
236    /// Supported construction path now that [`ControllerState`] is
237    /// `#[non_exhaustive]`; the `fabrics` field stays directly accessible.
238    #[must_use]
239    pub fn new(fabrics: Vec<FabricEntry>) -> Self {
240        Self { fabrics }
241    }
242}
243
244#[cfg(test)]
245#[allow(clippy::unwrap_used, clippy::expect_used)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn controller_state_new_builds_from_fabrics() {
251        // `ControllerState` is `#[non_exhaustive]`; `new` is the supported
252        // construction path. An empty list yields no fabrics.
253        let state = ControllerState::new(Vec::new());
254        assert!(state.fabrics.is_empty());
255    }
256
257    #[test]
258    fn reconstructed_signer_signs_and_verifies() {
259        // A standalone RingSigner round-trips through PKCS#8 and signs.
260        let (signer, pkcs8) = RingSigner::generate().expect("generate");
261        let entry_key = pkcs8.clone();
262        let reloaded = RingSigner::from_pkcs8(&entry_key).expect("reload");
263        // Both signers share the same public key.
264        assert_eq!(
265            signer.public_key().as_bytes(),
266            reloaded.public_key().as_bytes()
267        );
268        // The reloaded signer produces a verifiable signature.
269        let msg = b"controller identity";
270        let sig_bytes = reloaded.sign_p256_sha256(msg).expect("sign");
271        // `PublicKey::verify` takes a `&matter_cert::Signature`, not a raw `[u8; 64]`.
272        let sig = matter_cert::Signature::new(sig_bytes);
273        reloaded
274            .public_key()
275            .verify(msg, &sig)
276            .expect("reloaded signature verifies");
277    }
278}