Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume. Co-authored-by: Cursor <cursoragent@cursor.com>
347 lines
15 KiB
C++
347 lines
15 KiB
C++
/// @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 <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <stdexcept>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
#include "dpf.hpp"
|
|
|
|
namespace
|
|
{
|
|
|
|
using F = dpf::fp61;
|
|
|
|
template <typename Tuple, std::size_t... I>
|
|
std::array<F, sizeof...(I)> raws(const Tuple & shares, std::index_sequence<I...>)
|
|
{
|
|
return {{std::get<I>(shares).raw()...}};
|
|
}
|
|
|
|
template <std::size_t N, typename Tuple>
|
|
std::array<F, N> raws(const Tuple & shares)
|
|
{
|
|
return raws(shares, std::make_index_sequence<N>{});
|
|
}
|
|
|
|
template <std::size_t N>
|
|
std::array<dpf::shamir::point_share<F>, N> points(const std::array<F, N> & value)
|
|
{
|
|
std::array<dpf::shamir::point_share<F>, N> out{};
|
|
for (std::size_t i = 0; i < N; ++i)
|
|
out[i] = {static_cast<int>(i + 1), value[i]};
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(ShamirAdversarial, ExactlyKAcceptsALie)
|
|
{
|
|
const std::array<F, 2> coeff{{F{2}, F{3}}};
|
|
const auto dealt = dpf::shamir::deal<F, 3, 5>(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<F, 2> coeff{{F{2}, F{3}}};
|
|
const auto dealt = dpf::shamir::deal<F, 3, 5>(F{10}, coeff);
|
|
const auto honest = points<5>(raws<5>(dealt));
|
|
EXPECT_EQ((dpf::shamir::reconstruct<F, 3, 5>(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<F, 3, 5>(lied)), std::runtime_error)
|
|
<< "slot " << slot;
|
|
|
|
// The same lie, moved to the front of the argument list.
|
|
std::array<dpf::shamir::point_share<F>, 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<F, 3, 5>(front)), std::runtime_error)
|
|
<< "front slot " << slot;
|
|
}
|
|
}
|
|
|
|
TEST(ShamirAdversarial, ConsistentForgeryOpensTheForgedSecret)
|
|
{
|
|
const std::array<F, 2> coeff{{F{1}, F{4}}};
|
|
const auto forged = dpf::shamir::deal<F, 3, 5>(F{99}, coeff);
|
|
const auto held = points<5>(raws<5>(forged));
|
|
EXPECT_EQ((dpf::shamir::reconstruct<F, 3, 5>(held)), F{99});
|
|
|
|
const auto real = dpf::shamir::deal<F, 3, 5>(F{10}, std::array<F, 2>{{F{2}, F{3}}});
|
|
auto mixed = points<5>(raws<5>(real));
|
|
mixed[4] = held[4];
|
|
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(mixed)), std::runtime_error);
|
|
}
|
|
|
|
TEST(ShamirAdversarial, OrderOfAConsistentSetDoesNotMatter)
|
|
{
|
|
const std::array<F, 2> coeff{{F{2}, F{3}}};
|
|
const auto dealt = dpf::shamir::deal<F, 3, 5>(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<dpf::shamir::point_share<F>, 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<F, 3, 5>(subset)), F{10});
|
|
}
|
|
while (std::next_permutation(idx, idx + 3));
|
|
|
|
const auto forward = points<5>(raws<5>(dealt));
|
|
std::array<dpf::shamir::point_share<F>, 5> backward{};
|
|
for (std::size_t i = 0; i < 5; ++i)
|
|
backward[i] = forward[4 - i];
|
|
EXPECT_EQ((dpf::shamir::reconstruct<F, 3, 5>(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, 3, 5>(F{8}, std::array<F, 2>{{F{5}, F{1}}});
|
|
const std::array<dpf::shamir::point_share<F>, 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<F, 2, 5>(line)), F{6});
|
|
EXPECT_NE((dpf::shamir::reconstruct<F, 2, 5>(line)), F{8});
|
|
|
|
// A zero leading coefficient is only a (2,5) sharing.
|
|
const auto flat = dpf::shamir::deal<F, 3, 5>(F{8}, std::array<F, 2>{{F{5}, F{0}}});
|
|
const std::array<dpf::shamir::point_share<F>, 2> still_line{{
|
|
{1, std::get<0>(flat).raw()}, {4, std::get<3>(flat).raw()}}};
|
|
EXPECT_EQ((dpf::shamir::reconstruct<F, 2, 5>(still_line)), F{8});
|
|
|
|
auto [s0, s1, s2] = dpf::make_shamir_shares(F{20}, F{3});
|
|
const std::array<dpf::shamir::point_share<F>, 2> swapped{{
|
|
{1, s1.raw()}, {2, s0.raw()}}};
|
|
EXPECT_NE((dpf::shamir::reconstruct<F, 2, 3>(swapped)), F{20});
|
|
const std::array<dpf::shamir::point_share<F>, 3> swapped3{{
|
|
{1, s1.raw()}, {2, s0.raw()}, {3, s2.raw()}}};
|
|
EXPECT_THROW((dpf::shamir::reconstruct<F, 2, 3>(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, 3, 5>(F{10}, std::array<F, 2>{{F{2}, F{3}}});
|
|
const auto honest = points<5>(raws<5>(dealt));
|
|
const std::array<dpf::shamir::point_share<F>, 2> too_few{{honest[0], honest[1]}};
|
|
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(too_few)), std::invalid_argument);
|
|
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(honest.data(), 0)),
|
|
std::invalid_argument);
|
|
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(honest.data(), 6)),
|
|
std::invalid_argument);
|
|
|
|
const std::array<dpf::shamir::point_share<F>, 3> dup{{
|
|
{1, honest[0].value}, {1, honest[0].value}, {3, honest[2].value}}};
|
|
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(dup)), std::invalid_argument);
|
|
const std::array<dpf::shamir::point_share<F>, 3> zero_pt{{
|
|
{0, honest[0].value}, {2, honest[1].value}, {3, honest[2].value}}};
|
|
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(zero_pt)), std::invalid_argument);
|
|
const std::array<dpf::shamir::point_share<F>, 3> negative{{
|
|
{-1, honest[0].value}, {2, honest[1].value}, {3, honest[2].value}}};
|
|
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(negative)), std::invalid_argument);
|
|
const std::array<dpf::shamir::point_share<F>, 3> past{{
|
|
{1, honest[0].value}, {2, honest[1].value}, {6, honest[2].value}}};
|
|
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(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<F, 3, 5>(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<F, 3, 5>(points<5>(raws<5>(all)))), F{14});
|
|
}
|
|
|
|
TEST(ShamirAdversarial, ThresholdOneHidesNothingAndFullThresholdNeedsEveryShare)
|
|
{
|
|
const auto copied = dpf::shamir::deal<F, 1, 4>(F{4}, std::array<F, 0>{});
|
|
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<F, 1, 4>(all)), std::runtime_error);
|
|
|
|
const auto quad = dpf::shamir::deal<F, 4, 4>(
|
|
F{9}, std::array<F, 3>{{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<dpf::shamir::point_share<F>, 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<F, 4, 4>(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, 3, 4>(F{9}, std::array<F, 2>{{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<F, 2, 3>(
|
|
std::array<dpf::shamir::point_share<F>, 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<F, 2, 3>(
|
|
std::array<dpf::shamir::point_share<F>, 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, 3, 4>(F{0}, std::array<F, 2>{{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<F, 2, 3>(edge, std::array<F, 1>{{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<F, 0, 2, 3>, dpf::shamir_share<F, 0>>);
|
|
}
|
|
|
|
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, 3, 5>(
|
|
G{0x1b}, std::array<G, 2>{{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, 2, 15>(H{0xa}, std::array<H, 1>{{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, 2, 16>(H{0xa}, std::array<H, 1>{{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, 2, 17>(H{0xa}, std::array<H, 1>{{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, 2, 4>(
|
|
dpf::gf22{1}, std::array<dpf::gf22, 1>{{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});
|
|
}
|