Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions crates/ziggurat-driver/src/zigbee_stack/zdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ use ziggurat_zigbee::nwk::frame::{
};

use ziggurat_zigbee::zdp::{
DeviceAnnce, MgmtLqiReq, MgmtLqiRsp, MgmtRtgReq, MgmtRtgRsp, NeighborDescriptor, ParentAnnce,
ParentAnnceRsp, RoutingDescriptor, ZDP_PROFILE_ID, ZdpAffinity, ZdpClusterId, ZdpCommand,
DeviceAnnce, MgmtLqiReq, MgmtLqiRsp, MgmtRtgReq, MgmtRtgRsp, NeighborDescriptor, NodeDescReq,
NodeDescRsp, NodeDescriptor, ParentAnnce, ParentAnnceRsp, RoutingDescriptor,
STACK_COMPLIANCE_REVISION, ZDP_PROFILE_ID, ZdpAffinity, ZdpClusterId, ZdpCommand,
ZdpDeviceType, ZdpPermitJoining, ZdpRouteStatus, ZdpRxOnWhenIdle, ZdpStatus,
};

Expand All @@ -25,6 +26,10 @@ use crate::frame_token::TrafficClass;
/// EUI64s per Parent_annce frame, keeping the ASDU within the NWK payload budget.
const PARENT_ANNCE_CHILDREN_PER_FRAME: usize = 8;

fn should_process_parent_annce(source: Nwk, local: Nwk) -> bool {
source != local
}

/// Neighbor records per Mgmt_Lqi_rsp; the spec caps the count field at 2
/// (Table 2-101) and clients paginate with the start index.
const MGMT_LQI_DESCRIPTORS_PER_FRAME: usize = 2;
Expand All @@ -41,13 +46,49 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
}

match ZdpClusterId::try_from(aps_frame.cluster_id) {
Ok(ZdpClusterId::NodeDescReq) => self.handle_node_desc_req(nwk_frame, aps_frame),
Ok(ZdpClusterId::DeviceAnnce) => self.handle_device_annce(nwk_frame, aps_frame),
Ok(ZdpClusterId::ParentAnnce) => self.handle_parent_annce(nwk_frame, aps_frame),
Ok(ZdpClusterId::ParentAnnceRsp) => self.handle_parent_annce_rsp(nwk_frame, aps_frame),
Ok(ZdpClusterId::MgmtLqiReq) => self.handle_mgmt_lqi_req(nwk_frame, aps_frame),
Ok(ZdpClusterId::MgmtRtgReq) => self.handle_mgmt_rtg_req(nwk_frame, aps_frame),
// Management responses from other devices are the client's business
Ok(ZdpClusterId::MgmtLqiRsp | ZdpClusterId::MgmtRtgRsp) | Err(_) => {}
Ok(ZdpClusterId::NodeDescRsp | ZdpClusterId::MgmtLqiRsp | ZdpClusterId::MgmtRtgRsp)
| Err(_) => {}
}
}

/// Zigbee spec 2.4.4.1.2: answer a Node_Desc_req addressed to this node.
/// Zigbee 3.0 joiners use the advertised stack revision to decide whether
/// the Trust Center supports the Request-Key update procedure.
fn handle_node_desc_req(&self, nwk_frame: &NwkFrame, aps_frame: &ApsDataFrame) {
if nwk_frame.nwk_header.destination != self.state.network_address {
return;
}

let source = nwk_frame.nwk_header.source;
let (tsn, request) = match NodeDescReq::deserialize(&aps_frame.asdu) {
Ok(parsed) => parsed,
Err(err) => {
tracing::warn!("Malformed node descriptor request from {source:?}: {err}");
return;
}
};
if request.nwk_addr_of_interest != self.state.network_address {
return;
}

let response = NodeDescRsp {
status: ZdpStatus::Success,
nwk_addr_of_interest: self.state.network_address,
node_descriptor: NodeDescriptor::coordinator(),
};

tracing::info!(
"Answering node descriptor request from {source:?} with stack compliance revision {STACK_COMPLIANCE_REVISION}"
);
if let Err(err) = self.send_zdp_command(source, ApsDeliveryMode::Unicast, tsn, &response) {
tracing::warn!("Failed to send a node descriptor response to {source:?}: {err}");
}
}

Expand Down Expand Up @@ -225,6 +266,13 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
fn handle_parent_annce(&self, nwk_frame: &NwkFrame, aps_frame: &ApsDataFrame) {
let source = nwk_frame.nwk_header.source;

if !should_process_parent_annce(source, self.state.network_address) {
tracing::debug!(
"Ignoring looped-back parent announcement from our own network address"
);
return;
}

let (tsn, annce) = match ParentAnnce::deserialize(&aps_frame.asdu) {
Ok(parsed) => parsed,
Err(err) => {
Expand Down Expand Up @@ -441,3 +489,15 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
}
}
}

#[cfg(test)]
mod tests {
use super::should_process_parent_annce;
use ziggurat_ieee_802154::types::Nwk;

#[test]
fn coordinator_ignores_its_own_looped_back_parent_announcement() {
assert!(!should_process_parent_annce(Nwk(0x0000), Nwk(0x0000)));
assert!(should_process_parent_annce(Nwk(0x1234), Nwk(0x0000)));
}
}
54 changes: 54 additions & 0 deletions crates/ziggurat-zigbee/src/aps/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ impl ApsSecurity {
self.device_key(eui64).is_some() || self.tclk_seed.is_some()
}

/// Whether a fresh unique trust center link key has been issued but not yet
/// proven by the device with a Verify-Key command.
pub fn link_key_update_pending(&self, eui64: Eui64) -> bool {
self.devices
.get(&eui64)
.is_some_and(|entry| entry.pending_key.is_some())
}

pub fn device_key_count(&self) -> usize {
self.devices.values().filter(|e| e.key.is_some()).count()
}
Expand All @@ -238,6 +246,14 @@ impl ApsSecurity {
/// `fresh_key` is caller-generated randomness, used only when no TCLK seed is
/// configured.
pub fn issue_device_key(&mut self, eui64: Eui64, fresh_key: Key) -> Key {
if let Some(pending_key) = self
.devices
.get(&eui64)
.and_then(|entry| entry.pending_key.clone())
{
return pending_key;
}

let key = self
.tclk_seed
.as_ref()
Expand Down Expand Up @@ -519,3 +535,41 @@ impl ApsSecurity {
.map(|(frame, _key)| frame)
}
}

#[cfg(test)]
mod tests {
use super::*;

const LOCAL: Eui64 = Eui64([0x10; 8]);
const DEVICE: Eui64 = Eui64([0x20; 8]);

fn security() -> ApsSecurity {
ApsSecurity::new(Key(*b"ZigBeeAlliance09"), LOCAL, None)
}

#[test]
fn repeated_link_key_issue_reuses_the_pending_key() {
let mut security = security();
let first = security.issue_device_key(DEVICE, Key([0x31; 16]));
let retry = security.issue_device_key(DEVICE, Key([0x42; 16]));

assert_eq!(first, Key([0x31; 16]));
assert_eq!(retry, first);
assert!(security.link_key_update_pending(DEVICE));
assert!(!security.has_unique_link_key(DEVICE));
}

#[test]
fn verified_pending_key_becomes_the_active_unique_key() {
let mut security = security();
let pending = security.issue_device_key(DEVICE, Key([0x53; 16]));

assert_eq!(
security.verify_device_key(DEVICE, &verify_key_hash(&pending)),
Some(true)
);
assert!(!security.link_key_update_pending(DEVICE));
assert!(security.has_unique_link_key(DEVICE));
assert_eq!(security.device_link_key(DEVICE), pending);
}
}
99 changes: 99 additions & 0 deletions crates/ziggurat-zigbee/src/zdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,81 @@ pub const ZDP_PROFILE_ID: u16 = 0x0000;
#[derive(Debug, Eq, PartialEq, TryFromPrimitive, Clone, Copy)]
#[repr(u16)]
pub enum ZdpClusterId {
NodeDescReq = 0x0002,
DeviceAnnce = 0x0013,
ParentAnnce = 0x001F,
MgmtLqiReq = 0x0031,
MgmtRtgReq = 0x0032,
NodeDescRsp = 0x8002,
ParentAnnceRsp = 0x801F,
MgmtLqiRsp = 0x8031,
MgmtRtgRsp = 0x8032,
}

/// Zigbee spec 2.4.3.1.2: request the node descriptor for a network address.
#[abstract_bits]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct NodeDescReq {
pub nwk_addr_of_interest: Nwk,
}

impl ZdpCommand for NodeDescReq {
const CLUSTER_ID: ZdpClusterId = ZdpClusterId::NodeDescReq;
}

/// Stack-compliance revision advertised by this implementation.
pub const STACK_COMPLIANCE_REVISION: u8 = 22;

/// Zigbee spec 2.3.2.3: the coordinator's node descriptor.
#[abstract_bits]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct NodeDescriptor {
logical_type_and_flags: u8,
aps_flags_and_frequency_band: u8,
mac_capability_flags: u8,
manufacturer_code: u16,
maximum_buffer_size: u8,
maximum_incoming_transfer_size: u16,
server_mask: u16,
maximum_outgoing_transfer_size: u16,
descriptor_capability_field: u8,
}

impl NodeDescriptor {
/// Describe a 2.4 GHz coordinator that owns the primary Trust Center.
pub const fn coordinator() -> Self {
Self {
// Logical type Coordinator; no complex or user descriptor.
logical_type_and_flags: 0x00,
// APS flags 0; 2.4 GHz frequency band.
aps_flags_and_frequency_band: 0x40,
// Alternate PAN coordinator, FFD, mains powered, receiver on while idle,
// security capable, and able to allocate addresses.
mac_capability_flags: 0x8f,
manufacturer_code: 0x0000,
maximum_buffer_size: 82,
maximum_incoming_transfer_size: 82,
// Primary Trust Center (bit 0); stack compliance revision in bits 9..15.
server_mask: 0x0001 | ((STACK_COMPLIANCE_REVISION as u16) << 9),
maximum_outgoing_transfer_size: 82,
descriptor_capability_field: 0x00,
}
}
}

/// Zigbee spec 2.4.4.1.2: successful node descriptor response.
#[abstract_bits]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct NodeDescRsp {
pub status: ZdpStatus,
pub nwk_addr_of_interest: Nwk,
pub node_descriptor: NodeDescriptor,
}

impl ZdpCommand for NodeDescRsp {
const CLUSTER_ID: ZdpClusterId = ZdpClusterId::NodeDescRsp;
}

/// Zigbee spec Table 2-129 (partial): ZDP response status values.
#[abstract_bits(bits = 8)]
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
Expand Down Expand Up @@ -275,3 +341,36 @@ fn deserialize<T: AbstractBits>(bytes: &[u8]) -> Result<(u8, T), DeserializeErro

Ok((*tsn, command))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn node_descriptor_request_round_trips() {
let request = NodeDescReq {
nwk_addr_of_interest: Nwk(0x0000),
};

let encoded = request.serialize(0x27).unwrap();
assert_eq!(encoded, [0x27, 0x00, 0x00]);
assert_eq!(NodeDescReq::deserialize(&encoded).unwrap(), (0x27, request));
}

#[test]
fn coordinator_node_descriptor_advertises_r22_trust_center() {
let response = NodeDescRsp {
status: ZdpStatus::Success,
nwk_addr_of_interest: Nwk(0x0000),
node_descriptor: NodeDescriptor::coordinator(),
};

assert_eq!(
response.serialize(0x27).unwrap(),
[
0x27, 0x00, 0x00, 0x00, 0x00, 0x40, 0x8f, 0x00, 0x00, 0x52, 0x52, 0x00, 0x01, 0x2c,
0x52, 0x00, 0x00,
]
);
}
}
Loading