diff --git a/llarp/link/link_manager.cpp b/llarp/link/link_manager.cpp index 9b1444d3b..6c6a10c48 100644 --- a/llarp/link/link_manager.cpp +++ b/llarp/link/link_manager.cpp @@ -1119,114 +1119,87 @@ namespace llarp m.respond(serialize_response({{messages::STATUS_KEY, PathBuildMessage::NO_TRANSIT}}), true); return; } + try { - std::string payload{m.body()}, frame_payload; - std::string frame, hash, hop_payload, commkey, rx_id, tx_id, upstream; - ustring other_pubkey, outer_nonce, inner_nonce; - uint64_t lifetime; - - try + auto payload_list = oxenc::bt_deserialize>(m.body()); + if (payload_list.size() != path::MAX_LEN) { - oxenc::bt_list_consumer btlc{payload}; - frame_payload = btlc.consume_string(); - - oxenc::bt_dict_consumer frame_info{frame_payload}; - hash = frame_info.require("HASH"); - frame = frame_info.require("FRAME"); - - oxenc::bt_dict_consumer hop_dict{frame}; - hop_payload = frame_info.require("ENCRYPTED"); - outer_nonce = frame_info.require("NONCE"); - other_pubkey = frame_info.require("PUBKEY"); - - SharedSecret shared; - // derive shared secret using ephemeral pubkey and our secret key (and nonce) - if (!crypto::dh_server( - shared.data(), other_pubkey.data(), _router.pubkey(), inner_nonce.data())) - { - log::info(link_cat, "DH server initialization failed during path build"); - m.respond( - serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_CRYPTO}}), true); - return; - } - - // hash data and check against given hash - ShortHash digest; - if (!crypto::hmac( - digest.data(), - reinterpret_cast(frame.data()), - frame.size(), - shared)) - { - log::error(link_cat, "HMAC failed on path build request"); - m.respond( - serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_CRYPTO}}), true); - return; - } - if (!std::equal( - digest.begin(), digest.end(), reinterpret_cast(hash.data()))) - { - log::info(link_cat, "HMAC mismatch on path build request"); - m.respond( - serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_CRYPTO}}), true); - return; - } - - // decrypt frame with our hop info - if (!crypto::xchacha20( - reinterpret_cast(hop_payload.data()), - hop_payload.size(), - shared.data(), - outer_nonce.data())) - { - log::info(link_cat, "Decrypt failed on path build request"); - m.respond( - serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_CRYPTO}}), true); - return; - } - - oxenc::bt_dict_consumer hop_info{hop_payload}; - commkey = hop_info.require("COMMKEY"); - lifetime = hop_info.require("LIFETIME"); - inner_nonce = hop_info.require("NONCE"); - rx_id = hop_info.require("RX"); - tx_id = hop_info.require("TX"); - upstream = hop_info.require("UPSTREAM"); - } - catch (...) - { - log::warning(link_cat, "Error: failed to deserialize path build message"); - throw; - } - - if (frame.empty()) - { - log::info(link_cat, "Path build request received invalid frame"); + log::info(link_cat, "Path build message with wrong number of frames"); m.respond(serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_FRAMES}}), true); return; } + oxenc::bt_dict_consumer frame_info{payload_list.front()}; + auto hash = frame_info.require("HASH"); + auto frame = frame_info.require("FRAME"); + + oxenc::bt_dict_consumer hop_dict{frame}; + auto hop_payload = hop_dict.require("ENCRYPTED"); + auto outer_nonce = hop_dict.require("NONCE"); + auto other_pubkey = hop_dict.require("PUBKEY"); + + SharedSecret shared; + // derive shared secret using ephemeral pubkey and our secret key (and nonce) + if (!crypto::dh_server( + shared.data(), other_pubkey.data(), _router.pubkey(), outer_nonce.data())) + { + log::info(link_cat, "DH server initialization failed during path build"); + m.respond(serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_CRYPTO}}), true); + return; + } + + // hash data and check against given hash + ShortHash digest; + if (!crypto::hmac(digest.data(), frame.data(), frame.size(), shared)) + { + log::error(link_cat, "HMAC failed on path build request"); + m.respond(serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_CRYPTO}}), true); + return; + } + if (!std::equal(digest.begin(), digest.end(), hash.data())) + { + log::info(link_cat, "HMAC mismatch on path build request"); + m.respond(serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_CRYPTO}}), true); + return; + } + + // decrypt frame with our hop info + if (!crypto::xchacha20( + hop_payload.data(), hop_payload.size(), shared.data(), outer_nonce.data())) + { + log::info(link_cat, "Decrypt failed on path build request"); + m.respond(serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_CRYPTO}}), true); + return; + } + + oxenc::bt_dict_consumer hop_info{hop_payload}; + auto commkey = hop_info.require("COMMKEY"); + auto lifetime = hop_info.require("LIFETIME"); + auto inner_nonce = hop_info.require("NONCE"); + auto rx_id = hop_info.require("RX"); + auto tx_id = hop_info.require("TX"); + auto upstream = hop_info.require("UPSTREAM"); + // populate transit hop object with hop info // TODO: IP / path build limiting clients auto hop = std::make_shared(); hop->info.downstream = from; // extract pathIDs and check if zero or used - auto& hop_info = hop->info; - hop_info.txID.from_string(tx_id); - hop_info.rxID.from_string(rx_id); + hop->info.txID.from_string(tx_id); + hop->info.rxID.from_string(rx_id); - if (hop_info.txID.IsZero() || hop_info.rxID.IsZero()) + if (hop->info.txID.IsZero() || hop->info.rxID.IsZero()) { log::warning(link_cat, "Invalid PathID; PathIDs must be non-zero"); m.respond(serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_PATHID}}), true); return; } - hop_info.upstream.from_string(upstream); + hop->info.upstream.from_string(upstream); - if (_router.path_context().HasTransitHop(hop_info)) + if (_router.path_context().HasTransitHop(hop->info)) { log::warning(link_cat, "Invalid PathID; PathIDs must be unique"); m.respond(serialize_response({{messages::STATUS_KEY, PathBuildMessage::BAD_PATHID}}), true); @@ -1257,9 +1230,9 @@ namespace llarp } hop->started = _router.now(); - _router.persist_connection_until(hop_info.downstream, hop->ExpireTime() + 10s); + _router.persist_connection_until(hop->info.downstream, hop->ExpireTime() + 10s); - if (hop_info.upstream == _router.pubkey()) + if (hop->info.upstream == _router.pubkey()) { hop->terminal_hop = true; // we are terminal hop and everything is okay @@ -1268,14 +1241,27 @@ namespace llarp return; } - // rotate our frame to the end of the list and forward upstream - auto payload_list = oxenc::bt_deserialize(payload); - payload_list.splice(payload_list.end(), payload_list, payload_list.begin()); + // pop our frame, to be randomized after onion step and appended + auto end_frame = std::move(payload_list.front()); + payload_list.pop_front(); + auto onion_nonce = SymmNonce{inner_nonce.data()} ^ hop->nonceXOR; + // (de-)onion each further frame using the established shared secret and + // onion_nonce = inner_nonce ^ nonceXOR + // Note: final value passed to crypto::onion is xor factor, but that's for *after* the + // onion round to compute the return value, so we don't care about it. + for (auto& element : payload_list) + { + crypto::onion(element.data(), element.size(), hop->pathKey, onion_nonce, onion_nonce); + } + // randomize final frame. could probably paste our frame on the end and onion it with the + // rest, but it gains nothing over random. + randombytes(end_frame.data(), end_frame.size()); + payload_list.push_back(std::move(end_frame)); send_control_message( hop->info.upstream, "path_build", - bt_serialize(payload_list), + oxenc::bt_serialize(payload_list), [hop, this, prev_message = std::move(m)](oxen::quic::message m) { if (m) { diff --git a/llarp/path/pathbuilder.cpp b/llarp/path/pathbuilder.cpp index f84de9d25..2f51e5fae 100644 --- a/llarp/path/pathbuilder.cpp +++ b/llarp/path/pathbuilder.cpp @@ -429,40 +429,72 @@ namespace llarp path_cat, "{} building path -> {} : {}", Name(), path->ShortName(), path->HopsString()); oxenc::bt_list_producer frames; - + std::vector frame_str(path::MAX_LEN); auto& path_hops = path->hops; size_t n_hops = path_hops.size(); size_t last_len{0}; - for (size_t i = 0; i < n_hops; i++) + // each hop will be able to read the outer part of its frame and decrypt + // the inner part with that information. It will then do an onion step on the + // remaining frames so the next hop can read the outer part of its frame, + // and so on. As this de-onion happens from hop 1 to n, we create and onion + // the frames from hop n downto 1 (i.e. reverse order). The first frame is + // not onioned. + // + // Onion-ing the frames in this way will prevent relays controlled by + // the same entity from knowing they are part of the same path + // (unless they're adjacent in the path; nothing we can do about that obviously). + + // i from n_hops downto 0 + size_t i = n_hops; + while (i > 0) { + i--; bool lastHop = (i == (n_hops - 1)); const auto& nextHop = lastHop ? path_hops[i].rc.router_id() : path_hops[i + 1].rc.router_id(); PathBuildMessage::setup_hop_keys(path_hops[i], nextHop); - auto frame_str = PathBuildMessage::serialize(path_hops[i]); + frame_str[i] = PathBuildMessage::serialize(path_hops[i]); // all frames should be the same length...not sure what that is yet + // it may vary if path lifetime is non-default, as that is encoded as an + // integer in decimal, but it should be constant for a given path if (last_len != 0) - assert(frame_str.size() == last_len); + assert(frame_str[i].size() == last_len); - last_len = frame_str.size(); - frames.append(std::move(frame_str)); + last_len = frame_str[i].size(); + + // onion each previously-created frame using the established shared secret and + // onion_nonce = path_hops[i].nonce ^ path_hops[i].nonceXOR, which the transit hop + // will have recovered after decrypting its frame. + // Note: final value passed to crypto::onion is xor factor, but that's for *after* the + // onion round to compute the return value, so we don't care about it. + for (size_t j = n_hops - 1; j > i; j--) + { + auto onion_nonce = path_hops[i].nonce ^ path_hops[i].nonceXOR; + crypto::onion( + reinterpret_cast(frame_str[j].data()), + frame_str[j].size(), + path_hops[i].shared, + onion_nonce, + onion_nonce); + } } std::string dummy; dummy.reserve(last_len); - // append dummy frames; path build request must always have MAX_LEN frames - // TODO: with the data structured as it is now (bt-encoded dict as each frame) - // the dummy frames can't be completely random; they need to look like - // normal frames - for (size_t i = 0; i < path::MAX_LEN - n_hops; i++) + for (i = n_hops; i < path::MAX_LEN; i++) { - randombytes(reinterpret_cast(dummy.data()), dummy.size()); - frames.append(dummy); + frame_str[i].resize(last_len); + randombytes(reinterpret_cast(frame_str[i].data()), frame_str[i].size()); + } + + for (auto& str : frame_str) // NOLINT + { + frames.append(std::move(str)); } router->path_context().AddOwnPath(GetSelf(), path);