libdpf/include/dpf/pprf.hpp
Ryan Henry 0d22946a0e 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>
2026-09-28 05:59:19 -06:00

408 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// @file dpf/pprf.hpp
/// @brief GGM puncturable PRF on the library AES-128 PRG.
/// @details A master seed evaluates at every domain point. `puncture` at a
/// single `α` returns one sibling seed per level so evaluation
/// everywhere except `α` costs O(n) PRG calls. `puncture` over a
/// set `H` returns the copath of that set: every node whose parent
/// lies on a path to `H` and which itself does not. Shared prefixes
/// are stored once. The programmed leaf values are stored beside the
/// puncture when the holder of the master asks for them; an audit
/// opening of a replica-seed pool leaves them out (`program_hidden =
/// false`). Complexity of the set walk is O(|H| · n) PRG expansions
/// and at most |H| · n published nodes; the domain is never
/// materialized. One-point and set forms agree on `{α}`.
/// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors)
/// @license Released under a GNU General Public v2.0 (GPLv2) license.
#ifndef LIBDPF_INCLUDE_DPF_PPRF_HPP__
#define LIBDPF_INCLUDE_DPF_PPRF_HPP__
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <vector>
#include "hedley/hedley.h"
#include "dpf/prg_aes.hpp"
#include "dpf/random.hpp"
#include "dpf/utils.hpp"
namespace dpf
{
/// @brief Master key for a puncturable PRF over `InputT`.
/// @tparam InputT domain type (`uint32_t`, `simde_uint128`, ...)
/// @tparam PRG exterior/interior PRG; defaults to AES-128
template <typename InputT, typename PRG = dpf::prg::aes128>
struct pprf_master
{
using input_type = InputT;
using prg = PRG;
using block_type = typename PRG::block_type;
static constexpr std::size_t bitlength = utils::bitlength_of_v<InputT>;
block_type root{};
};
/// @brief Punctured key: sibling seed per level, optional programmed leaf.
template <typename InputT, typename PRG = dpf::prg::aes128>
struct pprf_punctured
{
using input_type = InputT;
using prg = PRG;
using block_type = typename PRG::block_type;
static constexpr std::size_t bitlength = utils::bitlength_of_v<InputT>;
input_type alpha{};
/// @brief `siblings[i]` is the seed for the sibling of the path bit at
/// level `i` (MSB = 0).
std::array<block_type, bitlength> siblings{};
/// @brief Path bit taken toward `alpha` at each level (1 = right).
std::array<std::uint8_t, bitlength> path_bits{};
/// @brief When set, `pprf_eval` at `alpha` returns this leaf.
std::optional<block_type> programmed{};
};
/// @brief One published node on the copath of a hidden set.
/// @details `level` is the number of path bits from the root (`0` only for the
/// empty-`H` root). `prefix` holds those bits right-aligned, so the
/// top `level` bits of a domain point `x` match when
/// `(x >> (n - level)) == prefix`.
template <typename InputT, typename PRG = dpf::prg::aes128>
struct pprf_copath_node
{
using input_type = InputT;
using prg = PRG;
using block_type = typename PRG::block_type;
std::size_t level{};
input_type prefix{};
block_type seed{};
};
/// @brief Copath of a hidden set: sorted unique points plus published nodes.
/// @details Empty `hidden` publishes the root so every domain point evaluates.
/// A full-domain `hidden` publishes nothing. When `programmed` is
/// non-empty it has one leaf per hidden point, in the same order as
/// `hidden`. A live seed is never among `nodes`.
template <typename InputT, typename PRG = dpf::prg::aes128>
struct pprf_copath
{
using input_type = InputT;
using prg = PRG;
using block_type = typename PRG::block_type;
static constexpr std::size_t bitlength = utils::bitlength_of_v<InputT>;
std::vector<input_type> hidden{};
std::vector<pprf_copath_node<InputT, PRG>> nodes{};
std::vector<block_type> programmed{};
};
/// @brief Sample a fresh master seed.
template <typename InputT, typename PRG = dpf::prg::aes128>
HEDLEY_WARN_UNUSED_RESULT
HEDLEY_NO_THROW
pprf_master<InputT, PRG> make_pprf_master() noexcept
{
pprf_master<InputT, PRG> m;
m.root = dpf::uniform_sample<typename PRG::block_type>();
return m;
}
namespace detail
{
namespace pprf_impl
{
/// @brief Path bit of `x` at `level` (0 = MSB).
template <typename InputT>
HEDLEY_CONST
HEDLEY_NO_THROW
HEDLEY_ALWAYS_INLINE
constexpr bool path_bit(InputT x, std::size_t level) noexcept
{
constexpr std::size_t n = utils::bitlength_of_v<InputT>;
constexpr auto to_int = utils::to_integral_type<InputT>{};
using integral_t = typename utils::to_integral_type<InputT>::integral_type;
const integral_t xi = static_cast<integral_t>(to_int(x));
const std::size_t shift = n - 1 - level;
return static_cast<bool>(utils::shift_right(xi, shift) & integral_t{1});
}
/// @brief Split `seed` into left (pos 0) and right (pos 1) children.
template <typename PRG>
HEDLEY_ALWAYS_INLINE
HEDLEY_NO_THROW
void expand_children(typename PRG::block_type seed,
typename PRG::block_type & left, typename PRG::block_type & right) noexcept
{
// Match DPF interior: pos 0 = left child, pos 1 = right child.
left = PRG::eval(seed, 0);
right = PRG::eval(seed, 1);
}
/// @brief Top `level` path bits of `x`, right-aligned.
template <typename InputT>
HEDLEY_CONST
HEDLEY_NO_THROW
HEDLEY_ALWAYS_INLINE
constexpr InputT path_prefix(InputT x, std::size_t level) noexcept
{
if (level == 0)
return InputT{};
constexpr std::size_t n = utils::bitlength_of_v<InputT>;
constexpr auto to_int = utils::to_integral_type<InputT>{};
using integral_t = typename utils::to_integral_type<InputT>::integral_type;
const integral_t xi = static_cast<integral_t>(to_int(x));
return static_cast<InputT>(utils::shift_right(xi, n - level));
}
/// @brief Whether `x` lies under the node `(level, prefix)`.
template <typename InputT>
HEDLEY_CONST
HEDLEY_NO_THROW
HEDLEY_ALWAYS_INLINE
constexpr bool prefix_matches(InputT x, std::size_t level,
InputT prefix) noexcept
{
return path_prefix(x, level) == prefix;
}
/// @brief Recursively publish the copath of `hidden[lo, hi)` under `seed`.
/// @details `hidden` must be sorted unique. Emits at most one node per
/// off-path sibling; shared prefixes are visited once. When
/// `program_hidden` is set, appends leaf seeds in sorted `hidden`
/// order.
template <typename InputT, typename PRG>
void collect_copath(typename PRG::block_type seed, std::size_t level,
InputT prefix, const std::vector<InputT> & hidden, std::size_t lo,
std::size_t hi, bool program_hidden,
std::vector<pprf_copath_node<InputT, PRG>> & nodes,
std::vector<typename PRG::block_type> & programmed)
{
constexpr std::size_t n = utils::bitlength_of_v<InputT>;
if (lo == hi)
return;
if (level == n)
{
if (program_hidden)
programmed.push_back(seed);
return;
}
// `hidden` is sorted by domain value, so MSB-first path order matches.
std::size_t mid = lo;
while (mid < hi && !path_bit(hidden[mid], level))
++mid;
typename PRG::block_type left{}, right{};
expand_children<PRG>(seed, left, right);
const InputT left_prefix =
static_cast<InputT>((static_cast<InputT>(prefix) << 1) | InputT{0});
const InputT right_prefix =
static_cast<InputT>((static_cast<InputT>(prefix) << 1) | InputT{1});
const bool left_on_path = mid > lo;
const bool right_on_path = hi > mid;
// Match the one-point walk: publish the off-path sibling, then descend.
if (left_on_path && right_on_path)
{
collect_copath<InputT, PRG>(left, level + 1, left_prefix, hidden, lo,
mid, program_hidden, nodes, programmed);
collect_copath<InputT, PRG>(right, level + 1, right_prefix, hidden, mid,
hi, program_hidden, nodes, programmed);
}
else if (left_on_path)
{
nodes.push_back(
pprf_copath_node<InputT, PRG>{level + 1, right_prefix, right});
collect_copath<InputT, PRG>(left, level + 1, left_prefix, hidden, lo,
mid, program_hidden, nodes, programmed);
}
else
{
nodes.push_back(
pprf_copath_node<InputT, PRG>{level + 1, left_prefix, left});
collect_copath<InputT, PRG>(right, level + 1, right_prefix, hidden, mid,
hi, program_hidden, nodes, programmed);
}
}
} // namespace pprf_impl
} // namespace detail
/// @brief Evaluate the master at `x`. Returns one AES block (the leaf seed).
template <typename InputT, typename PRG = dpf::prg::aes128>
HEDLEY_WARN_UNUSED_RESULT
HEDLEY_NO_THROW
typename PRG::block_type pprf_eval(const pprf_master<InputT, PRG> & master,
InputT x) noexcept
{
using block = typename PRG::block_type;
constexpr std::size_t n = pprf_master<InputT, PRG>::bitlength;
block cur = master.root;
for (std::size_t level = 0; level < n; ++level)
{
block left{}, right{};
detail::pprf_impl::expand_children<PRG>(cur, left, right);
cur = detail::pprf_impl::path_bit(x, level) ? right : left;
}
return cur;
}
/// @brief Puncture `master` at `alpha`. Optionally program `F(alpha)`.
/// @param program_alpha when true (default), store `F(α)` beside the siblings
template <typename InputT, typename PRG = dpf::prg::aes128>
HEDLEY_WARN_UNUSED_RESULT
HEDLEY_NO_THROW
pprf_punctured<InputT, PRG> puncture(const pprf_master<InputT, PRG> & master,
InputT alpha, bool program_alpha = true) noexcept
{
using block = typename PRG::block_type;
constexpr std::size_t n = pprf_master<InputT, PRG>::bitlength;
pprf_punctured<InputT, PRG> out{};
out.alpha = alpha;
block cur = master.root;
for (std::size_t level = 0; level < n; ++level)
{
block left{}, right{};
detail::pprf_impl::expand_children<PRG>(cur, left, right);
const bool bit = detail::pprf_impl::path_bit(alpha, level);
out.path_bits[level] = static_cast<std::uint8_t>(bit);
out.siblings[level] = bit ? left : right; // keep the off-path child
cur = bit ? right : left;
}
if (program_alpha)
out.programmed = cur;
return out;
}
/// @brief Evaluate a punctured key. At `alpha` returns the programmed leaf
/// when present; otherwise throws `std::invalid_argument`.
template <typename InputT, typename PRG = dpf::prg::aes128>
HEDLEY_WARN_UNUSED_RESULT
typename PRG::block_type pprf_eval(const pprf_punctured<InputT, PRG> & key,
InputT x)
{
using block = typename PRG::block_type;
constexpr std::size_t n = pprf_punctured<InputT, PRG>::bitlength;
if (x == key.alpha)
{
if (!key.programmed.has_value())
throw std::invalid_argument(
"pprf_eval: punctured point has no programmed value");
return *key.programmed;
}
// First level where x diverges from alpha; expand from that sibling.
for (std::size_t level = 0; level < n; ++level)
{
const bool xbit = detail::pprf_impl::path_bit(x, level);
const bool abit = static_cast<bool>(key.path_bits[level]);
if (xbit == abit)
continue;
block cur = key.siblings[level];
for (std::size_t j = level + 1; j < n; ++j)
{
block left{}, right{};
detail::pprf_impl::expand_children<PRG>(cur, left, right);
cur = detail::pprf_impl::path_bit(x, j) ? right : left;
}
return cur;
}
throw std::logic_error("pprf_eval: path didn't diverge from alpha");
}
/// @brief Puncture `master` at every point in `[first, last)`.
/// @details Duplicate points are collapsed. Empty `H` publishes the root.
/// Full-domain `H` publishes nothing. Shared path prefixes are
/// stored once. Visits O(|H| · n) nodes and emits at most |H| · n.
/// @param program_hidden defaults to `false` so an audit opening does not
/// carry the live seeds; set `true` to store `F(h)` for each `h ∈ H`
/// @return sorted unique `hidden`, the published copath, and optional leaves
template <typename InputT, typename PRG = dpf::prg::aes128, typename ForwardIt>
HEDLEY_WARN_UNUSED_RESULT
pprf_copath<InputT, PRG> puncture(const pprf_master<InputT, PRG> & master,
ForwardIt first, ForwardIt last, bool program_hidden = false)
{
pprf_copath<InputT, PRG> out{};
out.hidden.assign(first, last);
std::sort(out.hidden.begin(), out.hidden.end());
out.hidden.erase(std::unique(out.hidden.begin(), out.hidden.end()),
out.hidden.end());
if (out.hidden.empty())
{
out.nodes.push_back(
pprf_copath_node<InputT, PRG>{0, InputT{}, master.root});
return out;
}
if (program_hidden)
out.programmed.reserve(out.hidden.size());
detail::pprf_impl::collect_copath<InputT, PRG>(master.root, 0, InputT{},
out.hidden, 0, out.hidden.size(), program_hidden, out.nodes,
out.programmed);
return out;
}
/// @brief Evaluate a set-punctured key at `x`.
/// @details Expands from the deepest copath node whose prefix matches `x`,
/// using the same child split as the one-point eval. A hidden `x`
/// returns the programmed leaf when one was stored, and throws
/// `std::invalid_argument` otherwise. Throws the same when no
/// published node covers `x` (full-domain puncture).
template <typename InputT, typename PRG = dpf::prg::aes128>
HEDLEY_WARN_UNUSED_RESULT
typename PRG::block_type pprf_eval(const pprf_copath<InputT, PRG> & key,
InputT x)
{
using block = typename PRG::block_type;
constexpr std::size_t n = pprf_copath<InputT, PRG>::bitlength;
const auto hit = std::lower_bound(key.hidden.begin(), key.hidden.end(), x);
if (hit != key.hidden.end() && *hit == x)
{
if (key.programmed.empty())
throw std::invalid_argument(
"pprf_eval: hidden point has no programmed value");
const auto idx = static_cast<std::size_t>(
std::distance(key.hidden.begin(), hit));
return key.programmed[idx];
}
const pprf_copath_node<InputT, PRG> * best = nullptr;
for (const auto & node : key.nodes)
{
if (node.level == 0
|| detail::pprf_impl::prefix_matches(x, node.level, node.prefix))
{
if (best == nullptr || node.level > best->level)
best = &node;
}
}
if (best == nullptr)
throw std::invalid_argument("pprf_eval: no copath node covers point");
block cur = best->seed;
for (std::size_t level = best->level; level < n; ++level)
{
block left{}, right{};
detail::pprf_impl::expand_children<PRG>(cur, left, right);
cur = detail::pprf_impl::path_bit(x, level) ? right : left;
}
return cur;
}
} // namespace dpf
#endif // LIBDPF_INCLUDE_DPF_PPRF_HPP__