Merge pull request #2223 from tewinget/path-build-correctly

onion encrypt path build frames
This commit is contained in:
dr7ana 2023-11-27 09:26:46 -08:00 committed by GitHub
commit 28047ae72f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 124 additions and 106 deletions

View File

@ -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<std::deque<ustring>>(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<std::string>("HASH");
frame = frame_info.require<std::string>("FRAME");
oxenc::bt_dict_consumer hop_dict{frame};
hop_payload = frame_info.require<std::string>("ENCRYPTED");
outer_nonce = frame_info.require<ustring>("NONCE");
other_pubkey = frame_info.require<ustring>("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<unsigned char*>(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<const unsigned char*>(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<unsigned char*>(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<std::string>("COMMKEY");
lifetime = hop_info.require<uint64_t>("LIFETIME");
inner_nonce = hop_info.require<ustring>("NONCE");
rx_id = hop_info.require<std::string>("RX");
tx_id = hop_info.require<std::string>("TX");
upstream = hop_info.require<std::string>("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<ustring>("HASH");
auto frame = frame_info.require<ustring>("FRAME");
oxenc::bt_dict_consumer hop_dict{frame};
auto hop_payload = hop_dict.require<ustring>("ENCRYPTED");
auto outer_nonce = hop_dict.require<ustring>("NONCE");
auto other_pubkey = hop_dict.require<ustring>("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<std::string>("COMMKEY");
auto lifetime = hop_info.require<uint64_t>("LIFETIME");
auto inner_nonce = hop_info.require<ustring>("NONCE");
auto rx_id = hop_info.require<std::string>("RX");
auto tx_id = hop_info.require<std::string>("TX");
auto upstream = hop_info.require<std::string>("UPSTREAM");
// populate transit hop object with hop info
// TODO: IP / path build limiting clients
auto hop = std::make_shared<path::TransitHop>();
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<oxenc::bt_list>(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)
{

View File

@ -429,40 +429,72 @@ namespace llarp
path_cat, "{} building path -> {} : {}", Name(), path->ShortName(), path->HopsString());
oxenc::bt_list_producer frames;
std::vector<std::string> 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<unsigned char*>(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<uint8_t*>(dummy.data()), dummy.size());
frames.append(dummy);
frame_str[i].resize(last_len);
randombytes(reinterpret_cast<uint8_t*>(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);