Files
opendht/src/transport.cpp
T
Adrien Beraud e78e54b8c5 transport: send keyless Data envelopes once the session is confirmed
Sessions are keyed by peer address, so the receiver finds the keys from the source address: Data envelopes drop both keys from the header (30 bytes of overhead instead of 94) and need no key derivation to reject. A session is confirmed when the peer answers our envelope request with the session keys, or sends Data itself. Credentials only travel in key-carrying envelopes, which replaces Peer::acked. A received Initial envelope unconfirms the session, and full session caches evict instead of refusing, so a peer that derived keys keeps them.
2026-10-29 23:28:48 -04:00

483 lines
14 KiB
C++

// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "transport.h"
#include <gnutls/crypto.h>
#include <nettle/chacha-poly1305.h>
#include <nettle/curve25519.h>
#include <nettle/eddsa.h>
#include <nettle/hmac.h>
#include <nettle/memops.h>
#include <nettle/sha2.h>
#include <nettle/version.h>
#include <algorithm>
#include <cstring>
#include <stdexcept>
#include <string_view>
namespace dht {
namespace transport {
using namespace std::literals;
namespace {
constexpr auto NODE_ID_CONTEXT = "opendht-node-id-v1"sv;
constexpr auto KDF_SALT = "opendht-transport-v2"sv;
constexpr auto CREDENTIAL_CONTEXT = "opendht-credential-v1"sv;
constexpr auto ADDRESS_KEY_CONTEXT = "opendht-address-key-v1"sv;
constexpr auto ADDRESS_TOKEN_CONTEXT = "opendht-address-token-v1"sv;
constexpr uint8_t CREDENTIAL_VERSION {1};
void
randomBytes(uint8_t* data, size_t size, gnutls_rnd_level_t level)
{
if (gnutls_rnd(level, data, size) != 0)
throw std::runtime_error("Unable to generate random data");
}
Key
hmacSha256(const uint8_t* key, size_t keyLen, const uint8_t* data, size_t dataLen)
{
struct hmac_sha256_ctx ctx;
hmac_sha256_set_key(&ctx, keyLen, key);
hmac_sha256_update(&ctx, dataLen, data);
Key out;
#if NETTLE_VERSION_MAJOR >= 4
hmac_sha256_digest(&ctx, out.data());
#else
hmac_sha256_digest(&ctx, out.size(), out.data());
#endif
return out;
}
/* HMAC-SHA256(secret, context || address) */
Key
addressPrf(const Key& secret, std::string_view context, const uint8_t* address, size_t size)
{
struct hmac_sha256_ctx ctx;
hmac_sha256_set_key(&ctx, secret.size(), secret.data());
hmac_sha256_update(&ctx, context.size(), (const uint8_t*) context.data());
hmac_sha256_update(&ctx, size, address);
Key out;
#if NETTLE_VERSION_MAJOR >= 4
hmac_sha256_digest(&ctx, out.data());
#else
hmac_sha256_digest(&ctx, out.size(), out.data());
#endif
gnutls_memset(&ctx, 0, sizeof(ctx));
return out;
}
void
chachaDigest(struct chacha_poly1305_ctx* ctx, uint8_t* tag)
{
#if NETTLE_VERSION_MAJOR >= 4
chacha_poly1305_digest(ctx, tag);
#else
chacha_poly1305_digest(ctx, TAG_SIZE, tag);
#endif
}
void
writeU64(uint8_t* p, uint64_t v)
{
for (int i = 7; i >= 0; --i, v >>= 8)
p[i] = v & 0xFF;
}
uint64_t
readU64(const uint8_t* p)
{
uint64_t v {0};
for (int i = 0; i < 8; ++i)
v = (v << 8) | p[i];
return v;
}
constexpr size_t SIGNED_SIZE {CREDENTIAL_CONTEXT.size() + 1 + KEY_SIZE + 8};
std::array<uint8_t, SIGNED_SIZE>
signedData(const Key& key, uint64_t expiration)
{
std::array<uint8_t, SIGNED_SIZE> data;
auto p = std::copy(CREDENTIAL_CONTEXT.begin(), CREDENTIAL_CONTEXT.end(), data.begin());
*p++ = CREDENTIAL_VERSION;
p = std::copy(key.begin(), key.end(), p);
writeU64(&*p, expiration);
return data;
}
uint64_t
toSeconds(SystemTime t)
{
auto s = std::chrono::duration_cast<std::chrono::seconds>(t.time_since_epoch()).count();
return s > 0 ? static_cast<uint64_t>(s) : 0;
}
std::optional<Key>
dh(const Key& secret, const Key& pub)
{
Key out;
curve25519_mul(out.data(), secret.data(), pub.data());
if (out == Key {})
return std::nullopt;
return out;
}
/* Key derivation input, wiped on destruction */
struct KdfBuffer
{
std::array<uint8_t, 4 + 1 + 4 * KEY_SIZE + 1> data;
size_t size {0};
KdfBuffer& add(const uint8_t* d, size_t len)
{
std::memcpy(data.data() + size, d, len);
size += len;
return *this;
}
KdfBuffer& add(const Key& k) { return add(k.data(), k.size()); }
KdfBuffer& add(uint8_t b) { return add(&b, 1); }
KdfBuffer& add(NetId network)
{
for (int i = 0; i < 4; ++i)
add(static_cast<uint8_t>((network >> (24 - 8 * i)) & 0xFF));
return *this;
}
~KdfBuffer() { gnutls_memset(data.data(), 0, data.size()); }
};
/* HKDF-SHA256 with a single output block */
Key
hkdf(const KdfBuffer& ikm, KdfBuffer info)
{
auto prk = hmacSha256((const uint8_t*) KDF_SALT.data(), KDF_SALT.size(), ikm.data.data(), ikm.size);
info.add(uint8_t {1});
auto out = hmacSha256(prk.data(), prk.size(), info.data.data(), info.size);
wipe(prk);
return out;
}
KdfBuffer
kdfInfo(NetId network, EnvelopeType type, const Key& sender, const Key& senderEphemeral, const Key& receiver)
{
KdfBuffer info;
info.add(network).add(static_cast<uint8_t>(type)).add(sender).add(senderEphemeral).add(receiver);
return info;
}
void
wipe(std::optional<Key>& k)
{
if (k)
transport::wipe(*k);
}
} // namespace
KeyPair
KeyPair::generate()
{
Key secret = randomSecret();
auto kp = fromSecret(secret);
wipe(secret);
return kp;
}
KeyPair
KeyPair::fromSecret(const Key& secret)
{
KeyPair kp;
kp.secret = secret;
curve25519_mul_g(kp.pub.data(), secret.data());
return kp;
}
InfoHash
nodeId(const Key& pub)
{
struct sha256_ctx ctx;
sha256_init(&ctx);
sha256_update(&ctx, NODE_ID_CONTEXT.size(), (const uint8_t*) NODE_ID_CONTEXT.data());
sha256_update(&ctx, pub.size(), pub.data());
std::array<uint8_t, SHA256_DIGEST_SIZE> digest;
#if NETTLE_VERSION_MAJOR >= 4
sha256_digest(&ctx, digest.data());
#else
sha256_digest(&ctx, digest.size(), digest.data());
#endif
return InfoHash(digest.data(), HASH_LEN);
}
Key
randomSecret()
{
Key secret;
randomBytes(secret.data(), secret.size(), GNUTLS_RND_KEY);
return secret;
}
KeyPair
addressKey(const Key& epochSecret, const uint8_t* address, size_t size)
{
auto secret = addressPrf(epochSecret, ADDRESS_KEY_CONTEXT, address, size);
auto kp = KeyPair::fromSecret(secret);
wipe(secret);
return kp;
}
Key
addressToken(const Key& epochSecret, const uint8_t* address, size_t size)
{
return addressPrf(epochSecret, ADDRESS_TOKEN_CONTEXT, address, size);
}
Blob
Credential::pack() const
{
Blob b(PACKED_SIZE);
b[0] = CREDENTIAL_VERSION;
std::copy(key.begin(), key.end(), b.begin() + 1);
writeU64(b.data() + 1 + KEY_SIZE, toSeconds(expiration));
std::copy(signature.begin(), signature.end(), b.begin() + 1 + KEY_SIZE + 8);
return b;
}
Credential
Credential::unpack(const uint8_t* data, size_t size)
{
if (size != PACKED_SIZE or data[0] != CREDENTIAL_VERSION)
throw std::invalid_argument("Invalid transport credential");
Credential c;
std::copy_n(data + 1, KEY_SIZE, c.key.begin());
c.expiration = SystemTime(std::chrono::seconds(readU64(data + 1 + KEY_SIZE)));
std::copy_n(data + 1 + KEY_SIZE + 8, SIGNATURE_SIZE, c.signature.begin());
return c;
}
bool
Credential::verify(const std::vector<Key>& authorities, SystemTime now) const
{
if (now >= expiration)
return false;
auto data = signedData(key, toSeconds(expiration));
return std::any_of(authorities.begin(), authorities.end(), [&](const Key& authority) {
return ed25519_sha512_verify(authority.data(), data.size(), data.data(), signature.data()) == 1;
});
}
Authority
Authority::generate()
{
Key secret;
randomBytes(secret.data(), secret.size(), GNUTLS_RND_KEY);
auto a = fromSecret(secret);
wipe(secret);
return a;
}
Authority
Authority::fromSecret(const Key& secret)
{
Authority a;
a.secret = secret;
ed25519_sha512_public_key(a.pub.data(), secret.data());
return a;
}
Credential
Authority::issue(const Key& transportKey, SystemTime expiration) const
{
Credential c;
c.key = transportKey;
c.expiration = SystemTime(std::chrono::seconds(toSeconds(expiration)));
auto data = signedData(c.key, toSeconds(c.expiration));
ed25519_sha512_sign(pub.data(), secret.data(), data.size(), data.data(), c.signature.data());
return c;
}
std::optional<SessionKeys>
sessionKeys(const KeyPair& own, const KeyPair& ephemeral, const Key& peer, const Key& peerEphemeral, NetId network)
{
auto ee = dh(ephemeral.secret, peerEphemeral);
auto es = dh(ephemeral.secret, peer);
auto se = dh(own.secret, peerEphemeral);
auto ss = dh(own.secret, peer);
std::optional<SessionKeys> keys;
if (ee and es and se and ss) {
// The DH(sender, receiver) terms in order ee, es, se, ss, seen from each side
KdfBuffer tx, rx;
tx.add(*ee).add(*es).add(*se).add(*ss);
rx.add(*ee).add(*se).add(*es).add(*ss);
keys = SessionKeys {
hkdf(tx, kdfInfo(network, EnvelopeType::Session, own.pub, ephemeral.pub, peer).add(peerEphemeral)),
hkdf(rx, kdfInfo(network, EnvelopeType::Session, peer, peerEphemeral, own.pub).add(ephemeral.pub))};
}
for (auto* k : {&ee, &es, &se, &ss})
wipe(*k);
return keys;
}
std::optional<Key>
initialSendKey(const KeyPair& own, const KeyPair& ephemeral, const Key& peer, NetId network)
{
auto es = dh(ephemeral.secret, peer);
auto ss = dh(own.secret, peer);
std::optional<Key> key;
if (es and ss) {
KdfBuffer ikm;
ikm.add(*es).add(*ss);
key = hkdf(ikm, kdfInfo(network, EnvelopeType::Initial, own.pub, ephemeral.pub, peer));
}
wipe(es);
wipe(ss);
return key;
}
std::optional<Key>
initialReceiveKey(const KeyPair& own, const Key& peer, const Key& peerEphemeral, NetId network)
{
auto es = dh(own.secret, peerEphemeral);
auto ss = dh(own.secret, peer);
std::optional<Key> key;
if (es and ss) {
KdfBuffer ikm;
ikm.add(*es).add(*ss);
key = hkdf(ikm, kdfInfo(network, EnvelopeType::Initial, peer, peerEphemeral, own.pub));
}
wipe(es);
wipe(ss);
return key;
}
std::optional<Header>
parseHeader(const uint8_t* data, size_t size)
{
if (not isEnvelope(data, size))
return std::nullopt;
const uint8_t type = data[1] & ~CREDENTIAL_FLAG;
Header header {};
header.credential = data[1] & CREDENTIAL_FLAG;
if (type == static_cast<uint8_t>(EnvelopeType::Data)) {
if (header.credential)
return std::nullopt;
header.type = EnvelopeType::Data;
return header;
}
if (type < static_cast<uint8_t>(EnvelopeType::Initial) or type > static_cast<uint8_t>(EnvelopeType::Hello)
or size < HEADER_SIZE)
return std::nullopt;
header.type = static_cast<EnvelopeType>(type);
std::copy_n(data + 2, KEY_SIZE, header.sender.begin());
std::copy_n(data + 2 + KEY_SIZE, KEY_SIZE, header.ephemeral.begin());
return header;
}
namespace {
/* Appends the nonce and the encryption of `credential || data` to the authenticated header in `out` */
Blob
sealWith(const Key& key, Blob out, const Blob& credential, const uint8_t* data, size_t size)
{
const size_t headerSize = out.size();
const size_t len = credential.size() + size;
out.resize(headerSize + NONCE_SIZE + len + TAG_SIZE);
auto nonce = out.data() + headerSize;
randomBytes(nonce, NONCE_SIZE, GNUTLS_RND_NONCE);
auto payload = nonce + NONCE_SIZE;
std::copy(credential.begin(), credential.end(), payload);
if (size)
std::memcpy(payload + credential.size(), data, size);
struct chacha_poly1305_ctx ctx;
chacha_poly1305_set_key(&ctx, key.data());
chacha_poly1305_set_nonce(&ctx, nonce);
chacha_poly1305_update(&ctx, headerSize, out.data());
chacha_poly1305_encrypt(&ctx, len, payload, payload);
chachaDigest(&ctx, payload + len);
gnutls_memset(&ctx, 0, sizeof(ctx));
return out;
}
} // namespace
Blob
seal(const Key& key,
EnvelopeType type,
const Key& sender,
const Key& ephemeral,
const uint8_t* data,
size_t size,
const Blob& credential)
{
Blob header;
header.reserve(MAX_ENVELOPE_OVERHEAD + size);
header.push_back(ENVELOPE_MARKER);
header.push_back(static_cast<uint8_t>(type) | (credential.empty() ? 0 : CREDENTIAL_FLAG));
header.insert(header.end(), sender.begin(), sender.end());
header.insert(header.end(), ephemeral.begin(), ephemeral.end());
return sealWith(key, std::move(header), credential, data, size);
}
Blob
sealData(const Key& key, const uint8_t* data, size_t size)
{
Blob header;
header.reserve(DATA_ENVELOPE_OVERHEAD + size);
header.push_back(ENVELOPE_MARKER);
header.push_back(static_cast<uint8_t>(EnvelopeType::Data));
return sealWith(key, std::move(header), {}, data, size);
}
std::optional<Blob>
open(const Key& key, const uint8_t* data, size_t size)
{
auto header = parseHeader(data, size);
if (not header or header->type == EnvelopeType::Hello)
return std::nullopt;
const size_t headerSize = header->type == EnvelopeType::Data ? DATA_HEADER_SIZE : HEADER_SIZE;
if (size < headerSize + NONCE_SIZE + TAG_SIZE)
return std::nullopt;
const size_t len = size - headerSize - NONCE_SIZE - TAG_SIZE;
const auto nonce = data + headerSize;
struct chacha_poly1305_ctx ctx;
chacha_poly1305_set_key(&ctx, key.data());
chacha_poly1305_set_nonce(&ctx, nonce);
chacha_poly1305_update(&ctx, headerSize, data);
Blob out(len);
chacha_poly1305_decrypt(&ctx, len, out.data(), nonce + NONCE_SIZE);
std::array<uint8_t, TAG_SIZE> tag;
chachaDigest(&ctx, tag.data());
gnutls_memset(&ctx, 0, sizeof(ctx));
if (not memeql_sec(tag.data(), data + size - TAG_SIZE, TAG_SIZE))
return std::nullopt;
return out;
}
Blob
hello(const Key& sender, const Key& ephemeral, const Blob& credential, const uint8_t* data, size_t size)
{
Blob out;
out.reserve(HEADER_SIZE + credential.size() + size);
out.push_back(ENVELOPE_MARKER);
out.push_back(static_cast<uint8_t>(EnvelopeType::Hello));
out.insert(out.end(), sender.begin(), sender.end());
out.insert(out.end(), ephemeral.begin(), ephemeral.end());
out.insert(out.end(), credential.begin(), credential.end());
out.insert(out.end(), data, data + size);
return out;
}
void
wipe(Key& key)
{
gnutls_memset(key.data(), 0, key.size());
}
} // namespace transport
} // namespace dht