[nexus] add SRPC-TC-7 for SRP client key persistence (#12788)

This commit adds the Nexus test case 1_3_SRPC_TC_7 which verifies
that a Thread device re-registers its service with the same KEY
record after a reboot, as per the Thread 1.3 test specification.

The implementation includes:
- tests/nexus/test_1_3_SRPC_TC_7.cpp: Executes the test sequence
  by forming a Thread network with a Border Router (BR_1), a Router,
  and a DUT (TD_1). It registers a service on the DUT, simulates a
  reboot using Node::Reset(), and re-registers the same service.
- tests/nexus/verify_1_3_SRPC_TC_7.py: Performs automated verification
  of the captured traffic. It ensures that the SRP Update sent after
  reboot contains a KEY record identical to the one sent before
  reboot. It includes a monkey-patch to access the
  dns.key.public_key field in the packet verifier.
- Integrated the new test into tests/nexus/CMakeLists.txt and
  tests/nexus/run_nexus_tests.sh.

The test validates that the SRP client correctly persists its key
material across reboots, which is essential for maintaining service
registration continuity.
This commit is contained in:
Jonathan Hui
2026-03-29 20:14:56 -07:00
committed by GitHub
parent 1b20885508
commit 9bb7e37ff9
4 changed files with 402 additions and 0 deletions
+1
View File
@@ -278,6 +278,7 @@ ot_nexus_test(1_3_SRP_TC_15 "cert;nexus")
ot_nexus_test(1_3_SRPC_TC_1 "cert;nexus")
ot_nexus_test(1_3_SRPC_TC_4 "cert;nexus")
ot_nexus_test(1_3_SRPC_TC_5 "cert;nexus")
ot_nexus_test(1_3_SRPC_TC_7 "cert;nexus")
# Misc tests
ot_nexus_test(border_admitter "core;nexus")
+1
View File
@@ -213,6 +213,7 @@ DEFAULT_TESTS=(
"1_3_SRPC_TC_1"
"1_3_SRPC_TC_4"
"1_3_SRPC_TC_5"
"1_3_SRPC_TC_7"
)
# Use provided arguments or the default test list
+247
View File
@@ -0,0 +1,247 @@
/*
* 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 a network, in milliseconds.
*/
static constexpr uint32_t kJoinNetworkTime = 10 * 1000;
/**
* Time to advance for the network to stabilize, in milliseconds.
*/
static constexpr uint32_t kStabilizationTime = 10 * 1000;
/**
* Time to wait for SRP registration to complete.
*/
static constexpr uint32_t kSrpRegistrationTime = 5 * 1000;
/**
* Infrastructure interface index.
*/
static constexpr uint32_t kInfraIfIndex = 1;
/**
* SRP Lease time in seconds.
*/
static constexpr uint32_t kSrpLease1h = 3600;
/**
* SRP Key Lease time in seconds.
*/
static constexpr uint32_t kSrpKeyLease1d = 86400;
/**
* SRP service port.
*/
static constexpr uint16_t kSrpServicePort = 33333;
/**
* SRP service and host names.
*/
static const char kSrpServiceType[] = "_thread-test._udp";
static const char kSrpInstanceName[] = "service-test-1";
static const char kSrpHostName[] = "host-test-1";
void Test_1_3_SRPC_TC_7(const char *aJsonFileName)
{
/**
* 3.7. [1.3] [CERT] [COMPONENT] Thread Device Reboots - Re-registers service with same KEY
*
* 3.7.1. Purpose
* To verify that the Thread Component DUT:
* - Is able to re-register a service again after reboot.
* - Uses the same key for signing the SRP Update, as indicated in the KEY record.
*
* 3.7.2. Topology
* - BR 1 Border Router Reference Device and Leader
* - Router 1-Thread Router reference device
* - TD_1 (DUT)-Any Thread Device (FTD or MTD): Thread Component DUT
* - Eth 1-IPv6 host reference device on the AIL
*
* Spec Reference | V1.1 Section | V1.3.0 Section
* -----------------|--------------|---------------
* SRP Client | N/A | 2.3.2
*/
Core nexus;
Node &br1 = nexus.CreateNode();
Node &router1 = nexus.CreateNode();
Node &td1 = nexus.CreateNode();
Node &eth1 = nexus.CreateNode();
br1.SetName("BR_1");
router1.SetName("Router_1");
td1.SetName("TD_1");
eth1.SetName("Eth_1");
SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote));
Log("Step 1: Enable & form topology");
/**
* Step 1
* - Device: Eth 1, BR 1, Router 1, TD 1
* - Description (SRPC-3.7): Enable & form topology
* - Pass Criteria:
* - Single Thread Network is formed
*/
br1.Form();
nexus.AdvanceTime(kFormNetworkTime);
router1.Join(br1);
nexus.AdvanceTime(kJoinNetworkTime);
td1.Join(br1, Node::kAsFed);
nexus.AdvanceTime(kJoinNetworkTime);
SuccessOrQuit(eth1.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
br1.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
br1.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().SetEnabled(true));
SuccessOrQuit(br1.Get<Srp::Server>().SetAddressMode(Srp::Server::kAddressModeUnicast));
br1.Get<Srp::Server>().SetEnabled(true);
nexus.AdvanceTime(kStabilizationTime);
nexus.AddTestVar("BR_1_MLEID_ADDR", br1.Get<Mle::Mle>().GetMeshLocalEid().ToString().AsCString());
nexus.AddTestVar("TD_1_MLEID_ADDR", td1.Get<Mle::Mle>().GetMeshLocalEid().ToString().AsCString());
nexus.AddTestVar("TD_1_OMR_ADDR", td1.FindGlobalAddress().ToString().AsCString());
String<6> portString;
portString.Append("%u", br1.Get<Srp::Server>().GetPort());
nexus.AddTestVar("BR_1_SRP_PORT", portString.AsCString());
Log("Step 2: Harness instructs the DUT to register the service");
/**
* Step 2
* - Device: TD 1 (DUT)
* - Description (SRPC-3.7): Harness instructs the DUT to register the service: $ORIGIN default.service.arpa.
* service-test-1._thread-test._udp ( SRV 33333 host-test-1 ) host-test-1 AAAA <OMR address of TD_1> with the
* following options: Update Lease Option Lease: 60 minutes Key Lease: 1 day
* - Pass Criteria:
* - The DUT MUST send an SRP Update to BR_1
* - BR_1 MUST respond Rcode=0 (NoError).
*/
td1.Get<Srp::Client>().EnableAutoStartMode(nullptr, nullptr);
SuccessOrQuit(td1.Get<Srp::Client>().SetHostName(kSrpHostName));
SuccessOrQuit(td1.Get<Srp::Client>().EnableAutoHostAddress());
td1.Get<Srp::Client>().SetLeaseInterval(kSrpLease1h);
td1.Get<Srp::Client>().SetKeyLeaseInterval(kSrpKeyLease1d);
{
Srp::Client::Service service;
ClearAllBytes(service);
service.mName = kSrpServiceType;
service.mInstanceName = kSrpInstanceName;
service.mPort = kSrpServicePort;
SuccessOrQuit(td1.Get<Srp::Client>().AddService(service));
}
nexus.AdvanceTime(kSrpRegistrationTime);
VerifyOrQuit(td1.Get<Srp::Client>().GetHostInfo().GetState() == Srp::Client::kRegistered);
Log("Step 3: Harness instructs device to reboot.");
/**
* Step 3
* - Device: TD 1 (DUT)
* - Description (SRPC-3.7): Harness instructs device to reboot.
* - Pass Criteria:
* - N/A
*/
td1.Reset();
nexus.AdvanceTime(kStabilizationTime);
Log("Step 4: (Repeat step 2)");
/**
* Step 4
* - Device: TD 1 (DUT)
* - Description (SRPC-3.7): (Repeat step 2)
* - Pass Criteria:
* - The DUT MUST send an SRP Update to BR_1
* - The KEY record in the SRP Update MUST have an equal value to the KEY record that was sent in step 2
* - BR_1 MUST respond Rcode=0 (NoError).
*/
td1.Join(br1, Node::kAsFed);
nexus.AdvanceTime(kJoinNetworkTime);
td1.Get<Srp::Client>().EnableAutoStartMode(nullptr, nullptr);
SuccessOrQuit(td1.Get<Srp::Client>().SetHostName(kSrpHostName));
SuccessOrQuit(td1.Get<Srp::Client>().EnableAutoHostAddress());
td1.Get<Srp::Client>().SetLeaseInterval(kSrpLease1h);
td1.Get<Srp::Client>().SetKeyLeaseInterval(kSrpKeyLease1d);
{
Srp::Client::Service service;
ClearAllBytes(service);
service.mName = kSrpServiceType;
service.mInstanceName = kSrpInstanceName;
service.mPort = kSrpServicePort;
SuccessOrQuit(td1.Get<Srp::Client>().AddService(service));
}
nexus.AdvanceTime(kSrpRegistrationTime);
VerifyOrQuit(td1.Get<Srp::Client>().GetHostInfo().GetState() == Srp::Client::kRegistered);
nexus.SaveTestInfo(aJsonFileName);
}
} // namespace Nexus
} // namespace ot
int main(int argc, char *argv[])
{
ot::Nexus::Test_1_3_SRPC_TC_7((argc > 2) ? argv[2] : "test_1_3_SRPC_TC_7.json");
printf("All tests passed\n");
return 0;
}
+153
View File
@@ -0,0 +1,153 @@
#!/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
import struct
# 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 import layer_fields
from pktverify.addrs import Ipv6Addr
from pktverify.bytes import Bytes
# Monkey-patch dns.key.public_key field
layer_fields._LAYER_FIELDS['dns.key.public_key'] = layer_fields._list(layer_fields._bytes)
layer_fields._layer_containers.add('dns.key')
def verify(pv):
# 3.7. [1.3] [CERT] [COMPONENT] Thread Device Reboots - Re-registers service with same KEY
#
# 3.7.1. Purpose
# To verify that the Thread Component DUT:
# - Is able to re-register a service again after reboot.
# - Uses the same key for signing the SRP Update, as indicated in the KEY record.
#
# 3.7.2. Topology
# - BR 1 Border Router Reference Device and Leader
# - Router 1-Thread Router reference device
# - TD_1 (DUT)-Any Thread Device (FTD or MTD): Thread Component DUT
# - Eth 1-IPv6 host reference device on the AIL
#
# Spec Reference | V1.1 Section | V1.3.0 Section
# -----------------|--------------|---------------
# SRP Client | N/A | 2.3.2
pkts = pv.pkts
pv.summary.show()
BR_1_MLEID = Ipv6Addr(pv.vars['BR_1_MLEID_ADDR'])
TD_1_MLEID = Ipv6Addr(pv.vars['TD_1_MLEID_ADDR'])
TD_1_OMR_ADDR = Ipv6Addr(pv.vars['TD_1_OMR_ADDR'])
BR_1_SRP_PORT = int(pv.vars['BR_1_SRP_PORT'])
SRP_SERVICE_NAME = '_thread-test._udp.default.service.arpa'
SRP_INSTANCE_NAME = 'service-test-1.' + SRP_SERVICE_NAME
SRP_SERVICE_PORT = 33333
SRP_LEASE_1H_1D = Bytes(struct.pack('>II', 3600, 86400).hex())
# Step 1
# - Device: Eth 1, BR 1, Router 1, TD 1
# - Description (SRPC-3.7): Enable & form topology
# - Pass Criteria:
# - Single Thread Network is formed
print("Step 1: Enable & form topology")
# Step 2
# - Device: TD 1 (DUT)
# - Description (SRPC-3.7): Harness instructs the DUT to register the service: $ORIGIN default.service.arpa.
# service-test-1._thread-test._udp ( SRV 33333 host-test-1 ) host-test-1 AAAA <OMR address of TD_1> with the
# following options: Update Lease Option Lease: 60 minutes Key Lease: 1 day
# - Pass Criteria:
# - The DUT MUST send an SRP Update to BR_1
# - BR_1 MUST respond Rcode=0 (NoError).
print("Step 2: Harness instructs the DUT to register the service")
update1 = pkts.filter_ipv6_dst(BR_1_MLEID).\
filter_ipv6_src(TD_1_MLEID).\
filter(lambda p: p.udp.dstport == BR_1_SRP_PORT).\
filter(lambda p: p.dns.flags.opcode == consts.DNS_OPCODE_UPDATE).\
filter(lambda p: SRP_INSTANCE_NAME in verify_utils.as_list(p.dns.resp.name)).\
filter(lambda p: SRP_SERVICE_PORT in verify_utils.as_list(p.dns.srv.port)).\
filter(lambda p: TD_1_OMR_ADDR in verify_utils.as_list(p.dns.aaaa)).\
filter(lambda p: p.dns.opt.data is not None).\
filter(lambda p: any(data.format_compact() == SRP_LEASE_1H_1D.format_compact() for data in p.dns.opt.data)).\
must_next()
pkts.filter_ipv6_src(BR_1_MLEID).\
filter_ipv6_dst(TD_1_MLEID).\
filter(lambda p: p.udp.srcport == BR_1_SRP_PORT).\
filter(lambda p: p.dns.flags.response == 1).\
filter(lambda p: p.dns.flags.rcode == consts.DNS_RCODE_NOERROR).\
must_next()
key1 = update1.dns.key.public_key
# Step 3
# - Device: TD 1 (DUT)
# - Description (SRPC-3.7): Harness instructs device to reboot.
# - Pass Criteria:
# - N/A
print("Step 3: Harness instructs device to reboot.")
# Step 4
# - Device: TD 1 (DUT)
# - Description (SRPC-3.7): (Repeat step 2)
# - Pass Criteria:
# - The DUT MUST send an SRP Update to BR_1
# - The KEY record in the SRP Update MUST have an equal value to the KEY record that was sent in step 2
# - BR_1 MUST respond Rcode=0 (NoError).
print("Step 4: (Repeat step 2)")
update2 = pkts.filter_ipv6_dst(BR_1_MLEID).\
filter_ipv6_src(TD_1_MLEID).\
filter(lambda p: p.udp.dstport == BR_1_SRP_PORT).\
filter(lambda p: p.dns.flags.opcode == consts.DNS_OPCODE_UPDATE).\
filter(lambda p: SRP_INSTANCE_NAME in verify_utils.as_list(p.dns.resp.name)).\
filter(lambda p: SRP_SERVICE_PORT in verify_utils.as_list(p.dns.srv.port)).\
filter(lambda p: TD_1_OMR_ADDR in verify_utils.as_list(p.dns.aaaa)).\
filter(lambda p: p.dns.opt.data is not None).\
filter(lambda p: any(data.format_compact() == SRP_LEASE_1H_1D.format_compact() for data in p.dns.opt.data)).\
filter(lambda p: p.dns.key.public_key == key1).\
must_next()
pkts.filter_ipv6_src(BR_1_MLEID).\
filter_ipv6_dst(TD_1_MLEID).\
filter(lambda p: p.udp.srcport == BR_1_SRP_PORT).\
filter(lambda p: p.dns.flags.response == 1).\
filter(lambda p: p.dns.flags.rcode == consts.DNS_RCODE_NOERROR).\
must_next()
if __name__ == '__main__':
verify_utils.run_main(verify)