blob: 057e28587fc676dffeb408a92dabd8663ee8c61d [file]
/*
*
* Copyright (c) 2026 Project CHIP Authors
* All rights reserved.
*
* 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.
*/
/**
* @file
* FuzzTest harness for GroupDataProviderImpl's group, key-set and endpoint
* management API, reached in production from the GroupKeyManagement and Groups
* clusters.
*
* Driven as an operation sequence because the behaviour worth exercising is the
* index arithmetic and the persisted TLV round-trip across operations, not any
* single call. State persists between inputs.
*
* The same provider's receive-side key lookup is covered by
* FuzzSessionManagerGroupPW.
*/
#include <cstdint>
#include <cstring>
#include <mutex>
#include <optional>
#include <tuple>
#include <vector>
#include <fuzztest/fuzztest_core.h>
#include <gtest/gtest.h>
#include <credentials/GroupDataProviderImpl.h>
#include <crypto/CHIPCryptoPAL.h>
#include <crypto/DefaultSessionKeystore.h>
#if CHIP_CRYPTO_PSA
#include <psa/crypto.h>
#endif
#include <lib/core/CHIPCore.h>
#include <lib/support/AutoRelease.h>
#include <lib/support/CHIPMem.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/TestPersistentStorageDelegate.h>
namespace {
using namespace chip;
using namespace chip::Credentials;
using namespace fuzztest;
using GroupInfo = GroupDataProvider::GroupInfo;
using GroupKey = GroupDataProvider::GroupKey;
using KeySet = GroupDataProvider::KeySet;
// No FabricTable: nothing here resolves the index to a fabric.
constexpr FabricIndex kFuzzFabric = 1;
// Reused across inputs so a sequence acts on accumulated state; the pools are fixed-size,
// so it cannot grow without bound. Storage left behind by RemoveFabric therefore builds up
// over many inputs, which a per-input reset would hide. The cost is that a crash may need its
// predecessors to reproduce; re-run the corpus in order.
struct Fixture
{
TestPersistentStorageDelegate storage;
Crypto::DefaultSessionKeystore keystore;
GroupDataProviderImpl provider{ /*maxGroupsPerFabric*/ 5, /*maxGroupKeysPerFabric*/ 8 };
};
Fixture & GetFixture()
{
static Fixture * fixture = nullptr;
static std::once_flag once;
std::call_once(once, [] {
VerifyOrDie(chip::Platform::MemoryInit() == CHIP_NO_ERROR);
#if CHIP_CRYPTO_PSA
// A fuzz binary links gmock_main, which does not init PSA; key derivation needs it.
VerifyOrDie(psa_crypto_init() == PSA_SUCCESS);
#endif
auto * fx = new Fixture();
fx->provider.SetStorageDelegate(&fx->storage);
fx->provider.SetSessionKeystore(&fx->keystore);
VerifyOrDie(fx->provider.Init() == CHIP_NO_ERROR);
fixture = fx;
// The fixture is leaked, so Finish() needs an exit hook rather than a destructor.
std::atexit([] {
if (fixture != nullptr)
{
fixture->provider.Finish();
}
});
});
return *fixture;
}
void ProviderSequenceDoesNotCrash(const std::vector<std::tuple<uint8_t, uint16_t, uint16_t, uint16_t>> & ops)
{
GroupDataProvider * provider = &GetFixture().provider;
constexpr FabricIndex fabric = kFuzzFabric;
for (const auto & op : ops)
{
const uint8_t selector = std::get<0>(op);
const uint16_t a = std::get<1>(op);
const uint16_t b = std::get<2>(op);
const uint16_t c = std::get<3>(op);
switch (selector % 24)
{
case 0: {
GroupInfo info(static_cast<GroupId>(a), "G");
(void) provider->SetGroupInfo(fabric, info);
break;
}
case 1: {
GroupInfo info;
(void) provider->GetGroupInfo(fabric, static_cast<GroupId>(a), info);
break;
}
case 2:
(void) provider->RemoveGroupInfo(fabric, static_cast<GroupId>(a));
break;
case 3: {
GroupInfo info(static_cast<GroupId>(a), "G");
if (provider->SetGroupInfoAt(fabric, b, info) == CHIP_NO_ERROR)
{
// A stored entry must read back at the index it was written to.
GroupInfo readBack;
ASSERT_EQ(provider->GetGroupInfoAt(fabric, b, readBack), CHIP_NO_ERROR);
ASSERT_EQ(readBack.group_id, info.group_id);
}
break;
}
case 4: {
GroupInfo info;
(void) provider->GetGroupInfoAt(fabric, b, info);
break;
}
case 5:
(void) provider->RemoveGroupInfoAt(fabric, b);
break;
case 6:
if (provider->AddEndpoint(fabric, static_cast<GroupId>(a), static_cast<EndpointId>(b)) == CHIP_NO_ERROR)
{
ASSERT_TRUE(provider->HasEndpoint(fabric, static_cast<GroupId>(a), static_cast<EndpointId>(b)));
}
break;
case 7:
if (provider->RemoveEndpoint(fabric, static_cast<GroupId>(a), static_cast<EndpointId>(b)) == CHIP_NO_ERROR)
{
ASSERT_FALSE(provider->HasEndpoint(fabric, static_cast<GroupId>(a), static_cast<EndpointId>(b)));
}
break;
case 8:
(void) provider->HasEndpoint(fabric, static_cast<GroupId>(a), static_cast<EndpointId>(b));
break;
case 9:
(void) provider->RemoveEndpointAllGroups(fabric, static_cast<EndpointId>(b),
(c & 1) ? GroupDataProvider::GroupCleanupPolicy::kKeepGroupIfEmpty
: GroupDataProvider::GroupCleanupPolicy::kDeleteGroupIfEmpty);
break;
case 10:
(void) provider->RemoveEndpoints(fabric, static_cast<GroupId>(a));
break;
case 11:
if (provider->SetGroupKey(fabric, static_cast<GroupId>(a), c) == CHIP_NO_ERROR)
{
KeysetId readBack = 0;
ASSERT_EQ(provider->GetGroupKey(fabric, static_cast<GroupId>(a), readBack), CHIP_NO_ERROR);
ASSERT_EQ(readBack, c);
}
break;
case 12: {
GroupKey key(static_cast<GroupId>(a), c);
(void) provider->SetGroupKeyAt(fabric, b, key);
break;
}
case 13: {
KeysetId keysetId = 0;
(void) provider->GetGroupKey(fabric, static_cast<GroupId>(a), keysetId);
break;
}
case 14: {
GroupKey key;
(void) provider->GetGroupKeyAt(fabric, b, key);
break;
}
case 15:
(void) provider->RemoveGroupKeyAt(fabric, b);
break;
case 16: {
// c is the keyset id; a picks how many epoch keys are stored.
KeySet keySet(c, GroupDataProvider::SecurityPolicy::kTrustFirst, static_cast<uint8_t>(1 + (a % KeySet::kEpochKeysMax)));
for (uint8_t i = 0; i < keySet.num_keys_used; i++)
{
memset(keySet.epoch_keys[i].key, static_cast<int>(a + i), sizeof(keySet.epoch_keys[i].key));
keySet.epoch_keys[i].start_time = b;
}
const uint8_t compressedFabricId[8] = { 0xC3, 0x50, 0xBA, 0x91, 0x42, 0xE1, 0x6E, 0xF6 };
(void) provider->SetKeySet(fabric, ByteSpan(compressedFabricId), keySet);
break;
}
case 17: {
KeySet keySet;
(void) provider->GetKeySet(fabric, c, keySet);
break;
}
case 18:
(void) provider->RemoveKeySet(fabric, c);
break;
case 19: {
// AutoRelease matters: the iterator pools are fixed-size.
if (AutoRelease it(provider->IterateGroupInfo(fabric)); it)
{
GroupInfo info;
(void) it->Count();
while (it->Next(info))
{
}
}
if (AutoRelease it(provider->IterateGroupKeys(fabric)); it)
{
GroupKey key;
(void) it->Count();
while (it->Next(key))
{
}
}
if (AutoRelease it(provider->IterateKeySets(fabric)); it)
{
KeySet keySet;
(void) it->Count();
while (it->Next(keySet))
{
}
}
if (AutoRelease it(
provider->IterateEndpoints(fabric, (a & 1) ? std::make_optional(static_cast<GroupId>(b)) : std::nullopt));
it)
{
GroupDataProvider::GroupEndpoint endpoint;
(void) it->Count();
while (it->Next(endpoint))
{
}
}
break;
}
case 20:
(void) provider->RemoveFabric(fabric);
break;
case 21:
(void) provider->RemoveGroupKeys(fabric);
break;
case 22:
// The endpoint-only overload, distinct from the (group, endpoint) one.
(void) provider->RemoveEndpoint(fabric, static_cast<EndpointId>(b));
break;
case 23:
if (AutoRelease it(provider->IterateGroupSessions(a)); it)
{
GroupDataProvider::GroupSession session;
(void) it->Count();
while (it->Next(session))
{
}
}
break;
default:
break;
}
}
KeySet ipk;
(void) provider->GetIpkKeySet(fabric, ipk);
}
FUZZ_TEST(FuzzGroupDataProviderPW, ProviderSequenceDoesNotCrash)
.WithDomains(VectorOf(TupleOf(Arbitrary<uint8_t>(), Arbitrary<uint16_t>(), Arbitrary<uint16_t>(), Arbitrary<uint16_t>()))
.WithMaxSize(48));
} // namespace