/// @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 #include #include #include #include #include #include #include #include #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 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; block_type root{}; }; /// @brief Punctured key: sibling seed per level, optional programmed leaf. template 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; input_type alpha{}; /// @brief `siblings[i]` is the seed for the sibling of the path bit at /// level `i` (MSB = 0). std::array siblings{}; /// @brief Path bit taken toward `alpha` at each level (1 = right). std::array path_bits{}; /// @brief When set, `pprf_eval` at `alpha` returns this leaf. std::optional 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 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 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; std::vector hidden{}; std::vector> nodes{}; std::vector programmed{}; }; /// @brief Sample a fresh master seed. template HEDLEY_WARN_UNUSED_RESULT HEDLEY_NO_THROW pprf_master make_pprf_master() noexcept { pprf_master m; m.root = dpf::uniform_sample(); return m; } namespace detail { namespace pprf_impl { /// @brief Path bit of `x` at `level` (0 = MSB). template 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; constexpr auto to_int = utils::to_integral_type{}; using integral_t = typename utils::to_integral_type::integral_type; const integral_t xi = static_cast(to_int(x)); const std::size_t shift = n - 1 - level; return static_cast(utils::shift_right(xi, shift) & integral_t{1}); } /// @brief Split `seed` into left (pos 0) and right (pos 1) children. template 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 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; constexpr auto to_int = utils::to_integral_type{}; using integral_t = typename utils::to_integral_type::integral_type; const integral_t xi = static_cast(to_int(x)); return static_cast(utils::shift_right(xi, n - level)); } /// @brief Whether `x` lies under the node `(level, prefix)`. template 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 void collect_copath(typename PRG::block_type seed, std::size_t level, InputT prefix, const std::vector & hidden, std::size_t lo, std::size_t hi, bool program_hidden, std::vector> & nodes, std::vector & programmed) { constexpr std::size_t n = utils::bitlength_of_v; 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(seed, left, right); const InputT left_prefix = static_cast((static_cast(prefix) << 1) | InputT{0}); const InputT right_prefix = static_cast((static_cast(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(left, level + 1, left_prefix, hidden, lo, mid, program_hidden, nodes, programmed); collect_copath(right, level + 1, right_prefix, hidden, mid, hi, program_hidden, nodes, programmed); } else if (left_on_path) { nodes.push_back( pprf_copath_node{level + 1, right_prefix, right}); collect_copath(left, level + 1, left_prefix, hidden, lo, mid, program_hidden, nodes, programmed); } else { nodes.push_back( pprf_copath_node{level + 1, left_prefix, left}); collect_copath(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 HEDLEY_WARN_UNUSED_RESULT HEDLEY_NO_THROW typename PRG::block_type pprf_eval(const pprf_master & master, InputT x) noexcept { using block = typename PRG::block_type; constexpr std::size_t n = pprf_master::bitlength; block cur = master.root; for (std::size_t level = 0; level < n; ++level) { block left{}, right{}; detail::pprf_impl::expand_children(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 HEDLEY_WARN_UNUSED_RESULT HEDLEY_NO_THROW pprf_punctured puncture(const pprf_master & master, InputT alpha, bool program_alpha = true) noexcept { using block = typename PRG::block_type; constexpr std::size_t n = pprf_master::bitlength; pprf_punctured 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(cur, left, right); const bool bit = detail::pprf_impl::path_bit(alpha, level); out.path_bits[level] = static_cast(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 HEDLEY_WARN_UNUSED_RESULT typename PRG::block_type pprf_eval(const pprf_punctured & key, InputT x) { using block = typename PRG::block_type; constexpr std::size_t n = pprf_punctured::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(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(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 HEDLEY_WARN_UNUSED_RESULT pprf_copath puncture(const pprf_master & master, ForwardIt first, ForwardIt last, bool program_hidden = false) { pprf_copath 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{0, InputT{}, master.root}); return out; } if (program_hidden) out.programmed.reserve(out.hidden.size()); detail::pprf_impl::collect_copath(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 HEDLEY_WARN_UNUSED_RESULT typename PRG::block_type pprf_eval(const pprf_copath & key, InputT x) { using block = typename PRG::block_type; constexpr std::size_t n = pprf_copath::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::distance(key.hidden.begin(), hit)); return key.programmed[idx]; } const pprf_copath_node * 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(cur, left, right); cur = detail::pprf_impl::path_bit(x, level) ? right : left; } return cur; } } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_PPRF_HPP__