hs_pow: client side effort adjustment

The goal of this patch is to add an additional mechanism for adjusting
PoW effort upwards, where clients rather than services can choose to
solve their puzzles at a higher effort than what was suggested in the
descriptor.

I wanted to use hs_cache's existing unreachability stats to drive this
effort bump, but this revealed some cases where a circuit (intro or
rend) closed early on can end up in hs_cache with an all zero intro
point key, where nobody will find it. This moves intro_auth_pk
initialization earlier in a couple places and adds nonfatal asserts to
catch the problem if it shows up elsewhere.

The actual effort adjustment method I chose is to multiply the suggested
effort by (1 + unresponsive_count), then ensure the result is at least
1. If a service has suggested effort of 0 but we fail to connect,
retries will all use an effort of 1. If the suggestion was 50, we'll try
50, 100, 150, 200, etc. This is bounded both by our client effort limit
and by the limit on unresponsive_count (currently 5).

Signed-off-by: Micah Elizabeth Scott <beth@torproject.org>
This commit is contained in:
Micah Elizabeth Scott 2023-03-31 13:36:58 -07:00
parent ac466a2219
commit 903c6cf1ab
9 changed files with 142 additions and 83 deletions

View File

@ -564,14 +564,6 @@ circuit_expire_building(void)
continue;
}
/* Ignore circuits that are waiting for an introduction to a service with
* PoW enabled, it can take an arbitrary amount of time. They will get
* cleaned up if the SOCKS connection is closed. */
if (TO_ORIGIN_CIRCUIT(victim)->hs_with_pow_circ &&
victim->purpose == CIRCUIT_PURPOSE_C_REND_READY_INTRO_ACKED) {
continue;
}
build_state = TO_ORIGIN_CIRCUIT(victim)->build_state;
if (build_state && build_state->onehop_tunnel)
cutoff = begindir_cutoff;
@ -2560,6 +2552,11 @@ circuit_get_open_circ_or_launch(entry_connection_t *conn,
circ->hs_ident =
hs_ident_circuit_new(&edge_conn->hs_ident->identity_pk);
}
if (desired_circuit_purpose == CIRCUIT_PURPOSE_C_INTRODUCE_ACK_WAIT) {
if (hs_client_setup_intro_circ_auth_key(circ) < 0) {
return 0;
}
}
if (circ->base_.purpose == CIRCUIT_PURPOSE_C_ESTABLISH_REND &&
circ->base_.state == CIRCUIT_STATE_OPEN)
circuit_has_opened(circ);
@ -3012,6 +3009,16 @@ connection_ap_handshake_attach_circuit(entry_connection_t *conn)
conn, CIRCUIT_PURPOSE_C_INTRODUCE_ACK_WAIT, &introcirc);
if (retval < 0) return -1; /* failed */
if (rendcirc && introcirc) {
/* Let's fill out the hs_ident fully as soon as possible, so that
* unreachability counts can be updated properly even if circuits close
* early. */
tor_assert_nonfatal(!ed25519_public_key_is_zero(
&introcirc->hs_ident->intro_auth_pk));
ed25519_pubkey_copy(&rendcirc->hs_ident->intro_auth_pk,
&introcirc->hs_ident->intro_auth_pk);
}
if (retval > 0) {
/* one has already sent the intro. keep waiting. */
tor_assert(introcirc);

View File

@ -581,6 +581,8 @@ cache_client_intro_state_lookup(const ed25519_public_key_t *service_pk,
tor_assert(service_pk);
tor_assert(auth_key);
tor_assert_nonfatal(!ed25519_public_key_is_zero(service_pk));
tor_assert_nonfatal(!ed25519_public_key_is_zero(auth_key));
/* Lookup the intro state cache for this service key. */
cache = digest256map_get(hs_cache_client_intro_state, service_pk->pubkey);

View File

@ -251,6 +251,7 @@ create_intro_circuit_identifier(const hs_service_t *service,
ident = hs_ident_circuit_new(&service->keys.identity_pk);
ed25519_pubkey_copy(&ident->intro_auth_pk, &ip->auth_key_kp.pubkey);
tor_assert_nonfatal(!ed25519_public_key_is_zero(&ident->intro_auth_pk));
return ident;
}

View File

@ -549,6 +549,7 @@ find_desc_intro_point_by_ident(const hs_ident_circuit_t *ident,
tor_assert(ident);
tor_assert(desc);
tor_assert_nonfatal(!ed25519_public_key_is_zero(&ident->intro_auth_pk));
SMARTLIST_FOREACH_BEGIN(desc->encrypted_data.intro_points,
const hs_desc_intro_point_t *, ip) {
@ -634,15 +635,8 @@ send_introduce1(origin_circuit_t *intro_circ,
return -1; /* transient failure */
}
/* Cell has been sent successfully. Copy the introduction point
* authentication and encryption key in the rendezvous circuit identifier so
* we can compute the ntor keys when we receive the RENDEZVOUS2 cell. */
memcpy(&rend_circ->hs_ident->intro_enc_pk, &ip->enc_key,
sizeof(rend_circ->hs_ident->intro_enc_pk));
ed25519_pubkey_copy(&rend_circ->hs_ident->intro_auth_pk,
&intro_circ->hs_ident->intro_auth_pk);
/* Now, we wait for an ACK or NAK on this circuit. */
/* Cell has been sent successfully.
* Now, we wait for an ACK or NAK on this circuit. */
circuit_change_purpose(TO_CIRCUIT(intro_circ),
CIRCUIT_PURPOSE_C_INTRODUCE_ACK_WAIT);
/* Set timestamp_dirty, because circuit_expire_building expects it to
@ -657,6 +651,17 @@ send_introduce1(origin_circuit_t *intro_circ,
* tackle. If asked for higher, we solve it at this cap. */
#define CLIENT_MAX_POW_EFFORT 10000
/** Set a client-side minimum effort. If the client is choosing to increase
* effort on retry, it will always pick a value >= this lower limit. */
#define CLIENT_MIN_RETRY_POW_EFFORT 8
/** Client effort will double on every retry until this level is hit */
#define CLIENT_POW_EFFORT_DOUBLE_UNTIL 1000
/** After we reach DOUBLE_UNTIL, client effort is multiplied by this amount
* on every retry until we reach MAX_POW_EFFORT. */
#define CLIENT_POW_RETRY_MULTIPLIER (1.5f)
/** Send an INTRODUCE1 cell along the intro circuit and populate the rend
* circuit identifier with the needed key material for the e2e encryption.
* Return 0 on success, -1 if there is a transient error such that an action
@ -731,38 +736,77 @@ consider_sending_introduce1(origin_circuit_t *intro_circ,
goto perm_err;
}
/* If the descriptor contains PoW parameters then the service is
* expecting a PoW solution in the INTRODUCE cell, which we solve here. */
if (have_module_pow() &&
desc->encrypted_data.pow_params &&
desc->encrypted_data.pow_params->suggested_effort > 0) {
log_debug(LD_REND, "PoW params present in descriptor.");
/* Copy the introduction point authentication and encryption key
* in the rendezvous circuit identifier so we can compute the ntor keys
* when we receive the RENDEZVOUS2 cell. */
memcpy(&rend_circ->hs_ident->intro_enc_pk, &ip->enc_key,
sizeof(rend_circ->hs_ident->intro_enc_pk));
/* make sure we can't be tricked into hopeless quests */
if (desc->encrypted_data.pow_params->suggested_effort >
CLIENT_MAX_POW_EFFORT) {
/* Optionally choose to solve a client puzzle for this connection. This
* is only available if we have PoW support at compile time, and if the
* service has provided a PoW seed in its descriptor. The puzzle is enabled
* any time effort is nonzero, which can be recommended by the service or
* self-imposed as a result of previous timeouts.
*/
if (have_module_pow() && desc->encrypted_data.pow_params) {
hs_pow_solver_inputs_t pow_inputs = {
.effort = desc->encrypted_data.pow_params->suggested_effort
};
memcpy(pow_inputs.seed, desc->encrypted_data.pow_params->seed,
sizeof pow_inputs.seed);
log_debug(LD_REND, "PoW params present in descriptor, suggested_effort=%u",
pow_inputs.effort);
if (pow_inputs.effort > CLIENT_MAX_POW_EFFORT) {
log_notice(LD_REND, "Onion service suggested effort %d which is "
"higher than we want to solve. Solving at %d instead.",
desc->encrypted_data.pow_params->suggested_effort,
CLIENT_MAX_POW_EFFORT);
/* clobber it in-place. hopefully this won't have bad side effects. */
desc->encrypted_data.pow_params->suggested_effort =
CLIENT_MAX_POW_EFFORT;
pow_inputs.effort, CLIENT_MAX_POW_EFFORT);
pow_inputs.effort = CLIENT_MAX_POW_EFFORT;
}
const hs_cache_intro_state_t *state =
hs_cache_client_intro_state_find(&intro_circ->hs_ident->identity_pk,
&intro_circ->hs_ident->intro_auth_pk);
uint32_t unreachable_count = state ? state->unreachable_count : 0;
if (state) {
log_debug(LD_REND, "hs_cache state during PoW consideration, "
"error=%d timed_out=%d unreachable_count=%u",
state->error, state->timed_out, state->unreachable_count);
}
uint64_t new_effort = pow_inputs.effort;
for (unsigned n_retry = 0; n_retry < unreachable_count; n_retry++) {
if (new_effort >= CLIENT_MAX_POW_EFFORT) {
break;
}
if (new_effort < CLIENT_POW_EFFORT_DOUBLE_UNTIL) {
new_effort <<= 1;
} else {
new_effort = (uint64_t) (CLIENT_POW_RETRY_MULTIPLIER * new_effort);
}
new_effort = MAX((uint64_t)CLIENT_MIN_RETRY_POW_EFFORT, new_effort);
new_effort = MIN((uint64_t)CLIENT_MAX_POW_EFFORT, new_effort);
}
if (pow_inputs.effort != (uint32_t)new_effort) {
log_notice(LD_REND, "Increasing PoW effort from %d to %d after intro "
"point unreachable_count=%d",
pow_inputs.effort, (int)new_effort, unreachable_count);
pow_inputs.effort = (uint32_t)new_effort;
}
if (pow_inputs.effort > 0) {
/* send it to the client-side pow cpuworker for solving. */
intro_circ->hs_currently_solving_pow = 1;
if (0 != hs_pow_queue_work(intro_circ->global_identifier,
if (hs_pow_queue_work(intro_circ->global_identifier,
rend_circ->global_identifier,
desc->encrypted_data.pow_params)) {
log_debug(LD_REND, "Failed to enqueue PoW request");
&pow_inputs) != 0) {
log_warn(LD_REND, "Failed to enqueue PoW request");
}
/* can't proceed with the intro1 cell yet, so yield back to the
* main loop */
goto tran_err;
}
}
/* move on to the next phase: actually try to send it */
if (send_introduce1(intro_circ, rend_circ, desc, NULL, ip) < 0)
@ -796,8 +840,8 @@ consider_sending_introduce1(origin_circuit_t *intro_circ,
*
* Return 0 if everything went well, otherwise return -1 in the case of errors.
*/
static int
setup_intro_circ_auth_key(origin_circuit_t *circ)
int
hs_client_setup_intro_circ_auth_key(origin_circuit_t *circ)
{
const hs_descriptor_t *desc;
const hs_desc_intro_point_t *ip;
@ -843,13 +887,6 @@ client_intro_circ_has_opened(origin_circuit_t *circ)
log_info(LD_REND, "Introduction circuit %u has opened. Attaching streams.",
(unsigned int) TO_CIRCUIT(circ)->n_circ_id);
/* This is an introduction circuit so we'll attach the correct
* authentication key to the circuit identifier so it can be identified
* properly later on. */
if (setup_intro_circ_auth_key(circ) < 0) {
return;
}
connection_ap_attach_pending(1);
}
@ -2047,6 +2084,7 @@ hs_client_circuit_cleanup_on_free(const circuit_t *circ)
orig_circ = CONST_TO_ORIGIN_CIRCUIT(circ);
tor_assert(orig_circ->hs_ident);
const ed25519_public_key_t *intro_pk = &orig_circ->hs_ident->intro_auth_pk;
has_timed_out =
(circ->marked_for_close_orig_reason == END_CIRC_REASON_TIMEOUT);
@ -2061,22 +2099,22 @@ hs_client_circuit_cleanup_on_free(const circuit_t *circ)
safe_str_client(ed25519_fmt(&orig_circ->hs_ident->identity_pk)),
safe_str_client(build_state_get_exit_nickname(orig_circ->build_state)),
failure);
tor_assert_nonfatal(!ed25519_public_key_is_zero(intro_pk));
hs_cache_client_intro_state_note(&orig_circ->hs_ident->identity_pk,
&orig_circ->hs_ident->intro_auth_pk,
failure);
intro_pk, failure);
break;
case CIRCUIT_PURPOSE_C_INTRODUCING:
if (has_timed_out || !orig_circ->build_state) {
break;
}
tor_assert_nonfatal(!ed25519_public_key_is_zero(intro_pk));
failure = INTRO_POINT_FAILURE_UNREACHABLE;
log_info(LD_REND, "Failed v3 intro circ for service %s to intro point %s "
"(while building circuit). Marking as unreachable.",
safe_str_client(ed25519_fmt(&orig_circ->hs_ident->identity_pk)),
safe_str_client(build_state_get_exit_nickname(orig_circ->build_state)));
hs_cache_client_intro_state_note(&orig_circ->hs_ident->identity_pk,
&orig_circ->hs_ident->intro_auth_pk,
failure);
intro_pk, failure);
break;
default:
break;

View File

@ -119,6 +119,8 @@ int hs_client_any_intro_points_usable(const ed25519_public_key_t *service_pk,
int hs_client_refetch_hsdesc(const ed25519_public_key_t *identity_pk);
void hs_client_dir_info_changed(void);
int hs_client_setup_intro_circ_auth_key(origin_circuit_t *circ);
int hs_client_send_introduce1(origin_circuit_t *intro_circ,
origin_circuit_t *rend_circ);

View File

@ -177,7 +177,7 @@ unpack_equix_solution(const uint8_t *bytes_in,
* store the solution in pow_solution_out. Returns 0 on success and -1
* otherwise. Called by a client. */
int
hs_pow_solve(const hs_pow_desc_params_t *pow_params,
hs_pow_solve(const hs_pow_solver_inputs_t *pow_inputs,
hs_pow_solution_t *pow_solution_out)
{
int ret = -1;
@ -185,17 +185,15 @@ hs_pow_solve(const hs_pow_desc_params_t *pow_params,
uint8_t *challenge = NULL;
equix_ctx *ctx = NULL;
tor_assert(pow_params);
tor_assert(pow_inputs);
tor_assert(pow_solution_out);
/* Select E (just using suggested for now) */
uint32_t effort = pow_params->suggested_effort;
const uint32_t effort = pow_inputs->effort;
/* Generate a random nonce N. */
crypto_rand((char *)nonce, sizeof nonce);
/* Build EquiX challenge (C || N || INT_32(E)). */
challenge = build_equix_challenge(pow_params->seed, nonce, effort);
challenge = build_equix_challenge(pow_inputs->seed, nonce, effort);
ctx = build_equix_ctx(EQUIX_CTX_SOLVE);
if (!ctx) {
@ -218,7 +216,7 @@ hs_pow_solve(const hs_pow_desc_params_t *pow_params,
/* Store the effort E. */
pow_solution_out->effort = effort;
/* We only store the first 4 bytes of the seed C. */
memcpy(pow_solution_out->seed_head, pow_params->seed,
memcpy(pow_solution_out->seed_head, pow_inputs->seed,
sizeof(pow_solution_out->seed_head));
/* Store the solution S */
memcpy(&pow_solution_out->equix_solution, sol_bytes, sizeof sol_bytes);
@ -353,8 +351,8 @@ hs_pow_free_service_state(hs_pow_service_state_t *state)
*/
typedef struct pow_worker_job_t {
/** Input: The pow challenge we need to solve. */
hs_pow_desc_params_t *pow_params;
/** Inputs for the PoW solver (seed, chosen effort) */
hs_pow_solver_inputs_t pow_inputs;
/** State: we'll look these up to figure out how to proceed after. */
uint32_t intro_circ_identifier;
@ -377,15 +375,15 @@ pow_worker_threadfn(void *state_, void *work_)
pow_worker_job_t *job = work_;
job->pow_solution_out = tor_malloc_zero(sizeof(hs_pow_solution_t));
if (hs_pow_solve(job->pow_params, job->pow_solution_out)) {
log_info(LD_REND, "Haven't solved the PoW yet. Returning.");
if (hs_pow_solve(&job->pow_inputs, job->pow_solution_out)) {
log_warn(LD_REND, "Failed to run the proof of work solver");
tor_free(job->pow_solution_out);
job->pow_solution_out = NULL; /* how we signal that we came up empty */
return WQ_RPL_REPLY;
}
/* we have a winner! */
log_info(LD_REND, "cpuworker pow: we have a winner!");
log_info(LD_REND, "cpuworker has a proof of work solution");
return WQ_RPL_REPLY;
}
@ -397,7 +395,6 @@ pow_worker_job_free(pow_worker_job_t *job)
{
if (!job)
return;
tor_free(job->pow_params);
tor_free(job->pow_solution_out);
tor_free(job);
}
@ -470,14 +467,14 @@ pow_worker_replyfn(void *work_)
int
hs_pow_queue_work(uint32_t intro_circ_identifier,
uint32_t rend_circ_identifier,
const hs_pow_desc_params_t *pow_params)
const hs_pow_solver_inputs_t *pow_inputs)
{
tor_assert(in_main_thread());
pow_worker_job_t *job = tor_malloc_zero(sizeof(*job));
job->intro_circ_identifier = intro_circ_identifier;
job->rend_circ_identifier = rend_circ_identifier;
job->pow_params = tor_memdup(pow_params, sizeof(hs_pow_desc_params_t));
memcpy(&job->pow_inputs, pow_inputs, sizeof job->pow_inputs);
workqueue_entry_t *work;
work = cpuworker_queue_work(WQ_PRI_LOW,

View File

@ -56,6 +56,17 @@ typedef struct hs_pow_desc_params_t {
time_t expiration_time;
} hs_pow_desc_params_t;
/** The inputs to the PoW solver, derived from the descriptor data and the
* client's per-connection effort choices. */
typedef struct hs_pow_solver_inputs_t {
/** Seed value from a current descriptor */
uint8_t seed[HS_POW_SEED_LEN];
/** Effort chosen by the client. May be higher or ower than
* suggested_effort in the descriptor. */
uint32_t effort;
} hs_pow_solver_inputs_t;
/** State and parameters of PoW defenses, stored in the service state. */
typedef struct hs_pow_service_state_t {
/* If PoW defenses are enabled this is a priority queue containing acceptable
@ -124,7 +135,7 @@ typedef struct hs_pow_solution_t {
#define have_module_pow() (1)
/* API */
int hs_pow_solve(const hs_pow_desc_params_t *pow_params,
int hs_pow_solve(const hs_pow_solver_inputs_t *pow_inputs,
hs_pow_solution_t *pow_solution_out);
int hs_pow_verify(const hs_pow_service_state_t *pow_state,
@ -135,16 +146,16 @@ void hs_pow_free_service_state(hs_pow_service_state_t *state);
int hs_pow_queue_work(uint32_t intro_circ_identifier,
uint32_t rend_circ_identifier,
const hs_pow_desc_params_t *pow_params);
const hs_pow_solver_inputs_t *pow_inputs);
#else /* !defined(HAVE_MODULE_POW) */
#define have_module_pow() (0)
static inline int
hs_pow_solve(const hs_pow_desc_params_t *pow_params,
hs_pow_solve(const hs_pow_solver_inputs_t *pow_inputs,
hs_pow_solution_t *pow_solution_out)
{
(void)pow_params;
(void)pow_inputs;
(void)pow_solution_out;
return -1;
}
@ -173,11 +184,11 @@ hs_pow_free_service_state(hs_pow_service_state_t *state)
static inline int
hs_pow_queue_work(uint32_t intro_circ_identifier,
uint32_t rend_circ_identifier,
const hs_pow_desc_params_t *pow_params)
const hs_pow_solver_inputs_t *pow_inputs)
{
(void)intro_circ_identifier;
(void)rend_circ_identifier;
(void)pow_params;
(void)pow_inputs;
return -1;
}

View File

@ -177,6 +177,7 @@ helper_get_circ_and_stream_for_test(origin_circuit_t **circ_out,
/* prop224: Setup hs ident on the circuit */
or_circ->hs_ident = hs_ident_circuit_new(&service_pk);
or_circ->hs_ident->intro_auth_pk.pubkey[0] = 42;
TO_CIRCUIT(or_circ)->state = CIRCUIT_STATE_OPEN;
@ -1186,6 +1187,7 @@ test_socks_hs_errors(void *arg)
circ->purpose = CIRCUIT_PURPOSE_C_REND_READY;
ocirc = TO_ORIGIN_CIRCUIT(circ);
ocirc->hs_ident = hs_ident_circuit_new(&service_kp.pubkey);
ocirc->hs_ident->intro_auth_pk.pubkey[0] = 42;
ocirc->build_state = tor_malloc_zero(sizeof(cpath_build_state_t));
/* Code path will log this exit so build it. */
ocirc->build_state->chosen_exit = extend_info_new("TestNickname", digest,

View File

@ -187,17 +187,16 @@ test_hs_pow_vectors(void *arg)
uint8_t rng_bytes[HS_POW_NONCE_LEN];
hs_pow_solution_t output;
hs_pow_solution_t solution = { 0 };
hs_pow_desc_params_t params = {
.type = HS_POW_DESC_V1,
.suggested_effort = vectors[vec_i].effort,
hs_pow_solver_inputs_t input = {
.effort = vectors[vec_i].effort,
};
tt_int_op(strlen(seed_hex), OP_EQ, 2 * sizeof params.seed);
tt_int_op(strlen(seed_hex), OP_EQ, 2 * sizeof input.seed);
tt_int_op(strlen(solve_rng_hex), OP_EQ, 2 * sizeof rng_bytes);
tt_int_op(strlen(nonce_hex), OP_EQ, 2 * sizeof solution.nonce);
tt_int_op(strlen(sol_hex), OP_EQ, 2 * sizeof solution.equix_solution);
tt_int_op(base16_decode((char*)params.seed, HS_POW_SEED_LEN,
tt_int_op(base16_decode((char*)input.seed, HS_POW_SEED_LEN,
seed_hex, 2 * HS_POW_SEED_LEN),
OP_EQ, HS_POW_SEED_LEN);
tt_int_op(base16_decode((char*)rng_bytes, sizeof rng_bytes,
@ -210,11 +209,11 @@ test_hs_pow_vectors(void *arg)
sizeof solution.equix_solution,
sol_hex, 2 * sizeof solution.equix_solution),
OP_EQ, HS_POW_EQX_SOL_LEN);
memcpy(solution.seed_head, params.seed, HS_POW_SEED_HEAD_LEN);
memcpy(solution.seed_head, input.seed, HS_POW_SEED_HEAD_LEN);
memset(&output, 0xaa, sizeof output);
testing_enable_prefilled_rng(rng_bytes, HS_POW_NONCE_LEN);
tt_int_op(0, OP_EQ, hs_pow_solve(&params, &output));
tt_int_op(0, OP_EQ, hs_pow_solve(&input, &output));
testing_disable_prefilled_rng();
tt_mem_op(solution.seed_head, OP_EQ, output.seed_head,
@ -224,7 +223,7 @@ test_hs_pow_vectors(void *arg)
tt_mem_op(&solution.equix_solution, OP_EQ, &output.equix_solution,
sizeof output.equix_solution);
tt_int_op(testing_one_hs_pow_solution(&output, params.seed), OP_EQ, 0);
tt_int_op(testing_one_hs_pow_solution(&output, input.seed), OP_EQ, 0);
}
done: