| /* | |
| * | |
| * 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 |