Start implementing Proposal 327

This adds a new tor-hspow crate with the first layers of support in
place for onion service client puzzles as described in Proposal 327.

The API here is experimental, and it's currently only implementing
the self-contained parts of the client puzzle. So, it can verify and
solve puzzles, but it has no event loop integration or nonce replay
tracking or prioritization code yet. These things seem like they would
eventually live in the same crate.

Signed-off-by: Micah Elizabeth Scott <beth@torproject.org>
This commit is contained in:
Micah Elizabeth Scott 2023-06-28 15:15:17 -07:00
parent 16c9bdf178
commit e7aa1d6b62
13 changed files with 1070 additions and 0 deletions

14
Cargo.lock generated
View File

@ -4896,6 +4896,20 @@ dependencies = [
"tor-units",
]
[[package]]
name = "tor-hspow"
version = "0.1.0"
dependencies = [
"arrayvec",
"blake2",
"derive_more",
"equix",
"hex-literal",
"rand 0.8.5",
"thiserror",
"tor-hscrypto",
]
[[package]]
name = "tor-hsservice"
version = "0.2.2"

View File

@ -26,6 +26,7 @@ members = [
"crates/tor-protover",
"crates/tor-bytes",
"crates/tor-hscrypto",
"crates/tor-hspow",
"crates/tor-socksproto",
"crates/tor-checkable",
"crates/tor-cert",

View File

@ -0,0 +1,30 @@
[package]
name = "tor-hspow"
version = "0.1.0"
authors = ["The Tor Project, Inc.", "Micah Elizabeth Scott <beth@torproject.org>"]
edition = "2021"
rust-version = "1.65"
license = "MIT OR Apache-2.0"
homepage = "https://gitlab.torproject.org/tpo/core/arti/-/wikis/home"
description = "Solve and verify proof-of-work client puzzles used by onion services"
keywords = ["tor", "arti", "cryptography"]
categories = ["cryptography"]
repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
publish = false
[features]
default = []
full = ["tor-hscrypto/full"]
[dependencies]
arrayvec = "0.7.4"
blake2 = "0.10.6"
derive_more = "0.99.3"
equix = { path = "../equix", version = "0.1.0" }
rand = "0.8.5"
thiserror = "1"
tor-hscrypto = { version = "0.3.0", path = "../tor-hscrypto" }
[dev-dependencies]
hex-literal = "0.4.1"

View File

@ -0,0 +1,24 @@
# tor-hspow
Tor supports optional proof-of-work client puzzles, for mitigating denial of
service attacks on onion services. This crate implements the specific puzzle
algorithms we use, and infrastructure for solving puzzles asynchronously.
[Proposal 327] introduced our first algorithm variant, named `v1`.
It is based on the Equi-X asymmetric puzzle and an adjustable effort check
using a Blake2b hash of the proof. The underlying algorithm is provided by
the [`equix`] crate.
[Proposal 327]: https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/proposals/327-pow-over-intro.txt
## EXPERIMENTAL DRAFT
Just here as a proof-of-concept and to test the algorithms.
None of the API here is final, the current status is that lower layers are
exposed unnecessarily and upper layers are unwritten.
For Tor client puzzle support in Arti. ([#889])
[#889]: https://gitlab.torproject.org/tpo/core/arti/-/issues/889

View File

@ -0,0 +1,51 @@
//! Define error types for the `tor-hspow` crate
/// Error type for the onion service proof of work subsystem
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// Solution was incorrect
///
/// In general the detailed reason for failure should be ignored,
/// and it certainly should not be shared with clients. It's useful
/// for unit testing and possibly debugging. A particular type of flaw
/// in a solution could be exposed at a variety of layers in the
/// verification process depending on luck and algorithm parameters.
#[error("Incorrect solution to a client puzzle")]
BadSolution(#[source] SolutionError),
/// Runtime error while solving a proof of work puzzle
///
/// Something went wrong in the environment to prevent the
/// solver from completing.
#[error("Runtime error while solving a client puzzle: {0}")]
SolveRuntime(#[source] RuntimeError),
/// Runtime error while verifying a proof of work puzzle
///
/// Something went wrong in the environment to prevent the
/// verifier from coming to any conclusion.
#[error("Runtime error while verifying a client puzzle: {0}")]
VerifyRuntime(#[source] RuntimeError),
}
/// Detailed errors for ways a solution can fail verification
///
/// These errors must not be exposed to clients, who might
/// use them to gain an advantage in computing solutions.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SolutionError {
/// Solution errors from the v1 protocol implementation
#[error("V1, {0}")]
V1(#[from] crate::v1::SolutionError),
}
/// Detailed runtime errors
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RuntimeError {
/// Runtime errors from the v1 protocol implementation
#[error("V1, {0}")]
V1(#[from] crate::v1::RuntimeError),
}

View File

@ -0,0 +1,47 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
#![doc = include_str!("../README.md")]
// @@ begin lint list maintained by maint/add_warning @@
#![cfg_attr(not(ci_arti_stable), allow(renamed_and_removed_lints))]
#![cfg_attr(not(ci_arti_nightly), allow(unknown_lints))]
#![deny(missing_docs)]
#![warn(noop_method_call)]
#![deny(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![deny(clippy::missing_panics_doc)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
pub mod v1;
mod err;
pub use err::{Error, RuntimeError, SolutionError};

View File

@ -0,0 +1,15 @@
//! Version 1 client puzzle from [Proposal 327], using [`equix`]
//!
//! [Proposal 327]: https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/proposals/327-pow-over-intro.txt
mod challenge;
mod err;
mod solve;
mod types;
mod verify;
pub use equix::{RuntimeOption, SolutionByteArray};
pub use err::{RuntimeError, SolutionError};
pub use solve::{Solver, SolverInput};
pub use types::*;
pub use verify::Verifier;

View File

@ -0,0 +1,125 @@
//! Implement the `v1` protocol's challenge string format
//!
//! This is a packed byte-string which encodes our puzzle's parameters
//! as inputs for Equi-X. We need to construct challenge strings both to
//! solve and to verify puzzles.
use crate::v1::{Effort, Instance, Nonce, Seed, SolutionError, NONCE_LEN, SEED_LEN};
use arrayvec::ArrayVec;
use blake2::{digest::consts::U4, Blake2b, Digest};
/// Algorithm personalization string (P)
///
/// This becomes part of the challenge string, binding a puzzle solution to
/// this particular algorithm even if other similar protocols exist using
/// the same building blocks.
const P_STRING: &[u8] = b"Tor hs intro v1\0";
/// Length of the personalization string, in bytes
const P_STRING_LEN: usize = 16;
/// Length of the HsBlindId
const ID_LEN: usize = 32;
/// Location of the [`Seed`] within a [`Challenge`]
const SEED_OFFSET: usize = P_STRING_LEN + ID_LEN;
/// Location of the [`Nonce`] within a [`Challenge`]
const NONCE_OFFSET: usize = SEED_OFFSET + SEED_LEN;
/// Location of the [`Effort`] within a [`Challenge`]
const EFFORT_OFFSET: usize = NONCE_OFFSET + NONCE_LEN;
/// Packed length of an [`Effort`], in bytes
const EFFORT_LEN: usize = 4;
/// Total length of our Equi-X challenge string
const CHALLENGE_LEN: usize = EFFORT_OFFSET + EFFORT_LEN;
/// A fully assembled challenge string, with some access to inner fields
///
/// This is the combined input to Equi-X. Defined by Proposal 327
/// as `(P || ID || C || N || INT_32(E))`
#[derive(derive_more::AsRef, Debug, Clone, Eq, PartialEq)]
pub(super) struct Challenge([u8; CHALLENGE_LEN]);
impl Challenge {
/// Build a new [`Challenge`]
///
/// Copies [`Instance`], [`Effort`], and [`Nonce`] values into
/// a new byte array.
pub(super) fn new(instance: &Instance, effort: Effort, nonce: &Nonce) -> Self {
let mut result = ArrayVec::<u8, CHALLENGE_LEN>::new();
result.extend(P_STRING.iter().copied());
result.extend(instance.service().as_ref().iter().copied());
assert_eq!(result.len(), SEED_OFFSET);
result.extend(instance.seed().as_ref().iter().copied());
assert_eq!(result.len(), NONCE_OFFSET);
result.extend(nonce.as_ref().iter().copied());
assert_eq!(result.len(), EFFORT_OFFSET);
result.extend(effort.as_ref().to_be_bytes().into_iter());
Self(result.into_inner().expect("matching CHALLENGE_LEN"))
}
/// Clone the [`Seed`] portion of this challenge
pub(super) fn seed(&self) -> Seed {
let array: [u8; SEED_LEN] = self.0[SEED_OFFSET..(SEED_OFFSET + SEED_LEN)]
.try_into()
.expect("slice length correct");
array.into()
}
/// Clone the [`Nonce`] portion of this challenge
pub(super) fn nonce(&self) -> Nonce {
let array: [u8; NONCE_LEN] = self.0[NONCE_OFFSET..(NONCE_OFFSET + NONCE_LEN)]
.try_into()
.expect("slice length correct");
array.into()
}
/// Return the [`Effort`] used in this challenge
pub(super) fn effort(&self) -> Effort {
u32::from_be_bytes(
self.0[EFFORT_OFFSET..(EFFORT_OFFSET + EFFORT_LEN)]
.try_into()
.expect("slice length correct"),
)
.into()
}
/// Increment the [`Nonce`] value inside this challenge
pub(super) fn increment_nonce(&mut self) {
/// Wrapping increment for a serialized little endian value of arbitrary width.
fn inc_le_bytes(slice: &mut [u8]) {
for byte in slice {
let (value, overflow) = (*byte).overflowing_add(1);
*byte = value;
if !overflow {
break;
}
}
}
inc_le_bytes(&mut self.0[NONCE_OFFSET..(NONCE_OFFSET + NONCE_LEN)]);
}
/// Verify that a solution proof passes the effort test
///
/// This computes a Blake2b hash of the challenge and the serialized
/// Equi-X solution, and tests the result against the effort encoded
/// in the challenge string.
///
/// Used by both the [`crate::v1::Solver`] and the [`crate::v1::Verifier`].
pub(super) fn check_effort(
&self,
proof: &equix::SolutionByteArray,
) -> Result<(), SolutionError> {
let mut hasher = Blake2b::<U4>::new();
hasher.update(self.as_ref());
hasher.update(proof.as_ref());
let value = u32::from_be_bytes(hasher.finalize().into());
match value.checked_mul(*self.effort().as_ref()) {
Some(_) => Ok(()),
None => Err(SolutionError::Effort),
}
}
}

View File

@ -0,0 +1,32 @@
//! Error types local to the `v1` protocol implementation
/// Protocol-specific ways a solution can fail verification
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SolutionError {
/// Mismatch between [`super::SeedHead`] and [`super::Instance`]
#[error("Solution has an unrecognized Seed value")]
Seed,
/// The effort constraint `H(challenge | proof) * effort` failed
#[error("Failed to verify solution effort")]
Effort,
/// The Equi-X proof is not well-formed, it failed at least one order test
#[error("Failed to verify order of Equi-X proof")]
Order,
/// The Equi-X proof does not apply to this particular challenge
#[error("Failed to verify hash sums for Equi-X proof")]
HashSum,
/// Couldn't construct the HashX function, this tells us a working
/// solver should have rejected this [`super::Nonce`] value.
#[error("Solution requires a challenge string that fails HashX constraints")]
ChallengeConstraints,
}
/// Protocol-specific runtime errors
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RuntimeError {
/// Unexpected error or runtime compiler error from the Equi-X layer
#[error("Equi-X error, {0}")]
EquiX(#[from] equix::Error),
}

View File

@ -0,0 +1,127 @@
//! Solver implementation for v1 client puzzles
use crate::v1::challenge::Challenge;
use crate::v1::{Effort, Instance, Nonce, RuntimeError, RuntimeOption, Solution, NONCE_LEN};
use equix::{EquiXBuilder, HashError, SolverMemory};
use rand::{CryptoRng, Rng, RngCore};
/// All inputs necessary to run the [`Solver`]
#[derive(Debug, Clone)]
pub struct SolverInput {
/// The puzzle instance we're solving
instance: Instance,
/// Effort chosen by the client for this solver run
effort: Effort,
/// Configuration settings for Equi-X, as an [`EquiXBuilder`] instance
equix: EquiXBuilder,
}
impl SolverInput {
/// Construct a [`SolverInput`] by wrapping an [`Instance`].
///
/// This is a lower-level constructor.
/// Prefer [`Instance::with_effort()`]
pub fn new(instance: Instance, effort: Effort) -> Self {
SolverInput {
instance,
effort,
equix: Default::default(),
}
}
/// Select the HashX runtime to use for this Solver input.
/// By default, uses [`RuntimeOption::TryCompile`]
pub fn runtime(&mut self, option: RuntimeOption) -> &mut Self {
self.equix.runtime(option);
self
}
/// Begin solving with this input and a new random Nonce value generated
/// by a supplied [`Rng`]. May be parallelized if desired, by cloning
/// the [`SolverInput`] first.
pub fn solve<R: RngCore + CryptoRng>(self, rng: &mut R) -> Solver {
self.solve_with_nonce(&rng.gen::<[u8; NONCE_LEN]>().into())
}
/// Begin solving with a specified [`Nonce`].
/// This is not generally useful, but it's great for unit tests if you'd
/// like to skip to a deterministic location in the search.
pub fn solve_with_nonce(self, nonce: &Nonce) -> Solver {
Solver {
challenge: Challenge::new(&self.instance, self.effort, nonce),
equix: self.equix,
mem: SolverMemory::new(),
}
}
}
/// Make progress toward finding a [`Solution`]
///
/// Each [`Solver`] instance will own about 1.8 MB of temporary memory until
/// it is dropped. This interface supports cancelling an ongoing solve and it
/// supports multithreaded use, but it requires an external thread pool
/// implementation.
pub struct Solver {
/// The next assembled [`Challenge`] to try
challenge: Challenge,
/// Configuration settings for Equi-X, as an [`EquiXBuilder`] instance
equix: EquiXBuilder,
/// Temporary memory for Equi-X to use
mem: SolverMemory,
}
impl Solver {
/// Run the solver until it produces a [`Solution`]
///
/// This takes a random amount of time to finish, with no possibility
/// to cancel early. If you need cancellation, use [`Self::run_step()`]
/// instead.
pub fn run(&mut self) -> Result<Solution, RuntimeError> {
loop {
if let Some(solution) = self.run_step()? {
return Ok(solution);
}
}
}
/// Run the solver algorithm, returning when we are at a good stopping point.
///
/// Typical durations would be very roughly 10ms with the compiled hash
/// implementation or 250ms with the interpreted implementation.
///
/// These durations are far too long to include in any event loop
/// that's not built specifically for blocking operations, but they're
/// short enough that we still have a chance of cancelling a high-effort
/// solve. Step duration does not depend on effort choice.
///
/// Internally, this checks only one [`Nonce`] value. That's the only good
/// stopping point we have in Equi-X right now. If we really need finer
/// grained cancellation the equix crate could be modified to support
/// this but at a performance penalty.
///
/// It's possible to call this again after a solution has already
/// been returned, but the resulting solutions will have nearby [`Nonce`]
/// values so this is not recommended except for benchmarking.
pub fn run_step(&mut self) -> Result<Option<Solution>, RuntimeError> {
match self.equix.build(self.challenge.as_ref()) {
Ok(equix) => {
for candidate in equix.solve_with_memory(&mut self.mem) {
if self.challenge.check_effort(&candidate.to_bytes()).is_ok() {
return Ok(Some(Solution::new(
self.challenge.nonce(),
self.challenge.effort(),
self.challenge.seed().head(),
candidate,
)));
}
}
}
Err(equix::Error::Hash(HashError::ProgramConstraints)) => (),
Err(e) => {
return Err(e.into());
}
};
self.challenge.increment_nonce();
Ok(None)
}
}

View File

@ -0,0 +1,164 @@
//! Basic types used by the v1 client puzzle
use crate::v1::{SolutionByteArray, SolutionError, SolverInput, Verifier};
use tor_hscrypto::pk::HsBlindId;
/// Effort setting, a u32 value with linear scale
///
/// The numerical value is roughly the expected number of times we will
/// need to invoke the underlying solver (Equi-X) for the v1 proof-of-work
/// protocol to find a solution.
#[derive(
derive_more::AsRef, derive_more::From, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct Effort(u32);
/// Length of the random seed generated by servers and included in HsDir
pub const SEED_LEN: usize = 32;
/// The random portion of a challenge, distributed through HsDir
#[derive(derive_more::AsRef, derive_more::From, Debug, Clone, Eq, PartialEq)]
pub struct Seed([u8; SEED_LEN]);
impl Seed {
/// Make a new [`SeedHead`] from a prefix of this seed
pub fn head(&self) -> SeedHead {
SeedHead(
self.0[..SEED_HEAD_LEN]
.try_into()
.expect("slice length correct"),
)
}
}
/// Length of a seed prefix used to identify the entire seed
pub const SEED_HEAD_LEN: usize = 4;
/// A short seed prefix used in solutions to reference the complete seed
#[derive(derive_more::AsRef, derive_more::From, Debug, Clone, Copy, Eq, PartialEq)]
pub struct SeedHead([u8; SEED_HEAD_LEN]);
/// Length of the nonce value generated by clients and included in the solution
pub const NONCE_LEN: usize = 16;
/// Generated randomly by solvers and included in the solution
#[derive(derive_more::AsRef, derive_more::From, Debug, Clone, Eq, PartialEq)]
pub struct Nonce([u8; NONCE_LEN]);
/// One instance of this proof-of-work puzzle
///
/// Identified uniquely by the combination of onion service blinded Id key
/// plus a rotating seed chosen by the service.
#[derive(Debug, Clone)]
pub struct Instance {
/// Blinded public Id key, binding this puzzle to a specific onion service
service: HsBlindId,
/// Seed value distributed in the HsDir by that service
seed: Seed,
}
impl Instance {
/// A new puzzle instance, wrapping a service Id and service-chosen seed
pub fn new(service: HsBlindId, seed: Seed) -> Self {
Self { service, seed }
}
/// Start preparing a particular [`SolverInput`] for this [`Instance`],
/// by choosing an effort. All other settings are optional, accessed
/// via builder methods on [`SolverInput`].
pub fn with_effort(self, effort: Effort) -> SolverInput {
SolverInput::new(self, effort)
}
/// Use this instance to construct a [`Verifier`], which can verify
/// solutions and collect configuration options that control verification.
pub fn verifier(self) -> Verifier {
Verifier::new(self)
}
/// Get the [`HsBlindId`] identifying the service this puzzle is for
pub fn service(&self) -> &HsBlindId {
&self.service
}
/// Get the rotating random [`Seed`] used in this puzzle instance
pub fn seed(&self) -> &Seed {
&self.seed
}
}
/// One potential solution to some puzzle [`Instance`]
///
/// The existence of a [`Solution`] guarantees that the solution is well formed
/// (for example, the correct length, the correct order in the Equi-X solution)
/// but it makes no guarantee to actually solve any specific puzzle instance.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Solution {
/// Arbitrary value chosen by the solver to reach a valid solution.
/// Services are responsible for remembering used values to prevent replay.
nonce: Nonce,
/// The effort chosen by the client, verifiable due to its inclusion
/// in the Equi-X challenge construction.
effort: Effort,
/// Prefix of the Seed used in this puzzle Instance
seed_head: SeedHead,
/// Equi-X solution which claims to prove the above effort choice
proof: equix::Solution,
}
impl Solution {
/// Construct a new Solution around a well-formed [`equix::Solution`] proof
pub(super) fn new(
nonce: Nonce,
effort: Effort,
seed_head: SeedHead,
proof: equix::Solution,
) -> Self {
Solution {
nonce,
effort,
seed_head,
proof,
}
}
/// Try to build a [`Solution`] from an unvalidated [`SolutionByteArray`]
pub fn try_from_bytes(
nonce: Nonce,
effort: Effort,
seed_head: SeedHead,
bytes: &SolutionByteArray,
) -> Result<Self, SolutionError> {
Ok(Self::new(
nonce,
effort,
seed_head,
equix::Solution::try_from_bytes(bytes).map_err(|_| SolutionError::Order)?,
))
}
/// Get the winning [`Nonce`] value used in this solution
pub fn nonce(&self) -> &Nonce {
&self.nonce
}
/// Get the client-chosen and provable [`Effort`] value used in this solution
pub fn effort(&self) -> Effort {
self.effort
}
/// Get the [`SeedHead`] value identifying the puzzle this solution is for
pub fn seed_head(&self) -> SeedHead {
self.seed_head
}
/// Internal, access the [`equix::Solution`] backing the proof portion
pub(super) fn proof(&self) -> &equix::Solution {
&self.proof
}
/// Clone the proof portion of the solution in its canonical byte string format
pub fn proof_to_bytes(&self) -> SolutionByteArray {
self.proof.to_bytes()
}
}

View File

@ -0,0 +1,72 @@
//! Verifier implementation for v1 client puzzles
use crate::v1::challenge::Challenge;
use crate::v1::{Instance, RuntimeError, RuntimeOption, Solution, SolutionError};
use crate::Error;
use equix::{EquiXBuilder, HashError};
/// Checker for potential [`Solution`]s to a particular puzzle [`Instance`]
///
/// Holds information about the puzzle instance, and optional configuration
/// settings.
pub struct Verifier {
/// The puzzle instance we're verifying
instance: Instance,
/// Configuration settings for Equi-X, as an [`EquiXBuilder`] instance
equix: EquiXBuilder,
}
impl Verifier {
/// Construct a new [`Verifier`] by wrapping an [`Instance`]
pub fn new(instance: Instance) -> Self {
Self {
instance,
equix: Default::default(),
}
}
/// Select the HashX runtime to use for this verifier.
///
/// By default, uses [`RuntimeOption::TryCompile`]
pub fn runtime(&mut self, option: RuntimeOption) -> &mut Self {
self.equix.runtime(option);
self
}
/// Check whether a solution is valid for this puzzle instance
///
/// May return a [`SolutionError`] or a [`RuntimeError`]
pub fn check(&self, solution: &Solution) -> Result<(), Error> {
match self.check_seed(solution) {
Err(e) => Err(Error::BadSolution(e.into())),
Ok(()) => {
let challenge = Challenge::new(&self.instance, solution.effort(), solution.nonce());
match challenge.check_effort(&solution.proof_to_bytes()) {
Err(e) => Err(Error::BadSolution(e.into())),
Ok(()) => match self.equix.verify(challenge.as_ref(), solution.proof()) {
Ok(()) => Ok(()),
Err(equix::Error::HashSum) => {
Err(Error::BadSolution(SolutionError::HashSum.into()))
}
Err(equix::Error::Hash(HashError::ProgramConstraints)) => Err(
Error::BadSolution(SolutionError::ChallengeConstraints.into()),
),
Err(e) => Err(Error::VerifyRuntime(RuntimeError::EquiX(e).into())),
},
}
}
}
}
/// Check the [`super::SeedHead`] of a solution against an [`Instance`].
///
/// This is a very cheap test, this should come first so a service
/// can verify every [`Solution`] against its last two [`Instance`]s.
fn check_seed(&self, solution: &Solution) -> Result<(), SolutionError> {
if solution.seed_head() == self.instance.seed().head() {
Ok(())
} else {
Err(SolutionError::Seed)
}
}
}

View File

@ -0,0 +1,368 @@
//! Test vectors from the C tor implementation
use hex_literal::hex;
use tor_hscrypto::pk::HsBlindId;
use tor_hspow::v1::{Effort, Instance, Nonce, Seed, Solution, SolutionByteArray, SolutionError};
use tor_hspow::Error;
/// Short test vectors from C tor, covering verification only
#[test]
fn verify_only() {
// All zero, but only claims an effort of 1.
// Expect it will last until hash sum checks before failing.
assert!(matches!(
Instance::new(
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
)
.verifier()
.check(
&Solution::try_from_bytes(
hex!("00000000000000000000000000000000").into(),
1_u32.into(),
hex!("00000000").into(),
&hex!("00000000000000000000000000000000"),
)
.unwrap(),
),
Err(Error::BadSolution(tor_hspow::SolutionError::V1(
SolutionError::HashSum
)))
));
// All zero, but a higher effort claim. Should fail the effort check.
assert!(matches!(
Instance::new(
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
)
.verifier()
.check(
&Solution::try_from_bytes(
hex!("00000000000000000000000000000000").into(),
10_u32.into(),
hex!("00000000").into(),
&hex!("00000000000000000000000000000000"),
)
.unwrap(),
),
Err(Error::BadSolution(tor_hspow::SolutionError::V1(
SolutionError::Effort
)))
));
// Seed head mismatch
assert!(matches!(
Instance::new(
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
)
.verifier()
.check(
&Solution::try_from_bytes(
hex!("00000000000000000000000000000000").into(),
0_u32.into(),
hex!("00000001").into(),
&hex!("00000000000000000000000000000000"),
)
.unwrap(),
),
Err(Error::BadSolution(tor_hspow::SolutionError::V1(
SolutionError::Seed
)))
));
// Valid zero-effort solution
assert!(Instance::new(
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into()
)
.verifier()
.check(
&Solution::try_from_bytes(
hex!("55555555555555555555555555555555").into(),
0_u32.into(),
hex!("aaaaaaaa").into(),
&hex!("4312f87ceab844c78e1c793a913812d7")
)
.unwrap()
)
.is_ok());
// Valid high-effort solution
assert!(Instance::new(
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into()
)
.verifier()
.check(
&Solution::try_from_bytes(
hex!("59217255555555555555555555555555").into(),
1_000_000_u32.into(),
hex!("aaaaaaaa").into(),
&hex!("0f3db97b9cac20c1771680a1a34848d3")
)
.unwrap()
)
.is_ok());
// The claimed effort must exactly match what's was in the challenge
// when the Equi-X proof was created, or it will fail either the
// Effort or HashSum checks.
assert!(matches!(
Instance::new(
hex!("bfd298428562e530c52bdb36d81a0e293ef4a0e94d787f0f8c0c611f4f9e78ed").into(),
hex!("86fb0acf4932cda44dbb451282f415479462dd10cb97ff5e7e8e2a53c3767a7f").into()
)
.verifier()
.check(
&Solution::try_from_bytes(
hex!("2eff9fdbc34326d9d2f18ed277469c63").into(),
99_999_u32.into(),
hex!("86fb0acf").into(),
&hex!("400cb091139f86b352119f6e131802d6")
)
.unwrap()
),
Err(Error::BadSolution(tor_hspow::SolutionError::V1(
SolutionError::Effort
)))
));
// Otherwise good solution but with a corrupted nonce. This may fail
// either the Effort or HashSum checks.
assert!(matches!(
Instance::new(
hex!("bfd298428562e530c52bdb36d81a0e293ef4a0e94d787f0f8c0c611f4f9e78ed").into(),
hex!("86fb0acf4932cda44dbb451282f415479462dd10cb97ff5e7e8e2a53c3767a7f").into()
)
.verifier()
.check(
&Solution::try_from_bytes(
hex!("2eff9fdbc34326d9a2f18ed277469c63").into(),
100_000_u32.into(),
hex!("86fb0acf").into(),
&hex!("400cb091139f86b352119f6e131802d6")
)
.unwrap()
),
Err(Error::BadSolution(tor_hspow::SolutionError::V1(
SolutionError::Effort
)))
));
assert!(Instance::new(
hex!("bfd298428562e530c52bdb36d81a0e293ef4a0e94d787f0f8c0c611f4f9e78ed").into(),
hex!("86fb0acf4932cda44dbb451282f415479462dd10cb97ff5e7e8e2a53c3767a7f").into()
)
.verifier()
.check(
&Solution::try_from_bytes(
hex!("2eff9fdbc34326d9d2f18ed277469c63").into(),
100_000_u32.into(),
hex!("86fb0acf").into(),
&hex!("400cb091139f86b352119f6e131802d6")
)
.unwrap()
)
.is_ok());
}
/// Utility to solve and verify one puzzle
fn solve_and_verify(
effort: Effort,
first_nonce: Nonce,
seed: Seed,
service: HsBlindId,
expected_nonce: Nonce,
expected_proof: SolutionByteArray,
) {
let instance = Instance::new(service, seed);
let solution = instance
.clone()
.with_effort(effort)
.solve_with_nonce(&first_nonce)
.run()
.unwrap();
assert_eq!(solution.seed_head(), instance.seed().head());
assert_eq!(solution.effort(), effort);
assert_eq!(solution.nonce(), &expected_nonce);
assert_eq!(solution.proof_to_bytes(), expected_proof);
assert!(Solution::try_from_bytes(
expected_nonce,
effort,
instance.seed().head(),
&expected_proof
)
.is_ok());
assert!(instance.verifier().check(&solution).is_ok());
}
/// Longer running tests from C tor, covering solve and verify both
///
/// These tests are still optimized to complete without wasting too much time,
/// by artificially choosing a `first_nonce` only slightly lower than the
/// `expected_nonce` we want to find.
#[test]
fn solver() {
solve_and_verify(
0_u32.into(),
hex!("55555555555555555555555555555555").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("55555555555555555555555555555555").into(),
hex!("4312f87ceab844c78e1c793a913812d7"),
);
solve_and_verify(
1_u32.into(),
hex!("55555555555555555555555555555555").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("55555555555555555555555555555555").into(),
hex!("84355542ab2b3f79532ef055144ac5ab"),
);
solve_and_verify(
1_u32.into(),
hex!("55555555555555555555555555555555").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111110").into(),
hex!("55555555555555555555555555555555").into(),
hex!("115e4b70da858792fc205030b8c83af9"),
);
solve_and_verify(
2_u32.into(),
hex!("55555555555555555555555555555555").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("55555555555555555555555555555555").into(),
hex!("4600a93a535ed76dc746c99942ab7de2"),
);
solve_and_verify(
10_u32.into(),
hex!("55555555555555555555555555555555").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("56555555555555555555555555555555").into(),
hex!("128bbda5df2929c3be086de2aad34aed"),
);
solve_and_verify(
10_u32.into(),
hex!("ffffffffffffffffffffffffffffffff").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("01000000000000000000000000000000").into(),
hex!("203af985537fadb23f3ed5873b4c81ce"),
);
solve_and_verify(
1337_u32.into(),
hex!("7fffffffffffffffffffffffffffffff").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("4111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("01000000000000000000000000000000").into(),
hex!("31c377cb72796ed80ae77df6ac1d6bfd"),
);
solve_and_verify(
31337_u32.into(),
hex!("34a20000000000000000000000000000").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("36a20000000000000000000000000000").into(),
hex!("ca6899b91113aaf7536f28db42526bff"),
);
solve_and_verify(
100_u32.into(),
hex!("55555555555555555555555555555555").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("56555555555555555555555555555555").into(),
hex!("3a4122a240bd7abfc922ab3cbb9479ed"),
);
solve_and_verify(
1000_u32.into(),
hex!("d3555555555555555555555555555555").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("d4555555555555555555555555555555").into(),
hex!("338cc08f57697ce8ac2e4b453057d6e9"),
);
solve_and_verify(
10_000_u32.into(),
hex!("c5715555555555555555555555555555").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("c8715555555555555555555555555555").into(),
hex!("9f2d3d4ed831ac96ad34c25fb59ff3e2"),
);
solve_and_verify(
100_000_u32.into(),
hex!("418d5655555555555555555555555555").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("428d5655555555555555555555555555").into(),
hex!("9863f3acd2d15adfd244a7ca61d4c6ff"),
);
solve_and_verify(
1_000_000_u32.into(),
hex!("58217255555555555555555555555555").into(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").into(),
hex!("1111111111111111111111111111111111111111111111111111111111111111").into(),
hex!("59217255555555555555555555555555").into(),
hex!("0f3db97b9cac20c1771680a1a34848d3"),
);
solve_and_verify(
1_u32.into(),
hex!("d0aec1669384bfe5ed39cd724d6c7954").into(),
hex!("c52be1f8a5e6cc3b8fb71cfdbe272cbc91d4d035400f2f94fb0d0074794e0a07").into(),
hex!("bfd298428562e530c52bdb36d81a0e293ef4a0e94d787f0f8c0c611f4f9e78ed").into(),
hex!("d1aec1669384bfe5ed39cd724d6c7954").into(),
hex!("462606e5f8c2f3f844127b8bfdd6b4ff"),
);
solve_and_verify(
1_u32.into(),
hex!("b4d0e611e6935750fcf9406aae131f62").into(),
hex!("86fb0acf4932cda44dbb451282f415479462dd10cb97ff5e7e8e2a53c3767a7f").into(),
hex!("bfd298428562e530c52bdb36d81a0e293ef4a0e94d787f0f8c0c611f4f9e78ed").into(),
hex!("b4d0e611e6935750fcf9406aae131f62").into(),
hex!("9f3fbd50b1a83fb63284bde44318c0fd"),
);
solve_and_verify(
1_u32.into(),
hex!("b4d0e611e6935750fcf9406aae131f62").into(),
hex!("9dfbd06d86fed8e12de3ab214e1a63ea61f46253fe08346a20378da70c4a327d").into(),
hex!("bec632eb76123956f99a06d394fcbee8f135b8ed01f2e90aabe404cb0346744a").into(),
hex!("b4d0e611e6935750fcf9406aae131f62").into(),
hex!("161baa7490356292d020065fdbe55ffc"),
);
solve_and_verify(
1_u32.into(),
hex!("40559fdbc34326d9d2f18ed277469c63").into(),
hex!("86fb0acf4932cda44dbb451282f415479462dd10cb97ff5e7e8e2a53c3767a7f").into(),
hex!("bfd298428562e530c52bdb36d81a0e293ef4a0e94d787f0f8c0c611f4f9e78ed").into(),
hex!("40559fdbc34326d9d2f18ed277469c63").into(),
hex!("fa649c6a2c5c0bb6a3511b9ea4b448d1"),
);
solve_and_verify(
10_000_u32.into(),
hex!("34569fdbc34326d9d2f18ed277469c63").into(),
hex!("86fb0acf4932cda44dbb451282f415479462dd10cb97ff5e7e8e2a53c3767a7f").into(),
hex!("bfd298428562e530c52bdb36d81a0e293ef4a0e94d787f0f8c0c611f4f9e78ed").into(),
hex!("36569fdbc34326d9d2f18ed277469c63").into(),
hex!("2802951e623c74adc443ab93e99633ee"),
);
solve_and_verify(
100_000_u32.into(),
hex!("2cff9fdbc34326d9d2f18ed277469c63").into(),
hex!("86fb0acf4932cda44dbb451282f415479462dd10cb97ff5e7e8e2a53c3767a7f").into(),
hex!("bfd298428562e530c52bdb36d81a0e293ef4a0e94d787f0f8c0c611f4f9e78ed").into(),
hex!("2eff9fdbc34326d9d2f18ed277469c63").into(),
hex!("400cb091139f86b352119f6e131802d6"),
);
solve_and_verify(
1_000_000_u32.into(),
hex!("5243b3dbc34326d9d2f18ed277469c63").into(),
hex!("86fb0acf4932cda44dbb451282f415479462dd10cb97ff5e7e8e2a53c3767a7f").into(),
hex!("bfd298428562e530c52bdb36d81a0e293ef4a0e94d787f0f8c0c611f4f9e78ed").into(),
hex!("5543b3dbc34326d9d2f18ed277469c63").into(),
hex!("b47c718b56315e9697173a6bac1feaa4"),
);
}