Skip to content

Commit

Permalink
fuzz: add a new, more efficient, descriptor parsing target
Browse files Browse the repository at this point in the history
This new target focuses on fuzzing the actual descriptor parsing logic
by not requiring the fuzzer to produce valid keys (nor a valid checksum
for that matter).
This should make it much more efficient to find bugs we could introduce
moving forward.

Using a character as a marker (here '%') to be able to search and
replace in the string without having to mock the actual descriptor
parsing logic was an insight from Pieter Wuille.
  • Loading branch information
darosior committed Jun 15, 2023
1 parent 026624f commit 845810d
Showing 1 changed file with 115 additions and 0 deletions.
115 changes: 115 additions & 0 deletions src/test/fuzz/descriptor_parse.cpp
Expand Up @@ -3,11 +3,109 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <chainparams.h>
#include <key_io.h>
#include <pubkey.h>
#include <script/descriptor.h>
#include <test/fuzz/fuzz.h>
#include <util/chaintype.h>

//! Types are raw compressed pubkeys, raw xonly pubkeys, raw privkeys (WIF), xpubs, xprvs.
static constexpr uint8_t KEY_TYPES_COUNT{5};
//! How many keys we'll generate in total.
static constexpr uint8_t TOTAL_KEYS_GENERATED{std::numeric_limits<uint8_t>::max()};

/**
* Converts a mocked descriptor string to a valid one. Every key in a mocked descriptor key is
* represented by 2 hex characters preceded by the '%' character. We parse the two hex characters
* as an index in a list of pre-generated keys. This list contains keys of the various types
* accepted in descriptor keys expressions.
*/
class MockedDescriptorConverter {
//! 256 keys of various types.
std::array<std::string, TOTAL_KEYS_GENERATED> keys_str;

public:
// We derive the type of key to generate from the 1-byte id parsed from hex.
bool IdIsCPubKey(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 0; }
bool IdIsXOnlyPubKey(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 1; }
bool IdIsConstPrivKey(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 2; }
bool IdIsXpub(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 3; }
bool IdIsXprv(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 4; }

//! When initializing the target, populate the list of keys.
void Init() {
// The data to use as a private key or a seed for an xprv.
uint8_t key_data[32] = {1};
// Generate keys of all kinds and store them in the keys array.
for (uint8_t i{0}; i < TOTAL_KEYS_GENERATED; i++) {
key_data[31] = i;

// If this is a "raw" key, generate a normal privkey. Otherwise generate
// an extended one.
if (IdIsCPubKey(i) || IdIsXOnlyPubKey(i) || IdIsConstPrivKey(i)) {
CKey privkey;
privkey.Set(key_data, key_data + 32, true);
if (IdIsCPubKey(i)) {
const CPubKey pubkey{privkey.GetPubKey()};
keys_str[i] = HexStr(pubkey);
} else if (IdIsXOnlyPubKey(i)) {
const XOnlyPubKey pubkey{privkey.GetPubKey()};
keys_str[i] = HexStr(pubkey);
} else {
keys_str[i] = EncodeSecret(privkey);
}
} else {
CExtKey ext_privkey;
ext_privkey.SetSeed({(std::byte *)key_data, 32});
if (IdIsXprv(i)) {
keys_str[i] = EncodeExtKey(ext_privkey);
} else {
const CExtPubKey ext_pubkey{ext_privkey.Neuter()};
keys_str[i] = EncodeExtPubKey(ext_pubkey);
}
}
}
}

//! Parse an id in the keys vectors from a 2-characters hex string.
std::optional<uint8_t> IdxFromHex(std::string_view hex_characters) const {
if (hex_characters.size() != 2) return {};
auto idx = ParseHex(hex_characters);
if (idx.size() != 1) return {};
return idx[0];
}

//! Get a valid descriptor string from a descriptor string whose keys were mocked.
std::optional<std::string> GetDescriptor(std::string_view mocked_desc) const {
// The smallest fragment would be "pk(%00)"
if (mocked_desc.size() < 7) return {};

// The actual, valid, descriptor string to be returned.
std::string desc;
desc.reserve(mocked_desc.size());

// Replace all occurences of '%' followed by two hex characters with the corresponding key.
for (size_t i = 0; i < mocked_desc.size();) {
if (mocked_desc[i] == '%') {
if (i + 1 >= mocked_desc.size()) return {};
if (const auto idx = IdxFromHex(mocked_desc.substr(i + 1, 2))) {
desc += keys_str[*idx];
i += 3;
} else {
return {};
}
} else {
desc += mocked_desc[i++];
}
}

return desc;
}
};

//! The converter of mocked descriptors, needs to be initialized when the target is.
MockedDescriptorConverter MOCKED_DESC_CONVERTER;

/** Test a successfully parsed descriptor. */
static void TestDescriptor(const Descriptor& desc)
{
Expand All @@ -22,6 +120,23 @@ void initialize_descriptor_parse()
SelectParams(ChainType::MAIN);
}

void initialize_mocked_descriptor_parse()
{
initialize_descriptor_parse();
MOCKED_DESC_CONVERTER.Init();
}

FUZZ_TARGET_INIT(mocked_descriptor_parse, initialize_mocked_descriptor_parse)
{
const std::string mocked_descriptor{buffer.begin(), buffer.end()};
if (const auto descriptor = MOCKED_DESC_CONVERTER.GetDescriptor(mocked_descriptor)) {
FlatSigningProvider signing_provider;
std::string error;
const auto desc = Parse(*descriptor, signing_provider, error);
if (desc) TestDescriptor(*desc);
}
}

FUZZ_TARGET_INIT(descriptor_parse, initialize_descriptor_parse)
{
const std::string descriptor(buffer.begin(), buffer.end());
Expand Down

0 comments on commit 845810d

Please sign in to comment.