From c925cc3b31c52e915a3d6582b8e619d2273ea88e Mon Sep 17 00:00:00 2001 From: Samy Date: Wed, 2 Sep 2026 16:25:50 +0200 Subject: [PATCH] ringct: fuzz and harden Bulletproof verification --- src/ringct/bulletproofs.cc | 1 + src/ringct/bulletproofs_plus.cc | 1 + tests/data/fuzz/bulletproof/BP0 | Bin 674 -> 707 bytes tests/data/fuzz/bulletproof/BP1 | Bin 0 -> 803 bytes tests/data/fuzz/bulletproof/BP2 | Bin 0 -> 931 bytes tests/data/fuzz/bulletproof/BP3 | Bin 0 -> 1123 bytes tests/data/fuzz/bulletproof/BP4 | Bin 0 -> 1443 bytes tests/fuzz/CMakeLists.txt | 7 ++--- tests/fuzz/bulletproof.cpp | 36 ++++++++++++++++++++----- tests/unit_tests/bulletproofs.cpp | 14 ++++++++++ tests/unit_tests/bulletproofs_plus.cpp | 14 ++++++++++ 11 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 tests/data/fuzz/bulletproof/BP1 create mode 100644 tests/data/fuzz/bulletproof/BP2 create mode 100644 tests/data/fuzz/bulletproof/BP3 create mode 100644 tests/data/fuzz/bulletproof/BP4 diff --git a/src/ringct/bulletproofs.cc b/src/ringct/bulletproofs.cc index cf1ddc879..24274022c 100644 --- a/src/ringct/bulletproofs.cc +++ b/src/ringct/bulletproofs.cc @@ -837,6 +837,7 @@ bool bulletproof_VERIFY(const std::vector &proofs) CHECK_AND_ASSERT_MES(is_reduced(proof.t), false, "Input scalar not in range"); CHECK_AND_ASSERT_MES(proof.V.size() >= 1, false, "V does not have at least one element"); + CHECK_AND_ASSERT_MES(proof.V.size() <= maxM, false, "V has too many elements"); CHECK_AND_ASSERT_MES(proof.L.size() == proof.R.size(), false, "Mismatched L and R sizes"); CHECK_AND_ASSERT_MES(proof.L.size() > 0, false, "Empty proof"); diff --git a/src/ringct/bulletproofs_plus.cc b/src/ringct/bulletproofs_plus.cc index ea2cd3459..cf9990b43 100644 --- a/src/ringct/bulletproofs_plus.cc +++ b/src/ringct/bulletproofs_plus.cc @@ -827,6 +827,7 @@ try_again: CHECK_AND_ASSERT_MES(is_reduced(proof.d1), false, "Input scalar not in range"); CHECK_AND_ASSERT_MES(proof.V.size() >= 1, false, "V does not have at least one element"); + CHECK_AND_ASSERT_MES(proof.V.size() <= maxM, false, "V has too many elements"); CHECK_AND_ASSERT_MES(proof.L.size() == proof.R.size(), false, "Mismatched L and R sizes"); CHECK_AND_ASSERT_MES(proof.L.size() > 0, false, "Empty proof"); diff --git a/tests/data/fuzz/bulletproof/BP0 b/tests/data/fuzz/bulletproof/BP0 index 17590b770f6a74ccb1e5065fcbfc4b979033cfff..5f7d0c794d337d6f325fc8e957529d4ab22b8df5 100644 GIT binary patch literal 707 zcmV;!0zCZzMTBY>VmG}n^ph$X8|J|Ndqd_fyFeVTFm~&k7J84vUv1X1ahjndF>C^64pWe^Q|j3ZLwFYU<74 z0;`%vp{No`xzgg%6Gtu_EV9>7FQ!l^^y~ZhFD5hQl9mHqnRdt7hW+0L9PgbPI+;}> zn#7VHmAvOFff5QQID)a$2@ULaa}byd1}W{URdV5}qhbMkFQrgc|;hQzMf5>mBA z2Rw)+$W5?Q`nPPhd-7MEUEf9rkE}W9>`5aSEi=Pmj;7pt<+;D)v$X&Q&|qZ(R-<$S zvy~YVy>l;~Ee0D>F4(J1SYx~jZW|II^RfsdeMlV)%Z-J0pfKR2UGI}@#>d}rC##+3 zh)QMnkt}5NQKcc>ZKjMUdjUSrlVS_Om(%YKB}0(oK$c-vl02eL_h1#3M1lfpVItl$76hsm2Q=FNk_m p+SCJq&<$%&FYlTX@R65jdtfY1$gk)?k|iw>j_@`C$P*Aq+yX_ZP`dyC literal 674 zcmV;T0$u%NxPE*JzbSo!yl8;_<%@E7;r2giX!3#@nHF}2-yz+wfqp|7UR-Qdwj9+b zzCA1#U07b7lAA#mNq&?bjnQ%5NX~;-+@vW>_jHL#6B`fLDMRr~G4Z7+HOcdq{^g>cW0<5H-Txs zK4^_g#!JWQSe6SEjg1uh@>$ribRSaIIv4B`#$HHNU(+&)9-{sr$Z-P(gY>f%HczN- za1u>r^P-gfu0d}oXj61%803yc{grnI+ce515z8NmJGsZgqc*F{cSD9a4_JXrYCEgK z%fU~J??G4(LyRLT^RI)RLH>-2{RZt_&3b>tvmin9NbYfKg>2LXjkZ0h6G>=P`ET*& zIaow&d`@bEw*Y88(e}D&0nva?>R-g?jlXw>y_Coit+~vfKkQ}2lQ*1LLgnUTZlR%L zz&TUwpTi`WrrpG?6{ZANr-8Qo8*}(rEWbzx2GslX-cyLfzVka(l*7S~7FDVQhwsS9 z-tJ3}RYQVg(7>|!YlEm_LUn=EtnEqaT?pVzdl}L*H?^X9{p;_{h12KI`A%;1N_xfW zwtheafYmjZkv7ZvWIiU}7i9>2E_`jlZqsGpb`20yw_I(6-#3eqm-x7dHajrQ3eXdfwg5aH4heiKcz@yZ#LI_3x$jRo zP|8a34cKy3t|62EU5ceKO0?rk{rb{XE1$H1^ZDF0#KpzSwL9&|*G-GN4EBiR+{Y6= zxkyX^?uN1&+RM&m3p*0{1G$wM6rgk14vz;EWFS)LR+Nl#vm1~CqY1M!00p8zEI>r} II8_ht0=-5~!~g&Q diff --git a/tests/data/fuzz/bulletproof/BP1 b/tests/data/fuzz/bulletproof/BP1 new file mode 100644 index 0000000000000000000000000000000000000000..9a8814a7bbbd4de9adf6fbe9fdc199e63ab5195d GIT binary patch literal 803 zcmV+;1Kj)qdTT@V38^yea>`PH1X7$?!HU@o{BpI8af&cCrZ{Q4Gg7MqI^r^Mhp`Fd zb(wAj*6p$cqxTM5KS z4P{4%Z;%lY43>gCRwz-y=fQtd-vTI0=6C|S)_tSBP)BG;=1U9^H6zi;yI7ZyFAa6y zF`_J2d_Vf^R>w$SEhM1T9WAAsVbzHV3S$q{997}Yn-yi-hoAYx&? zZcgkB2P$P6tnDshR62yGFqrs{9{gX7LN&!J|Na-8#~#y2p{{8etfu-!#sq(Pbs?eD z3W$IS5y!(d7(1w*K`&u;@-^s7!odw2xv~wdOiqol0;-C;(@^^_#O$EKpxKM}dUhLf z07~nVE@o;ufnzwA1OZ2D{zcLph;6gbFask*W2*3(#|bW8`f#nD!~&l@>j2UI<6HR? z+|uR?7_J<*vm;g!if0 za+{lP70*D+2q?O$l!eNRJ)#ZNfCtu}p5`jVR{JLyY7*u|{E~jUTb9@@@ zD$H^<+Z-M;dJ1Q0S6Oh|}iQ28VrG}GBQ5_GmfSVT)hh` zAwc4`>f3NOh8JimbR-ey-wg;lfb7s&Nt>-%cEHW}{{V_Re+vKr literal 0 HcmV?d00001 diff --git a/tests/data/fuzz/bulletproof/BP2 b/tests/data/fuzz/bulletproof/BP2 new file mode 100644 index 0000000000000000000000000000000000000000..f31ddf601b11429afe6c53497c69c27e522b5da8 GIT binary patch literal 931 zcmV;U16=$Bsaj>rTtD~XLu0p2N`%Le1G(vdK;if4BWJzw4KVt4s5N|9Oaxf@-CTDV znlZA4XgYc9(w8@auIo3s6-p(y|I28BafW@=zj){wOQQ!z=40-FoJX~!4g#W{%7od3 zIyCWZ>|fjJM}FxD#*SHwa(b*X$RULeiouI};Ubs5M97luM5*Qqyj!rmOS5bj_o4sI zw#Tb*$n*wG+-OPTOt*CQn)n#Xaa;k{r!+R_&*M~;v930;v$(P*3c&+qg?xaCBC#j> z3i@Ybrd1KxxooPF*+m#Y^@C$s-lA?vh)6umW-sS&&r@tUQbUAa5SJp(BbOhTlF&3t|@=g0T z*CZRgQ&ojKRT~Lx}pfM<7kCoTuq#jxpt=0`=LobvJKDJDQ)m zVUAE9eQ^b7JZ{q9O!1vB?3uFS^W?a(5BKRe+wUkE-q6yHQ!4$C1Jnfj-M)*MW8KSM zBo*P=Y9(8?GO?mWw+J7vEOyoZ@z+jr)s#eCzNl#3byV})e{aCfV(Ndf7%gy9Z+Gg| z`WA(VGIn|szKsqtq8hlz_C1B<1E^ErIo2#Hs3w8^<{*@D6K@lO?I~U>fwow#a=3+c zY=RNPZTgqJ>MWfgAhmp#`eC2tY*!WVE^JxsBeUmzB+VV#G~y$#fEW$OR&tr|cS-AD ziM)Qcb6_~6>bHd1?6;Xp@#1rMY{28gWje~|ky#WN-dMT3gMri@H@8E%oYzCgFk?qD zhM)s0Kc9`vOpcxaLuyLbGZsPZB><}^%>R*o7jw_F8ZK zNWdn(sGPqGhpw9W&`s0l4{7&d|CJAEgv zlvptg4jsaWl9QEIA;vzxwk^mpDh#UosvmuuujU(kmLH;s1R!17TS0>>VE-7SbA_=} F^$s4x&)5I} literal 0 HcmV?d00001 diff --git a/tests/data/fuzz/bulletproof/BP3 b/tests/data/fuzz/bulletproof/BP3 new file mode 100644 index 0000000000000000000000000000000000000000..4a2dd07e5d5e2d523853413ca093f7f5f6c6ccaa GIT binary patch literal 1123 zcmV-p1f2T_N~js%45hJMLc<68t0XDlOY46LCT+1_xr&!Vv4N-{k$d57DB2Y{G8H@6 zgBO1mgvB4ANbb+9C$MYA543eyEMBquncV&V1F$%W(wvZU~ZrM<}(G-3a_9 z;&0Y!5u$;$Lt!jCr4L%420`x8>{(Ib0(D{k5hpf!R;*(@tcYOwozCz%Kt%fY%>;uv z9^qCzT2dAgdCf^cfjU4xhPt~?M%zM7o{zm>P+gc{^aM^NA2a0B_!7s8>zU}GeKABp zUCDh{sEV5gu>5O-4_zX|L0uyI=C-;_8}a8w4HfNz{k%aJEYqz42~rSJjqIGN=gsG- z^7s2@_S~5ygP8G0yT72cIJ`rMGENlpSvjKB_~O?pdDs@~(rjLtb?jsP=jl{U5 zTPe+FO9u1x4CHfVk1%z8D0NlZ5?PSaXgNNX zH0PsTx-}jEr;eMw4fTV_eV8${f-GdWgIhekA&+L32%5^a+OlQCwyOj7s9P$DeB3vv zgR)qtfH9>sSrYv<;qYy}TQMcIui?*B!ek&fHrG1HjojRIhDIdOHW$I{GslEKy4-p! z%XxfDtDoa808K8U2{qolHVHr69?AjiCMtONSHST^!{tsids_g4Gw%FA!o~Owg&6|8@SsT`!@d6SW^!ruP3?R9EZPFR(%;DY&Xw{zwI)(^w^H69i&19YL2w+I>Jkh05xYz%!6;Ec2^4m7Pt*=5vOBp9*gbG;{y} literal 0 HcmV?d00001 diff --git a/tests/data/fuzz/bulletproof/BP4 b/tests/data/fuzz/bulletproof/BP4 new file mode 100644 index 0000000000000000000000000000000000000000..13425afb69effe0a3a9cd8cbda89fe14cad6b1e8 GIT binary patch literal 1443 zcmV;U1zh?NDkSP@83P~5@gCEr|Hyt!#C71HiJ-0CL|T6nHMOBdk4^c;EZ49j9LQBCB-|;oM|-3!ejtheP}7y~!B1=g_rJCY z2Sv&A!PM<+n~>^RO?gKudKJ*U2W;k#Dk0YWE1mWva;y{R(kfHU6Kvm@?4`UEe)d4g zM?pRKjc4XFZly4x3rJ9#w@Vb~Jyk#q*-nc=JS`06|9voZUjwFm^^Vd1myBXAc#3#C zUY3o@;}`cc7v`Qm5q$Xs$Dzx-W-QEcEZu?M2hFZ$l^o7|3dr>Ujpdp1K``MPCd9h` z8F?+i=~XCjY}0hZ_`dx^vwYEBonhP&_4{HZPR^&;M=~gEej(Z0vTz+q#X|M=mLiQB zQ)~A@9aR{kgtb?cdyysXm4#tsJOE`%kQ}!kj^(b$`iDzCL5f?z@z-xtLf^p3xeDvn z7OmYh`~bvZyIP!?%s^j-I)+bZ=`avn^$;N?8e)Dz&n2C`NU@8g@PzGKTcx=EoNz9j zP;z^lGVA`Y8uK62S=qbPw_n{4kIz`U{65O6Ipnad_CX7ucR#DJen7^`BHdx1j+{N^#s7?(oo76s0lj^}S ztOPH60~RAgLb{YnQHVRu%`vm%7ruxgMIChu+69P#lV9A?0)C*s`c?TX=PLgO^b~XT z`>%C?D;SOi#GRH1x==W1g4a({aotwSvml-aYpviIvdfeA;J|4tBeZYt zA^Kny*`!Ys+5D$_bJI^W94443@Ar|Dp1i!LtC#f!s{=1~)nB#38CC|?&I&qwpm95U z-Ru2bdUCXyB2qSlsUr=R9V3c|wF8v2n_1FbBFTfC8x8;qW@oL1h0sl8t1)PKgaYPb$IQ|)|D^4J z|E`k%E4L=kX)1vgwVUZxyO))kTHks$-oSw69v%vi;Oq(U+&wtByH=^+W@T)G!lK)( zn#A=asu!iO_9h3l6)x>Idc9L3rf8$lSB45djznO)rQH6$JHL6<2G@AOyeYxFsoJ)v zTVvu;LO`8lv$zsn^Z@3DQ_ZG1>RyiU!Q;yP+kUD0N{t)E3;&m$s;>h;wZrlu~p zk#yg)nTl7ZObeX$kZ8(|%LlfxyIt!Ftz{VbG#(Eb?*uO$eQ<3oV3ae5q-}tyktOuR zl4eU2Y$Nbc9$EWD)l>x=6zA@```3ix*82kKueGYXC+86K{8H~c-(J}Mj2PDjH xzK2idjdXElT4~s9B?U+rP5o`}*965}`1<&n3$Dp$ha_9=KKvXEd$yQ4p#xp! +#include + +namespace +{ + struct fuzz_input + { + rct::keyV commitments; + rct::Bulletproof proof; + + BEGIN_SERIALIZE_OBJECT() + FIELD(commitments) + FIELD(proof) + END_SERIALIZE() + }; +} BEGIN_INIT_SIMPLE_FUZZER() END_INIT_SIMPLE_FUZZER() BEGIN_SIMPLE_FUZZER() - binary_archive ba{{buf, len}}; - rct::Bulletproof proof{}; - ::serialization::serialize(ba, proof); + if (len > 4096) + return 0; + + fuzz_input input; + const std::string blob{reinterpret_cast(buf), len}; + if (!serialization::parse_binary(blob, input)) + return 0; + + input.proof.V = std::move(input.commitments); + rct::bulletproof_VERIFY(input.proof); END_SIMPLE_FUZZER() diff --git a/tests/unit_tests/bulletproofs.cpp b/tests/unit_tests/bulletproofs.cpp index 5d8d4d38a..197ae8bc2 100644 --- a/tests/unit_tests/bulletproofs.cpp +++ b/tests/unit_tests/bulletproofs.cpp @@ -77,6 +77,20 @@ TEST(bulletproofs, valid_multi_random) } } +TEST(bulletproofs, invalid_too_many_outputs) +{ + std::vector amounts(16, 0); + rct::keyV gamma(16); + for (rct::key &mask: gamma) + mask = rct::skGen(); + rct::Bulletproof proof = bulletproof_PROVE(amounts, gamma); + ASSERT_TRUE(rct::bulletproof_VERIFY(proof)); + proof.V.push_back(proof.V.back()); + proof.L.push_back(proof.L.back()); + proof.R.push_back(proof.R.back()); + ASSERT_FALSE(rct::bulletproof_VERIFY(proof)); +} + TEST(bulletproofs, multi_splitting) { rct::ctkeyV sc, pc; diff --git a/tests/unit_tests/bulletproofs_plus.cpp b/tests/unit_tests/bulletproofs_plus.cpp index c1b5e1326..b998c5128 100644 --- a/tests/unit_tests/bulletproofs_plus.cpp +++ b/tests/unit_tests/bulletproofs_plus.cpp @@ -77,6 +77,20 @@ TEST(bulletproofs_plus, valid_multi_random) } } +TEST(bulletproofs_plus, invalid_too_many_outputs) +{ + std::vector amounts(16, 0); + rct::keyV gamma(16); + for (rct::key &mask: gamma) + mask = rct::skGen(); + rct::BulletproofPlus proof = bulletproof_plus_PROVE(amounts, gamma); + ASSERT_TRUE(rct::bulletproof_plus_VERIFY(proof)); + proof.V.push_back(proof.V.back()); + proof.L.push_back(proof.L.back()); + proof.R.push_back(proof.R.back()); + ASSERT_FALSE(rct::bulletproof_plus_VERIFY(proof)); +} + TEST(bulletproofs_plus, valid_aggregated) { static const size_t N_PROOFS = 8;