use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6};
use std::time::Duration;
use matter_cert::{MatterTime, TrustedRoots};
use matter_crypto::{derive_compressed_fabric_id, CaseCredentials};
use matter_transport::{Discovery, MrpEvent, ProtocolId, ServiceKind, SessionId, SessionManager};
use crate::driver::case::{resolve_operational_with_attempts, run_case};
use crate::driver::datagram::AsyncDatagram;
use crate::driver::error::DriverError;
use crate::driver::exchange::secured_round_trip;
use crate::driver::TransportReliability;
use crate::im::{CommandPath, ImStatus};
use crate::CommissionedFabric;
use crate::CommissionerConfig;
pub const STREAM_PEER: SocketAddr =
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 5540, 0, 0));
const RESPONSE_DEADLINE: Duration = Duration::from_secs(30);
const CONNECT_NETWORK_RESPONSE_DEADLINE: Duration = Duration::from_secs(60);
fn connect_network_deadline(connect_max_time_seconds: u16) -> Duration {
if connect_max_time_seconds == 0 {
CONNECT_NETWORK_RESPONSE_DEADLINE
} else {
let secs =
connect_max_time_seconds.max(crate::state_machine::DEFAULT_CONNECT_MAX_TIME_SECONDS);
Duration::from_secs(u64::from(secs))
}
}
const RESPONSE_DEADLINE_EXCHANGE_SENTINEL: u16 = 0;
mod attr_id {
pub(super) const BASIC_COMMISSIONING_INFO: u32 = 0x0001;
pub(super) const FEATURE_MAP: u32 =
crate::clusters::network_commissioning::attribute_id::FEATURE_MAP;
pub(super) const CONNECT_MAX_TIME_SECONDS: u32 =
crate::clusters::network_commissioning::attribute_id::CONNECT_MAX_TIME_SECONDS;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum InvokeOutcome {
Command(Vec<u8>),
Status(ImStatus),
}
pub(crate) async fn dispatch_invoke<T: AsyncDatagram>(
transport: &T,
sessions: &mut SessionManager,
session_id: SessionId,
peer: SocketAddr,
path: CommandPath,
fields_tlv: &[u8],
) -> Result<InvokeOutcome, DriverError> {
const OP_INVOKE_REQUEST: u8 = 0x08;
let msg = crate::im::build_invoke_request(path, fields_tlv);
let resp = secured_round_trip(
transport,
sessions,
session_id,
peer,
OP_INVOKE_REQUEST,
ProtocolId::INTERACTION_MODEL,
&msg,
)
.await?;
match crate::im::parse_invoke_response(&resp.payload)? {
crate::im::InvokeResponse::Command { fields_tlv, .. } => {
Ok(InvokeOutcome::Command(fields_tlv))
}
crate::im::InvokeResponse::Status(s) => Ok(InvokeOutcome::Status(s)),
}
}
pub(crate) fn extract_read_payload(
expect: crate::Expectation,
report: &crate::im::ReportData,
) -> Result<Vec<u8>, DriverError> {
use crate::im::ImError;
use crate::Expectation;
use matter_codec::{Tag, TlvWriter, Value};
match expect {
Expectation::NetworkCommissioningInfo => {
let feat_val = report
.attributes()
.find(|(p, _)| {
p.cluster == crate::clusters::network_commissioning::CLUSTER_ID
&& p.attribute == attr_id::FEATURE_MAP
})
.map(|(_, v)| v)
.ok_or_else(|| {
DriverError::Im(ImError::MissingField(
"FeatureMap attribute absent from NetworkCommissioning ReportData",
))
})?;
let raw = match feat_val {
Value::Uint(n) => *n,
_ => {
return Err(DriverError::Im(ImError::UnexpectedValue(
"FeatureMap value is not a Uint",
)))
}
};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.put_uint(Tag::Anonymous, raw)
.map_err(|e| DriverError::Im(ImError::Codec(e)))?;
Ok(buf)
}
Expectation::CommissioningInfo => {
let struct_val = report
.attributes()
.find(|(p, _)| {
p.cluster == crate::clusters::general_commissioning::CLUSTER_ID
&& p.attribute == attr_id::BASIC_COMMISSIONING_INFO
})
.map(|(_, v)| v)
.ok_or_else(|| {
DriverError::Im(ImError::MissingField(
"BasicCommissioningInfo attribute absent from GeneralCommissioning ReportData",
))
})?;
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.write_value(Tag::Anonymous, struct_val)
.map_err(|e| DriverError::Im(ImError::Codec(e)))?;
Ok(buf)
}
_ => Err(DriverError::Im(ImError::UnexpectedValue(
"extract_read_payload called with a non-read Expectation",
))),
}
}
fn extract_connect_max_time_seconds(report: &crate::im::ReportData) -> Option<u16> {
use crate::clusters::network_commissioning as nc;
use matter_codec::{Tag, TlvWriter, Value};
let raw = report
.attributes()
.find(|(p, _)| {
p.cluster == nc::CLUSTER_ID && p.attribute == attr_id::CONNECT_MAX_TIME_SECONDS
})
.and_then(|(_, v)| match v {
Value::Uint(n) => Some(*n),
_ => None,
})?;
let mut buf = Vec::new();
TlvWriter::new(&mut buf)
.put_uint(Tag::Anonymous, raw)
.ok()?;
nc::decode_connect_max_time_seconds(&buf).ok()
}
async fn flush_pending_acks<T: AsyncDatagram>(
transport: &T,
sessions: &mut SessionManager,
peer: SocketAddr,
) -> Result<(), DriverError> {
use std::time::{Duration, Instant};
const FLUSH_HORIZON: Duration = Duration::from_millis(500);
while let Some(deadline) = sessions.poll_timeout() {
let wait = deadline.saturating_duration_since(Instant::now());
if wait > FLUSH_HORIZON {
break;
}
tokio::time::sleep(wait).await;
for event in sessions.handle_timeout(Instant::now()) {
match event {
MrpEvent::Retransmit { packet, .. }
| MrpEvent::SendStandaloneAck { packet, .. } => {
transport.send_to(&packet, peer).await?;
}
_ => {}
}
}
}
Ok(())
}
pub(crate) async fn dispatch_read<T: AsyncDatagram>(
transport: &T,
sessions: &mut SessionManager,
session_id: SessionId,
peer: SocketAddr,
paths: &[crate::im::AttributePath],
) -> Result<crate::im::ReportData, DriverError> {
const OP_READ_REQUEST: u8 = 0x02;
let msg = crate::im::build_read_request(paths);
let resp = secured_round_trip(
transport,
sessions,
session_id,
peer,
OP_READ_REQUEST,
ProtocolId::INTERACTION_MODEL,
&msg,
)
.await?;
#[cfg(feature = "tracing")]
tracing::debug!(
report_data_tlv = %crate::hexdump::hex(&resp.payload),
"ReportData received"
);
let report = crate::im::parse_report_data(&resp.payload)?;
Ok(report)
}
const RESOLVE_POLL_ATTEMPTS: usize = 50;
const RESOLVE_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100);
pub struct DriverConfig<'a> {
pub commissioner: CommissionerConfig<'a>,
pub commissionable_addr: Option<SocketAddr>,
pub passcode: u32,
pub commissioner_noc: &'a matter_cert::MatterCertificate,
pub commissioner_signer_pkcs8: &'a [u8],
}
pub async fn resolve_commissionable<D: Discovery>(
discovery: &mut D,
discriminator: u16,
) -> Result<SocketAddr, DriverError> {
let short = ((discriminator >> 8) & 0x0F) as u8;
let handle = discovery
.query(ServiceKind::Commissionable)
.map_err(DriverError::Transport)?;
let window_open =
|svc: &matter_transport::MatterService| svc.txt_str("CM").is_none_or(|v| v != "0");
for _ in 0..RESOLVE_POLL_ATTEMPTS {
let results = discovery.poll_results(handle);
for svc in results.iter().filter(|s| window_open(s)) {
if svc.txt_str("D").and_then(|d| d.parse::<u16>().ok()) == Some(discriminator) {
if let Some(addr) = crate::driver::case::preferred_address(&svc.addresses) {
discovery.stop_query(handle);
return Ok(SocketAddr::new(addr, svc.port));
}
}
}
for svc in results.iter().filter(|s| window_open(s)) {
let advertised = svc.txt_str("D").and_then(|d| d.parse::<u16>().ok());
if let Some(adv) = advertised {
if ((adv >> 8) & 0x0F) as u8 == short {
if let Some(addr) = crate::driver::case::preferred_address(&svc.addresses) {
discovery.stop_query(handle);
return Ok(SocketAddr::new(addr, svc.port));
}
}
}
}
tokio::time::sleep(RESOLVE_POLL_INTERVAL).await;
}
discovery.stop_query(handle);
Err(DriverError::Discovery(format!(
"commissionable device with discriminator {discriminator} (short {short:#x}) not found via mDNS"
)))
}
pub(crate) async fn rollback<T: AsyncDatagram>(
transport: &T,
sessions: &mut SessionManager,
pase_session_id: SessionId,
peer: SocketAddr,
) {
let path = CommandPath {
endpoint: 0,
cluster: crate::clusters::general_commissioning::CLUSTER_ID,
command: crate::clusters::general_commissioning::command_id::ARM_FAIL_SAFE,
};
let fields = crate::clusters::general_commissioning::encode_arm_fail_safe(0, 0);
let _ = dispatch_invoke(transport, sessions, pase_session_id, peer, path, &fields).await;
}
#[allow(clippy::too_many_arguments)] pub(crate) async fn establish_case_session<T: AsyncDatagram, D: Discovery>(
transport: &T,
sessions: &mut SessionManager,
discovery: &mut D,
root_public_key: &[u8; 65],
fabric_id: u64,
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
peer_node_id: u64,
now: MatterTime,
resolve_attempts: u32,
) -> Result<(SessionId, SocketAddr), DriverError> {
let compressed =
derive_compressed_fabric_id(root_public_key, fabric_id).map_err(DriverError::Crypto)?;
let peer_addr =
resolve_operational_with_attempts(discovery, compressed, peer_node_id, resolve_attempts)
.await?;
let peer_fabric_id = credentials.fabric_id;
let sid = run_case(
transport,
sessions,
peer_addr,
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
now,
)
.await?;
Ok((sid, peer_addr))
}
pub struct BleDriverConfig<'a> {
pub commissioner: CommissionerConfig<'a>,
pub passcode: u32,
pub commissioner_noc: &'a matter_cert::MatterCertificate,
pub commissioner_signer_pkcs8: &'a [u8],
}
pub async fn commission<T, D>(
transport: &T,
discovery: &mut D,
config: DriverConfig<'_>,
) -> Result<CommissionedFabric, DriverError>
where
T: AsyncDatagram,
D: matter_transport::Discovery,
{
let commissioner_noc = config.commissioner_noc;
let commissioner_pkcs8 = config.commissioner_signer_pkcs8;
let peer = if let Some(addr) = config.commissionable_addr {
addr
} else {
let disc = config.commissioner.setup_payload.discriminator.as_u16();
resolve_commissionable(discovery, disc).await?
};
run_commission(
transport,
peer,
transport,
discovery,
config.commissioner,
config.passcode,
commissioner_noc,
commissioner_pkcs8,
TransportReliability::Mrp,
crate::driver::case::RESOLVE_POLL_ATTEMPTS,
)
.await
}
pub async fn commission_ble<B, U, D>(
btp: &B,
udp: &U,
discovery: &mut D,
config: BleDriverConfig<'_>,
) -> Result<CommissionedFabric, DriverError>
where
B: AsyncDatagram,
U: AsyncDatagram,
D: matter_transport::Discovery,
{
run_commission(
btp,
STREAM_PEER,
udp,
discovery,
config.commissioner,
config.passcode,
config.commissioner_noc,
config.commissioner_signer_pkcs8,
TransportReliability::TransportProvides,
crate::driver::case::BLE_RESOLVE_POLL_ATTEMPTS,
)
.await
}
#[allow(clippy::too_many_arguments)] async fn run_commission<P, O, D>(
pase_transport: &P,
pase_peer: SocketAddr,
op_transport: &O,
discovery: &mut D,
commissioner_cfg: CommissionerConfig<'_>,
passcode: u32,
commissioner_noc: &matter_cert::MatterCertificate,
commissioner_pkcs8: &[u8],
reliability: TransportReliability,
resolve_attempts: u32,
) -> Result<CommissionedFabric, DriverError>
where
P: AsyncDatagram,
O: AsyncDatagram,
D: matter_transport::Discovery,
{
use matter_crypto::RingSigner;
let commissioner_noc = commissioner_noc.clone();
let mut sessions = SessionManager::new();
let pase_sid = crate::driver::pase::run_pase_with(
pase_transport,
&mut sessions,
pase_peer,
passcode,
reliability,
)
.await?;
let pase_attestation_challenge = sessions
.get(pase_sid)
.ok_or(DriverError::Handshake(
"PASE session missing after run_pase",
))?
.keys
.attestation_key;
let mut commissioner_cfg = commissioner_cfg;
commissioner_cfg.pase_attestation_challenge = pase_attestation_challenge;
let fabric = commissioner_cfg.fabric;
let commissioner_node_id = commissioner_cfg.commissioner_node_id;
let ipk_epoch_key = commissioner_cfg.ipk_epoch_key;
let validation_time = commissioner_cfg.now;
let commissioner_signer_value =
RingSigner::from_pkcs8(commissioner_pkcs8).map_err(DriverError::Crypto)?;
let mut commissioner_signer: Option<RingSigner> = Some(commissioner_signer_value);
let mut case_slot: Option<(SessionId, SocketAddr)> = None;
let mut sm = crate::Commissioner::new(commissioner_cfg)?;
let outcome = run_poll_loop(
&mut sm,
pase_transport,
op_transport,
discovery,
&mut sessions,
pase_sid,
pase_peer,
&mut case_slot,
&mut commissioner_signer,
&commissioner_noc,
commissioner_node_id,
&ipk_epoch_key,
fabric,
validation_time,
reliability,
resolve_attempts,
)
.await;
match outcome {
Ok(fabric) => Ok(fabric),
Err(exit) => {
if disarm_on_exit(&exit) {
let _ = with_response_deadline(reliability, None, async {
rollback(pase_transport, &mut sessions, pase_sid, pase_peer).await;
Ok(())
})
.await;
}
Err(exit.into_driver_error())
}
}
}
async fn with_response_deadline<T>(
reliability: TransportReliability,
connect_max_time_seconds: Option<u16>,
fut: impl std::future::Future<Output = Result<T, DriverError>>,
) -> Result<T, DriverError> {
match reliability {
TransportReliability::Mrp => fut.await,
TransportReliability::TransportProvides => {
let deadline = match connect_max_time_seconds {
None => RESPONSE_DEADLINE,
Some(secs) => connect_network_deadline(secs),
};
match tokio::time::timeout(deadline, fut).await {
Ok(result) => result,
Err(_elapsed) => Err(DriverError::Timeout {
exchange_id: RESPONSE_DEADLINE_EXCHANGE_SENTINEL,
}),
}
}
}
}
fn disarm_on_exit(exit: &LoopExit) -> bool {
match exit {
LoopExit::Failed(_) => true,
LoopExit::Aborted { disarm, .. } => *disarm,
}
}
enum LoopExit {
Aborted {
disarm: bool,
reason: String,
},
Failed(DriverError),
}
impl LoopExit {
fn into_driver_error(self) -> DriverError {
match self {
LoopExit::Aborted { reason, .. } => DriverError::Aborted(reason),
LoopExit::Failed(e) => e,
}
}
}
impl From<DriverError> for LoopExit {
fn from(e: DriverError) -> Self {
LoopExit::Failed(e)
}
}
impl From<crate::CommissioningError> for LoopExit {
fn from(e: crate::CommissioningError) -> Self {
LoopExit::Failed(DriverError::from(e))
}
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
async fn run_poll_loop<P, O, D>(
sm: &mut crate::Commissioner,
pase_transport: &P,
op_transport: &O,
discovery: &mut D,
sessions: &mut SessionManager,
pase_sid: SessionId,
pase_peer: SocketAddr,
case_slot: &mut Option<(SessionId, SocketAddr)>,
commissioner_signer: &mut Option<matter_crypto::RingSigner>,
commissioner_noc: &matter_cert::MatterCertificate,
commissioner_node_id: u64,
ipk_epoch_key: &[u8; 16],
fabric: &crate::FabricRecord,
validation_time: MatterTime,
reliability: TransportReliability,
resolve_attempts: u32,
) -> Result<CommissionedFabric, LoopExit>
where
P: AsyncDatagram,
O: AsyncDatagram,
D: matter_transport::Discovery,
{
use crate::{Action, SessionContext};
use matter_cert::{TrustAnchor, TrustedRoots};
loop {
let action = sm.poll().map_err(DriverError::from)?;
match action {
Action::Invoke {
session,
endpoint,
cluster,
command,
payload,
expect,
} => {
let path = crate::im::CommandPath {
endpoint,
cluster,
command,
};
let is_connect_network = cluster
== crate::clusters::network_commissioning::CLUSTER_ID
&& command
== crate::clusters::network_commissioning::command_id::CONNECT_NETWORK;
let connect_max_time_seconds =
is_connect_network.then(|| sm.connect_max_time_seconds());
let outcome = match session {
SessionContext::Pase => {
with_response_deadline(
reliability,
connect_max_time_seconds,
dispatch_invoke(
pase_transport,
sessions,
pase_sid,
pase_peer,
path,
&payload,
),
)
.await?
}
SessionContext::Case => {
let (sid, addr) = case_slot.ok_or(DriverError::Handshake(
"CASE session required but not yet established",
))?;
dispatch_invoke(op_transport, sessions, sid, addr, path, &payload).await?
}
};
let response_payload: Vec<u8> = match outcome {
InvokeOutcome::Command(fields) => fields,
InvokeOutcome::Status(crate::im::ImStatus::Success) => vec![0x00],
InvokeOutcome::Status(crate::im::ImStatus::Failure(code)) => vec![code],
InvokeOutcome::Status(_) => vec![0x01],
};
sm.on_response(expect, &response_payload)?;
}
Action::ReadAttribute {
session,
endpoint,
cluster,
attributes,
expect,
} => {
let paths: Vec<crate::im::AttributePath> = attributes
.iter()
.map(|&attr| crate::im::AttributePath {
endpoint,
cluster,
attribute: attr,
})
.collect();
let report = match session {
SessionContext::Pase => {
with_response_deadline(
reliability,
None,
dispatch_read(pase_transport, sessions, pase_sid, pase_peer, &paths),
)
.await?
}
SessionContext::Case => {
let (sid, addr) = case_slot.ok_or(DriverError::Handshake(
"CASE session required but not yet established for ReadAttribute",
))?;
dispatch_read(op_transport, sessions, sid, addr, &paths).await?
}
};
let read_payload = extract_read_payload(expect, &report)?;
sm.on_response(expect, &read_payload)?;
if expect == crate::Expectation::NetworkCommissioningInfo {
if let Some(secs) = extract_connect_max_time_seconds(&report) {
sm.set_connect_max_time_seconds(secs);
}
}
}
Action::EstablishCase {
fabric_id,
peer_node_id,
} => {
flush_pending_acks(pase_transport, sessions, pase_peer).await?;
let signer = commissioner_signer.take().ok_or(DriverError::Handshake(
"EstablishCase emitted more than once per commission() run",
))?;
let compressed_fabric_id = matter_crypto::derive_compressed_fabric_id(
fabric.root_public_key.as_bytes(),
fabric_id,
)
.map_err(DriverError::Crypto)?;
let operational_ipk = matter_crypto::operational::derive_operational_ipk(
ipk_epoch_key,
&compressed_fabric_id,
)
.map_err(DriverError::Crypto)?;
let credentials = CaseCredentials {
noc: commissioner_noc.clone(),
icac: fabric.icac_cert.clone(),
signer: Box::new(signer),
fabric_id,
node_id: commissioner_node_id,
ipk: operational_ipk,
rcac_public_key: *fabric.root_public_key.as_bytes(),
};
let mut trusted_roots = TrustedRoots::new();
trusted_roots.add(TrustAnchor::from_root_cert(&fabric.root_cert));
match establish_case_session(
op_transport,
sessions,
discovery,
fabric.root_public_key.as_bytes(),
fabric_id,
credentials,
trusted_roots,
peer_node_id,
validation_time,
resolve_attempts,
)
.await
{
Ok((sid, addr)) => {
*case_slot = Some((sid, addr));
sm.on_case_established()?;
}
Err(e) => {
#[cfg(feature = "tracing")]
tracing::debug!(error = %e, "CASE establishment failed");
#[cfg(not(feature = "tracing"))]
let _ = &e;
sm.on_response(crate::Expectation::CaseFailed, &[])?;
}
}
}
Action::Done(commissioned_fabric) => {
return Ok(commissioned_fabric);
}
Action::Abort {
send_disarm_failsafe,
reason,
} => {
return Err(LoopExit::Aborted {
disarm: send_disarm_failsafe,
reason,
});
}
Action::EvictCase { .. } => {
return Err(LoopExit::Failed(DriverError::Handshake(
"unexpected Action::EvictCase in M6 commission() loop (multi-fabric not implemented)",
)));
}
}
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::items_after_statements, // nested struct/impl in test body
clippy::too_many_lines // CASE integration tests are inherently verbose
)] mod tests {
use super::*;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use std::time::Instant;
use matter_codec::{Tag, TlvWriter};
use matter_crypto::pase::PaseSessionKeys;
use matter_transport::{
DecodeInboundOutput, MatterService, MrpFlags, PeerHint, ProtocolId, QueryHandle,
SessionRole,
};
use crate::driver::datagram::InMemoryDatagram;
#[derive(Clone, Debug, PartialEq, Eq)]
struct InvokeRequest {
path: crate::im::CommandPath,
fields_tlv: Vec<u8>,
}
fn parse_invoke_request(bytes: &[u8]) -> Result<InvokeRequest, crate::im::ImError> {
use crate::im::{
error::ImError, expect_message_struct, read_container_members, read_container_value,
skip_container, CommandPath,
};
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut r = TlvReader::new(bytes);
expect_message_struct(&mut r)?;
loop {
match r.next()? {
None | Some(Element::ContainerEnd) => {
return Err(ImError::MissingField("InvokeRequests"))
}
Some(Element::ContainerStart {
tag: Tag::Context(2),
kind: ContainerKind::Array,
}) => break,
Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
Some(_) => {}
}
}
match r.next()? {
Some(Element::ContainerStart {
kind: ContainerKind::Structure,
..
}) => {}
_ => return Err(ImError::MissingField("CommandDataIB")),
}
let mut path: Option<CommandPath> = None;
let mut fields_tlv: Vec<u8> = Vec::new();
loop {
match r.next()? {
None => return Err(ImError::MissingField("CommandDataIB.body")),
Some(Element::ContainerEnd) => break,
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::List,
}) => {
let members = read_container_members(&mut r)?;
let mut endpoint = None;
let mut cluster = None;
let mut command = None;
for (tag, v) in &members {
match (tag, v) {
(Tag::Context(0), Value::Uint(n)) => {
endpoint = Some(u16::try_from(*n).map_err(|_| {
ImError::UnexpectedValue("CommandPath.endpoint exceeds u16")
})?);
}
(Tag::Context(1), Value::Uint(n)) => {
cluster = Some(u32::try_from(*n).map_err(|_| {
ImError::UnexpectedValue("CommandPath.cluster exceeds u32")
})?);
}
(Tag::Context(2), Value::Uint(n)) => {
command = Some(u32::try_from(*n).map_err(|_| {
ImError::UnexpectedValue("CommandPath.command exceeds u32")
})?);
}
_ => {}
}
}
path = Some(CommandPath {
endpoint: endpoint.ok_or(ImError::MissingField("CommandPath.endpoint"))?,
cluster: cluster.ok_or(ImError::MissingField("CommandPath.cluster"))?,
command: command.ok_or(ImError::MissingField("CommandPath.command"))?,
});
}
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind,
}) => {
let v = read_container_value(&mut r, kind)?;
let mut buf = Vec::new();
let mut w = matter_codec::TlvWriter::new(&mut buf);
w.write_value(Tag::Anonymous, &v).unwrap();
fields_tlv = buf;
}
Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
Some(_) => {}
}
}
if fields_tlv.is_empty() {
let mut buf = Vec::new();
let mut w = matter_codec::TlvWriter::new(&mut buf);
w.write_value(Tag::Anonymous, &Value::Structure(Vec::new()))
.unwrap();
fields_tlv = buf;
}
Ok(InvokeRequest {
path: path.ok_or(ImError::MissingField("CommandDataIB.CommandPath"))?,
fields_tlv,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ArmFailSafeFields {
expiry_length_seconds: u16,
breadcrumb: u64,
}
fn decode_arm_fail_safe_fields(tlv: &[u8]) -> ArmFailSafeFields {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut r = TlvReader::new(tlv);
if !matches!(
r.next().ok().flatten(),
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
})
) {
return ArmFailSafeFields {
expiry_length_seconds: 0,
breadcrumb: 0,
};
}
let mut expiry: u16 = 0;
let mut breadcrumb: u64 = 0;
loop {
match r.next().ok().flatten() {
None | Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(v),
}) => {
expiry = u16::try_from(v).unwrap_or(0);
}
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Uint(v),
}) => {
breadcrumb = v;
}
Some(_) => {}
}
}
ArmFailSafeFields {
expiry_length_seconds: expiry,
breadcrumb,
}
}
fn encode_arm_fail_safe_response(error_code: u8) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), u64::from(error_code)).unwrap();
w.end_container().unwrap();
buf
}
struct FakeDiscovery {
service: MatterService,
}
impl Discovery for FakeDiscovery {
fn publish(&mut self, _s: &MatterService) -> matter_transport::Result<()> {
Ok(())
}
fn unpublish(&mut self, _n: &str, _k: ServiceKind) -> matter_transport::Result<()> {
Ok(())
}
fn query(&mut self, _k: ServiceKind) -> matter_transport::Result<QueryHandle> {
Ok(QueryHandle(1))
}
fn stop_query(&mut self, _h: QueryHandle) {}
fn poll_results(&mut self, _h: QueryHandle) -> Vec<MatterService> {
vec![self.service.clone()]
}
}
#[tokio::test]
async fn resolve_commissionable_matches_discriminator() {
const DISCRIMINATOR: u16 = 0xF00;
let mut txt = HashMap::new();
txt.insert("D".to_string(), DISCRIMINATOR.to_string().into_bytes());
let mut disc = FakeDiscovery {
service: MatterService::new(
"AABBCCDDEEFF1122".to_string(),
ServiceKind::Commissionable,
vec![IpAddr::V4(Ipv4Addr::new(192, 0, 2, 42))],
5540,
txt,
),
};
let addr = resolve_commissionable(&mut disc, DISCRIMINATOR)
.await
.unwrap();
assert_eq!(
addr,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 42)), 5540)
);
}
#[tokio::test]
async fn resolve_commissionable_matches_short_discriminator_from_manual_code() {
const DEVICE_LONG: u16 = 0x4B4;
const MANUAL_SHORT_PACKED: u16 = 0x0400;
let mut txt = HashMap::new();
txt.insert("D".to_string(), DEVICE_LONG.to_string().into_bytes());
let mut disc = FakeDiscovery {
service: MatterService::new(
"3C64CF0B1D42".to_string(),
ServiceKind::Commissionable,
vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 248))],
5540,
txt,
),
};
let addr = resolve_commissionable(&mut disc, MANUAL_SHORT_PACKED)
.await
.unwrap();
assert_eq!(
addr,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 248)), 5540)
);
}
fn paired_pase_sessions() -> (SessionManager, SessionManager) {
let keys = PaseSessionKeys {
ke: [0u8; 16],
i2r_key: [1u8; 16],
r2i_key: [2u8; 16],
attestation_key: [3u8; 16],
};
let mut ctrl = SessionManager::new();
let mut dev = SessionManager::new();
ctrl.register_pase(keys.clone(), SessionRole::Initiator, 1, PeerHint::default());
dev.register_pase(keys, SessionRole::Responder, 1, PeerHint::default());
(ctrl, dev)
}
fn build_canned_invoke_response(path: crate::im::CommandPath, fields_tlv: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap(); w.put_bool(Tag::Context(0), false).unwrap(); w.start_array(Tag::Context(1)).unwrap(); {
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap();
w.put_uint(Tag::Context(0), u64::from(path.endpoint))
.unwrap();
w.put_uint(Tag::Context(1), u64::from(path.cluster))
.unwrap();
w.put_uint(Tag::Context(2), u64::from(path.command))
.unwrap();
w.end_container().unwrap(); w.put_preencoded(Tag::Context(1), fields_tlv).unwrap();
w.end_container().unwrap(); w.end_container().unwrap(); }
w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), u64::from(crate::im::IM_REVISION))
.unwrap();
w.end_container().unwrap(); buf
}
#[tokio::test]
async fn dispatch_invoke_returns_command_fields() {
let (mut ctrl, mut dev) = paired_pase_sessions();
let session = SessionId(1);
let (ctrl_io, dev_io) = InMemoryDatagram::pair();
let dev_addr = dev_io.local_addr();
let ctrl_addr = ctrl_io.local_addr();
let canned_fields: Vec<u8> = vec![0x15, 0x18];
let path = CommandPath {
endpoint: 0,
cluster: 0x0030, command: 0x00, };
let canned_response = build_canned_invoke_response(path, &canned_fields);
let controller =
dispatch_invoke(&ctrl_io, &mut ctrl, session, dev_addr, path, &canned_fields);
let device = async {
loop {
let (pkt, _) = dev_io.recv_from().await.unwrap();
if let DecodeInboundOutput::AppMessage { exchange_id, .. } =
dev.decode_inbound(&pkt, Instant::now()).unwrap()
{
let out = dev
.encode_outbound(
session,
Some(exchange_id),
0x09,
ProtocolId::INTERACTION_MODEL,
&canned_response,
MrpFlags { reliable: true },
Instant::now(),
)
.unwrap();
dev_io.send_to(&out.wire_bytes, ctrl_addr).await.unwrap();
break;
}
}
};
let (outcome, ()) = tokio::join!(controller, device);
assert_eq!(outcome.unwrap(), InvokeOutcome::Command(canned_fields));
}
fn build_canned_report_data(
entries: &[(crate::im::AttributePath, matter_codec::Value)],
) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap(); w.start_array(Tag::Context(1)).unwrap(); for (path, value) in entries {
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(1)).unwrap();
w.put_uint(Tag::Context(2), u64::from(path.endpoint))
.unwrap();
w.put_uint(Tag::Context(3), u64::from(path.cluster))
.unwrap();
w.put_uint(Tag::Context(4), u64::from(path.attribute))
.unwrap();
w.end_container().unwrap(); w.write_value(Tag::Context(2), value).unwrap();
w.end_container().unwrap(); w.end_container().unwrap(); }
w.end_container().unwrap(); w.put_uint(Tag::Context(0xFF), u64::from(crate::im::IM_REVISION))
.unwrap();
w.end_container().unwrap(); buf
}
#[test]
fn extract_read_payload_network_commissioning_info_bare_uint() {
use crate::im::{AttributePath, AttributeReportItem, ReportOp};
use crate::Expectation;
use matter_codec::Value;
let feat_val: u64 = 0x01;
let report = crate::im::ReportData::new(
vec![AttributeReportItem::new(
AttributePath {
endpoint: 0,
cluster: crate::clusters::network_commissioning::CLUSTER_ID, attribute: crate::clusters::network_commissioning::attribute_id::FEATURE_MAP, },
ReportOp::Replace,
Value::Uint(feat_val),
None,
)],
None,
false,
false,
);
let payload = extract_read_payload(Expectation::NetworkCommissioningInfo, &report).unwrap();
let mut expected = Vec::new();
let mut w = matter_codec::TlvWriter::new(&mut expected);
w.put_uint(matter_codec::Tag::Anonymous, feat_val).unwrap();
assert_eq!(
payload, expected,
"NetworkCommissioningInfo payload mismatch: got {payload:02x?}, expected {expected:02x?}",
);
let features = crate::clusters::network_commissioning::decode_feature_map(&payload)
.expect("decode_feature_map should accept the re-encoded payload");
assert!(
features.contains(
crate::clusters::network_commissioning::NetworkCommissioningFeature::WIFI
),
"WIFI bit must be set after round-trip",
);
}
#[test]
fn extract_read_payload_picks_basic_commissioning_info_at_0x0001() {
use crate::im::{AttributePath, AttributeReportItem, ReportOp};
use crate::Expectation;
use matter_codec::{Tag, Value};
let gc = crate::clusters::general_commissioning::CLUSTER_ID;
let item = |attribute: u32, value: Value| {
AttributeReportItem::new(
AttributePath {
endpoint: 0,
cluster: gc,
attribute,
},
ReportOp::Replace,
value,
None,
)
};
let report = crate::im::ReportData::new(
vec![
item(0x0004, Value::Bool(true)), item(0x0002, Value::Uint(0)), item(
0x0001, Value::Structure(vec![
(Tag::Context(0), Value::Uint(60)),
(Tag::Context(1), Value::Uint(900)),
]),
),
item(0x0000, Value::Uint(0)), ],
None,
false,
false,
);
let payload = extract_read_payload(Expectation::CommissioningInfo, &report).unwrap();
assert_eq!(
payload.first(),
Some(&0x15u8),
"extractor must return the BasicCommissioningInfo struct, \
not another attribute's value"
);
assert_eq!(payload.last(), Some(&0x18u8));
}
#[test]
fn extract_read_payload_commissioning_info_struct() {
use crate::im::{AttributePath, AttributeReportItem, ReportOp};
use crate::Expectation;
use matter_codec::{Tag, Value};
let struct_value = Value::Structure(vec![
(Tag::Context(0), Value::Uint(120)),
(Tag::Context(1), Value::Uint(900)),
]);
let report = crate::im::ReportData::new(
vec![AttributeReportItem::new(
AttributePath {
endpoint: 0,
cluster: crate::clusters::general_commissioning::CLUSTER_ID, attribute: attr_id::BASIC_COMMISSIONING_INFO, },
ReportOp::Replace,
struct_value.clone(),
None,
)],
None,
false,
false,
);
let payload = extract_read_payload(Expectation::CommissioningInfo, &report).unwrap();
assert_eq!(
payload.first(),
Some(&0x15u8),
"should start with anon-struct byte"
);
assert_eq!(
payload.last(),
Some(&0x18u8),
"should end with end-container"
);
let info =
crate::clusters::general_commissioning::decode_basic_commissioning_info(&payload)
.expect("decode_basic_commissioning_info should accept the re-encoded payload");
assert_eq!(info.failsafe_expiry_length_seconds, 120);
assert_eq!(info.max_cumulative_failsafe_seconds, 900);
}
#[test]
fn extract_read_payload_missing_feature_map_returns_error() {
use crate::Expectation;
let report = crate::im::ReportData::new(Vec::new(), None, false, false);
let err = extract_read_payload(Expectation::NetworkCommissioningInfo, &report)
.expect_err("missing attribute should fail");
assert!(
matches!(err, DriverError::Im(_)),
"expected DriverError::Im, got {err:?}",
);
}
#[test]
fn extract_read_payload_non_read_expectation_returns_error() {
use crate::Expectation;
let report = crate::im::ReportData::new(Vec::new(), None, false, false);
let err = extract_read_payload(Expectation::ArmFailsafeResponse, &report)
.expect_err("non-read expectation should fail");
assert!(
matches!(err, DriverError::Im(_)),
"expected DriverError::Im, got {err:?}",
);
}
#[tokio::test]
async fn dispatch_read_and_extract_network_commissioning_info() {
use crate::im::AttributePath;
use crate::Expectation;
use matter_codec::Value;
let (mut ctrl, mut dev) = paired_pase_sessions();
let session = SessionId(1);
let (ctrl_io, dev_io) = InMemoryDatagram::pair();
let dev_addr = dev_io.local_addr();
let ctrl_addr = ctrl_io.local_addr();
let feat_val: u64 = 0x01;
let entries = vec![(
AttributePath {
endpoint: 0,
cluster: crate::clusters::network_commissioning::CLUSTER_ID,
attribute: crate::clusters::network_commissioning::attribute_id::FEATURE_MAP,
},
Value::Uint(feat_val),
)];
let canned_report = build_canned_report_data(&entries);
let paths = vec![AttributePath {
endpoint: 0,
cluster: crate::clusters::network_commissioning::CLUSTER_ID,
attribute: crate::clusters::network_commissioning::attribute_id::FEATURE_MAP,
}];
let controller = dispatch_read(&ctrl_io, &mut ctrl, session, dev_addr, &paths);
let device = async {
loop {
let (pkt, _) = dev_io.recv_from().await.unwrap();
if let DecodeInboundOutput::AppMessage { exchange_id, .. } =
dev.decode_inbound(&pkt, Instant::now()).unwrap()
{
let out = dev
.encode_outbound(
session,
Some(exchange_id),
0x05,
ProtocolId::INTERACTION_MODEL,
&canned_report,
MrpFlags { reliable: true },
Instant::now(),
)
.unwrap();
dev_io.send_to(&out.wire_bytes, ctrl_addr).await.unwrap();
break;
}
}
};
let (report_result, ()) = tokio::join!(controller, device);
let report = report_result.unwrap();
let attrs: Vec<_> = report.attributes().collect();
assert_eq!(attrs.len(), 1);
assert_eq!(
attrs[0].0.cluster,
crate::clusters::network_commissioning::CLUSTER_ID
);
assert_eq!(*attrs[0].1, Value::Uint(feat_val));
let payload = extract_read_payload(Expectation::NetworkCommissioningInfo, &report).unwrap();
let features = crate::clusters::network_commissioning::decode_feature_map(&payload)
.expect("decode_feature_map should accept the extracted payload");
assert!(features
.contains(crate::clusters::network_commissioning::NetworkCommissioningFeature::WIFI),);
}
#[test]
fn disarm_on_exit_decision() {
assert!(
disarm_on_exit(&LoopExit::Failed(DriverError::Handshake(
"mid-flight failure"
))),
"a propagated failure must disarm the failsafe best-effort"
);
assert!(
disarm_on_exit(&LoopExit::Aborted {
disarm: true,
reason: "device rejected".to_string(),
}),
"an abort that requests disarm must disarm"
);
assert!(
!disarm_on_exit(&LoopExit::Aborted {
disarm: false,
reason: "failsafe never armed".to_string(),
}),
"an abort that says not to disarm must be honored"
);
}
#[test]
fn loop_exit_maps_to_driver_error() {
let aborted = LoopExit::Aborted {
disarm: true,
reason: "boom".to_string(),
};
match aborted.into_driver_error() {
DriverError::Aborted(r) => assert_eq!(r, "boom"),
other => panic!("expected Aborted, got {other:?}"),
}
match LoopExit::Failed(DriverError::Handshake("h")).into_driver_error() {
DriverError::Handshake("h") => {}
other => panic!("expected Handshake, got {other:?}"),
}
}
#[tokio::test]
async fn with_response_deadline_mrp_is_transparent() {
let out = with_response_deadline(
TransportReliability::Mrp,
None,
std::future::ready(Ok::<u8, DriverError>(7)),
)
.await
.unwrap();
assert_eq!(out, 7);
}
#[tokio::test]
async fn with_response_deadline_transport_provides_passes_ready_value() {
let out = with_response_deadline(
TransportReliability::TransportProvides,
None,
std::future::ready(Ok::<u8, DriverError>(9)),
)
.await
.unwrap();
assert_eq!(out, 9);
}
#[tokio::test(start_paused = true)]
async fn with_response_deadline_transport_provides_times_out_at_30s() {
let start = tokio::time::Instant::now();
let err = with_response_deadline(
TransportReliability::TransportProvides,
None,
std::future::pending::<Result<(), DriverError>>(),
)
.await
.expect_err("a never-replying dispatch must hit the response deadline");
assert!(matches!(err, DriverError::Timeout { .. }), "got {err:?}");
assert_eq!(
start.elapsed(),
RESPONSE_DEADLINE,
"non-ConnectNetwork deadline must be 30 s"
);
}
#[tokio::test(start_paused = true)]
async fn with_response_deadline_connect_network_absent_falls_back_to_60s() {
let start = tokio::time::Instant::now();
let err = with_response_deadline(
TransportReliability::TransportProvides,
Some(0),
std::future::pending::<Result<(), DriverError>>(),
)
.await
.expect_err("a never-replying ConnectNetwork must hit the fallback deadline");
assert!(matches!(err, DriverError::Timeout { .. }), "got {err:?}");
assert_eq!(
start.elapsed(),
CONNECT_NETWORK_RESPONSE_DEADLINE,
"ConnectNetwork deadline with no reported ConnectMaxTimeSeconds must be 60 s"
);
}
#[tokio::test(start_paused = true)]
async fn with_response_deadline_connect_network_floors_small_value_at_90s() {
let start = tokio::time::Instant::now();
let err = with_response_deadline(
TransportReliability::TransportProvides,
Some(30),
std::future::pending::<Result<(), DriverError>>(),
)
.await
.expect_err("a never-replying ConnectNetwork must hit the floored deadline");
assert!(matches!(err, DriverError::Timeout { .. }), "got {err:?}");
assert_eq!(
start.elapsed(),
Duration::from_secs(90),
"ConnectNetwork deadline must floor a small ConnectMaxTimeSeconds at 90 s"
);
}
#[tokio::test(start_paused = true)]
async fn with_response_deadline_connect_network_honors_large_reported_value() {
let start = tokio::time::Instant::now();
let err = with_response_deadline(
TransportReliability::TransportProvides,
Some(120),
std::future::pending::<Result<(), DriverError>>(),
)
.await
.expect_err("a never-replying ConnectNetwork must hit the reported deadline");
assert!(matches!(err, DriverError::Timeout { .. }), "got {err:?}");
assert_eq!(
start.elapsed(),
Duration::from_secs(120),
"ConnectNetwork deadline must honor a ConnectMaxTimeSeconds above the floor"
);
}
#[test]
fn connect_network_deadline_covers_floor_cases() {
assert_eq!(
connect_network_deadline(0),
CONNECT_NETWORK_RESPONSE_DEADLINE,
"absent ConnectMaxTimeSeconds falls back to the fixed 60s default"
);
assert_eq!(
connect_network_deadline(30),
Duration::from_secs(90),
"a reported value below the floor is raised to 90s"
);
assert_eq!(
connect_network_deadline(120),
Duration::from_secs(120),
"a reported value above the floor is honored as-is"
);
}
#[tokio::test]
async fn rollback_sends_arm_fail_safe_zero_over_pase() {
use crate::im::CommandPath;
use matter_transport::{DecodeInboundOutput, MrpFlags, ProtocolId};
let (mut ctrl, mut dev) = paired_pase_sessions();
let pase_session = SessionId(1);
let (ctrl_io, dev_io) = InMemoryDatagram::pair();
let dev_addr = dev_io.local_addr();
let ctrl_addr = ctrl_io.local_addr();
let controller = rollback(&ctrl_io, &mut ctrl, pase_session, dev_addr);
let device = async {
let (pkt, _) = dev_io.recv_from().await.unwrap();
let msg = dev.decode_inbound(&pkt, Instant::now()).unwrap();
let (exchange_id, payload) = match msg {
DecodeInboundOutput::AppMessage {
exchange_id,
payload,
..
} => (exchange_id, payload),
other => panic!("expected AppMessage, got {other:?}"),
};
let invoke = parse_invoke_request(&payload).unwrap();
assert_eq!(
invoke.path,
CommandPath {
endpoint: 0,
cluster: crate::clusters::general_commissioning::CLUSTER_ID,
command: crate::clusters::general_commissioning::command_id::ARM_FAIL_SAFE,
}
);
let arm = decode_arm_fail_safe_fields(&invoke.fields_tlv);
assert_eq!(arm.expiry_length_seconds, 0, "expiry must be 0 for disarm");
assert_eq!(arm.breadcrumb, 0);
let ok_fields = encode_arm_fail_safe_response(0);
let resp = build_canned_invoke_response(invoke.path, &ok_fields);
let out = dev
.encode_outbound(
pase_session,
Some(exchange_id),
0x09,
ProtocolId::INTERACTION_MODEL,
&resp,
MrpFlags { reliable: true },
Instant::now(),
)
.unwrap();
dev_io.send_to(&out.wire_bytes, ctrl_addr).await.unwrap();
};
tokio::join!(controller, device);
}
#[tokio::test]
async fn establish_case_session_returns_session_id() {
use std::collections::HashMap;
use matter_cert::test_support::{build_unsigned, with_signature, TestCertFields};
use matter_cert::{
BasicConstraints, DistinguishedName, DnAttribute, Extensions, KeyIdentifier, KeyUsage,
MatterCertificate, MatterTime, PublicKey, Signature, TrustAnchor, TrustedRoots,
};
use matter_crypto::{
CaseCredentials, CaseResponder, CaseSigner, RingSigner, Sigma1Outcome,
};
use matter_transport::{
MatterService, QueryHandle, ServiceKind, SessionKeys, SessionManager,
};
use crate::driver::case::operational_instance_name;
use crate::driver::datagram::InMemoryDatagram;
use crate::driver::unsecured::{decode_unsecured, encode_unsecured};
const T_FABRIC_ID: u64 = 0x4242_4242_4242_4242;
const T_INITIATOR_NODE: u64 = 0xDEAD_BEEF_CAFE_F00D;
const T_RESPONDER_NODE: u64 = 0xBABE_FEED_1234_5678;
const T_IPK: [u8; 16] = [0x77; 16];
const T_RCAC_SKI: [u8; 20] = [0x01; 20];
const T_NOC_SKI: [u8; 20] = [0x02; 20];
fn build_test_rcac() -> (MatterCertificate, RingSigner, [u8; 65]) {
let (rcac_signer, _) = RingSigner::generate().unwrap();
let rcac_pub = *rcac_signer.public_key().as_bytes();
let rcac_dn = DistinguishedName::new(vec![DnAttribute::RcacId(1)]);
let ext = Extensions::builder()
.basic_constraints(Some(BasicConstraints::new(true, Some(1))))
.key_usage(Some(KeyUsage::KEY_CERT_SIGN))
.subject_key_identifier(Some(KeyIdentifier(T_RCAC_SKI)))
.authority_key_identifier(Some(KeyIdentifier(T_RCAC_SKI)))
.build();
let fields = TestCertFields {
serial: vec![0x01],
issuer: rcac_dn.clone(),
not_before: MatterTime::from_unix_secs(1_700_000_000),
not_after: MatterTime::from_unix_secs(2_500_000_000),
subject: rcac_dn,
public_key: PublicKey::new(rcac_pub).unwrap(),
extensions: ext,
signature: Signature::new([0u8; 64]),
};
let unsigned = build_unsigned(fields);
let tbs = unsigned.to_x509_tbs_der().unwrap();
let sig = rcac_signer.sign_p256_sha256(&tbs).unwrap();
(
with_signature(&unsigned, Signature::new(sig)),
rcac_signer,
rcac_pub,
)
}
fn build_test_noc(
rcac_signer: &RingSigner,
node_id: u64,
) -> (MatterCertificate, RingSigner) {
let (noc_signer, _) = RingSigner::generate().unwrap();
let noc_pub = *noc_signer.public_key().as_bytes();
let subj = DistinguishedName::new(vec![
DnAttribute::FabricId(T_FABRIC_ID),
DnAttribute::NodeId(node_id),
]);
let issuer = DistinguishedName::new(vec![DnAttribute::RcacId(1)]);
let ext = Extensions::builder()
.basic_constraints(Some(BasicConstraints::new(false, None)))
.key_usage(Some(KeyUsage::DIGITAL_SIGNATURE))
.subject_key_identifier(Some(KeyIdentifier(T_NOC_SKI)))
.authority_key_identifier(Some(KeyIdentifier(T_RCAC_SKI)))
.build();
let fields = TestCertFields {
serial: vec![0x02],
issuer,
not_before: MatterTime::from_unix_secs(1_700_000_000),
not_after: MatterTime::from_unix_secs(2_500_000_000),
subject: subj,
public_key: PublicKey::new(noc_pub).unwrap(),
extensions: ext,
signature: Signature::new([0u8; 64]),
};
let unsigned = build_unsigned(fields);
let tbs = unsigned.to_x509_tbs_der().unwrap();
let sig = rcac_signer.sign_p256_sha256(&tbs).unwrap();
(with_signature(&unsigned, Signature::new(sig)), noc_signer)
}
fn make_creds(
noc: MatterCertificate,
signer: RingSigner,
node_id: u64,
rcac_pub: [u8; 65],
) -> CaseCredentials {
CaseCredentials {
noc,
icac: None,
signer: Box::new(signer),
fabric_id: T_FABRIC_ID,
node_id,
ipk: T_IPK,
rcac_public_key: rcac_pub,
}
}
let (rcac, rcac_signer, rcac_pub) = build_test_rcac();
let (init_noc, init_signer) = build_test_noc(&rcac_signer, T_INITIATOR_NODE);
let (resp_noc, resp_signer) = build_test_noc(&rcac_signer, T_RESPONDER_NODE);
let init_creds = make_creds(init_noc, init_signer, T_INITIATOR_NODE, rcac_pub);
let resp_creds = make_creds(resp_noc, resp_signer, T_RESPONDER_NODE, rcac_pub);
let ctrl_roots = {
let mut r = TrustedRoots::new();
r.add(TrustAnchor::from_root_cert(&rcac));
r
};
let resp_roots = {
let mut r = TrustedRoots::new();
r.add(TrustAnchor::from_root_cert(&rcac));
r
};
let (ctrl_io, dev_io) = InMemoryDatagram::pair();
let dev_addr = dev_io.local_addr();
let ctrl_addr = ctrl_io.local_addr();
let mut sessions = SessionManager::new();
let compressed =
matter_crypto::derive_compressed_fabric_id(&rcac_pub, T_FABRIC_ID).unwrap();
let instance = operational_instance_name(compressed, T_RESPONDER_NODE);
struct FakeOpDiscovery {
service: MatterService,
}
impl matter_transport::Discovery for FakeOpDiscovery {
fn publish(&mut self, _s: &MatterService) -> matter_transport::Result<()> {
Ok(())
}
fn unpublish(&mut self, _n: &str, _k: ServiceKind) -> matter_transport::Result<()> {
Ok(())
}
fn query(&mut self, _k: ServiceKind) -> matter_transport::Result<QueryHandle> {
Ok(QueryHandle(1))
}
fn stop_query(&mut self, _h: QueryHandle) {}
fn poll_results(&mut self, _h: QueryHandle) -> Vec<MatterService> {
vec![self.service.clone()]
}
}
let mut discovery = FakeOpDiscovery {
service: MatterService::new(
instance,
ServiceKind::Operational,
vec![dev_addr.ip()],
dev_addr.port(),
HashMap::new(),
),
};
const OP_SIGMA2: u8 = 0x31;
let device = async {
let mut responder = CaseResponder::new(
resp_creds,
resp_roots,
0x00D2,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let (p, _) = dev_io.recv_from().await.unwrap();
let m = decode_unsecured(&p).unwrap();
assert!(matches!(
responder.handle_sigma1(&m.payload).unwrap(),
Sigma1Outcome::NewSession
));
let sigma2 = responder.next_message().unwrap();
let wire = encode_unsecured(
200,
m.exchange_id,
OP_SIGMA2,
matter_transport::ProtocolId::SECURE_CHANNEL,
false,
true,
Some(m.message_counter),
None,
&sigma2,
);
dev_io.send_to(&wire, ctrl_addr).await.unwrap();
let (p, _) = dev_io.recv_from().await.unwrap();
let m = decode_unsecured(&p).unwrap();
responder.handle_sigma3(&m.payload).unwrap();
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&0u16.to_le_bytes());
let report = encode_unsecured(
201,
m.exchange_id,
0x40,
matter_transport::ProtocolId::SECURE_CHANNEL,
false,
true,
Some(m.message_counter),
None,
&body,
);
dev_io.send_to(&report, ctrl_addr).await.unwrap();
let ack = tokio::time::timeout(std::time::Duration::from_secs(2), dev_io.recv_from())
.await
.expect("controller must ack the StatusReport")
.unwrap();
let ack = decode_unsecured(&ack.0).unwrap();
assert_eq!(ack.opcode, 0x10);
responder.finish().unwrap()
};
let controller = establish_case_session(
&ctrl_io,
&mut sessions,
&mut discovery,
&rcac_pub,
T_FABRIC_ID,
init_creds,
ctrl_roots,
T_RESPONDER_NODE,
MatterTime::from_unix_secs(2_000_000_000),
crate::driver::case::RESOLVE_POLL_ATTEMPTS,
);
let (ctrl_result, dev_out) = tokio::join!(controller, device);
let (sid, resolved_addr) = ctrl_result.unwrap();
assert_eq!(
resolved_addr, dev_addr,
"establish_case_session must return the mDNS-resolved operational address"
);
let registered = sessions.get(sid).unwrap();
assert_eq!(registered.keys, SessionKeys::from_case_output(&dev_out));
}
#[tokio::test]
async fn flush_pending_acks_delivers_buffered_standalone_ack() {
use std::time::Instant;
let (mut ctrl, mut dev) = paired_pase_sessions();
let session = SessionId(1);
let (ctrl_io, dev_io) = InMemoryDatagram::pair();
let dev_addr = dev_io.local_addr();
let ctrl_addr = ctrl_io.local_addr();
let controller = async {
let resp = secured_round_trip(
&ctrl_io,
&mut ctrl,
session,
dev_addr,
0x08,
ProtocolId::INTERACTION_MODEL,
b"req",
)
.await
.unwrap();
assert_eq!(resp.payload, b"resp");
flush_pending_acks(&ctrl_io, &mut ctrl, dev_addr)
.await
.unwrap();
};
let device = async {
let (pkt, _) = dev_io.recv_from().await.unwrap();
let DecodeInboundOutput::AppMessage { exchange_id, .. } =
dev.decode_inbound(&pkt, Instant::now()).unwrap()
else {
panic!("expected request");
};
let out = dev
.encode_outbound(
session,
Some(exchange_id),
0x09,
ProtocolId::INTERACTION_MODEL,
b"resp",
MrpFlags { reliable: true },
Instant::now(),
)
.unwrap();
dev_io.send_to(&out.wire_bytes, ctrl_addr).await.unwrap();
let (ack_pkt, _) =
tokio::time::timeout(std::time::Duration::from_secs(2), dev_io.recv_from())
.await
.expect("flush must deliver the standalone ack")
.unwrap();
assert!(
matches!(
dev.decode_inbound(&ack_pkt, Instant::now()).unwrap(),
DecodeInboundOutput::AckOnly { .. }
),
"flushed packet must be a standalone ack"
);
};
let ((), ()) = tokio::join!(controller, device);
}
#[tokio::test]
async fn dispatch_read_and_extract_commissioning_info() {
use crate::im::AttributePath;
use crate::Expectation;
use matter_codec::{Tag, Value};
let (mut ctrl, mut dev) = paired_pase_sessions();
let session = SessionId(1);
let (ctrl_io, dev_io) = InMemoryDatagram::pair();
let dev_addr = dev_io.local_addr();
let ctrl_addr = ctrl_io.local_addr();
let struct_value = Value::Structure(vec![
(Tag::Context(0), Value::Uint(120)),
(Tag::Context(1), Value::Uint(900)),
]);
let entries = vec![(
AttributePath {
endpoint: 0,
cluster: crate::clusters::general_commissioning::CLUSTER_ID,
attribute: attr_id::BASIC_COMMISSIONING_INFO,
},
struct_value,
)];
let canned_report = build_canned_report_data(&entries);
let paths = vec![AttributePath {
endpoint: 0,
cluster: crate::clusters::general_commissioning::CLUSTER_ID,
attribute: attr_id::BASIC_COMMISSIONING_INFO,
}];
let controller = dispatch_read(&ctrl_io, &mut ctrl, session, dev_addr, &paths);
let device = async {
loop {
let (pkt, _) = dev_io.recv_from().await.unwrap();
if let DecodeInboundOutput::AppMessage { exchange_id, .. } =
dev.decode_inbound(&pkt, Instant::now()).unwrap()
{
let out = dev
.encode_outbound(
session,
Some(exchange_id),
0x05,
ProtocolId::INTERACTION_MODEL,
&canned_report,
MrpFlags { reliable: true },
Instant::now(),
)
.unwrap();
dev_io.send_to(&out.wire_bytes, ctrl_addr).await.unwrap();
break;
}
}
};
let (report_result, ()) = tokio::join!(controller, device);
let report = report_result.unwrap();
assert_eq!(report.attributes().count(), 1);
let payload = extract_read_payload(Expectation::CommissioningInfo, &report).unwrap();
let info =
crate::clusters::general_commissioning::decode_basic_commissioning_info(&payload)
.expect("decode_basic_commissioning_info should accept the extracted payload");
assert_eq!(info.failsafe_expiry_length_seconds, 120);
assert_eq!(info.max_cumulative_failsafe_seconds, 900);
}
}