/* * 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. */ #include "openthread-core-config.h" #include "test_platform.h" #include "test_util.h" #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE #include #include "instance/instance.hpp" #define OT_TCAT_X509_CERT \ "-----BEGIN CERTIFICATE-----\n" \ "MIIB6TCCAZCgAwIBAgICNekwCgYIKoZIzj0EAwIwcTEmMCQGA1UEAwwdVGhyZWFk\n" \ "IENlcnRpZmljYXRpb24gRGV2aWNlQ0ExGTAXBgNVBAoMEFRocmVhZCBHcm91cCBJ\n" \ "bmMxEjAQBgNVBAcMCVNhbiBSYW1vbjELMAkGA1UECAwCQ0ExCzAJBgNVBAYTAlVT\n" \ "MCAXDTI0MDUwNzA5Mzk0NVoYDzI5OTkxMjMxMDkzOTQ1WjA8MSEwHwYDVQQDDBhU\n" \ "Q0FUIEV4YW1wbGUgRGV2aWNlQ2VydDExFzAVBgNVBAUTDjQ3MjMtOTgzMy0wMDAx\n" \ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE11h/4vKZXVXv+1GDZo066spItloT\n" \ "dpCi0bux0jvpQSHLdQBIc+40zVCxMDRUvbX//vJKGsSJKOVUlCojQ2wIdqNLMEkw\n" \ "HwYDVR0jBBgwFoAUX6sbKWiIodS0MaiGYefnZlnt+BkwEAYJKwYBBAGC3yoCBAMC\n" \ "AQUwFAYJKwYBBAGC3yoDBAcEBSABAQEBMAoGCCqGSM49BAMCA0cAMEQCIHWu+Rd1\n" \ "VRlzrD8KbuyJcJFTXh2sQ9UIrFIA7+4e/GVcAiAVBdGqTxbt3TGkBBllpafAUB2/\n" \ "s0GJj7E33oblqy5eHQ==\n" \ "-----END CERTIFICATE-----\n" #define OT_TCAT_PRIV_KEY \ "-----BEGIN EC PRIVATE KEY-----\n" \ "MHcCAQEEIIqKM1QTlNaquV74W6Viz/ggXoLqlPOP6LagSyaFO3oUoAoGCCqGSM49\n" \ "AwEHoUQDQgAE11h/4vKZXVXv+1GDZo066spItloTdpCi0bux0jvpQSHLdQBIc+40\n" \ "zVCxMDRUvbX//vJKGsSJKOVUlCojQ2wIdg==\n" \ "-----END EC PRIVATE KEY-----\n" #define OT_TCAT_TRUSTED_ROOT_CERTIFICATE \ "-----BEGIN CERTIFICATE-----\n" \ "MIICOzCCAeGgAwIBAgIJAKOc2hehOGoBMAoGCCqGSM49BAMCMHExJjAkBgNVBAMM\n" \ "HVRocmVhZCBDZXJ0aWZpY2F0aW9uIERldmljZUNBMRkwFwYDVQQKDBBUaHJlYWQg\n" \ "R3JvdXAgSW5jMRIwEAYDVQQHDAlTYW4gUmFtb24xCzAJBgNVBAgMAkNBMQswCQYD\n" \ "VQQGEwJVUzAeFw0yNDA1MDMyMDAyMThaFw00NDA0MjgyMDAyMThaMHExJjAkBgNV\n" \ "BAMMHVRocmVhZCBDZXJ0aWZpY2F0aW9uIERldmljZUNBMRkwFwYDVQQKDBBUaHJl\n" \ "YWQgR3JvdXAgSW5jMRIwEAYDVQQHDAlTYW4gUmFtb24xCzAJBgNVBAgMAkNBMQsw\n" \ "CQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGy850VBIPTkN3oL\n" \ "x++zIUsZk2k26w4fuieFz9oNvjdb5W14+Yf3mvGWsl4NHyLxqhmamVAR4h7zWRlZ\n" \ "0XyMVpKjYjBgMB4GA1UdEQQXMBWBE3RvbUB0aHJlYWRncm91cC5vcmcwDgYDVR0P\n" \ "AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFF+rGyloiKHUtDGo\n" \ "hmHn52ZZ7fgZMAoGCCqGSM49BAMCA0gAMEUCIQCTq1qjPZs9fAJB6ppTXs588Pnu\n" \ "eVFOwC8bd//D99KiHAIgU84kwFHIyDvFqu6y+u1hFqBGsiuTmKwZ2PHhVe/xK1k=\n" \ "-----END CERTIFICATE-----\n" #define COMM_NETWORK_NAME "OpenThread-c64e" #define COMM_XPAN_ID {0xde, 0xad, 0x00, 0xbe, 0xef, 0x00, 0xca, 0xfe} #define COMM_XPAN_ID_ALT {0xef, 0x13, 0x98, 0xc2, 0xfd, 0x50, 0x4b, 0x67} // Fake time/alarm platform, overriding the weak definitions in test_platform.cpp. Time only // progresses when a test calls AdvanceTime(). static uint32_t sNow = 0; static uint32_t sAlarmTime; static bool sAlarmOn = false; extern "C" { void otPlatAlarmMilliStop(otInstance *) { sAlarmOn = false; } void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt) { sAlarmOn = true; sAlarmTime = aT0 + aDt; } uint32_t otPlatAlarmMilliGetNow(void) { return sNow; } // Override the weak test platform stub to prevent buffer NULL dereference during the tests. otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *) { static otRadioFrame sTxFrame; static uint8_t sTxPsdu[OT_RADIO_FRAME_MAX_SIZE]; sTxFrame.mPsdu = sTxPsdu; return &sTxFrame; } } // extern "C" namespace ot { namespace MeshCoP { static constexpr char kPskdVendor[] = "J01NM3"; static constexpr char kUrl[] = "dummy_url"; static constexpr char kDomainName[] = "DefaultDomain"; static constexpr char kNetworkName[] = COMM_NETWORK_NAME; static constexpr char kWrongName[] = "WrongName"; static const uint8_t kExtPanId[8] = COMM_XPAN_ID; static const uint8_t kExtPanIdAlt[8] = COMM_XPAN_ID_ALT; static constexpr uint16_t kConnectionId = 0; static constexpr int kCertificateThreadVersion = 2; static constexpr int kCertificateAuthorizationField = 3; static constexpr otTcatVendorInfo vendorInfo = {.mProvisioningUrl = kUrl, .mPskdString = kPskdVendor}; // TCAT command class bits for expressing any combination of classes in tests static constexpr uint16_t kClassNone = 0; static constexpr uint16_t kClassGeneral = 1 << TcatAgent::kGeneral; static constexpr uint16_t kClassCommissioning = 1 << TcatAgent::kCommissioning; static constexpr uint16_t kClassExtraction = 1 << TcatAgent::kExtraction; static constexpr uint16_t kClassDecommissioning = 1 << TcatAgent::kDecommissioning; static constexpr uint16_t kClassApplication = 1 << TcatAgent::kApplication; // TCAT authorization fields static const uint8_t kDeviceCert1AuthField[5] = {0x20, 0x01, 0x01, 0x01, 0x01}; static const uint8_t kDeviceCert2AuthField[5] = {0x20, 0x02, 0x03, 0x04, 0x24}; static const uint8_t kCommCert1AuthField[5] = {0x21, 0x01, 0x01, 0x01, 0x01}; static const uint8_t kCommCert2AuthField[5] = {0x21, 0x1F, 0x3F, 0x3F, 0x3F}; static const uint8_t kCommCert4AuthField[5] = {0x21, 0x21, 0x05, 0x09, 0x11}; static const uint8_t kCommCert5AuthField[5] = {0x21, 0x03, 0x02, 0x83, 0x41}; static const otOperationalDataset kFullDataset = { .mActiveTimestamp = { .mSeconds = 1, .mTicks = 0, .mAuthoritative = false, }, .mNetworkKey = { .m8 = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, }, .mNetworkName = {COMM_NETWORK_NAME}, .mExtendedPanId = { .m8 = COMM_XPAN_ID, }, .mMeshLocalPrefix = { .m8 = {0xfd, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, }, .mPanId = 0x1234, .mChannel = 11, .mPskc = { .m8 = {0xc2, 0x3a, 0x76, 0xe9, 0x8f, 0x1a, 0x64, 0x83, 0x63, 0x9b, 0x1a, 0xc1, 0x27, 0x1e, 0x2e, 0x27}, }, .mSecurityPolicy = { .mRotationTime = 672, .mObtainNetworkKeyEnabled = true, .mNativeCommissioningEnabled = true, .mRoutersEnabled = true, .mExternalCommissioningEnabled = true, }, .mChannelMask = 0x07fff800, .mComponents = { .mIsActiveTimestampPresent = true, .mIsNetworkKeyPresent = true, .mIsNetworkNamePresent = true, .mIsExtendedPanIdPresent = true, .mIsMeshLocalPrefixPresent = true, .mIsPanIdPresent = true, .mIsChannelPresent = true, .mIsPskcPresent = true, .mIsSecurityPolicyPresent = true, .mIsChannelMaskPresent = true, }, }; static const otOperationalDataset kPartialDataset = { .mNetworkKey = { .m8 = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, }, .mComponents = { .mIsActiveTimestampPresent = false, .mIsNetworkKeyPresent = true, .mIsNetworkNamePresent = false, .mIsExtendedPanIdPresent = false, .mIsMeshLocalPrefixPresent = false, .mIsPanIdPresent = false, .mIsChannelPresent = false, .mIsPskcPresent = false, .mIsSecurityPolicyPresent = false, .mIsChannelMaskPresent = false, }, }; static Dataset::Info sFullDataset, sPartialDataset; static NetworkName sCommNetworkName, sCommDomainName; static ExtendedPanId sCommExtPanId; static TcatAgent::CertificateAuthorizationField sCommAuth, sDeviceAuth; // Helper class to test the BLE Secure connection state and validate the documented connect-callback contract. class TestBleSecure { public: TestBleSecure(void) : mIsConnected(false) , mIsBleConnectionOpen(false) , mConnectCallbackCount(0) , mContractHonored(true) { } void HandleBleSecureConnect(bool aConnected, bool aBleConnectionOpen) { // A TLS session cannot exist without an open BLE link to carry it. if (aConnected && !aBleConnectionOpen) { printf("TestBleSecure: illegal pair reported (aConnected=1, aBleConnectionOpen=0)\n"); mContractHonored = false; } // The callback must fire only on a change of the pair, so never twice in a row with identical values. if (aConnected == mIsConnected && aBleConnectionOpen == mIsBleConnectionOpen) { printf("TestBleSecure: pair repeated without change (aConnected=%d, aBleConnectionOpen=%d)\n", aConnected, aBleConnectionOpen); mContractHonored = false; } mIsConnected = aConnected; mIsBleConnectionOpen = aBleConnectionOpen; mConnectCallbackCount++; } // Returns TRUE if the contract has been honored by every callback so far AND the currently reported state and // the callback count (since the last reset) match the expected values. bool Verify(bool aConnected, bool aBleConnectionOpen, uint32_t aExpectedCallbackCount) const { bool ok = mContractHonored && (mIsConnected == aConnected) && (mIsBleConnectionOpen == aBleConnectionOpen) && (mConnectCallbackCount == aExpectedCallbackCount); if (!ok) { printf("TestBleSecure::Verify mismatch: got (aConnected=%d, aBleConnectionOpen=%d, count=%lu, " "contractHonored=%d), expected (aConnected=%d, aBleConnectionOpen=%d, count=%lu)\n", mIsConnected, mIsBleConnectionOpen, ToUlong(mConnectCallbackCount), mContractHonored, aConnected, aBleConnectionOpen, ToUlong(aExpectedCallbackCount)); } return ok; } bool IsContractHonored(void) const { return mContractHonored; } uint32_t GetConnectCallbackCount(void) const { return mConnectCallbackCount; } void ResetConnectCallbackCount(void) { mConnectCallbackCount = 0; } private: bool mIsConnected; bool mIsBleConnectionOpen; uint32_t mConnectCallbackCount; bool mContractHonored; }; static void HandleBleSecureConnect(otInstance *aInstance, bool aConnected, bool aBleConnectionOpen, void *aContext) { OT_UNUSED_VARIABLE(aInstance); static_cast(aContext)->HandleBleSecureConnect(aConnected, aBleConnectionOpen); } // test helper to validate that only classes set '1' in aCommandClassesBitmap are authorized and others not. static bool CommandClassesAuthorized(const TcatAgent *aAgent, const uint16_t aCommandClassesBitmap) { bool validationResult = true; static_assert(TcatAgent::kInvalid < 16, "kInvalid must be less than 16 to fit in uint16_t"); for (uint16_t i = TcatAgent::kGeneral; i <= TcatAgent::kInvalid; i++) { const bool isAuthorizedByAgent = aAgent->IsCommandClassAuthorized(static_cast(i)); const bool isAuthorizationExpected = (aCommandClassesBitmap & (1 << i)) != 0; if (isAuthorizedByAgent != isAuthorizationExpected) { printf("Expected command class %d authorization '%d', but TCAT Agent reports '%d'\n", i, isAuthorizationExpected, isAuthorizedByAgent); validationResult = false; } } return validationResult; } // test helper to validate if the Set Active Dataset command would be successful, given the dataset to write // and the current state of the device/agent. static bool IsSetActiveDatasetSuccessful(const TcatAgent *aAgent, const Dataset::Info &aDatasetInfo) { Dataset dataset; // Convert high-level Dataset::Info into TLVs representation required by IsSetActiveDatasetAuthorized() VerifyOrQuit(dataset.WriteTlvsFrom(aDatasetInfo) == kErrorNone); return aAgent->Get().IsDisabled() && aAgent->IsSetActiveDatasetAuthorized(&dataset); } // Advances fake time by aDuration, firing any expired alarms and processing tasklets along the way. static void AdvanceTime(Instance *aInstance, uint32_t aDuration) { uint32_t time = sNow + aDuration; while (sAlarmOn && TimeMilli(sAlarmTime) <= TimeMilli(time)) { sNow = sAlarmTime; sAlarmOn = false; otPlatAlarmMilliFired(aInstance); otTaskletsProcess(aInstance); } sNow = time; otTaskletsProcess(aInstance); } // Observer for the TCAT join callback (`otHandleTcatJoin`), counting join (Start) vs. leave (Stop) invocations. struct TcatJoinCounters { uint32_t mJoinCount; // invocations reporting aIsJoin == true (StartThreadInterface) uint32_t mLeaveCount; // invocations reporting aIsJoin == false (StopThreadInterface / Decommission) Error mLastError; }; static void HandleTcatJoin(otInstance *aInstance, bool aIsJoin, otError aError, void *aContext) { OT_UNUSED_VARIABLE(aInstance); TcatJoinCounters &counters = *static_cast(aContext); if (aIsJoin) { counters.mJoinCount++; } else { counters.mLeaveCount++; } counters.mLastError = static_cast(aError); } // Observer for the public OpenThread state-changed callback (`otStateChangedCallback`). Used to verify // that the Notifier coalesces multiple changes into a single notification rather than delivering them // across several callbacks. struct StateChangeObserver { uint32_t mTotalCount; // total number of state-changed callbacks received uint32_t mNetworkKeyCount; // callbacks that reported OT_CHANGED_NETWORK_KEY uint32_t mExtPanIdCount; // callbacks that reported OT_CHANGED_THREAD_EXT_PANID uint32_t mBothInOneCount; // callbacks that reported BOTH of the above together (i.e. coalesced) }; static void HandleNotifierStateChanged(otChangedFlags aFlags, void *aContext) { static constexpr otChangedFlags kBothFlags = OT_CHANGED_NETWORK_KEY | OT_CHANGED_THREAD_EXT_PANID; StateChangeObserver &observer = *static_cast(aContext); observer.mTotalCount++; if (aFlags & OT_CHANGED_NETWORK_KEY) { observer.mNetworkKeyCount++; } if (aFlags & OT_CHANGED_THREAD_EXT_PANID) { observer.mExtPanIdCount++; } if ((aFlags & kBothFlags) == kBothFlags) { observer.mBothInOneCount++; } } static Instance *TestInitInstanceTcat(void) { Instance *instance = testInitInstance(); sAlarmOn = false; // discard any pending alarm of a previous test's instance otBleSecureSetCertificate(instance, reinterpret_cast(OT_TCAT_X509_CERT), sizeof(OT_TCAT_X509_CERT), reinterpret_cast(OT_TCAT_PRIV_KEY), sizeof(OT_TCAT_PRIV_KEY)); otBleSecureSetCaCertificateChain(instance, reinterpret_cast(OT_TCAT_TRUSTED_ROOT_CERTIFICATE), sizeof(OT_TCAT_TRUSTED_ROOT_CERTIFICATE)); otBleSecureSetSslAuthMode(instance, true); SuccessOrQuit(otBleSecureSetTcatVendorInfo(instance, &vendorInfo)); // reset default data items used across tests sFullDataset = AsCoreType(&kFullDataset); sPartialDataset = AsCoreType(&kPartialDataset); IgnoreError(sCommNetworkName.Set(kNetworkName)); IgnoreError(sCommDomainName.Set(kDomainName)); memcpy(&sCommExtPanId, &kExtPanId, sizeof(sCommExtPanId)); memcpy(&sCommAuth, &kCommCert1AuthField, sizeof(sCommAuth)); memcpy(&sDeviceAuth, &kDeviceCert1AuthField, sizeof(sDeviceAuth)); sPlatBleLastAdvSetDataLen = 0; memset(sPlatBleLastAdvSetData, 0, OT_TCAT_ADVERTISEMENT_MAX_LEN); sPlatBleAdvertising = false; return instance; } void TestTcatConnectionAndCertAttributes(void) { uint8_t attributeBuffer[8]; size_t attributeLen; TestBleSecure ble; Instance *instance = TestInitInstanceTcat(); // Validate BLE secure and Tcat start APIs VerifyOrQuit(otBleSecureTcatStart(instance, nullptr) == kErrorInvalidState); SuccessOrQuit(otBleSecureStart(instance, HandleBleSecureConnect, nullptr, true, &ble)); VerifyOrQuit(otBleSecureStart(instance, HandleBleSecureConnect, nullptr, true, nullptr) == kErrorAlready); SuccessOrQuit(otBleSecureTcatStart(instance, nullptr)); // Validate connection callbacks when platform informs that peer has connected/disconnected. VerifyOrQuit(!otBleSecureIsConnected(instance)); otPlatBleGapOnConnected(instance, kConnectionId); VerifyOrQuit(ble.Verify(/* aConnected */ false, /* aBleConnectionOpen */ true, /* aExpectedCallbackCount */ 1)); otPlatBleGapOnDisconnected(instance, kConnectionId); VerifyOrQuit(ble.Verify(/* aConnected */ false, /* aBleConnectionOpen */ false, /* aExpectedCallbackCount */ 2)); // Verify that Thread-attribute parsing isn't available yet when not connected as client or server. attributeLen = sizeof(attributeBuffer); VerifyOrQuit(otBleSecureGetThreadAttributeFromPeerCertificate(instance, kCertificateAuthorizationField, &attributeBuffer[0], &attributeLen) == kErrorInvalidState); attributeLen = sizeof(attributeBuffer); VerifyOrQuit(otBleSecureGetThreadAttributeFromOwnCertificate( instance, kCertificateThreadVersion, &attributeBuffer[0], &attributeLen) == kErrorInvalidState); // Validate connection callbacks when calling `otBleSecureDisconnect()` otPlatBleGapOnConnected(instance, kConnectionId); VerifyOrQuit(ble.Verify(/* aConnected */ false, /* aBleConnectionOpen */ true, /* aExpectedCallbackCount */ 3)); ble.ResetConnectCallbackCount(); otBleSecureDisconnect(instance); // Regression test: a locally-initiated disconnect (with no TLS session) must invoke the connect callback // exactly once, with the fully-disconnected pair. VerifyOrQuit(ble.Verify(/* aConnected */ false, /* aBleConnectionOpen */ false, /* aExpectedCallbackCount */ 1)); // Validate TLS connection can be started (as client) only when peer is BLE-connected otPlatBleGapOnConnected(instance, kConnectionId); SuccessOrQuit(otBleSecureConnect(instance)); VerifyOrQuit(otBleSecureIsConnectionActive(instance)); // Once in TLS client connecting state, the below cert eval functions are available. // Test that the Thread-specific attributes from own certificate can be decoded properly. attributeLen = 1; SuccessOrQuit(otBleSecureGetThreadAttributeFromOwnCertificate(instance, kCertificateThreadVersion, &attributeBuffer[0], &attributeLen)); VerifyOrQuit(attributeLen == 1 && attributeBuffer[0] >= kThreadVersion1p4); static_assert(5 == sizeof(kDeviceCert1AuthField), "expectedTcatAuthField size incorrect for test"); attributeLen = 5; SuccessOrQuit(otBleSecureGetThreadAttributeFromOwnCertificate(instance, kCertificateAuthorizationField, &attributeBuffer[0], &attributeLen)); VerifyOrQuit(attributeLen == 5 && memcmp(&kDeviceCert1AuthField, &attributeBuffer, attributeLen) == 0); // Validate TLS client connection can be started only when peer is BLE-connected otBleSecureDisconnect(instance); VerifyOrQuit(otBleSecureConnect(instance) == kErrorInvalidState); // Validate Tcat agent state changes after stopping BLE secure VerifyOrQuit(otBleSecureIsTcatAgentStarted(instance)); otBleSecureStop(instance); VerifyOrQuit(!otBleSecureIsTcatAgentStarted(instance)); VerifyOrQuit(ble.IsContractHonored()); testFreeInstance(instance); } void TestTcatAdvertisementUpdates(void) { TestBleSecure ble; Instance *instance = TestInitInstanceTcat(); uint8_t advDataSnapshot[OT_TCAT_ADVERTISEMENT_MAX_LEN]; uint16_t advDataSnapshotLen; VerifyOrQuit(sPlatBleLastAdvSetDataLen == 0, "Adv data should be unset before BleSecure start"); SuccessOrQuit(otBleSecureStart(instance, HandleBleSecureConnect, nullptr, true, &ble)); SuccessOrQuit(otBleSecureTcatStart(instance, nullptr)); VerifyOrQuit(sPlatBleLastAdvSetDataLen > 0, "Adv data should be set after BleSecure start"); VerifyOrQuit(sPlatBleAdvertising, "Advertising should be started after BleSecure start"); advDataSnapshotLen = sPlatBleLastAdvSetDataLen; memcpy(advDataSnapshot, sPlatBleLastAdvSetData, advDataSnapshotLen); otPlatBleGapOnConnected(instance, kConnectionId); sPlatBleAdvertising = false; // model BLE platform behavior: advertising stops when a client connects. SuccessOrQuit(otBleSecureConnect(instance)); // mock "TLS handshake active" by initiating a client connection. // BLE connect and initiating TLS does not change the adv data. VerifyOrQuit(sPlatBleLastAdvSetDataLen == advDataSnapshotLen && memcmp(sPlatBleLastAdvSetData, advDataSnapshot, advDataSnapshotLen) == 0, "Adv data changed unexpectedly after BLE connect"); advDataSnapshotLen = sPlatBleLastAdvSetDataLen; memcpy(advDataSnapshot, sPlatBleLastAdvSetData, advDataSnapshotLen); // Commissioner sets dataset, then disconnects its BLE suddenly instance->Get().SaveLocal(sPartialDataset); otPlatBleGapOnDisconnected(instance, kConnectionId); // Since the TLS session teardown is ongoing, no change in advertisement state yet. VerifyOrQuit(!sPlatBleAdvertising, "Advertising restarted while TLS teardown is still ongoing"); // Still within the TLS teardown guard time (kGuardTimeNewConnectionMilli). Meanwhile time advances and // the advertisement data is updated to reflect the new sPartialDataset state. But not advertising yet. AdvanceTime(instance, 1000); VerifyOrQuit(sPlatBleLastAdvSetDataLen == advDataSnapshotLen, "Adv data length changed unexpectedly"); VerifyOrQuit(memcmp(sPlatBleLastAdvSetData, advDataSnapshot, advDataSnapshotLen) != 0, "Adv data did not change after processing sPartialDataset, which it should due to S flag"); VerifyOrQuit(!sPlatBleAdvertising, "Advertising already restarted while TLS teardown is still ongoing"); // Take new snapshot of advertisement data. advDataSnapshotLen = sPlatBleLastAdvSetDataLen; memcpy(advDataSnapshot, sPlatBleLastAdvSetData, advDataSnapshotLen); // Advance time beyond the guard time, so that the TLS session fully disconnects and the (deferred) // advertising restart is performed now. AdvanceTime(instance, 1000 + 10); VerifyOrQuit(sPlatBleAdvertising, "Advertising was not restarted after TLS teardown completed"); // Adv content itself is not changed now - it was already done while waiting for the TLS guard time. VerifyOrQuit(sPlatBleLastAdvSetDataLen == advDataSnapshotLen, "Adv data length changed unexpectedly"); VerifyOrQuit(memcmp(sPlatBleLastAdvSetData, advDataSnapshot, advDataSnapshotLen) == 0, "Adv data changed unexpectedly after TLS guard timeout expired"); otBleSecureStop(instance); VerifyOrQuit(ble.IsContractHonored()); testFreeInstance(instance); } class UnitTester { private: // Mock action: TCAT Commissioner connects with authorization aCommAuth while device has aDeviceAuth. static void MockCommissionerConnected(TcatAgent *aAgent, const TcatAgent::CertificateAuthorizationField aCommAuth, const TcatAgent::CertificateAuthorizationField aDeviceAuth, bool aIsCommissionedAtStart) { // This mock function mimics the steps in TcatAgent::Connected() without requiring the actual TLS // session object. aAgent->ClearCommissionerState(); aAgent->mCommissionerAuthorizationField = aCommAuth; aAgent->mDeviceAuthorizationField = aDeviceAuth; aAgent->mCanOverwriteDataset = !aIsCommissionedAtStart; aAgent->mNextState = (aAgent->mState == TcatAgent::kStateActiveTemporary) ? TcatAgent::kStateStandby : TcatAgent::kStateActive; aAgent->mState = TcatAgent::kStateConnected; aAgent->NotifyStateChange(); } // Mock condition: commissioner has or has not the given Extended Pan ID in its certificate. static void MockExtPanId(TcatAgent *aAgent, bool aCommHasExtPanId, const ExtendedPanId *aExtPanId) { aAgent->mCommissionerHasExtendedPanId = aCommHasExtPanId; aAgent->mCommissionerExtendedPanId = *aExtPanId; } // Mock condition: commissioner has or has not the given Network Name in its certificate. static void MockNetworkName(TcatAgent *aAgent, bool aCommHasNetworkName, const NetworkName *aNetworkName) { aAgent->mCommissionerHasNetworkName = aCommHasNetworkName; aAgent->mCommissionerNetworkName = *aNetworkName; } // Mock condition: commissioner has or has not the given Domain Name in its certificate. static void MockDomainName(TcatAgent *aAgent, bool aCommHasDomainName, const NetworkName *aDomainName) { aAgent->mCommissionerHasDomainName = aCommHasDomainName; aAgent->mCommissionerDomainName = *aDomainName; } // Mock operation: TCAT Commissioner writes `aDataset` as the new local Active Operational Dataset. // It models the behavior of `HandleSetActiveOperationalDataset()`, without checking authorization. static void MockWriteActiveDataset(Instance *aInstance, const Dataset::Info &aDataset) { SuccessOrQuit(aInstance->Get().Disable()); aInstance->Get().SaveLocal(aDataset); aInstance->Get().mIsSourceOfDatasetChange = true; otTaskletsProcess(aInstance); } // Mock operation: some entity other than a TCAT Commissioner / Agent caused a dataset change. // The existing state of Thread (MLE) is not changed. static void MockActiveDatasetChanged(Instance *aInstance, const Dataset::Info &aDataset) { aInstance->Get().SaveLocal(aDataset); otTaskletsProcess(aInstance); } // Mock operation: an entity other than a TCAT Commissioner clears the active dataset. static void MockActiveDatasetCleared(Instance *aInstance) { SuccessOrQuit(aInstance->Get().Disable()); aInstance->Get().Clear(); otTaskletsProcess(aInstance); } // Mock operation: TCAT Commissioner sends the Decommission command. `HandleDecommission()` itself can't be // called here because it reads the peer certificate from a real (mbedtls) TLS session, which these unit // tests don't set up. So its authorization check is mimicked and the rest of it - `Decommission()` - is // invoked with a stand-in commissioner certificate. static void MockDecommission(Instance *aInstance) { static uint8_t sCommissionerCert[] = {0x30, 0x82, 0x01, 0x00}; TcatAgent &agent = aInstance->Get(); VerifyOrQuit(agent.IsCommandClassAuthorized(TcatAgent::kDecommissioning)); agent.Decommission(sCommissionerCert, sizeof(sCommissionerCert)); agent.mJoinCallback.InvokeIfSet(aInstance, /* aIsJoin */ false, kErrorNone); otTaskletsProcess(aInstance); } // Mock operation: the device attaches to a Thread network. Implemented by directly becoming Leader, // which is synchronous (no need to drive the attach state machine) and signals `kEventThreadRoleChanged` // just like a real attach. Requires a complete Active Dataset to be present. static void MockDeviceAttachedToNetwork(Instance *aInstance) { SuccessOrQuit(otThreadBecomeLeader(aInstance)); otTaskletsProcess(aInstance); VerifyOrQuit(otThreadGetDeviceRole(aInstance) == OT_DEVICE_ROLE_LEADER); } static void MockDeviceDetachedFromNetwork(Instance *aInstance) { otTaskletsProcess(aInstance); VerifyOrQuit(otThreadGetDeviceRole(aInstance) == OT_DEVICE_ROLE_DISABLED); } public: static void TestTcatCommissioner1Auth(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); VerifyOrQuit(!instance->Get().IsCommissioned()); // validate no Commissioner authorizations if not connected VerifyOrQuit(CommandClassesAuthorized(agent, kClassNone)); // Mock TCAT Commissioner 1 connects to the agent - verify it has access to all classes // ==================================================================================== MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, false); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!instance->Get().IsCommissioned()); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Write a partial Active Dataset and verify that Commissioner can still overwrite this with another dataset // if needed. MockWriteActiveDataset(instance, sPartialDataset); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!instance->Get().IsCommissioned()); VerifyOrQuit(instance->Get().IsPartiallyComplete()); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); // Write a full Active Dataset and verify that Commissioner can still overwrite this. MockWriteActiveDataset(instance, sFullDataset); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(instance->Get().IsCommissioned()); VerifyOrQuit(!instance->Get().IsPartiallyComplete()); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); // And back to partial dataset. MockWriteActiveDataset(instance, sPartialDataset); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!instance->Get().IsCommissioned()); VerifyOrQuit(instance->Get().IsPartiallyComplete()); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); // provide PSKc proof-of-possession - verify access is same as before agent->mPskcVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!instance->Get().IsCommissioned()); VerifyOrQuit(instance->Get().IsPartiallyComplete()); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); testFreeInstance(instance); } static void TestTcatCommissioner2Auth(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); // Mock TCAT Commissioner 2 connects to the agent - verify it only has access to class General by default. // CommCert2 contains Network Name and Extended PAN ID in this initial test, but not the (also-required) // Thread Domain Name. // ======================================================================================================= memcpy(&sCommAuth, &kCommCert2AuthField, sizeof(sCommAuth)); MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, false); MockNetworkName(agent, true, &sCommNetworkName); MockExtPanId(agent, true, &sCommExtPanId); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); // Verify that Set Active Dataset can't be used yet, despite a matching XPAN ID and Network Name for the // dataset that the Commissioner wants to write. VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // provide PSKd proof-of-possession - this is required for all 4 command classes, but not sufficient yet. // So verify there's no change. agent->mPskdVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // Commissioner cert now has a matching Domain Name - does not unlock any new classes, because // Network Name and XPAN ID can't match, due to Device being uncommissioned. Writing Active Dataset works now. MockDomainName(agent, true, &sCommDomainName); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); // Writing a partial dataset does not work: misses the required Network Name and XPAN ID VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Commissioner now has no XPAN ID anymore in cert - verify this prevents Set Active Dataset. // It's a misconfig in the Commissioner's cert. MockExtPanId(agent, false, &sCommExtPanId); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Commissioner now has correct XPAN ID in cert, but not matching the dataset it wants to write. MockExtPanId(agent, true, &sCommExtPanId); sFullDataset.mExtendedPanId.m8[2]++; // modify bits in dataset to be written. VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Commissioner now attempts to write a dataset with XPAN ID matching to that in cert. sFullDataset.mExtendedPanId = sCommExtPanId; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); // New situation: Active Dataset is now configured (device is commissioned), but XPAN ID in Dataset // doesn't match the XPAN ID in the Commissioner's cert; and PSKc proof is not given yet, // so most classes remain unavailable. sFullDataset.mExtendedPanId.m8[2]++; // modify bits in dataset to be written. instance->Get().SaveLocal(sFullDataset); VerifyOrQuit(instance->Get().IsCommissioned()); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // XPAN ID now matches again; class Commissioning authorization (0x1F) is restored. It doesn't require // PSKc proof. sFullDataset.mExtendedPanId = sCommExtPanId; instance->Get().SaveLocal(sFullDataset); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning)); // Active Dataset can be overwritten because Device was uncommissioned at session start. VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Now PSKc proof is given, unlocking more command classes agent->mPskcVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Commissioner connects again - this time, the Device is already commissioned at the start of the session. MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, true); MockNetworkName(agent, true, &sCommNetworkName); MockExtPanId(agent, true, &sCommExtPanId); MockDomainName(agent, true, &sCommDomainName); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // PSKd proof does not authorize Set Active Dataset - because device is already commissioned. agent->mPskdVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); agent->mPskcVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); testFreeInstance(instance); } static void TestTcatCommissioner4Auth(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); // Mock TCAT Commissioner 4 connects to the Device - verify it only has access to class General by default. // The Device is commissioned already at start of the TCAT Link. // ======================================================================================================= instance->Get().SaveLocal(sFullDataset); memcpy(&sCommAuth, &kCommCert4AuthField, sizeof(sCommAuth)); MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, true); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // PSKc proof - satisfies 0x21. Set Active Dataset is not allowed: Device already commissioned. agent->mPskcVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // Matching network name now in Commissioner cert - satisfies 0x05 MockNetworkName(agent, true, &sCommNetworkName); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // New situation: matching XPAN ID present in Comm cert - satisfies 0x09 MockExtPanId(agent, true, &sCommExtPanId); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // New situation: Thread Domain name in cert matches - Application class added. MockDomainName(agent, true, &sCommDomainName); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // New situation: PSKc proof not given - Commissioning class is revoked agent->mPskcVerified = false; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // New situation: network name present, but mismatch - Extraction revoked NetworkName wrongNetworkName; SuccessOrQuit(wrongNetworkName.Set("WrongName")); MockNetworkName(agent, true, &wrongNetworkName); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // New situation: XPAN ID present, but mismatch - Decommissioning revoked sFullDataset.mExtendedPanId.m8[4]++; // change bits to force a mismatch instance->Get().SaveLocal(sFullDataset); MockExtPanId(agent, true, &sCommExtPanId); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // New situation: XPAN ID not present in dataset - same as before sFullDataset.mExtendedPanId = sCommExtPanId; // restore changes bits of above sFullDataset.mComponents.mIsExtendedPanIdPresent = false; instance->Get().SaveLocal(sFullDataset); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // New situation: Network Name not present in dataset - same as before sFullDataset.mComponents.mIsNetworkNamePresent = false; instance->Get().SaveLocal(sFullDataset); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // New situation: Device is decommissioned (by some other Commissioner). Then, this Commissioner // connects again and does PSKc proof. Set Active Dataset access should now be allowed. instance->Get().Clear(); MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, false); agent->mPskcVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); testFreeInstance(instance); } static void TestTcatCommissioner5Auth(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); // Mock TCAT Commissioner 5 connects to the agent - it requires checks that are unknown to the Device // ================================================================================================== memcpy(&sCommAuth, &kCommCert5AuthField, sizeof(sCommAuth)); MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, true); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // PSKd proof is given: it won't enable Extraction, because Extraction access flag bit 0 = 0. // Also it won't enable Decommissioning, because this class has an unknown flag bit 7 set i.e. the Commissioner // is configured to require a method that the TCAT Device doesn't know about. Application class is also not // enabled, since it requires a check with unknown flag bit 6. Device will enable Commissioning class. agent->mPskdVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Connect again and test the situation that Device was uncommissioned at connection start. // Commissioning is now enabled with PSKd proof. instance->Get().Clear(); MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, false); agent->mPskdVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); testFreeInstance(instance); } static void TestTcatCommissioner1AuthWithDeviceRequirements(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); // test different auth info: for TCAT Device 2 which has specific authorization requirements per class. memcpy(&sDeviceAuth, &kDeviceCert2AuthField, sizeof(sDeviceAuth)); // Mock TCAT Commissioner 1 connects to the agent - verify // ==================================================================================== memcpy(&sCommAuth, &kCommCert1AuthField, sizeof(sCommAuth)); MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, false); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassExtraction)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // PSKd proof agent->mPskdVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // New situation: Device is commissioned and Commissioner has matching Network Name; and connects. instance->Get().SaveLocal(sFullDataset); MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, true); MockNetworkName(agent, true, &sCommNetworkName); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // PSKc proof agent->mPskcVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // If Network Name does not match NetworkName wrongNetworkName; SuccessOrQuit(wrongNetworkName.Set(kWrongName)); MockNetworkName(agent, true, &wrongNetworkName); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassExtraction | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); agent->mPskdVerified = true; agent->mPskcVerified = false; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); agent->mPskcVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); testFreeInstance(instance); } static void TestTcatCommissioner2AuthWithDeviceRequirements(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); // test different auth info: for TCAT Device 2 which has specific authorization requirements per class. memcpy(&sDeviceAuth, &kDeviceCert2AuthField, sizeof(sDeviceAuth)); // Mock TCAT Commissioner 2 connects to the agent // ============================================== memcpy(&sCommAuth, &kCommCert2AuthField, sizeof(sCommAuth)); MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, false); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // PSKd proof agent->mPskdVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Network Name match MockNetworkName(agent, true, &sCommNetworkName); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // XPAN ID match MockExtPanId(agent, true, &sCommExtPanId); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Domain Name match - but Commissioning in general is still not authorized, due to missing Active Dataset. // Hence the Network Name and XPAN ID checks cannot succeed in general. They will succeed now for the // specific 'Set Active Dataset' command. MockDomainName(agent, true, &sCommDomainName); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); // the partial dataset cannot be written, because it lacks the required Network Name and XPAN ID combo. VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // PSKc proof agent->mPskcVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Try write a full dataset with differing XPAN ID - this fails sFullDataset.mExtendedPanId.m8[2]++; VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // Test an equivalent case to above where the device does have a full dataset stored already, and the // Commissioner connects. Now it has full access to all classes due to matching Network Name / XPAN ID combo. sFullDataset = AsCoreType(&kFullDataset); instance->Get().SaveLocal(sFullDataset); MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, true); agent->mPskdVerified = true; agent->mPskcVerified = true; MockNetworkName(agent, true, &sCommNetworkName); MockExtPanId(agent, true, &sCommExtPanId); MockDomainName(agent, true, &sCommDomainName); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassDecommissioning | kClassExtraction | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); testFreeInstance(instance); } static void TestTcatCommissioner4AuthWithExistingPartialDataset(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); // TCAT device was commissioned earlier on with a partial dataset. instance->Get().SaveLocal(sPartialDataset); // Mock TCAT Commissioner 4 connects to the Device. // static const uint8_t kCommCert4AuthField[5] = {0x21, 0x21, 0x05, 0x09, 0x11}; memcpy(&sCommAuth, &kCommCert4AuthField, sizeof(sCommAuth)); MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, true); MockNetworkName(agent, true, &sCommNetworkName); MockExtPanId(agent, true, &sCommExtPanId); // It wants access to Extraction class (0x05) based on matching Network Name, but it's denied. // Decommissioning (0x09) based on matching XPAN ID is also denied. VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // PSKc proof - satisfies 0x21. Set Active Dataset is not allowed: Device already commissioned. agent->mPskcVerified = true; VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); // As a sanity check, redo the test assuming that a (matching) full dataset was initially in the Device. // Now, Extraction and Commissioning classes do work because of matching elements in the Commcert. instance->Get().SaveLocal(sFullDataset); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); testFreeInstance(instance); } static void TestTcatDatasetOverwrite(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); VerifyOrQuit(!instance->Get().IsCommissioned()); // A fresh session on an uncommissioned device: writing the Active Dataset is allowed. MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, /* aIsCommissionedAtStart */ false); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); // TCAT agent itself writes a dataset. Overwriting this is allowed. MockWriteActiveDataset(instance, sFullDataset); VerifyOrQuit(instance->Get().IsCommissioned()); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); // A bumped Active Timestamp (e.g. applied by the network) is a change not made by this Agent, // however the network key and ExtPanId remain the same, so dataset-overwriting is still allowed - // considering it still as 'same network'. { Dataset::Info bumpedInfo = sFullDataset; bumpedInfo.mActiveTimestamp.mSeconds = 272899; MockActiveDatasetChanged(instance, bumpedInfo); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); } // Another module/process changes the Active Dataset content significantly (here: a different Network Key). // The agent now refuses to overwrite the dataset. { Dataset::Info externalInfo = sFullDataset; externalInfo.mNetworkKey.m8[0] ^= 0xff; MockActiveDatasetChanged(instance, externalInfo); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); } // After the Active Dataset is cleared (decommissioned), by an entity other than the TCAT agent, // the agent still consider the TCAT session as no longer authorized to overwrite datasets. // The TCAT Commissioner would need a fresh reconnection to authorize dataset-(over)writing again. MockActiveDatasetCleared(instance); VerifyOrQuit(!instance->Get().IsCommissioned()); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // TCAT Commissioner reconnects and can write a dataset again. MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, /* aIsCommissionedAtStart */ false); VerifyOrQuit(!instance->Get().IsCommissioned()); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); testFreeInstance(instance); } static void TestTcatDatasetOverwriteAfterAttach(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); // TCAT Commissioner connects and writes dataset. MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, /* aIsCommissionedAtStart */ false); MockWriteActiveDataset(instance, sFullDataset); VerifyOrQuit(instance->Get().IsCommissioned()); // Before attaching, the agent still permits overwriting its own freshly-written dataset. VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); for (int i = 0; i < 3; i++) { // Once the device is attached to a Thread network, the TCAT Commissioner can no longer overwrite the // Active Dataset (this prevents a dataset from propagating to other already-networked devices). SuccessOrQuit(agent->HandleStartThreadInterface()); MockDeviceAttachedToNetwork(instance); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // When the device has detached from Thread, the TCAT Commissioner can overwrite the Active Dataset // even without a Decommission command, because it's still in the same TCAT session. SuccessOrQuit(agent->HandleStopThreadInterface()); MockDeviceDetachedFromNetwork(instance); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); } // Attach to Thread Network again to prepare for the next test. SuccessOrQuit(agent->HandleStartThreadInterface()); MockDeviceAttachedToNetwork(instance); // Another module/process changes the Active Dataset content significantly (here: a different Network Key). { Dataset::Info externalInfo = sFullDataset; externalInfo.mNetworkKey.m8[0] ^= 0xff; MockActiveDatasetChanged(instance, externalInfo); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); } // Thread is stopped SuccessOrQuit(agent->HandleStopThreadInterface()); MockDeviceDetachedFromNetwork(instance); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // Attach to Thread Network again SuccessOrQuit(agent->HandleStartThreadInterface()); MockDeviceAttachedToNetwork(instance); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // After the Active Dataset is cleared (decommissioned) by an external source, the agent // still cannot set the active dataset. MockActiveDatasetCleared(instance); VerifyOrQuit(!instance->Get().IsCommissioned()); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // TCAT Commissioner reconnects and then can write a dataset again. MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, /* aIsCommissionedAtStart */ false); VerifyOrQuit(!instance->Get().IsCommissioned()); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); testFreeInstance(instance); } static void TestTcatRepeatedCommandActivation(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); TcatJoinCounters counters = {}; // Set up a commissioned device: Commissioning class authorized, a full Active Dataset present, and the // Thread interface initially down. Observe the TCAT join callback throughout. MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, /* aIsCommissionedAtStart */ false); MockWriteActiveDataset(instance, sFullDataset); agent->mJoinCallback.Set(HandleTcatJoin, &counters); VerifyOrQuit(instance->Get().IsCommissioned()); VerifyOrQuit(instance->Get().IsDisabled()); // First StartThreadInterface brings the interface up and reports a successful join. SuccessOrQuit(agent->HandleStartThreadInterface()); VerifyOrQuit(!instance->Get().IsDisabled()); VerifyOrQuit(counters.mJoinCount == 1 && counters.mLastError == kErrorNone); // Repeated StartThreadInterface while already started is an idempotent success (TCAT spec): the interface // stays up and the successful-join response is repeated on each invocation. SuccessOrQuit(agent->HandleStartThreadInterface()); SuccessOrQuit(agent->HandleStartThreadInterface()); VerifyOrQuit(!instance->Get().IsDisabled()); VerifyOrQuit(counters.mJoinCount == 1 && counters.mLeaveCount == 0 && counters.mLastError == kErrorNone); // First StopThreadInterface brings the interface back down and reports a successful leave. SuccessOrQuit(agent->HandleStopThreadInterface()); VerifyOrQuit(instance->Get().IsDisabled()); VerifyOrQuit(counters.mLeaveCount == 1 && counters.mLastError == kErrorNone && counters.mJoinCount == 1); // Repeated StopThreadInterface while already stopped is an idempotent success (TCAT spec), but the // 'already stopped' case does NOT re-invoke the join callback. SuccessOrQuit(agent->HandleStopThreadInterface()); SuccessOrQuit(agent->HandleStopThreadInterface()); VerifyOrQuit(instance->Get().IsDisabled()); VerifyOrQuit(counters.mLeaveCount == 1 && counters.mLastError == kErrorNone && counters.mJoinCount == 1); testFreeInstance(instance); } // Verifies that a Decommission command restores the Commissioner's authorization to (over)write the Active // Dataset within the same TCAT session, so that commissioning/decommissioning cycles can be repeated. static void TestTcatDecommissionRestoresDatasetWrite(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); TcatJoinCounters counters = {}; // A device that was commissioned by some other entity (not this TCAT Agent) before the session started. MockActiveDatasetChanged(instance, sFullDataset); VerifyOrQuit(instance->Get().IsCommissioned()); // The Commissioner connects: it is not authorized to overwrite the existing Active Dataset, but it is // authorized to decommission the device. MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, /* aIsCommissionedAtStart */ true); agent->mJoinCallback.Set(HandleTcatJoin, &counters); VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction | kClassDecommissioning | kClassApplication)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); for (int i = 0; i < 3; i++) { // Decommission erases the Active Dataset and reports a successful 'leave'. MockDecommission(instance); VerifyOrQuit(!instance->Get().IsCommissioned()); VerifyOrQuit(instance->Get().IsDisabled()); VerifyOrQuit(counters.mLeaveCount == static_cast(i + 1) && counters.mJoinCount == 0 && counters.mLastError == kErrorNone); // Regression test: decommissioning clears the Network Key, which signals a Notifier event that would // otherwise be mistaken for an external dataset change and immediately revoke the authorization again. VerifyOrQuit(agent->mCanOverwriteDataset, "Decommission must restore dataset-write authorization"); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); // The Commissioner recommissions the device in the same session, and may still overwrite afterwards. MockWriteActiveDataset(instance, sFullDataset); VerifyOrQuit(instance->Get().IsCommissioned()); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(IsSetActiveDatasetSuccessful(agent, sPartialDataset)); } // A dataset change by another module still revokes the authorization after a Decommission. MockDecommission(instance); VerifyOrQuit(agent->mCanOverwriteDataset); MockActiveDatasetChanged(instance, sFullDataset); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sFullDataset)); VerifyOrQuit(!IsSetActiveDatasetSuccessful(agent, sPartialDataset)); testFreeInstance(instance); } // Verifies the OpenThread Notifier assumption that TcatAgent::HandleNotifierEvents() relies on: // multiple state changes that occur back-to-back are coalesced into a single notification carrying all of // the corresponding event flags. static void TestTcatNotifierCoalescesEvents(void) { Instance *instance = TestInitInstanceTcat(); TcatAgent *agent = &instance->Get(); StateChangeObserver observer = {}; // Commit a baseline Active Dataset and drain all resulting notifications. instance->Get().SaveLocal(sFullDataset); otTaskletsProcess(instance); // A Commissioner connects to the (uncommissioned) device MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, /* aIsCommissionedAtStart */ false); VerifyOrQuit(agent->IsStarted()); VerifyOrQuit(agent->mCanOverwriteDataset); // Start observing state-changed notifications. SuccessOrQuit(otSetStateChangedCallback(instance, HandleNotifierStateChanged, &observer)); // The agent writes a new Active Dataset that differs in exactly 2 items - signalled as two distinct // Notifier events. { Dataset::Info changed = sFullDataset; changed.mNetworkKey.m8[0] ^= 0xff; changed.mExtendedPanId.m8[0] ^= 0xff; instance->Get().SaveLocal(changed); agent->mIsSourceOfDatasetChange = true; // model that this Agent is the source of the change } // repeat the tasklets processing to ensure subsequent notifer calls are not made. for (int i = 0; i < 3; i++) { // Process pending tasklets: this drives the Notifier to emit the accumulated events. otTaskletsProcess(instance); VerifyOrQuit(observer.mTotalCount == 1, "two changes must produce exactly one notification, not two"); VerifyOrQuit(observer.mNetworkKeyCount == 1, "Network Key change must be reported exactly once"); VerifyOrQuit(observer.mExtPanIdCount == 1, "Extended PAN ID change must be reported exactly once"); VerifyOrQuit(observer.mBothInOneCount == 1, "both changes must be coalesced into the same notification"); // Because the agent saw both events coalesced while mIsSourceOfDatasetChange was set, it recognizes the // change as its own and retains the Commissioner's authorization to overwrite the dataset. VerifyOrQuit(agent->mCanOverwriteDataset, "a self-made dataset change must not revoke authorization"); } otRemoveStateChangeCallback(instance, HandleNotifierStateChanged, &observer); testFreeInstance(instance); } }; // class UnitTester } // namespace MeshCoP } // namespace ot #endif // OPENTHREAD_CONFIG_BLE_TCAT_ENABLE int main(void) { #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE ot::MeshCoP::TestTcatConnectionAndCertAttributes(); ot::MeshCoP::TestTcatAdvertisementUpdates(); ot::MeshCoP::UnitTester::TestTcatCommissioner1Auth(); ot::MeshCoP::UnitTester::TestTcatCommissioner2Auth(); ot::MeshCoP::UnitTester::TestTcatCommissioner4Auth(); ot::MeshCoP::UnitTester::TestTcatCommissioner5Auth(); ot::MeshCoP::UnitTester::TestTcatCommissioner1AuthWithDeviceRequirements(); ot::MeshCoP::UnitTester::TestTcatCommissioner2AuthWithDeviceRequirements(); ot::MeshCoP::UnitTester::TestTcatCommissioner4AuthWithExistingPartialDataset(); ot::MeshCoP::UnitTester::TestTcatDatasetOverwrite(); ot::MeshCoP::UnitTester::TestTcatDatasetOverwriteAfterAttach(); ot::MeshCoP::UnitTester::TestTcatRepeatedCommandActivation(); ot::MeshCoP::UnitTester::TestTcatDecommissionRestoresDatasetWrite(); ot::MeshCoP::UnitTester::TestTcatNotifierCoalescesEvents(); printf("All tests passed\n"); #else printf("TCAT feature is not enabled\n"); #endif return 0; }