Files
Esko Dijk 5c90231e48 [tcat] fix unit tests (#12875)
Due to a state retention issue in the unit test platform, TCAT tests were passing in ways they should not.
Now with the new settings/flash clearing per #12875 applied, these tests were failing.

This fixes TCAT unit tests to pass again and better express the expected behavior also.
2026-04-14 08:44:27 -07:00

843 lines
44 KiB
C++

/*
* 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 <openthread/ble_secure.h>
#include "cli/cli_dataset.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}
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 BLE connection state.
class TestBleSecure
{
public:
TestBleSecure(void)
: mIsConnected(false)
, mIsBleConnectionOpen(false)
{
}
void HandleBleSecureConnect(bool aConnected, bool aBleConnectionOpen)
{
mIsConnected = aConnected;
mIsBleConnectionOpen = aBleConnectionOpen;
}
bool IsConnected(void) const { return mIsConnected; }
bool IsBleConnectionOpen(void) const { return mIsBleConnectionOpen; }
private:
bool mIsConnected;
bool mIsBleConnectionOpen;
};
static void HandleBleSecureConnect(otInstance *aInstance, bool aConnected, bool aBleConnectionOpen, void *aContext)
{
OT_UNUSED_VARIABLE(aInstance);
static_cast<TestBleSecure *>(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<TcatAgent::CommandClass>(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 Set Active Dataset commands are authorized or not, given the dataset to write.
static bool SetActiveDatasetAuthorized(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->IsSetActiveDatasetAuthorized(&dataset);
}
static Instance *TestInitInstanceTcat(void)
{
Instance *instance = testInitInstance();
otBleSecureSetCertificate(instance, reinterpret_cast<const uint8_t *>(OT_TCAT_X509_CERT), sizeof(OT_TCAT_X509_CERT),
reinterpret_cast<const uint8_t *>(OT_TCAT_PRIV_KEY), sizeof(OT_TCAT_PRIV_KEY));
otBleSecureSetCaCertificateChain(instance, reinterpret_cast<const uint8_t *>(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));
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.IsConnected() && ble.IsBleConnectionOpen());
otPlatBleGapOnDisconnected(instance, kConnectionId);
VerifyOrQuit(!ble.IsConnected() && !ble.IsBleConnectionOpen());
// 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.IsConnected() && ble.IsBleConnectionOpen());
otBleSecureDisconnect(instance);
VerifyOrQuit(!ble.IsConnected() && !ble.IsBleConnectionOpen());
// 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));
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->mIsCommissioned = 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;
}
public:
static void TestTcatCommissioner1Auth(void)
{
Instance *instance = TestInitInstanceTcat();
TcatAgent *agent = &instance->Get<TcatAgent>();
VerifyOrQuit(!instance->Get<ActiveDatasetManager>().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<ActiveDatasetManager>().IsCommissioned());
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sPartialDataset));
// Write a partial Active Dataset and verify that Commissioner can still overwrite this with another dataset
// if needed.
instance->Get<ActiveDatasetManager>().SaveLocal(sPartialDataset);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction |
kClassDecommissioning | kClassApplication));
VerifyOrQuit(!instance->Get<ActiveDatasetManager>().IsCommissioned());
VerifyOrQuit(instance->Get<ActiveDatasetManager>().IsPartiallyComplete());
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sFullDataset));
// Write a full Active Dataset and verify that Commissioner can still overwrite this.
instance->Get<ActiveDatasetManager>().SaveLocal(sFullDataset);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction |
kClassDecommissioning | kClassApplication));
VerifyOrQuit(instance->Get<ActiveDatasetManager>().IsCommissioned());
VerifyOrQuit(!instance->Get<ActiveDatasetManager>().IsPartiallyComplete());
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sPartialDataset));
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sFullDataset));
// And back to partial dataset.
instance->Get<ActiveDatasetManager>().SaveLocal(sPartialDataset);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction |
kClassDecommissioning | kClassApplication));
VerifyOrQuit(!instance->Get<ActiveDatasetManager>().IsCommissioned());
VerifyOrQuit(instance->Get<ActiveDatasetManager>().IsPartiallyComplete());
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sPartialDataset));
VerifyOrQuit(SetActiveDatasetAuthorized(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<ActiveDatasetManager>().IsCommissioned());
VerifyOrQuit(instance->Get<ActiveDatasetManager>().IsPartiallyComplete());
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sPartialDataset));
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sFullDataset));
testFreeInstance(instance);
}
static void TestTcatCommissioner2Auth(void)
{
Instance *instance = TestInitInstanceTcat();
TcatAgent *agent = &instance->Get<TcatAgent>();
// 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(!SetActiveDatasetAuthorized(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(!SetActiveDatasetAuthorized(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(SetActiveDatasetAuthorized(agent, sFullDataset));
// Writing a partial dataset does not work: misses the required Network Name and XPAN ID
VerifyOrQuit(!SetActiveDatasetAuthorized(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(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(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(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(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(SetActiveDatasetAuthorized(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<ActiveDatasetManager>().SaveLocal(sFullDataset);
VerifyOrQuit(instance->Get<ActiveDatasetManager>().IsCommissioned());
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
// XPAN ID now matches again; class Commissioning authorization (0x1F) is restored. It doesn't require
// PSKc proof.
sFullDataset.mExtendedPanId = sCommExtPanId;
instance->Get<ActiveDatasetManager>().SaveLocal(sFullDataset);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning));
// Active Dataset can be overwritten because Device was uncommissioned at session start.
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
// Now PSKc proof is given, unlocking more command classes
agent->mPskcVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction |
kClassDecommissioning | kClassApplication));
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(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(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
// PSKd proof does not authorize Set Active Dataset - because device is already commissioned.
agent->mPskdVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
agent->mPskcVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction |
kClassDecommissioning | kClassApplication));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
testFreeInstance(instance);
}
static void TestTcatCommissioner4Auth(void)
{
Instance *instance = TestInitInstanceTcat();
TcatAgent *agent = &instance->Get<TcatAgent>();
// 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<ActiveDatasetManager>().SaveLocal(sFullDataset);
memcpy(&sCommAuth, &kCommCert4AuthField, sizeof(sCommAuth));
MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, true);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
// PSKc proof - satisfies 0x21. Set Active Dataset is not allowed: Device already commissioned.
agent->mPskcVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
// Matching network name now in Commissioner cert - satisfies 0x05
MockNetworkName(agent, true, &sCommNetworkName);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction));
VerifyOrQuit(!SetActiveDatasetAuthorized(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(!SetActiveDatasetAuthorized(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(!SetActiveDatasetAuthorized(agent, sFullDataset));
// New situation: PSKc proof not given - Commissioning class is revoked
agent->mPskcVerified = false;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassExtraction | kClassDecommissioning |
kClassApplication));
VerifyOrQuit(!SetActiveDatasetAuthorized(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(!SetActiveDatasetAuthorized(agent, sFullDataset));
// New situation: XPAN ID present, but mismatch - Decommissioning revoked
sFullDataset.mExtendedPanId.m8[4]++; // change bits to force a mismatch
instance->Get<ActiveDatasetManager>().SaveLocal(sFullDataset);
MockExtPanId(agent, true, &sCommExtPanId);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassApplication));
VerifyOrQuit(!SetActiveDatasetAuthorized(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<ActiveDatasetManager>().SaveLocal(sFullDataset);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassApplication));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
// New situation: Network Name not present in dataset - same as before
sFullDataset.mComponents.mIsNetworkNamePresent = false;
instance->Get<ActiveDatasetManager>().SaveLocal(sFullDataset);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassApplication));
VerifyOrQuit(!SetActiveDatasetAuthorized(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<ActiveDatasetManager>().Clear();
MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, false);
agent->mPskcVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning));
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sPartialDataset));
testFreeInstance(instance);
}
static void TestTcatCommissioner5Auth(void)
{
Instance *instance = TestInitInstanceTcat();
TcatAgent *agent = &instance->Get<TcatAgent>();
// 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(!SetActiveDatasetAuthorized(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(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
// Connect again and test the situation that Device was uncommissioned at connection start.
// Commissioning is now enabled with PSKd proof.
instance->Get<ActiveDatasetManager>().Clear();
MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, false);
agent->mPskdVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning));
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sPartialDataset));
testFreeInstance(instance);
}
static void TestTcatCommissioner1AuthWithDeviceRequirements(void)
{
Instance *instance = TestInitInstanceTcat();
TcatAgent *agent = &instance->Get<TcatAgent>();
// 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(!SetActiveDatasetAuthorized(agent, sFullDataset));
// PSKd proof
agent->mPskdVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction));
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sPartialDataset));
// New situation: Device is commissioned and Commissioner has matching Network Name; and connects.
instance->Get<ActiveDatasetManager>().SaveLocal(sFullDataset);
MockCommissionerConnected(agent, sCommAuth, sDeviceAuth, true);
MockNetworkName(agent, true, &sCommNetworkName);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassExtraction | kClassDecommissioning |
kClassApplication));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
// PSKc proof
agent->mPskcVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassExtraction | kClassDecommissioning |
kClassApplication));
VerifyOrQuit(!SetActiveDatasetAuthorized(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(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
agent->mPskdVerified = true;
agent->mPskcVerified = false;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
agent->mPskcVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction |
kClassApplication));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
testFreeInstance(instance);
}
static void TestTcatCommissioner2AuthWithDeviceRequirements(void)
{
Instance *instance = TestInitInstanceTcat();
TcatAgent *agent = &instance->Get<TcatAgent>();
// 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(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
// PSKd proof
agent->mPskdVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
// Network Name match
MockNetworkName(agent, true, &sCommNetworkName);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
// XPAN ID match
MockExtPanId(agent, true, &sCommExtPanId);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(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(SetActiveDatasetAuthorized(agent, sFullDataset));
// the partial dataset cannot be written, because it lacks the required Network Name and XPAN ID combo.
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
// PSKc proof
agent->mPskcVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral));
VerifyOrQuit(SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
// Try write a full dataset with differing XPAN ID - this fails
sFullDataset.mExtendedPanId.m8[2]++;
VerifyOrQuit(!SetActiveDatasetAuthorized(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<ActiveDatasetManager>().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(!SetActiveDatasetAuthorized(agent, sFullDataset));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sPartialDataset));
testFreeInstance(instance);
}
static void TestTcatCommissioner4AuthWithExistingPartialDataset(void)
{
Instance *instance = TestInitInstanceTcat();
TcatAgent *agent = &instance->Get<TcatAgent>();
// TCAT device was commissioned earlier on with a partial dataset.
instance->Get<ActiveDatasetManager>().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(!SetActiveDatasetAuthorized(agent, sFullDataset));
// PSKc proof - satisfies 0x21. Set Active Dataset is not allowed: Device already commissioned.
agent->mPskcVerified = true;
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning));
VerifyOrQuit(!SetActiveDatasetAuthorized(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<ActiveDatasetManager>().SaveLocal(sFullDataset);
VerifyOrQuit(CommandClassesAuthorized(agent, kClassGeneral | kClassCommissioning | kClassExtraction |
kClassDecommissioning));
VerifyOrQuit(!SetActiveDatasetAuthorized(agent, sFullDataset));
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::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();
printf("All tests passed\n");
#else
printf("TCAT feature is not enabled\n");
#endif
return 0;
}