blob: c224fbdbf31f37702eb76a0e726a84c1599f18c2 [file]
/*
*
* Copyright (c) 2026 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 <pw_unit_test/framework.h>
#include <controller/CertificateChainRequestTracker.h>
using namespace chip;
using namespace chip::Controller;
namespace {
TEST(CertificateChainRequestTracker, HandlesSingleResponse)
{
CertificateChainRequestTracker tracker;
const uint8_t certificateBytes[] = { 0x01, 0x02, 0x03, 0x04 };
ASSERT_EQ(tracker.HandleResponse(ByteSpan(certificateBytes), NullOptional, NullOptional), CHIP_NO_ERROR);
EXPECT_TRUE(tracker.IsComplete());
EXPECT_FALSE(tracker.IsSegmentedTransfer());
EXPECT_TRUE(tracker.GetCertificate().data_equal(ByteSpan(certificateBytes)));
}
TEST(CertificateChainRequestTracker, ReassemblesSegmentedResponse)
{
CertificateChainRequestTracker tracker;
const uint8_t firstSegment[] = { 0x10, 0x11, 0x12 };
const uint8_t secondSegment[] = { 0x13, 0x14 };
ASSERT_EQ(tracker.HandleResponse(ByteSpan(firstSegment), MakeOptional<uint16_t>(static_cast<uint16_t>(5)),
MakeOptional<uint16_t>(static_cast<uint16_t>(1))),
CHIP_NO_ERROR);
EXPECT_FALSE(tracker.IsComplete());
ASSERT_TRUE(tracker.NextSegmentId().HasValue());
EXPECT_EQ(tracker.NextSegmentId().Value(), 1);
ASSERT_EQ(tracker.HandleResponse(ByteSpan(secondSegment), MakeOptional<uint16_t>(static_cast<uint16_t>(5)), NullOptional),
CHIP_NO_ERROR);
EXPECT_TRUE(tracker.IsComplete());
EXPECT_FALSE(tracker.HasPendingSegment());
const uint8_t expected[] = { 0x10, 0x11, 0x12, 0x13, 0x14 };
EXPECT_TRUE(tracker.GetCertificate().data_equal(ByteSpan(expected)));
}
TEST(CertificateChainRequestTracker, ReassemblesCertificateLargerThanLegacyLimit)
{
// A legacy PAI subject key may have an ML-DSA issuer signature. Response metadata,
// rather than the request profile, determines how many segments to assemble.
CertificateChainRequestTracker tracker;
uint8_t certificate[1300];
for (size_t i = 0; i < sizeof(certificate); ++i)
{
certificate[i] = static_cast<uint8_t>(i);
}
const ByteSpan document(certificate);
constexpr uint16_t segmentSize = CertificateChainRequestTracker::kDefaultSegmentSize;
for (uint16_t segment = 0; segment < 3; ++segment)
{
const size_t offset = static_cast<size_t>(segment) * segmentSize;
const bool last = segment == 2;
ASSERT_EQ(tracker.HandleResponse(document.SubSpan(offset, last ? sizeof(certificate) - offset : segmentSize),
MakeOptional<uint16_t>(static_cast<uint16_t>(sizeof(certificate))),
last ? NullOptional : MakeOptional<uint16_t>(static_cast<uint16_t>(segment + 1))),
CHIP_NO_ERROR);
EXPECT_EQ(tracker.IsComplete(), last);
}
EXPECT_TRUE(tracker.GetCertificate().data_equal(document));
}
TEST(CertificateChainRequestTracker, RejectsEmptyFirstSegmentWithoutChangingState)
{
CertificateChainRequestTracker tracker;
const auto totalSize = MakeOptional<uint16_t>(static_cast<uint16_t>(2));
const auto nextId = MakeOptional<uint16_t>(static_cast<uint16_t>(1));
// A continuation must make progress instead of allowing an unbounded sequence of empty responses.
EXPECT_EQ(tracker.HandleResponse(ByteSpan(), totalSize, nextId), CHIP_ERROR_INVALID_ARGUMENT);
EXPECT_FALSE(tracker.IsComplete());
EXPECT_FALSE(tracker.IsSegmentedTransfer());
EXPECT_FALSE(tracker.HasPendingSegment());
EXPECT_TRUE(tracker.GetCertificate().empty());
const uint8_t certificate[] = { 0x10, 0x11 };
ASSERT_EQ(tracker.HandleResponse(ByteSpan(certificate), totalSize, NullOptional), CHIP_NO_ERROR);
EXPECT_TRUE(tracker.IsComplete());
EXPECT_TRUE(tracker.GetCertificate().data_equal(ByteSpan(certificate)));
}
TEST(CertificateChainRequestTracker, RejectsEmptyContinuationWithoutChangingState)
{
CertificateChainRequestTracker tracker;
const auto totalSize = MakeOptional<uint16_t>(static_cast<uint16_t>(3));
const uint8_t firstSegment[] = { 0x10 };
ASSERT_EQ(tracker.HandleResponse(ByteSpan(firstSegment), totalSize, MakeOptional<uint16_t>(static_cast<uint16_t>(1))),
CHIP_NO_ERROR);
EXPECT_EQ(tracker.HandleResponse(ByteSpan(), totalSize, MakeOptional<uint16_t>(static_cast<uint16_t>(2))),
CHIP_ERROR_INVALID_ARGUMENT);
EXPECT_FALSE(tracker.IsComplete());
EXPECT_TRUE(tracker.IsSegmentedTransfer());
ASSERT_TRUE(tracker.NextSegmentId().HasValue());
EXPECT_EQ(tracker.NextSegmentId().Value(), 1);
EXPECT_TRUE(tracker.GetCertificate().empty());
// Continuing at the same segment ID proves that the rejected response consumed neither an ID nor bytes.
const uint8_t secondSegment[] = { 0x11 };
ASSERT_EQ(tracker.HandleResponse(ByteSpan(secondSegment), totalSize, MakeOptional<uint16_t>(static_cast<uint16_t>(2))),
CHIP_NO_ERROR);
const uint8_t finalSegment[] = { 0x12 };
ASSERT_EQ(tracker.HandleResponse(ByteSpan(finalSegment), totalSize, NullOptional), CHIP_NO_ERROR);
EXPECT_TRUE(tracker.IsComplete());
EXPECT_FALSE(tracker.HasPendingSegment());
const uint8_t expected[] = { 0x10, 0x11, 0x12 };
EXPECT_TRUE(tracker.GetCertificate().data_equal(ByteSpan(expected)));
}
TEST(CertificateChainRequestTracker, RejectsInconsistentSegmentedResponse)
{
CertificateChainRequestTracker tracker;
const uint8_t firstSegment[] = { 0x20, 0x21, 0x22 };
ASSERT_EQ(tracker.HandleResponse(ByteSpan(firstSegment), MakeOptional<uint16_t>(static_cast<uint16_t>(5)),
MakeOptional<uint16_t>(static_cast<uint16_t>(1))),
CHIP_NO_ERROR);
const uint8_t badSecondSegment[] = { 0x23 };
EXPECT_EQ(tracker.HandleResponse(ByteSpan(badSecondSegment), MakeOptional<uint16_t>(static_cast<uint16_t>(6)), NullOptional),
CHIP_ERROR_INVALID_ARGUMENT);
}
TEST(CertificateChainRequestTracker, AcceptsMaximumSupportedCertificateSize)
{
CertificateChainRequestTracker tracker;
uint8_t certificate[Credentials::kMaxDERCertLengthMlDsa65] = {};
ASSERT_EQ(tracker.HandleResponse(ByteSpan(certificate), MakeOptional<uint16_t>(static_cast<uint16_t>(sizeof(certificate))),
NullOptional),
CHIP_NO_ERROR);
EXPECT_TRUE(tracker.IsComplete());
EXPECT_TRUE(tracker.GetCertificate().data_equal(ByteSpan(certificate)));
}
TEST(CertificateChainRequestTracker, EnforcesSubjectAndIssuerBoundsBeforeAcceptingResponse)
{
using Profile = CertificateChainRequestTracker::CryptoProfile;
struct TestCase
{
Profile subject;
Profile issuer;
uint16_t limit;
};
const TestCase cases[] = {
{ Profile::kEcdsaMatterLegacy, Profile::kEcdsaMatterLegacy, 600 },
{ Profile::kEcdsaMatterLegacy, Profile::kMlDsa44, 4732 },
{ Profile::kMlDsa44, Profile::kEcdsaMatterLegacy, 4732 },
{ Profile::kMlDsa44, Profile::kMlDsa44, 4732 },
{ Profile::kEcdsaMatterLegacy, Profile::kMlDsa65, 6261 },
{ Profile::kMlDsa44, Profile::kMlDsa65, 6261 },
{ Profile::kMlDsa65, Profile::kMlDsa44, 6261 },
{ Profile::kMlDsa65, Profile::kMlDsa65, 6261 },
{ Profile::kUnknownEnumValue, Profile::kMlDsa44, 6261 },
{ Profile::kMlDsa44, Profile::kUnknownEnumValue, 6261 },
};
uint8_t certificate[Credentials::kMaxDERCertLengthMlDsa65 + 1] = {};
for (const auto & test : cases)
{
for (const bool segmented : { false, true })
{
CertificateChainRequestTracker tracker;
tracker.Reset(test.subject, test.issuer);
const auto total = segmented ? MakeOptional<uint16_t>(static_cast<uint16_t>(test.limit + 1)) : NullOptional;
// Reject the advertised total on the first byte, before accepting any state.
EXPECT_EQ(tracker.HandleResponse(ByteSpan(certificate, segmented ? 1 : test.limit + 1), total,
segmented ? MakeOptional<uint16_t>(static_cast<uint16_t>(1)) : NullOptional),
CHIP_ERROR_MESSAGE_TOO_LONG);
EXPECT_FALSE(tracker.IsComplete());
EXPECT_FALSE(tracker.IsSegmentedTransfer());
EXPECT_FALSE(tracker.HasPendingSegment());
EXPECT_TRUE(tracker.GetCertificate().empty());
ASSERT_EQ(tracker.HandleResponse(ByteSpan(certificate, test.limit), segmented ? MakeOptional(test.limit) : NullOptional,
NullOptional),
CHIP_NO_ERROR);
EXPECT_EQ(tracker.GetCertificate().size(), test.limit);
}
}
}
TEST(CertificateChainRequestTracker, ResetRestoresDefaultBound)
{
using Profile = CertificateChainRequestTracker::CryptoProfile;
CertificateChainRequestTracker tracker;
tracker.Reset(Profile::kEcdsaMatterLegacy, Profile::kEcdsaMatterLegacy);
tracker.Reset();
const uint8_t firstByte[] = { 1 };
EXPECT_EQ(tracker.HandleResponse(ByteSpan(firstByte), MakeOptional<uint16_t>(static_cast<uint16_t>(6261)),
MakeOptional<uint16_t>(static_cast<uint16_t>(1))),
CHIP_NO_ERROR);
}
TEST(CertificateChainRequestTracker, RejectsOversizedDocument)
{
CertificateChainRequestTracker tracker;
const uint8_t firstSegment[] = { 0x42 };
EXPECT_EQ(tracker.HandleResponse(
ByteSpan(firstSegment),
MakeOptional<uint16_t>(static_cast<uint16_t>(CertificateChainRequestTracker::kMaxCertificateDocumentSize + 1)),
MakeOptional<uint16_t>(static_cast<uint16_t>(1))),
CHIP_ERROR_MESSAGE_TOO_LONG);
}
TEST(CertificateChainRequestTracker, RejectsOversizedSingleResponse)
{
CertificateChainRequestTracker tracker;
uint8_t oversizedCertificate[CertificateChainRequestTracker::kMaxCertificateDocumentSize + 1] = {};
EXPECT_EQ(tracker.HandleResponse(ByteSpan(oversizedCertificate), NullOptional, NullOptional), CHIP_ERROR_MESSAGE_TOO_LONG);
}
} // namespace