Skip to content

Commit 18892f6

Browse files
committed
fix: network commissioning
1 parent 5d7dfdc commit 18892f6

File tree

16 files changed

+387
-141
lines changed

16 files changed

+387
-141
lines changed

src/drivers/ot-rcp-driver.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ export class OTRCPDriver {
133133
onAPSSendTransportKeyNWK: async (address16, key, keySeqNum, destination64) => {
134134
await this.apsHandler.sendTransportKeyNWK(address16, key, keySeqNum, destination64);
135135
},
136+
onAPSSendStartKeyUpdateRequest: async (nwkDest16, nwkDest64, keyNegotiationProtocol, preSharedSecret) => {
137+
await this.apsHandler.sendStartKeyUpdateRequest(nwkDest16, nwkDest64, keyNegotiationProtocol, preSharedSecret);
138+
},
136139
};
137140

138141
this.nwkHandler = new NWKHandler(this.context, this.macHandler, nwkCallbacks);

src/zigbee-stack/aps-handler.ts

Lines changed: 198 additions & 89 deletions
Large diffs are not rendered by default.

src/zigbee-stack/nwk-handler.ts

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ import {
99
type MACHeader,
1010
ZigbeeMACConsts,
1111
} from "../zigbee/mac.js";
12-
import { GlobalTlv, GlobalTlvConsts, readZigbeeTlvs } from "../zigbee/tlvs.js";
12+
import {
13+
GlobalTlv,
14+
KeyNegotationProtocol,
15+
KeyNegotationProtocolMask,
16+
type PreSharedSecret,
17+
PreSharedSecretMask,
18+
readZigbeeTlvs,
19+
} from "../zigbee/tlvs.js";
1320
import { ZigbeeConsts, ZigbeeKeyType, type ZigbeeSecurityHeader, ZigbeeSecurityLevel } from "../zigbee/zigbee.js";
1421
import {
1522
encodeZigbeeNWKFrame,
@@ -34,6 +41,13 @@ const NS = "nwk-handler";
3441
export interface NWKHandlerCallbacks {
3542
/** Send APS TRANSPORT_KEY for network key */
3643
onAPSSendTransportKeyNWK: (destination16: number, networkKey: Buffer, keySequenceNumber: number, destination64: bigint) => Promise<void>;
44+
/** Send APS ZDO START_KEY_UPDATE_REQUEST */
45+
onAPSSendStartKeyUpdateRequest: (
46+
nwkDest16: number,
47+
nwkDest64: bigint,
48+
keyNegotiationProtocol: KeyNegotationProtocol,
49+
preSharedSecret: PreSharedSecret,
50+
) => Promise<void>;
3751
}
3852

3953
/** The number of OctetDurations until a route discovery expires. */
@@ -1300,15 +1314,15 @@ export class NWKHandler {
13001314
status = MACAssociationStatus.PAN_FULL;
13011315
} else {
13021316
status = ZigbeeNWKConsts.ASSOC_STATUS_ADDR_CONFLICT;
1303-
requiresTransportKey = !secured;
1317+
requiresTransportKey = !secured; // only if Trust Center Rejoin
13041318
const neighbor = macHeader.source16 === nwkHeader.source16;
13051319

13061320
await this.#context.associate(newAddress16, nwkHeader.source64, decodedCap, neighbor, false);
13071321
}
13081322
} else {
13091323
newAddress16 = nwkHeader.source16;
13101324
status = MACAssociationStatus.SUCCESS;
1311-
requiresTransportKey = !secured;
1325+
requiresTransportKey = !secured; // only if Trust Center Rejoin
13121326
const neighbor = macHeader.source16 === nwkHeader.source16;
13131327

13141328
await this.#context.associate(newAddress16, nwkHeader.source64, decodedCap, neighbor, false);
@@ -1318,7 +1332,7 @@ export class NWKHandler {
13181332
await this.sendRejoinResp(nwkHeader.source16, newAddress16, status);
13191333

13201334
if (requiresTransportKey) {
1321-
// XXX: is this spec?
1335+
// XXX: use tunnel even if direct Coordinator<>rejoiner?
13221336
await this.#callbacks.onAPSSendTransportKeyNWK(
13231337
newAddress16,
13241338
this.#context.netParams.networkKey,
@@ -1878,8 +1892,9 @@ export class NWKHandler {
18781892
const commissioningType = data.readUInt8(offset);
18791893
offset += 1;
18801894
const secured = nwkHeader.frameControl.security;
1895+
const initialJoin = commissioningType === ZigbeeNWKCommissioningType.INITIAL_JOIN;
18811896

1882-
if (secured && commissioningType === ZigbeeNWKCommissioningType.INITIAL_JOIN) {
1897+
if (secured && initialJoin) {
18831898
return; // per spec, drop
18841899
}
18851900

@@ -1888,30 +1903,39 @@ export class NWKHandler {
18881903
const decodedCap = decodeMACCapabilities(capabilities);
18891904
const [globalTlvs] = readZigbeeTlvs(data, offset);
18901905
const joinerTlv = globalTlvs[GlobalTlv.JOINER_ENCAPSULATION];
1891-
let selectedKeyNegotiationMethod = GlobalTlvConsts.KEY_NEGOTATION_METHOD_STATIC; // fallback
18921906

1893-
if (joinerTlv !== undefined) {
1894-
// device is R23
1895-
const fragmentationParametersTlv = joinerTlv.additionalTlvs[GlobalTlv.FRAGMENTATION_PARAMETERS];
1896-
const supportedKeyNegotiationMethodsTlv = joinerTlv.additionalTlvs[GlobalTlv.SUPPORTED_KEY_NEGOTIATION_METHODS];
1907+
if (joinerTlv === undefined) {
1908+
return; // invalid
1909+
}
1910+
1911+
const supportedKeyNegotiationMethodsTlv = joinerTlv.additionalTlvs[GlobalTlv.SUPPORTED_KEY_NEGOTIATION_METHODS];
1912+
const rejoin = commissioningType === ZigbeeNWKCommissioningType.REJOIN;
18971913

1898-
if (fragmentationParametersTlv !== undefined) {
1899-
// TODO
1914+
if (supportedKeyNegotiationMethodsTlv === undefined) {
1915+
if (!rejoin) {
1916+
return; // invalid
1917+
}
1918+
} else {
1919+
// TODO: support other protocols
1920+
if (!(supportedKeyNegotiationMethodsTlv.keyNegotiationProtocolsBitmask & KeyNegotationProtocolMask.Z3)) {
1921+
return; // per spec, must always be supported
19001922
}
19011923

1902-
if (supportedKeyNegotiationMethodsTlv !== undefined) {
1903-
// TODO
1904-
const bitmask = supportedKeyNegotiationMethodsTlv.keyNegotiationProtocolsBitmask;
1924+
if (!(supportedKeyNegotiationMethodsTlv.preSharedSecretsBitmask & PreSharedSecretMask.INSTALL_CODE_KEY)) {
1925+
// XXX: spec?
1926+
await this.sendCommissioningResponse(nwkHeader.source16, 0xffff, MACAssociationStatus.PAN_ACCESS_DENIED);
19051927

1906-
// TODO: by order of "most security"?
1907-
if (bitmask & GlobalTlvConsts.KEY_NEGOTATION_METHOD_SHA256) {
1908-
selectedKeyNegotiationMethod = GlobalTlvConsts.KEY_NEGOTATION_METHOD_SHA256;
1909-
} else if (bitmask & GlobalTlvConsts.KEY_NEGOTATION_METHOD_MMO128) {
1910-
selectedKeyNegotiationMethod = GlobalTlvConsts.KEY_NEGOTATION_METHOD_MMO128;
1911-
}
1928+
return; // others currently not supported
19121929
}
19131930
}
19141931

1932+
// TODO: store in state?
1933+
const fragmentationParametersTlv = joinerTlv.additionalTlvs[GlobalTlv.FRAGMENTATION_PARAMETERS];
1934+
1935+
if (fragmentationParametersTlv === undefined) {
1936+
return; // invalid
1937+
}
1938+
19151939
logger.debug(
19161940
() =>
19171941
`<=== NWK COMMISSIONING_REQUEST[macSrc=${macHeader.source16}:${macHeader.source64} nwkSrc=${nwkHeader.source16}:${nwkHeader.source64} sec=${secured} type=${commissioningType} cap=${capabilities}]`,
@@ -1924,8 +1948,8 @@ export class NWKHandler {
19241948
let status: MACAssociationStatus | number = MACAssociationStatus.PAN_ACCESS_DENIED;
19251949
let requiresTransportKey = false;
19261950

1927-
if (commissioningType === ZigbeeNWKCommissioningType.INITIAL_JOIN) {
1928-
if (this.#context.trustCenterPolicies.allowJoins) {
1951+
if (initialJoin) {
1952+
if (this.#context.macAssociationPermit && this.#context.trustCenterPolicies.allowJoins) {
19291953
if (this.#context.address16ToAddress64.has(nwkHeader.source16)) {
19301954
// device address is conflicting, assign new one
19311955
newAddress16 = this.#context.assignNetworkAddress();
@@ -1948,7 +1972,7 @@ export class NWKHandler {
19481972
await this.#context.associate(newAddress16, nwkHeader.source64, decodedCap, neighbor, true);
19491973
}
19501974
}
1951-
} else if (commissioningType === ZigbeeNWKCommissioningType.REJOIN) {
1975+
} else if (rejoin) {
19521976
const device = this.#context.deviceTable.get(nwkHeader.source64);
19531977

19541978
if (device?.authorized) {
@@ -1961,15 +1985,15 @@ export class NWKHandler {
19611985
status = MACAssociationStatus.PAN_FULL;
19621986
} else {
19631987
status = ZigbeeNWKConsts.ASSOC_STATUS_ADDR_CONFLICT;
1964-
requiresTransportKey = !secured;
1988+
requiresTransportKey = !secured; // only if Trust Center Rejoin
19651989
const neighbor = macHeader.source16 === nwkHeader.source16;
19661990

19671991
await this.#context.associate(newAddress16, nwkHeader.source64, decodedCap, neighbor, false);
19681992
}
19691993
} else {
19701994
newAddress16 = nwkHeader.source16;
19711995
status = MACAssociationStatus.SUCCESS;
1972-
requiresTransportKey = !secured;
1996+
requiresTransportKey = !secured; // only if Trust Center Rejoin
19731997
const neighbor = macHeader.source16 === nwkHeader.source16;
19741998

19751999
await this.#context.associate(newAddress16, nwkHeader.source64, decodedCap, neighbor, false);
@@ -1980,21 +2004,29 @@ export class NWKHandler {
19802004
await this.sendCommissioningResponse(nwkHeader.source16, newAddress16, status);
19812005

19822006
if (requiresTransportKey) {
1983-
// TODO: might need to be different if R23 or not
1984-
if (selectedKeyNegotiationMethod === GlobalTlvConsts.KEY_NEGOTATION_METHOD_STATIC) {
1985-
const dest64 = this.#context.address16ToAddress64.get(newAddress16);
1986-
1987-
if (dest64 !== undefined && requiresTransportKey) {
1988-
await this.#callbacks.onAPSSendTransportKeyNWK(
1989-
newAddress16,
1990-
this.#context.netParams.networkKey,
1991-
this.#context.netParams.networkKeySequenceNumber,
1992-
dest64,
1993-
);
1994-
this.#context.markNetworkKeyTransported(dest64);
1995-
}
2007+
if (this.#context.trustCenterPolicies.keyNegotiationProtocol === KeyNegotationProtocol.Z3) {
2008+
await this.#callbacks.onAPSSendTransportKeyNWK(
2009+
newAddress16,
2010+
this.#context.netParams.networkKey,
2011+
this.#context.netParams.networkKeySequenceNumber,
2012+
nwkHeader.source64,
2013+
);
2014+
this.#context.markNetworkKeyTransported(nwkHeader.source64);
19962015
} else {
1997-
// TODO START_KEY_UPDATE ZDO
2016+
if (rejoin) {
2017+
// At this time this Revision of the specification does not support negotiating a new link key during rejoin.
2018+
// Therefore, devices certified to this Revision SHALL not include the Supported Key Negotiation Methods Global TLV
2019+
// inside the Joiner Encapsulation TLV so it is clear to the Trust Center that the device does not support this behavior.
2020+
// Future revisions of this specification that support this would include this TLV as a clear sign the rejoining device supports this new functionality.
2021+
return;
2022+
}
2023+
2024+
await this.#callbacks.onAPSSendStartKeyUpdateRequest(
2025+
newAddress16,
2026+
nwkHeader.source64,
2027+
this.#context.trustCenterPolicies.keyNegotiationProtocol,
2028+
this.#context.trustCenterPolicies.preSharedSecret,
2029+
);
19982030
}
19992031
}
20002032
}
@@ -2029,7 +2061,7 @@ export class NWKHandler {
20292061
false, // nwkSecurity
20302062
ZigbeeConsts.COORDINATOR_ADDRESS, // nwkSource16
20312063
requestSource16, // nwkDest16
2032-
this.#context.address16ToAddress64.get(requestSource16), // nwkDest64
2064+
this.#context.address16ToAddress64.get(newAddress16), // nwkDest64
20332065
CONFIG_NWK_MAX_HOPS, // nwkRadius
20342066
);
20352067
}

src/zigbee-stack/stack-context.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readFile, rm, writeFile } from "node:fs/promises";
22
import { logger } from "../utils/logger.js";
33
import { decodeMACCapabilities, encodeMACCapabilities, type MACCapabilities, type MACHeader } from "../zigbee/mac.js";
4+
import { KeyNegotationProtocol, PreSharedSecret } from "../zigbee/tlvs.js";
45
import {
56
aes128MmoHash,
67
computeInstallCodeCRC,
@@ -100,6 +101,10 @@ export type TrustCenterPolicies = {
100101
* A setting of FALSE means rejoins are only allowed with trust center link keys where the KeyAttributes of the apsDeviceKeyPairSet entry indicates VERIFIED_KEY.
101102
*/
102103
allowRejoinsWithWellKnownKey: boolean;
104+
/** R23+ */
105+
keyNegotiationProtocol: KeyNegotationProtocol;
106+
/** R23+ */
107+
preSharedSecret: PreSharedSecret;
103108
/** This value controls whether devices are allowed to request a Trust Center Link Key after they have joined the network. */
104109
allowTCKeyRequest: TrustCenterKeyRequestPolicy;
105110
/** This policy indicates whether a node on the network that transmits a ZDO Mgmt_Permit_Join with a significance set to 1 is allowed to effect the local Trust Center’s policies. */
@@ -389,6 +394,10 @@ export class StackContext {
389394
allowJoins: false,
390395
installCode: InstallCodePolicy.NOT_REQUIRED,
391396
allowRejoinsWithWellKnownKey: true,
397+
// TODO: support other protocols
398+
keyNegotiationProtocol: KeyNegotationProtocol.Z3,
399+
// TODO: support other secrets
400+
preSharedSecret: PreSharedSecret.INSTALL_CODE_KEY,
392401
allowTCKeyRequest: TrustCenterKeyRequestPolicy.ALLOWED,
393402
networkKeyUpdatePeriod: 0, // disable
394403
networkKeyUpdateMethod: NetworkKeyUpdateMethod.BROADCAST,

src/zigbee/tlvs.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import type { RequiredNonNullable } from "../utils/types.js";
22
import { ZigbeeConsts } from "./zigbee.js";
33

4-
export const enum GlobalTlvConsts {
5-
KEY_NEGOTATION_METHOD_STATIC = 0b000,
6-
KEY_NEGOTATION_METHOD_MMO128 = 0b010,
7-
KEY_NEGOTATION_METHOD_SHA256 = 0b100,
8-
}
9-
104
export const enum GlobalTlv {
115
/** minLen=2 */
126
MANUFACTURER_SPECIFIC = 64,
@@ -33,6 +27,41 @@ export const enum GlobalTlv {
3327
// Reserved = 77-255
3428
}
3529

30+
/** uint8 */
31+
export const enum KeyNegotationProtocol {
32+
Z3 = 0x0,
33+
MMO128 = 0x1,
34+
SHA256 = 0x2,
35+
}
36+
37+
/** uint8 */
38+
export const enum KeyNegotationProtocolMask {
39+
Z3 = 0b01,
40+
MMO128 = 0b10,
41+
SHA256 = 0b11,
42+
}
43+
44+
/** uint8 */
45+
export const enum PreSharedSecret {
46+
SYMMETRIC_AUTHENTICATION_TOKEN = 0x00,
47+
INSTALL_CODE_KEY = 0x01,
48+
PASSCODE_KEY = 0x02,
49+
BASIC_ACCESS_KEY = 0x03,
50+
ADMINISTRATIVE_ACCESS_KEY = 0x04,
51+
// 0x04-0xfe reserved
52+
ANONYMOUS_WELL_KNOWN_SECRET = 0xff,
53+
}
54+
55+
/** uint8 */
56+
export const enum PreSharedSecretMask {
57+
SYMMETRIC_AUTHENTICATION_TOKEN = 0b00001,
58+
INSTALL_CODE_KEY = 0b00010,
59+
PASSCODE_KEY = 0b00100,
60+
BASIC_ACCESS_KEY = 0b01000,
61+
ADMINISTRATIVE_ACCESS_KEY = 0b10000,
62+
// others reserved
63+
}
64+
3665
type GlobalTlvEncapsulated = {
3766
additionalTlvs: ZigbeeGlobalTlvs;
3867
additionalLocalTlvs: Map<number, Buffer>;
@@ -406,6 +435,7 @@ export function readZigbeeTlvs(data: Buffer, offset: number, parent?: number): [
406435
return [globalTlvs, localTlvs, offset];
407436
}
408437

438+
/** Byte length: 12 */
409439
export function writeZigbeeTlvSupportedKeyNegotiationMethods(
410440
data: Buffer,
411441
offset: number,
@@ -420,6 +450,7 @@ export function writeZigbeeTlvSupportedKeyNegotiationMethods(
420450
return offset;
421451
}
422452

453+
/** Byte length: 7 */
423454
export function writeZigbeeTlvFragmentationParameters(
424455
data: Buffer,
425456
offset: number,
@@ -434,6 +465,7 @@ export function writeZigbeeTlvFragmentationParameters(
434465
return offset;
435466
}
436467

468+
/** Byte length: 4 */
437469
export function writeZigbeeTlvRouterInformation(data: Buffer, offset: number, tlv: RequiredNonNullable<GlobalTlvRouterInformation>): number {
438470
offset = data.writeUInt8(GlobalTlv.ROUTER_INFORMATION, offset);
439471
offset = data.writeUInt8(1, offset); // per spec, actual data length is `length field + 1`

src/zigbee/zigbee.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const enum ZigbeeConsts {
2727
//---- ZDO
2828
ZDO_ENDPOINT = 0x00,
2929
ZDO_PROFILE_ID = 0x0000,
30+
ZDO_SUCCESS = 0x00,
3031
NETWORK_ADDRESS_REQUEST = 0x0000,
3132
IEEE_ADDRESS_REQUEST = 0x0001,
3233
NODE_DESCRIPTOR_REQUEST = 0x0002,
@@ -37,6 +38,8 @@ export const enum ZigbeeConsts {
3738
LQI_TABLE_REQUEST = 0x0031,
3839
ROUTING_TABLE_REQUEST = 0x0032,
3940
NWK_UPDATE_REQUEST = 0x0038,
41+
START_KEY_UPDATE_REQUEST = 0x0045,
42+
START_KEY_UPDATE_RESPONSE = 0x8045,
4043

4144
//---- Green Power
4245
GP_ENDPOINT = 0xf2,

test/compliance/aps.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ describe("Zigbee 4.0 Application Support (APS) Layer Compliance", () => {
107107
};
108108
mockNWKHandlerCallbacks = {
109109
onAPSSendTransportKeyNWK: vi.fn(),
110+
onAPSSendStartKeyUpdateRequest: vi.fn(),
110111
};
111112
mockNWKGPHandlerCallbacks = {
112113
onGPFrame: vi.fn(),

0 commit comments

Comments
 (0)