| /* | |
| * | |
| * 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 the X.509 helper surface of the crypto PAL | |
| * (`CHIPCryptoPAL.h`): key-identifier, distinguished-name, VID/PID and | |
| * CRL-distribution-point extraction, issuance-time comparison, resigned | |
| * certificate matching, and CSR verification. | |
| * | |
| * These helpers each parse a DER certificate independently of the chain | |
| * validators, so they are covered here rather than in | |
| * `FuzzChipCertPW.cpp`, which already drives | |
| * `Crypto::ValidateCertificateChain` and | |
| * `Crypto::VerifyAttestationCertificateFormat`. | |
| * | |
| * Seeds come from the compiled attestation-certificate test vectors, not | |
| * from a directory read, so they travel with the binary. They are attached | |
| * to the individual domains, because only domain-level seeds are injected | |
| * when the suite runs under the libFuzzer compatibility engine. Each pool | |
| * leads with the coherent FFF1 chain (PAA_FFF1 -> PAI_FFF1_8000 -> | |
| * DAC_FFF1_8000_0000): FuzzTest builds its pristine seed tuple from the | |
| * first seed of each domain, so a mismatched lead would fail the parse and | |
| * leave the helpers unreached. | |
| */ | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <string> | |
| #include <vector> | |
| #include <fuzztest/fuzztest_core.h> | |
| #include <gtest/gtest.h> | |
| #include <credentials/tests/CHIPAttCert_test_vectors.h> | |
| #include <crypto/CHIPCryptoPAL.h> | |
| #include <lib/support/CHIPMem.h> | |
| #include <lib/support/CodeUtils.h> | |
| #include <lib/support/Span.h> | |
| #if CHIP_CRYPTO_PSA | |
| #include <psa/crypto.h> | |
| #endif | |
| namespace { | |
| using namespace chip; | |
| using namespace chip::Crypto; | |
| using namespace chip::TestCerts; | |
| using namespace fuzztest; | |
| using Bytes = std::vector<uint8_t>; | |
| // The mbedTLS and PSA certificate backends import keys through PSA, which | |
| // rejects every operation until the driver is initialized -- without this the | |
| // CSR case would pass without verifying anything. | |
| void EnsureInitialized() | |
| { | |
| static const bool sInitialized = [] { | |
| VerifyOrDie(Platform::MemoryInit() == CHIP_NO_ERROR); | |
| #if CHIP_CRYPTO_PSA | |
| VerifyOrDie(psa_crypto_init() == PSA_SUCCESS); | |
| #endif | |
| return true; | |
| }(); | |
| (void) sInitialized; | |
| } | |
| Bytes ToBytes(const ByteSpan & span) | |
| { | |
| return Bytes(span.begin(), span.end()); | |
| } | |
| // Matches the DER cap used by the other certificate harnesses. | |
| constexpr size_t kMaxDerCertLen = 8192; | |
| // Distinguished-name attributes are bounded by kMaxCertificateDistinguishedNameLength; | |
| // allow twice that so over-long values are still exercised. | |
| constexpr size_t kMaxAttributeStringLen = 2 * kMaxCertificateDistinguishedNameLength; | |
| std::vector<Bytes> ToSeeds(const std::vector<ByteSpan> & spans) | |
| { | |
| std::vector<Bytes> seeds; | |
| seeds.reserve(spans.size()); | |
| for (const ByteSpan & span : spans) | |
| { | |
| seeds.push_back(ToBytes(span)); | |
| } | |
| return seeds; | |
| } | |
| // Device attestation certificates, leading with the FFF1 chain leaf and | |
| // followed by variants covering other vendor/product ids and every validity | |
| // edge case the test corpus provides. | |
| auto AnyDacCert() | |
| { | |
| return Arbitrary<Bytes>() | |
| .WithMaxSize(kMaxDerCertLen) | |
| .WithSeeds(ToSeeds({ | |
| sTestCert_DAC_FFF1_8000_0000_Cert, | |
| sTestCert_DAC_FFF1_8000_0004_Cert, | |
| sTestCert_DAC_FFF2_8001_0008_Cert, | |
| sTestCert_DAC_FFF2_8002_0017_Cert, | |
| sTestCert_DAC_FFF2_8003_0018_FB_Cert, | |
| sTestCert_DAC_FFF2_8004_001C_FB_Cert, | |
| sTestCert_DAC_FFF2_8004_0020_ValInPast_Cert, | |
| sTestCert_DAC_FFF2_8004_0021_ValInFuture_Cert, | |
| sTestCert_DAC_FFF2_8004_0030_Val1SecBefore_Cert, | |
| sTestCert_DAC_FFF2_8006_0034_ValInFuture_Cert, | |
| })); | |
| } | |
| // Product attestation intermediates, leading with the FFF1 chain issuer. | |
| auto AnyPaiCert() | |
| { | |
| return Arbitrary<Bytes>() | |
| .WithMaxSize(kMaxDerCertLen) | |
| .WithSeeds(ToSeeds({ | |
| sTestCert_PAI_FFF1_8000_Cert, | |
| sTestCert_PAI_FFF2_8001_Cert, | |
| sTestCert_PAI_FFF2_8001_Resigned_Cert, | |
| sTestCert_PAI_FFF2_8001_ResignedSKIDDiff_Cert, | |
| sTestCert_PAI_FFF2_8001_ResignedSubjectDiff_Cert, | |
| sTestCert_PAI_FFF2_8004_FB_Cert, | |
| sTestCert_PAI_FFF2_8005_ValInPast_Cert, | |
| sTestCert_PAI_FFF2_8006_ValInFuture_Cert, | |
| sTestCert_PAI_FFF2_NoPID_Cert, | |
| })); | |
| } | |
| // Certificates that actually carry a CRL distribution point, in every shape the | |
| // corpus provides: single/multiple DPs, multiple URIs, an HTTPS scheme, an | |
| // over-long URI, a wrong URI prefix, and one/two CRL issuers. | |
| auto AnyCdpCert() | |
| { | |
| return Arbitrary<Bytes>() | |
| .WithMaxSize(kMaxDerCertLen) | |
| .WithSeeds(ToSeeds({ | |
| sTestCert_DAC_FFF1_8000_0000_CDP_Issuer_PAA_FFF1_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_2DPs_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_2URIs_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_HTTPS_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_Long_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_Wrong_Prefix_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_2CRLIssuers_PAA_FFF1_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_CRL_Issuer_PAA_FFF1_2DPs_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_Issuer_PAA_NoVID_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_Issuer_PAI_FFF2_8004_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_CDP_Issuer_PAI_FFF2_8004_Long_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_2CDPs_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_2CDPs_Issuer_PAA_FFF1_Cert, | |
| sTestCert_DAC_FFF1_8000_0000_2CDPs_Issuer_PAI_FFF2_8004_Cert, | |
| })); | |
| } | |
| // IsCertificateValidAtCurrentTime reads the wall clock, so its result is not | |
| // reproducible across runs; only the absence of a crash is checked here. | |
| void IssuanceTimeComparison(const Bytes & candidate, const Bytes & issuer) | |
| { | |
| EnsureInitialized(); | |
| const ByteSpan candidateSpan(candidate.data(), candidate.size()); | |
| RETURN_SAFELY_IGNORED IsCertificateValidAtIssuance(candidateSpan, ByteSpan(issuer.data(), issuer.size())); | |
| RETURN_SAFELY_IGNORED IsCertificateValidAtCurrentTime(candidateSpan); | |
| } | |
| FUZZ_TEST(ChipCryptoPal, IssuanceTimeComparison).WithDomains(AnyDacCert(), AnyPaiCert()); | |
| void SerialNumberExtraction(const Bytes & cert) | |
| { | |
| EnsureInitialized(); | |
| uint8_t serialNumberBuf[kMaxCertificateSerialNumberLength] = { 0 }; | |
| MutableByteSpan serialNumber(serialNumberBuf); | |
| RETURN_SAFELY_IGNORED ExtractSerialNumberFromX509Cert(ByteSpan(cert.data(), cert.size()), serialNumber); | |
| } | |
| FUZZ_TEST(ChipCryptoPal, SerialNumberExtraction).WithDomains(AnyDacCert()); | |
| void KeyIdentifierExtraction(const Bytes & cert) | |
| { | |
| EnsureInitialized(); | |
| const ByteSpan certSpan(cert.data(), cert.size()); | |
| uint8_t skidBuf[kSubjectKeyIdentifierLength]; | |
| MutableByteSpan skid(skidBuf); | |
| RETURN_SAFELY_IGNORED ExtractSKIDFromX509Cert(certSpan, skid); | |
| uint8_t akidBuf[kAuthorityKeyIdentifierLength]; | |
| MutableByteSpan akid(akidBuf); | |
| RETURN_SAFELY_IGNORED ExtractAKIDFromX509Cert(certSpan, akid); | |
| } | |
| FUZZ_TEST(ChipCryptoPal, KeyIdentifierExtraction).WithDomains(AnyDacCert()); | |
| void CrlDistributionPointUriExtraction(const Bytes & cert) | |
| { | |
| EnsureInitialized(); | |
| char cdpBuf[kMaxCRLDistributionPointURLLength] = { '\0' }; | |
| MutableCharSpan cdpUrl(cdpBuf); | |
| RETURN_SAFELY_IGNORED ExtractCRLDistributionPointURIFromX509Cert(ByteSpan(cert.data(), cert.size()), cdpUrl); | |
| } | |
| FUZZ_TEST(ChipCryptoPal, CrlDistributionPointUriExtraction).WithDomains(AnyCdpCert()); | |
| void CrlIssuerExtraction(const Bytes & cert) | |
| { | |
| EnsureInitialized(); | |
| uint8_t crlIssuerBuf[kMaxCertificateDistinguishedNameLength] = { 0 }; | |
| MutableByteSpan crlIssuer(crlIssuerBuf); | |
| RETURN_SAFELY_IGNORED ExtractCDPExtensionCRLIssuerFromX509Cert(ByteSpan(cert.data(), cert.size()), crlIssuer); | |
| } | |
| FUZZ_TEST(ChipCryptoPal, CrlIssuerExtraction).WithDomains(AnyCdpCert()); | |
| void VidPidExtractionFromCert(const Bytes & cert) | |
| { | |
| EnsureInitialized(); | |
| AttestationCertVidPid vidPid; | |
| RETURN_SAFELY_IGNORED ExtractVIDPIDFromX509Cert(ByteSpan(cert.data(), cert.size()), vidPid); | |
| } | |
| FUZZ_TEST(ChipCryptoPal, VidPidExtractionFromCert).WithDomains(AnyDacCert()); | |
| auto AnyDNAttrType() | |
| { | |
| return ElementOf({ DNAttrType::kUnspecified, DNAttrType::kCommonName, DNAttrType::kMatterVID, DNAttrType::kMatterPID }); | |
| } | |
| // The attribute string is parsed as text (including the "Mvid:"/"Mpid:" prefixes | |
| // embedded in a common name), so it is fuzzed as a free-form string rather than | |
| // seeded from certificates. | |
| void VidPidExtractionFromAttributeString(DNAttrType attrType, const std::string & attrString) | |
| { | |
| EnsureInitialized(); | |
| AttestationCertVidPid vidPid; | |
| AttestationCertVidPid vidPidFromCN; | |
| const ByteSpan attrSpan(reinterpret_cast<const uint8_t *>(attrString.data()), attrString.size()); | |
| RETURN_SAFELY_IGNORED ExtractVIDPIDFromAttributeString(attrType, attrSpan, vidPid, vidPidFromCN); | |
| } | |
| FUZZ_TEST(ChipCryptoPal, VidPidExtractionFromAttributeString) | |
| .WithDomains(AnyDNAttrType(), | |
| Arbitrary<std::string>() | |
| .WithMaxSize(kMaxAttributeStringLen) | |
| .WithSeeds({ "Mvid:FFF1", "Mpid:8000", "Mvid:FFF1 Mpid:8000", "Matter Test DAC 0000", | |
| "Mvid:", "Mvid:FFFF Mpid:FFFF" })); | |
| void SubjectAndIssuerExtraction(const Bytes & cert) | |
| { | |
| EnsureInitialized(); | |
| const ByteSpan certSpan(cert.data(), cert.size()); | |
| uint8_t subjectBuf[kMaxCertificateDistinguishedNameLength] = { 0 }; | |
| MutableByteSpan subject(subjectBuf); | |
| RETURN_SAFELY_IGNORED ExtractSubjectFromX509Cert(certSpan, subject); | |
| uint8_t issuerBuf[kMaxCertificateDistinguishedNameLength] = { 0 }; | |
| MutableByteSpan issuer(issuerBuf); | |
| RETURN_SAFELY_IGNORED ExtractIssuerFromX509Cert(certSpan, issuer); | |
| } | |
| FUZZ_TEST(ChipCryptoPal, SubjectAndIssuerExtraction).WithDomains(AnyDacCert()); | |
| // The resigned-certificate lookup walks a caller-supplied candidate list, so the | |
| // list length is fuzzed alongside the reference certificate. The candidates are | |
| // the resigned PAI variants, which are what the helper exists to match. | |
| const ByteSpan kResignedCandidates[] = { | |
| sTestCert_PAI_FFF2_8001_Resigned_Cert, | |
| sTestCert_PAI_FFF2_8001_ResignedSKIDDiff_Cert, | |
| sTestCert_PAI_FFF2_8001_ResignedSubjectDiff_Cert, | |
| sTestCert_PAI_FFF2_NoPID_Resigned_Cert, | |
| }; | |
| void ResignedCertificateLookup(const Bytes & cert, size_t candidateCount) | |
| { | |
| EnsureInitialized(); | |
| ByteSpan replacement; | |
| RETURN_SAFELY_IGNORED ReplaceCertIfResignedCertFound(ByteSpan(cert.data(), cert.size()), kResignedCandidates, candidateCount, | |
| replacement); | |
| } | |
| FUZZ_TEST(ChipCryptoPal, ResignedCertificateLookup) | |
| .WithDomains(AnyPaiCert(), InRange<size_t>(0, MATTER_ARRAY_SIZE(kResignedCandidates))); | |
| // A well-formed CSR, used as the lead seed so that mutations start from a | |
| // structurally valid request. | |
| const uint8_t kGoodCsr[] = { | |
| 0x30, 0x81, 0xca, 0x30, 0x70, 0x02, 0x01, 0x00, 0x30, 0x0e, 0x31, 0x0c, 0x30, 0x0a, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x03, | |
| 0x43, 0x53, 0x52, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, | |
| 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xa3, 0xbe, 0xa1, 0xf5, 0x42, 0x01, 0x07, 0x3c, 0x4b, 0x75, 0x85, 0xd8, | |
| 0xe2, 0x98, 0xac, 0x2f, 0xf6, 0x98, 0xdb, 0xd9, 0x5b, 0xe0, 0x7e, 0xc1, 0x04, 0xd5, 0x73, 0xc5, 0xb0, 0x90, 0x77, 0x27, 0x00, | |
| 0x1e, 0x22, 0xc7, 0x89, 0x5e, 0x4d, 0x75, 0x07, 0x89, 0x82, 0x0f, 0x49, 0xb6, 0x59, 0xd5, 0xc5, 0x15, 0x7d, 0x93, 0xe6, 0x80, | |
| 0x5c, 0x70, 0x89, 0x0a, 0x43, 0x10, 0x3d, 0xeb, 0x3d, 0x4a, 0xa0, 0x00, 0x30, 0x0c, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, | |
| 0x04, 0x03, 0x02, 0x05, 0x00, 0x03, 0x48, 0x00, 0x30, 0x45, 0x02, 0x20, 0x1d, 0x86, 0x21, 0xb4, 0xc2, 0xe1, 0xa9, 0xf3, 0xbc, | |
| 0xc8, 0x7c, 0xda, 0xb4, 0xb9, 0xc6, 0x8c, 0xd0, 0xe4, 0x9a, 0x9c, 0xef, 0x02, 0x93, 0x98, 0x27, 0x7e, 0x81, 0x21, 0x5d, 0x20, | |
| 0x9d, 0x32, 0x02, 0x21, 0x00, 0x8b, 0x6b, 0x49, 0xb6, 0x7d, 0x3e, 0x67, 0x9e, 0xb1, 0x22, 0xd3, 0x63, 0x82, 0x40, 0x4f, 0x49, | |
| 0xa4, 0xdc, 0x17, 0x35, 0xac, 0x4b, 0x7a, 0xbf, 0x52, 0x05, 0x58, 0x68, 0xe0, 0xaa, 0xd2, 0x8e, | |
| }; | |
| void CertificateSigningRequestVerification(const Bytes & csr) | |
| { | |
| EnsureInitialized(); | |
| // The key is an output: on success it is filled in from the CSR. | |
| P256PublicKey csrPublicKey; | |
| RETURN_SAFELY_IGNORED VerifyCertificateSigningRequest(csr.data(), csr.size(), csrPublicKey); | |
| } | |
| FUZZ_TEST(ChipCryptoPal, CertificateSigningRequestVerification) | |
| .WithDomains(Arbitrary<Bytes>().WithMaxSize(kMaxDerCertLen).WithSeeds({ Bytes(kGoodCsr, kGoodCsr + sizeof(kGoodCsr)) })); | |
| } // namespace |