Checkpoint the party/runtime stack before share-program and malicious-mode work.

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>
This commit is contained in:
Ryan Henry 2026-09-28 05:59:19 -06:00
parent 695f8e84f7
commit 0d22946a0e
1835 changed files with 170291 additions and 2849 deletions

View file

@ -0,0 +1,347 @@
/// @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});
}