Skip to main content

matter_controller/
fabric.rs

1//! Fabric creation. Mints the fabric trust root (RCAC + IPK) and the
2//! controller's **stable** commissioner operational identity in one shot.
3//! The commissioner NOC is minted here exactly once and persisted; every
4//! later CASE handshake reuses it (retiring M6.6.4's per-call minting).
5
6use std::sync::Arc;
7
8use matter_cert::MatterTime;
9use matter_commissioning::{issue_icac, issue_noc, FabricRecord, NocRng, VerifiedCsr};
10use matter_crypto::{RingSigner, Signer};
11
12use crate::error::Error;
13use crate::state::{CommissionerIdentity, FabricEntry, IcacIdentity};
14
15/// Inputs for creating a new fabric.
16///
17/// `#[non_exhaustive]`: future fabric-creation knobs (e.g. an explicit IPK or
18/// an ICAC tier) can be added without a semver break. Construct via
19/// [`FabricConfig::new`] from outside this crate.
20#[derive(Debug, Clone)]
21#[non_exhaustive]
22pub struct FabricConfig {
23    /// Matter fabric identifier (spec §6.2.1).
24    pub fabric_id: u64,
25    /// RCAC subject DN's `rcac-id` value.
26    pub rcac_id: u64,
27    /// The stable node ID the controller takes on this fabric.
28    pub commissioner_node_id: u64,
29    /// `(not_before, not_after)` validity for the RCAC and commissioner NOC.
30    pub validity: (MatterTime, MatterTime),
31    /// When `true`, `create_fabric` mints an intermediate CA (ICAC) under
32    /// the RCAC and signs the commissioner NOC (and, later, all NOCs
33    /// issued on this fabric) under the ICAC instead of directly under the
34    /// RCAC. Defaults to `false` (the flat RCAC->NOC path) via
35    /// [`FabricConfig::new`].
36    pub issue_icac: bool,
37}
38
39impl FabricConfig {
40    /// Construct a fabric configuration.
41    ///
42    /// This is the supported construction path now that [`FabricConfig`] is
43    /// `#[non_exhaustive]`; the public fields remain readable/writable in
44    /// place.
45    #[must_use]
46    pub fn new(
47        fabric_id: u64,
48        rcac_id: u64,
49        commissioner_node_id: u64,
50        validity: (MatterTime, MatterTime),
51    ) -> Self {
52        Self {
53            fabric_id,
54            rcac_id,
55            commissioner_node_id,
56            validity,
57            issue_icac: false,
58        }
59    }
60}
61
62/// Create a fabric: generate the RCAC root key + self-signed RCAC, a fresh
63/// IPK, the commissioner operational keypair, and the commissioner NOC.
64///
65/// The returned [`FabricEntry`] is fully persistable (private keys captured
66/// as PKCS#8 DER) and has no devices yet.
67///
68/// # Errors
69///
70/// Returns [`Error::Signer`] if key generation fails, or [`Error::Noc`] if
71/// RCAC construction or NOC issuance fails.
72pub fn create_fabric(cfg: &FabricConfig, rng: &dyn NocRng) -> Result<FabricEntry, Error> {
73    // 1. RCAC root key + self-signed root certificate.
74    let (root_signer, rcac_pkcs8) =
75        RingSigner::generate().map_err(|e| Error::Signer(e.to_string()))?;
76    let root_arc: Arc<dyn Signer> = Arc::new(root_signer);
77    let mut fabric_record = FabricRecord::new_root_only(
78        cfg.fabric_id,
79        root_arc,
80        cfg.validity.0,
81        cfg.validity.1,
82        cfg.rcac_id,
83        rng,
84    )?;
85
86    // 1b. Optionally mint an ICAC tier under the RCAC. This must happen
87    //     BEFORE the commissioner NOC is issued below, so the commissioner
88    //     NOC itself is signed under the ICAC (matching what a real
89    //     ICAC-tier fabric does for every NOC it issues).
90    let icac_identity = if cfg.issue_icac {
91        let (icac_signer_raw, icac_pkcs8) =
92            RingSigner::generate().map_err(|e| Error::Signer(e.to_string()))?;
93        let icac_public_key = icac_signer_raw.public_key().clone();
94        // Single-ICAC fabric: reuse `cfg.rcac_id` as the ICAC's `IcacId`
95        // DN value too. `RcacId` and `IcacId` are distinct DN attribute
96        // types (spec §6.5.5), so the shared numeric id is unambiguous —
97        // there is no collision between "RCAC id 7" and "ICAC id 7".
98        let icac_cert = issue_icac(
99            &fabric_record,
100            cfg.rcac_id,
101            &icac_public_key,
102            cfg.validity,
103            rng,
104        )
105        .map_err(Error::Noc)?;
106        fabric_record.icac_signer = Some(Arc::new(icac_signer_raw));
107        fabric_record.icac_cert = Some(icac_cert.clone());
108        Some(IcacIdentity {
109            cert: icac_cert,
110            pkcs8: icac_pkcs8,
111        })
112    } else {
113        None
114    };
115
116    // 2. Commissioner operational keypair.
117    let (comm_signer, comm_pkcs8) =
118        RingSigner::generate().map_err(|e| Error::Signer(e.to_string()))?;
119    let comm_public_key = comm_signer.public_key().clone();
120
121    // 3. Mint the commissioner NOC over our own key. We generated the key
122    //    ourselves, so there is no device CSR to verify — `VerifiedCsr`
123    //    here asserts "this public key is trusted for issuance", which is
124    //    sound for our own identity. When `fabric_record.icac_signer`/
125    //    `icac_cert` are `Some` (set just above), `issue_noc` signs this
126    //    under the ICAC instead of the RCAC.
127    let verified = VerifiedCsr {
128        public_key: comm_public_key,
129    };
130    let noc = issue_noc(
131        &fabric_record,
132        &verified,
133        cfg.commissioner_node_id,
134        &[], // no CASE Authenticated Tags for the controller identity
135        cfg.validity,
136        rng,
137    )?;
138
139    Ok(FabricEntry {
140        fabric_id: cfg.fabric_id,
141        ipk: fabric_record.identity_protection_key,
142        rcac_cert: fabric_record.root_cert.clone(),
143        rcac_pkcs8,
144        commissioner: CommissionerIdentity {
145            node_id: cfg.commissioner_node_id,
146            operational_pkcs8: comm_pkcs8,
147            noc,
148        },
149        devices: Vec::new(),
150        group_keys: Vec::new(),
151        outbound_group_counter: 0,
152        icd_clients: Vec::new(),
153        icac: icac_identity,
154    })
155}
156
157#[cfg(test)]
158#[allow(clippy::unwrap_used, clippy::expect_used)] // Test code: CLAUDE.md allows unwrap/expect with justification.
159mod tests {
160    use super::*;
161    use matter_commissioning::SystemNocRng;
162
163    fn sample_cfg() -> FabricConfig {
164        FabricConfig::new(
165            0xDEAD_BEEF_0000_0001,
166            1,
167            0x0000_0000_0000_0001,
168            (
169                MatterTime::from_unix_secs(1_700_000_000),
170                MatterTime::NO_EXPIRY,
171            ),
172        )
173    }
174
175    #[test]
176    fn new_constructor_sets_all_fields() {
177        // `FabricConfig` is `#[non_exhaustive]`; `new` is the supported
178        // construction path. Verify it populates every field.
179        let cfg = FabricConfig::new(
180            7,
181            9,
182            3,
183            (MatterTime::from_unix_secs(1), MatterTime::NO_EXPIRY),
184        );
185        assert_eq!(cfg.fabric_id, 7);
186        assert_eq!(cfg.rcac_id, 9);
187        assert_eq!(cfg.commissioner_node_id, 3);
188        assert_eq!(cfg.validity.0, MatterTime::from_unix_secs(1));
189    }
190
191    #[test]
192    fn creates_fabric_with_no_devices() {
193        let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
194        assert_eq!(fabric.fabric_id, 0xDEAD_BEEF_0000_0001);
195        assert_eq!(fabric.commissioner.node_id, 1);
196        assert!(fabric.devices.is_empty());
197        assert!(!fabric.rcac_pkcs8.is_empty());
198        assert!(!fabric.commissioner.operational_pkcs8.is_empty());
199    }
200
201    #[test]
202    fn commissioner_noc_is_signed_by_the_rcac() {
203        let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
204        let rcac_key = fabric.rcac_cert.public_key();
205        fabric
206            .commissioner
207            .noc
208            .verify_signed_by(rcac_key)
209            .expect("commissioner NOC must verify under the RCAC");
210    }
211
212    #[test]
213    fn default_path_has_no_icac_and_noc_issuer_is_rcac() {
214        // `issue_icac = false` (the `FabricConfig::new` default) must not
215        // mint an ICAC, and the commissioner NOC's issuer must be the RCAC
216        // subject (the flat RCAC->NOC path, byte-unchanged from Task 7).
217        let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
218        assert!(fabric.icac.is_none());
219        assert_eq!(fabric.commissioner.noc.issuer(), fabric.rcac_cert.subject());
220    }
221
222    #[test]
223    fn issue_icac_true_mints_chain_and_signs_commissioner_noc_under_icac() {
224        let mut cfg = sample_cfg();
225        cfg.issue_icac = true;
226        let entry = create_fabric(&cfg, &SystemNocRng).expect("create");
227
228        // The fabric entry carries a minted ICAC.
229        let icac = entry.icac.clone().expect("icac must be Some");
230
231        // Reconstructing the runtime FabricRecord restores both the ICAC
232        // signer and cert.
233        let rec = entry.to_fabric_record().expect("to_fabric_record");
234        assert!(rec.icac_signer.is_some());
235        assert!(rec.icac_cert.is_some());
236
237        // The commissioner NOC's issuer DN is the ICAC's subject, not the
238        // RCAC's.
239        assert_eq!(entry.commissioner.noc.issuer(), icac.cert.subject());
240        assert_ne!(entry.commissioner.noc.issuer(), entry.rcac_cert.subject());
241
242        // 3-tier chain linkage + signature verification:
243        // RCAC issued/signed the ICAC...
244        assert_eq!(icac.cert.issuer(), entry.rcac_cert.subject());
245        icac.cert
246            .verify_signed_by(entry.rcac_cert.public_key())
247            .expect("icac must verify under the rcac's public key");
248        // ...and the ICAC issued/signed the commissioner NOC.
249        assert_eq!(entry.commissioner.noc.issuer(), icac.cert.subject());
250        entry
251            .commissioner
252            .noc
253            .verify_signed_by(icac.cert.public_key())
254            .expect("commissioner noc must verify under the icac's public key");
255
256        // Snapshot round-trip preserves `icac` as `Some` with a matching
257        // cert.
258        let state = crate::state::ControllerState::new(vec![entry.clone()]);
259        let bytes = crate::snapshot::serialize(&state).expect("serialize");
260        let restored = crate::snapshot::deserialize(&bytes).expect("deserialize");
261        let restored_icac = restored.fabrics[0]
262            .icac
263            .clone()
264            .expect("icac must round-trip as Some");
265        assert_eq!(
266            restored_icac.cert.to_tlv().expect("tlv"),
267            icac.cert.to_tlv().expect("tlv"),
268            "restored icac cert must byte-match the original"
269        );
270    }
271
272    #[test]
273    fn commissioner_signer_matches_persisted_noc_key() {
274        // The persisted operational key must correspond to the NOC's
275        // public key — i.e. we can actually use the identity we minted.
276        let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
277        let signer = fabric.commissioner_signer().expect("reload signer");
278        assert_eq!(
279            signer.public_key().as_bytes(),
280            fabric.commissioner.noc.public_key().as_bytes(),
281            "persisted op key must match the NOC subject public key"
282        );
283    }
284}