use std::sync::Arc;
use matter_cert::MatterTime;
use matter_commissioning::{issue_icac, issue_noc, FabricRecord, NocRng, VerifiedCsr};
use matter_crypto::{RingSigner, Signer};
use crate::error::Error;
use crate::state::{CommissionerIdentity, FabricEntry, IcacIdentity};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FabricConfig {
pub fabric_id: u64,
pub rcac_id: u64,
pub commissioner_node_id: u64,
pub validity: (MatterTime, MatterTime),
pub issue_icac: bool,
}
impl FabricConfig {
#[must_use]
pub fn new(
fabric_id: u64,
rcac_id: u64,
commissioner_node_id: u64,
validity: (MatterTime, MatterTime),
) -> Self {
Self {
fabric_id,
rcac_id,
commissioner_node_id,
validity,
issue_icac: false,
}
}
}
fn validate_validity(window: (MatterTime, MatterTime)) -> Result<(), Error> {
let (not_before, not_after) = window;
if not_before.0 == 0 {
return Err(Error::InvalidFabricValidity(format!(
"not_before is {not_before:?} (the Matter epoch, 2000-01-01T00:00:00Z) — pass a \
real wall-clock time, e.g. MatterTime::from_unix_secs(current_unix_time)"
)));
}
if not_after != MatterTime::NO_EXPIRY && not_after <= not_before {
return Err(Error::InvalidFabricValidity(format!(
"not_after ({not_after:?}) must be after not_before ({not_before:?}), or \
MatterTime::NO_EXPIRY for no expiry"
)));
}
Ok(())
}
const MAX_NOT_BEFORE_AHEAD_SECS: u32 = 24 * 60 * 60;
fn describe(t: MatterTime) -> String {
format!("MatterTime({}) = unix {}", t.0, t.to_unix_secs())
}
pub(crate) fn validate_validity_against_now(
window: (MatterTime, MatterTime),
now: MatterTime,
) -> Result<(), Error> {
let (not_before, not_after) = window;
let limit = now.0.saturating_add(MAX_NOT_BEFORE_AHEAD_SECS);
if not_before.0 > limit {
return Err(Error::InvalidFabricValidity(format!(
"not_before ({}) is more than {MAX_NOT_BEFORE_AHEAD_SECS}s ahead of this host's clock \
({}) — the certificate would install but be not-yet-valid, failing every CASE session \
afterwards. A common cause is passing a MILLISECOND timestamp to \
MatterTime::from_unix_secs, which saturates to MatterTime(u32::MAX)",
describe(not_before),
describe(now),
)));
}
if not_after != MatterTime::NO_EXPIRY && not_after <= now {
return Err(Error::InvalidFabricValidity(format!(
"not_after ({}) is already in the past — this host's clock reads {}. The certificate \
would install (ValidateChipRCAC skips RCAC validity times) but every CASE session \
afterwards would fail with kExpired. Pass a future not_after, or \
MatterTime::NO_EXPIRY for no expiry",
describe(not_after),
describe(now),
)));
}
Ok(())
}
pub(crate) fn create_fabric(cfg: &FabricConfig, rng: &dyn NocRng) -> Result<FabricEntry, Error> {
validate_validity(cfg.validity)?;
let (root_signer, rcac_pkcs8) =
RingSigner::generate().map_err(|e| Error::Signer(e.to_string()))?;
let root_arc: Arc<dyn Signer> = Arc::new(root_signer);
let mut fabric_record = FabricRecord::new_root_only(
cfg.fabric_id,
root_arc,
cfg.validity.0,
cfg.validity.1,
cfg.rcac_id,
rng,
)?;
let icac_identity = if cfg.issue_icac {
let (icac_signer_raw, icac_pkcs8) =
RingSigner::generate().map_err(|e| Error::Signer(e.to_string()))?;
let icac_public_key = icac_signer_raw.public_key().clone();
let icac_cert = issue_icac(
&fabric_record,
cfg.rcac_id,
&icac_public_key,
cfg.validity,
rng,
)
.map_err(Error::Noc)?;
fabric_record.icac_signer = Some(Arc::new(icac_signer_raw));
fabric_record.icac_cert = Some(icac_cert.clone());
Some(IcacIdentity {
cert: icac_cert,
pkcs8: icac_pkcs8,
})
} else {
None
};
let (comm_signer, comm_pkcs8) =
RingSigner::generate().map_err(|e| Error::Signer(e.to_string()))?;
let comm_public_key = comm_signer.public_key().clone();
let verified = VerifiedCsr {
public_key: comm_public_key,
};
let noc = issue_noc(
&fabric_record,
&verified,
cfg.commissioner_node_id,
&[], cfg.validity,
rng,
)?;
Ok(FabricEntry {
fabric_id: cfg.fabric_id,
ipk: fabric_record.identity_protection_key,
rcac_cert: fabric_record.root_cert.clone(),
rcac_pkcs8,
commissioner: CommissionerIdentity {
node_id: cfg.commissioner_node_id,
operational_pkcs8: comm_pkcs8,
noc,
},
devices: Vec::new(),
group_keys: Vec::new(),
outbound_group_counter: 0,
icd_clients: Vec::new(),
icac: icac_identity,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
use matter_commissioning::SystemNocRng;
fn sample_cfg() -> FabricConfig {
FabricConfig::new(
0xDEAD_BEEF_0000_0001,
1,
0x0000_0000_0000_0001,
(
MatterTime::from_unix_secs(1_700_000_000),
MatterTime::NO_EXPIRY,
),
)
}
#[test]
fn new_constructor_sets_all_fields() {
let cfg = FabricConfig::new(
7,
9,
3,
(MatterTime::from_unix_secs(1), MatterTime::NO_EXPIRY),
);
assert_eq!(cfg.fabric_id, 7);
assert_eq!(cfg.rcac_id, 9);
assert_eq!(cfg.commissioner_node_id, 3);
assert_eq!(cfg.validity.0, MatterTime::from_unix_secs(1));
}
#[test]
fn creates_fabric_with_no_devices() {
let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
assert_eq!(fabric.fabric_id, 0xDEAD_BEEF_0000_0001);
assert_eq!(fabric.commissioner.node_id, 1);
assert!(fabric.devices.is_empty());
assert!(!fabric.rcac_pkcs8.is_empty());
assert!(!fabric.commissioner.operational_pkcs8.is_empty());
}
#[test]
fn commissioner_noc_is_signed_by_the_rcac() {
let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
let rcac_key = fabric.rcac_cert.public_key();
fabric
.commissioner
.noc
.verify_signed_by(rcac_key)
.expect("commissioner NOC must verify under the RCAC");
}
#[test]
fn default_path_has_no_icac_and_noc_issuer_is_rcac() {
let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
assert!(fabric.icac.is_none());
assert_eq!(fabric.commissioner.noc.issuer(), fabric.rcac_cert.subject());
}
#[test]
fn issue_icac_true_mints_chain_and_signs_commissioner_noc_under_icac() {
let mut cfg = sample_cfg();
cfg.issue_icac = true;
let entry = create_fabric(&cfg, &SystemNocRng).expect("create");
let icac = entry.icac.clone().expect("icac must be Some");
let rec = entry.to_fabric_record().expect("to_fabric_record");
assert!(rec.icac_signer.is_some());
assert!(rec.icac_cert.is_some());
assert_eq!(entry.commissioner.noc.issuer(), icac.cert.subject());
assert_ne!(entry.commissioner.noc.issuer(), entry.rcac_cert.subject());
assert_eq!(icac.cert.issuer(), entry.rcac_cert.subject());
icac.cert
.verify_signed_by(entry.rcac_cert.public_key())
.expect("icac must verify under the rcac's public key");
assert_eq!(entry.commissioner.noc.issuer(), icac.cert.subject());
entry
.commissioner
.noc
.verify_signed_by(icac.cert.public_key())
.expect("commissioner noc must verify under the icac's public key");
let state = crate::state::ControllerState::new(vec![entry.clone()]);
let bytes = crate::snapshot::serialize(&state).expect("serialize");
let restored = crate::snapshot::deserialize(&bytes).expect("deserialize");
let restored_icac = restored.fabrics[0]
.icac
.clone()
.expect("icac must round-trip as Some");
assert_eq!(
restored_icac.cert.to_tlv().expect("tlv"),
icac.cert.to_tlv().expect("tlv"),
"restored icac cert must byte-match the original"
);
}
#[test]
fn rejects_not_before_at_matter_epoch_zero() {
let mut cfg = sample_cfg();
cfg.validity = (MatterTime::from_unix_secs(0), MatterTime::NO_EXPIRY);
let err = create_fabric(&cfg, &SystemNocRng).expect_err("epoch-zero not_before");
assert!(
matches!(err, Error::InvalidFabricValidity(_)),
"expected InvalidFabricValidity, got {err:?}"
);
assert!(
err.to_string().contains("not_before"),
"error must name not_before: {err}"
);
}
#[test]
fn rejects_not_before_at_matter_epoch_zero_via_raw_constructor() {
let mut cfg = sample_cfg();
cfg.validity = (MatterTime(0), MatterTime::NO_EXPIRY);
let err = create_fabric(&cfg, &SystemNocRng).expect_err("epoch-zero not_before");
assert!(matches!(err, Error::InvalidFabricValidity(_)));
}
#[test]
fn rejects_inverted_validity_window() {
let mut cfg = sample_cfg();
cfg.validity = (
MatterTime::from_unix_secs(1_700_000_100),
MatterTime::from_unix_secs(1_700_000_000),
);
let err = create_fabric(&cfg, &SystemNocRng).expect_err("inverted window");
assert!(
matches!(err, Error::InvalidFabricValidity(_)),
"expected InvalidFabricValidity, got {err:?}"
);
assert!(
err.to_string().contains("not_after"),
"error must name not_after: {err}"
);
}
#[test]
fn rejects_empty_validity_window() {
let mut cfg = sample_cfg();
let t = MatterTime::from_unix_secs(1_700_000_000);
cfg.validity = (t, t);
let err = create_fabric(&cfg, &SystemNocRng).expect_err("empty window");
assert!(matches!(err, Error::InvalidFabricValidity(_)));
}
#[test]
fn accepts_no_expiry_sentinel() {
let cfg = sample_cfg(); create_fabric(&cfg, &SystemNocRng).expect("NO_EXPIRY must be accepted");
}
#[test]
fn rejects_not_before_far_in_the_future() {
let now = MatterTime::from_unix_secs(1_700_000_000);
let window = (
MatterTime::from_unix_secs(1_700_000_000_000),
MatterTime::NO_EXPIRY,
);
assert_eq!(window.0, MatterTime(u32::MAX), "precondition: saturated");
let err = validate_validity_against_now(window, now)
.expect_err("millisecond not_before must be rejected");
assert!(
matches!(err, Error::InvalidFabricValidity(_)),
"expected InvalidFabricValidity, got {err:?}"
);
assert!(
err.to_string().contains("MILLISECOND"),
"error must name the likely cause: {err}"
);
}
#[test]
fn not_before_within_the_skew_window_is_accepted() {
let now = MatterTime::from_unix_secs(1_700_000_000);
let edge = MatterTime(now.0 + MAX_NOT_BEFORE_AHEAD_SECS);
validate_validity_against_now((edge, MatterTime::NO_EXPIRY), now)
.expect("not_before exactly at the skew limit must be accepted");
}
#[test]
fn not_before_one_second_past_the_skew_window_is_rejected() {
let now = MatterTime::from_unix_secs(1_700_000_000);
let over = MatterTime(now.0 + MAX_NOT_BEFORE_AHEAD_SECS + 1);
let err = validate_validity_against_now((over, MatterTime::NO_EXPIRY), now)
.expect_err("one second past the skew limit must be rejected");
assert!(matches!(err, Error::InvalidFabricValidity(_)));
}
#[test]
fn rejects_an_already_expired_window() {
let now = MatterTime::from_unix_secs(1_795_000_000);
let window = (
MatterTime::from_unix_secs(1_700_000_000),
MatterTime::from_unix_secs(1_731_536_000),
);
let err = validate_validity_against_now(window, now)
.expect_err("an expired not_after must be rejected");
assert!(
matches!(err, Error::InvalidFabricValidity(_)),
"expected InvalidFabricValidity, got {err:?}"
);
assert!(
err.to_string().contains("not_after"),
"error must name not_after: {err}"
);
assert!(
err.to_string().contains("unix 1731536000"),
"error must render not_after in readable unix seconds: {err}"
);
}
#[test]
fn not_after_exactly_at_now_is_rejected() {
let now = MatterTime::from_unix_secs(1_700_000_000);
let window = (MatterTime::from_unix_secs(1_699_000_000), now);
let err = validate_validity_against_now(window, now)
.expect_err("not_after == now must be rejected");
assert!(matches!(err, Error::InvalidFabricValidity(_)));
}
#[test]
fn not_after_one_second_past_now_is_accepted() {
let now = MatterTime::from_unix_secs(1_700_000_000);
let window = (
MatterTime::from_unix_secs(1_699_000_000),
MatterTime(now.0 + 1),
);
validate_validity_against_now(window, now)
.expect("a not_after one second ahead must be accepted");
}
#[test]
fn no_expiry_not_after_is_exempt_from_the_expiry_check() {
let now = MatterTime::from_unix_secs(1_700_000_000);
let window = (
MatterTime::from_unix_secs(1_699_000_000),
MatterTime::NO_EXPIRY,
);
validate_validity_against_now(window, now)
.expect("NO_EXPIRY must be exempt from the expiry check");
}
#[test]
fn backdated_not_before_is_always_accepted() {
let now = MatterTime::from_unix_secs(1_700_000_000);
let backdated = MatterTime::from_unix_secs(1_700_000_000 - 3600);
validate_validity_against_now((backdated, MatterTime::NO_EXPIRY), now)
.expect("a backdated not_before must be accepted");
}
#[test]
fn commissioner_signer_matches_persisted_noc_key() {
let fabric = create_fabric(&sample_cfg(), &SystemNocRng).expect("create");
let signer = fabric.commissioner_signer().expect("reload signer");
assert_eq!(
signer.public_key().as_bytes(),
fabric.commissioner.noc.public_key().as_bytes(),
"persisted op key must match the NOC subject public key"
);
}
}