Skip to content

Commit 06e210f

Browse files
authored
[sub-mac] redo security processing for every (re)transmission (openthread#13093)
Retransmissions of frames containing time-dependent header Information Elements (IEs), such as CSL or Time Sync, require updates to these IEs to reflect the exact time of sending. If the frame counter is not incremented for these retransmissions, it leads to nonce reuse in AES-CCM encryption, which is a security vulnerability. This commit addresses this issue by ensuring that every transmission attempt (initial or retry) uses a fresh frame counter: - Deferred security processing from `SubMac::Send()` to `SubMac::BeginTransmit()`. - Upon retransmission in `SubMac::HandleTransmitDone()`, the frame is restored to plaintext via `TxFrame::DecryptTransmitAesCcm()` and security flags are cleared. - This allows time-dependent IEs to be updated and a new frame counter to be assigned for every attempt. Added a Nexus test case `retransmission_security` to verify that both CSL and standard MAC retransmissions use incrementing frame counters and updated CSL phases.
1 parent 5783555 commit 06e210f

11 files changed

Lines changed: 414 additions & 3 deletions

examples/platforms/simulation/openthread-core-simulation-config.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_TIMING_ENABLE 1
7878
#endif
7979

80+
#ifndef OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
81+
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE 1
82+
#endif
8083
#endif // OPENTHREAD_RADIO
8184

8285
#ifndef OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE

examples/platforms/utils/mac_frame.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,12 @@ void otMacFrameUpdateTimeIe(otRadioFrame *aFrame, uint64_t aRadioTime, otRadioCo
404404

405405
otError otMacFrameProcessTxSfd(otRadioFrame *aFrame, uint64_t aRadioTime, otRadioContext *aRadioContext)
406406
{
407+
otError error = OT_ERROR_NONE;
408+
409+
aFrame->mInfo.mTxInfo.mTimestamp = aRadioTime;
410+
411+
VerifyOrExit(!otMacFrameIsSecurityEnabled(aFrame) || !aFrame->mInfo.mTxInfo.mIsSecurityProcessed);
412+
407413
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
408414
if (aRadioContext->mCslPresent) // CSL IE should be filled for every transmit attempt
409415
{
@@ -413,8 +419,10 @@ otError otMacFrameProcessTxSfd(otRadioFrame *aFrame, uint64_t aRadioTime, otRadi
413419
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
414420
otMacFrameUpdateTimeIe(aFrame, aRadioTime, aRadioContext);
415421
#endif
416-
aFrame->mInfo.mTxInfo.mTimestamp = aRadioTime;
417-
return otMacFrameProcessTransmitSecurity(aFrame, aRadioContext);
422+
error = otMacFrameProcessTransmitSecurity(aFrame, aRadioContext);
423+
424+
exit:
425+
return error;
418426
}
419427

420428
bool otMacFrameSrcAddrMatchCslReceiverPeer(const otRadioFrame *aFrame, const otRadioContext *aRadioContext)

src/core/config/mac.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,15 @@
382382
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_RX_ON_WHEN_IDLE_ENABLE 0
383383
#endif
384384

385+
/**
386+
* @def OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
387+
*
388+
* Define to 1 to enable software retransmission security logic.
389+
*/
390+
#ifndef OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
391+
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE 1
392+
#endif
393+
385394
/**
386395
* @def OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
387396
*

src/core/mac/mac_frame.cpp

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
#include "common/log.hpp"
4242
#include "common/num_utils.hpp"
4343
#include "radio/trel_link.hpp"
44-
#if OPENTHREAD_FTD || OPENTHREAD_MTD || OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE
44+
#if OPENTHREAD_FTD || OPENTHREAD_MTD || OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE || \
45+
(OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE)
4546
#include "crypto/aes_ccm.hpp"
4647
#endif
4748

@@ -1404,6 +1405,39 @@ void TxFrame::ProcessTransmitAesCcm(const ExtAddress &aExtAddress)
14041405
#endif // OPENTHREAD_FTD || OPENTHREAD_MTD || OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE
14051406
}
14061407

1408+
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
1409+
void TxFrame::DecryptTransmitAesCcm(const ExtAddress &aExtAddress)
1410+
{
1411+
uint32_t frameCounter = 0;
1412+
uint8_t securityLevel;
1413+
uint8_t nonce[Crypto::AesCcm::kNonceSize];
1414+
uint8_t tagLength;
1415+
Crypto::AesCcm aesCcm;
1416+
1417+
VerifyOrExit(GetSecurityEnabled() && IsSecurityProcessed());
1418+
1419+
SuccessOrExit(GetSecurityLevel(securityLevel));
1420+
SuccessOrExit(GetFrameCounter(frameCounter));
1421+
1422+
Crypto::AesCcm::GenerateNonce(aExtAddress, frameCounter, securityLevel, nonce);
1423+
1424+
aesCcm.SetKey(GetAesKey());
1425+
tagLength = GetFooterLength() - GetFcsSize();
1426+
1427+
aesCcm.Init(GetHeaderLength(), GetPayloadLength(), tagLength, nonce, sizeof(nonce));
1428+
aesCcm.Header(GetHeader(), GetHeaderLength());
1429+
aesCcm.Payload(GetPayload(), GetPayload(), GetPayloadLength(), Crypto::AesCcm::kDecrypt);
1430+
// Note: We skip aesCcm.Finalize() checking because we are only decrypting back to plaintext,
1431+
// and we know the ciphertext was generated correctly by us previously.
1432+
1433+
SetIsSecurityProcessed(false);
1434+
SetIsHeaderUpdated(false);
1435+
1436+
exit:
1437+
return;
1438+
}
1439+
#endif // OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
1440+
14071441
void TxFrame::GenerateImmAck(const RxFrame &aFrame, bool aIsFramePending)
14081442
{
14091443
uint16_t fcf = static_cast<uint16_t>(kTypeAck) | aFrame.GetVersion();

src/core/mac/mac_frame.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,14 @@ class Frame : public otRadioFrame
626626
#endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
627627

628628
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
629+
/**
630+
* Indicates whether the frame contains header IEs.
631+
*
632+
* @retval TRUE The frame contains header IEs.
633+
* @retval FALSE The frame contains no header IEs.
634+
*/
635+
bool HasHeaderIe(void) const { return FindHeaderIeIndex() != kInvalidIndex; }
636+
629637
/**
630638
* Returns a pointer to the Header IE.
631639
*
@@ -1240,6 +1248,14 @@ class TxFrame : public Frame
12401248
*/
12411249
void ProcessTransmitAesCcm(const ExtAddress &aExtAddress);
12421250

1251+
/**
1252+
* Decrypts the frame which was previously encrypted.
1253+
*
1254+
* @param[in] aExtAddress A reference to the extended address, which will be used to generate nonce
1255+
* for AES CCM computation.
1256+
*/
1257+
void DecryptTransmitAesCcm(const ExtAddress &aExtAddress);
1258+
12431259
/**
12441260
* Indicates whether or not the frame has security processed.
12451261
*

src/core/mac/sub_mac.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ SubMac::SubMac(Instance &aInstance)
6363
mCslParentAccuracy.Init();
6464
#endif
6565

66+
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && !OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
67+
// Assuming the platform must deal with the retransmission security correctly.
68+
OT_ASSERT(mRadioCaps & OT_RADIO_CAPS_TRANSMIT_RETRIES);
69+
#endif
6670
Init();
6771
}
6872

@@ -601,6 +605,15 @@ void SubMac::HandleTransmitDone(TxFrame &aFrame, RxFrame *aAckFrame, Error aErro
601605
mTransmitRetries++;
602606
aFrame.SetIsARetransmission(true);
603607

608+
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
609+
if (aFrame.GetSecurityEnabled() && aFrame.IsSecurityProcessed() && aFrame.HasHeaderIe())
610+
{
611+
aFrame.DecryptTransmitAesCcm(GetExtAddress());
612+
}
613+
614+
ProcessTransmitSecurity();
615+
#endif
616+
604617
#if OPENTHREAD_CONFIG_MAC_ADD_DELAY_ON_NO_ACK_ERROR_BEFORE_RETRY
605618
if (aError == kErrorNoAck)
606619
{

tests/nexus/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ ot_nexus_test(1_4_PIC_TC_3 "cert;nexus")
394394
ot_nexus_test(1_4_PIC_TC_4 "cert;nexus")
395395
ot_nexus_test(1_4_CS_TC_3 "cert;nexus")
396396
ot_nexus_test(inform_previous_parent_on_reattach "cert;nexus")
397+
ot_nexus_test(retransmission_security "core;nexus")
397398

398399
# Misc tests
399400
ot_nexus_test(anycast "core;nexus")

tests/nexus/openthread-core-nexus-config.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
#define OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE 1
7575
#define OPENTHREAD_CONFIG_DNS_CLIENT_BIND_UDP_TO_THREAD_NETIF 1
7676
#define OPENTHREAD_CONFIG_DNS_DSO_ENABLE 0
77+
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE 1
7778
#define OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE 1
7879
#define OPENTHREAD_CONFIG_DNSSD_DISCOVERY_PROXY_ENABLE 1
7980
#define OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE 1

tests/nexus/run_nexus_tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ DEFAULT_TESTS=(
238238
"leader_reboot_multiple_link_request"
239239
"router_reboot_multiple_link_request"
240240
"pbbr_aloc"
241+
"retransmission_security"
241242
)
242243

243244
# Use provided arguments or the default test list
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 <stdio.h>
30+
31+
#include "platform/nexus_core.hpp"
32+
#include "platform/nexus_node.hpp"
33+
34+
namespace ot {
35+
namespace Nexus {
36+
37+
/**
38+
* Time to advance for a node to form a network and become leader, in milliseconds.
39+
*/
40+
static constexpr uint32_t kFormNetworkTime = 13 * 1000;
41+
42+
/**
43+
* Time to advance for a node to join as a SSED.
44+
*/
45+
static constexpr uint32_t kAttachAsSsedTime = 20 * 1000;
46+
47+
/**
48+
* CSL Period in milliseconds.
49+
*/
50+
static constexpr uint32_t kCslPeriodMs = 100;
51+
52+
/**
53+
* CSL Period in units of 10 symbols.
54+
*/
55+
static constexpr uint32_t kCslPeriod = kCslPeriodMs * 1000 / OT_US_PER_TEN_SYMBOLS;
56+
57+
/**
58+
* Time to advance for CSL synchronization to complete, in milliseconds.
59+
*/
60+
static constexpr uint32_t kCslSyncTime = 5 * 1000;
61+
62+
/**
63+
* Payload size for a standard ICMPv6 Echo Request.
64+
*/
65+
static constexpr uint16_t kEchoPayloadSize = 10;
66+
67+
void TestRetransmissionSecurity(void)
68+
{
69+
/**
70+
* Retransmission Security Test
71+
*
72+
* Topology:
73+
* - Leader (DUT)
74+
* - SSED_1
75+
*
76+
* Purpose:
77+
* Validate that retransmitted frames with CSL IE use an incremented frame counter
78+
* and are correctly encrypted for each attempt.
79+
*/
80+
81+
Core nexus;
82+
83+
Node &leader = nexus.CreateNode();
84+
Node &ssed1 = nexus.CreateNode();
85+
86+
leader.SetName("LEADER");
87+
ssed1.SetName("SSED_1");
88+
89+
nexus.AdvanceTime(0);
90+
91+
SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote));
92+
93+
Log("---------------------------------------------------------------------------------------");
94+
Log("Step 1: Network Formation");
95+
96+
AllowLinkBetween(leader, ssed1);
97+
98+
leader.Form();
99+
nexus.AdvanceTime(kFormNetworkTime);
100+
VerifyOrQuit(leader.Get<Mle::Mle>().IsLeader());
101+
102+
ssed1.Join(leader, Node::kAsSed);
103+
nexus.AdvanceTime(kAttachAsSsedTime);
104+
VerifyOrQuit(ssed1.Get<Mle::Mle>().IsAttached());
105+
106+
ssed1.Get<Mac::Mac>().SetCslPeriod(kCslPeriod);
107+
nexus.AdvanceTime(kCslSyncTime);
108+
VerifyOrQuit(ssed1.Get<Mac::Mac>().IsCslEnabled());
109+
110+
Log("---------------------------------------------------------------------------------------");
111+
Log("Step 2: Trigger Retransmissions (SSED to Leader)");
112+
113+
// Turn off leader radio so it misses SSED frames and SSED retries
114+
SuccessOrQuit(otPlatRadioSleep(&leader.GetInstance()));
115+
116+
ssed1.SendEchoRequest(leader.Get<Mle::Mle>().GetMeshLocalEid(), 0x1234, kEchoPayloadSize);
117+
118+
// Advance time enough for multiple retries.
119+
nexus.AdvanceTime(2000);
120+
121+
// Turn leader radio back on
122+
SuccessOrQuit(otPlatRadioReceive(&leader.GetInstance(), leader.Get<Mac::Mac>().GetPanChannel()));
123+
124+
nexus.SaveTestInfo("test_retransmission_security.json");
125+
}
126+
127+
} // namespace Nexus
128+
} // namespace ot
129+
130+
int main(void)
131+
{
132+
ot::Nexus::TestRetransmissionSecurity();
133+
printf("All tests passed\n");
134+
return 0;
135+
}

0 commit comments

Comments
 (0)