142 lines
5.3 KiB
C#
142 lines
5.3 KiB
C#
|
|
using System.Text;
|
||
|
|
using System.Text.Json;
|
||
|
|
using NBitcoin;
|
||
|
|
using PalladiumWallet.Core.Chain;
|
||
|
|
using PalladiumWallet.Core.Crypto;
|
||
|
|
using PalladiumWallet.Core.Net;
|
||
|
|
using PalladiumWallet.Core.Spv;
|
||
|
|
using PalladiumWallet.Core.Storage;
|
||
|
|
using PalladiumWallet.Core.Wallet;
|
||
|
|
|
||
|
|
namespace PalladiumWallet.Fuzz;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Fuzz targets over the parsers that consume untrusted input (server responses,
|
||
|
|
/// wallet files, user-pasted keys/addresses/amounts). Each target enforces the
|
||
|
|
/// parser's error contract: the exception types documented as its failure mode
|
||
|
|
/// are swallowed, anything else escapes and is reported as a crash by the fuzzer.
|
||
|
|
/// </summary>
|
||
|
|
public static class FuzzTargets
|
||
|
|
{
|
||
|
|
public static readonly IReadOnlyDictionary<string, Action<byte[]>> All =
|
||
|
|
new Dictionary<string, Action<byte[]>>
|
||
|
|
{
|
||
|
|
["header"] = Header,
|
||
|
|
["merkle"] = Merkle,
|
||
|
|
["slip132"] = Slip132Keys,
|
||
|
|
["bip39"] = Bip39Mnemonic,
|
||
|
|
["address"] = Address,
|
||
|
|
["coinamount"] = CoinAmountParse,
|
||
|
|
["walletdoc"] = WalletDoc,
|
||
|
|
["encfile"] = EncFile,
|
||
|
|
["peers"] = Peers,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// <summary>Runs one input through a named target (used by replay and the regression test).</summary>
|
||
|
|
public static void Run(string target, byte[] data) => All[target](data);
|
||
|
|
|
||
|
|
private static string Utf8(byte[] data) => Encoding.UTF8.GetString(data);
|
||
|
|
|
||
|
|
// 80-byte block headers arrive from the server both as raw bytes and as hex.
|
||
|
|
// Contract: exactly 80 bytes never throws; any other length is ArgumentException;
|
||
|
|
// the hex overload may also reject non-hex text with FormatException.
|
||
|
|
private static void Header(byte[] data)
|
||
|
|
{
|
||
|
|
try { BlockHeaderInfo.Parse(data); }
|
||
|
|
catch (ArgumentException) when (data.Length != BlockHeaderInfo.Size) { }
|
||
|
|
|
||
|
|
try { BlockHeaderInfo.Parse(Utf8(data)); }
|
||
|
|
catch (FormatException) { }
|
||
|
|
catch (ArgumentException) { }
|
||
|
|
}
|
||
|
|
|
||
|
|
// Merkle proof verification runs on server-supplied txid/pos/branch.
|
||
|
|
// Contract: never throws, for any position (including negative) and branch.
|
||
|
|
private static void Merkle(byte[] data)
|
||
|
|
{
|
||
|
|
if (data.Length < 68)
|
||
|
|
return;
|
||
|
|
var txid = new uint256(data.AsSpan(0, 32));
|
||
|
|
var position = BitConverter.ToInt32(data, 32);
|
||
|
|
var root = new uint256(data.AsSpan(36, 32));
|
||
|
|
var branch = new List<uint256>();
|
||
|
|
for (var offset = 68; offset + 32 <= data.Length; offset += 32)
|
||
|
|
branch.Add(new uint256(data.AsSpan(offset, 32)));
|
||
|
|
|
||
|
|
MerkleProof.Verify(txid, position, branch, root);
|
||
|
|
}
|
||
|
|
|
||
|
|
// SLIP-132 extended keys are pasted by the user on import.
|
||
|
|
// Contract: the Try* pair never throws, on any profile.
|
||
|
|
private static void Slip132Keys(byte[] data)
|
||
|
|
{
|
||
|
|
var text = Utf8(data);
|
||
|
|
foreach (var profile in new[] { ChainProfiles.Mainnet, ChainProfiles.Testnet, ChainProfiles.Regtest })
|
||
|
|
{
|
||
|
|
Slip132.TryDecodePublic(text, profile, out _, out _);
|
||
|
|
Slip132.TryDecodePrivate(text, profile, out _, out _);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mnemonics are pasted by the user on restore. Contract: TryParse never throws,
|
||
|
|
// with auto-detected and explicit wordlist language alike.
|
||
|
|
private static void Bip39Mnemonic(byte[] data)
|
||
|
|
{
|
||
|
|
var text = Utf8(data);
|
||
|
|
Bip39.TryParse(text, out _);
|
||
|
|
if (data.Length > 0)
|
||
|
|
Bip39.TryParse(text, out _, (MnemonicLanguage)(data[0] % 8));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Recipient addresses are pasted by the user (or scanned from a QR).
|
||
|
|
// Contract: NBitcoin rejects anything invalid with FormatException.
|
||
|
|
private static void Address(byte[] data)
|
||
|
|
{
|
||
|
|
try { BitcoinAddress.Create(Utf8(data), PalladiumNetworks.Mainnet); }
|
||
|
|
catch (FormatException) { }
|
||
|
|
}
|
||
|
|
|
||
|
|
// Amounts are typed by the user in the currently selected unit.
|
||
|
|
// Contract: TryParse* never throws for any of the official units.
|
||
|
|
private static void CoinAmountParse(byte[] data)
|
||
|
|
{
|
||
|
|
var text = Utf8(data);
|
||
|
|
foreach (var unit in CoinAmount.Units)
|
||
|
|
CoinAmount.TryParseIn(text, unit, out _);
|
||
|
|
CoinAmount.TryParseCoins(text, out _);
|
||
|
|
}
|
||
|
|
|
||
|
|
// The plaintext wallet document is read from disk at every open.
|
||
|
|
// Contract: malformed JSON or schema is JsonException/InvalidDataException.
|
||
|
|
private static void WalletDoc(byte[] data)
|
||
|
|
{
|
||
|
|
try { WalletDocument.FromJson(Utf8(data)); }
|
||
|
|
catch (JsonException) { }
|
||
|
|
catch (InvalidDataException) { }
|
||
|
|
}
|
||
|
|
|
||
|
|
// The encrypted container wraps the document on disk; a tampered file must
|
||
|
|
// fail with the two typed exceptions, never a raw parser error, and
|
||
|
|
// IsEncrypted (the format sniffer) must never throw at all.
|
||
|
|
private static void EncFile(byte[] data)
|
||
|
|
{
|
||
|
|
var text = Utf8(data);
|
||
|
|
EncryptedFile.IsEncrypted(text);
|
||
|
|
try { EncryptedFile.Decrypt(text, "fuzz-password"); }
|
||
|
|
catch (WrongPasswordException) { }
|
||
|
|
catch (InvalidDataException) { }
|
||
|
|
}
|
||
|
|
|
||
|
|
// server.peers.subscribe responses are attacker-controlled JSON.
|
||
|
|
// Contract: any well-formed JSON of any shape parses to a (possibly empty)
|
||
|
|
// peer list without throwing.
|
||
|
|
private static void Peers(byte[] data)
|
||
|
|
{
|
||
|
|
JsonDocument doc;
|
||
|
|
try { doc = JsonDocument.Parse(data); }
|
||
|
|
catch (JsonException) { return; }
|
||
|
|
using (doc)
|
||
|
|
ElectrumApi.ParsePeers(doc.RootElement);
|
||
|
|
}
|
||
|
|
}
|