blob: e943ae0e1972bf74f7a756b05710104c7041b8a2 [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
* Seeded FuzzTest harness for ASN1Reader.
*
* Coverage-targeting changes vs the prior version:
*
* * Massively wider seed pool — beyond the 5 Root/ICA cert DERs we now
* include Node certs, FWSign certs, ICA01_1, error-case certs (which
* deliberately violate well-formedness in different parts of the DER
* tree), public keys (SubjectPublicKeyInfo SEQUENCEs), and SKID/AKID
* DER blobs. Each seed exercises a different ASN.1 constructed-type
* shape.
*
* * Walk now also exercises EnterEncapsulatedType / ExitEncapsulatedType
* and GetConstructedType — paths the previous walker never touched.
*
* * Three FUZZ_TESTs split the input domain so per-test coverage stays
* focused: full-cert walk, key-only walk, KeyId-only walk.
*
* * Idempotency property: re-Init + re-walk must produce the same
* observable Class/Tag/ValueLen sequence.
*/
#include <cstdint>
#include <fcntl.h>
#include <string>
#include <unistd.h>
#include <vector>
#include <pw_fuzzer/fuzztest.h>
#include <pw_unit_test/framework.h>
#include <credentials/tests/CHIPCert_test_vectors.h>
#include <lib/asn1/ASN1.h>
#include <lib/support/CHIPMem.h>
#include <lib/support/Span.h>
namespace {
using namespace chip;
using namespace chip::ASN1;
using namespace chip::TestCerts;
using namespace fuzztest;
void EnsureInitialized()
{
static const bool sInitialized = [] {
VerifyOrDie(Platform::MemoryInit() == CHIP_NO_ERROR);
return true;
}();
(void) sInitialized;
}
inline std::string SpanToString(ByteSpan s)
{
return std::string(reinterpret_cast<const char *>(s.data()), s.size());
}
// ===== Seed pools =====
// Full-cert DER (TBSCertificate inside SEQUENCE OF SEQUENCE etc.). These
// exercise the deep-nesting, OID, BIT STRING, and INTEGER paths.
std::vector<std::string> CertDerSeeds()
{
return {
SpanToString(sTestCert_Root01_DER), SpanToString(sTestCert_Root02_DER), SpanToString(sTestCert_Root03_DER),
SpanToString(sTestCert_ICA01_DER), SpanToString(sTestCert_ICA02_DER), SpanToString(sTestCert_ICA01_1_DER),
SpanToString(sTestCert_FWSign01_DER), SpanToString(sTestCert_Node01_01_DER), SpanToString(sTestCert_Node01_02_DER),
};
}
// SubjectPublicKeyInfo blobs (SEQUENCE { algorithm, BIT STRING }) — exercise
// BIT STRING + AlgorithmIdentifier OID handling.
std::vector<std::string> KeySeeds()
{
return {
SpanToString(sTestCert_Root01_PublicKey), SpanToString(sTestCert_Root02_PublicKey),
SpanToString(sTestCert_Root03_PublicKey), SpanToString(sTestCert_ICA01_PublicKey),
SpanToString(sTestCert_ICA02_PublicKey), SpanToString(sTestCert_Node01_01_PublicKey),
SpanToString(sTestCert_Node01_02_PublicKey), SpanToString(sTestCert_FWSign01_PublicKey),
};
}
// Subject/Authority Key Identifier raw OCTET STRINGs (often 20 bytes — SHA-1).
std::vector<std::string> KeyIdSeeds()
{
return {
SpanToString(sTestCert_Root01_SubjectKeyId), SpanToString(sTestCert_Root01_AuthorityKeyId),
SpanToString(sTestCert_ICA01_SubjectKeyId), SpanToString(sTestCert_ICA01_AuthorityKeyId),
SpanToString(sTestCert_Node01_01_SubjectKeyId), SpanToString(sTestCert_Node01_01_AuthorityKeyId),
SpanToString(sTestCert_FWSign01_SubjectKeyId), SpanToString(sTestCert_FWSign01_AuthorityKeyId),
};
}
// ===== Walker =====
// Deep walk that exercises every read API the public ASN1Reader exposes.
// Bounded by depth and total-elements to prevent runaway iteration on
// deeply-nested or oversized inputs while still going deeper than a single Next loop.
void Walk(ASN1Reader & reader, int depth, int & remaining)
{
if (depth > 12)
return;
while (reader.Next() == CHIP_NO_ERROR)
{
if (--remaining <= 0)
return;
// Read every metadata accessor.
(void) reader.GetClass();
(void) reader.GetTag();
(void) reader.GetValue();
(void) reader.GetValueLen();
(void) reader.IsConstructed();
(void) reader.IsIndefiniteLen();
(void) reader.IsEndOfContents();
(void) reader.IsContained();
// Try every Get* — most will fail with WRONG_TLV_TYPE / ENCODING but
// hitting their entry path is what we want for coverage.
int64_t intVal = 0;
(void) reader.GetInteger(intVal);
bool boolVal = false;
(void) reader.GetBoolean(boolVal);
OID oid = kOID_NotSpecified;
(void) reader.GetObjectId(oid);
uint32_t bitStr = 0;
(void) reader.GetBitString(bitStr);
ASN1UniversalTime t;
(void) reader.GetUTCTime(t);
(void) reader.GetGeneralizedTime(t);
if (reader.IsConstructed())
{
// Touch the raw constructed access path too.
const uint8_t * cVal = nullptr;
uint32_t cLen = 0;
(void) reader.GetConstructedType(cVal, cLen);
if (reader.EnterConstructedType() == CHIP_NO_ERROR)
{
Walk(reader, depth + 1, remaining);
(void) reader.ExitConstructedType();
}
}
else if (reader.GetClass() == kASN1TagClass_Universal && reader.GetTag() == kASN1UniversalTag_BitString)
{
// BIT STRING bodies in X.509 frequently encapsulate a SEQUENCE
// (e.g. SubjectPublicKeyInfo's bit string wraps an EC point or a
// SEQUENCE of integers). Exercise the encapsulated-type path.
if (reader.EnterEncapsulatedType() == CHIP_NO_ERROR)
{
Walk(reader, depth + 1, remaining);
(void) reader.ExitEncapsulatedType();
}
}
else if (reader.GetClass() == kASN1TagClass_Universal && reader.GetTag() == kASN1UniversalTag_OctetString)
{
// SKID/AKID OCTET STRINGs sometimes wrap a SEQUENCE in extension
// values; try the encapsulated path here too.
if (reader.EnterEncapsulatedType() == CHIP_NO_ERROR)
{
Walk(reader, depth + 1, remaining);
(void) reader.ExitEncapsulatedType();
}
}
}
}
void DriveOnce(const std::string & der)
{
EnsureInitialized();
ASN1Reader reader;
reader.Init(reinterpret_cast<const uint8_t *>(der.data()), der.size());
int budget = 8192;
Walk(reader, 0, budget);
}
// ===== FUZZ_TESTs =====
void ASN1ReaderCertWalk(const std::string & der)
{
DriveOnce(der);
}
FUZZ_TEST(ASN1ReaderPW, ASN1ReaderCertWalk).WithDomains(Arbitrary<std::string>().WithSeeds(CertDerSeeds()).WithMaxSize(8192));
void ASN1ReaderKeyWalk(const std::string & der)
{
DriveOnce(der);
}
FUZZ_TEST(ASN1ReaderPW, ASN1ReaderKeyWalk).WithDomains(Arbitrary<std::string>().WithSeeds(KeySeeds()).WithMaxSize(2048));
void ASN1ReaderKeyIdWalk(const std::string & der)
{
DriveOnce(der);
}
FUZZ_TEST(ASN1ReaderPW, ASN1ReaderKeyIdWalk).WithDomains(Arbitrary<std::string>().WithSeeds(KeyIdSeeds()).WithMaxSize(256));
// Idempotency property — Init + Walk twice must yield the same observable
// (Class, Tag, ValueLen, IsConstructed) sequence. Internal state corruption
// (e.g. saved-context array overwrite) shows up as divergent walks.
struct ElemSnap
{
uint8_t cls;
uint8_t tag;
uint32_t valueLen;
bool constructed;
bool operator==(const ElemSnap & o) const
{
return cls == o.cls && tag == o.tag && valueLen == o.valueLen && constructed == o.constructed;
}
};
void CollectFlat(ASN1Reader & reader, std::vector<ElemSnap> & out, int & remaining)
{
while (reader.Next() == CHIP_NO_ERROR)
{
if (--remaining <= 0)
return;
out.push_back({ reader.GetClass(), reader.GetTag(), reader.GetValueLen(), reader.IsConstructed() });
if (reader.IsConstructed())
{
if (reader.EnterConstructedType() == CHIP_NO_ERROR)
{
CollectFlat(reader, out, remaining);
(void) reader.ExitConstructedType();
}
}
}
}
void ASN1ReaderIdempotency(const std::string & der)
{
EnsureInitialized();
std::vector<ElemSnap> first;
{
ASN1Reader reader;
reader.Init(reinterpret_cast<const uint8_t *>(der.data()), der.size());
int budget = 4096;
CollectFlat(reader, first, budget);
}
std::vector<ElemSnap> second;
{
ASN1Reader reader;
reader.Init(reinterpret_cast<const uint8_t *>(der.data()), der.size());
int budget = 4096;
CollectFlat(reader, second, budget);
}
ASSERT_EQ(first.size(), second.size());
for (size_t i = 0; i < first.size(); ++i)
{
ASSERT_TRUE(first[i] == second[i]);
}
}
FUZZ_TEST(ASN1ReaderPW, ASN1ReaderIdempotency).WithDomains(Arbitrary<std::string>().WithSeeds(CertDerSeeds()).WithMaxSize(8192));
// Drive DumpASN1 — the debug pretty-printer. Cheap coverage win: it
// re-exercises the entire ASN1Reader::Next/EnterConstructedType/Get* surface
// from a separate caller, plus prints to a /dev/null FILE* so the printf
// paths get covered without polluting stdout.
void ASN1DumpFuzz(const std::string & der)
{
EnsureInitialized();
ASN1Reader reader;
reader.Init(reinterpret_cast<const uint8_t *>(der.data()), der.size());
// Redirect stdout for the duration so the debug prints don't spam fuzz
// output. fmemopen would be cleaner but DumpASN1 uses raw printf().
fflush(stdout);
int saved = dup(STDOUT_FILENO);
int devnull = open("/dev/null", O_WRONLY);
if (devnull >= 0)
{
dup2(devnull, STDOUT_FILENO);
close(devnull);
}
(void) DumpASN1(reader, "fuzz: ", " ");
fflush(stdout);
if (saved >= 0)
{
dup2(saved, STDOUT_FILENO);
close(saved);
}
}
FUZZ_TEST(ASN1ReaderPW, ASN1DumpFuzz).WithDomains(Arbitrary<std::string>().WithSeeds(CertDerSeeds()).WithMaxSize(8192));
// =====================================================================
// ASN1Writer coverage — encode primitives, then read back via ASN1Reader,
// asserting structural equality. The writer was at 0% line coverage before
// this; here every Put*/Start*/End* path gets exercised.
// =====================================================================
void ASN1WriterRoundtripInteger(int64_t value)
{
EnsureInitialized();
uint8_t buf[64];
ASN1Writer writer;
writer.Init(buf, sizeof(buf));
if (writer.PutInteger(value) != CHIP_NO_ERROR)
return;
ASN1Reader reader;
reader.Init(buf, writer.GetLengthWritten());
ASSERT_EQ(reader.Next(), CHIP_NO_ERROR);
int64_t decoded = 0;
ASSERT_EQ(reader.GetInteger(decoded), CHIP_NO_ERROR);
ASSERT_EQ(decoded, value);
}
FUZZ_TEST(ASN1ReaderPW, ASN1WriterRoundtripInteger).WithDomains(Arbitrary<int64_t>());
void ASN1WriterRoundtripBoolean(bool value)
{
EnsureInitialized();
uint8_t buf[16];
ASN1Writer writer;
writer.Init(buf, sizeof(buf));
if (writer.PutBoolean(value) != CHIP_NO_ERROR)
return;
ASN1Reader reader;
reader.Init(buf, writer.GetLengthWritten());
ASSERT_EQ(reader.Next(), CHIP_NO_ERROR);
bool decoded = !value;
ASSERT_EQ(reader.GetBoolean(decoded), CHIP_NO_ERROR);
ASSERT_EQ(decoded, value);
}
FUZZ_TEST(ASN1ReaderPW, ASN1WriterRoundtripBoolean).WithDomains(Arbitrary<bool>());
void ASN1WriterRoundtripBitString(uint32_t value)
{
EnsureInitialized();
uint8_t buf[64];
ASN1Writer writer;
writer.Init(buf, sizeof(buf));
if (writer.PutBitString(value) != CHIP_NO_ERROR)
return;
ASN1Reader reader;
reader.Init(buf, writer.GetLengthWritten());
ASSERT_EQ(reader.Next(), CHIP_NO_ERROR);
uint32_t decoded = 0;
ASSERT_EQ(reader.GetBitString(decoded), CHIP_NO_ERROR);
ASSERT_EQ(decoded, value);
}
FUZZ_TEST(ASN1ReaderPW, ASN1WriterRoundtripBitString).WithDomains(Arbitrary<uint32_t>());
void ASN1WriterConstructedNesting(const std::vector<uint8_t> & blob, bool encapsulate)
{
EnsureInitialized();
if (blob.size() > 1024)
return;
uint8_t buf[2048];
ASN1Writer writer;
writer.Init(buf, sizeof(buf));
if (writer.StartConstructedType(kASN1TagClass_Universal, /*Sequence*/ 0x10) != CHIP_NO_ERROR)
return;
if (writer.PutOctetString(blob.data(), static_cast<uint16_t>(blob.size())) != CHIP_NO_ERROR)
return;
if (encapsulate)
{
if (writer.StartEncapsulatedType(kASN1TagClass_Universal, /*BitString*/ 0x03, /*bitStringEncoding=*/true) == CHIP_NO_ERROR)
{
(void) writer.PutInteger(0x42);
(void) writer.EndEncapsulatedType();
}
}
if (writer.EndConstructedType() != CHIP_NO_ERROR)
return;
// Read the result back to make sure the lengths are coherent.
const size_t written = static_cast<size_t>(writer.GetLengthWritten());
ASN1Reader reader;
reader.Init(buf, written);
int budget = 4096;
Walk(reader, 0, budget);
}
FUZZ_TEST(ASN1ReaderPW, ASN1WriterConstructedNesting)
.WithDomains(Arbitrary<std::vector<uint8_t>>().WithMaxSize(1024), Arbitrary<bool>());
// =============================================================================
// ASN1OID coverage — the OID lookup tables in ASN1OID.cpp are at ~28% line
// coverage because most enum values aren't exercised by cert-walk fuzz alone.
// This explicitly drives the table for every interesting OID.
// =============================================================================
auto AnyKnownOID()
{
return ElementOf<OID>({
// PubKey / Sig
kOID_PubKeyAlgo_ECPublicKey,
kOID_SigAlgo_ECDSAWithSHA256,
// Attribute Types (X.500 names + Matter-specific)
kOID_AttributeType_CommonName,
kOID_AttributeType_Surname,
kOID_AttributeType_SerialNumber,
kOID_AttributeType_CountryName,
kOID_AttributeType_LocalityName,
kOID_AttributeType_StateOrProvinceName,
kOID_AttributeType_OrganizationName,
kOID_AttributeType_OrganizationalUnitName,
kOID_AttributeType_Title,
kOID_AttributeType_GivenName,
kOID_AttributeType_DomainComponent,
kOID_AttributeType_MatterNodeId,
kOID_AttributeType_MatterFirmwareSigningId,
kOID_AttributeType_MatterICACId,
kOID_AttributeType_MatterRCACId,
kOID_AttributeType_MatterFabricId,
kOID_AttributeType_MatterCASEAuthTag,
kOID_AttributeType_MatterVidVerificationSignerId,
// Curves and extensions
kOID_EllipticCurve_prime256v1,
kOID_Extension_BasicConstraints,
kOID_Extension_KeyUsage,
kOID_Extension_ExtendedKeyUsage,
kOID_Extension_SubjectKeyIdentifier,
// Sentinels
kOID_NotSpecified,
kOID_Unknown,
});
}
void OIDLookupRoundtrip(OID oid)
{
EnsureInitialized();
const uint8_t * encoded = nullptr;
uint16_t encodedLen = 0;
if (!GetEncodedObjectID(oid, encoded, encodedLen))
{
// Sentinels (NotSpecified / Unknown) won't have an encoding — that's
// legitimate; touch the secondary path and return.
(void) GetOIDCategory(oid);
(void) GetOIDName(oid);
return;
}
// Encode-side helpers covered: feed back through ParseObjectID and check
// we get the same enum back out.
const OID parsed = ParseObjectID(encoded, encodedLen);
ASSERT_EQ(parsed, oid);
// Touch both observation accessors on the resulting OID.
(void) GetOIDCategory(parsed);
const char * name = GetOIDName(parsed);
(void) name;
}
FUZZ_TEST(ASN1ReaderPW, OIDLookupRoundtrip).WithDomains(AnyKnownOID());
// Direct ParseObjectID with arbitrary bytes — covers the linear-search miss
// path and the empty-input early-return.
void ParseObjectIDFuzz(const std::vector<uint8_t> & encoded)
{
EnsureInitialized();
if (encoded.size() > UINT16_MAX)
return;
OID oid = ParseObjectID(encoded.data(), static_cast<uint16_t>(encoded.size()));
(void) GetOIDCategory(oid);
(void) GetOIDName(oid);
}
FUZZ_TEST(ASN1ReaderPW, ParseObjectIDFuzz).WithDomains(Arbitrary<std::vector<uint8_t>>().WithMaxSize(64));
} // namespace