mirror of
https://github.com/espressif/openthread.git
synced 2026-06-05 21:14:49 +00:00
[sub-mac] redo security processing for every (re)transmission (#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.
This commit is contained in:
@@ -77,6 +77,9 @@
|
|||||||
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_TIMING_ENABLE 1
|
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_TIMING_ENABLE 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
|
||||||
|
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE 1
|
||||||
|
#endif
|
||||||
#endif // OPENTHREAD_RADIO
|
#endif // OPENTHREAD_RADIO
|
||||||
|
|
||||||
#ifndef OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
|
#ifndef OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
|
||||||
|
|||||||
@@ -404,6 +404,12 @@ exit:
|
|||||||
|
|
||||||
otError otMacFrameProcessTxSfd(otRadioFrame *aFrame, uint64_t aRadioTime, otRadioContext *aRadioContext)
|
otError otMacFrameProcessTxSfd(otRadioFrame *aFrame, uint64_t aRadioTime, otRadioContext *aRadioContext)
|
||||||
{
|
{
|
||||||
|
otError error = OT_ERROR_NONE;
|
||||||
|
|
||||||
|
aFrame->mInfo.mTxInfo.mTimestamp = aRadioTime;
|
||||||
|
|
||||||
|
VerifyOrExit(!otMacFrameIsSecurityEnabled(aFrame) || !aFrame->mInfo.mTxInfo.mIsSecurityProcessed);
|
||||||
|
|
||||||
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
|
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
|
||||||
if (aRadioContext->mCslPresent) // CSL IE should be filled for every transmit attempt
|
if (aRadioContext->mCslPresent) // CSL IE should be filled for every transmit attempt
|
||||||
{
|
{
|
||||||
@@ -413,8 +419,10 @@ otError otMacFrameProcessTxSfd(otRadioFrame *aFrame, uint64_t aRadioTime, otRadi
|
|||||||
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
|
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
|
||||||
otMacFrameUpdateTimeIe(aFrame, aRadioTime, aRadioContext);
|
otMacFrameUpdateTimeIe(aFrame, aRadioTime, aRadioContext);
|
||||||
#endif
|
#endif
|
||||||
aFrame->mInfo.mTxInfo.mTimestamp = aRadioTime;
|
error = otMacFrameProcessTransmitSecurity(aFrame, aRadioContext);
|
||||||
return otMacFrameProcessTransmitSecurity(aFrame, aRadioContext);
|
|
||||||
|
exit:
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool otMacFrameSrcAddrMatchCslReceiverPeer(const otRadioFrame *aFrame, const otRadioContext *aRadioContext)
|
bool otMacFrameSrcAddrMatchCslReceiverPeer(const otRadioFrame *aFrame, const otRadioContext *aRadioContext)
|
||||||
|
|||||||
@@ -382,6 +382,15 @@
|
|||||||
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_RX_ON_WHEN_IDLE_ENABLE 0
|
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_RX_ON_WHEN_IDLE_ENABLE 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @def OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
|
||||||
|
*
|
||||||
|
* Define to 1 to enable software retransmission security logic.
|
||||||
|
*/
|
||||||
|
#ifndef OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
|
||||||
|
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE 1
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @def OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
|
* @def OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -41,7 +41,8 @@
|
|||||||
#include "common/log.hpp"
|
#include "common/log.hpp"
|
||||||
#include "common/num_utils.hpp"
|
#include "common/num_utils.hpp"
|
||||||
#include "radio/trel_link.hpp"
|
#include "radio/trel_link.hpp"
|
||||||
#if OPENTHREAD_FTD || OPENTHREAD_MTD || OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE
|
#if OPENTHREAD_FTD || OPENTHREAD_MTD || OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE || \
|
||||||
|
(OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE)
|
||||||
#include "crypto/aes_ccm.hpp"
|
#include "crypto/aes_ccm.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -1404,6 +1405,39 @@ exit:
|
|||||||
#endif // OPENTHREAD_FTD || OPENTHREAD_MTD || OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE
|
#endif // OPENTHREAD_FTD || OPENTHREAD_MTD || OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
|
||||||
|
void TxFrame::DecryptTransmitAesCcm(const ExtAddress &aExtAddress)
|
||||||
|
{
|
||||||
|
uint32_t frameCounter = 0;
|
||||||
|
uint8_t securityLevel;
|
||||||
|
uint8_t nonce[Crypto::AesCcm::kNonceSize];
|
||||||
|
uint8_t tagLength;
|
||||||
|
Crypto::AesCcm aesCcm;
|
||||||
|
|
||||||
|
VerifyOrExit(GetSecurityEnabled() && IsSecurityProcessed());
|
||||||
|
|
||||||
|
SuccessOrExit(GetSecurityLevel(securityLevel));
|
||||||
|
SuccessOrExit(GetFrameCounter(frameCounter));
|
||||||
|
|
||||||
|
Crypto::AesCcm::GenerateNonce(aExtAddress, frameCounter, securityLevel, nonce);
|
||||||
|
|
||||||
|
aesCcm.SetKey(GetAesKey());
|
||||||
|
tagLength = GetFooterLength() - GetFcsSize();
|
||||||
|
|
||||||
|
aesCcm.Init(GetHeaderLength(), GetPayloadLength(), tagLength, nonce, sizeof(nonce));
|
||||||
|
aesCcm.Header(GetHeader(), GetHeaderLength());
|
||||||
|
aesCcm.Payload(GetPayload(), GetPayload(), GetPayloadLength(), Crypto::AesCcm::kDecrypt);
|
||||||
|
// Note: We skip aesCcm.Finalize() checking because we are only decrypting back to plaintext,
|
||||||
|
// and we know the ciphertext was generated correctly by us previously.
|
||||||
|
|
||||||
|
SetIsSecurityProcessed(false);
|
||||||
|
SetIsHeaderUpdated(false);
|
||||||
|
|
||||||
|
exit:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif // OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
|
||||||
|
|
||||||
void TxFrame::GenerateImmAck(const RxFrame &aFrame, bool aIsFramePending)
|
void TxFrame::GenerateImmAck(const RxFrame &aFrame, bool aIsFramePending)
|
||||||
{
|
{
|
||||||
uint16_t fcf = static_cast<uint16_t>(kTypeAck) | aFrame.GetVersion();
|
uint16_t fcf = static_cast<uint16_t>(kTypeAck) | aFrame.GetVersion();
|
||||||
|
|||||||
@@ -626,6 +626,14 @@ public:
|
|||||||
#endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
|
#endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
|
||||||
|
|
||||||
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
|
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
|
||||||
|
/**
|
||||||
|
* Indicates whether the frame contains header IEs.
|
||||||
|
*
|
||||||
|
* @retval TRUE The frame contains header IEs.
|
||||||
|
* @retval FALSE The frame contains no header IEs.
|
||||||
|
*/
|
||||||
|
bool HasHeaderIe(void) const { return FindHeaderIeIndex() != kInvalidIndex; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a pointer to the Header IE.
|
* Returns a pointer to the Header IE.
|
||||||
*
|
*
|
||||||
@@ -1240,6 +1248,14 @@ public:
|
|||||||
*/
|
*/
|
||||||
void ProcessTransmitAesCcm(const ExtAddress &aExtAddress);
|
void ProcessTransmitAesCcm(const ExtAddress &aExtAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the frame which was previously encrypted.
|
||||||
|
*
|
||||||
|
* @param[in] aExtAddress A reference to the extended address, which will be used to generate nonce
|
||||||
|
* for AES CCM computation.
|
||||||
|
*/
|
||||||
|
void DecryptTransmitAesCcm(const ExtAddress &aExtAddress);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether or not the frame has security processed.
|
* Indicates whether or not the frame has security processed.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ SubMac::SubMac(Instance &aInstance)
|
|||||||
mCslParentAccuracy.Init();
|
mCslParentAccuracy.Init();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && !OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
|
||||||
|
// Assuming the platform must deal with the retransmission security correctly.
|
||||||
|
OT_ASSERT(mRadioCaps & OT_RADIO_CAPS_TRANSMIT_RETRIES);
|
||||||
|
#endif
|
||||||
Init();
|
Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,6 +605,15 @@ void SubMac::HandleTransmitDone(TxFrame &aFrame, RxFrame *aAckFrame, Error aErro
|
|||||||
mTransmitRetries++;
|
mTransmitRetries++;
|
||||||
aFrame.SetIsARetransmission(true);
|
aFrame.SetIsARetransmission(true);
|
||||||
|
|
||||||
|
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE
|
||||||
|
if (aFrame.GetSecurityEnabled() && aFrame.IsSecurityProcessed() && aFrame.HasHeaderIe())
|
||||||
|
{
|
||||||
|
aFrame.DecryptTransmitAesCcm(GetExtAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessTransmitSecurity();
|
||||||
|
#endif
|
||||||
|
|
||||||
#if OPENTHREAD_CONFIG_MAC_ADD_DELAY_ON_NO_ACK_ERROR_BEFORE_RETRY
|
#if OPENTHREAD_CONFIG_MAC_ADD_DELAY_ON_NO_ACK_ERROR_BEFORE_RETRY
|
||||||
if (aError == kErrorNoAck)
|
if (aError == kErrorNoAck)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ ot_nexus_test(1_4_PIC_TC_3 "cert;nexus")
|
|||||||
ot_nexus_test(1_4_PIC_TC_4 "cert;nexus")
|
ot_nexus_test(1_4_PIC_TC_4 "cert;nexus")
|
||||||
ot_nexus_test(1_4_CS_TC_3 "cert;nexus")
|
ot_nexus_test(1_4_CS_TC_3 "cert;nexus")
|
||||||
ot_nexus_test(inform_previous_parent_on_reattach "cert;nexus")
|
ot_nexus_test(inform_previous_parent_on_reattach "cert;nexus")
|
||||||
|
ot_nexus_test(retransmission_security "core;nexus")
|
||||||
|
|
||||||
# Misc tests
|
# Misc tests
|
||||||
ot_nexus_test(anycast "core;nexus")
|
ot_nexus_test(anycast "core;nexus")
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
#define OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE 1
|
#define OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE 1
|
||||||
#define OPENTHREAD_CONFIG_DNS_CLIENT_BIND_UDP_TO_THREAD_NETIF 1
|
#define OPENTHREAD_CONFIG_DNS_CLIENT_BIND_UDP_TO_THREAD_NETIF 1
|
||||||
#define OPENTHREAD_CONFIG_DNS_DSO_ENABLE 0
|
#define OPENTHREAD_CONFIG_DNS_DSO_ENABLE 0
|
||||||
|
#define OPENTHREAD_CONFIG_MAC_SOFTWARE_RETX_SECURITY_ENABLE 1
|
||||||
#define OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE 1
|
#define OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE 1
|
||||||
#define OPENTHREAD_CONFIG_DNSSD_DISCOVERY_PROXY_ENABLE 1
|
#define OPENTHREAD_CONFIG_DNSSD_DISCOVERY_PROXY_ENABLE 1
|
||||||
#define OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE 1
|
#define OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE 1
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ DEFAULT_TESTS=(
|
|||||||
"leader_reboot_multiple_link_request"
|
"leader_reboot_multiple_link_request"
|
||||||
"router_reboot_multiple_link_request"
|
"router_reboot_multiple_link_request"
|
||||||
"pbbr_aloc"
|
"pbbr_aloc"
|
||||||
|
"retransmission_security"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use provided arguments or the default test list
|
# Use provided arguments or the default test list
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026, The OpenThread Authors.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
* 1. Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
* 3. Neither the name of the copyright holder nor the
|
||||||
|
* names of its contributors may be used to endorse or promote products
|
||||||
|
* derived from this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "platform/nexus_core.hpp"
|
||||||
|
#include "platform/nexus_node.hpp"
|
||||||
|
|
||||||
|
namespace ot {
|
||||||
|
namespace Nexus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time to advance for a node to form a network and become leader, in milliseconds.
|
||||||
|
*/
|
||||||
|
static constexpr uint32_t kFormNetworkTime = 13 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time to advance for a node to join as a SSED.
|
||||||
|
*/
|
||||||
|
static constexpr uint32_t kAttachAsSsedTime = 20 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSL Period in milliseconds.
|
||||||
|
*/
|
||||||
|
static constexpr uint32_t kCslPeriodMs = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSL Period in units of 10 symbols.
|
||||||
|
*/
|
||||||
|
static constexpr uint32_t kCslPeriod = kCslPeriodMs * 1000 / OT_US_PER_TEN_SYMBOLS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time to advance for CSL synchronization to complete, in milliseconds.
|
||||||
|
*/
|
||||||
|
static constexpr uint32_t kCslSyncTime = 5 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload size for a standard ICMPv6 Echo Request.
|
||||||
|
*/
|
||||||
|
static constexpr uint16_t kEchoPayloadSize = 10;
|
||||||
|
|
||||||
|
void TestRetransmissionSecurity(void)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Retransmission Security Test
|
||||||
|
*
|
||||||
|
* Topology:
|
||||||
|
* - Leader (DUT)
|
||||||
|
* - SSED_1
|
||||||
|
*
|
||||||
|
* Purpose:
|
||||||
|
* Validate that retransmitted frames with CSL IE use an incremented frame counter
|
||||||
|
* and are correctly encrypted for each attempt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Core nexus;
|
||||||
|
|
||||||
|
Node &leader = nexus.CreateNode();
|
||||||
|
Node &ssed1 = nexus.CreateNode();
|
||||||
|
|
||||||
|
leader.SetName("LEADER");
|
||||||
|
ssed1.SetName("SSED_1");
|
||||||
|
|
||||||
|
nexus.AdvanceTime(0);
|
||||||
|
|
||||||
|
SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote));
|
||||||
|
|
||||||
|
Log("---------------------------------------------------------------------------------------");
|
||||||
|
Log("Step 1: Network Formation");
|
||||||
|
|
||||||
|
AllowLinkBetween(leader, ssed1);
|
||||||
|
|
||||||
|
leader.Form();
|
||||||
|
nexus.AdvanceTime(kFormNetworkTime);
|
||||||
|
VerifyOrQuit(leader.Get<Mle::Mle>().IsLeader());
|
||||||
|
|
||||||
|
ssed1.Join(leader, Node::kAsSed);
|
||||||
|
nexus.AdvanceTime(kAttachAsSsedTime);
|
||||||
|
VerifyOrQuit(ssed1.Get<Mle::Mle>().IsAttached());
|
||||||
|
|
||||||
|
ssed1.Get<Mac::Mac>().SetCslPeriod(kCslPeriod);
|
||||||
|
nexus.AdvanceTime(kCslSyncTime);
|
||||||
|
VerifyOrQuit(ssed1.Get<Mac::Mac>().IsCslEnabled());
|
||||||
|
|
||||||
|
Log("---------------------------------------------------------------------------------------");
|
||||||
|
Log("Step 2: Trigger Retransmissions (SSED to Leader)");
|
||||||
|
|
||||||
|
// Turn off leader radio so it misses SSED frames and SSED retries
|
||||||
|
SuccessOrQuit(otPlatRadioSleep(&leader.GetInstance()));
|
||||||
|
|
||||||
|
ssed1.SendEchoRequest(leader.Get<Mle::Mle>().GetMeshLocalEid(), 0x1234, kEchoPayloadSize);
|
||||||
|
|
||||||
|
// Advance time enough for multiple retries.
|
||||||
|
nexus.AdvanceTime(2000);
|
||||||
|
|
||||||
|
// Turn leader radio back on
|
||||||
|
SuccessOrQuit(otPlatRadioReceive(&leader.GetInstance(), leader.Get<Mac::Mac>().GetPanChannel()));
|
||||||
|
|
||||||
|
nexus.SaveTestInfo("test_retransmission_security.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Nexus
|
||||||
|
} // namespace ot
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
ot::Nexus::TestRetransmissionSecurity();
|
||||||
|
printf("All tests passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (c) 2026, The OpenThread Authors.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# 1. Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# 3. Neither the name of the copyright holder nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
#
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add the current directory to sys.path to find verify_utils
|
||||||
|
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.append(CUR_DIR)
|
||||||
|
|
||||||
|
import verify_utils
|
||||||
|
from pktverify import consts
|
||||||
|
from pktverify.layer_fields_container import LayerFieldsContainer
|
||||||
|
|
||||||
|
|
||||||
|
def get_val(field):
|
||||||
|
if field is None:
|
||||||
|
return None
|
||||||
|
s = str(field)
|
||||||
|
if s.lower() == 'null' or s == '':
|
||||||
|
return None
|
||||||
|
return field
|
||||||
|
|
||||||
|
|
||||||
|
def get_int(field):
|
||||||
|
val = get_val(field)
|
||||||
|
if val is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(str(val), 0)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def verify(pv):
|
||||||
|
pkts = pv.pkts
|
||||||
|
|
||||||
|
LEADER = pv.vars['LEADER']
|
||||||
|
SSED_1 = pv.vars['SSED_1']
|
||||||
|
|
||||||
|
LEADER_RLOC16 = get_int(pv.vars.get('LEADER_RLOC16'))
|
||||||
|
SSED_1_RLOC16 = get_int(pv.vars.get('SSED_1_RLOC16'))
|
||||||
|
|
||||||
|
LEADER_STR = str(LEADER).lower()
|
||||||
|
SSED_1_STR = str(SSED_1).lower()
|
||||||
|
|
||||||
|
print(f"LEADER: {LEADER}, RLOC16: {hex(LEADER_RLOC16) if LEADER_RLOC16 is not None else 'None'}")
|
||||||
|
print(f"SSED_1: {SSED_1}, RLOC16: {hex(SSED_1_RLOC16) if SSED_1_RLOC16 is not None else 'None'}")
|
||||||
|
|
||||||
|
groups = {}
|
||||||
|
|
||||||
|
# 1. Collect all MAC Data frames
|
||||||
|
for i in range(len(pkts)):
|
||||||
|
p = pkts[i]
|
||||||
|
if not hasattr(p, 'wpan') or get_int(p.wpan.frame_type) != consts.MAC_FRAME_TYPE_DATA:
|
||||||
|
continue
|
||||||
|
|
||||||
|
src16 = get_int(getattr(p.wpan, 'src16', None))
|
||||||
|
src64 = get_val(getattr(p.wpan, 'src64', None))
|
||||||
|
dst16 = get_int(getattr(p.wpan, 'dst16', None))
|
||||||
|
seq = get_int(p.wpan.seq_no)
|
||||||
|
|
||||||
|
if dst16 == 0xffff: # Skip broadcast
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if from LEADER or SSED_1
|
||||||
|
is_relevant_src = False
|
||||||
|
if src64 is not None:
|
||||||
|
src_str = str(src64).lower()
|
||||||
|
if src_str == LEADER_STR or src_str == SSED_1_STR:
|
||||||
|
is_relevant_src = True
|
||||||
|
elif src16 is not None:
|
||||||
|
if src16 == LEADER_RLOC16 or src16 == SSED_1_RLOC16:
|
||||||
|
is_relevant_src = True
|
||||||
|
|
||||||
|
if not is_relevant_src:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Group key identifies the (source, sequence) pair.
|
||||||
|
# Destination might change between RLOC16 and ExtAddr in some edge cases,
|
||||||
|
# but sequence numbers are usually unique enough within a short window.
|
||||||
|
src_id = str(src64).lower() if src64 is not None else hex(src16)
|
||||||
|
group_key = (src_id, seq)
|
||||||
|
if group_key not in groups:
|
||||||
|
groups[group_key] = []
|
||||||
|
groups[group_key].append(p)
|
||||||
|
|
||||||
|
# 2. Filter groups that are retransmissions (> 1 packet)
|
||||||
|
retry_groups = [g for k, g in groups.items() if len(g) > 1]
|
||||||
|
print(f"Found {len(retry_groups)} retransmission groups.")
|
||||||
|
|
||||||
|
if not retry_groups:
|
||||||
|
raise RuntimeError("No retransmissions detected in pcap. The test might not have triggered retries correctly.")
|
||||||
|
|
||||||
|
found_csl_retry = False
|
||||||
|
|
||||||
|
# 3. Verify each group
|
||||||
|
for group in retry_groups:
|
||||||
|
p0 = group[0]
|
||||||
|
src_id = str(p0.wpan.src64).lower() if get_val(getattr(p0.wpan, 'src64', None)) else hex(get_int(
|
||||||
|
p0.wpan.src16))
|
||||||
|
seq = get_int(p0.wpan.seq_no)
|
||||||
|
|
||||||
|
# Check for CSL IE in any packet of the group (it should be in all if it's a CSL retry)
|
||||||
|
has_csl = False
|
||||||
|
for p in group:
|
||||||
|
has_csl = p.wpan.has('header_ie.csl.period')
|
||||||
|
if has_csl:
|
||||||
|
csl_period = p.wpan.header_ie.csl.period
|
||||||
|
has_csl = str(csl_period) != 'null'
|
||||||
|
|
||||||
|
if has_csl:
|
||||||
|
break
|
||||||
|
|
||||||
|
type_str = "CSL" if has_csl else "Non-CSL"
|
||||||
|
print(f"Verifying {type_str} seq={seq} from {src_id} ({len(group)} attempts)")
|
||||||
|
|
||||||
|
if has_csl:
|
||||||
|
found_csl_retry = True
|
||||||
|
|
||||||
|
last_counter = -1
|
||||||
|
last_phase = -1
|
||||||
|
for i, p in enumerate(group):
|
||||||
|
counter = get_int(getattr(getattr(p.wpan, 'aux_sec', None), 'frame_counter', None))
|
||||||
|
has_header_ie = get_val(getattr(p.wpan, 'header_ie', None)) is not None
|
||||||
|
|
||||||
|
if has_csl:
|
||||||
|
print(f" Attempt {i}: Counter={counter}, HeaderIE={has_header_ie}")
|
||||||
|
if counter is not None and has_header_ie:
|
||||||
|
if last_counter != -1 and counter <= last_counter:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Nonce reuse detected! Frame counter {counter} did not increment for CSL seq {seq}")
|
||||||
|
last_counter = counter
|
||||||
|
|
||||||
|
# Check CSL phase
|
||||||
|
try:
|
||||||
|
phase = get_int(p.wpan.header_ie.csl.phase)
|
||||||
|
if phase is not None:
|
||||||
|
print(f" Attempt {i}: CSL Phase={phase}")
|
||||||
|
if phase == last_phase:
|
||||||
|
print(
|
||||||
|
f" Warning: CSL phase did not update between attempts for seq {seq} (likely fast retry)"
|
||||||
|
)
|
||||||
|
last_phase = phase
|
||||||
|
else:
|
||||||
|
print(f" Attempt {i}: CSL Phase parsed as None")
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
print(f" Attempt {i}: CSL Phase not parsed")
|
||||||
|
else:
|
||||||
|
# For non-CSL, same counter is expected/allowed in standard 15.4 retries
|
||||||
|
print(f" Attempt {i}: Counter={counter} (Allowed to be same as previous)")
|
||||||
|
if counter is not None:
|
||||||
|
if counter < last_counter:
|
||||||
|
raise RuntimeError(f"Frame counter {counter} went backwards for Non-CSL seq {seq}")
|
||||||
|
last_counter = counter
|
||||||
|
|
||||||
|
if not found_csl_retry:
|
||||||
|
raise RuntimeError("No CSL retransmissions found. Expected SSED to Leader retries with CSL IEs.")
|
||||||
|
|
||||||
|
print("All verification steps passed!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
verify_utils.run_main(verify)
|
||||||
Reference in New Issue
Block a user