/// @file shamir_adversarial_test.cpp /// @brief Corruption and misuse checks for (K,N) Shamir sharing. /// @details Exactly K shares accept any values, including a mislabeled point. /// Each further share is checked against the polynomial of the first /// K, wherever the bad share sits in that list. A full set of shares /// of a different secret is consistent and opens that secret. A zero /// leading coefficient drops the real threshold. (2,3) shamir3 /// rejects a party outside 1..3 and an inconsistent third share. /// Threshold 1 hides nothing: every share is the secret. When K = N /// there is no extra share, so a tampered full set is not detected. #include #include #include #include #include #include #include #include "dpf.hpp" namespace { using F = dpf::fp61; template std::array raws(const Tuple & shares, std::index_sequence) { return {{std::get(shares).raw()...}}; } template std::array raws(const Tuple & shares) { return raws(shares, std::make_index_sequence{}); } template std::array, N> points(const std::array & value) { std::array, N> out{}; for (std::size_t i = 0; i < N; ++i) out[i] = {static_cast(i + 1), value[i]}; return out; } } // namespace TEST(ShamirAdversarial, ExactlyKAcceptsALie) { const std::array coeff{{F{2}, F{3}}}; const auto dealt = dpf::shamir::deal(F{10}, coeff); auto bad = std::get<0>(dealt); bad += F{1}; const F opened = dpf::shamir::reconstruct(bad, std::get<1>(dealt), std::get<2>(dealt)); EXPECT_NE(opened, F{10}); } TEST(ShamirAdversarial, ExtraShareCatchesALieInAnySlot) { const std::array coeff{{F{2}, F{3}}}; const auto dealt = dpf::shamir::deal(F{10}, coeff); const auto honest = points<5>(raws<5>(dealt)); EXPECT_EQ((dpf::shamir::reconstruct(honest)), F{10}); for (std::size_t slot = 0; slot < 5; ++slot) { auto lied = honest; lied[slot].value = lied[slot].value + F{1}; EXPECT_THROW((dpf::shamir::reconstruct(lied)), std::runtime_error) << "slot " << slot; // The same lie, moved to the front of the argument list. std::array, 5> front{}; front[0] = lied[slot]; std::size_t n = 1; for (std::size_t i = 0; i < 5; ++i) if (i != slot) front[n++] = honest[i]; EXPECT_THROW((dpf::shamir::reconstruct(front)), std::runtime_error) << "front slot " << slot; } } TEST(ShamirAdversarial, ConsistentForgeryOpensTheForgedSecret) { const std::array coeff{{F{1}, F{4}}}; const auto forged = dpf::shamir::deal(F{99}, coeff); const auto held = points<5>(raws<5>(forged)); EXPECT_EQ((dpf::shamir::reconstruct(held)), F{99}); const auto real = dpf::shamir::deal(F{10}, std::array{{F{2}, F{3}}}); auto mixed = points<5>(raws<5>(real)); mixed[4] = held[4]; EXPECT_THROW((dpf::shamir::reconstruct(mixed)), std::runtime_error); } TEST(ShamirAdversarial, OrderOfAConsistentSetDoesNotMatter) { const std::array coeff{{F{2}, F{3}}}; const auto dealt = dpf::shamir::deal(F{10}, coeff); const F values[5] = { std::get<0>(dealt).raw(), std::get<1>(dealt).raw(), std::get<2>(dealt).raw(), std::get<3>(dealt).raw(), std::get<4>(dealt).raw()}; int idx[3] = {0, 2, 4}; do { const std::array, 3> subset{{ {idx[0] + 1, values[idx[0]]}, {idx[1] + 1, values[idx[1]]}, {idx[2] + 1, values[idx[2]]}}}; EXPECT_EQ((dpf::shamir::reconstruct(subset)), F{10}); } while (std::next_permutation(idx, idx + 3)); const auto forward = points<5>(raws<5>(dealt)); std::array, 5> backward{}; for (std::size_t i = 0; i < 5; ++i) backward[i] = forward[4 - i]; EXPECT_EQ((dpf::shamir::reconstruct(backward)), F{10}); } TEST(ShamirAdversarial, WrongThresholdAndMislabeledPoint) { // p(x) = 8 + 5x + x^2. The line through p(1)=14 and p(2)=22 opens 6, not 8. const auto steep = dpf::shamir::deal(F{8}, std::array{{F{5}, F{1}}}); const std::array, 2> line{{ {1, std::get<0>(steep).raw()}, {2, std::get<1>(steep).raw()}}}; EXPECT_EQ(std::get<0>(steep).raw(), F{14}); EXPECT_EQ(std::get<1>(steep).raw(), F{22}); EXPECT_EQ((dpf::shamir::reconstruct(line)), F{6}); EXPECT_NE((dpf::shamir::reconstruct(line)), F{8}); // A zero leading coefficient is only a (2,5) sharing. const auto flat = dpf::shamir::deal(F{8}, std::array{{F{5}, F{0}}}); const std::array, 2> still_line{{ {1, std::get<0>(flat).raw()}, {4, std::get<3>(flat).raw()}}}; EXPECT_EQ((dpf::shamir::reconstruct(still_line)), F{8}); auto [s0, s1, s2] = dpf::make_shamir_shares(F{20}, F{3}); const std::array, 2> swapped{{ {1, s1.raw()}, {2, s0.raw()}}}; EXPECT_NE((dpf::shamir::reconstruct(swapped)), F{20}); const std::array, 3> swapped3{{ {1, s1.raw()}, {2, s0.raw()}, {3, s2.raw()}}}; EXPECT_THROW((dpf::shamir::reconstruct(swapped3)), std::runtime_error); } TEST(ShamirAdversarial, OneShareDoesNotFixTheSecret) { // p(1) = 13 for both (secret, slope) = (10, 3) and (11, 2). auto [a0, a1, a2] = dpf::make_shamir_shares(F{10}, F{3}); auto [b0, b1, b2] = dpf::make_shamir_shares(F{11}, F{2}); EXPECT_EQ(a0.raw(), b0.raw()); EXPECT_EQ(a0.raw(), F{13}); EXPECT_EQ((dpf::reconstruct(a0, a1)), F{10}); EXPECT_EQ((dpf::reconstruct(b0, b1)), F{11}); EXPECT_NE((dpf::reconstruct(a0, b1)), F{10}); (void)a2; (void)b2; } TEST(ShamirAdversarial, RejectsCountPointsAndOneSidedEdit) { const auto dealt = dpf::shamir::deal(F{10}, std::array{{F{2}, F{3}}}); const auto honest = points<5>(raws<5>(dealt)); const std::array, 2> too_few{{honest[0], honest[1]}}; EXPECT_THROW((dpf::shamir::reconstruct(too_few)), std::invalid_argument); EXPECT_THROW((dpf::shamir::reconstruct(honest.data(), 0)), std::invalid_argument); EXPECT_THROW((dpf::shamir::reconstruct(honest.data(), 6)), std::invalid_argument); const std::array, 3> dup{{ {1, honest[0].value}, {1, honest[0].value}, {3, honest[2].value}}}; EXPECT_THROW((dpf::shamir::reconstruct(dup)), std::invalid_argument); const std::array, 3> zero_pt{{ {0, honest[0].value}, {2, honest[1].value}, {3, honest[2].value}}}; EXPECT_THROW((dpf::shamir::reconstruct(zero_pt)), std::invalid_argument); const std::array, 3> negative{{ {-1, honest[0].value}, {2, honest[1].value}, {3, honest[2].value}}}; EXPECT_THROW((dpf::shamir::reconstruct(negative)), std::invalid_argument); const std::array, 3> past{{ {1, honest[0].value}, {2, honest[1].value}, {6, honest[2].value}}}; EXPECT_THROW((dpf::shamir::reconstruct(past)), std::invalid_argument); auto one = std::get<0>(dealt); one += F{4}; const F sided = dpf::shamir::reconstruct(one, std::get<1>(dealt), std::get<2>(dealt)); EXPECT_NE(sided, F{10}); auto with_extra = honest; with_extra[0].value = one.raw(); EXPECT_THROW((dpf::shamir::reconstruct(with_extra)), std::runtime_error); auto all = dealt; std::get<0>(all) += F{4}; std::get<1>(all) += F{4}; std::get<2>(all) += F{4}; std::get<3>(all) += F{4}; std::get<4>(all) += F{4}; EXPECT_EQ((dpf::shamir::reconstruct(points<5>(raws<5>(all)))), F{14}); } TEST(ShamirAdversarial, ThresholdOneHidesNothingAndFullThresholdNeedsEveryShare) { const auto copied = dpf::shamir::deal(F{4}, std::array{}); EXPECT_EQ(std::get<0>(copied).raw(), F{4}); EXPECT_EQ(std::get<3>(copied).raw(), F{4}); auto lie = std::get<0>(copied); lie += F{1}; EXPECT_EQ(dpf::shamir::reconstruct(lie), F{5}); auto all = points<4>(raws<4>(copied)); all[2].value = all[2].value + F{1}; EXPECT_THROW((dpf::shamir::reconstruct(all)), std::runtime_error); const auto quad = dpf::shamir::deal( F{9}, std::array{{F{1}, F{2}, F{3}}}); const F opened4 = dpf::shamir::reconstruct(std::get<0>(quad), std::get<1>(quad), std::get<2>(quad), std::get<3>(quad)); EXPECT_EQ(opened4, F{9}); const std::array, 3> missing{{ {1, std::get<0>(quad).raw()}, {2, std::get<1>(quad).raw()}, {3, std::get<2>(quad).raw()}}}; EXPECT_THROW((dpf::shamir::reconstruct(missing)), std::invalid_argument); // K = N, so the full set is exactly K shares and a lie is not detected. auto bad = std::get<3>(quad); bad += F{1}; const F lied = dpf::shamir::reconstruct(std::get<0>(quad), std::get<1>(quad), std::get<2>(quad), bad); EXPECT_NE(lied, F{9}); const auto room = dpf::shamir::deal(F{9}, std::array{{F{1}, F{2}}}); auto bad_extra = std::get<3>(room); bad_extra += F{1}; EXPECT_THROW((dpf::shamir::reconstruct(std::get<0>(room), std::get<1>(room), std::get<2>(room), bad_extra)), std::runtime_error); } TEST(ShamirAdversarial, Shamir3RejectsBadPartiesAndMatchesTheGeneralOpen) { const auto s = dpf::shamir3::share_secret(F{42}); const F from_general = dpf::shamir::reconstruct( std::array, 2>{{ {s[0].party, s[0].value}, {s[2].party, s[2].value}}}); const F from_shamir3 = dpf::shamir3::reconstruct(s[0], s[2]); EXPECT_EQ(from_shamir3, from_general); const F from_three = dpf::shamir3::reconstruct(s[0], s[1], s[2]); EXPECT_EQ(from_three, F{42}); EXPECT_THROW((dpf::shamir3::reconstruct( dpf::shamir3::share{0, F{1}}, dpf::shamir3::share{1, F{1}})), std::invalid_argument); EXPECT_THROW((dpf::shamir3::reconstruct( dpf::shamir3::share{1, F{1}}, dpf::shamir3::share{4, F{1}})), std::invalid_argument); EXPECT_THROW((dpf::shamir3::reconstruct( dpf::shamir3::share{2, F{1}}, dpf::shamir3::share{2, F{2}})), std::invalid_argument); EXPECT_THROW((dpf::shamir3::reconstruct(s[0], s[1], dpf::shamir3::share{4, s[2].value})), std::invalid_argument); auto bad = s[2]; bad.value = bad.value + F{1}; EXPECT_THROW((dpf::shamir3::reconstruct(s[0], s[1], bad)), std::runtime_error); EXPECT_THROW((dpf::shamir::reconstruct( std::array, 3>{{ {s[0].party, s[0].value}, {s[1].party, s[1].value}, {bad.party, bad.value}}})), std::runtime_error); EXPECT_THROW((dpf::shamir3::add(s[0], s[1])), std::invalid_argument); const auto doubled = dpf::shamir3::add(s[0], s[0]); EXPECT_EQ(doubled.value, s[0].value + s[0].value); EXPECT_THROW((dpf::shamir3::typed_share<0>(s[1])), std::invalid_argument); const auto typed = dpf::shamir3::typed_share<0>(s[0]); EXPECT_EQ(typed.raw(), s[0].value); auto [t0, t1, t2] = dpf::make_shamir_shares(F{42}, F{0}); t2 += F{1}; EXPECT_THROW((dpf::reconstruct(t0, t1, t2)), std::runtime_error); EXPECT_THROW((dpf::shamir::reconstruct(t2, t0, t1)), std::runtime_error); } TEST(ShamirAdversarial, ZeroAndFieldEdge) { const auto zeros = dpf::shamir::deal(F{0}, std::array{{F{0}, F{0}}}); EXPECT_EQ(std::get<0>(zeros).raw(), F{0}); EXPECT_EQ(std::get<3>(zeros).raw(), F{0}); const F opened0 = dpf::shamir::reconstruct( std::get<1>(zeros), std::get<2>(zeros), std::get<3>(zeros)); EXPECT_EQ(opened0, F{0}); const F edge{dpf::fp61_mod - 1}; const auto high = dpf::shamir::deal(edge, std::array{{edge}}); const F opened_edge = dpf::reconstruct(std::get<0>(high), std::get<2>(high)); EXPECT_EQ(opened_edge, edge); static_assert(std::is_same_v, dpf::shamir_share>); } TEST(ShamirAdversarial, Gf2PointsMustFitAndLiesAreXor) { // GF(2^8) has room for points 1..5. A one-bit lie is invisible at K=3 // and caught when an honest extra share is present. using G = dpf::gf28; const auto dealt = dpf::shamir::deal( G{0x1b}, std::array{{G{2}, G{9}}}); auto bad = std::get<0>(dealt); bad += G{1}; const G sided = dpf::shamir::reconstruct(bad, std::get<1>(dealt), std::get<2>(dealt)); EXPECT_NE(sided, G{0x1b}); auto extra = std::get<3>(dealt); EXPECT_THROW((dpf::shamir::reconstruct(bad, std::get<1>(dealt), std::get<2>(dealt), extra)), std::runtime_error); // Adding a share to itself is XOR, so the opened secret is 0, not twice. const G doubled = dpf::shamir::reconstruct( std::get<0>(dealt) + std::get<0>(dealt), std::get<1>(dealt) + std::get<1>(dealt), std::get<2>(dealt) + std::get<2>(dealt)); EXPECT_EQ(doubled, G{0}); // GF(16) holds points 1..15. Point 16 is 0. Point 17 aliases point 1. using H = dpf::gf24; const auto fit = dpf::shamir::deal(H{0xa}, std::array{{H{0x3}}}); EXPECT_EQ((dpf::shamir::reconstruct(std::get<0>(fit), std::get<14>(fit))), H{0xa}); const auto zero_pt = dpf::shamir::deal(H{0xa}, std::array{{H{0x3}}}); // Point 16 is 0 in GF(16), so that share is p(0), the secret. EXPECT_EQ(std::get<15>(zero_pt).raw(), H{0xa}.raw()); EXPECT_THROW((dpf::shamir::reconstruct(std::get<0>(zero_pt), std::get<15>(zero_pt))), std::invalid_argument); const auto alias = dpf::shamir::deal(H{0xa}, std::array{{H{0x3}}}); EXPECT_EQ(std::get<0>(alias).raw(), std::get<16>(alias).raw()); EXPECT_THROW((dpf::shamir::reconstruct(std::get<0>(alias), std::get<16>(alias))), std::invalid_argument); // GF(4) holds a (2,3) sharing. Point 4 is 0. const auto small = dpf::shamir::deal( dpf::gf22{1}, std::array{{dpf::gf22{2}}}); EXPECT_THROW((dpf::shamir::reconstruct(std::get<0>(small), std::get<3>(small))), std::invalid_argument); EXPECT_EQ((dpf::shamir::reconstruct(std::get<0>(small), std::get<2>(small))), dpf::gf22{1}); }