diff options
Diffstat (limited to 'src/feature/hs')
-rw-r--r-- | src/feature/hs/.may_include | 1 | ||||
-rw-r--r-- | src/feature/hs/hs_cell.c | 138 | ||||
-rw-r--r-- | src/feature/hs/hs_cell.h | 15 | ||||
-rw-r--r-- | src/feature/hs/hs_circuit.c | 77 | ||||
-rw-r--r-- | src/feature/hs/hs_circuit.h | 11 | ||||
-rw-r--r-- | src/feature/hs/hs_circuitmap.c | 4 | ||||
-rw-r--r-- | src/feature/hs/hs_client.c | 126 | ||||
-rw-r--r-- | src/feature/hs/hs_client.h | 1 | ||||
-rw-r--r-- | src/feature/hs/hs_common.c | 50 | ||||
-rw-r--r-- | src/feature/hs/hs_common.h | 7 | ||||
-rw-r--r-- | src/feature/hs/hs_config.c | 494 | ||||
-rw-r--r-- | src/feature/hs/hs_config.h | 3 | ||||
-rw-r--r-- | src/feature/hs/hs_descriptor.c | 26 | ||||
-rw-r--r-- | src/feature/hs/hs_descriptor.h | 7 | ||||
-rw-r--r-- | src/feature/hs/hs_ob.c | 408 | ||||
-rw-r--r-- | src/feature/hs/hs_ob.h | 40 | ||||
-rw-r--r-- | src/feature/hs/hs_options.inc | 36 | ||||
-rw-r--r-- | src/feature/hs/hs_opts_st.h | 30 | ||||
-rw-r--r-- | src/feature/hs/hs_service.c | 187 | ||||
-rw-r--r-- | src/feature/hs/hs_service.h | 22 | ||||
-rw-r--r-- | src/feature/hs/include.am | 4 |
21 files changed, 1322 insertions, 365 deletions
diff --git a/src/feature/hs/.may_include b/src/feature/hs/.may_include index 424c745c12..11c5ffbb14 100644 --- a/src/feature/hs/.may_include +++ b/src/feature/hs/.may_include @@ -1 +1,2 @@ *.h +*.inc diff --git a/src/feature/hs/hs_cell.c b/src/feature/hs/hs_cell.c index 52bd663200..fc9f4a2654 100644 --- a/src/feature/hs/hs_cell.c +++ b/src/feature/hs/hs_cell.c @@ -13,6 +13,7 @@ #include "feature/hs_common/replaycache.h" #include "feature/hs/hs_cell.h" +#include "feature/hs/hs_ob.h" #include "core/crypto/hs_ntor.h" #include "core/or/origin_circuit_st.h" @@ -67,14 +68,17 @@ compute_introduce_mac(const uint8_t *encoded_cell, size_t encoded_cell_len, memwipe(mac_msg, 0, sizeof(mac_msg)); } -/** From a set of keys, subcredential and the ENCRYPTED section of an - * INTRODUCE2 cell, return a newly allocated intro cell keys structure. - * Finally, the client public key is copied in client_pk. On error, return - * NULL. */ +/** + * From a set of keys, a list of subcredentials, and the ENCRYPTED section of + * an INTRODUCE2 cell, return an array of newly allocated intro cell keys + * structures. Finally, the client public key is copied in client_pk. On + * error, return NULL. + **/ static hs_ntor_intro_cell_keys_t * get_introduce2_key_material(const ed25519_public_key_t *auth_key, const curve25519_keypair_t *enc_key, - const uint8_t *subcredential, + size_t n_subcredentials, + const hs_subcredential_t *subcredentials, const uint8_t *encrypted_section, curve25519_public_key_t *client_pk) { @@ -82,17 +86,19 @@ get_introduce2_key_material(const ed25519_public_key_t *auth_key, tor_assert(auth_key); tor_assert(enc_key); - tor_assert(subcredential); + tor_assert(n_subcredentials > 0); + tor_assert(subcredentials); tor_assert(encrypted_section); tor_assert(client_pk); - keys = tor_malloc_zero(sizeof(*keys)); + keys = tor_calloc(n_subcredentials, sizeof(hs_ntor_intro_cell_keys_t)); /* First bytes of the ENCRYPTED section are the client public key. */ memcpy(client_pk->public_key, encrypted_section, CURVE25519_PUBKEY_LEN); - if (hs_ntor_service_get_introduce1_keys(auth_key, enc_key, client_pk, - subcredential, keys) < 0) { + if (hs_ntor_service_get_introduce1_keys_multi(auth_key, enc_key, client_pk, + n_subcredentials, + subcredentials, keys) < 0) { /* Don't rely on the caller to wipe this on error. */ memwipe(client_pk, 0, sizeof(curve25519_public_key_t)); tor_free(keys); @@ -747,6 +753,74 @@ hs_cell_parse_intro_established(const uint8_t *payload, size_t payload_len) return ret; } +/** For the encrypted INTRO2 cell in <b>encrypted_section</b>, use the crypto + * material in <b>data</b> to compute the right ntor keys. Also validate the + * INTRO2 MAC to ensure that the keys are the right ones. + * + * Return NULL on failure to either produce the key material or on MAC + * validation. Else return a newly allocated intro keys object. */ +static hs_ntor_intro_cell_keys_t * +get_introduce2_keys_and_verify_mac(hs_cell_introduce2_data_t *data, + const uint8_t *encrypted_section, + size_t encrypted_section_len) +{ + hs_ntor_intro_cell_keys_t *intro_keys = NULL; + hs_ntor_intro_cell_keys_t *intro_keys_result = NULL; + + /* Build the key material out of the key material found in the cell. */ + intro_keys = get_introduce2_key_material(data->auth_pk, data->enc_kp, + data->n_subcredentials, + data->subcredentials, + encrypted_section, + &data->client_pk); + if (intro_keys == NULL) { + log_info(LD_REND, "Invalid INTRODUCE2 encrypted data. Unable to " + "compute key material"); + return NULL; + } + + /* Make sure we are not about to underflow. */ + if (BUG(encrypted_section_len < DIGEST256_LEN)) { + return NULL; + } + + /* Validate MAC from the cell and our computed key material. The MAC field + * in the cell is at the end of the encrypted section. */ + intro_keys_result = tor_malloc_zero(sizeof(*intro_keys_result)); + for (unsigned i = 0; i < data->n_subcredentials; ++i) { + uint8_t mac[DIGEST256_LEN]; + + /* The MAC field is at the very end of the ENCRYPTED section. */ + size_t mac_offset = encrypted_section_len - sizeof(mac); + /* Compute the MAC. Use the entire encoded payload with a length up to the + * ENCRYPTED section. */ + compute_introduce_mac(data->payload, + data->payload_len - encrypted_section_len, + encrypted_section, encrypted_section_len, + intro_keys[i].mac_key, + sizeof(intro_keys[i].mac_key), + mac, sizeof(mac)); + /* Time-invariant conditional copy: if the MAC is what we expected, then + * set intro_keys_result to intro_keys[i]. Otherwise, don't: but don't + * leak which one it was! */ + bool equal = tor_memeq(mac, encrypted_section + mac_offset, sizeof(mac)); + memcpy_if_true_timei(equal, intro_keys_result, &intro_keys[i], + sizeof(*intro_keys_result)); + } + + /* We no longer need intro_keys. */ + memwipe(intro_keys, 0, + sizeof(hs_ntor_intro_cell_keys_t) * data->n_subcredentials); + tor_free(intro_keys); + + if (safe_mem_is_zero(intro_keys_result, sizeof(*intro_keys_result))) { + log_info(LD_REND, "Invalid MAC validation for INTRODUCE2 cell"); + tor_free(intro_keys_result); /* sets intro_keys_result to NULL */ + } + + return intro_keys_result; +} + /** Parse the INTRODUCE2 cell using data which contains everything we need to * do so and contains the destination buffers of information we extract and * compute from the cell. Return 0 on success else a negative value. The @@ -795,47 +869,29 @@ hs_cell_parse_introduce2(hs_cell_introduce2_data_t *data, /* Check our replay cache for this introduction point. */ if (replaycache_add_test_and_elapsed(data->replay_cache, encrypted_section, encrypted_section_len, &elapsed)) { - log_warn(LD_REND, "Possible replay detected! An INTRODUCE2 cell with the" + log_warn(LD_REND, "Possible replay detected! An INTRODUCE2 cell with the " "same ENCRYPTED section was seen %ld seconds ago. " "Dropping cell.", (long int) elapsed); goto done; } - /* Build the key material out of the key material found in the cell. */ - intro_keys = get_introduce2_key_material(data->auth_pk, data->enc_kp, - data->subcredential, - encrypted_section, - &data->client_pk); - if (intro_keys == NULL) { - log_info(LD_REND, "Invalid INTRODUCE2 encrypted data. Unable to " - "compute key material on circuit %u for service %s", - TO_CIRCUIT(circ)->n_circ_id, + /* First bytes of the ENCRYPTED section are the client public key (they are + * guaranteed to exist because of the length check above). We are gonna use + * the client public key to compute the ntor keys and decrypt the payload: + */ + memcpy(&data->client_pk.public_key, encrypted_section, + CURVE25519_PUBKEY_LEN); + + /* Get the right INTRODUCE2 ntor keys and verify the cell MAC */ + intro_keys = get_introduce2_keys_and_verify_mac(data, encrypted_section, + encrypted_section_len); + if (!intro_keys) { + log_warn(LD_REND, "Could not get valid INTRO2 keys on circuit %u " + "for service %s", TO_CIRCUIT(circ)->n_circ_id, safe_str_client(service->onion_address)); goto done; } - /* Validate MAC from the cell and our computed key material. The MAC field - * in the cell is at the end of the encrypted section. */ - { - uint8_t mac[DIGEST256_LEN]; - /* The MAC field is at the very end of the ENCRYPTED section. */ - size_t mac_offset = encrypted_section_len - sizeof(mac); - /* Compute the MAC. Use the entire encoded payload with a length up to the - * ENCRYPTED section. */ - compute_introduce_mac(data->payload, - data->payload_len - encrypted_section_len, - encrypted_section, encrypted_section_len, - intro_keys->mac_key, sizeof(intro_keys->mac_key), - mac, sizeof(mac)); - if (tor_memcmp(mac, encrypted_section + mac_offset, sizeof(mac))) { - log_info(LD_REND, "Invalid MAC validation for INTRODUCE2 cell on " - "circuit %u for service %s", - TO_CIRCUIT(circ)->n_circ_id, - safe_str_client(service->onion_address)); - goto done; - } - } - { /* The ENCRYPTED_DATA section starts just after the CLIENT_PK. */ const uint8_t *encrypted_data = diff --git a/src/feature/hs/hs_cell.h b/src/feature/hs/hs_cell.h index 80f37057d2..2b28c44c50 100644 --- a/src/feature/hs/hs_cell.h +++ b/src/feature/hs/hs_cell.h @@ -16,6 +16,8 @@ * 3.2.2 of the specification). Below this value, the cell must be padded. */ #define HS_CELL_INTRODUCE1_MIN_SIZE 246 +struct hs_subcredential_t; + /** This data structure contains data that we need to build an INTRODUCE1 cell * used by the INTRODUCE1 build function. */ typedef struct hs_cell_introduce1_data_t { @@ -29,7 +31,7 @@ typedef struct hs_cell_introduce1_data_t { /** Introduction point encryption public key. */ const curve25519_public_key_t *enc_pk; /** Subcredentials of the service. */ - const uint8_t *subcredential; + const struct hs_subcredential_t *subcredential; /** Onion public key for the ntor handshake. */ const curve25519_public_key_t *onion_pk; /** Rendezvous cookie. */ @@ -55,9 +57,14 @@ typedef struct hs_cell_introduce2_data_t { owned by the introduction point object through which we received the INTRO2 cell*/ const curve25519_keypair_t *enc_kp; - /** Subcredentials of the service. Pointer owned by the descriptor that owns - the introduction point through which we received the INTRO2 cell. */ - const uint8_t *subcredential; + /** + * Length of the subcredentials array below. + **/ + size_t n_subcredentials; + /** Array of <b>n_subcredentials</b> subcredentials for the service. Pointer + * owned by the descriptor that owns the introduction point through which we + * received the INTRO2 cell. */ + const struct hs_subcredential_t *subcredentials; /** Payload of the received encoded cell. */ const uint8_t *payload; /** Size of the payload of the received encoded cell. */ diff --git a/src/feature/hs/hs_circuit.c b/src/feature/hs/hs_circuit.c index 90805a98b7..447f664f81 100644 --- a/src/feature/hs/hs_circuit.c +++ b/src/feature/hs/hs_circuit.c @@ -19,6 +19,7 @@ #include "feature/client/circpathbias.h" #include "feature/hs/hs_cell.h" #include "feature/hs/hs_circuit.h" +#include "feature/hs/hs_ob.h" #include "feature/hs/hs_circuitmap.h" #include "feature/hs/hs_client.h" #include "feature/hs/hs_ident.h" @@ -367,10 +368,10 @@ get_service_anonymity_string(const hs_service_t *service) * success, a circuit identifier is attached to the circuit with the needed * data. This function will try to open a circuit for a maximum value of * MAX_REND_FAILURES then it will give up. */ -static void -launch_rendezvous_point_circuit(const hs_service_t *service, - const hs_service_intro_point_t *ip, - const hs_cell_introduce2_data_t *data) +MOCK_IMPL(STATIC void, +launch_rendezvous_point_circuit,(const hs_service_t *service, + const hs_service_intro_point_t *ip, + const hs_cell_introduce2_data_t *data)) { int circ_needs_uptime; time_t now = time(NULL); @@ -578,7 +579,7 @@ retry_service_rendezvous_point(const origin_circuit_t *circ) static int setup_introduce1_data(const hs_desc_intro_point_t *ip, const node_t *rp_node, - const uint8_t *subcredential, + const hs_subcredential_t *subcredential, hs_cell_introduce1_data_t *intro1_data) { int ret = -1; @@ -621,6 +622,20 @@ setup_introduce1_data(const hs_desc_intro_point_t *ip, } /** Helper: cleanup function for client circuit. This is for every HS version. + * It is called from hs_circ_cleanup_on_close() entry point. */ +static void +cleanup_on_close_client_circ(circuit_t *circ) +{ + tor_assert(circ); + + if (circuit_is_hs_v3(circ)) { + hs_client_circuit_cleanup_on_close(circ); + } + /* It is possible the circuit has an HS purpose but no identifier (rend_data + * or hs_ident). Thus possible that this passes through. */ +} + +/** Helper: cleanup function for client circuit. This is for every HS version. * It is called from hs_circ_cleanup_on_free() entry point. */ static void cleanup_on_free_client_circ(circuit_t *circ) @@ -633,7 +648,7 @@ cleanup_on_free_client_circ(circuit_t *circ) hs_client_circuit_cleanup_on_free(circ); } /* It is possible the circuit has an HS purpose but no identifier (rend_data - * or hs_ident). Thus possible that this passess through. */ + * or hs_ident). Thus possible that this passes through. */ } /* ========== */ @@ -958,6 +973,42 @@ hs_circ_handle_intro_established(const hs_service_t *service, return ret; } +/** + * Go into <b>data</b> and add the right subcredential to be able to handle + * this incoming cell. + * + * <b>desc_subcred</b> is the subcredential of the descriptor that corresponds + * to the intro point that received this intro request. This subcredential + * should be used if we are not an onionbalance instance. + * + * Return 0 if everything went well, or -1 in case of internal error. + */ +static int +get_subcredential_for_handling_intro2_cell(const hs_service_t *service, + hs_cell_introduce2_data_t *data, + const hs_subcredential_t *desc_subcred) +{ + /* Handle the simple case first: We are not an onionbalance instance and we + * should just use the regular descriptor subcredential */ + if (!hs_ob_service_is_instance(service)) { + data->n_subcredentials = 1; + data->subcredentials = desc_subcred; + return 0; + } + + /* This should not happen since we should have made onionbalance + * subcredentials when we created our descriptors. */ + if (BUG(!service->state.ob_subcreds)) { + return -1; + } + + /* We are an onionbalance instance: */ + data->n_subcredentials = service->state.n_ob_subcreds; + data->subcredentials = service->state.ob_subcreds; + + return 0; +} + /** We just received an INTRODUCE2 cell on the established introduction circuit * circ. Handle the INTRODUCE2 payload of size payload_len for the given * circuit and service. This cell is associated with the intro point object ip @@ -966,7 +1017,7 @@ int hs_circ_handle_introduce2(const hs_service_t *service, const origin_circuit_t *circ, hs_service_intro_point_t *ip, - const uint8_t *subcredential, + const hs_subcredential_t *subcredential, const uint8_t *payload, size_t payload_len) { int ret = -1; @@ -983,12 +1034,16 @@ hs_circ_handle_introduce2(const hs_service_t *service, * parsed, decrypted and key material computed correctly. */ data.auth_pk = &ip->auth_key_kp.pubkey; data.enc_kp = &ip->enc_key_kp; - data.subcredential = subcredential; data.payload = payload; data.payload_len = payload_len; data.link_specifiers = smartlist_new(); data.replay_cache = ip->replay_cache; + if (get_subcredential_for_handling_intro2_cell(service, + &data, subcredential)) { + goto done; + } + if (hs_cell_parse_introduce2(&data, circ, service) < 0) { goto done; } @@ -1092,7 +1147,7 @@ int hs_circ_send_introduce1(origin_circuit_t *intro_circ, origin_circuit_t *rend_circ, const hs_desc_intro_point_t *ip, - const uint8_t *subcredential) + const hs_subcredential_t *subcredential) { int ret = -1; ssize_t payload_len; @@ -1252,6 +1307,10 @@ hs_circ_cleanup_on_close(circuit_t *circ) { tor_assert(circ); + if (circuit_purpose_is_hs_client(circ->purpose)) { + cleanup_on_close_client_circ(circ); + } + /* On close, we simply remove it from the circuit map. It can not be used * anymore. We keep this code path fast and lean. */ diff --git a/src/feature/hs/hs_circuit.h b/src/feature/hs/hs_circuit.h index 92231369c6..22e936e685 100644 --- a/src/feature/hs/hs_circuit.h +++ b/src/feature/hs/hs_circuit.h @@ -46,15 +46,16 @@ int hs_circ_handle_intro_established(const hs_service_t *service, origin_circuit_t *circ, const uint8_t *payload, size_t payload_len); +struct hs_subcredential_t; int hs_circ_handle_introduce2(const hs_service_t *service, const origin_circuit_t *circ, hs_service_intro_point_t *ip, - const uint8_t *subcredential, + const struct hs_subcredential_t *subcredential, const uint8_t *payload, size_t payload_len); int hs_circ_send_introduce1(origin_circuit_t *intro_circ, origin_circuit_t *rend_circ, const hs_desc_intro_point_t *ip, - const uint8_t *subcredential); + const struct hs_subcredential_t *subcredential); int hs_circ_send_establish_rendezvous(origin_circuit_t *circ); /* e2e circuit API. */ @@ -78,6 +79,12 @@ create_rp_circuit_identifier(const hs_service_t *service, const curve25519_public_key_t *server_pk, const struct hs_ntor_rend_cell_keys_t *keys); +struct hs_cell_introduce2_data_t; +MOCK_DECL(STATIC void, +launch_rendezvous_point_circuit,(const hs_service_t *service, + const hs_service_intro_point_t *ip, + const struct hs_cell_introduce2_data_t *data)); + #endif /* defined(HS_CIRCUIT_PRIVATE) */ #endif /* !defined(TOR_HS_CIRCUIT_H) */ diff --git a/src/feature/hs/hs_circuitmap.c b/src/feature/hs/hs_circuitmap.c index 2343d729dd..466a02de39 100644 --- a/src/feature/hs/hs_circuitmap.c +++ b/src/feature/hs/hs_circuitmap.c @@ -76,11 +76,11 @@ hs_circuit_hash_token(const circuit_t *circuit) HT_PROTOTYPE(hs_circuitmap_ht, // The name of the hashtable struct circuit_t, // The name of the element struct, hs_circuitmap_node, // The name of HT_ENTRY member - hs_circuit_hash_token, hs_circuits_have_same_token) + hs_circuit_hash_token, hs_circuits_have_same_token); HT_GENERATE2(hs_circuitmap_ht, circuit_t, hs_circuitmap_node, hs_circuit_hash_token, hs_circuits_have_same_token, - 0.6, tor_reallocarray, tor_free_) + 0.6, tor_reallocarray, tor_free_); #ifdef TOR_UNIT_TESTS diff --git a/src/feature/hs/hs_client.c b/src/feature/hs/hs_client.c index 68d6619cea..c3697d0c1d 100644 --- a/src/feature/hs/hs_client.c +++ b/src/feature/hs/hs_client.c @@ -646,7 +646,7 @@ send_introduce1(origin_circuit_t *intro_circ, /* Send the INTRODUCE1 cell. */ if (hs_circ_send_introduce1(intro_circ, rend_circ, ip, - desc->subcredential) < 0) { + &desc->subcredential) < 0) { if (TO_CIRCUIT(intro_circ)->marked_for_close) { /* If the introduction circuit was closed, we were unable to send the * cell for some reasons. In any case, the intro circuit has to be @@ -961,6 +961,87 @@ client_get_random_intro(const ed25519_public_key_t *service_pk) return ei; } +/** Return true iff all intro points for the given service have timed out. */ +static bool +intro_points_all_timed_out(const ed25519_public_key_t *service_pk) +{ + bool ret = false; + + tor_assert(service_pk); + + const hs_descriptor_t *desc = hs_cache_lookup_as_client(service_pk); + if (BUG(!desc)) { + /* We can't introduce without a descriptor so ending up here means somehow + * between the introduction failure and this, the cache entry was removed + * which shouldn't be possible in theory. */ + goto end; + } + + SMARTLIST_FOREACH_BEGIN(desc->encrypted_data.intro_points, + const hs_desc_intro_point_t *, ip) { + const hs_cache_intro_state_t *state = + hs_cache_client_intro_state_find(service_pk, + &ip->auth_key_cert->signed_key); + if (!state || !state->timed_out) { + /* No state or if this intro point has not timed out, we are done since + * clearly not all of them have timed out. */ + goto end; + } + } SMARTLIST_FOREACH_END(ip); + + /* Exiting the loop here means that all intro points we've looked at have + * timed out. Note that we can _not_ have a descriptor without intro points + * in the client cache. */ + ret = true; + + end: + return ret; +} + +/** Called when a rendezvous circuit has timed out. Every stream attached to + * the circuit will get set with the SOCKS5_HS_REND_FAILED (0xF3) extended + * error code so if the connection to the rendezvous point ends up not + * working, this code could be sent back as a reason. */ +static void +socks_mark_rend_circuit_timed_out(const origin_circuit_t *rend_circ) +{ + tor_assert(rend_circ); + + /* For each entry connection attached to this rendezvous circuit, report + * the error. */ + for (edge_connection_t *edge = rend_circ->p_streams; edge; + edge = edge->next_stream) { + entry_connection_t *entry = EDGE_TO_ENTRY_CONN(edge); + if (entry->socks_request) { + entry->socks_request->socks_extended_error_code = + SOCKS5_HS_REND_FAILED; + } + } +} + +/** Called when introduction has failed meaning there is no more usable + * introduction points to be used (either NACKed or failed) for the given + * entry connection. + * + * This function only reports back the SOCKS5_HS_INTRO_FAILED (0xF2) code or + * SOCKS5_HS_INTRO_TIMEDOUT (0xF7) if all intros have timed out. The caller + * has to make sure to close the entry connections. */ +static void +socks_mark_introduction_failed(entry_connection_t *conn, + const ed25519_public_key_t *identity_pk) +{ + socks5_reply_status_t code = SOCKS5_HS_INTRO_FAILED; + + tor_assert(conn); + tor_assert(conn->socks_request); + tor_assert(identity_pk); + + if (intro_points_all_timed_out(identity_pk)) { + code = SOCKS5_HS_INTRO_TIMEDOUT; + } + conn->socks_request->socks_extended_error_code = code; +} + /** For this introduction circuit, we'll look at if we have any usable * introduction point left for this service. If so, we'll use the circuit to * re-extend to a new intro point. Else, we'll close the circuit and its @@ -1313,6 +1394,10 @@ client_desc_has_arrived(const smartlist_t *entry_conns) if (!hs_client_any_intro_points_usable(identity_pk, desc)) { log_info(LD_REND, "Hidden service descriptor is unusable. " "Closing streams."); + /* Report the extended socks error code that we were unable to introduce + * to the service. */ + socks_mark_introduction_failed(entry_conn, identity_pk); + connection_mark_unattached_ap(entry_conn, END_STREAM_REASON_RESOLVEFAILED); /* We are unable to use the descriptor so remove the directory request @@ -1762,6 +1847,37 @@ get_hs_client_auths_map(void) /* ========== */ /** Called when a circuit was just cleaned up. This is done right before the + * circuit is marked for close. */ +void +hs_client_circuit_cleanup_on_close(const circuit_t *circ) +{ + bool has_timed_out; + + tor_assert(circ); + tor_assert(CIRCUIT_IS_ORIGIN(circ)); + + has_timed_out = + (circ->marked_for_close_orig_reason == END_CIRC_REASON_TIMEOUT); + + switch (circ->purpose) { + case CIRCUIT_PURPOSE_C_ESTABLISH_REND: + case CIRCUIT_PURPOSE_C_REND_READY: + case CIRCUIT_PURPOSE_C_REND_READY_INTRO_ACKED: + case CIRCUIT_PURPOSE_C_REND_JOINED: + /* Report extended SOCKS error code when a rendezvous circuit times out. + * This MUST be done on_close() because it is possible the entry + * connection would get closed before the circuit is freed and thus + * would fail to report the error code. */ + if (has_timed_out) { + socks_mark_rend_circuit_timed_out(CONST_TO_ORIGIN_CIRCUIT(circ)); + } + break; + default: + break; + } +} + +/** Called when a circuit was just cleaned up. This is done right before the * circuit is freed. */ void hs_client_circuit_cleanup_on_free(const circuit_t *circ) @@ -1845,7 +1961,7 @@ hs_client_decode_descriptor(const char *desc_str, hs_descriptor_t **desc) { hs_desc_decode_status_t ret; - uint8_t subcredential[DIGEST256_LEN]; + hs_subcredential_t subcredential; ed25519_public_key_t blinded_pubkey; hs_client_service_authorization_t *client_auth = NULL; curve25519_secret_key_t *client_auth_sk = NULL; @@ -1865,13 +1981,13 @@ hs_client_decode_descriptor(const char *desc_str, uint64_t current_time_period = hs_get_time_period_num(0); hs_build_blinded_pubkey(service_identity_pk, NULL, 0, current_time_period, &blinded_pubkey); - hs_get_subcredential(service_identity_pk, &blinded_pubkey, subcredential); + hs_get_subcredential(service_identity_pk, &blinded_pubkey, &subcredential); } /* Parse descriptor */ - ret = hs_desc_decode_descriptor(desc_str, subcredential, + ret = hs_desc_decode_descriptor(desc_str, &subcredential, client_auth_sk, desc); - memwipe(subcredential, 0, sizeof(subcredential)); + memwipe(&subcredential, 0, sizeof(subcredential)); if (ret != HS_DESC_DECODE_OK) { goto err; } diff --git a/src/feature/hs/hs_client.h b/src/feature/hs/hs_client.h index d0a3a7015f..a11caa309f 100644 --- a/src/feature/hs/hs_client.h +++ b/src/feature/hs/hs_client.h @@ -110,6 +110,7 @@ int hs_client_send_introduce1(origin_circuit_t *intro_circ, origin_circuit_t *rend_circ); void hs_client_circuit_has_opened(origin_circuit_t *circ); +void hs_client_circuit_cleanup_on_close(const circuit_t *circ); void hs_client_circuit_cleanup_on_free(const circuit_t *circ); int hs_client_receive_rendezvous_acked(origin_circuit_t *circ, diff --git a/src/feature/hs/hs_common.c b/src/feature/hs/hs_common.c index f8b031cc26..4639cdb68a 100644 --- a/src/feature/hs/hs_common.c +++ b/src/feature/hs/hs_common.c @@ -22,6 +22,7 @@ #include "feature/hs/hs_client.h" #include "feature/hs/hs_common.h" #include "feature/hs/hs_dos.h" +#include "feature/hs/hs_ob.h" #include "feature/hs/hs_ident.h" #include "feature/hs/hs_service.h" #include "feature/hs_common/shared_random_client.h" @@ -808,12 +809,12 @@ hs_parse_address_impl(const char *address, ed25519_public_key_t *key_out, } /** Using the given identity public key and a blinded public key, compute the - * subcredential and put it in subcred_out (must be of size DIGEST256_LEN). + * subcredential and put it in subcred_out. * This can't fail. */ void hs_get_subcredential(const ed25519_public_key_t *identity_pk, const ed25519_public_key_t *blinded_pk, - uint8_t *subcred_out) + hs_subcredential_t *subcred_out) { uint8_t credential[DIGEST256_LEN]; crypto_digest_t *digest; @@ -841,7 +842,8 @@ hs_get_subcredential(const ed25519_public_key_t *identity_pk, sizeof(credential)); crypto_digest_add_bytes(digest, (const char *) blinded_pk->pubkey, ED25519_PUBKEY_LEN); - crypto_digest_get_digest(digest, (char *) subcred_out, DIGEST256_LEN); + crypto_digest_get_digest(digest, (char *) subcred_out->subcred, + SUBCRED_LEN); crypto_digest_free(digest); memwipe(credential, 0, sizeof(credential)); @@ -909,30 +911,35 @@ hs_set_conn_addr_port(const smartlist_t *ports, edge_connection_t *conn) * case the caller would want only one field. checksum_out MUST at least be 2 * bytes long. * - * Return 0 if parsing went well; return -1 in case of error. */ + * Return 0 if parsing went well; return -1 in case of error and if errmsg is + * non NULL, a human readable string message is set. */ int -hs_parse_address(const char *address, ed25519_public_key_t *key_out, - uint8_t *checksum_out, uint8_t *version_out) +hs_parse_address_no_log(const char *address, ed25519_public_key_t *key_out, + uint8_t *checksum_out, uint8_t *version_out, + const char **errmsg) { char decoded[HS_SERVICE_ADDR_LEN]; tor_assert(address); + if (errmsg) { + *errmsg = NULL; + } + /* Obvious length check. */ if (strlen(address) != HS_SERVICE_ADDR_LEN_BASE32) { - log_warn(LD_REND, "Service address %s has an invalid length. " - "Expected %lu but got %lu.", - escaped_safe_str(address), - (unsigned long) HS_SERVICE_ADDR_LEN_BASE32, - (unsigned long) strlen(address)); + if (errmsg) { + *errmsg = "Invalid length"; + } goto invalid; } /* Decode address so we can extract needed fields. */ if (base32_decode(decoded, sizeof(decoded), address, strlen(address)) != sizeof(decoded)) { - log_warn(LD_REND, "Service address %s can't be decoded.", - escaped_safe_str(address)); + if (errmsg) { + *errmsg = "Unable to base32 decode"; + } goto invalid; } @@ -944,6 +951,22 @@ hs_parse_address(const char *address, ed25519_public_key_t *key_out, return -1; } +/** Same has hs_parse_address_no_log() but emits a log warning on parsing + * failure. */ +int +hs_parse_address(const char *address, ed25519_public_key_t *key_out, + uint8_t *checksum_out, uint8_t *version_out) +{ + const char *errmsg = NULL; + int ret = hs_parse_address_no_log(address, key_out, checksum_out, + version_out, &errmsg); + if (ret < 0) { + log_warn(LD_REND, "Service address %s failed to be parsed: %s", + escaped_safe_str(address), errmsg); + } + return ret; +} + /** Validate a given onion address. The length, the base32 decoding, and * checksum are validated. Return 1 if valid else 0. */ int @@ -1807,6 +1830,7 @@ hs_free_all(void) hs_service_free_all(); hs_cache_free_all(); hs_client_free_all(); + hs_ob_free_all(); } /** For the given origin circuit circ, decrement the number of rendezvous diff --git a/src/feature/hs/hs_common.h b/src/feature/hs/hs_common.h index 8f743d4d37..997b7298a6 100644 --- a/src/feature/hs/hs_common.h +++ b/src/feature/hs/hs_common.h @@ -179,6 +179,10 @@ void hs_build_address(const struct ed25519_public_key_t *key, uint8_t version, int hs_address_is_valid(const char *address); int hs_parse_address(const char *address, struct ed25519_public_key_t *key_out, uint8_t *checksum_out, uint8_t *version_out); +int hs_parse_address_no_log(const char *address, + struct ed25519_public_key_t *key_out, + uint8_t *checksum_out, uint8_t *version_out, + const char **errmsg); void hs_build_blinded_pubkey(const struct ed25519_public_key_t *pubkey, const uint8_t *secret, size_t secret_len, @@ -210,9 +214,10 @@ const uint8_t *rend_data_get_pk_digest(const rend_data_t *rend_data, routerstatus_t *pick_hsdir(const char *desc_id, const char *desc_id_base32); +struct hs_subcredential_t; void hs_get_subcredential(const struct ed25519_public_key_t *identity_pk, const struct ed25519_public_key_t *blinded_pk, - uint8_t *subcred_out); + struct hs_subcredential_t *subcred_out); uint64_t hs_get_previous_time_period_num(time_t now); uint64_t hs_get_time_period_num(time_t now); diff --git a/src/feature/hs/hs_config.c b/src/feature/hs/hs_config.c index 64656b1935..0dad8dd6d8 100644 --- a/src/feature/hs/hs_config.c +++ b/src/feature/hs/hs_config.c @@ -26,12 +26,68 @@ #include "feature/hs/hs_common.h" #include "feature/hs/hs_config.h" #include "feature/hs/hs_client.h" +#include "feature/hs/hs_ob.h" #include "feature/hs/hs_service.h" #include "feature/rend/rendclient.h" #include "feature/rend/rendservice.h" #include "lib/encoding/confline.h" +#include "lib/conf/confdecl.h" +#include "lib/confmgt/confmgt.h" + +#include "feature/hs/hs_opts_st.h" #include "app/config/or_options_st.h" +/* Declare the table mapping hs options to hs_opts_t */ +#define CONF_CONTEXT TABLE +#include "feature/hs/hs_options.inc" +#undef CONF_CONTEXT + +/** Magic number for hs_opts_t. */ +#define HS_OPTS_MAGIC 0x6f6e796e + +static const config_format_t hs_opts_fmt = { + .size = sizeof(hs_opts_t), + .magic = { "hs_opts_t", + HS_OPTS_MAGIC, + offsetof(hs_opts_t, magic) }, + .vars = hs_opts_t_vars, +}; + +/** Global configuration manager to handle HS sections*/ +static config_mgr_t *hs_opts_mgr = NULL; + +/** + * Return a configuration manager for the hs_opts_t configuration type. + **/ +static const config_mgr_t * +get_hs_opts_mgr(void) +{ + if (PREDICT_UNLIKELY(hs_opts_mgr == NULL)) { + hs_opts_mgr = config_mgr_new(&hs_opts_fmt); + config_mgr_freeze(hs_opts_mgr); + } + return hs_opts_mgr; +} + +/** + * Allocate, initialize, and return a new hs_opts_t. + **/ +static hs_opts_t * +hs_opts_new(void) +{ + const config_mgr_t *mgr = get_hs_opts_mgr(); + hs_opts_t *r = config_new(mgr); + tor_assert(r); + config_init(mgr, r); + return r; +} + +/** + * Free an hs_opts_t. + **/ +#define hs_opts_free(opts) \ + config_free(get_hs_opts_mgr(), (opts)) + /** Using the given list of services, stage them into our global state. Every * service version are handled. This function can remove entries in the given * service_list. @@ -116,33 +172,27 @@ service_is_duplicate_in_list(const smartlist_t *service_list, return ret; } -/** Helper function: Given an configuration option name, its value, a minimum - * min and a maxium max, parse the value as a uint64_t. On success, ok is set - * to 1 and ret is the parsed value. On error, ok is set to 0 and ret must be - * ignored. This function logs both on error and success. */ -static uint64_t -helper_parse_uint64(const char *opt, const char *value, uint64_t min, - uint64_t max, int *ok) +/** Check whether an integer <b>i</b> is out of bounds (not between <b>low</b> + * and <b>high</b> incusive). If it is, then log a warning about the option + * <b>name</b>, and return true. Otherwise return false. */ +static bool +check_value_oob(int i, const char *name, int low, int high) { - uint64_t ret = 0; - - tor_assert(opt); - tor_assert(value); - tor_assert(ok); - - *ok = 0; - ret = tor_parse_uint64(value, 10, min, max, ok, NULL); - if (!*ok) { - log_warn(LD_CONFIG, "%s must be between %" PRIu64 " and %"PRIu64 - ", not %s.", - opt, min, max, value); - goto err; + if (i < low || i > high) { + log_warn(LD_CONFIG, "%s must be between %d and %d, not %d.", + name, low, high, i); + return true; } - log_info(LD_CONFIG, "%s was parsed to %" PRIu64, opt, ret); - err: - return ret; + return false; } +/** + * Helper: check whether the integer value called <b>name</b> in <b>opts</b> + * is out-of-bounds. + **/ +#define CHECK_OOB(opts, name, low, high) \ + check_value_oob((opts)->name, #name, (low), (high)) + /** Helper function: Given a configuration option and its value, parse the * value as a hs_circuit_id_protocol_t. On success, ok is set to 1 and ret is * the parse value. On error, ok is set to 0 and the "none" @@ -189,6 +239,12 @@ config_learn_service_version(hs_service_t *service) return version; } +/** + * Header key indicating the start of a new hidden service configuration + * block. + **/ +static const char SECTION_HEADER[] = "HiddenServiceDir"; + /** Return true iff the given options starting at line_ for a hidden service * contains at least one invalid option. Each hidden service option don't * apply to all versions so this function can find out. The line_ MUST start @@ -219,6 +275,7 @@ config_has_invalid_options(const config_line_t *line_, "HiddenServiceEnableIntroDoSDefense", "HiddenServiceEnableIntroDoSRatePerSec", "HiddenServiceEnableIntroDoSBurstPerSec", + "HiddenServiceOnionBalanceInstance", NULL /* End marker. */ }; @@ -242,8 +299,11 @@ config_has_invalid_options(const config_line_t *line_, for (int i = 0; optlist[i]; i++) { const char *opt = optlist[i]; for (line = line_; line; line = line->next) { - if (!strcasecmp(line->key, "HiddenServiceDir")) { - /* We just hit the next hidden service, stop right now. */ + if (!strcasecmp(line->key, SECTION_HEADER)) { + /* We just hit the next hidden service, stop right now. + * (This shouldn't be possible, now that we have partitioned the list + * into sections.) */ + tor_assert_nonfatal_unreached(); goto end; } if (!strcasecmp(line->key, opt)) { @@ -302,105 +362,68 @@ config_validate_service(const hs_service_config_t *config) return -1; } -/** Configuration funcion for a version 3 service. The line_ must be pointing - * to the directive directly after a HiddenServiceDir. That way, when hitting - * the next HiddenServiceDir line or reaching the end of the list of lines, we - * know that we have to stop looking for more options. The given service +/** Configuration funcion for a version 3 service. The given service * object must be already allocated and passed through * config_generic_service() prior to calling this function. * * Return 0 on success else a negative value. */ static int -config_service_v3(const config_line_t *line_, +config_service_v3(const hs_opts_t *hs_opts, hs_service_config_t *config) { - int have_num_ip = 0; - bool export_circuit_id = false; /* just to detect duplicate options */ - bool dos_enabled = false, dos_rate_per_sec = false; - bool dos_burst_per_sec = false; - const char *dup_opt_seen = NULL; - const config_line_t *line; - tor_assert(config); + tor_assert(hs_opts); - for (line = line_; line; line = line->next) { - int ok = 0; - if (!strcasecmp(line->key, "HiddenServiceDir")) { - /* We just hit the next hidden service, stop right now. */ - break; - } - /* Number of introduction points. */ - if (!strcasecmp(line->key, "HiddenServiceNumIntroductionPoints")) { - config->num_intro_points = - (unsigned int) helper_parse_uint64(line->key, line->value, - NUM_INTRO_POINTS_DEFAULT, - HS_CONFIG_V3_MAX_INTRO_POINTS, - &ok); - if (!ok || have_num_ip) { - if (have_num_ip) - dup_opt_seen = line->key; - goto err; - } - have_num_ip = 1; - continue; - } - if (!strcasecmp(line->key, "HiddenServiceExportCircuitID")) { - config->circuit_id_protocol = - helper_parse_circuit_id_protocol(line->key, line->value, &ok); - if (!ok || export_circuit_id) { - if (export_circuit_id) { - dup_opt_seen = line->key; - } - goto err; - } - export_circuit_id = true; - continue; - } - if (!strcasecmp(line->key, "HiddenServiceEnableIntroDoSDefense")) { - config->has_dos_defense_enabled = - (unsigned int) helper_parse_uint64(line->key, line->value, - HS_CONFIG_V3_DOS_DEFENSE_DEFAULT, - 1, &ok); - if (!ok || dos_enabled) { - if (dos_enabled) { - dup_opt_seen = line->key; - } - goto err; - } - dos_enabled = true; - continue; - } - if (!strcasecmp(line->key, "HiddenServiceEnableIntroDoSRatePerSec")) { - config->intro_dos_rate_per_sec = - (unsigned int) helper_parse_uint64(line->key, line->value, - HS_CONFIG_V3_DOS_DEFENSE_RATE_PER_SEC_MIN, - HS_CONFIG_V3_DOS_DEFENSE_RATE_PER_SEC_MAX, &ok); - if (!ok || dos_rate_per_sec) { - if (dos_rate_per_sec) { - dup_opt_seen = line->key; - } - goto err; - } - dos_rate_per_sec = true; - log_info(LD_REND, "Service INTRO2 DoS defenses rate set to: %" PRIu32, - config->intro_dos_rate_per_sec); - continue; + /* Number of introduction points. */ + if (CHECK_OOB(hs_opts, HiddenServiceNumIntroductionPoints, + NUM_INTRO_POINTS_DEFAULT, + HS_CONFIG_V3_MAX_INTRO_POINTS)) { + goto err; + } + config->num_intro_points = hs_opts->HiddenServiceNumIntroductionPoints; + + /* Circuit ID export setting. */ + if (hs_opts->HiddenServiceExportCircuitID) { + int ok; + config->circuit_id_protocol = + helper_parse_circuit_id_protocol("HiddenServcieExportCircuitID", + hs_opts->HiddenServiceExportCircuitID, + &ok); + if (!ok) { + goto err; } - if (!strcasecmp(line->key, "HiddenServiceEnableIntroDoSBurstPerSec")) { - config->intro_dos_burst_per_sec = - (unsigned int) helper_parse_uint64(line->key, line->value, - HS_CONFIG_V3_DOS_DEFENSE_BURST_PER_SEC_MIN, - HS_CONFIG_V3_DOS_DEFENSE_BURST_PER_SEC_MAX, &ok); - if (!ok || dos_burst_per_sec) { - if (dos_burst_per_sec) { - dup_opt_seen = line->key; - } - goto err; - } - dos_burst_per_sec = true; - log_info(LD_REND, "Service INTRO2 DoS defenses burst set to: %" PRIu32, - config->intro_dos_burst_per_sec); - continue; + } + + /* Is the DoS defense enabled? */ + config->has_dos_defense_enabled = + hs_opts->HiddenServiceEnableIntroDoSDefense; + + /* Rate for DoS defense */ + if (CHECK_OOB(hs_opts, HiddenServiceEnableIntroDoSRatePerSec, + HS_CONFIG_V3_DOS_DEFENSE_RATE_PER_SEC_MIN, + HS_CONFIG_V3_DOS_DEFENSE_RATE_PER_SEC_MAX)) { + goto err; + } + config->intro_dos_rate_per_sec = + hs_opts->HiddenServiceEnableIntroDoSRatePerSec; + log_info(LD_REND, "Service INTRO2 DoS defenses rate set to: %" PRIu32, + config->intro_dos_rate_per_sec); + + if (CHECK_OOB(hs_opts, HiddenServiceEnableIntroDoSBurstPerSec, + HS_CONFIG_V3_DOS_DEFENSE_BURST_PER_SEC_MIN, + HS_CONFIG_V3_DOS_DEFENSE_BURST_PER_SEC_MAX)) { + goto err; + } + config->intro_dos_burst_per_sec = + hs_opts->HiddenServiceEnableIntroDoSBurstPerSec; + log_info(LD_REND, "Service INTRO2 DoS defenses burst set to: %" PRIu32, + config->intro_dos_burst_per_sec); + + /* Is this an onionbalance instance? */ + if (hs_opts->HiddenServiceOnionBalanceInstance) { + /* Option is enabled, parse config file. */ + if (! hs_ob_parse_config_file(config)) { + goto err; } } @@ -415,13 +438,10 @@ config_service_v3(const config_line_t *line_, return 0; err: - if (dup_opt_seen) { - log_warn(LD_CONFIG, "Duplicate directive %s.", dup_opt_seen); - } return -1; } -/** Configure a service using the given options in line_ and options. This is +/** Configure a service using the given options in hs_opts and options. This is * called for any service regardless of its version which means that all * directives in this function are generic to any service version. This * function will also check the validity of the service directory path. @@ -433,133 +453,73 @@ config_service_v3(const config_line_t *line_, * * Return 0 on success else -1. */ static int -config_generic_service(const config_line_t *line_, +config_generic_service(const hs_opts_t *hs_opts, const or_options_t *options, hs_service_t *service) { - int dir_seen = 0; - const config_line_t *line; hs_service_config_t *config; - /* If this is set, we've seen a duplicate of this option. Keep the string - * so we can log the directive. */ - const char *dup_opt_seen = NULL; - /* These variables will tell us if we ever have duplicate. */ - int have_version = 0, have_allow_unknown_ports = 0; - int have_dir_group_read = 0, have_max_streams = 0; - int have_max_streams_close = 0; - - tor_assert(line_); + + tor_assert(hs_opts); tor_assert(options); tor_assert(service); /* Makes thing easier. */ config = &service->config; - /* The first line starts with HiddenServiceDir so we consider what's next is - * the configuration of the service. */ - for (line = line_; line ; line = line->next) { - int ok = 0; - - /* This indicate that we have a new service to configure. */ - if (!strcasecmp(line->key, "HiddenServiceDir")) { - /* This function only configures one service at a time so if we've - * already seen one, stop right now. */ - if (dir_seen) { - break; + /* Directory where the service's keys are stored. */ + tor_assert(hs_opts->HiddenServiceDir); + config->directory_path = tor_strdup(hs_opts->HiddenServiceDir); + log_info(LD_CONFIG, "%s=%s. Configuring...", + SECTION_HEADER, escaped(config->directory_path)); + + /* Protocol version for the service. */ + if (hs_opts->HiddenServiceVersion == -1) { + /* No value was set; stay with the default. */ + } else if (CHECK_OOB(hs_opts, HiddenServiceVersion, + HS_VERSION_MIN, HS_VERSION_MAX)) { + goto err; + } else { + config->hs_version_explicitly_set = 1; + config->version = hs_opts->HiddenServiceVersion; + } + + /* Virtual port. */ + for (const config_line_t *portline = hs_opts->HiddenServicePort; + portline; portline = portline->next) { + char *err_msg = NULL; + /* XXX: Can we rename this? */ + rend_service_port_config_t *portcfg = + rend_service_parse_port_config(portline->value, " ", &err_msg); + if (!portcfg) { + if (err_msg) { + log_warn(LD_CONFIG, "%s", err_msg); } - /* Ok, we've seen one and we are about to configure it. */ - dir_seen = 1; - config->directory_path = tor_strdup(line->value); - log_info(LD_CONFIG, "HiddenServiceDir=%s. Configuring...", - escaped(config->directory_path)); - continue; - } - if (BUG(!dir_seen)) { + tor_free(err_msg); goto err; } - /* Version of the service. */ - if (!strcasecmp(line->key, "HiddenServiceVersion")) { - service->config.version = - (uint32_t) helper_parse_uint64(line->key, line->value, HS_VERSION_MIN, - HS_VERSION_MAX, &ok); - if (!ok || have_version) { - if (have_version) - dup_opt_seen = line->key; - goto err; - } - have_version = service->config.hs_version_explicitly_set = 1; - continue; - } - /* Virtual port. */ - if (!strcasecmp(line->key, "HiddenServicePort")) { - char *err_msg = NULL; - /* XXX: Can we rename this? */ - rend_service_port_config_t *portcfg = - rend_service_parse_port_config(line->value, " ", &err_msg); - if (!portcfg) { - if (err_msg) { - log_warn(LD_CONFIG, "%s", err_msg); - } - tor_free(err_msg); - goto err; - } - tor_assert(!err_msg); - smartlist_add(config->ports, portcfg); - log_info(LD_CONFIG, "HiddenServicePort=%s for %s", - line->value, escaped(config->directory_path)); - continue; - } - /* Do we allow unknown ports. */ - if (!strcasecmp(line->key, "HiddenServiceAllowUnknownPorts")) { - config->allow_unknown_ports = - (unsigned int) helper_parse_uint64(line->key, line->value, 0, 1, &ok); - if (!ok || have_allow_unknown_ports) { - if (have_allow_unknown_ports) - dup_opt_seen = line->key; - goto err; - } - have_allow_unknown_ports = 1; - continue; - } - /* Directory group readable. */ - if (!strcasecmp(line->key, "HiddenServiceDirGroupReadable")) { - config->dir_group_readable = - (unsigned int) helper_parse_uint64(line->key, line->value, 0, 1, &ok); - if (!ok || have_dir_group_read) { - if (have_dir_group_read) - dup_opt_seen = line->key; - goto err; - } - have_dir_group_read = 1; - continue; - } - /* Maximum streams per circuit. */ - if (!strcasecmp(line->key, "HiddenServiceMaxStreams")) { - config->max_streams_per_rdv_circuit = - helper_parse_uint64(line->key, line->value, 0, - HS_CONFIG_MAX_STREAMS_PER_RDV_CIRCUIT, &ok); - if (!ok || have_max_streams) { - if (have_max_streams) - dup_opt_seen = line->key; - goto err; - } - have_max_streams = 1; - continue; - } - /* Maximum amount of streams before we close the circuit. */ - if (!strcasecmp(line->key, "HiddenServiceMaxStreamsCloseCircuit")) { - config->max_streams_close_circuit = - (unsigned int) helper_parse_uint64(line->key, line->value, 0, 1, &ok); - if (!ok || have_max_streams_close) { - if (have_max_streams_close) - dup_opt_seen = line->key; - goto err; - } - have_max_streams_close = 1; - continue; - } + tor_assert(!err_msg); + smartlist_add(config->ports, portcfg); + log_info(LD_CONFIG, "HiddenServicePort=%s for %s", + portline->value, escaped(config->directory_path)); } + /* Do we allow unknown ports? */ + config->allow_unknown_ports = hs_opts->HiddenServiceAllowUnknownPorts; + + /* Directory group readable. */ + config->dir_group_readable = hs_opts->HiddenServiceDirGroupReadable; + + /* Maximum streams per circuit. */ + if (CHECK_OOB(hs_opts, HiddenServiceMaxStreams, + 0, HS_CONFIG_MAX_STREAMS_PER_RDV_CIRCUIT)) { + goto err; + } + config->max_streams_per_rdv_circuit = hs_opts->HiddenServiceMaxStreams; + + /* Maximum amount of streams before we close the circuit. */ + config->max_streams_close_circuit = + hs_opts->HiddenServiceMaxStreamsCloseCircuit; + /* Check if we are configured in non anonymous mode meaning every service * becomes a single onion service. */ if (rend_service_non_anonymous_mode_enabled(options)) { @@ -569,9 +529,6 @@ config_generic_service(const config_line_t *line_, /* Success */ return 0; err: - if (dup_opt_seen) { - log_warn(LD_CONFIG, "Duplicate directive %s.", dup_opt_seen); - } return -1; } @@ -581,11 +538,13 @@ config_generic_service(const config_line_t *line_, * the service to the given list and return 0. On error, nothing is added to * the list and a negative value is returned. */ static int -config_service(const config_line_t *line, const or_options_t *options, +config_service(config_line_t *line, const or_options_t *options, smartlist_t *service_list) { int ret; hs_service_t *service = NULL; + hs_opts_t *hs_opts = NULL; + char *msg = NULL; tor_assert(line); tor_assert(options); @@ -594,9 +553,25 @@ config_service(const config_line_t *line, const or_options_t *options, /* We have a new hidden service. */ service = hs_service_new(options); + /* Try to validate and parse the configuration lines into 'hs_opts' */ + hs_opts = hs_opts_new(); + ret = config_assign(get_hs_opts_mgr(), hs_opts, line, 0, &msg); + if (ret < 0) { + log_warn(LD_REND, "Can't parse configuration for onion service: %s", msg); + goto err; + } + tor_assert_nonfatal(msg == NULL); + validation_status_t vs = config_validate(get_hs_opts_mgr(), NULL, + hs_opts, &msg); + if (vs < 0) { + log_warn(LD_REND, "Bad configuration for onion service: %s", msg); + goto err; + } + tor_assert_nonfatal(msg == NULL); + /* We'll configure that service as a generic one and then pass it to a * specific function according to the configured version number. */ - if (config_generic_service(line, options, service) < 0) { + if (config_generic_service(hs_opts, options, service) < 0) { goto err; } @@ -631,10 +606,10 @@ config_service(const config_line_t *line, const or_options_t *options, * directory line, the function knows that it has to stop parsing. */ switch (service->config.version) { case HS_VERSION_TWO: - ret = rend_config_service(line->next, options, &service->config); + ret = rend_config_service(hs_opts, options, &service->config); break; case HS_VERSION_THREE: - ret = config_service_v3(line->next, &service->config); + ret = config_service_v3(hs_opts, &service->config); break; default: /* We do validate before if we support the parsed version. */ @@ -653,11 +628,14 @@ config_service(const config_line_t *line, const or_options_t *options, /* Passes, add it to the given list. */ smartlist_add(service_list, service); + hs_opts_free(hs_opts); return 0; err: hs_service_free(service); + hs_opts_free(hs_opts); + tor_free(msg); return -1; } @@ -667,8 +645,8 @@ config_service(const config_line_t *line, const or_options_t *options, int hs_config_service_all(const or_options_t *options, int validate_only) { - int dir_option_seen = 0, ret = -1; - const config_line_t *line; + int ret = -1; + config_line_t *remaining = NULL; smartlist_t *new_service_list = NULL; tor_assert(options); @@ -677,23 +655,24 @@ hs_config_service_all(const or_options_t *options, int validate_only) * validation and staging for >= v3. */ new_service_list = smartlist_new(); - for (line = options->RendConfigLines; line; line = line->next) { - /* Ignore all directives that aren't the start of a service. */ - if (strcasecmp(line->key, "HiddenServiceDir")) { - if (!dir_option_seen) { - log_warn(LD_CONFIG, "%s with no preceding HiddenServiceDir directive", - line->key); - goto err; - } - continue; - } - /* Flag that we've seen a directory directive and we'll use it to make - * sure that the torrc options ordering is actually valid. */ - dir_option_seen = 1; + /* We need to start with a HiddenServiceDir line */ + if (options->RendConfigLines && + strcasecmp(options->RendConfigLines->key, SECTION_HEADER)) { + log_warn(LD_CONFIG, "%s with no preceding %s directive", + options->RendConfigLines->key, SECTION_HEADER); + goto err; + } + + remaining = config_lines_dup(options->RendConfigLines); + while (remaining) { + config_line_t *section = remaining; + remaining = config_lines_partition(section, SECTION_HEADER); /* Try to configure this service now. On success, it will be added to the * list and validated against the service in that same list. */ - if (config_service(line, options, new_service_list) < 0) { + int rv = config_service(section, options, new_service_list); + config_free_lines(section); + if (rv < 0) { goto err; } } @@ -753,3 +732,12 @@ hs_config_client_auth_all(const or_options_t *options, int validate_only) done: return ret; } + +/** + * Free all resources held by the hs_config.c module. + **/ +void +hs_config_free_all(void) +{ + config_mgr_free(hs_opts_mgr); +} diff --git a/src/feature/hs/hs_config.h b/src/feature/hs/hs_config.h index 5694cf1e9b..c60b4fbb5d 100644 --- a/src/feature/hs/hs_config.h +++ b/src/feature/hs/hs_config.h @@ -30,5 +30,6 @@ int hs_config_service_all(const or_options_t *options, int validate_only); int hs_config_client_auth_all(const or_options_t *options, int validate_only); -#endif /* !defined(TOR_HS_CONFIG_H) */ +void hs_config_free_all(void); +#endif /* !defined(TOR_HS_CONFIG_H) */ diff --git a/src/feature/hs/hs_descriptor.c b/src/feature/hs/hs_descriptor.c index d1c81bbff8..50a46fb40f 100644 --- a/src/feature/hs/hs_descriptor.c +++ b/src/feature/hs/hs_descriptor.c @@ -212,7 +212,7 @@ build_secret_input(const hs_descriptor_t *desc, memcpy(secret_input, secret_data, secret_data_len); offset += secret_data_len; /* Copy subcredential. */ - memcpy(secret_input + offset, desc->subcredential, DIGEST256_LEN); + memcpy(secret_input + offset, desc->subcredential.subcred, DIGEST256_LEN); offset += DIGEST256_LEN; /* Copy revision counter value. */ set_uint64(secret_input + offset, @@ -1019,10 +1019,6 @@ desc_encode_v3(const hs_descriptor_t *desc, tor_assert(encoded_out); tor_assert(desc->plaintext_data.version == 3); - if (BUG(desc->subcredential == NULL)) { - goto err; - } - /* Build the non-encrypted values. */ { char *encoded_cert; @@ -1376,8 +1372,7 @@ encrypted_data_length_is_valid(size_t len) * and return the buffer's length. The caller should wipe and free its content * once done with it. This function can't fail. */ static size_t -build_descriptor_cookie_keys(const uint8_t *subcredential, - size_t subcredential_len, +build_descriptor_cookie_keys(const hs_subcredential_t *subcredential, const curve25519_secret_key_t *sk, const curve25519_public_key_t *pk, uint8_t **keys_out) @@ -1399,7 +1394,7 @@ build_descriptor_cookie_keys(const uint8_t *subcredential, /* Calculate KEYS = KDF(subcredential | SECRET_SEED, 40) */ xof = crypto_xof_new(); - crypto_xof_add_bytes(xof, subcredential, subcredential_len); + crypto_xof_add_bytes(xof, subcredential->subcred, SUBCRED_LEN); crypto_xof_add_bytes(xof, secret_seed, sizeof(secret_seed)); crypto_xof_squeeze_bytes(xof, keystream, keystream_len); crypto_xof_free(xof); @@ -1434,7 +1429,8 @@ decrypt_descriptor_cookie(const hs_descriptor_t *desc, tor_assert(!fast_mem_is_zero( (char *) &desc->superencrypted_data.auth_ephemeral_pubkey, sizeof(desc->superencrypted_data.auth_ephemeral_pubkey))); - tor_assert(!fast_mem_is_zero((char *) desc->subcredential, DIGEST256_LEN)); + tor_assert(!fast_mem_is_zero((char *) desc->subcredential.subcred, + DIGEST256_LEN)); /* Catch potential code-flow cases of an unitialized private key sneaking * into this function. */ @@ -1444,7 +1440,7 @@ decrypt_descriptor_cookie(const hs_descriptor_t *desc, /* Get the KEYS component to derive the CLIENT-ID and COOKIE-KEY. */ keystream_length = - build_descriptor_cookie_keys(desc->subcredential, DIGEST256_LEN, + build_descriptor_cookie_keys(&desc->subcredential, client_auth_sk, &desc->superencrypted_data.auth_ephemeral_pubkey, &keystream); @@ -2572,7 +2568,7 @@ hs_desc_decode_plaintext(const char *encoded, * set to NULL. */ hs_desc_decode_status_t hs_desc_decode_descriptor(const char *encoded, - const uint8_t *subcredential, + const hs_subcredential_t *subcredential, const curve25519_secret_key_t *client_auth_sk, hs_descriptor_t **desc_out) { @@ -2590,7 +2586,7 @@ hs_desc_decode_descriptor(const char *encoded, goto err; } - memcpy(desc->subcredential, subcredential, sizeof(desc->subcredential)); + memcpy(&desc->subcredential, subcredential, sizeof(desc->subcredential)); ret = hs_desc_decode_plaintext(encoded, &desc->plaintext_data); if (ret != HS_DESC_DECODE_OK) { @@ -2680,7 +2676,7 @@ hs_desc_encode_descriptor,(const hs_descriptor_t *desc, * symmetric only if the client auth is disabled. That is, the descriptor * cookie will be NULL. */ if (!descriptor_cookie) { - ret = hs_desc_decode_descriptor(*encoded_out, desc->subcredential, + ret = hs_desc_decode_descriptor(*encoded_out, &desc->subcredential, NULL, NULL); if (BUG(ret != HS_DESC_DECODE_OK)) { ret = -1; @@ -2884,7 +2880,7 @@ hs_desc_build_fake_authorized_client(void) * key, and descriptor cookie, build the auth client so we can then encode the * descriptor for publication. client_out must be already allocated. */ void -hs_desc_build_authorized_client(const uint8_t *subcredential, +hs_desc_build_authorized_client(const hs_subcredential_t *subcredential, const curve25519_public_key_t *client_auth_pk, const curve25519_secret_key_t * auth_ephemeral_sk, @@ -2912,7 +2908,7 @@ hs_desc_build_authorized_client(const uint8_t *subcredential, /* Get the KEYS part so we can derive the CLIENT-ID and COOKIE-KEY. */ keystream_length = - build_descriptor_cookie_keys(subcredential, DIGEST256_LEN, + build_descriptor_cookie_keys(subcredential, auth_ephemeral_sk, client_auth_pk, &keystream); tor_assert(keystream_length > 0); diff --git a/src/feature/hs/hs_descriptor.h b/src/feature/hs/hs_descriptor.h index 639dd31c8f..08daa904b6 100644 --- a/src/feature/hs/hs_descriptor.h +++ b/src/feature/hs/hs_descriptor.h @@ -14,6 +14,7 @@ #include "core/or/or.h" #include "trunnel/ed25519_cert.h" /* needed for trunnel */ #include "feature/nodelist/torcert.h" +#include "core/crypto/hs_ntor.h" /* for hs_subcredential_t */ /* Trunnel */ struct link_specifier_t; @@ -238,7 +239,7 @@ typedef struct hs_descriptor_t { /** Subcredentials of a service, used by the client and service to decrypt * the encrypted data. */ - uint8_t subcredential[DIGEST256_LEN]; + hs_subcredential_t subcredential; } hs_descriptor_t; /** Return true iff the given descriptor format version is supported. */ @@ -277,7 +278,7 @@ MOCK_DECL(int, char **encoded_out)); int hs_desc_decode_descriptor(const char *encoded, - const uint8_t *subcredential, + const hs_subcredential_t *subcredential, const curve25519_secret_key_t *client_auth_sk, hs_descriptor_t **desc_out); int hs_desc_decode_plaintext(const char *encoded, @@ -302,7 +303,7 @@ void hs_desc_authorized_client_free_(hs_desc_authorized_client_t *client); hs_desc_authorized_client_t *hs_desc_build_fake_authorized_client(void); -void hs_desc_build_authorized_client(const uint8_t *subcredential, +void hs_desc_build_authorized_client(const hs_subcredential_t *subcredential, const curve25519_public_key_t * client_auth_pk, const curve25519_secret_key_t * diff --git a/src/feature/hs/hs_ob.c b/src/feature/hs/hs_ob.c new file mode 100644 index 0000000000..9499c28d20 --- /dev/null +++ b/src/feature/hs/hs_ob.c @@ -0,0 +1,408 @@ +/* Copyright (c) 2017-2020, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +/** + * \file hs_ob.c + * \brief Implement Onion Balance specific code. + **/ + +#define HS_OB_PRIVATE + +#include "feature/hs/hs_service.h" + +#include "feature/nodelist/networkstatus.h" +#include "feature/nodelist/networkstatus_st.h" + +#include "lib/confmgt/confmgt.h" +#include "lib/encoding/confline.h" + +#include "feature/hs/hs_ob.h" + +/* Options config magic number. */ +#define OB_OPTIONS_MAGIC 0x631DE7EA + +/* Helper macros. */ +#define VAR(varname, conftype, member, initvalue) \ + CONFIG_VAR_ETYPE(ob_options_t, varname, conftype, member, 0, initvalue) +#define V(member,conftype,initvalue) \ + VAR(#member, conftype, member, initvalue) + +/* Dummy instance of ob_options_t, used for type-checking its members with + * CONF_CHECK_VAR_TYPE. */ +DUMMY_TYPECHECK_INSTANCE(ob_options_t); + +/* Array of variables for the config file options. */ +static const config_var_t config_vars[] = { + V(MasterOnionAddress, LINELIST, NULL), + + END_OF_CONFIG_VARS +}; + +/* "Extra" variable in the state that receives lines we can't parse. This + * lets us preserve options from versions of Tor newer than us. */ +static const struct_member_t config_extra_vars = { + .name = "__extra", + .type = CONFIG_TYPE_LINELIST, + .offset = offsetof(ob_options_t, ExtraLines), +}; + +/* Configuration format of ob_options_t. */ +static const config_format_t config_format = { + .size = sizeof(ob_options_t), + .magic = { + "ob_options_t", + OB_OPTIONS_MAGIC, + offsetof(ob_options_t, magic_), + }, + .vars = config_vars, + .extra = &config_extra_vars, +}; + +/* Global configuration manager for the config file. */ +static config_mgr_t *config_options_mgr = NULL; + +/* Return the configuration manager for the config file. */ +static const config_mgr_t * +get_config_options_mgr(void) +{ + if (PREDICT_UNLIKELY(config_options_mgr == NULL)) { + config_options_mgr = config_mgr_new(&config_format); + config_mgr_freeze(config_options_mgr); + } + return config_options_mgr; +} + +#define ob_option_free(val) \ + FREE_AND_NULL(ob_options_t, ob_option_free_, (val)) + +/** Helper: Free a config options object. */ +static void +ob_option_free_(ob_options_t *opts) +{ + if (opts == NULL) { + return; + } + config_free(get_config_options_mgr(), opts); +} + +/** Return an allocated config options object. */ +static ob_options_t * +ob_option_new(void) +{ + ob_options_t *opts = config_new(get_config_options_mgr()); + config_init(get_config_options_mgr(), opts); + return opts; +} + +/** Helper function: From the configuration line value which is an onion + * address with the ".onion" extension, find the public key and put it in + * pkey_out. + * + * On success, true is returned. Else, false and pkey is untouched. */ +static bool +get_onion_public_key(const char *value, ed25519_public_key_t *pkey_out) +{ + char address[HS_SERVICE_ADDR_LEN_BASE32 + 1]; + + tor_assert(value); + tor_assert(pkey_out); + + if (strcmpend(value, ".onion")) { + /* Not a .onion extension, bad format. */ + return false; + } + + /* Length validation. The -1 is because sizeof() counts the NUL byte. */ + if (strlen(value) > + (HS_SERVICE_ADDR_LEN_BASE32 + sizeof(".onion") - 1)) { + /* Too long, bad format. */ + return false; + } + + /* We don't want the .onion so we add 2 because size - 1 is copied with + * strlcpy() in order to accomodate the NUL byte and sizeof() counts the NUL + * byte so we need to remove them from the equation. */ + strlcpy(address, value, strlen(value) - sizeof(".onion") + 2); + + if (hs_parse_address_no_log(address, pkey_out, NULL, NULL, NULL) < 0) { + return false; + } + + /* Success. */ + return true; +} + +/** Parse the given ob options in opts and set the service config object + * accordingly. + * + * Return 1 on success else 0. */ +static int +ob_option_parse(hs_service_config_t *config, const ob_options_t *opts) +{ + int ret = 0; + config_line_t *line; + + tor_assert(config); + tor_assert(opts); + + for (line = opts->MasterOnionAddress; line; line = line->next) { + /* Allocate config list if need be. */ + if (!config->ob_master_pubkeys) { + config->ob_master_pubkeys = smartlist_new(); + } + ed25519_public_key_t *pubkey = tor_malloc_zero(sizeof(*pubkey)); + + if (!get_onion_public_key(line->value, pubkey)) { + log_warn(LD_REND, "OnionBalance: MasterOnionAddress %s is invalid", + line->value); + tor_free(pubkey); + goto end; + } + smartlist_add(config->ob_master_pubkeys, pubkey); + log_notice(LD_REND, "OnionBalance: MasterOnionAddress %s registered", + line->value); + } + /* Success. */ + ret = 1; + + end: + /* No keys added, we free the list since no list means no onion balance + * support for this tor instance. */ + if (smartlist_len(config->ob_master_pubkeys) == 0) { + smartlist_free(config->ob_master_pubkeys); + } + return ret; +} + +/** For the given master public key and time period, compute the subcredential + * and put them into subcredential. The subcredential parameter needs to be at + * least DIGEST256_LEN in size. */ +static void +build_subcredential(const ed25519_public_key_t *pkey, uint64_t tp, + hs_subcredential_t *subcredential) +{ + ed25519_public_key_t blinded_pubkey; + + tor_assert(pkey); + tor_assert(subcredential); + + hs_build_blinded_pubkey(pkey, NULL, 0, tp, &blinded_pubkey); + hs_get_subcredential(pkey, &blinded_pubkey, subcredential); +} + +/* + * Public API. + */ + +/** Return true iff the given service is configured as an onion balance + * instance. To satisfy that condition, there must at least be one master + * ed25519 public key configured. */ +bool +hs_ob_service_is_instance(const hs_service_t *service) +{ + if (BUG(service == NULL)) { + return false; + } + + /* No list, we are not an instance. */ + if (!service->config.ob_master_pubkeys) { + return false; + } + + return smartlist_len(service->config.ob_master_pubkeys) > 0; +} + +/** Read and parse the config file at fname on disk. The service config object + * is populated with the options if any. + * + * Return 1 on success else 0. This is to follow the "ok" convention in + * hs_config.c. */ +int +hs_ob_parse_config_file(hs_service_config_t *config) +{ + static const char *fname = "ob_config"; + int ret = 0; + char *content = NULL, *errmsg = NULL, *config_file_path = NULL; + ob_options_t *options = NULL; + config_line_t *lines = NULL; + + tor_assert(config); + + /* Read file from disk. */ + config_file_path = hs_path_from_filename(config->directory_path, fname); + content = read_file_to_str(config_file_path, 0, NULL); + if (!content) { + log_warn(LD_FS, "OnionBalance: Unable to read config file %s", + escaped(config_file_path)); + goto end; + } + + /* Parse lines. */ + if (config_get_lines(content, &lines, 0) < 0) { + goto end; + } + + options = ob_option_new(); + config_assign(get_config_options_mgr(), options, lines, 0, &errmsg); + if (errmsg) { + log_warn(LD_REND, "OnionBalance: Unable to parse config file: %s", + errmsg); + tor_free(errmsg); + goto end; + } + + /* Parse the options and set the service config object with the details. */ + ret = ob_option_parse(config, options); + + end: + config_free_lines(lines); + ob_option_free(options); + tor_free(content); + tor_free(config_file_path); + return ret; +} + +/** Compute all possible subcredentials for every onion master key in the given + * service config object. subcredentials_out is allocated and set as an + * continous array containing all possible values. + * + * On success, return the number of subcredential put in the array which will + * correspond to an arry of size: n * DIGEST256_LEN where DIGEST256_LEN is the + * length of a single subcredential. + * + * If the given configuration object has no OB master keys configured, 0 is + * returned and subcredentials_out is set to NULL. + * + * Otherwise, this can't fail. */ +STATIC size_t +compute_subcredentials(const hs_service_t *service, + hs_subcredential_t **subcredentials_out) +{ + unsigned int num_pkeys, idx = 0; + hs_subcredential_t *subcreds = NULL; + const int steps[3] = {0, -1, 1}; + const unsigned int num_steps = ARRAY_LENGTH(steps); + const uint64_t tp = hs_get_time_period_num(0); + + tor_assert(service); + tor_assert(subcredentials_out); + /* Our caller has checked these too */ + tor_assert(service->desc_current); + tor_assert(service->desc_next); + + /* Make sure we are an OB instance, or bail out. */ + num_pkeys = smartlist_len(service->config.ob_master_pubkeys); + if (!num_pkeys) { + *subcredentials_out = NULL; + return 0; + } + + /* Time to build all the subcredentials for each time period: two for each + * instance descriptor plus three for the onionbalance frontend service: the + * previous one (-1), the current one (0) and the next one (1) for each + * configured key in order to accomodate client and service consensus skew. + * + * If the client consensus after_time is at 23:00 but the service one is at + * 01:00, the client will be using the previous time period where the + * service will think it is the client next time period. Thus why we have + * to try them all. + * + * The normal use case works because the service gets the descriptor object + * that corresponds to the intro point's request, and because each + * descriptor corresponds to a specific subcredential, we get the right + * subcredential out of it, and use that to do the decryption. + * + * As a slight optimization, statistically, the current time period (0) will + * be the one to work first so we'll put them first in the array to maximize + * our chance of success. */ + + /* We use a flat array, not a smartlist_t, in order to minimize memory + * allocation. + * + * Size of array is: length of a single subcredential multiplied by the + * number of time period we need to compute and finally multiplied by the + * total number of keys we are about to process. In other words, for each + * key, we allocate 3 subcredential slots. Then in the end we also add two + * subcredentials for this instance's active descriptors. */ + subcreds = + tor_calloc((num_steps * num_pkeys) + 2, sizeof(hs_subcredential_t)); + + /* For each master pubkey we add 3 subcredentials: */ + for (unsigned int i = 0; i < num_steps; i++) { + SMARTLIST_FOREACH_BEGIN(service->config.ob_master_pubkeys, + const ed25519_public_key_t *, pkey) { + build_subcredential(pkey, tp + steps[i], &subcreds[idx]); + idx++; + } SMARTLIST_FOREACH_END(pkey); + } + + /* And then in the end we add the two subcredentials of the current active + * instance descriptors */ + memcpy(&subcreds[idx++], &service->desc_current->desc->subcredential, + sizeof(hs_subcredential_t)); + memcpy(&subcreds[idx++], &service->desc_next->desc->subcredential, + sizeof(hs_subcredential_t)); + + log_info(LD_REND, "Refreshing %u onionbalance keys (TP #%d).", + idx, (int)tp); + + *subcredentials_out = subcreds; + return idx; +} + +/** + * If we are an Onionbalance instance, refresh our keys. + * + * If we are not an Onionbalance instance or we are not ready to do so, this + * is a NOP. + * + * This function is called everytime we build a new descriptor. That's because + * we want our Onionbalance keys to always use up-to-date subcredentials both + * for the instance (ourselves) and for the onionbalance frontend. + */ +void +hs_ob_refresh_keys(hs_service_t *service) +{ + hs_subcredential_t *ob_subcreds = NULL; + size_t num_subcreds; + + tor_assert(service); + + /* Don't do any of this if we are not configured as an OB instance */ + if (!hs_ob_service_is_instance(service)) { + return; + } + + /* We need both service descriptors created to make onionbalance keys. + * + * That's because we fetch our own (the instance's) subcredentials from our + * own descriptors which should always include the latest subcredentials that + * clients would use. + * + * This function is called with each descriptor build, so we will be + * eventually be called when both descriptors are created. */ + if (!service->desc_current || !service->desc_next) { + return; + } + + /* Get a new set of subcreds */ + num_subcreds = compute_subcredentials(service, &ob_subcreds); + if (BUG(!num_subcreds)) { + return; + } + + /* Delete old subcredentials if any */ + if (service->state.ob_subcreds) { + tor_free(service->state.ob_subcreds); + } + + service->state.ob_subcreds = ob_subcreds; + service->state.n_ob_subcreds = num_subcreds; +} + +/** Free any memory allocated by the onionblance subsystem. */ +void +hs_ob_free_all(void) +{ + config_mgr_free(config_options_mgr); +} diff --git a/src/feature/hs/hs_ob.h b/src/feature/hs/hs_ob.h new file mode 100644 index 0000000000..d6e6e73a84 --- /dev/null +++ b/src/feature/hs/hs_ob.h @@ -0,0 +1,40 @@ +/* Copyright (c) 2020, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +/** + * \file hs_ob.h + * \brief Header file for the specific code for onion balance. + **/ + +#ifndef TOR_HS_OB_H +#define TOR_HS_OB_H + +#include "feature/hs/hs_service.h" + +bool hs_ob_service_is_instance(const hs_service_t *service); + +int hs_ob_parse_config_file(hs_service_config_t *config); + +struct hs_subcredential_t; + +void hs_ob_free_all(void); + +void hs_ob_refresh_keys(hs_service_t *service); + +#ifdef HS_OB_PRIVATE + +STATIC size_t compute_subcredentials(const hs_service_t *service, + struct hs_subcredential_t **subcredentials); + +typedef struct ob_options_t { + /** Magic number to identify the structure in memory. */ + uint32_t magic_; + /** Master Onion Address(es). */ + struct config_line_t *MasterOnionAddress; + /** Extra Lines for configuration we might not know. */ + struct config_line_t *ExtraLines; +} ob_options_t; + +#endif /* defined(HS_OB_PRIVATE) */ + +#endif /* !defined(TOR_HS_OB_H) */ diff --git a/src/feature/hs/hs_options.inc b/src/feature/hs/hs_options.inc new file mode 100644 index 0000000000..1a1444fd05 --- /dev/null +++ b/src/feature/hs/hs_options.inc @@ -0,0 +1,36 @@ +/* Copyright (c) 2001 Matej Pfajfar. + * Copyright (c) 2001-2004, Roger Dingledine. + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2019, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +/** + * @file dirauth_options.inc + * @brief Declare configuration options for a single hidden service. + * + * Note that this options file behaves differently from most, since it + * is not used directly by the options manager. Instead, it is applied to + * a group of hidden service options starting with a HiddenServiceDir and + * extending up to the next HiddenServiceDir. + **/ + +/** Holds configuration for a single hidden service. */ +BEGIN_CONF_STRUCT(hs_opts_t) + +CONF_VAR(HiddenServiceDir, FILENAME, 0, NULL) +CONF_VAR(HiddenServiceDirGroupReadable, BOOL, 0, "0") +CONF_VAR(HiddenServicePort, LINELIST, 0, NULL) +// "-1" means "auto" here. +CONF_VAR(HiddenServiceVersion, INT, 0, "-1") +CONF_VAR(HiddenServiceAuthorizeClient, STRING, 0, NULL) +CONF_VAR(HiddenServiceAllowUnknownPorts, BOOL, 0, "0") +CONF_VAR(HiddenServiceMaxStreams, POSINT, 0, "0") +CONF_VAR(HiddenServiceMaxStreamsCloseCircuit, BOOL, 0, "0") +CONF_VAR(HiddenServiceNumIntroductionPoints, POSINT, 0, "3") +CONF_VAR(HiddenServiceExportCircuitID, STRING, 0, NULL) +CONF_VAR(HiddenServiceEnableIntroDoSDefense, BOOL, 0, "0") +CONF_VAR(HiddenServiceEnableIntroDoSRatePerSec, POSINT, 0, "25") +CONF_VAR(HiddenServiceEnableIntroDoSBurstPerSec, POSINT, 0, "200") +CONF_VAR(HiddenServiceOnionBalanceInstance, BOOL, 0, "0") + +END_CONF_STRUCT(hs_opts_t) diff --git a/src/feature/hs/hs_opts_st.h b/src/feature/hs/hs_opts_st.h new file mode 100644 index 0000000000..279f0d6da6 --- /dev/null +++ b/src/feature/hs/hs_opts_st.h @@ -0,0 +1,30 @@ +/* Copyright (c) 2001 Matej Pfajfar. + * Copyright (c) 2001-2004, Roger Dingledine. + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2020, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +/** + * @file dirauth_options_st.h + * @brief Structure hs_opts_t to hold options for a single hidden service. + **/ + +#ifndef TOR_FEATURE_HS_HS_OPTS_ST_H +#define TOR_FEATURE_HS_HS_OPTS_ST_H + +#include "lib/conf/confdecl.h" +#define CONF_CONTEXT STRUCT +#include "feature/hs/hs_options.inc" +#undef CONF_CONTEXT + +/** + * An hs_opts_t holds the parsed options for a single HS configuration + * section. + * + * This name ends with 'opts' instead of 'options' to signal that it is not + * handled directly by the or_options_t configuration manager, but that + * first we partition the "HiddenService*" options by section. + **/ +typedef struct hs_opts_t hs_opts_t; + +#endif /* !defined(TOR_FEATURE_HS_HS_OPTS_ST_H) */ diff --git a/src/feature/hs/hs_service.c b/src/feature/hs/hs_service.c index b366ce83d9..a42879a48f 100644 --- a/src/feature/hs/hs_service.c +++ b/src/feature/hs/hs_service.c @@ -41,6 +41,7 @@ #include "feature/hs/hs_intropoint.h" #include "feature/hs/hs_service.h" #include "feature/hs/hs_stats.h" +#include "feature/hs/hs_ob.h" #include "feature/dircommon/dir_connection_st.h" #include "core/or/edge_connection_st.h" @@ -151,11 +152,11 @@ HT_PROTOTYPE(hs_service_ht, /* Name of hashtable. */ hs_service_t, /* Object contained in the map. */ hs_service_node, /* The name of the HT_ENTRY member. */ hs_service_ht_hash, /* Hashing function. */ - hs_service_ht_eq) /* Compare function for objects. */ + hs_service_ht_eq); /* Compare function for objects. */ HT_GENERATE2(hs_service_ht, hs_service_t, hs_service_node, hs_service_ht_hash, hs_service_ht_eq, - 0.6, tor_reallocarray, tor_free_) + 0.6, tor_reallocarray, tor_free_); /** Query the given service map with a public key and return a service object * if found else NULL. It is also possible to set a directory path in the @@ -267,6 +268,11 @@ service_clear_config(hs_service_config_t *config) service_authorized_client_free(p)); smartlist_free(config->clients); } + if (config->ob_master_pubkeys) { + SMARTLIST_FOREACH(config->ob_master_pubkeys, ed25519_public_key_t *, k, + tor_free(k)); + smartlist_free(config->ob_master_pubkeys); + } memset(config, 0, sizeof(*config)); } @@ -701,8 +707,8 @@ get_extend_info_from_intro_point(const hs_service_intro_point_t *ip, /** Return the number of introduction points that are established for the * given descriptor. */ -static unsigned int -count_desc_circuit_established(const hs_service_descriptor_t *desc) +MOCK_IMPL(STATIC unsigned int, +count_desc_circuit_established, (const hs_service_descriptor_t *desc)) { unsigned int count = 0; @@ -884,10 +890,18 @@ move_hs_state(hs_service_t *src_service, hs_service_t *dst_service) if (dst->replay_cache_rend_cookie != NULL) { replaycache_free(dst->replay_cache_rend_cookie); } + dst->replay_cache_rend_cookie = src->replay_cache_rend_cookie; + src->replay_cache_rend_cookie = NULL; /* steal pointer reference */ + dst->next_rotation_time = src->next_rotation_time; - src->replay_cache_rend_cookie = NULL; /* steal pointer reference */ + if (src->ob_subcreds) { + dst->ob_subcreds = src->ob_subcreds; + dst->n_ob_subcreds = src->n_ob_subcreds; + + src->ob_subcreds = NULL; /* steal pointer reference */ + } } /** Register services that are in the staging list. Once this function returns, @@ -1764,7 +1778,8 @@ build_service_desc_superencrypted(const hs_service_t *service, sizeof(curve25519_public_key_t)); /* Test that subcred is not zero because we might use it below */ - if (BUG(fast_mem_is_zero((char*)desc->desc->subcredential, DIGEST256_LEN))) { + if (BUG(fast_mem_is_zero((char*)desc->desc->subcredential.subcred, + DIGEST256_LEN))) { return -1; } @@ -1781,7 +1796,7 @@ build_service_desc_superencrypted(const hs_service_t *service, /* Prepare the client for descriptor and then add to the list in the * superencrypted part of the descriptor */ - hs_desc_build_authorized_client(desc->desc->subcredential, + hs_desc_build_authorized_client(&desc->desc->subcredential, &client->client_pk, &desc->auth_ephemeral_kp.seckey, desc->descriptor_cookie, desc_client); @@ -1837,7 +1852,7 @@ build_service_desc_plaintext(const hs_service_t *service, /* Set the subcredential. */ hs_get_subcredential(&service->keys.identity_pk, &desc->blinded_kp.pubkey, - desc->desc->subcredential); + &desc->desc->subcredential); plaintext = &desc->desc->plaintext_data; @@ -1980,9 +1995,15 @@ build_service_descriptor(hs_service_t *service, uint64_t time_period_num, /* Assign newly built descriptor to the next slot. */ *desc_out = desc; + /* Fire a CREATED control port event. */ hs_control_desc_event_created(service->onion_address, &desc->blinded_kp.pubkey); + + /* If we are an onionbalance instance, we refresh our keys when we rotate + * descriptors. */ + hs_ob_refresh_keys(service); + return; err: @@ -2825,7 +2846,7 @@ upload_descriptor_to_hsdir(const hs_service_t *service, /* Let's avoid doing that if tor is configured to not publish. */ if (!get_options()->PublishHidServDescriptors) { log_info(LD_REND, "Service %s not publishing descriptor. " - "PublishHidServDescriptors is set to 1.", + "PublishHidServDescriptors is set to 0.", safe_str_client(service->onion_address)); goto end; } @@ -3042,13 +3063,85 @@ service_desc_hsdirs_changed(const hs_service_t *service, return should_reupload; } +/** These are all the reasons why a descriptor upload can't occur. We use + * those to log the reason properly with the right rate limiting and for the + * right descriptor. */ +typedef enum { + LOG_DESC_UPLOAD_REASON_MISSING_IPS = 0, + LOG_DESC_UPLOAD_REASON_IP_NOT_ESTABLISHED = 1, + LOG_DESC_UPLOAD_REASON_NOT_TIME = 2, + LOG_DESC_UPLOAD_REASON_NO_LIVE_CONSENSUS = 3, + LOG_DESC_UPLOAD_REASON_NO_DIRINFO = 4, +} log_desc_upload_reason_t; + +/** Maximum number of reasons. This is used to allocate the static array of + * all rate limiting objects. */ +#define LOG_DESC_UPLOAD_REASON_MAX LOG_DESC_UPLOAD_REASON_NO_DIRINFO + +/** Log the reason why we can't upload the given descriptor for the given + * service. This takes a message string (allocated by the caller) and a + * reason. + * + * Depending on the reason and descriptor, different rate limit applies. This + * is done because this function will basically be called every second. Each + * descriptor for each reason uses its own log rate limit object in order to + * avoid message suppression for different reasons and descriptors. */ +static void +log_cant_upload_desc(const hs_service_t *service, + const hs_service_descriptor_t *desc, const char *msg, + const log_desc_upload_reason_t reason) +{ + /* Writing the log every minute shouldn't be too annoying for log rate limit + * since this can be emitted every second for each descriptor. + * + * However, for one specific case, we increase it to 10 minutes because it + * is hit constantly, as an expected behavior, which is the reason + * indicating that it is not the time to upload. */ + static ratelim_t limits[2][LOG_DESC_UPLOAD_REASON_MAX + 1] = + { { RATELIM_INIT(60), RATELIM_INIT(60), RATELIM_INIT(60 * 10), + RATELIM_INIT(60), RATELIM_INIT(60) }, + { RATELIM_INIT(60), RATELIM_INIT(60), RATELIM_INIT(60 * 10), + RATELIM_INIT(60), RATELIM_INIT(60) }, + }; + bool is_next_desc = false; + unsigned int rlim_pos = 0; + ratelim_t *rlim = NULL; + + tor_assert(service); + tor_assert(desc); + tor_assert(msg); + + /* Make sure the reason value is valid. It should never happen because we + * control that value in the code flow but will be apparent during + * development if a reason is added but LOG_DESC_UPLOAD_REASON_NUM_ is not + * updated. */ + if (BUG(reason > LOG_DESC_UPLOAD_REASON_MAX)) { + return; + } + + /* Ease our life. Flag that tells us if the descriptor is the next one. */ + is_next_desc = (service->desc_next == desc); + + /* Current descriptor is the first element in the ratelimit object array. + * The next descriptor is the second element. */ + rlim_pos = (is_next_desc ? 1 : 0); + /* Get the ratelimit object for the reason _and_ right descriptor. */ + rlim = &limits[rlim_pos][reason]; + + log_fn_ratelim(rlim, LOG_INFO, LD_REND, + "Service %s can't upload its %s descriptor: %s", + safe_str_client(service->onion_address), + (is_next_desc) ? "next" : "current", msg); +} + /** Return 1 if the given descriptor from the given service can be uploaded * else return 0 if it can not. */ static int should_service_upload_descriptor(const hs_service_t *service, const hs_service_descriptor_t *desc, time_t now) { - unsigned int num_intro_points; + char *msg = NULL; + unsigned int num_intro_points, count_ip_established; tor_assert(service); tor_assert(desc); @@ -3068,34 +3161,54 @@ should_service_upload_descriptor(const hs_service_t *service, * upload descriptor in this case. We need at least one for the service to * be reachable. */ if (desc->missing_intro_points && num_intro_points == 0) { + msg = tor_strdup("Missing intro points"); + log_cant_upload_desc(service, desc, msg, + LOG_DESC_UPLOAD_REASON_MISSING_IPS); goto cannot; } /* Check if all our introduction circuit have been established for all the * intro points we have selected. */ - if (count_desc_circuit_established(desc) != num_intro_points) { + count_ip_established = count_desc_circuit_established(desc); + if (count_ip_established != num_intro_points) { + tor_asprintf(&msg, "Intro circuits aren't yet all established (%d/%d).", + count_ip_established, num_intro_points); + log_cant_upload_desc(service, desc, msg, + LOG_DESC_UPLOAD_REASON_IP_NOT_ESTABLISHED); goto cannot; } /* Is it the right time to upload? */ if (desc->next_upload_time > now) { + tor_asprintf(&msg, "Next upload time is %ld, it is now %ld.", + (long int) desc->next_upload_time, (long int) now); + log_cant_upload_desc(service, desc, msg, + LOG_DESC_UPLOAD_REASON_NOT_TIME); goto cannot; } /* Don't upload desc if we don't have a live consensus */ if (!networkstatus_get_live_consensus(now)) { + msg = tor_strdup("No live consensus"); + log_cant_upload_desc(service, desc, msg, + LOG_DESC_UPLOAD_REASON_NO_LIVE_CONSENSUS); goto cannot; } /* Do we know enough router descriptors to have adequate vision of the HSDir hash ring? */ if (!router_have_minimum_dir_info()) { + msg = tor_strdup("Not enough directory information"); + log_cant_upload_desc(service, desc, msg, + LOG_DESC_UPLOAD_REASON_NO_DIRINFO); goto cannot; } /* Can upload! */ return 1; + cannot: + tor_free(msg); return 0; } @@ -3369,7 +3482,7 @@ service_handle_introduce2(origin_circuit_t *circ, const uint8_t *payload, /* The following will parse, decode and launch the rendezvous point circuit. * Both current and legacy cells are handled. */ - if (hs_circ_handle_introduce2(service, circ, ip, desc->desc->subcredential, + if (hs_circ_handle_introduce2(service, circ, ip, &desc->desc->subcredential, payload, payload_len) < 0) { goto err; } @@ -3990,6 +4103,50 @@ hs_service_load_all_keys(void) return -1; } +/** Log the status of introduction points for all version 3 onion services + * at log severity <b>severity</b>. + */ +void +hs_service_dump_stats(int severity) +{ + origin_circuit_t *circ; + + FOR_EACH_SERVICE_BEGIN(hs) { + + tor_log(severity, LD_GENERAL, "Service configured in %s:", + service_escaped_dir(hs)); + FOR_EACH_DESCRIPTOR_BEGIN(hs, desc) { + + DIGEST256MAP_FOREACH(desc->intro_points.map, key, + hs_service_intro_point_t *, ip) { + const node_t *intro_node; + const char *nickname; + + intro_node = get_node_from_intro_point(ip); + if (!intro_node) { + tor_log(severity, LD_GENERAL, " Couldn't find intro point, " + "skipping"); + continue; + } + nickname = node_get_nickname(intro_node); + if (!nickname) { + continue; + } + + circ = hs_circ_service_get_intro_circ(ip); + if (!circ) { + tor_log(severity, LD_GENERAL, " Intro point at %s: no circuit", + nickname); + continue; + } + tor_log(severity, LD_GENERAL, " Intro point %s: circuit is %s", + nickname, circuit_state_to_string(circ->base_.state)); + } DIGEST256MAP_FOREACH_END; + + } FOR_EACH_DESCRIPTOR_END; + } FOR_EACH_SERVICE_END; +} + /** Put all service object in the given service list. After this, the caller * looses ownership of every elements in the list and responsible to free the * list pointer. */ @@ -4048,6 +4205,11 @@ hs_service_free_(hs_service_t *service) replaycache_free(service->state.replay_cache_rend_cookie); } + /* Free onionbalance subcredentials (if any) */ + if (service->state.ob_subcreds) { + tor_free(service->state.ob_subcreds); + } + /* Wipe service keys. */ memwipe(&service->keys.identity_sk, 0, sizeof(service->keys.identity_sk)); @@ -4100,6 +4262,7 @@ hs_service_free_all(void) { rend_service_free_all(); service_free_all(); + hs_config_free_all(); } #ifdef TOR_UNIT_TESTS diff --git a/src/feature/hs/hs_service.h b/src/feature/hs/hs_service.h index 8809411e01..b5bff5bee5 100644 --- a/src/feature/hs/hs_service.h +++ b/src/feature/hs/hs_service.h @@ -248,10 +248,14 @@ typedef struct hs_service_config_t { /** Does this service export the circuit ID of its clients? */ hs_circuit_id_protocol_t circuit_id_protocol; - /* DoS defenses. For the ESTABLISH_INTRO cell extension. */ + /** DoS defenses. For the ESTABLISH_INTRO cell extension. */ unsigned int has_dos_defense_enabled : 1; uint32_t intro_dos_rate_per_sec; uint32_t intro_dos_burst_per_sec; + + /** If set, contains the Onion Balance master ed25519 public key (taken from + * an .onion addresses) that this tor instance serves as backend. */ + smartlist_t *ob_master_pubkeys; } hs_service_config_t; /** Service state. */ @@ -275,6 +279,14 @@ typedef struct hs_service_state_t { /** When is the next time we should rotate our descriptors. This is has to be * done at the start time of the next SRV protocol run. */ time_t next_rotation_time; + + /* If this is an onionbalance instance, this is an array of subcredentials + * that should be used when decrypting an INTRO2 cell. If this is not an + * onionbalance instance, this is NULL. + * See [ONIONBALANCE] section in rend-spec-v3.txt for more details . */ + hs_subcredential_t *ob_subcreds; + /* Number of OB subcredentials */ + size_t n_ob_subcreds; } hs_service_state_t; /** Representation of a service running on this tor instance. */ @@ -300,9 +312,6 @@ typedef struct hs_service_t { hs_service_descriptor_t *desc_current; /** Next descriptor. */ hs_service_descriptor_t *desc_next; - - /* XXX: Credential (client auth.) #20700. */ - } hs_service_t; /** For the service global hash map, we define a specific type for it which @@ -364,6 +373,8 @@ void hs_service_upload_desc_to_dir(const char *encoded_desc, hs_circuit_id_protocol_t hs_service_exports_circuit_id(const ed25519_public_key_t *pk); +void hs_service_dump_stats(int severity); + #ifdef HS_SERVICE_PRIVATE #ifdef TOR_UNIT_TESTS @@ -375,6 +386,9 @@ STATIC hs_service_t *get_first_service(void); STATIC hs_service_intro_point_t *service_intro_point_find_by_ident( const hs_service_t *service, const hs_ident_circuit_t *ident); + +MOCK_DECL(STATIC unsigned int, count_desc_circuit_established, + (const hs_service_descriptor_t *desc)); #endif /* defined(TOR_UNIT_TESTS) */ /* Service accessors. */ diff --git a/src/feature/hs/include.am b/src/feature/hs/include.am index 5e69607e59..af1dc65585 100644 --- a/src/feature/hs/include.am +++ b/src/feature/hs/include.am @@ -13,6 +13,7 @@ LIBTOR_APP_A_SOURCES += \ src/feature/hs/hs_dos.c \ src/feature/hs/hs_ident.c \ src/feature/hs/hs_intropoint.c \ + src/feature/hs/hs_ob.c \ src/feature/hs/hs_service.c \ src/feature/hs/hs_stats.c @@ -30,6 +31,9 @@ noinst_HEADERS += \ src/feature/hs/hs_dos.h \ src/feature/hs/hs_ident.h \ src/feature/hs/hs_intropoint.h \ + src/feature/hs/hs_ob.h \ + src/feature/hs/hs_opts_st.h \ + src/feature/hs/hs_options.inc \ src/feature/hs/hs_service.h \ src/feature/hs/hs_stats.h \ src/feature/hs/hsdir_index_st.h |