diff --git a/.gitignore b/.gitignore index 6f9b6a5..ef9101a 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,7 @@ settings.local.json # Local agent/tooling metadata .agents/ .codex/ + +# Fuzzing artifacts (tests/PalladiumWallet.Fuzz) +findings/ +crash-*.bin diff --git a/PalladiumWallet.sln b/PalladiumWallet.sln index 700f567..542ccaa 100644 --- a/PalladiumWallet.sln +++ b/PalladiumWallet.sln @@ -1,64 +1,134 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{84E60614-5042-48EC-B349-290FB0CA7BA8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.Core", "src\Core\PalladiumWallet.Core.csproj", "{A7D0EF95-B206-4646-99DD-1D2BBB7AF978}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.App", "src\App\PalladiumWallet.App.csproj", "{13EE9780-5810-4229-BFCF-6003172534DD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.Cli", "src\Cli\PalladiumWallet.Cli.csproj", "{D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FDF1822C-58D6-4B35-93EA-6A85E1292933}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.Tests", "tests\PalladiumWallet.Tests\PalladiumWallet.Tests.csproj", "{C7E79E8E-B1DE-4053-9FB4-853814766CE0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.App.Desktop", "src\App.Desktop\PalladiumWallet.App.Desktop.csproj", "{A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.App.Android", "src\App.Android\PalladiumWallet.App.Android.csproj", "{BCC5BE4A-B909-4043-B0FB-B5A839349578}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Release|Any CPU.Build.0 = Release|Any CPU - {13EE9780-5810-4229-BFCF-6003172534DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13EE9780-5810-4229-BFCF-6003172534DD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13EE9780-5810-4229-BFCF-6003172534DD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13EE9780-5810-4229-BFCF-6003172534DD}.Release|Any CPU.Build.0 = Release|Any CPU - {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Release|Any CPU.Build.0 = Release|Any CPU - {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Release|Any CPU.Build.0 = Release|Any CPU - {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Release|Any CPU.Build.0 = Release|Any CPU - {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {A7D0EF95-B206-4646-99DD-1D2BBB7AF978} = {84E60614-5042-48EC-B349-290FB0CA7BA8} - {13EE9780-5810-4229-BFCF-6003172534DD} = {84E60614-5042-48EC-B349-290FB0CA7BA8} - {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416} = {84E60614-5042-48EC-B349-290FB0CA7BA8} - {C7E79E8E-B1DE-4053-9FB4-853814766CE0} = {FDF1822C-58D6-4B35-93EA-6A85E1292933} - {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7} = {84E60614-5042-48EC-B349-290FB0CA7BA8} - {BCC5BE4A-B909-4043-B0FB-B5A839349578} = {84E60614-5042-48EC-B349-290FB0CA7BA8} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{84E60614-5042-48EC-B349-290FB0CA7BA8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.Core", "src\Core\PalladiumWallet.Core.csproj", "{A7D0EF95-B206-4646-99DD-1D2BBB7AF978}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.App", "src\App\PalladiumWallet.App.csproj", "{13EE9780-5810-4229-BFCF-6003172534DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.Cli", "src\Cli\PalladiumWallet.Cli.csproj", "{D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FDF1822C-58D6-4B35-93EA-6A85E1292933}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.Tests", "tests\PalladiumWallet.Tests\PalladiumWallet.Tests.csproj", "{C7E79E8E-B1DE-4053-9FB4-853814766CE0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.App.Desktop", "src\App.Desktop\PalladiumWallet.App.Desktop.csproj", "{A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.App.Android", "src\App.Android\PalladiumWallet.App.Android.csproj", "{BCC5BE4A-B909-4043-B0FB-B5A839349578}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PalladiumWallet.Fuzz", "tests\PalladiumWallet.Fuzz\PalladiumWallet.Fuzz.csproj", "{BC5932E5-E6F7-42A2-AC39-697C9A737A3C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{BF9B33E3-314A-3621-C1F0-AFD074692421}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Debug|x64.Build.0 = Debug|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Debug|x86.ActiveCfg = Debug|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Debug|x86.Build.0 = Debug|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Release|Any CPU.Build.0 = Release|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Release|x64.ActiveCfg = Release|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Release|x64.Build.0 = Release|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Release|x86.ActiveCfg = Release|Any CPU + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978}.Release|x86.Build.0 = Release|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Debug|x64.Build.0 = Debug|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Debug|x86.Build.0 = Debug|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Release|Any CPU.Build.0 = Release|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Release|x64.ActiveCfg = Release|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Release|x64.Build.0 = Release|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Release|x86.ActiveCfg = Release|Any CPU + {13EE9780-5810-4229-BFCF-6003172534DD}.Release|x86.Build.0 = Release|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Debug|x64.Build.0 = Debug|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Debug|x86.Build.0 = Debug|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Release|Any CPU.Build.0 = Release|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Release|x64.ActiveCfg = Release|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Release|x64.Build.0 = Release|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Release|x86.ActiveCfg = Release|Any CPU + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416}.Release|x86.Build.0 = Release|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Debug|x64.ActiveCfg = Debug|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Debug|x64.Build.0 = Debug|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Debug|x86.ActiveCfg = Debug|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Debug|x86.Build.0 = Debug|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Release|Any CPU.Build.0 = Release|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Release|x64.ActiveCfg = Release|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Release|x64.Build.0 = Release|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Release|x86.ActiveCfg = Release|Any CPU + {C7E79E8E-B1DE-4053-9FB4-853814766CE0}.Release|x86.Build.0 = Release|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Debug|x64.Build.0 = Debug|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Debug|x86.ActiveCfg = Debug|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Debug|x86.Build.0 = Debug|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Release|Any CPU.Build.0 = Release|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Release|x64.ActiveCfg = Release|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Release|x64.Build.0 = Release|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Release|x86.ActiveCfg = Release|Any CPU + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7}.Release|x86.Build.0 = Release|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Debug|x64.ActiveCfg = Debug|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Debug|x64.Build.0 = Debug|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Debug|x86.ActiveCfg = Debug|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Debug|x86.Build.0 = Debug|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Release|Any CPU.Build.0 = Release|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Release|x64.ActiveCfg = Release|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Release|x64.Build.0 = Release|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Release|x86.ActiveCfg = Release|Any CPU + {BCC5BE4A-B909-4043-B0FB-B5A839349578}.Release|x86.Build.0 = Release|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Debug|x64.Build.0 = Debug|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Debug|x86.Build.0 = Debug|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Release|Any CPU.Build.0 = Release|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Release|x64.ActiveCfg = Release|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Release|x64.Build.0 = Release|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Release|x86.ActiveCfg = Release|Any CPU + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A7D0EF95-B206-4646-99DD-1D2BBB7AF978} = {84E60614-5042-48EC-B349-290FB0CA7BA8} + {13EE9780-5810-4229-BFCF-6003172534DD} = {84E60614-5042-48EC-B349-290FB0CA7BA8} + {D1AE035A-6DAC-46F4-90FB-F1AE2A79D416} = {84E60614-5042-48EC-B349-290FB0CA7BA8} + {C7E79E8E-B1DE-4053-9FB4-853814766CE0} = {FDF1822C-58D6-4B35-93EA-6A85E1292933} + {A5D1DD48-7485-43F0-BFE3-2F645EC4D1E7} = {84E60614-5042-48EC-B349-290FB0CA7BA8} + {BCC5BE4A-B909-4043-B0FB-B5A839349578} = {84E60614-5042-48EC-B349-290FB0CA7BA8} + {BC5932E5-E6F7-42A2-AC39-697C9A737A3C} = {FDF1822C-58D6-4B35-93EA-6A85E1292933} + {BF9B33E3-314A-3621-C1F0-AFD074692421} = {84E60614-5042-48EC-B349-290FB0CA7BA8} + EndGlobalSection +EndGlobal diff --git a/tests/PalladiumWallet.Fuzz/Corpus/address/bech32.bin b/tests/PalladiumWallet.Fuzz/Corpus/address/bech32.bin new file mode 100644 index 0000000..fa4de45 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/address/bech32.bin @@ -0,0 +1 @@ +plm1qdq3gu2zvg9lyr8gxd6yln4wavc5tlp8prmvfay \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/address/garbage.bin b/tests/PalladiumWallet.Fuzz/Corpus/address/garbage.bin new file mode 100644 index 0000000..95d09f2 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/address/garbage.bin @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/address/legacy-ish.bin b/tests/PalladiumWallet.Fuzz/Corpus/address/legacy-ish.bin new file mode 100644 index 0000000..efaad60 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/address/legacy-ish.bin @@ -0,0 +1 @@ +PB6q3PB6q3PB6q3PB6q3PB6q3PB6q3PB6q \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/bip39/empty.bin b/tests/PalladiumWallet.Fuzz/Corpus/bip39/empty.bin new file mode 100644 index 0000000..e69de29 diff --git a/tests/PalladiumWallet.Fuzz/Corpus/bip39/english-12.bin b/tests/PalladiumWallet.Fuzz/Corpus/bip39/english-12.bin new file mode 100644 index 0000000..1cba8e0 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/bip39/english-12.bin @@ -0,0 +1 @@ +abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/bip39/regression-notsupported.bin b/tests/PalladiumWallet.Fuzz/Corpus/bip39/regression-notsupported.bin new file mode 100644 index 0000000..5b36e35 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/bip39/regression-notsupported.bin @@ -0,0 +1 @@ +ábÊco ábaco0ábacoábÊ \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/bip39/spanish-word.bin b/tests/PalladiumWallet.Fuzz/Corpus/bip39/spanish-word.bin new file mode 100644 index 0000000..4f18b88 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/bip39/spanish-word.bin @@ -0,0 +1 @@ +ábaco ábaco ábaco \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/coinamount/comma.bin b/tests/PalladiumWallet.Fuzz/Corpus/coinamount/comma.bin new file mode 100644 index 0000000..273bcd8 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/coinamount/comma.bin @@ -0,0 +1 @@ +0,001 \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/coinamount/huge.bin b/tests/PalladiumWallet.Fuzz/Corpus/coinamount/huge.bin new file mode 100644 index 0000000..4a013cd --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/coinamount/huge.bin @@ -0,0 +1 @@ +79228162514264337593543950335 \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/coinamount/plain.bin b/tests/PalladiumWallet.Fuzz/Corpus/coinamount/plain.bin new file mode 100644 index 0000000..e26cdce --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/coinamount/plain.bin @@ -0,0 +1 @@ +1.50000000 \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/encfile/container.bin b/tests/PalladiumWallet.Fuzz/Corpus/encfile/container.bin new file mode 100644 index 0000000..767fcd0 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/encfile/container.bin @@ -0,0 +1 @@ +{"Format":"plm-wallet-aesgcm-v1","Iterations":2,"Salt":"AAAAAAAAAAAAAAAAAAAAAA==","Nonce":"AAAAAAAAAAAAAAAA","Tag":"AAAAAAAAAAAAAAAAAAAAAA==","Data":"AAAA"} \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/encfile/not-json.bin b/tests/PalladiumWallet.Fuzz/Corpus/encfile/not-json.bin new file mode 100644 index 0000000..b6fc4c6 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/encfile/not-json.bin @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/encfile/wrong-format.bin b/tests/PalladiumWallet.Fuzz/Corpus/encfile/wrong-format.bin new file mode 100644 index 0000000..3b5f8f2 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/encfile/wrong-format.bin @@ -0,0 +1 @@ +{"Format":"other","Iterations":2,"Salt":"","Nonce":"","Tag":"","Data":""} \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/header/genesis-hex.bin b/tests/PalladiumWallet.Fuzz/Corpus/header/genesis-hex.bin new file mode 100644 index 0000000..15816b9 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/header/genesis-hex.bin @@ -0,0 +1 @@ +0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/header/genesis.bin b/tests/PalladiumWallet.Fuzz/Corpus/header/genesis.bin new file mode 100644 index 0000000..df52c61 Binary files /dev/null and b/tests/PalladiumWallet.Fuzz/Corpus/header/genesis.bin differ diff --git a/tests/PalladiumWallet.Fuzz/Corpus/header/short.bin b/tests/PalladiumWallet.Fuzz/Corpus/header/short.bin new file mode 100644 index 0000000..f66c9cf Binary files /dev/null and b/tests/PalladiumWallet.Fuzz/Corpus/header/short.bin differ diff --git a/tests/PalladiumWallet.Fuzz/Corpus/merkle/no-branch.bin b/tests/PalladiumWallet.Fuzz/Corpus/merkle/no-branch.bin new file mode 100644 index 0000000..b2506c7 Binary files /dev/null and b/tests/PalladiumWallet.Fuzz/Corpus/merkle/no-branch.bin differ diff --git a/tests/PalladiumWallet.Fuzz/Corpus/merkle/one-level.bin b/tests/PalladiumWallet.Fuzz/Corpus/merkle/one-level.bin new file mode 100644 index 0000000..d18d8a9 Binary files /dev/null and b/tests/PalladiumWallet.Fuzz/Corpus/merkle/one-level.bin differ diff --git a/tests/PalladiumWallet.Fuzz/Corpus/peers/odd-shapes.bin b/tests/PalladiumWallet.Fuzz/Corpus/peers/odd-shapes.bin new file mode 100644 index 0000000..13b52d1 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/peers/odd-shapes.bin @@ -0,0 +1 @@ +[[1,2,3],{"a":1},"x",[["h","n",[42,"t"]]]] \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/peers/regression-bad-utf8.bin b/tests/PalladiumWallet.Fuzz/Corpus/peers/regression-bad-utf8.bin new file mode 100644 index 0000000..4bd5bd9 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/peers/regression-bad-utf8.bin @@ -0,0 +1 @@ +[["1.2.3.4","peer.example",["v1.4.2","p10","t50001","‡50002"]]] \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/peers/typical.bin b/tests/PalladiumWallet.Fuzz/Corpus/peers/typical.bin new file mode 100644 index 0000000..8851650 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/peers/typical.bin @@ -0,0 +1 @@ +[["1.2.3.4","peer.example",["v1.4.2","p10","t50001","s50002"]]] \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/slip132/garbage.bin b/tests/PalladiumWallet.Fuzz/Corpus/slip132/garbage.bin new file mode 100644 index 0000000..119b20b --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/slip132/garbage.bin @@ -0,0 +1 @@ +not-a-key \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/slip132/xpub-prefix.bin b/tests/PalladiumWallet.Fuzz/Corpus/slip132/xpub-prefix.bin new file mode 100644 index 0000000..52fe509 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/slip132/xpub-prefix.bin @@ -0,0 +1 @@ +xpub661MyMwAqRbcF \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/slip132/zpub.bin b/tests/PalladiumWallet.Fuzz/Corpus/slip132/zpub.bin new file mode 100644 index 0000000..801c2dc --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/slip132/zpub.bin @@ -0,0 +1 @@ +zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/walletdoc/bad-version.bin b/tests/PalladiumWallet.Fuzz/Corpus/walletdoc/bad-version.bin new file mode 100644 index 0000000..1d79824 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/walletdoc/bad-version.bin @@ -0,0 +1 @@ +{"Version":99} \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/walletdoc/minimal.bin b/tests/PalladiumWallet.Fuzz/Corpus/walletdoc/minimal.bin new file mode 100644 index 0000000..98eb66f --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/walletdoc/minimal.bin @@ -0,0 +1 @@ +{"Version":1,"Network":"mainnet","ScriptKind":"NativeSegwit"} \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/Corpus/walletdoc/not-json.bin b/tests/PalladiumWallet.Fuzz/Corpus/walletdoc/not-json.bin new file mode 100644 index 0000000..b6fc4c6 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Corpus/walletdoc/not-json.bin @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/tests/PalladiumWallet.Fuzz/FuzzTargets.cs b/tests/PalladiumWallet.Fuzz/FuzzTargets.cs new file mode 100644 index 0000000..2aad4c6 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/FuzzTargets.cs @@ -0,0 +1,141 @@ +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; + +/// +/// 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. +/// +public static class FuzzTargets +{ + public static readonly IReadOnlyDictionary> All = + new Dictionary> + { + ["header"] = Header, + ["merkle"] = Merkle, + ["slip132"] = Slip132Keys, + ["bip39"] = Bip39Mnemonic, + ["address"] = Address, + ["coinamount"] = CoinAmountParse, + ["walletdoc"] = WalletDoc, + ["encfile"] = EncFile, + ["peers"] = Peers, + }; + + /// Runs one input through a named target (used by replay and the regression test). + 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(); + 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); + } +} diff --git a/tests/PalladiumWallet.Fuzz/PalladiumWallet.Fuzz.csproj b/tests/PalladiumWallet.Fuzz/PalladiumWallet.Fuzz.csproj new file mode 100644 index 0000000..e169bbb --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/PalladiumWallet.Fuzz.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + false + + false + + + + + + + + + + + + + diff --git a/tests/PalladiumWallet.Fuzz/Program.cs b/tests/PalladiumWallet.Fuzz/Program.cs new file mode 100644 index 0000000..6d0bbb2 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/Program.cs @@ -0,0 +1,215 @@ +using System.Text; +using PalladiumWallet.Fuzz; + +// Fuzzing entry point. One binary, one target per process (fuzzers want a +// single deterministic execution path). Modes: +// +// --afl run under afl-fuzz (instrumented Core, see fuzz.sh) +// --libfuzzer run under libfuzzer-dotnet +// --random N [seed] built-in dumb fuzzer: N corpus mutations (no tooling needed) +// ... replay saved inputs (used by the regression test / triage) +// --make-seeds regenerate the seed corpus +// +// A "crash" is any exception escaping the target: each target already swallows +// the exception types that are part of the parser's documented contract. + +if (args.Length >= 2 && args[0] == "--make-seeds") +{ + SeedCorpus.WriteAll(args[1]); + Console.WriteLine($"Seed corpus written to {args[1]}"); + return 0; +} + +if (args.Length < 2 || !FuzzTargets.All.TryGetValue(args[0], out var target)) +{ + Console.Error.WriteLine("Usage: PalladiumWallet.Fuzz --afl | --libfuzzer | --random N [seed] | ..."); + Console.Error.WriteLine(" PalladiumWallet.Fuzz --make-seeds "); + Console.Error.WriteLine($"Targets: {string.Join(' ', FuzzTargets.All.Keys)}"); + return 2; +} + +switch (args[1]) +{ + case "--afl": + SharpFuzz.Fuzzer.Run(stream => + { + using var ms = new MemoryStream(); + stream.CopyTo(ms); + target(ms.ToArray()); + }); + return 0; + + case "--libfuzzer": + SharpFuzz.Fuzzer.LibFuzzer.Run(span => target(span.ToArray())); + return 0; + + case "--random": + { + var iterations = args.Length > 2 ? int.Parse(args[2]) : 100_000; + var seed = args.Length > 3 ? ulong.Parse(args[3]) : 0xF022_5EEDUL; + return RandomFuzz.Run(args[0], target, iterations, seed); + } + + default: + { + var failures = 0; + foreach (var path in args[1..]) + foreach (var file in Directory.Exists(path) + ? Directory.EnumerateFiles(path).OrderBy(f => f, StringComparer.Ordinal) + : (IEnumerable)[path]) + { + var data = File.ReadAllBytes(file); + try + { + target(data); + } + catch (Exception ex) + { + failures++; + Console.Error.WriteLine($"CRASH {args[0]} on {file}:"); + Console.Error.WriteLine(ex); + } + } + return failures == 0 ? 0 : 1; + } +} + +/// +/// Minimal corpus-mutation fuzzer for smoke runs and CI: not coverage-guided, +/// but catches shallow contract violations without afl++/libFuzzer installed. +/// Deterministic for a given seed; on crash it saves the input for replay. +/// +internal static class RandomFuzz +{ + public static int Run(string name, Action target, int iterations, ulong seed) + { + var corpus = SeedCorpus.For(name); + var rng = seed; + ulong Next() { rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17; return rng; } + + for (var i = 0; i < iterations; i++) + { + var data = (byte[])corpus[(int)(Next() % (ulong)corpus.Count)].Clone(); + var mutations = 1 + (int)(Next() % 8); + for (var m = 0; m < mutations && data.Length > 0; m++) + switch (Next() % 4) + { + case 0: data[Next() % (ulong)data.Length] = (byte)Next(); break; + case 1: data[Next() % (ulong)data.Length] ^= (byte)(1 << (int)(Next() % 8)); break; + case 2: Array.Resize(ref data, (int)(Next() % 512) + 1); break; + case 3: data = [.. data, .. data[..(int)(Next() % (ulong)data.Length)]]; break; + } + try + { + target(data); + } + catch (Exception ex) + { + var crashFile = $"crash-{name}-{i}.bin"; + File.WriteAllBytes(crashFile, data); + Console.Error.WriteLine( + $"CRASH {name} at iteration {i}: {ex.GetType().Name}: {ex.Message} (input saved to {crashFile})"); + return 1; + } + } + Console.WriteLine($"{name}: {iterations} random mutations, no contract violations"); + return 0; + } +} + +/// +/// Seed inputs per target: small valid (or near-valid) examples that give the +/// mutation engines a meaningful starting shape. Regenerate with --make-seeds. +/// +internal static class SeedCorpus +{ + private static readonly Dictionary Seeds = new() + { + ["header"] = + [ + ("genesis", Convert.FromHexString( + "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c")), + ("genesis-hex", Encoding.UTF8.GetBytes( + "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c")), + ("short", [0x01, 0x00, 0x00, 0x00]), + ], + ["merkle"] = + [ + ("one-level", [.. new byte[32], .. BitConverter.GetBytes(1), .. new byte[32], .. Enumerable.Repeat((byte)0xAA, 32)]), + ("no-branch", [.. Enumerable.Repeat((byte)0x11, 32), .. BitConverter.GetBytes(0), .. Enumerable.Repeat((byte)0x11, 32)]), + ], + ["slip132"] = + [ + ("zpub", Encoding.UTF8.GetBytes( + "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs")), + ("xpub-prefix", Encoding.UTF8.GetBytes("xpub661MyMwAqRbcF")), + ("garbage", Encoding.UTF8.GetBytes("not-a-key")), + ], + ["bip39"] = + [ + ("english-12", Encoding.UTF8.GetBytes( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")), + ("spanish-word", Encoding.UTF8.GetBytes("ábaco ábaco ábaco")), + ("empty", []), + // Regression: NBitcoin AutoDetect → "Unknown" language → NotSupportedException + // escaped TryParse (fixed by catching it). + ("regression-notsupported", Convert.FromHexString( + "c3a162ca636f20c3a16261636f30c3a16261636fc3a162ca")), + ], + ["address"] = + [ + ("bech32", Encoding.UTF8.GetBytes("plm1qdq3gu2zvg9lyr8gxd6yln4wavc5tlp8prmvfay")), + ("legacy-ish", Encoding.UTF8.GetBytes("PB6q3PB6q3PB6q3PB6q3PB6q3PB6q3PB6q")), + ("garbage", Encoding.UTF8.GetBytes("hello world")), + ], + ["coinamount"] = + [ + ("plain", Encoding.UTF8.GetBytes("1.50000000")), + ("comma", Encoding.UTF8.GetBytes("0,001")), + ("huge", Encoding.UTF8.GetBytes("79228162514264337593543950335")), + ], + ["walletdoc"] = + [ + ("minimal", Encoding.UTF8.GetBytes( + """{"Version":1,"Network":"mainnet","ScriptKind":"NativeSegwit"}""")), + ("bad-version", Encoding.UTF8.GetBytes("""{"Version":99}""")), + ("not-json", Encoding.UTF8.GetBytes("hello")), + ], + ["encfile"] = + [ + // Well-formed container with a tiny iteration count so every replay and + // mutated descendant skips the full PBKDF2 cost (decryption then fails + // with the typed WrongPasswordException — exactly the contract under test). + ("container", Encoding.UTF8.GetBytes( + """{"Format":"plm-wallet-aesgcm-v1","Iterations":2,"Salt":"AAAAAAAAAAAAAAAAAAAAAA==","Nonce":"AAAAAAAAAAAAAAAA","Tag":"AAAAAAAAAAAAAAAAAAAAAA==","Data":"AAAA"}""")), + ("wrong-format", Encoding.UTF8.GetBytes( + """{"Format":"other","Iterations":2,"Salt":"","Nonce":"","Tag":"","Data":""}""")), + ("not-json", Encoding.UTF8.GetBytes("hello")), + ], + ["peers"] = + [ + ("typical", Encoding.UTF8.GetBytes( + """[["1.2.3.4","peer.example",["v1.4.2","p10","t50001","s50002"]]]""")), + ("odd-shapes", Encoding.UTF8.GetBytes("""[[1,2,3],{"a":1},"x",[["h","n",[42,"t"]]]]""")), + // Regression: raw invalid-UTF-8 byte inside a JSON string parses but cannot + // be transcoded by GetString() → InvalidOperationException (fixed in AsString). + ("regression-bad-utf8", Convert.FromHexString( + "5b5b22312e322e332e34222c22706565722e6578616d706c65222c5b2276312e342e32222c22703130222c227435303030" + + "31222c22873530303032225d5d5d")), + ], + }; + + public static IReadOnlyList For(string target) => + Seeds[target].Select(s => s.Data).ToList(); + + public static void WriteAll(string root) + { + foreach (var (target, seeds) in Seeds) + { + var dir = Path.Combine(root, target); + Directory.CreateDirectory(dir); + foreach (var (name, data) in seeds) + File.WriteAllBytes(Path.Combine(dir, name + ".bin"), data); + } + } +} diff --git a/tests/PalladiumWallet.Fuzz/README.md b/tests/PalladiumWallet.Fuzz/README.md new file mode 100644 index 0000000..7fc2c17 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/README.md @@ -0,0 +1,66 @@ +# Fuzzing + +Fuzz targets over every parser that consumes **untrusted input**: server +responses (block headers, Merkle proofs, peer lists), wallet files (plaintext +document and encrypted container), and user-pasted text (mnemonics, SLIP-132 +keys, addresses, amounts). + +Each target encodes the parser's **error contract**: the exception types +documented as its failure mode are swallowed, anything else escaping is a +finding. Targets: `header` `merkle` `slip132` `bip39` `address` `coinamount` +`walletdoc` `encfile` `peers` (see `FuzzTargets.cs` for each contract). + +## Three ways to run + +**1. Corpus replay — automatic.** The seed corpus (including regression inputs +for every crash found so far) replays through all targets on every +`dotnet test` run via `FuzzCorpusTests`, so fixed findings cannot come back. + +**2. Built-in random smoke — no tooling.** Not coverage-guided, but catches +shallow contract violations in seconds: + +```bash +dotnet run -- bip39 --random 50000 # target, iterations [, seed] +``` + +**3. Coverage-guided campaign — afl++.** The real thing; run it before a +release or after touching any parser: + +```bash +apt install afl++ +dotnet tool install --global SharpFuzz.CommandLine +./fuzz.sh header # one target per campaign +./fuzz.sh peers -V 3600 # extra args go to afl-fuzz +``` + +`fuzz.sh` builds Release, instruments `PalladiumWallet.Core.dll` (and NBitcoin) +with SharpFuzz, and launches afl-fuzz with the target's seed corpus. Findings +land in `findings//crashes/`. + +## Triage workflow + +```bash +dotnet run -- findings//crashes/ # replay: full stack trace +``` + +Fix the parser (typed exception or graceful rejection — see the hardening +commits for `Bip39.TryParse`, `ElectrumApi.ParsePeers`, `EncryptedFile.Decrypt` +as examples), then add the input to `SeedCorpus` in `Program.cs` as a +`regression-*` seed and regenerate with `dotnet run -- --make-seeds Corpus`. + +## Findings so far + +The first smoke run found two real bugs, both reachable from untrusted input: + +- `Bip39.TryParse` crashed with `NotSupportedException` on restore text + resembling no wordlist (NBitcoin's `Wordlist.AutoDetect` throws for its + internal "Unknown" language). +- `ElectrumApi.ParsePeers` crashed with `InvalidOperationException` on a peer + list containing a JSON string with invalid UTF-8 bytes (parses fine, + fails at `GetString()` transcoding) — attacker-controlled server data. + +Plus two hardening changes made so the `encfile`/`peers` contracts could be +strict at all: `EncryptedFile.Decrypt` maps every malformed-container failure +to `InvalidDataException` and bounds the PBKDF2 iteration count (a tampered +file could previously demand 2^31 iterations, hanging the wallet at open), and +`ParsePeers` tolerates any JSON shape. diff --git a/tests/PalladiumWallet.Fuzz/fuzz.sh b/tests/PalladiumWallet.Fuzz/fuzz.sh new file mode 100755 index 0000000..ee4ff85 --- /dev/null +++ b/tests/PalladiumWallet.Fuzz/fuzz.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Coverage-guided fuzzing with afl++ + SharpFuzz. +# +# One-time setup: +# apt install afl++ (or build from source) +# dotnet tool install --global SharpFuzz.CommandLine +# +# Usage: +# ./fuzz.sh [afl-fuzz args...] +# ./fuzz.sh header +# ./fuzz.sh peers -V 3600 # time-limited campaign (seconds) +# +# Targets: header merkle slip132 bip39 address coinamount walletdoc encfile peers +# +# Findings land in findings//crashes/: replay one with +# dotnet run -- findings//crashes/ +# then add it to the seed corpus in Program.cs as a regression input. +set -euo pipefail +cd "$(dirname "$0")" + +TARGET="${1:?usage: ./fuzz.sh [afl-fuzz args...]}" +shift || true + +command -v afl-fuzz >/dev/null || { echo "afl-fuzz not found: apt install afl++"; exit 1; } +command -v sharpfuzz >/dev/null || { echo "sharpfuzz not found: dotnet tool install --global SharpFuzz.CommandLine"; exit 1; } + +dotnet build -c Release +BIN="bin/Release/net10.0" + +# Instrument the assemblies under test (idempotent: SharpFuzz skips already- +# instrumented DLLs). Core is the real target; NBitcoin gives the fuzzer +# visibility into the library our parsers delegate to. +sharpfuzz "$BIN/PalladiumWallet.Core.dll" +sharpfuzz "$BIN/NBitcoin.dll" || true + +mkdir -p "findings/$TARGET" +afl-fuzz -i "Corpus/$TARGET" -o "findings/$TARGET" -t 5000 "$@" \ + -- dotnet "$BIN/PalladiumWallet.Fuzz.dll" "$TARGET" --afl diff --git a/tests/PalladiumWallet.Tests/FuzzCorpusTests.cs b/tests/PalladiumWallet.Tests/FuzzCorpusTests.cs new file mode 100644 index 0000000..8ce3cbb --- /dev/null +++ b/tests/PalladiumWallet.Tests/FuzzCorpusTests.cs @@ -0,0 +1,36 @@ +using PalladiumWallet.Fuzz; + +namespace PalladiumWallet.Tests; + +/// +/// Replays the fuzzing seed corpus (tests/PalladiumWallet.Fuzz/Corpus, copied +/// next to the test binary) through every fuzz target on each test run: the +/// corpus includes the regression inputs for crashes found by past campaigns, +/// so a fixed contract violation can never silently come back. The real +/// coverage-guided campaigns run separately via tests/PalladiumWallet.Fuzz/fuzz.sh. +/// +public class FuzzCorpusTests +{ + public static TheoryData Targets() + { + var data = new TheoryData(); + foreach (var name in FuzzTargets.All.Keys) + data.Add(name); + return data; + } + + [Theory] + [MemberData(nameof(Targets))] + public void Il_corpus_del_target_rispetta_il_contratto_del_parser(string target) + { + var dir = Path.Combine(AppContext.BaseDirectory, "Corpus", target); + Assert.True(Directory.Exists(dir), $"seed corpus missing for '{target}' at {dir}"); + + foreach (var file in Directory.EnumerateFiles(dir)) + { + // Any escaping exception is a contract violation: the target itself + // swallows the exception types documented as the parser's failure mode. + FuzzTargets.Run(target, File.ReadAllBytes(file)); + } + } +} diff --git a/tests/PalladiumWallet.Tests/PalladiumWallet.Tests.csproj b/tests/PalladiumWallet.Tests/PalladiumWallet.Tests.csproj index d2a304c..a58e472 100644 --- a/tests/PalladiumWallet.Tests/PalladiumWallet.Tests.csproj +++ b/tests/PalladiumWallet.Tests/PalladiumWallet.Tests.csproj @@ -21,8 +21,9 @@ - - + + +