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_noc, FabricRecord, NocRng, VerifiedCsr};
10use matter_crypto::{RingSigner, Signer};
11
12use crate::error::Error;
13use crate::state::{CommissionerIdentity, FabricEntry};
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}
32
33impl FabricConfig {
34    /// Construct a fabric configuration.
35    ///
36    /// This is the supported construction path now that [`FabricConfig`] is
37    /// `#[non_exhaustive]`; the public fields remain readable/writable in
38    /// place.
39    #[must_use]
40    pub fn new(
41        fabric_id: u64,
42        rcac_id: u64,
43        commissioner_node_id: u64,
44        validity: (MatterTime, MatterTime),
45    ) -> Self {
46        Self {
47            fabric_id,
48            rcac_id,
49            commissioner_node_id,
50            validity,
51        }
52    }
53}
54
55/// Create a fabric: generate the RCAC root key + self-signed RCAC, a fresh
56/// IPK, the commissioner operational keypair, and the commissioner NOC.
57///
58/// The returned [`FabricEntry`] is fully persistable (private keys captured
59/// as PKCS#8 DER) and has no devices yet.
60///
61/// # Errors
62///
63/// Returns [`Error::Signer`] if key generation fails, or [`Error::Noc`] if
64/// RCAC construction or NOC issuance fails.
65pub fn create_fabric(cfg: &FabricConfig, rng: &dyn NocRng) -> Result<FabricEntry, Error> {
66    // 1. RCAC root key + self-signed root certificate.
67    let (root_signer, rcac_pkcs8) =
68        RingSigner::generate().map_err(|e| Error::Signer(e.to_string()))?;
69    let root_arc: Arc<dyn Signer> = Arc::new(root_signer);
70    let fabric_record = FabricRecord::new_root_only(
71        cfg.fabric_id,
72        root_arc,
73        cfg.validity.0,
74        cfg.validity.1,
75        cfg.rcac_id,
76        rng,
77    )?;
78
79    // 2. Commissioner operational keypair.
80    let (comm_signer, comm_pkcs8) =
81        RingSigner::generate().map_err(|e| Error::Signer(e.to_string()))?;
82    let comm_public_key = comm_signer.public_key().clone();
83
84    // 3. Mint the commissioner NOC over our own key. We generated the key
85    //    ourselves, so there is no device CSR to verify — `VerifiedCsr`
86    //    here asserts "this public key is trusted for issuance", which is
87    //    sound for our own identity.
88    let verified = VerifiedCsr {
89        public_key: comm_public_key,
90    };
91    let noc = issue_noc(
92        &fabric_record,
93        &verified,
94        cfg.commissioner_node_id,
95        &[], // no CASE Authenticated Tags for the controller identity
96        cfg.validity,
97        rng,
98    )?;
99
100    Ok(FabricEntry {
101        fabric_id: cfg.fabric_id,
102        ipk: fabric_record.identity_protection_key,
103        rcac_cert: fabric_record.root_cert.clone(),
104        rcac_pkcs8,
105        commissioner: CommissionerIdentity {
106            node_id: cfg.commissioner_node_id,
107            operational_pkcs8: comm_pkcs8,
108            noc,
109        },
110        devices: Vec::new(),
111        group_keys: Vec::new(),
112        outbound_group_counter: 0,
113        icd_clients: Vec::new(),
114    })
115}
116
117#[cfg(test)]
118#[allow(clippy::unwrap_used, clippy::expect_used)] // Test code: CLAUDE.md allows unwrap/expect with justification.
119mod tests {
120    use super::*;
121    use matter_commissioning::SystemNocRng;
122
123    fn sample_cfg() -> FabricConfig {
124        FabricConfig::new(
125            0xDEAD_BEEF_0000_0001,
126            1,
127            0x0000_0000_0000_0001,
128            (
129                MatterTime::from_unix_secs(1_700_000_000),
130                MatterTime::NO_EXPIRY,
131            ),
132        )
133    }
134
135    #[test]
136    fn new_constructor_sets_all_fields() {
137        // `FabricConfig` is `#[non_exhaustive]`; `new` is the supported
138        // construction path. Verify it populates every field.
139        let cfg = FabricConfig::new(
140            7,
141            9,
142            3,
143            (MatterTime::from_unix_secs(1), MatterTime::NO_EXPIRY),
144        );
145        assert_eq!(cfg.fabric_id, 7);
146        assert_eq!(cfg.rcac_id, 9);
147        assert_eq!(cfg.commissioner_node_id, 3);
148        assert_eq!(cfg.validity.0, MatterTime::from_unix_secs(1));
149    }
150
151    #[test]
152    fn creates_fabric_with_no_devices() {
153        let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
154        assert_eq!(fabric.fabric_id, 0xDEAD_BEEF_0000_0001);
155        assert_eq!(fabric.commissioner.node_id, 1);
156        assert!(fabric.devices.is_empty());
157        assert!(!fabric.rcac_pkcs8.is_empty());
158        assert!(!fabric.commissioner.operational_pkcs8.is_empty());
159    }
160
161    #[test]
162    fn commissioner_noc_is_signed_by_the_rcac() {
163        let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
164        let rcac_key = fabric.rcac_cert.public_key();
165        fabric
166            .commissioner
167            .noc
168            .verify_signed_by(rcac_key)
169            .expect("commissioner NOC must verify under the RCAC");
170    }
171
172    #[test]
173    fn commissioner_signer_matches_persisted_noc_key() {
174        // The persisted operational key must correspond to the NOC's
175        // public key — i.e. we can actually use the identity we minted.
176        let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
177        let signer = fabric.commissioner_signer().expect("reload signer");
178        assert_eq!(
179            signer.public_key().as_bytes(),
180            fabric.commissioner.noc.public_key().as_bytes(),
181            "persisted op key must match the NOC subject public key"
182        );
183    }
184}