From f0cc2809cc39e6cad60f25157c34b18c9e36589e Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Mon, 6 Apr 2026 15:39:31 -0700 Subject: [PATCH] [nexus] implement 1.4 Thread Administration Sharing test (1_4_CS_TC_3) (#12832) This commit implements the Thread 1.4 Credential Sharing (CS) TC-3 Nexus test, focusing on Thread Administration Sharing using ePSKc. The test case covers mDNS discovery, ePSKc generation/validation, and DTLS secure transport for TMF message exchange. Detailed changes: - Implement full 26-step Nexus test in tests/nexus/test_1_4_CS_TC_3.cpp covering the following procedures: - mDNS discovery of meshcop and meshcop-e service instances. - Validation of State Bitmap (sb) and other TXT record fields. - Generation and Verhoeff-based validation of One-Time Passcodes. - DTLS handshakes using correct and incorrect ePSKc values. - MGMT_ACTIVE_GET and MGMT_PENDING_GET request/response exchanges over the secure DTLS session. - Testing of ephemeral key expiration and max connection attempts. - Add Python-based packet verification script in tests/nexus/verify_1_4_CS_TC_3.py using pktverify to ensure protocol compliance. - Register the 1_4_CS_TC_3 test in tests/nexus/CMakeLists.txt. - Add 1_4_CS_TC_3 to the default test list in run_nexus_tests.sh. --- tests/nexus/CMakeLists.txt | 1 + tests/nexus/run_nexus_tests.sh | 1 + tests/nexus/test_1_4_CS_TC_3.cpp | 917 ++++++++++++++++++++++++++++++ tests/nexus/verify_1_4_CS_TC_3.py | 393 +++++++++++++ 4 files changed, 1312 insertions(+) create mode 100644 tests/nexus/test_1_4_CS_TC_3.cpp create mode 100644 tests/nexus/verify_1_4_CS_TC_3.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 8d63c34f4..9904ec2e8 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -293,6 +293,7 @@ ot_nexus_test(1_4_TREL_TC_4 "cert;nexus") ot_nexus_test(1_4_TREL_TC_5 "cert;nexus") ot_nexus_test(1_4_TREL_TC_6 "cert;nexus") ot_nexus_test(1_4_DNS_TC_1 "cert;nexus") +ot_nexus_test(1_4_CS_TC_3 "cert;nexus") # Misc tests ot_nexus_test(border_admitter "core;nexus") diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index 0cc36ee9e..6d4f65f4c 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -227,6 +227,7 @@ DEFAULT_TESTS=( "1_4_TREL_TC_5" "1_4_TREL_TC_6" "1_4_DNS_TC_1" + "1_4_CS_TC_3" ) # Use provided arguments or the default test list diff --git a/tests/nexus/test_1_4_CS_TC_3.cpp b/tests/nexus/test_1_4_CS_TC_3.cpp new file mode 100644 index 000000000..63c577222 --- /dev/null +++ b/tests/nexus/test_1_4_CS_TC_3.cpp @@ -0,0 +1,917 @@ +/* + * 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 + +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" +#include "thread/uri_paths.hpp" +#include "utils/verhoeff_checksum.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 = 50 * Time::kOneSecondInMsec; + +/** + * Time to advance for the DUT to connect to the network, in milliseconds. + */ +static constexpr uint32_t kAttachTime = 10 * Time::kOneSecondInMsec; + +/** + * Time to advance for mDNS discovery, in milliseconds. + */ +static constexpr uint32_t kMdnsDiscoveryTime = 5 * Time::kOneSecondInMsec; + +/** + * Time to advance for DTLS connection, in milliseconds. + */ +static constexpr uint32_t kConnectTimeout = 3 * Time::kOneSecondInMsec; + +/** + * Time to advance for TMF message exchange, in milliseconds. + */ +static constexpr uint32_t kTmfExchangeTime = 1 * Time::kOneSecondInMsec; + +/** + * Max ePSKc connection attempts. + */ +static constexpr uint8_t kMaxEpskcConnAttempts = 10; + +/** + * The filename for the DTLS key log. + */ +static const char kKeyLogFilename[] = "/tmp/test_1_4_CS_TC_3.keys"; + +/** + * The master secret length in bytes. + */ +static constexpr uint8_t kMasterSecretLength = 48; + +/** + * The random buffer length in bytes. + */ +static constexpr uint8_t kRandomBufferLength = MeshCoP::SecureTransport::kSecureTransportRandomBufferSize; + +/** + * Infrastructure interface index. + */ +static constexpr uint32_t kInfraIfIndex = 1; + +/** + * The UDP port for ePSKc DTLS session. + */ +static constexpr uint16_t kEpskcPort = 1234; + +/** + * Time to wait for DTLS retransmissions to finish (in milliseconds). + */ +static constexpr uint32_t kDtlsRetransmissionWaitTime = 60 * Time::kOneSecondInMsec; + +/** + * Time to wait for DUT state to be cleaned up after DTLS session (in milliseconds). + */ +static constexpr uint32_t kDutStateCleanTime = 100 * Time::kOneSecondInMsec; + +using EphemeralKeyManager = MeshCoP::BorderAgent::EphemeralKeyManager; +using Manager = MeshCoP::BorderAgent::Manager; + +static void HandleMbedtlsExportKeys(void *aContext, + mbedtls_ssl_key_export_type aType, + const unsigned char *aMasterSecret, + size_t aMasterSecretLen, + const unsigned char aClientRandom[kRandomBufferLength], + const unsigned char aServerRandom[kRandomBufferLength], + mbedtls_tls_prf_types aTlsPrfType) +{ + FILE *file = fopen(kKeyLogFilename, "a"); + + OT_UNUSED_VARIABLE(aContext); + OT_UNUSED_VARIABLE(aMasterSecretLen); + OT_UNUSED_VARIABLE(aServerRandom); + OT_UNUSED_VARIABLE(aTlsPrfType); + + if (file != nullptr) + { + if (aType == MBEDTLS_SSL_KEY_EXPORT_TLS12_MASTER_SECRET) + { + fprintf(file, "CLIENT_RANDOM "); + for (int i = 0; i < kRandomBufferLength; i++) + { + fprintf(file, "%02x", aClientRandom[i]); + } + fprintf(file, " "); + for (int i = 0; i < kMasterSecretLength; i++) + { + fprintf(file, "%02x", aMasterSecret[i]); + } + fprintf(file, "\n"); + } + fclose(file); + } +} + +typedef Dns::Name::Buffer DnsName; + +struct BrowseOutcome +{ + DnsName mServiceInstance; + DnsName mHostName; + uint16_t mPort; + Ip6::Address mAddress; +}; + +static Array sBrowseOutcomes; + +static void HandleBrowseCallback(otInstance *aInstance, const Dns::Multicast::Core::BrowseResult *aResult) +{ + BrowseOutcome *outcome; + + OT_UNUSED_VARIABLE(aInstance); + + outcome = sBrowseOutcomes.PushBack(); + VerifyOrQuit(outcome != nullptr); + + SuccessOrQuit(StringCopy(outcome->mServiceInstance, aResult->mServiceInstance)); +} + +static void HandleSrvCallback(otInstance *aInstance, const Dns::Multicast::Core::SrvResult *aResult) +{ + OT_UNUSED_VARIABLE(aInstance); + + for (BrowseOutcome &outcome : sBrowseOutcomes) + { + if (StringMatch(outcome.mServiceInstance, aResult->mServiceInstance)) + { + outcome.mPort = aResult->mPort; + SuccessOrQuit(StringCopy(outcome.mHostName, aResult->mHostName)); + break; + } + } +} + +static void HandleAddressCallback(otInstance *aInstance, const Dns::Multicast::Core::AddressResult *aResult) +{ + OT_UNUSED_VARIABLE(aInstance); + + for (BrowseOutcome &outcome : sBrowseOutcomes) + { + if (StringMatch(outcome.mHostName, aResult->mHostName)) + { + if (aResult->mAddressesLength > 0) + { + outcome.mAddress = AsCoreType(&aResult->mAddresses[0].mAddress); + } + break; + } + } +} + +static void DiscoverMeshcopEService(Node &aCommNode, Core &aNexus, Ip6::Address &aAddress, uint16_t &aPort) +{ + sBrowseOutcomes.Clear(); + { + Dns::Multicast::Core::Browser browser; + ClearAllBytes(browser); + browser.mServiceType = "_meshcop-e._udp"; + browser.mInfraIfIndex = kInfraIfIndex; + browser.mCallback = HandleBrowseCallback; + SuccessOrQuit(aCommNode.Get().StartBrowser(browser)); + aNexus.AdvanceTime(kMdnsDiscoveryTime); + SuccessOrQuit(aCommNode.Get().StopBrowser(browser)); + } + + VerifyOrQuit(sBrowseOutcomes.GetLength() == 1); + + { + Dns::Multicast::Core::SrvResolver srvResolver; + ClearAllBytes(srvResolver); + srvResolver.mServiceInstance = sBrowseOutcomes[0].mServiceInstance; + srvResolver.mServiceType = "_meshcop-e._udp"; + srvResolver.mInfraIfIndex = kInfraIfIndex; + srvResolver.mCallback = HandleSrvCallback; + SuccessOrQuit(aCommNode.Get().StartSrvResolver(srvResolver)); + aNexus.AdvanceTime(kMdnsDiscoveryTime); + SuccessOrQuit(aCommNode.Get().StopSrvResolver(srvResolver)); + } + + { + Dns::Multicast::Core::AddressResolver addrResolver; + ClearAllBytes(addrResolver); + addrResolver.mHostName = sBrowseOutcomes[0].mHostName; + addrResolver.mInfraIfIndex = kInfraIfIndex; + addrResolver.mCallback = HandleAddressCallback; + SuccessOrQuit(aCommNode.Get().StartIp6AddressResolver(addrResolver)); + aNexus.AdvanceTime(kMdnsDiscoveryTime); + SuccessOrQuit(aCommNode.Get().StopIp6AddressResolver(addrResolver)); + } + + aPort = sBrowseOutcomes[0].mPort; + aAddress = sBrowseOutcomes[0].mAddress; +} + +void Test1_4_CS_TC_3(void) +{ + /** + * 12.3. [1.4] [CERT] [COMPONENT] BR component creates numeric passcode and Candidate connects + * + * 12.3.1. Purpose + * This test is for automatically testing BR component DUTs. Instead of a test operator typing the passcode, the + * DUT generates a passcode and shares it via the Thread Harness API to the Harness so that it can execute the + * automated test. No UI, GUI or display is needed on the BR DUT in this case. + * In detail, the purpose is to verify that the Border Router DUT component: + * - correctly implements the Thread Credential Sharing feature by supporting the Thread Administration Sharing + * procedure + * - is able to generate a numeric Thread Administration One-Time Passcode consisting of 9 digits + * - allows an external Candidate to connect using the ePSKc that is based on the passcode + * - is able to facilitate the Candidate’s operations + * - enables access to a Candidate using the ePSKc, only once. + * + * 12.3.2. Topology + * - BR_1 – Border Router Thread Component DUT + * - Router_1 – Thread Router reference device and Leader + * - Comm_1 – IPv6 host reference device on the AIL that implements a Commissioner Candidate role. + * + * Spec Reference | V1.4 Section + * --------------------------|----------------- + * Thread Credential Sharing | 8.4.1.2.3, 8.4.9 + */ + + Core nexus; + + Node &router1 = nexus.CreateNode(); + Node &br1 = nexus.CreateNode(); + Node &comm1 = nexus.CreateNode(); + + router1.SetName("Router_1"); + br1.SetName("BR_1"); + comm1.SetName("Comm_1"); + + router1.Get().SetEnabled(false); + comm1.Get().SetEnabled(false); + + nexus.AdvanceTime(0); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote)); + + // Remove any existing key log file. + remove(kKeyLogFilename); + +#if OPENTHREAD_CONFIG_MBEDTLS_PROVIDES_SSL_KEY_EXPORT + static_cast(comm1.Get()) + .SetKeylogCallback(HandleMbedtlsExportKeys, &comm1.Get()); +#endif + + SuccessOrQuit(br1.Get().SetEnabled(true, kInfraIfIndex)); + SuccessOrQuit(comm1.Get().SetEnabled(true, kInfraIfIndex)); + + comm1.Get().Up(); + + // Wait for the interfaces to be ready and addresses to be configured. + nexus.AdvanceTime(Time::kOneSecondInMsec); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 1: Enable and form network."); + + /** + * Step 1 + * - Device: Router_1, BR_1 (DUT) + * - Description (CS-12.3): Enable and form network. The DUT is configured with the Network Key of the test + * Thread Network, so that it can connect to it. + * - Pass Criteria: The DUT MUST connect to Router_1’s network. + */ + + router1.Form(); + nexus.AdvanceTime(kFormNetworkTime); + + br1.Join(router1); + nexus.AdvanceTime(kAttachTime); + VerifyOrQuit(br1.Get().IsAttached()); + + SuccessOrQuit(comm1.Get().SetPanChannel(router1.Get().GetPanChannel())); + comm1.Get().SetPanId(router1.Get().GetPanId()); + comm1.Get().Up(); + + br1.Get().SetEnabled(true); + nexus.AdvanceTime(Time::kOneSecondInMsec); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 2: Comm_1 discovers BR DUT service."); + + /** + * Step 2 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to do mDNS Qtype=PTR discovery query for DNS-SD service type + * “_meshcop._udp.local”, to find the BR DUT service. ... Harness then verifies data in the SRV record and TXT + * record as detailed to the right. + * - Pass Criteria: + * - The DUT MUST respond with an mDNS message that contains one PTR record of type “_meshcop._udp.local” in the + * Answer section. + * - Comm_1 MUST be able to obtain the TXT record as detailed on the left. + * - In the TXT record: + * - ePSKc Support flag (Bit 11) in State Bitmap (sb) MUST be ‘1’ (ePSKc supported) + * - ‘vn’ field MUST be present and contain >= 1 characters. + * - ‘mn’ field MUST be present and contain >= 1 characters. + * - Comm_1 MUST be able to obtain the SRV record as detailed on the left. Harness logs and stores the port + * number in the SRV record for later use. + */ + + // Border Agent starts automatically when attached. + VerifyOrQuit(br1.Get().IsEnabled()); + + { + Dns::Multicast::Core::Browser browser; + ClearAllBytes(browser); + browser.mServiceType = "_meshcop._udp"; + browser.mInfraIfIndex = kInfraIfIndex; + browser.mCallback = HandleBrowseCallback; + SuccessOrQuit(comm1.Get().StartBrowser(browser)); + nexus.AdvanceTime(kMdnsDiscoveryTime); + SuccessOrQuit(comm1.Get().StopBrowser(browser)); + } + + VerifyOrQuit(sBrowseOutcomes.GetLength() == 1); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 3: BR_1 (DUT) produces passcode."); + + /** + * Step 3 + * - Device: BR_1 (DUT) + * - Description (CS-12.3): Harness instructs the DUT, via Test Harness API, to produce the Thread Administration + * One-Time Passcode. + * - Pass Criteria: Passcode is successfully retrieved. + */ + + EphemeralKeyManager::Tap tap1; + SuccessOrQuit(tap1.GenerateRandom()); + Log("Passcode 1: %s", tap1.mTap); + nexus.AddTestVar("Passcode1", tap1.mTap); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 4: Harness validates the passcode."); + + /** + * Step 4 + * - Device: Harness + * - Description (CS-12.3): Harness validates the passcode. Harness calculates the ePSKc as follows: each + * character from the passcode string is taken as an ePSKc octet. + * - Pass Criteria: + * - Harness validates the following, using its passcode validation function: + * - The passcode MUST contain only digits 0-9. + * - The passcode MUST be 9 digits long. + * - The last digit of the passcode MUST be equal to the check digit calculated by the Verhoeff algorithm on + * the first 8 digits. + */ + + SuccessOrQuit(tap1.Validate()); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 5: Comm_1 discovers meshcop-e service."); + + /** + * Step 5 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to discover a DNS-SD service type via mDNS, Qtype=PTR, name + * “_meshcop-e._udp.local”. ... Harness stores the discovered IPv6 address and port for later use; and logs it. + * - Pass Criteria: + * - The DUT MUST respond (mDNS) with a DNS-SD message that contains one PTR record of type + * “_meshcop-e._udp.local” in the Answer section. + * - Comm_1 MUST be able to obtain an IPv6 address (from AAAA record) and port (from SRV record) of the + * “meshcop-e” service as detailed on the left. + */ + + SuccessOrQuit(br1.Get().Start(tap1.mTap, 0, kEpskcPort)); + + uint16_t epskcPort; + Ip6::Address br1Addr; + DiscoverMeshcopEService(comm1, nexus, br1Addr, epskcPort); + Log("Discovered ePSKc address: %s, port: %u", br1Addr.ToString().AsCString(), epskcPort); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 6: Comm_1 connects with incorrect ePSKc."); + + /** + * Step 6 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to connect to the DUT on the address and port found in step 5. + * For the DTLS PSK, it uses as ePSKc an incorrect ePSKc which is calculated as follows: 1. Seven octets + * (b[0]…b[6]) copied from the correct ePSKc of step 4. 2. One octet (b[7]) as a random digit that differs from + * the correct ePSKc of step 4. 3. One octet (b[8]) calculated as the correct Verhoeff check digit over the + * previous 8 numeric digits. ... Harness then waits for 3 seconds. + * - Pass Criteria: + * - The DTLS handshake/connection to the DUT MUST NOT succeed: the DUT MUST reject attempt to establish a DTLS + * session. + */ + + { + char incorrectTap[EphemeralKeyManager::Tap::kLength + 1]; + char checksum; + uint8_t i = 7; + + memcpy(incorrectTap, tap1.mTap, EphemeralKeyManager::Tap::kLength + 1); + incorrectTap[i]++; + if (incorrectTap[i] > '9') + { + incorrectTap[i] = '0'; + } + incorrectTap[EphemeralKeyManager::Tap::kLength - 1] = '\0'; + SuccessOrQuit(Utils::VerhoeffChecksum::Calculate(incorrectTap, checksum)); + incorrectTap[EphemeralKeyManager::Tap::kLength - 1] = checksum; + + Ip6::SockAddr br1SockAddr; + br1SockAddr.SetAddress(br1Addr); + br1SockAddr.SetPort(epskcPort); + + SuccessOrQuit(comm1.Get().SetPsk(reinterpret_cast(incorrectTap), + EphemeralKeyManager::Tap::kLength)); + SuccessOrQuit(comm1.Get().Open(Ip6::kNetifBackbone)); + SuccessOrQuit(comm1.Get().Connect(br1SockAddr)); + + nexus.AdvanceTime(kConnectTimeout); + VerifyOrQuit(!comm1.Get().IsConnected()); + comm1.Get().Close(); + + // Wait longer to ensure all retransmissions are finished. + nexus.AdvanceTime(kDtlsRetransmissionWaitTime); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 7: Comm_1 connects with correct ePSKc."); + + /** + * Step 7 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to connect again to the DUT on the address and port found in + * step 5. For the DTLS PSK, it now uses the correct ePSKc as calculated in step 4. + * - Pass Criteria: The DTLS handshake/connection to the DUT MUST succeed. + */ + + { + Ip6::SockAddr br1SockAddr; + br1SockAddr.SetAddress(br1Addr); + br1SockAddr.SetPort(epskcPort); + + SuccessOrQuit(comm1.Get().SetPsk(reinterpret_cast(tap1.mTap), + EphemeralKeyManager::Tap::kLength)); + SuccessOrQuit(comm1.Get().Open(Ip6::kNetifBackbone)); + SuccessOrQuit(comm1.Get().Connect(br1SockAddr)); + + nexus.AdvanceTime(kConnectTimeout); + VerifyOrQuit(comm1.Get().IsConnected()); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 8: Comm_1 sends MGMT_ACTIVE_GET.req."); + + /** + * Step 8 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to send TMF message MGMT_ACTIVE_GET.req to the DUT over the + * secure DTLS session. The Get TLV is not included in the request. + * - Pass Criteria: + * - Comm_1 MUST receive the response TMF message MGMT_ACTIVE_GET.rsp over the secure DTLS session. + * - Response payload MUST contain Network Key TLV, ... + */ + + { + Coap::Message *message; + message = comm1.Get().AllocateAndInitPriorityConfirmablePostMessage(kUriActiveGet); + VerifyOrQuit(message != nullptr); + SuccessOrQuit(comm1.Get().SendMessage(*message)); + } + nexus.AdvanceTime(kTmfExchangeTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 9: Comm_1 sends MGMT_PENDING_GET.req."); + + /** + * Step 9 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to send MGMT_PENDING_GET.req to the DUT over the secure DTLS + * session. The Get TLV is not included in the request. + * - Pass Criteria: + * - Comm_1 MUST receive the TMF response message MGMT_PENDING_GET.rsp. + * - Response CoAP payload MUST be empty. + */ + + { + Coap::Message *message; + message = comm1.Get().AllocateAndInitPriorityConfirmablePostMessage(kUriPendingGet); + VerifyOrQuit(message != nullptr); + SuccessOrQuit(comm1.Get().SendMessage(*message)); + } + nexus.AdvanceTime(kTmfExchangeTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 10: Comm_1 closes the DTLS connection."); + + /** + * Step 10 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to close the Commissioner session, i.e. close the DTLS + * connection. Then, wait for 3 seconds. + * - Pass Criteria: N/A + */ + + comm1.Get().Close(); + nexus.AdvanceTime(3 * Time::kOneSecondInMsec); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 11: Comm_1 connects again (should fail)."); + + /** + * Step 11 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to connect again to the DUT on the address and port found in + * step 5. For the DTLS PSK, it uses the correct ePSKc as determined in step 4. ... + * - Pass Criteria: + * - The DTLS handshake/connection to the DUT MUST NOT succeed: the DUT MUST reject the DTLS connection attempt. + */ + + { + Ip6::SockAddr br1SockAddr; + br1SockAddr.SetAddress(br1Addr); + br1SockAddr.SetPort(epskcPort); + + SuccessOrQuit(comm1.Get().Open(Ip6::kNetifBackbone)); + SuccessOrQuit(comm1.Get().Connect(br1SockAddr)); + + nexus.AdvanceTime(kConnectTimeout); + VerifyOrQuit(!comm1.Get().IsConnected()); + comm1.Get().Close(); + + // Wait much longer to ensure all retransmissions are finished and the DUT state is clean. + // MbedTLS retransmissions can last for a while. + nexus.AdvanceTime(kDutStateCleanTime); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 12: Wait 3s and clear mDNS cache."); + + /** + * Step 12 + * - Device: Harness + * - Description (CS-12.3): Harness waits for 3 seconds. Harness clears its mDNS discovery cache. + * - Pass Criteria: N/A + */ + + nexus.AdvanceTime(3 * Time::kOneSecondInMsec); + // Nexus mDNS cache is handled by AdvanceTime usually or explicitly. + // In this test, we just clear our browse outcomes. + sBrowseOutcomes.Clear(); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 13: Comm_1 discovers meshcop-e service (should fail)."); + + /** + * Step 13 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to discover a service type via mDNS, Qtype=PTR, name + * “_meshcop-e._udp.local”. + * - Pass Criteria: The DUT MUST NOT respond with an mDNS message that contains a PTR record of type + * “_meshcop-e._udp.local” in the Answer section. + */ + + { + Dns::Multicast::Core::Browser browser; + ClearAllBytes(browser); + browser.mServiceType = "_meshcop-e._udp"; + browser.mInfraIfIndex = kInfraIfIndex; + browser.mCallback = HandleBrowseCallback; + SuccessOrQuit(comm1.Get().StartBrowser(browser)); + nexus.AdvanceTime(kMdnsDiscoveryTime); + SuccessOrQuit(comm1.Get().StopBrowser(browser)); + } + VerifyOrQuit(sBrowseOutcomes.GetLength() == 0); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 14: BR_1 (DUT) produces new passcode."); + + /** + * Step 14 + * - Device: BR_1 (DUT) + * - Description (CS-12.3): Harness instructs the DUT, via Test Harness API, to produce the Thread Administration + * One-Time Passcode. + * - Pass Criteria: Passcode is successfully retrieved. + */ + + EphemeralKeyManager::Tap tap2; + SuccessOrQuit(tap2.GenerateRandom()); + Log("Passcode 2: %s", tap2.mTap); + nexus.AddTestVar("Passcode2", tap2.mTap); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 15: Harness validates the new passcode."); + + /** + * Step 15 + * - Device: Harness + * - Description (CS-12.3): Harness validates the passcode. From the passcode, the Harness calculates the ePSKc + * using the procedure of step 4. + * - Pass Criteria: + * - Harness validates the following, using its passcode validation function: + * - The passcode MUST differ from the passcode previously obtained in step 4 + * - The passcode MUST contain only digits 0-9 + * - The passcode MUST be 9 digits long + * - The last digit of the passcode MUST be equal to the check digit calculated by the Verhoeff algorithm on + * the first 8 digits. + */ + + SuccessOrQuit(tap2.Validate()); + VerifyOrQuit(!StringMatch(tap2.mTap, tap1.mTap)); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 16: Comm_1 discovers meshcop-e service."); + + /** + * Step 16 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to discover a service type via mDNS, Qtype=PTR, name + * “_meshcop-e._udp.local”. ... Harness stores the discovered IPv6 address and port for later use; and logs these. + * - Pass Criteria: + * - The DUT MUST respond with an mDNS message that contains one PTR record of type “_meshcop-e._udp.local” in + * the Answer section. + * - Comm_1 MUST be able to obtain an IPv6 address (from AAAA record) and port (from SRV record) of the + * “meshcop-e” service as detailed on the left. + */ + + SuccessOrQuit(br1.Get().Start(tap2.mTap, 0, kEpskcPort + 1)); + + DiscoverMeshcopEService(comm1, nexus, br1Addr, epskcPort); + Log("Discovered ePSKc address: %s, port: %u", br1Addr.ToString().AsCString(), epskcPort); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 17: Comm_1 connects with correct ePSKc."); + + /** + * Step 17 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to connect to the DUT on the address and port found in step + * 16. For the DTLS PSK, it uses the correct ePSKc as calculated in step 15. + * - Pass Criteria: The DTLS handshake/connection to the DUT MUST succeed. + */ + + { + Ip6::SockAddr br1SockAddr; + br1SockAddr.SetAddress(br1Addr); + br1SockAddr.SetPort(epskcPort); + + SuccessOrQuit(comm1.Get().SetPsk(reinterpret_cast(tap2.mTap), + EphemeralKeyManager::Tap::kLength)); + SuccessOrQuit(comm1.Get().Open(Ip6::kNetifBackbone)); + SuccessOrQuit(comm1.Get().Connect(br1SockAddr)); + + nexus.AdvanceTime(kConnectTimeout); + VerifyOrQuit(comm1.Get().IsConnected()); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 18: Comm_1 sends MGMT_ACTIVE_GET.req."); + + /** + * Step 18 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs the device to send TMF message MGMT_ACTIVE_GET.req to the DUT over + * the secure DTLS session. The Get TLV is not included in the request. + * - Pass Criteria: + * - Comm_1 MUST receive the response TMF message MGMT_ACTIVE_GET.rsp over the secure DTLS session. + * - Response payload MUST contain Network Key TLV, ... + */ + + { + Coap::Message *message; + message = comm1.Get().AllocateAndInitPriorityConfirmablePostMessage(kUriActiveGet); + VerifyOrQuit(message != nullptr); + SuccessOrQuit(comm1.Get().SendMessage(*message)); + } + nexus.AdvanceTime(kTmfExchangeTime); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 19: Comm_1 closes the DTLS connection."); + + /** + * Step 19 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to close the DTLS connection. + * - Pass Criteria: N/A + */ + + comm1.Get().Close(); + nexus.AdvanceTime(3 * Time::kOneSecondInMsec); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 20: BR_1 (DUT) produces new passcode."); + + /** + * Step 20 + * - Device: BR_1 (DUT) + * - Description (CS-12.3): Harness instructs the DUT, via Test Harness API, to produce the Thread Administration + * One-Time Passcode. + * - Pass Criteria: Passcode is successfully retrieved. + */ + + EphemeralKeyManager::Tap tap3; + SuccessOrQuit(tap3.GenerateRandom()); + Log("Passcode 3: %s", tap3.mTap); + nexus.AddTestVar("Passcode3", tap3.mTap); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 21: Harness validates the new passcode."); + + /** + * Step 21 + * - Device: Harness + * - Description (CS-12.3): Harness validates the passcode. From the passcode, the Harness calculates the ePSKc + * again using the procedure of step 4. + * - Pass Criteria: + * - Harness validates the following, equal to step 4, using its passcode validation function: + * - The passcode MUST contain only digits 0-9. + * - The passcode MUST be 9 digits long. + * - The last digit of the passcode MUST be equal to the check digit calculated by the Verhoeff algorithm on + * the first 8 digits. + * - Harness also validates that the passcode MUST be different from the passcodes obtained before in steps 4 + * and 15. + */ + + SuccessOrQuit(tap3.Validate()); + VerifyOrQuit(!StringMatch(tap3.mTap, tap1.mTap)); + VerifyOrQuit(!StringMatch(tap3.mTap, tap2.mTap)); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 22: Comm_1 discovers meshcop-e service."); + + /** + * Step 22 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to discover a service type via mDNS, Qtype=PTR, name + * “_meshcop-e._udp.local”. ... Harness stores the discovered IPv6 address and port for later use; and logs these. + * - Pass Criteria: + * - DUT MUST respond with an mDNS message that contains one PTR record of type “_meshcop-e._udp.local” in the + * Answer section. + * - Comm_1 MUST be able to obtain an IPv6 address (from AAAA record) and port (from SRV record) of the + * “meshcop-e” service as detailed on the left. + */ + + SuccessOrQuit(br1.Get().Start(tap3.mTap, 0, kEpskcPort + 2)); + + DiscoverMeshcopEService(comm1, nexus, br1Addr, epskcPort); + Log("Discovered ePSKc address: %s, port: %u", br1Addr.ToString().AsCString(), epskcPort); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 23: Comm_1 connects with incorrect ePSKc 10 times."); + + /** + * Step 23 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to connect to the DUT on the address and port found in step + * 22. For the DTLS PSK, it uses an incorrect ePSKc as calculated using the procedure of step 6 applied to the + * passcode found in step 21. This connection attempt is done in total MAX_EPSKC_CONN_ATTEMPTS (10) times. Each + * time, the pass criteria are checked. Between attempts, the Harness pauses for 2 seconds. + * - Pass Criteria: + * - The DTLS handshake/connection to DUT MUST NOT succeed. + * - In all cases, the DUT MUST NOT send ICMPv6 Port Unreachable error - this way of failing is not allowed here. + * The DUT MUST respond with DTLS messages. + */ + + { + char incorrectTap[EphemeralKeyManager::Tap::kLength + 1]; + char checksum; + uint8_t i = 7; + + memcpy(incorrectTap, tap3.mTap, EphemeralKeyManager::Tap::kLength + 1); + incorrectTap[i]++; + if (incorrectTap[i] > '9') + { + incorrectTap[i] = '0'; + } + incorrectTap[EphemeralKeyManager::Tap::kLength - 1] = '\0'; + SuccessOrQuit(Utils::VerhoeffChecksum::Calculate(incorrectTap, checksum)); + incorrectTap[EphemeralKeyManager::Tap::kLength - 1] = checksum; + + Ip6::SockAddr br1SockAddr; + br1SockAddr.SetAddress(br1Addr); + br1SockAddr.SetPort(epskcPort); + + SuccessOrQuit(comm1.Get().SetPsk(reinterpret_cast(incorrectTap), + EphemeralKeyManager::Tap::kLength)); + + for (uint8_t attempt = 1; attempt <= kMaxEpskcConnAttempts; attempt++) + { + Log("Attempt %u with incorrect ePSKc.", attempt); + SuccessOrQuit(comm1.Get().Open(Ip6::kNetifBackbone)); + SuccessOrQuit(comm1.Get().Connect(br1SockAddr)); + nexus.AdvanceTime(kConnectTimeout); + VerifyOrQuit(!comm1.Get().IsConnected()); + comm1.Get().Close(); + nexus.AdvanceTime(2 * Time::kOneSecondInMsec); + } + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 24: Comm_1 connects with correct ePSKc (should fail)."); + + /** + * Step 24 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to connect again to the DUT on the address and port found in + * step 22. For the DTLS PSK, it uses the correct ePSKc as calculated in step 21. But the connection still fails + * due to the maximum number of attempts passed. + * - Pass Criteria: + * - The DTLS handshake/connection to DUT MUST NOT succeed. + * - Note: the reason that the correct ePSKc does not succeed here is that the maximum number of failed + * connection attempts has been reached. This is being tested here. + */ + + { + Ip6::SockAddr br1SockAddr; + br1SockAddr.SetAddress(br1Addr); + br1SockAddr.SetPort(epskcPort); + + SuccessOrQuit(comm1.Get().SetPsk(reinterpret_cast(tap3.mTap), + EphemeralKeyManager::Tap::kLength)); + SuccessOrQuit(comm1.Get().Open(Ip6::kNetifBackbone)); + SuccessOrQuit(comm1.Get().Connect(br1SockAddr)); + + nexus.AdvanceTime(kConnectTimeout); + VerifyOrQuit(!comm1.Get().IsConnected()); + comm1.Get().Close(); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Step 25: Comm_1 clears its mDNS discovery cache."); + + /** + * Step 25 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs device to clear its mDNS discovery cache. + * - Pass Criteria: N/A + */ + + sBrowseOutcomes.Clear(); + + Log("---------------------------------------------------------------------------------------"); + Log("Step 26: Comm_1 discovers meshcop-e service (should fail)."); + + /** + * Step 26 + * - Device: Comm_1 + * - Description (CS-12.3): Harness instructs the device to discover a service type via mDNS, Qtype=PTR, name + * “_meshcop-e._udp.local”. + * - Pass Criteria: The DUT MUST NOT respond with an mDNS message that contains a PTR record of type + * “_meshcop-e._udp.local” in the Answer section. + */ + + { + Dns::Multicast::Core::Browser browser; + ClearAllBytes(browser); + browser.mServiceType = "_meshcop-e._udp"; + browser.mInfraIfIndex = kInfraIfIndex; + browser.mCallback = HandleBrowseCallback; + SuccessOrQuit(comm1.Get().StartBrowser(browser)); + nexus.AdvanceTime(kMdnsDiscoveryTime); + SuccessOrQuit(comm1.Get().StopBrowser(browser)); + } + VerifyOrQuit(sBrowseOutcomes.GetLength() == 0); + + nexus.SaveTestInfo("test_1_4_CS_TC_3.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::Test1_4_CS_TC_3(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_1_4_CS_TC_3.py b/tests/nexus/verify_1_4_CS_TC_3.py new file mode 100644 index 000000000..80264ec22 --- /dev/null +++ b/tests/nexus/verify_1_4_CS_TC_3.py @@ -0,0 +1,393 @@ +#!/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.null_field import nullField + +# Constants +EPSKC_PORT = 1234 +ALERT_DESC_CLOSE_NOTIFY = 0 +ALERT_LEVEL_FATAL = 2 +ALERT_DESC_DECRYPT_ERROR = 51 + +MESHCOP_SERVICE = '_meshcop._udp.local' +MESHCOP_E_SERVICE = '_meshcop-e._udp.local' + + +def verify(pv): + pkts = pv.pkts + pv.summary.show() + + COMM_1_LL = 'fe80::ff:fe00:2' + BR_1_LL = 'fe80::ff:fe00:1' + BR_1_INFRA = 'fe80::ff:fe00:1' + + def get_txt_entries(p): + entries = {} + txt_list = [] + + # Try to get all values if it's a list + layer = None + if hasattr(p, 'mdns') and p.mdns: + layer = p.mdns + elif hasattr(p, 'dns') and p.dns: + layer = p.dns + + if layer: + txt_list = layer.txt + if not isinstance(txt_list, list): + txt_list = [txt_list] + + for txt in txt_list: + if isinstance(txt, nullField.__class__): + continue + # txt is now a Bytes object (hex representation) + # Find the '=' character (0x3d) + try: + eq_idx = txt.index(0x3d) + key = bytes(txt[:eq_idx]).decode() + value = txt[eq_idx + 1:] + entries[key] = value + except ValueError: + entries[bytes(txt).decode()] = None + + return entries + + # Step 1: Enable and form network. + print("Step 1: Enable and form network.") + + # Step 2: Comm_1 discovers BR DUT service. + print("Step 2: Comm_1 discovers BR DUT service.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst('ff02::fb').\ + filter(lambda p: p.udp.dstport == 5353).\ + filter(lambda p: MESHCOP_SERVICE in p.mdns.qry.name).\ + must_next() + + mdns_resp = pkts.filter_ipv6_src(BR_1_LL).\ + filter_ipv6_dst('ff02::fb').\ + filter(lambda p: p.udp.dstport == 5353).\ + filter(lambda p: MESHCOP_SERVICE in p.mdns.resp.name).\ + must_next() + + # In the TXT record: + # - ePSKc Support flag (Bit 11) in State Bitmap (sb) MUST be ‘1’ (ePSKc supported) + # - ‘vn’ field MUST be present and contain >= 1 characters. + # - ‘mn’ field MUST be present and contain >= 1 characters. + txts = get_txt_entries(mdns_resp) + assert 'sb' in txts, "sb field missing in TXT record" + sb = struct.unpack('>I', txts['sb'])[0] + assert (sb >> 11) & 1 == 1, f"ePSKc support flag (bit 11) in sb is not 1 (sb={sb:08x})" + assert 'vn' in txts and len(txts['vn']) >= 1, "vn field missing or empty in TXT record" + assert 'mn' in txts and len(txts['mn']) >= 1, "mn field missing or empty in TXT record" + + # Step 3 & 4: Passcode generation and validation. + print("Step 3 & 4: Passcode generation and validation.") + # (Validated by C++ test) + + # Step 5: Comm_1 discovers meshcop-e service. + print("Step 5: Comm_1 discovers meshcop-e service.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst('ff02::fb').\ + filter(lambda p: p.udp.dstport == 5353).\ + filter(lambda p: MESHCOP_E_SERVICE in p.mdns.qry.name).\ + must_next() + + mdns_resp = pkts.filter_ipv6_src(BR_1_LL).\ + filter_ipv6_dst('ff02::fb').\ + filter(lambda p: p.udp.dstport == 5353).\ + filter(lambda p: MESHCOP_E_SERVICE in p.mdns.resp.name).\ + must_next() + + port_step5 = mdns_resp.mdns.srv.port[0] + print(f"Step 5 Discovered port: {port_step5}") + + # Step 6: Comm_1 connects with incorrect ePSKc. + print("Step 6: Comm_1 connects with incorrect ePSKc.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step5).\ + filter(lambda p: consts.CONTENT_HANDSHAKE in p.dtls.record.content_type).\ + filter(lambda p: consts.HANDSHAKE_CLIENT_HELLO in p.dtls.handshake.type).\ + filter(lambda p: p.dtls.handshake.cookie is not nullField).\ + must_next() + + pkts.filter_ipv6_src(BR_1_INFRA).\ + filter_ipv6_dst(COMM_1_LL).\ + filter(lambda p: p.udp.srcport == port_step5).\ + filter(lambda p: consts.CONTENT_ALERT in p.dtls.record.content_type).\ + must_next() + + # Step 7: Comm_1 connects with correct ePSKc. + print("Step 7: Comm_1 connects with correct ePSKc.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step5).\ + filter(lambda p: consts.CONTENT_HANDSHAKE in p.dtls.record.content_type).\ + filter(lambda p: consts.HANDSHAKE_CLIENT_HELLO in p.dtls.handshake.type).\ + filter(lambda p: p.dtls.handshake.cookie is not nullField).\ + must_next() + + pkts.filter_ipv6_src(BR_1_INFRA).\ + filter_ipv6_dst(COMM_1_LL).\ + filter(lambda p: p.udp.srcport == port_step5).\ + filter(lambda p: consts.CONTENT_HANDSHAKE in p.dtls.record.content_type).\ + filter(lambda p: consts.HANDSHAKE_FINISHED in p.dtls.handshake.type).\ + must_next() + + # Step 8: Comm_1 sends MGMT_ACTIVE_GET.req. + print("Step 8: Comm_1 sends MGMT_ACTIVE_GET.req.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step5).\ + filter(lambda p: p.coap.opt.uri_path == ['c', 'ag']).\ + must_next() + + active_get_rsp = pkts.filter_ipv6_src(BR_1_INFRA).\ + filter_ipv6_dst(COMM_1_LL).\ + filter(lambda p: p.udp.srcport == port_step5).\ + filter(lambda p: p.coap.type == 2).\ + must_next() + + # Response payload MUST contain Network Key, Ext PAN ID, Network Name, PAN ID, PSKc, Security Policy, + # Active Timestamp, Mesh-Local Prefix, Channel, Channel Mask. + # Note: pktverify uses 'master_key' for Network Key TLV. + active_get_rsp.must_verify(lambda p: all( + p.coap.tlv.has(field) for field in [ + 'network_key', + 'ext_pan_id', + 'network_name', + 'pan_id', + 'pskc', + 'security_policy', + 'active_timestamp', + 'mesh_local_prefix', + 'channel', + 'channel_mask', + ])) + + # Step 9: Comm_1 sends MGMT_PENDING_GET.req. + print("Step 9: Comm_1 sends MGMT_PENDING_GET.req.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step5).\ + filter(lambda p: p.coap.opt.uri_path == ['c', 'pg']).\ + must_next() + + pending_get_rsp = pkts.filter_ipv6_src(BR_1_INFRA).\ + filter_ipv6_dst(COMM_1_LL).\ + filter(lambda p: p.udp.srcport == port_step5).\ + filter(lambda p: p.coap.type == 2).\ + must_next() + + # Response CoAP payload MUST be empty. + assert pending_get_rsp.coap.payload is nullField, "MGMT_PENDING_GET response payload is not empty" + + # Step 10: Comm_1 closes the DTLS connection. + print("Step 10: Comm_1 closes the DTLS connection.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step5).\ + filter(lambda p: consts.CONTENT_ALERT in p.dtls.record.content_type).\ + must_next() + + # Step 11: Comm_1 connects again (should fail). + print("Step 11: Comm_1 connects again (should fail).") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step5).\ + filter(lambda p: consts.CONTENT_HANDSHAKE in p.dtls.record.content_type).\ + filter(lambda p: consts.HANDSHAKE_CLIENT_HELLO in p.dtls.handshake.type).\ + must_next() + + # The DUT may either send an ALERT or simply not respond (since ephemeral key is stopped). + # We check if there's an ALERT, but don't fail if there isn't one as long as connection doesn't succeed. + pkts.copy().filter_ipv6_src(BR_1_INFRA).\ + filter_ipv6_dst(COMM_1_LL).\ + filter(lambda p: p.udp.srcport == port_step5).\ + filter(lambda p: consts.CONTENT_HANDSHAKE in p.dtls.record.content_type).\ + filter(lambda p: consts.HANDSHAKE_FINISHED in p.dtls.handshake.type).\ + must_not_next() + + # Step 13: Comm_1 discovers meshcop-e service (should fail). + print("Step 13: Comm_1 discovers meshcop-e service (should fail).") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst('ff02::fb').\ + filter(lambda p: p.udp.dstport == 5353).\ + filter(lambda p: MESHCOP_E_SERVICE in p.mdns.qry.name).\ + must_next() + + # Step 16: Comm_1 discovers meshcop-e service. + print("Step 16: Comm_1 discovers meshcop-e service.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst('ff02::fb').\ + filter(lambda p: p.udp.dstport == 5353).\ + filter(lambda p: MESHCOP_E_SERVICE in p.mdns.qry.name).\ + must_next() + + mdns_resp = pkts.filter_ipv6_src(BR_1_LL).\ + filter_ipv6_dst('ff02::fb').\ + filter(lambda p: p.udp.dstport == 5353).\ + filter(lambda p: MESHCOP_E_SERVICE in p.mdns.resp.name).\ + must_next() + + port_step16 = mdns_resp.mdns.srv.port[0] + print(f"Step 16 Discovered port: {port_step16}") + + # Step 17: Comm_1 connects with correct ePSKc. + print("Step 17: Comm_1 connects with correct ePSKc.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step16).\ + filter(lambda p: consts.CONTENT_HANDSHAKE in p.dtls.record.content_type).\ + filter(lambda p: consts.HANDSHAKE_CLIENT_HELLO in p.dtls.handshake.type).\ + filter(lambda p: p.dtls.handshake.cookie is not nullField).\ + must_next() + + pkts.filter_ipv6_src(BR_1_INFRA).\ + filter_ipv6_dst(COMM_1_LL).\ + filter(lambda p: p.udp.srcport == port_step16).\ + filter(lambda p: consts.CONTENT_HANDSHAKE in p.dtls.record.content_type).\ + filter(lambda p: consts.HANDSHAKE_FINISHED in p.dtls.handshake.type).\ + must_next() + + # Step 18: Comm_1 sends MGMT_ACTIVE_GET.req. + print("Step 18: Comm_1 sends MGMT_ACTIVE_GET.req.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step16).\ + filter(lambda p: p.coap.opt.uri_path == ['c', 'ag']).\ + must_next() + + active_get_rsp = pkts.filter_ipv6_src(BR_1_INFRA).\ + filter_ipv6_dst(COMM_1_LL).\ + filter(lambda p: p.udp.srcport == port_step16).\ + filter(lambda p: p.coap.type == 2).\ + must_next() + + active_get_rsp.must_verify(lambda p: all( + p.coap.tlv.has(field) for field in [ + 'network_key', + 'ext_pan_id', + 'network_name', + 'pan_id', + 'pskc', + 'security_policy', + 'active_timestamp', + 'mesh_local_prefix', + 'channel', + 'channel_mask', + ])) + + # Step 19: Comm_1 closes the DTLS connection. + print("Step 19: Comm_1 closes the DTLS connection.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step16).\ + filter(lambda p: consts.CONTENT_ALERT in p.dtls.record.content_type).\ + must_next() + + # Step 22: Comm_1 discovers meshcop-e service. + print("Step 22: Comm_1 discovers meshcop-e service.") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst('ff02::fb').\ + filter(lambda p: p.udp.dstport == 5353).\ + filter(lambda p: MESHCOP_E_SERVICE in p.mdns.qry.name).\ + must_next() + + mdns_resp = pkts.filter_ipv6_src(BR_1_LL).\ + filter_ipv6_dst('ff02::fb').\ + filter(lambda p: p.udp.dstport == 5353).\ + filter(lambda p: MESHCOP_E_SERVICE in p.mdns.resp.name).\ + must_next() + + port_step22 = mdns_resp.mdns.srv.port[0] + print(f"Step 22 Discovered port: {port_step22}") + + # Step 23: Comm_1 connects with incorrect ePSKc 10 times. + print("Step 23: Comm_1 connects with incorrect ePSKc 10 times.") + for i in range(10): + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step22).\ + filter(lambda p: consts.CONTENT_HANDSHAKE in p.dtls.record.content_type).\ + filter(lambda p: consts.HANDSHAKE_CLIENT_HELLO in p.dtls.handshake.type).\ + filter(lambda p: p.dtls.handshake.cookie is not nullField).\ + must_next() + + pkts.filter_ipv6_src(BR_1_INFRA).\ + filter_ipv6_dst(COMM_1_LL).\ + filter(lambda p: p.udp.srcport == port_step22).\ + filter(lambda p: consts.CONTENT_ALERT in p.dtls.record.content_type).\ + must_next() + + # Step 24: Comm_1 connects with correct ePSKc (should fail). + print("Step 24: Comm_1 connects with correct ePSKc (should fail).") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst(BR_1_INFRA).\ + filter(lambda p: p.udp.dstport == port_step22).\ + filter(lambda p: consts.CONTENT_HANDSHAKE in p.dtls.record.content_type).\ + filter(lambda p: consts.HANDSHAKE_CLIENT_HELLO in p.dtls.handshake.type).\ + must_next() + + # Similar to Step 11, the DUT may either send an ALERT or not respond. + pkts.copy().filter_ipv6_src(BR_1_INFRA).\ + filter_ipv6_dst(COMM_1_LL).\ + filter(lambda p: p.udp.srcport == port_step22).\ + filter(lambda p: consts.CONTENT_HANDSHAKE in p.dtls.record.content_type).\ + filter(lambda p: consts.HANDSHAKE_FINISHED in p.dtls.handshake.type).\ + must_not_next() + + # Step 26: Comm_1 discovers meshcop-e service (should fail). + print("Step 26: Comm_1 discovers meshcop-e service (should fail).") + pkts.filter_ipv6_src(COMM_1_LL).\ + filter_ipv6_dst('ff02::fb').\ + filter(lambda p: p.udp.dstport == 5353).\ + filter(lambda p: MESHCOP_E_SERVICE in p.mdns.qry.name).\ + must_next() + + +if __name__ == '__main__': + # Add keylog file to Wireshark preferences if it exists. + keylog_file = '/tmp/test_1_4_CS_TC_3.keys' + if os.path.exists(keylog_file): + consts.WIRESHARK_OVERRIDE_PREFS['tls.keylog_file'] = keylog_file + for port in [1234, 1235, 1236]: + consts.WIRESHARK_DECODE_AS_ENTRIES[f'udp.port=={port}'] = 'dtls' + consts.WIRESHARK_DECODE_AS_ENTRIES[f'dtls.port=={port}'] = 'coap' + verify_utils.run_main(verify)