onion encrypt path build frames

path build frames should be onioned at each hop to avoid a bad actor
controlling two nodes in a path being able to know (with certainty,
temporal correlation is hard to avoid) that they're hops on the same
path.  This is desirable as in the worst case someone could be your edge
hop and terminal hop on a path, and now the terminal hop knows your IP
making the path basically pointless.
This commit is contained in:
Thomas Winget 2023-11-14 17:06:10 -05:00
parent d7e2e52ee4
commit 2e5c856cf3
2 changed files with 135 additions and 102 deletions

View File

@ -1117,114 +1117,97 @@ 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_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<std::string>>(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()};
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");
// 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);
@ -1255,9 +1238,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
@ -1266,14 +1249,32 @@ 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(
reinterpret_cast<unsigned char*>(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(reinterpret_cast<uint8_t*>(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);