blob: 1a6f8db69cd2409266651a88dff09ef7a81db455 [file]
/*
* Copyright (c) 2025 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <app/clusters/tls-certificate-management-server/TLSCertificateManagementCluster.h>
#include <clusters/TlsCertificateManagement/Attributes.h>
#include <clusters/TlsCertificateManagement/Metadata.h>
#include <pw_unit_test/framework.h>
#include <app/DefaultSafeAttributePersistenceProvider.h>
#include <app/SafeAttributePersistenceProvider.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <app/server-cluster/testing/AttributeTesting.h>
#include <app/server-cluster/testing/ClusterTester.h>
#include <app/server-cluster/testing/TestServerClusterContext.h>
#include <credentials/CHIPCert.h>
#include <credentials/FabricTable.h>
#include <crypto/CHIPCryptoPAL.h>
#include <lib/support/ReadOnlyBuffer.h>
#include <lib/support/TimeUtils.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::Tls;
using namespace chip::app::Clusters::TlsCertificateManagement;
using namespace chip::Testing;
namespace {
constexpr EndpointId kTestEndpointId = 1;
constexpr uint8_t kMaxRootCertificates = 10;
constexpr uint8_t kMaxClientCertificates = 10;
// Helper function to generate a valid test certificate
CHIP_ERROR GenerateTestCertificate(MutableByteSpan & certSpan)
{
using namespace Credentials;
using namespace Crypto;
// Generate a keypair for the certificate
P256Keypair keypair;
ReturnErrorOnFailure(keypair.Initialize(Crypto::ECPKeyTarget::ECDSA));
// Set up certificate parameters with valid dates
// validityStart = 1: Just after CHIP epoch (2000-01-01 00:00:01 UTC) - always in the past
// Note: 0 is special (kNullCertTime) and maps to 9999-12-31, which would fail
// validityEnd = 0: kNullCertTime maps to 9999-12-31 23:59:59 UTC - always in the future
// This ensures the certificate passes IsCertificateValidAtCurrentTime regardless of system time
uint32_t validityStart = 1;
uint32_t validityEnd = kNullCertTime;
ChipDN subjectDN;
ReturnErrorOnFailure(subjectDN.AddAttribute_MatterRCACId(0x1234ABCD));
X509CertRequestParams certParams = { 1, validityStart, validityEnd, subjectDN, subjectDN };
// Generate the certificate
return NewRootX509Cert(certParams, keypair, certSpan);
}
// Mock implementations
class MockCertificateTable : public CertificateTable
{
public:
struct MockRootCert
{
FabricIndex fabric;
TLSCAID caid;
};
struct MockClientCert
{
FabricIndex fabric;
TLSCCDID ccdid;
};
std::vector<MockRootCert> rootCerts;
std::vector<MockClientCert> clientCerts;
CHIP_ERROR Init(PersistentStorageDelegate & storage) override { return CHIP_NO_ERROR; }
void Finish() override {}
CHIP_ERROR UpsertRootCertificateEntry(FabricIndex fabric_index, Optional<TLSCAID> & id, RootBuffer & buffer,
const ByteSpan & certificate) override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR GetRootCertificateEntry(FabricIndex fabric_index, TLSCAID id, BufferedRootCert & entry) override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR HasRootCertificateEntry(FabricIndex fabric_index, TLSCAID id) override
{
for (const auto & cert : rootCerts)
{
if (cert.fabric == fabric_index && cert.caid == id)
{
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR IterateRootCertificates(FabricIndex fabric, BufferedRootCert & store, IterateRootCertFnType iterateFn) override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR GetClientCertificateEntry(FabricIndex fabric_index, TLSCCDID id, BufferedClientCert & entry) override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR HasClientCertificateEntry(FabricIndex fabric_index, TLSCCDID id) override
{
for (const auto & cert : clientCerts)
{
if (cert.fabric == fabric_index && cert.ccdid == id)
{
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR IterateClientCertificates(FabricIndex fabric, BufferedClientCert & store, IterateClientCertFnType iterateFn) override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR RemoveRootCertificate(FabricIndex fabric_index, TLSCAID id) override { return CHIP_NO_ERROR; }
CHIP_ERROR GetRootCertificateCount(FabricIndex fabric, uint8_t & outCount) override
{
outCount = 0;
return CHIP_NO_ERROR;
}
CHIP_ERROR PrepareClientCertificate(FabricIndex fabric, const ByteSpan & nonce, ClientBuffer & buffer, Optional<TLSCCDID> & id,
MutableByteSpan & csr, MutableByteSpan & nonceSignature) override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR UpdateClientCertificateEntry(FabricIndex fabric_index, TLSCCDID id, ClientBuffer & buffer,
const ClientCertStruct & entry) override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR RemoveClientCertificate(FabricIndex fabric_index, TLSCCDID id) override { return CHIP_NO_ERROR; }
CHIP_ERROR GetClientCertificateCount(FabricIndex fabric, uint8_t & outCount) override
{
outCount = 0;
return CHIP_NO_ERROR;
}
CHIP_ERROR RemoveFabric(FabricIndex fabricIndex) override { return CHIP_NO_ERROR; }
};
class MockCertificateDependencyChecker : public CertificateDependencyChecker
{
public:
CHIP_ERROR RootCertCanBeRemoved(EndpointId matterEndpoint, FabricIndex fabric, TLSCAID id) override { return CHIP_NO_ERROR; }
CHIP_ERROR ClientCertCanBeRemoved(EndpointId matterEndpoint, FabricIndex fabric, TLSCCDID id) override { return CHIP_NO_ERROR; }
};
class MockTLSCertificateManagementDelegate : public TLSCertificateManagementDelegate
{
public:
struct MockRootCert
{
EndpointId matterEndpoint;
FabricIndex fabric;
TLSCAID caid;
std::vector<uint8_t> certificate;
std::vector<uint8_t> fingerprint;
};
struct MockClientCert
{
EndpointId matterEndpoint;
FabricIndex fabric;
TLSCCDID ccdid;
std::vector<uint8_t> certificate;
std::vector<uint8_t> fingerprint;
TLSCAID caid;
};
std::vector<MockRootCert> rootCerts;
std::vector<MockClientCert> clientCerts;
Protocols::InteractionModel::Status provisionRootStatus = Protocols::InteractionModel::Status::Success;
Protocols::InteractionModel::Status provisionClientStatus = Protocols::InteractionModel::Status::Success;
Protocols::InteractionModel::Status removeRootStatus = Protocols::InteractionModel::Status::Success;
Protocols::InteractionModel::Status removeClientStatus = Protocols::InteractionModel::Status::Success;
Protocols::InteractionModel::Status generateCsrStatus = Protocols::InteractionModel::Status::Success;
TLSCAID nextRootCertId = 1;
mutable TLSCCDID nextClientCertId = 1;
// Reference to mock certificate table for coordinating GenerateClientCsr with HasClientCertificateEntry
MockCertificateTable * mCertTable = nullptr;
void SetCertificateTable(MockCertificateTable * certTable) { mCertTable = certTable; }
Protocols::InteractionModel::Status ProvisionRootCert(EndpointId matterEndpoint, FabricIndex fabric,
const ProvisionRootCertificateType & provisionReq,
TLSCAID & outCaid) override
{
if (provisionRootStatus != Protocols::InteractionModel::Status::Success)
{
return provisionRootStatus;
}
outCaid = nextRootCertId++;
MockRootCert cert;
cert.matterEndpoint = matterEndpoint;
cert.fabric = fabric;
cert.caid = outCaid;
cert.certificate = std::vector<uint8_t>(provisionReq.certificate.data(),
provisionReq.certificate.data() + provisionReq.certificate.size());
rootCerts.push_back(cert);
return Protocols::InteractionModel::Status::Success;
}
CHIP_ERROR LoadedRootCerts(EndpointId matterEndpoint, FabricIndex fabric,
LoadedRootCertificateCallback loadedCallback) const override
{
for (const auto & cert : rootCerts)
{
if (cert.matterEndpoint == matterEndpoint && cert.fabric == fabric)
{
Structs::TLSCertStruct::Type decodable;
decodable.caid = cert.caid;
decodable.certificate.SetValue(ByteSpan(cert.certificate.data(), cert.certificate.size()));
ReturnErrorOnFailure(loadedCallback(decodable));
}
}
return CHIP_NO_ERROR;
}
CHIP_ERROR RootCertsForFabric(EndpointId matterEndpoint, FabricIndex fabric,
RootCertificateListCallback loadedCallback) const override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR FindRootCert(EndpointId matterEndpoint, FabricIndex fabric, TLSCAID id,
LoadedRootCertificateCallback loadedCallback) const override
{
for (const auto & cert : rootCerts)
{
if (cert.matterEndpoint == matterEndpoint && cert.fabric == fabric && cert.caid == id)
{
Structs::TLSCertStruct::Type decodable;
decodable.caid = cert.caid;
decodable.certificate.SetValue(ByteSpan(cert.certificate.data(), cert.certificate.size()));
return loadedCallback(decodable);
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR LookupRootCertByFingerprint(EndpointId matterEndpoint, FabricIndex fabric, const ByteSpan & fingerprint,
LoadedRootCertificateCallback loadedCallback) const override
{
for (const auto & cert : rootCerts)
{
if (cert.matterEndpoint == matterEndpoint && cert.fabric == fabric && cert.fingerprint.size() == fingerprint.size() &&
memcmp(cert.fingerprint.data(), fingerprint.data(), fingerprint.size()) == 0)
{
Structs::TLSCertStruct::Type decodable;
decodable.caid = cert.caid;
decodable.certificate.SetValue(ByteSpan(cert.certificate.data(), cert.certificate.size()));
return loadedCallback(decodable);
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR LookupRootCert(EndpointId matterEndpoint, FabricIndex fabric, const ByteSpan & cert,
LoadedRootCertificateCallback loadedCallback) const override
{
return CHIP_ERROR_NOT_FOUND;
}
Protocols::InteractionModel::Status RemoveRootCert(EndpointId matterEndpoint, FabricIndex fabric, TLSCAID id) override
{
if (removeRootStatus != Protocols::InteractionModel::Status::Success)
{
return removeRootStatus;
}
for (auto it = rootCerts.begin(); it != rootCerts.end(); ++it)
{
if (it->matterEndpoint == matterEndpoint && it->fabric == fabric && it->caid == id)
{
rootCerts.erase(it);
return Protocols::InteractionModel::Status::Success;
}
}
return Protocols::InteractionModel::Status::NotFound;
}
Protocols::InteractionModel::Status GenerateClientCsr(EndpointId matterEndpoint, FabricIndex fabric,
const ClientCsrType & request,
GeneratedCsrCallback loadedCallback) const override
{
if (generateCsrStatus != Protocols::InteractionModel::Status::Success)
{
return generateCsrStatus;
}
ClientCsrResponseType response;
uint8_t dummyCsr[32] = { 0x01, 0x02, 0x03 };
uint8_t dummySig[32] = { 0x04, 0x05, 0x06 };
response.csr = ByteSpan(dummyCsr, sizeof(dummyCsr));
response.nonceSignature = ByteSpan(dummySig, sizeof(dummySig));
// Allocate a ccdid and add entry to certificate table (simulates real delegate behavior)
TLSCCDID ccdid = nextClientCertId++;
if (mCertTable != nullptr)
{
mCertTable->clientCerts.push_back({ fabric, ccdid });
}
response.ccdid = ccdid;
return loadedCallback(response);
}
Protocols::InteractionModel::Status ProvisionClientCert(EndpointId matterEndpoint, FabricIndex fabric,
const ProvisionClientCertificateType & provisionReq) override
{
if (provisionClientStatus != Protocols::InteractionModel::Status::Success)
{
return provisionClientStatus;
}
MockClientCert cert;
cert.matterEndpoint = matterEndpoint;
cert.fabric = fabric;
cert.ccdid = provisionReq.ccdid;
cert.certificate = std::vector<uint8_t>(provisionReq.clientCertificate.data(),
provisionReq.clientCertificate.data() + provisionReq.clientCertificate.size());
clientCerts.push_back(cert);
return Protocols::InteractionModel::Status::Success;
}
CHIP_ERROR LoadedClientCerts(EndpointId matterEndpoint, FabricIndex fabric,
LoadedClientCertificateCallback loadedCallback) const override
{
for (const auto & cert : clientCerts)
{
if (cert.matterEndpoint == matterEndpoint && cert.fabric == fabric)
{
Structs::TLSClientCertificateDetailStruct::Type decodable;
decodable.ccdid = cert.ccdid;
decodable.clientCertificate.SetValue(
DataModel::MakeNullable(ByteSpan(cert.certificate.data(), cert.certificate.size())));
ReturnErrorOnFailure(loadedCallback(decodable));
}
}
return CHIP_NO_ERROR;
}
CHIP_ERROR ClientCertsForFabric(EndpointId matterEndpoint, FabricIndex fabric,
ClientCertificateListCallback loadedCallback) const override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR FindClientCert(EndpointId matterEndpoint, FabricIndex fabric, TLSCCDID id,
LoadedClientCertificateCallback loadedCallback) const override
{
for (const auto & cert : clientCerts)
{
if (cert.matterEndpoint == matterEndpoint && cert.fabric == fabric && cert.ccdid == id)
{
Structs::TLSClientCertificateDetailStruct::Type decodable;
decodable.ccdid = cert.ccdid;
decodable.clientCertificate.SetValue(
DataModel::MakeNullable(ByteSpan(cert.certificate.data(), cert.certificate.size())));
return loadedCallback(decodable);
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR LookupClientCertByFingerprint(EndpointId matterEndpoint, FabricIndex fabric, const ByteSpan & fingerprint,
LoadedClientCertificateCallback loadedCallback) const override
{
for (const auto & cert : clientCerts)
{
if (cert.matterEndpoint == matterEndpoint && cert.fabric == fabric && cert.fingerprint.size() == fingerprint.size() &&
memcmp(cert.fingerprint.data(), fingerprint.data(), fingerprint.size()) == 0)
{
Structs::TLSClientCertificateDetailStruct::Type decodable;
decodable.ccdid = cert.ccdid;
decodable.clientCertificate.SetValue(
DataModel::MakeNullable(ByteSpan(cert.certificate.data(), cert.certificate.size())));
return loadedCallback(decodable);
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR LookupClientCert(EndpointId matterEndpoint, FabricIndex fabric, const ByteSpan & certificate,
LoadedClientCertificateCallback loadedCallback) const override
{
return CHIP_ERROR_NOT_FOUND;
}
Protocols::InteractionModel::Status RemoveClientCert(EndpointId matterEndpoint, FabricIndex fabric, TLSCCDID id) override
{
if (removeClientStatus != Protocols::InteractionModel::Status::Success)
{
return removeClientStatus;
}
for (auto it = clientCerts.begin(); it != clientCerts.end(); ++it)
{
if (it->matterEndpoint == matterEndpoint && it->fabric == fabric && it->ccdid == id)
{
clientCerts.erase(it);
return Protocols::InteractionModel::Status::Success;
}
}
return Protocols::InteractionModel::Status::NotFound;
}
};
struct TestTLSCertificateManagementCluster : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { Platform::MemoryShutdown(); }
void SetUp() override
{
VerifyOrDie(mPersistenceProvider.Init(&mClusterTester.GetServerClusterContext().storage) == CHIP_NO_ERROR);
app::SetSafeAttributePersistenceProvider(&mPersistenceProvider);
// Wire up mock delegate to mock certificate table so GenerateClientCsr adds entries
mMockDelegate.SetCertificateTable(&mMockCertTable);
// Add some test root certificates for the test fabric index (151)
constexpr FabricIndex kMockTestFabric = static_cast<FabricIndex>(151);
mMockCertTable.rootCerts.push_back({ kMockTestFabric, 1 });
mMockCertTable.rootCerts.push_back({ kMockTestFabric, 2 });
EXPECT_EQ(mCluster.Startup(mClusterTester.GetServerClusterContext()), CHIP_NO_ERROR);
}
void TearDown() override
{
mCluster.Shutdown(ClusterShutdownType::kClusterShutdown);
app::SetSafeAttributePersistenceProvider(nullptr);
}
MockTLSCertificateManagementDelegate mMockDelegate;
MockCertificateTable mMockCertTable;
MockCertificateDependencyChecker mMockDependencyChecker;
FabricTable mFabricTable;
TLSCertificateManagementCluster mCluster{ { mFabricTable }, kTestEndpointId, mMockDelegate, mMockDependencyChecker,
mMockCertTable, kMaxRootCertificates, kMaxClientCertificates };
ClusterTester mClusterTester{ mCluster };
app::DefaultSafeAttributePersistenceProvider mPersistenceProvider;
};
TEST_F(TestTLSCertificateManagementCluster, TestReadMaxRootCertificates)
{
uint8_t maxRootCertificates = 0;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::MaxRootCertificates::Id, maxRootCertificates), CHIP_NO_ERROR);
EXPECT_EQ(maxRootCertificates, kMaxRootCertificates);
}
TEST_F(TestTLSCertificateManagementCluster, TestReadMaxClientCertificates)
{
uint8_t maxClientCertificates = 0;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::MaxClientCertificates::Id, maxClientCertificates), CHIP_NO_ERROR);
EXPECT_EQ(maxClientCertificates, kMaxClientCertificates);
}
TEST_F(TestTLSCertificateManagementCluster, TestReadClusterRevision)
{
uint16_t clusterRevision = 0;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::ClusterRevision::Id, clusterRevision), CHIP_NO_ERROR);
EXPECT_EQ(clusterRevision, kRevision);
}
TEST_F(TestTLSCertificateManagementCluster, TestReadFeatureMap)
{
uint32_t featureMap = 1;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, 0u);
}
TEST_F(TestTLSCertificateManagementCluster, TestReadProvisionedRootCertificatesEmpty)
{
Attributes::ProvisionedRootCertificates::TypeInfo::DecodableType list;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::ProvisionedRootCertificates::Id, list), CHIP_NO_ERROR);
auto it = list.begin();
EXPECT_FALSE(it.Next());
}
TEST_F(TestTLSCertificateManagementCluster, TestReadProvisionedClientCertificatesEmpty)
{
Attributes::ProvisionedClientCertificates::TypeInfo::DecodableType list;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::ProvisionedClientCertificates::Id, list), CHIP_NO_ERROR);
auto it = list.begin();
EXPECT_FALSE(it.Next());
}
TEST_F(TestTLSCertificateManagementCluster, TestProvisionRootCertificateSuccess)
{
// Generate a valid test certificate
uint8_t certBuffer[Credentials::kMaxDERCertLength];
MutableByteSpan certSpan(certBuffer);
CHIP_ERROR certGenErr = GenerateTestCertificate(certSpan);
ASSERT_EQ(certGenErr, CHIP_NO_ERROR);
EXPECT_GT(certSpan.size(), 0u);
ChipLogProgress(Zcl, "Generated certificate of size %u", static_cast<unsigned>(certSpan.size()));
Commands::ProvisionRootCertificate::Type request;
request.certificate = certSpan;
auto result = mClusterTester.Invoke<Commands::ProvisionRootCertificate::Type>(request);
EXPECT_TRUE(result.IsSuccess());
if (!result.IsSuccess())
{
if (result.status.has_value())
{
ChipLogError(Zcl, "ProvisionRootCertificate failed with status: 0x%x",
to_underlying(result.status.value().GetStatusCode().GetStatus()));
}
else
{
ChipLogError(Zcl, "ProvisionRootCertificate failed with no status code");
}
}
if (result.response.has_value())
{
EXPECT_EQ(result.response.value().caid, 1);
}
// Verify certificate was added
EXPECT_EQ(mMockDelegate.rootCerts.size(), 1u);
if (mMockDelegate.rootCerts.size() > 0)
{
EXPECT_EQ(mMockDelegate.rootCerts[0].caid, 1);
}
}
TEST_F(TestTLSCertificateManagementCluster, TestFindRootCertificateSuccess)
{
// Generate a valid test certificate
uint8_t certBuffer[Credentials::kMaxDERCertLength];
MutableByteSpan certSpan(certBuffer);
EXPECT_EQ(GenerateTestCertificate(certSpan), CHIP_NO_ERROR);
// First provision a certificate
Commands::ProvisionRootCertificate::Type provisionReq;
provisionReq.certificate = certSpan;
auto provisionResult = mClusterTester.Invoke<Commands::ProvisionRootCertificate::Type>(provisionReq);
EXPECT_TRUE(provisionResult.IsSuccess());
ASSERT_TRUE(provisionResult.response.has_value());
TLSCAID provisionedId = 0;
if (provisionResult.response.has_value())
{
provisionedId = provisionResult.response.value().caid;
}
// Now find it
Commands::FindRootCertificate::Type findReq;
findReq.caid = provisionedId;
auto findResult = mClusterTester.Invoke<Commands::FindRootCertificate::Type>(findReq);
EXPECT_TRUE(findResult.IsSuccess());
if (findResult.response.has_value())
{
auto iter = findResult.response.value().certificateDetails.begin();
ASSERT_TRUE(iter.Next());
EXPECT_EQ(iter.GetValue().caid, provisionedId);
EXPECT_TRUE(iter.GetValue().certificate.Value().data_equal(certSpan));
}
}
TEST_F(TestTLSCertificateManagementCluster, TestFindRootCertificateNotFound)
{
Commands::FindRootCertificate::Type request;
request.caid = 999; // Non-existent certificate
auto result = mClusterTester.Invoke<Commands::FindRootCertificate::Type>(request);
EXPECT_FALSE(result.IsSuccess());
if (result.status.has_value())
{
EXPECT_EQ(result.status.value().GetStatusCode().GetStatus(), Protocols::InteractionModel::Status::NotFound);
}
}
TEST_F(TestTLSCertificateManagementCluster, TestRemoveRootCertificateSuccess)
{
// Generate a valid test certificate
uint8_t certBuffer[Credentials::kMaxDERCertLength];
MutableByteSpan certSpan(certBuffer);
EXPECT_EQ(GenerateTestCertificate(certSpan), CHIP_NO_ERROR);
// First provision a certificate
Commands::ProvisionRootCertificate::Type provisionReq;
provisionReq.certificate = certSpan;
auto provisionResult = mClusterTester.Invoke<Commands::ProvisionRootCertificate::Type>(provisionReq);
EXPECT_TRUE(provisionResult.IsSuccess());
ASSERT_TRUE(provisionResult.response.has_value());
TLSCAID provisionedId = 0;
if (provisionResult.response.has_value())
{
provisionedId = provisionResult.response.value().caid;
}
EXPECT_EQ(mMockDelegate.rootCerts.size(), 1u);
// Now remove it
Commands::RemoveRootCertificate::Type removeReq;
removeReq.caid = provisionedId;
auto removeResult = mClusterTester.Invoke<Commands::RemoveRootCertificate::Type>(removeReq);
EXPECT_TRUE(removeResult.IsSuccess());
// Verify it was removed
EXPECT_EQ(mMockDelegate.rootCerts.size(), 0u);
}
TEST_F(TestTLSCertificateManagementCluster, TestRemoveRootCertificateNotFound)
{
Commands::RemoveRootCertificate::Type request;
request.caid = 999; // Non-existent certificate
auto result = mClusterTester.Invoke<Commands::RemoveRootCertificate::Type>(request);
EXPECT_FALSE(result.IsSuccess());
if (result.status.has_value())
{
EXPECT_EQ(result.status.value().GetStatusCode().GetStatus(), Protocols::InteractionModel::Status::NotFound);
}
}
TEST_F(TestTLSCertificateManagementCluster, TestGenerateClientCsrSuccess)
{
Commands::ClientCSR::Type request;
uint8_t dummyNonce[32] = { 0x01, 0x02, 0x03 };
request.nonce = ByteSpan(dummyNonce, sizeof(dummyNonce));
auto result = mClusterTester.Invoke<Commands::ClientCSR::Type>(request);
EXPECT_TRUE(result.IsSuccess());
if (result.response.has_value())
{
EXPECT_GT(result.response.value().csr.size(), 0u);
EXPECT_GT(result.response.value().nonceSignature.size(), 0u);
EXPECT_EQ(result.response.value().ccdid, 1);
}
}
TEST_F(TestTLSCertificateManagementCluster, TestProvisionClientCertificateSuccess)
{
// Step 1: Generate a CSR to create the key pair and get the ccdid
Commands::ClientCSR::Type csrRequest;
uint8_t dummyNonce[32] = { 0x01, 0x02, 0x03 };
csrRequest.nonce = ByteSpan(dummyNonce, sizeof(dummyNonce));
auto csrResult = mClusterTester.Invoke<Commands::ClientCSR::Type>(csrRequest);
EXPECT_TRUE(csrResult.IsSuccess());
ASSERT_TRUE(csrResult.response.has_value());
TLSCCDID ccdid = 0;
if (csrResult.response.has_value())
{
ccdid = csrResult.response.value().ccdid;
}
// Step 2: Generate a valid test certificate
uint8_t certBuffer[Credentials::kMaxDERCertLength];
MutableByteSpan certSpan(certBuffer);
EXPECT_EQ(GenerateTestCertificate(certSpan), CHIP_NO_ERROR);
// Step 3: Provision the client certificate using the ccdid from the CSR
Commands::ProvisionClientCertificate::Type request;
request.clientCertificate = certSpan;
request.ccdid = ccdid;
auto result = mClusterTester.Invoke<Commands::ProvisionClientCertificate::Type>(request);
EXPECT_TRUE(result.IsSuccess());
// Verify certificate was added
EXPECT_EQ(mMockDelegate.clientCerts.size(), 1u);
if (mMockDelegate.clientCerts.size() > 0)
{
EXPECT_EQ(mMockDelegate.clientCerts[0].ccdid, ccdid);
}
}
TEST_F(TestTLSCertificateManagementCluster, TestFindClientCertificateSuccess)
{
// Step 1: Generate a CSR to create the key pair and get the ccdid
Commands::ClientCSR::Type csrRequest;
uint8_t dummyNonce[32] = { 0x01, 0x02, 0x03 };
csrRequest.nonce = ByteSpan(dummyNonce, sizeof(dummyNonce));
auto csrResult = mClusterTester.Invoke<Commands::ClientCSR::Type>(csrRequest);
EXPECT_TRUE(csrResult.IsSuccess());
ASSERT_TRUE(csrResult.response.has_value());
TLSCCDID ccdid = 0;
if (csrResult.response.has_value())
{
ccdid = csrResult.response.value().ccdid;
}
// Step 2: Generate a valid test client certificate
uint8_t certBuffer[Credentials::kMaxDERCertLength];
MutableByteSpan certSpan(certBuffer);
EXPECT_EQ(GenerateTestCertificate(certSpan), CHIP_NO_ERROR);
// Step 3: Provision the client certificate
Commands::ProvisionClientCertificate::Type provisionReq;
provisionReq.clientCertificate = certSpan;
provisionReq.ccdid = ccdid;
auto provisionResult = mClusterTester.Invoke<Commands::ProvisionClientCertificate::Type>(provisionReq);
EXPECT_TRUE(provisionResult.IsSuccess());
// Now find it
Commands::FindClientCertificate::Type findReq;
findReq.ccdid = ccdid;
auto findResult = mClusterTester.Invoke<Commands::FindClientCertificate::Type>(findReq);
EXPECT_TRUE(findResult.IsSuccess());
if (findResult.response.has_value())
{
auto iter = findResult.response.value().certificateDetails.begin();
ASSERT_TRUE(iter.Next());
EXPECT_EQ(iter.GetValue().ccdid, ccdid);
EXPECT_TRUE(iter.GetValue().clientCertificate.Value().Value().data_equal(certSpan));
}
}
TEST_F(TestTLSCertificateManagementCluster, TestFindClientCertificateNotFound)
{
Commands::FindClientCertificate::Type request;
request.ccdid = 999; // Non-existent certificate
auto result = mClusterTester.Invoke<Commands::FindClientCertificate::Type>(request);
EXPECT_FALSE(result.IsSuccess());
if (result.status.has_value())
{
EXPECT_EQ(result.status.value().GetStatusCode().GetStatus(), Protocols::InteractionModel::Status::NotFound);
}
}
TEST_F(TestTLSCertificateManagementCluster, TestRemoveClientCertificateSuccess)
{
// Step 1: Generate a CSR to create the key pair and get the ccdid
Commands::ClientCSR::Type csrRequest;
uint8_t dummyNonce[32] = { 0x01, 0x02, 0x03 };
csrRequest.nonce = ByteSpan(dummyNonce, sizeof(dummyNonce));
auto csrResult = mClusterTester.Invoke<Commands::ClientCSR::Type>(csrRequest);
EXPECT_TRUE(csrResult.IsSuccess());
ASSERT_TRUE(csrResult.response.has_value());
TLSCCDID ccdid = 0;
if (csrResult.response.has_value())
{
ccdid = csrResult.response.value().ccdid;
}
// Step 2: Generate a valid test client certificate
uint8_t certBuffer[Credentials::kMaxDERCertLength];
MutableByteSpan certSpan(certBuffer);
EXPECT_EQ(GenerateTestCertificate(certSpan), CHIP_NO_ERROR);
// Step 3: Provision the client certificate
Commands::ProvisionClientCertificate::Type provisionReq;
provisionReq.clientCertificate = certSpan;
provisionReq.ccdid = ccdid;
auto provisionResult = mClusterTester.Invoke<Commands::ProvisionClientCertificate::Type>(provisionReq);
EXPECT_TRUE(provisionResult.IsSuccess());
EXPECT_EQ(mMockDelegate.clientCerts.size(), 1u);
// Now remove it
Commands::RemoveClientCertificate::Type removeReq;
removeReq.ccdid = ccdid;
auto removeResult = mClusterTester.Invoke<Commands::RemoveClientCertificate::Type>(removeReq);
EXPECT_TRUE(removeResult.IsSuccess());
// Verify it was removed
EXPECT_EQ(mMockDelegate.clientCerts.size(), 0u);
}
TEST_F(TestTLSCertificateManagementCluster, TestRemoveClientCertificateNotFound)
{
Commands::RemoveClientCertificate::Type request;
request.ccdid = 999; // Non-existent certificate
auto result = mClusterTester.Invoke<Commands::RemoveClientCertificate::Type>(request);
EXPECT_FALSE(result.IsSuccess());
if (result.status.has_value())
{
EXPECT_EQ(result.status.value().GetStatusCode().GetStatus(), Protocols::InteractionModel::Status::NotFound);
}
}
} // namespace