diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 4fdd306a8..3a981aea3 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -395,6 +395,7 @@ ot_nexus_test(dns_client_config_auto_start "core;nexus") ot_nexus_test(dtls "core;nexus") ot_nexus_test(form_join "core;nexus") ot_nexus_test(ipv6_source_selection "core;nexus") +ot_nexus_test(key_rotation_guard_time "core;nexus") ot_nexus_test(log_override "core;nexus") ot_nexus_test(mac_scan "core;nexus") ot_nexus_test(mle_blocking_downgrade "core;nexus") diff --git a/tests/nexus/test_key_rotation_guard_time.cpp b/tests/nexus/test_key_rotation_guard_time.cpp new file mode 100644 index 000000000..e1dfd0f97 --- /dev/null +++ b/tests/nexus/test_key_rotation_guard_time.cpp @@ -0,0 +1,192 @@ +/* + * 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 + +#include "meshcop/dataset_manager.hpp" +#include "meshcop/dataset_updater.hpp" +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" +#include "thread/key_manager.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 child and upgrade to a router, in milliseconds. + */ +static constexpr uint32_t kAttachTime = 200 * 1000; + +/** + * Time to wait for ICMPv6 Echo response, in milliseconds. + */ +static constexpr uint32_t kEchoTimeout = 5000; + +void TestKeyRotationGuardTime(void) +{ + Core nexus; + + Node &leader = nexus.CreateNode(); + Node &child = nexus.CreateNode(); + Node &reed = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + + leader.SetName("LEADER"); + child.SetName("CHILD"); + reed.SetName("REED"); + router.SetName("ROUTER"); + + nexus.AdvanceTime(0); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote)); + + Log("---------------------------------------------------------------------------------------"); + Log("Form the network"); + + AllowLinkBetween(leader, child); + AllowLinkBetween(leader, reed); + AllowLinkBetween(leader, router); + + leader.Form(); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(leader.Get().IsLeader()); + + child.Join(leader, Node::kAsMed); + reed.Join(leader, Node::kAsFed); + router.Join(leader, Node::kAsFtd); + + nexus.AdvanceTime(kAttachTime); + VerifyOrQuit(child.Get().IsChild()); + VerifyOrQuit(reed.Get().IsChild()); + VerifyOrQuit(router.Get().IsRouter()); + + Log("---------------------------------------------------------------------------------------"); + Log("Validate initial key sequence counter and key switch guard time"); + + Node *nodes[] = {&leader, &child, &reed, &router}; + + for (Node *node : nodes) + { + VerifyOrQuit(node->Get().GetCurrentKeySequence() == 0); + VerifyOrQuit(node->Get().GetKeySwitchGuardTime() == 624); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Change the key rotation time and verify key switch guard time"); + + uint16_t rotationTimes[] = {100, 1, 10, 888, 2}; + + for (uint16_t rotationTime : rotationTimes) + { + MeshCoP::Dataset::Info datasetInfo; + + datasetInfo.Clear(); + datasetInfo.Update().SetToDefault(); + datasetInfo.Update().mRotationTime = rotationTime; + + SuccessOrQuit(leader.Get().RequestUpdate(datasetInfo, nullptr, nullptr)); + + nexus.AdvanceTime(301 * 1000); // Wait for propagation (default delay is 300s) + + uint16_t effectiveRotationTime = + (rotationTime < SecurityPolicy::kMinKeyRotationTime) ? SecurityPolicy::kMinKeyRotationTime : rotationTime; + static constexpr uint32_t kKeySwitchGuardTimePercentage = 93; + uint16_t expectedGuardTime = + static_cast(static_cast(effectiveRotationTime) * kKeySwitchGuardTimePercentage / 100); + + for (Node *node : nodes) + { + VerifyOrQuit(node->Get().GetKeySwitchGuardTime() == expectedGuardTime); + } + } + + Log("---------------------------------------------------------------------------------------"); + Log("Wait for automatic rotation to 1"); + + // All nodes should already have rotationTime = 2 and guardTime = 1 hour from the end of the previous loop. + // Trigger rotation by waiting 2 hours. + nexus.AdvanceTime(2 * 3600 * 1000); + + for (Node *node : nodes) + { + VerifyOrQuit(node->Get().GetCurrentKeySequence() == 1); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Manually increment key sequence counter on `router` and verify guard time prevents update on others"); + + router.Get().SetCurrentKeySequence(2, KeyManager::kForceUpdate | KeyManager::kResetGuardTimer); + + // Advance 20 minutes. Guard timer is still active on others. + nexus.AdvanceTime(20 * 60 * 1000); + + VerifyOrQuit(router.Get().GetCurrentKeySequence() == 2); + VerifyOrQuit(leader.Get().GetCurrentKeySequence() == 1); + VerifyOrQuit(child.Get().GetCurrentKeySequence() == 1); + VerifyOrQuit(reed.Get().GetCurrentKeySequence() == 1); + + Log("---------------------------------------------------------------------------------------"); + Log("Verify nodes can still communicate"); + + nexus.SendAndVerifyEchoRequest(leader, router.Get().GetMeshLocalEid(), 0, 64, kEchoTimeout); + nexus.SendAndVerifyEchoRequest(router, child.Get().GetMeshLocalEid(), 0, 64, kEchoTimeout); + + Log("---------------------------------------------------------------------------------------"); + Log("Wait for guard time to expire and verify all nodes update"); + + // Total wait of 4 hours since manual update of router. + // T+1h: others' guard timer expires, they update to 2. + // T+2h: router rotates to 3. + // T+3h: others' guard timer expires, they update to 3. + nexus.AdvanceTime(4 * 3600 * 1000); + + for (Node *node : nodes) + { + VerifyOrQuit(node->Get().GetCurrentKeySequence() >= 3); + } + + nexus.SendAndVerifyEchoRequest(leader, router.Get().GetMeshLocalEid(), 0, 64, kEchoTimeout); + nexus.SendAndVerifyEchoRequest(router, child.Get().GetMeshLocalEid(), 0, 64, kEchoTimeout); + + nexus.SaveTestInfo("test_key_rotation_guard_time.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestKeyRotationGuardTime(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/thread-cert/test_key_rotation_and_key_guard_time.py b/tests/scripts/thread-cert/test_key_rotation_and_key_guard_time.py deleted file mode 100755 index 595ffea18..000000000 --- a/tests/scripts/thread-cert/test_key_rotation_and_key_guard_time.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2024, 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 ipaddress -import unittest -import math - -import command -import config -import thread_cert - -# Test description: -# -# This test verifies key rotation and key guard time mechanisms. -# -# -# Topology: -# -# leader --- router -# | \ -# | \ -# child reed -# - -LEADER = 1 -CHILD = 2 -REED = 3 -ROUTER = 4 - - -class MleMsgKeySeqJump(thread_cert.TestCase): - USE_MESSAGE_FACTORY = False - SUPPORT_NCP = False - - TOPOLOGY = { - LEADER: { - 'name': 'LEADER', - 'mode': 'rdn', - }, - CHILD: { - 'name': 'CHILD', - 'is_mtd': True, - 'mode': 'rn', - }, - REED: { - 'name': 'REED', - 'mode': 'rn' - }, - ROUTER: { - 'name': 'ROUTER', - 'mode': 'rdn', - }, - } - - def test(self): - leader = self.nodes[LEADER] - child = self.nodes[CHILD] - reed = self.nodes[REED] - router = self.nodes[ROUTER] - - nodes = [leader, child, reed, router] - - #------------------------------------------------------------------- - # Form the network. - - for node in nodes: - node.set_key_sequence_counter(0) - - leader.start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(leader.get_state(), 'leader') - - child.start() - reed.start() - self.simulator.go(5) - self.assertEqual(child.get_state(), 'child') - self.assertEqual(reed.get_state(), 'child') - - router.start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - self.assertEqual(router.get_state(), 'router') - - #------------------------------------------------------------------- - # Validate the initial key seq counter and key switch guard time - - for node in nodes: - self.assertEqual(node.get_key_sequence_counter(), 0) - self.assertEqual(node.get_key_switch_guardtime(), 624) - - #------------------------------------------------------------------- - # Change the key rotation time a bunch of times and make sure that - # the key switch guard time is properly changed (should be set - # to 93% of the rotation time). - - for rotation_time in [100, 1, 10, 888, 2]: - reed.start_dataset_updater(security_policy=[rotation_time, 'onrc']) - guardtime = math.floor(rotation_time * 93 / 100) if rotation_time >= 2 else 1 - self.simulator.go(100) - for node in nodes: - self.assertEqual(node.get_key_switch_guardtime(), guardtime) - - #------------------------------------------------------------------- - # Wait for key rotation time (2 hours) and check that all nodes - # moved to the next key seq counter - - self.simulator.go(2 * 60 * 60) - for node in nodes: - self.assertEqual(node.get_key_sequence_counter(), 1) - - #------------------------------------------------------------------- - # Manually increment the key sequence counter on leader and make - # sure other nodes are not updated due to key guard time. - - router.set_key_sequence_counter(2) - - self.simulator.go(50 * 60) - - self.assertEqual(router.get_key_sequence_counter(), 2) - - for node in [leader, reed, child]: - self.assertEqual(node.get_key_sequence_counter(), 1) - - #------------------------------------------------------------------- - # Make sure nodes can communicate with each other. - - self.assertTrue(leader.ping(router.get_mleid())) - self.assertTrue(router.ping(child.get_mleid())) - - #------------------------------------------------------------------- - # Wait for rotation time to expire. Validate that the `router` - # has moved to key seq `3` and all other nodes also followed. - - self.simulator.go(75 * 60) - - self.assertEqual(router.get_key_sequence_counter(), 3) - - for node in nodes: - self.assertEqual(node.get_key_sequence_counter(), 3) - - self.assertTrue(leader.ping(router.get_mleid())) - self.assertTrue(router.ping(child.get_mleid())) - - -if __name__ == '__main__': - unittest.main()