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