Skip to content

Commit cf83064

Browse files
committed
[thread-direct] add TD wake command wire format and default wake key derivation
Adds MAC-layer definitions for the Thread Direct Wake Command and the default Wake Key derivation in KeyManager. `Frame::kMacCmdDirect` (0x54) and `Frame::kThreadMacCmdWake` (0x01) define the command identifiers per the Thread Direct MAC command structure. `WakeFrameType` enumerates the three wake types. `KeyManager::GetDefaultWakeKey()` derives the Wake Key as HMAC-SHA256(NetworkKey, "Thread-Wake"), caching the result and invalidating it whenever the Network Key changes. Integration with SubMac and the TX/RX paths lands in the next PR.
1 parent 15b6e2d commit cf83064

7 files changed

Lines changed: 478 additions & 0 deletions

File tree

src/core/mac/mac_frame.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,30 @@ Error Frame::ValidatePsdu(void) const
297297
return error;
298298
}
299299

300+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
301+
bool Frame::IsTdWakeCommand(void) const
302+
{
303+
bool isTdWake = false;
304+
uint8_t commandId;
305+
uint8_t index;
306+
307+
VerifyOrExit(IsMacCommand() && IsVersion2015());
308+
SuccessOrExit(GetCommandId(commandId));
309+
VerifyOrExit(commandId == kMacCmdDirect);
310+
311+
// The Thread Cmd ID immediately follows the Command ID byte in the payload.
312+
index = FindPayloadIndex();
313+
VerifyOrExit(index != kInvalidIndex);
314+
VerifyOrExit(index + 1 < mLength - GetFooterLength());
315+
VerifyOrExit(mPsdu[index + 1] == kThreadMacCmdWake);
316+
317+
isTdWake = true;
318+
319+
exit:
320+
return isTdWake;
321+
}
322+
#endif // OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
323+
300324
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
301325
bool Frame::IsWakeupFrame(void) const
302326
{
@@ -1448,6 +1472,61 @@ void TxFrame::DecryptTransmitAesCcm(const ExtAddress &aExtAddress)
14481472
}
14491473
#endif // OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
14501474

1475+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE
1476+
Error TxFrame::GenerateThreadDirectWakeCommand(PanId aPanId,
1477+
const ExtAddress &aDstExtAddress,
1478+
const ExtAddress &aSrcExtAddress,
1479+
WakeFrameType aWakeType,
1480+
uint8_t aRendezvousTimeTenSym,
1481+
uint8_t aRetryInterval,
1482+
uint8_t aRetryCount)
1483+
{
1484+
uint16_t fcf;
1485+
uint8_t secCtl;
1486+
FrameBuilder builder;
1487+
Address dst;
1488+
Address src;
1489+
1490+
dst.SetExtended(aDstExtAddress);
1491+
src.SetExtended(aSrcExtAddress);
1492+
1493+
// 2015 MAC Command frame, extended dst+src, Dest PAN ID present,
1494+
// security enabled. No ACK requested - wake frames are sent as a burst without per-frame ACK.
1495+
fcf = kTypeMacCmd | kVersion2015 | kFcfSecurityEnabled;
1496+
fcf |= DetermineFcfAddrType(dst, kFcfDstAddrShift);
1497+
fcf |= DetermineFcfAddrType(src, kFcfSrcAddrShift);
1498+
1499+
ClearAllBytes(mInfo.mTxInfo);
1500+
builder.Init(mPsdu, GetMtu());
1501+
1502+
IgnoreError(builder.AppendUint<kLittleEndian>(fcf));
1503+
IgnoreError(builder.AppendUint8(0)); // Sequence number (filled by SubMac)
1504+
IgnoreError(builder.AppendUint<kLittleEndian>(aPanId)); // Destination PAN ID
1505+
IgnoreError(builder.AppendMacAddress(dst)); // Destination extended address
1506+
IgnoreError(builder.AppendMacAddress(src)); // Source extended address
1507+
1508+
// Auxiliary Security Header: Enc-Mic-32, Key ID Mode 1 (1-byte key index for Wake Key)
1509+
secCtl = kKeyIdMode1 | kSecurityEncMic32;
1510+
IgnoreError(builder.AppendUint8(secCtl));
1511+
builder.AppendLength(CalculateSecurityHeaderSize(secCtl) -
1512+
sizeof(secCtl)); // Frame counter + key ID (filled by SubMac)
1513+
1514+
// Encrypted payload: Thread Direct MAC Command bytes
1515+
IgnoreError(builder.AppendUint8(kMacCmdDirect)); // 0x54
1516+
IgnoreError(builder.AppendUint8(kThreadMacCmdWake)); // 0x01
1517+
IgnoreError(builder.AppendUint8(static_cast<uint8_t>(aWakeType))); // Wake Frame Type
1518+
IgnoreError(builder.AppendUint8(aRendezvousTimeTenSym)); // Rendezvous time
1519+
IgnoreError(builder.AppendUint8(static_cast<uint8_t>(((aRetryInterval & 0x0f) << 4) | (aRetryCount & 0x0f))));
1520+
1521+
// Reserve space for MIC and FCS (filled during AES-CCM processing)
1522+
builder.AppendLength(CalculateMicSize(secCtl) + GetFcsSize());
1523+
1524+
mLength = builder.GetLength();
1525+
1526+
return kErrorNone;
1527+
}
1528+
#endif // OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE
1529+
14511530
void TxFrame::GenerateImmAck(const RxFrame &aFrame, bool aIsFramePending)
14521531
{
14531532
uint16_t fcf = static_cast<uint16_t>(kTypeAck) | aFrame.GetVersion();

src/core/mac/mac_frame.hpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,32 @@ class Frame : public otRadioFrame
129129
kMacCmdBeaconRequest = 7,
130130
kMacCmdCoordinatorRealignment = 8,
131131
kMacCmdGtsRequest = 9,
132+
kMacCmdDirect = 0x54, ///< Thread Direct MAC Command (IEEE 802.15.4 vendor-specific command)
132133
};
133134

135+
/**
136+
* Thread MAC Command IDs - byte 1 of the kMacCmdDirect (0x54) payload.
137+
*/
138+
enum ThreadMacCmdId : uint8_t
139+
{
140+
kThreadMacCmdAdvertisement = 0x00,
141+
kThreadMacCmdWake = 0x01,
142+
kThreadMacCmdDirectLink = 0x02,
143+
};
144+
145+
/**
146+
* Wake Frame Type - byte 2 of the Thread Wake Command payload.
147+
*/
148+
enum WakeFrameType : uint8_t
149+
{
150+
kWakeFrameTypeDirectLink = 0x00,
151+
kWakeFrameTypePowerOutage = 0x01,
152+
kWakeFrameTypeConnectionless = 0x02,
153+
};
154+
155+
static constexpr uint8_t kWakeKeyIndex =
156+
129; ///< Key index for the TD Wake Key. Must equal OT_MAC_FRAME_WAKE_KEY_INDEX.
157+
134158
static constexpr uint16_t kInfoStringSize = 128; ///< Max chars for `InfoString` (ToInfoString()).
135159

136160
static constexpr uint8_t kPreambleSize = 4;
@@ -187,6 +211,14 @@ class Frame : public otRadioFrame
187211
bool IsMacCommand(void) const { return GetType() == kTypeMacCmd; }
188212

189213
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
214+
/**
215+
* Returns whether the frame is a Thread Direct Wake Command (MAC Command 0x54 / Thread Cmd 0x01).
216+
*
217+
* @retval TRUE If this is a Thread Direct Wake Command.
218+
* @retval FALSE Otherwise.
219+
*/
220+
bool IsTdWakeCommand(void) const;
221+
190222
/**
191223
* This method returns whether the frame is an IEEE 802.15.4 Wake-up frame.
192224
*
@@ -1342,6 +1374,30 @@ class TxFrame : public Frame
13421374
void SetTimeSyncSeq(uint8_t aTimeSyncSeq) { mInfo.mTxInfo.mIeInfo->mTimeSyncSeq = aTimeSyncSeq; }
13431375
#endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
13441376

1377+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE
1378+
/**
1379+
* Generates a Thread Direct Wake Command frame (MAC Command 0x54 / Thread Cmd 0x01).
1380+
*
1381+
* @param[in] aPanId The Thread network PAN ID.
1382+
* @param[in] aDstExtAddress Extended address of the target Wake Listener.
1383+
* @param[in] aSrcExtAddress Extended address of this Wake Initiator.
1384+
* @param[in] aWakeType Wake Frame Type byte.
1385+
* @param[in] aRendezvousTimeTenSym Remaining wake sequence duration in 10-symbol units (0-255).
1386+
* @param[in] aRetryInterval Connection retry interval (4-bit value).
1387+
* @param[in] aRetryCount Connection retry count (4-bit value).
1388+
*
1389+
* @retval kErrorNone Frame generated successfully.
1390+
* @retval kErrorInvalidArgs Source address is not set.
1391+
*/
1392+
Error GenerateThreadDirectWakeCommand(PanId aPanId,
1393+
const ExtAddress &aDstExtAddress,
1394+
const ExtAddress &aSrcExtAddress,
1395+
WakeFrameType aWakeType,
1396+
uint8_t aRendezvousTimeTenSym,
1397+
uint8_t aRetryInterval,
1398+
uint8_t aRetryCount);
1399+
#endif
1400+
13451401
/**
13461402
* Generate Imm-Ack in this frame object.
13471403
*

src/core/thread/key_manager.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ const uint8_t KeyManager::kThreadString[] = {
4545
'T', 'h', 'r', 'e', 'a', 'd',
4646
};
4747

48+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
49+
const uint8_t KeyManager::kWakeKeyString[] = {
50+
'T', 'h', 'r', 'e', 'a', 'd', '-', 'W', 'a', 'k', 'e',
51+
};
52+
#endif
53+
4854
#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
4955
const uint8_t KeyManager::kHkdfExtractSaltString[] = {'T', 'h', 'r', 'e', 'a', 'd', 'S', 'e', 'q', 'u', 'e', 'n',
5056
'c', 'e', 'M', 'a', 's', 't', 'e', 'r', 'K', 'e', 'y'};
@@ -179,6 +185,10 @@ KeyManager::KeyManager(Instance &aInstance)
179185
{
180186
otPlatCryptoInit();
181187

188+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
189+
mWakeKeyValid = false;
190+
#endif
191+
182192
#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
183193
mNetworkKeyRef = Crypto::Storage::kInvalidKeyRef;
184194
mPskcRef = Crypto::Storage::kInvalidKeyRef;
@@ -281,6 +291,9 @@ void KeyManager::SetNetworkKey(const NetworkKey &aNetworkKey)
281291
Get<Notifier>().Signal(kEventThreadKeySeqCounterChanged);
282292

283293
mKeySequence = 0;
294+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
295+
mWakeKeyValid = false;
296+
#endif
284297
UpdateKeyMaterial();
285298
ResetFrameCounters();
286299

@@ -330,6 +343,37 @@ void KeyManager::ComputeTrelKey(uint32_t aKeySequence, Mac::Key &aKey) const
330343
}
331344
#endif
332345

346+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
347+
const Mac::KeyMaterial &KeyManager::GetDefaultWakeKey(void)
348+
{
349+
if (!mWakeKeyValid)
350+
{
351+
Crypto::HmacSha256 hmac;
352+
Crypto::HmacSha256::Hash hash;
353+
Crypto::Key cryptoKey;
354+
Mac::Key wakeKey;
355+
356+
#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
357+
cryptoKey.SetAsKeyRef(mNetworkKeyRef);
358+
#else
359+
cryptoKey.Set(mNetworkKey.m8, NetworkKey::kSize);
360+
#endif
361+
362+
hmac.Start(cryptoKey);
363+
hmac.Update(kWakeKeyString);
364+
hmac.Finish(hash);
365+
366+
static_assert(Mac::Key::kSize <= Crypto::HmacSha256::Hash::kSize, "Wake Key size exceeds HMAC output size");
367+
memcpy(wakeKey.m8, hash.m8, Mac::Key::kSize);
368+
369+
mWakeKeyMaterial.SetFrom(wakeKey, kExportableMacKeys);
370+
mWakeKeyValid = true;
371+
}
372+
373+
return mWakeKeyMaterial;
374+
}
375+
#endif // OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
376+
333377
void KeyManager::UpdateKeyMaterial(void)
334378
{
335379
HashKeys hashKeys;
@@ -692,6 +736,9 @@ void KeyManager::SetNetworkKeyRef(otNetworkKeyRef aKeyRef)
692736
Get<Notifier>().Signal(kEventNetworkKeyChanged);
693737
Get<Notifier>().Signal(kEventThreadKeySeqCounterChanged);
694738
mKeySequence = 0;
739+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
740+
mWakeKeyValid = false;
741+
#endif
695742
UpdateKeyMaterial();
696743
ResetFrameCounters();
697744

src/core/thread/key_manager.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,18 @@ class KeyManager : public InstanceLocator, private NonCopyable
449449
*/
450450
void IncrementMleFrameCounter(void);
451451

452+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
453+
/**
454+
* Returns the default Thread Direct Wake Key as `Mac::KeyMaterial`.
455+
*
456+
* The Wake Key is derived as HMAC-SHA256(NetworkKey, "Thread-Wake") and cached.
457+
* The cache is invalidated whenever the Network Key changes.
458+
*
459+
* @returns A reference to the cached Wake Key material.
460+
*/
461+
const Mac::KeyMaterial &GetDefaultWakeKey(void);
462+
#endif
463+
452464
/**
453465
* Returns the KEK as `KekKeyMaterial`
454466
*
@@ -614,6 +626,10 @@ class KeyManager : public InstanceLocator, private NonCopyable
614626

615627
static const uint8_t kThreadString[];
616628

629+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
630+
static const uint8_t kWakeKeyString[];
631+
#endif
632+
617633
#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
618634
static const uint8_t kHkdfExtractSaltString[];
619635
static const uint8_t kTrelInfoString[];
@@ -629,6 +645,11 @@ class KeyManager : public InstanceLocator, private NonCopyable
629645
Mle::KeyMaterial mMleKey;
630646
Mle::KeyMaterial mTemporaryMleKey;
631647

648+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
649+
Mac::KeyMaterial mWakeKeyMaterial;
650+
bool mWakeKeyValid : 1;
651+
#endif
652+
632653
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
633654
Mle::KeyMaterial mTemporaryMacKey;
634655
#endif

tests/unit/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ ot_unit_test(hmac_sha256)
229229
ot_unit_test(ip4_header)
230230
ot_unit_test(ip6_header)
231231
ot_unit_test(ip_address)
232+
ot_unit_test(key_manager)
232233
ot_unit_test(link_metrics_manager)
233234
ot_unit_test(link_quality)
234235
ot_unit_test(linked_list)

tests/unit/test_key_manager.cpp

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) 2026, The OpenThread Authors.
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
* 1. Redistributions of source code must retain the above copyright
8+
* notice, this list of conditions and the following disclaimer.
9+
* 2. Redistributions in binary form must reproduce the above copyright
10+
* notice, this list of conditions and the following disclaimer in the
11+
* documentation and/or other materials provided with the distribution.
12+
* 3. Neither the name of the copyright holder nor the
13+
* names of its contributors may be used to endorse or promote products
14+
* derived from this software without specific prior written permission.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
*/
28+
29+
#include "common/code_utils.hpp"
30+
#include "common/debug.hpp"
31+
#include "thread/key_manager.hpp"
32+
33+
#include "test_platform.h"
34+
#include "test_util.hpp"
35+
36+
namespace ot {
37+
38+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
39+
40+
void TestWakeKeyDerivation(void)
41+
{
42+
// `KeyManager::GetDefaultWakeKey()` matches the first 16 bytes of
43+
// HMAC-SHA256 over the literal "Thread-Wake" using the Thread Network Key
44+
// as the HMAC key (see `KeyManager::GetDefaultWakeKey()`).
45+
//
46+
// Test vector (network key is non-zero on purpose):
47+
// Network Key : 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
48+
// Wake Key : f5 ca 7d 94 57 48 c4 55 e8 ad 69 6a 76 fb 18 d0
49+
//
50+
// Recompute with:
51+
// python3 -c "import
52+
// hmac,hashlib;k=bytes(range(16));print(hmac.new(k,b'Thread-Wake',hashlib.sha256).digest()[:16].hex())"
53+
54+
constexpr uint8_t kNetworkKeyBytes[OT_NETWORK_KEY_SIZE] = {
55+
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
56+
};
57+
58+
constexpr uint8_t kExpectedWakeKey[Mac::Key::kSize] = {
59+
0xf5, 0xca, 0x7d, 0x94, 0x57, 0x48, 0xc4, 0x55, 0xe8, 0xad, 0x69, 0x6a, 0x76, 0xfb, 0x18, 0xd0,
60+
};
61+
62+
Instance *instance;
63+
NetworkKey networkKey;
64+
Mac::Key derivedKey;
65+
66+
instance = testInitInstance();
67+
VerifyOrQuit(instance != nullptr);
68+
69+
memcpy(networkKey.m8, kNetworkKeyBytes, sizeof(networkKey.m8));
70+
instance->Get<KeyManager>().SetNetworkKey(networkKey);
71+
72+
const Mac::KeyMaterial &wakeMaterial = instance->Get<KeyManager>().GetDefaultWakeKey();
73+
74+
wakeMaterial.ExtractKey(derivedKey);
75+
VerifyOrQuit(memcmp(derivedKey.GetBytes(), kExpectedWakeKey, Mac::Key::kSize) == 0, "Wake key derivation mismatch");
76+
77+
testFreeInstance(instance);
78+
}
79+
80+
#endif // OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
81+
82+
} // namespace ot
83+
84+
int main(void)
85+
{
86+
#if OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_INITIATOR_ENABLE || OPENTHREAD_CONFIG_THREAD_DIRECT_WAKE_LISTENER_ENABLE
87+
ot::TestWakeKeyDerivation();
88+
#endif
89+
printf("All tests passed\n");
90+
return 0;
91+
}

0 commit comments

Comments
 (0)