mirror of
https://github.com/savoirfairelinux/opendht.git
synced 2026-11-08 04:21:51 +00:00
transport: make private network grants permanent
This commit is contained in:
1 parent
fa78704062
commit
ccb0b4340e
9 files changed
+84
-111
No files matched your search
+47
-32
@@ -12,6 +12,7 @@ Implementation: [include/opendht/transport.h](../include/opendht/transport.h), [
|
||||
- **Authenticated node IDs**: a node can only send encrypted messages under the node ID that derives from its own key.
|
||||
- **Backward compatibility**: nodes without this feature keep working with new nodes, in plaintext.
|
||||
- **Optional private networks**: when configured, a node only talks to nodes holding a credential signed by a trusted authority.
|
||||
- **Permanent membership**: grants do not expire. Without a compromise or an intentional membership change, no grant renewal, authority rotation, or operator maintenance is required.
|
||||
- **Forward secrecy**: recorded traffic stays confidential after a node's long-term key is compromised, once the ephemeral keys used for it are erased (at most one hour later). The first packet to a peer has weaker guarantees ([§5.2](#52-key-schedule)).
|
||||
- **Address validation**: a Session or Data envelope proves that its sender receives packets at its source address. It replaces the legacy write tokens between encrypted nodes ([§6.6](#66-write-authorization)).
|
||||
- **Low cost**: routing discovery also establishes the session, with a few X25519 operations per peer every 30 minutes. Once a session is established, 30 bytes of overhead per packet, minus the 40-byte token in messages that carried one.
|
||||
@@ -28,7 +29,7 @@ Implementation: [include/opendht/transport.h](../include/opendht/transport.h), [
|
||||
|---|---|
|
||||
| `a ‖ b` | Concatenation of byte strings |
|
||||
| `"text"` | ASCII bytes, without terminator |
|
||||
| `u32be(x)`, `u64be(x)` | Unsigned integer, big-endian, 4 or 8 bytes |
|
||||
| `u32be(x)` | Unsigned integer, big-endian, 4 bytes |
|
||||
| `sk_A`, `pk_A` | Static transport secret and public key of node A |
|
||||
| `esk_A`, `epk_A` | Ephemeral secret and public key of node A for the address of its peer |
|
||||
| `DH(x, Y)` | `X25519(x, Y)` |
|
||||
@@ -81,27 +82,28 @@ When encryption is disabled (`TransportConfig::enabled = false`), the node uses
|
||||
|
||||
An authority is an Ed25519 key pair. Its secret is the 32-byte Ed25519 seed. Its public key identifies the private network, and nodes are configured with a list of trusted authority public keys (`TransportConfig::authorities`). A network is private if and only if this list is non-empty.
|
||||
|
||||
Any one trusted authority can grant membership. A node can trust multiple authorities simultaneously, allowing old and replacement grants to coexist during an explicit transition. There is no scheduled authority rotation.
|
||||
|
||||
### 4.4 Credential (private networks)
|
||||
|
||||
A credential is an authority's signature binding a transport public key to an expiration time. It is 105 bytes:
|
||||
A membership grant, called a credential in the API and CLI, is an authority's signature over a transport public key. It never expires and contains no timestamp. It is 97 bytes:
|
||||
|
||||
```
|
||||
offset size field
|
||||
0 1 version = 0x01
|
||||
0 1 version = 0x02
|
||||
1 32 pk transport public key of the node
|
||||
33 8 expiry u64be, seconds since the Unix epoch
|
||||
41 64 signature Ed25519 signature by the authority
|
||||
33 64 signature Ed25519 signature by the authority
|
||||
```
|
||||
|
||||
The signature covers:
|
||||
|
||||
```
|
||||
"opendht-credential-v1" ‖ 0x01 ‖ pk ‖ u64be(expiry)
|
||||
"opendht-credential-v2" ‖ 0x02 ‖ pk
|
||||
```
|
||||
|
||||
A credential is **valid** at time `t` when both of these hold:
|
||||
- `t < expiry`, using the verifier's system clock;
|
||||
- the signature verifies under at least one trusted authority.
|
||||
A credential is **valid** when its signature verifies under at least one trusted authority. Admission does not depend on a system clock. The same grant can be reused across restarts and session-key rotations without an update.
|
||||
|
||||
Version 1 expiring credentials are rejected, not reinterpreted as permanent grants. Migrating from that format requires issuing a version 2 grant once.
|
||||
|
||||
Credentials are not bound to a network ID. An authority key therefore defines a network: use distinct authorities for distinct networks.
|
||||
|
||||
@@ -194,8 +196,8 @@ offset size field
|
||||
|
||||
- The ciphertext is `ChaCha20-Poly1305(K_sender→receiver, nonce, plaintext, aad = header)`.
|
||||
- The header is bytes `[0, 66)`, or `[0, 2)` for Data: marker, type, and the sender keys. Because it is the associated data, an attacker cannot change the type, the credential flag or the sender keys without the tag check failing.
|
||||
- The plaintext is the msgpack-encoded DHT message. With the credential flag, it is preceded by the sender's 105-byte credential.
|
||||
- The overhead is 30 bytes for Data, 94 bytes for Initial and Session, or 199 bytes with a credential.
|
||||
- The plaintext is the msgpack-encoded DHT message. With the credential flag, it is preceded by the sender's 97-byte credential.
|
||||
- The overhead is 30 bytes for Data, 94 bytes for Initial and Session, or 191 bytes with a credential.
|
||||
- Nonces are 96 bits from the system CSPRNG. For `n` packets under one key, the probability of a nonce collision is about $n^2 / 2^{97}$. Keys change at least every epoch, so this is negligible.
|
||||
|
||||
`0xC1` is the one byte value that msgpack never uses. Every legacy OpenDHT message is a msgpack map, which starts with `0x80`–`0x8F` or `0xDE`–`0xDF`. This marker therefore unambiguously separates envelopes from plaintext messages:
|
||||
@@ -207,7 +209,7 @@ offset size field
|
||||
`Hello` introduces a node to a peer whose key is unknown. This happens when pinging a bare address, typically a bootstrap node. A Hello is **not encrypted**:
|
||||
|
||||
```
|
||||
0xC1 ‖ 0x03 ‖ pk_sender ‖ epk_sender ‖ credential (105 bytes) ‖ msgpack ping
|
||||
0xC1 ‖ 0x03 ‖ pk_sender ‖ epk_sender ‖ credential (97 bytes) ‖ msgpack ping
|
||||
```
|
||||
|
||||
The sender cannot encrypt to a key it doesn't know, and X25519 keys cannot sign, so **a Hello proves nothing about its sender**. Its credential travels in clear and can be copied. The receiver therefore:
|
||||
@@ -262,7 +264,7 @@ The confirmation is lost when the session keys change: on epoch rotation, or whe
|
||||
|
||||
**Value searches.** A cold peer first receives `find_node(search_target)`. Its reply supplies its ephemeral and closer candidates in the same exchange. The existing distance-ordered search dispatch contacts closer candidates immediately, within the existing four-active-peer limit; it does not wait for a SELECT reply from the farther responder. This routing probe has separate state: it cannot satisfy a GET, pagination, or an empty-result callback. Responders remain eligible for subsequent value retrieval. A cold holder typically needs three exchanges (`find_node`, SELECT IDs, GET by ID), while a session-ready holder retains its usual two. A routing probe's first timeout frees its active search slot while protocol retries continue, rather than waiting for final expiration.
|
||||
|
||||
Value data is fragmented so that each packet fits in the 1280-byte IPv6 minimum MTU: `1280 − 40 − 8 = 1232` bytes of UDP payload, less 48 bytes reserved for the msgpack framing of a fragment, less the overhead of the envelope the fragments are sent in. Fragments carry `1184` bytes of value data in plaintext, `1154` in Data envelopes, `1090` in Initial or Session envelopes, and `985` with a credential.
|
||||
Value data is fragmented so that each packet fits in the 1280-byte IPv6 minimum MTU: `1280 − 40 − 8 = 1232` bytes of UDP payload, less 48 bytes reserved for the msgpack framing of a fragment, less the overhead of the envelope the fragments are sent in. Fragments carry `1184` bytes of value data in plaintext, `1154` in Data envelopes, `1090` in Initial or Session envelopes, and `993` with a credential.
|
||||
|
||||
This sizes value fragments only. Complete replies and node lists are not yet subject to a whole-datagram MTU budget; node-list trimming and packet-size enforcement remain separate work.
|
||||
|
||||
@@ -283,11 +285,11 @@ For each received packet, a node applies the following checks in order:
|
||||
5. **Session:** first try the keys cached in each epoch for (source address, `pk_sender`, `epk_sender`). Otherwise, apply the public-key limit, then for our current epoch then the previous one: derive the keys with our ephemeral key for the source address, and decrypt. Keep newly derived keys pending until the packet passes authorization and message validation.
|
||||
6. **Data:** decrypt with the session cached for the source address in our current epoch, then in the previous one. Its `pk_sender` and `epk_sender` are those of the session. This needs no key derivation and no public-key limit.
|
||||
7. Drop the packet if it was not decrypted.
|
||||
8. **Credential flag:** strip the 105-byte credential. In a private network, verify it as for Hello, unless the peer is already authorized; on success, the peer is authorized until the credential expires.
|
||||
**No credential:** in a private network, drop the packet unless the peer is currently authorized.
|
||||
8. **Credential flag:** strip the 97-byte credential. In a private network, verify it as for Hello, unless the peer is already authorized; on success, record the peer as authorized, without an expiry.
|
||||
**No credential:** in a private network, drop the packet unless the peer is already authorized.
|
||||
9. Record `epk_sender` on a temporary peer record. It is **validated for the source address** if the envelope was a Session or Data envelope: opening it required our ephemeral key for that address, which was only ever sent there. Existing peer state is updated only after parsing, ID binding and the processing checks below.
|
||||
|
||||
**Session cache.** The epoch caches one session per source address, including the verified credential expiry in private networks. Authorization therefore survives the release of a client-mode peer's node record, but never its credential expiry. Newly derived Session keys enter the cache only after credential authorization, parsing, network and node ID checks, and basic fragment validation. Replies must match a pending request or listener. Admission is per packet, so subsequent Data fragments can decrypt before the whole message is assembled. Sending to the exact address for which the peer's ephemeral key is validated can also cache a session. Answers to Initial and Hello envelopes never use or change cached sessions: their source address may be spoofed.
|
||||
**Session cache.** The epoch caches one session per source address, including its verified admission in private networks. Authorization therefore survives the release of a client-mode peer's node record. Newly derived Session keys enter the cache only after credential authorization, parsing, network and node ID checks, and basic fragment validation. Replies must match a pending request or listener. Admission is per packet, so subsequent Data fragments can decrypt before the whole message is assembled. Sending to the exact address for which the peer's ephemeral key is validated can also cache a session. Answers to Initial and Hello envelopes never use or change cached sessions: their source address may be spoofed. If both the peer record and cached admission are lost, the same permanent credential is verified again; no replacement grant is needed.
|
||||
|
||||
3. **Plaintext in a private network.** Drop any packet that is not an envelope.
|
||||
|
||||
@@ -314,7 +316,7 @@ For each received packet, a node applies the following checks in order:
|
||||
- **Public-key work:** one unit per packet needing a key agreement or a signature verification, charged before that work. `Config::max_crypto_per_sec`, by default 512 per second, and 64 per address. A unit covers the packet and its answer, including credential verification:
|
||||
- each Initial envelope (two X25519 operations, and five more to answer a request);
|
||||
- each Session envelope from an uncached (address, `pk_sender`, `epk_sender`) triple (five X25519 operations per epoch tried);
|
||||
- each cached Session envelope requiring credential verification after authorization expires (one charge, not an additional charge for packets already admitted for key agreement);
|
||||
- each cached Session envelope whose peer has not yet been authorized and needs credential verification (one charge, not an additional charge for packets already admitted for key agreement);
|
||||
- each Hello (one Ed25519 verification, and the key derivation of its answer);
|
||||
- each plaintext request from a sender whose key is known or advertised, answered in an Initial envelope (three X25519 operations).
|
||||
|
||||
@@ -386,14 +388,14 @@ sequenceDiagram
|
||||
```
|
||||
|
||||
- **Credential exchange.** Each side attaches its credential to its Initial and Session envelopes. Data envelopes, sent once the peer has accepted us, never carry it.
|
||||
- **Authorization lifetime.** A peer stays authorized until its credential expires, according to the local clock. After that, its packets without a credential are dropped.
|
||||
- **Authorization lifetime.** Admission does not expire. Session keys still rotate for forward secrecy, independently of membership. A lost admission record is recovered using the same grant, without operator action.
|
||||
- **Node lists.** Listed nodes without a valid matching key are discarded.
|
||||
- **Startup checks.** The node refuses to start (`DhtException`) if:
|
||||
- encryption is disabled while authorities are configured;
|
||||
- the configured credential does not match the transport key;
|
||||
- the credential is not valid for the configured authorities.
|
||||
|
||||
A warning is logged if the credential expires within a week. The credential is read once on start, so renewing it requires a restart.
|
||||
The credential and trusted authorities are read on start. Restart with the updated configuration when explicitly changing a grant or the trust set; this also discards cached admissions.
|
||||
|
||||
## 8. Key persistence
|
||||
|
||||
@@ -456,10 +458,10 @@ Each item states who it applies to, what that party can do, and what it cannot d
|
||||
- It can read every Initial envelope ever sent to B, including recorded ones. Initial keys depend only on `sk_B` and the sender's public keys.
|
||||
- It can send Initial envelopes to B under any other node's ID. B answers `ping` and `find_node` in them, under keys the attacker cannot derive, and changes no state ([§6.5](#65-receiving), step 7). GET and writes are refused with `401`.
|
||||
- It cannot read Session or Data envelopes between B and other nodes, recorded or live, nor impersonate another node to B in them. That requires B's ephemeral secret, or the other node's static secret.
|
||||
- In a private network, it stays admitted until B's credential expires. The credential is not secret: Hellos carry it in clear.
|
||||
- In a private network, it stays admitted for as long as B's granting authority is trusted. The credential is not secret: Hellos carry it in clear.
|
||||
- Compromise of a running node also exposes its epoch secrets, and with them all sessions of its current and previous epoch.
|
||||
|
||||
Recover by changing the key. This changes the node ID, and in a private network needs a new credential. The old key cannot be revoked in a public network.
|
||||
Recover by changing the key. This changes the node ID, and in a private network needs a new credential. Changing B's key alone does not exclude the compromised key: its grant remains valid until its authority is distrusted, as described below. The old key cannot be revoked in a public network.
|
||||
|
||||
- **Initial envelopes are not forward-secret against the receiver.** An Initial envelope stays confidential if the sender's static key is compromised later, but not if the receiver's is. Initial envelopes carry:
|
||||
- `ping` or `find_node` to a node whose ephemeral key is unknown, including their retransmissions;
|
||||
@@ -489,15 +491,13 @@ Each item states who it applies to, what that party can do, and what it cannot d
|
||||
- A unit stands for three to ten X25519 operations, so the default allows about 5000 per second. `Config::max_crypto_per_sec` adapts it to the CPU available.
|
||||
- An attacker reachable at many addresses, for example an IPv6 prefix, can fill the session cache of an epoch: one slot costs it one round trip. Evicted peers have their Data envelopes dropped until their next retransmission, which starts a new session.
|
||||
|
||||
- **No revocation.** Applies to private networks. A credential cannot be revoked before it expires. A peer that presented a valid credential stays authorized until that credential's expiry, so a compromised or excluded node keeps participating until then. To exclude it earlier, every other node must stop trusting the authority that signed its credential. That also excludes every other node that only holds credentials from that authority. Keep validity periods short.
|
||||
|
||||
- **Clock dependence.** Applies to private networks. Credential expiry is checked against each verifier's system clock. A verifier whose clock is behind keeps accepting an expired credential for as long as it is behind; one whose clock is ahead rejects a valid credential early. A node checks its own credential the same way at startup.
|
||||
- **No per-grant revocation.** Applies to private networks. A grant is permanent: a compromised or excluded node can participate for as long as any authority that signed a grant for its key remains trusted. Compromise of an authority allows issuing arbitrary new grants. Exclusion requires every remaining node to remove the affected authority from its trust set and restart, discarding cached admissions. This also excludes every member relying only on that authority; retained members need grants from another trusted authority. Multiple accepted authorities provide a transition path, but do not neutralize a compromised authority while it is still trusted. There is no clock-based fallback, revocation list, or automatic expiry.
|
||||
|
||||
- **Metadata.** Anyone observing the traffic sees:
|
||||
- in Initial and Session envelopes, the sender's static key, hence its node ID, and its ephemeral key: who talks to whom, at each first contact, retransmission and epoch rotation;
|
||||
- in Data envelopes, only the type; the sender is still identified by its address, from the Session envelopes that preceded;
|
||||
- in every envelope, the type, the credential flag, the size and the timing, which can reveal the kind of message, for example fragmented value data;
|
||||
- Hello messages entirely, including the credential, which reveals the node's key and the credential's expiry. A copied credential is useless without the matching secret key;
|
||||
- Hello messages entirely, including the credential, which reveals the node's key. A copied credential is useless without the matching secret key;
|
||||
- in public networks, plaintext messages entirely, including node lists.
|
||||
|
||||
Node addresses are not hidden: they are in node lists, and on the packets themselves.
|
||||
@@ -512,8 +512,8 @@ With `dhtnode`:
|
||||
# Once: create the authority (net.key = secret seed, net.pub = hex public key)
|
||||
dhtnode --gen-network-authority net
|
||||
|
||||
# For each node: create its transport key, and a credential valid for 90 days
|
||||
dhtnode --transport-key node1.key --issue-credential node1.cred --authority-key net.key --validity 90
|
||||
# Once per node: create its transport key and permanent membership grant
|
||||
dhtnode --transport-key node1.key --issue-credential node1.cred --authority-key net.key
|
||||
|
||||
# Run a node
|
||||
dhtnode --transport-key node1.key --network-credential node1.cred --network-authority $(cat net.pub)
|
||||
@@ -521,7 +521,22 @@ dhtnode --transport-key node1.key --network-credential node1.cred --network-auth
|
||||
|
||||
Authority generation refuses to run if either output path already exists, including a dangling symlink.
|
||||
|
||||
`--network-authority` can be repeated to trust several authorities, for example during an authority rotation. Keep the authority secret offline. Distribute only `net.pub` and the per-node credentials.
|
||||
Keep the authority secret offline. Distribute only `net.pub` and the per-node credentials. Without a compromise or an intentional membership change, keep these keys, grants, and trust sets unchanged: there is no scheduled renewal or maintenance.
|
||||
|
||||
`--network-authority` can be repeated to trust several authorities during an explicit transition:
|
||||
|
||||
```sh
|
||||
dhtnode --transport-key node1.key --network-credential node1.cred \
|
||||
--network-authority $(cat net.pub) --network-authority $(cat replacement.pub)
|
||||
```
|
||||
|
||||
To move away from an authority:
|
||||
|
||||
1. Create a replacement authority and configure retained members to trust both old and replacement public keys. Restart them with this overlapping trust set.
|
||||
2. Issue replacement grants only to retained, uncompromised transport keys and restart those nodes with their new grants. Old and replacement grants interoperate while both authorities are trusted. A compromised transport key must be replaced as well.
|
||||
3. Remove the old authority from every retained node's configuration and restart them, clearing prior admissions. Only grants from the remaining trusted authorities are then accepted.
|
||||
|
||||
A compromised authority or a compromised key's old grant remains usable wherever the old authority is still trusted. The overlap is a transition mechanism, not immediate exclusion; it ends only when all retained nodes stop trusting the affected authority. Uncompromised grants from other authorities do not need updating.
|
||||
|
||||
From the library, set `Config::transport` before starting:
|
||||
- `authorities`;
|
||||
@@ -537,13 +552,13 @@ From the library, set `Config::transport` before starting:
|
||||
| Credential flag | `0x80` |
|
||||
| Key, nonce, tag sizes | 32, 12, 16 bytes |
|
||||
| Envelope header | 66 bytes, 2 for Data |
|
||||
| Envelope overhead | 30 bytes for Data, 94 for Initial and Session (199 with credential) |
|
||||
| Envelope overhead | 30 bytes for Data, 94 for Initial and Session (191 with credential) |
|
||||
| Epoch (ephemeral keys and tokens) | 30 minutes, previous epoch accepted for one more period |
|
||||
| Cached sessions per epoch | 16384, least recently used evicted |
|
||||
| Credential size, version | 105 bytes, `0x01` |
|
||||
| Credential size, version | 97 bytes, `0x02` |
|
||||
| Node ID context | `"opendht-node-id-v1"` |
|
||||
| HKDF salt | `"opendht-transport-v2"` |
|
||||
| Credential signature context | `"opendht-credential-v1"` |
|
||||
| Credential signature context | `"opendht-credential-v2"` |
|
||||
| Ephemeral key, token contexts | `"opendht-address-key-v1"`, `"opendht-address-token-v1"` |
|
||||
| Token size | 32 bytes |
|
||||
| Value fragment size | 1184 bytes, less the envelope overhead (1154 to 985) |
|
||||
| Value fragment size | 1184 bytes, less the envelope overhead (1154 to 993) |
|
||||
@@ -532,7 +532,7 @@ private:
|
||||
transport::SessionKeys keys;
|
||||
/* the peer has these keys and accepted us: send Data envelopes */
|
||||
bool confirmed {false};
|
||||
transport::SystemTime authorizedUntil {};
|
||||
bool authorized {false};
|
||||
};
|
||||
/* The secret from which ephemeral keys and tokens derive, and session keys, all erased together.
|
||||
* A session is cached for an address only once its peer proved to receive there: it opened an envelope
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "utils.h"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
@@ -19,7 +18,6 @@ constexpr size_t KEY_SIZE {32};
|
||||
constexpr size_t SIGNATURE_SIZE {64};
|
||||
using Key = std::array<uint8_t, KEY_SIZE>;
|
||||
using Signature = std::array<uint8_t, SIGNATURE_SIZE>;
|
||||
using SystemTime = std::chrono::system_clock::time_point;
|
||||
|
||||
/** Erases secret key material. */
|
||||
OPENDHT_PUBLIC void wipe(Key& key);
|
||||
@@ -50,13 +48,12 @@ OPENDHT_PUBLIC KeyPair addressKey(const Key& epochSecret, const uint8_t* address
|
||||
/** Write token of the plaintext peer at `address` (IP and port), derived from an epoch secret. */
|
||||
OPENDHT_PUBLIC Key addressToken(const Key& epochSecret, const uint8_t* address, size_t size);
|
||||
|
||||
/** Authority-signed binding of a transport public key to a private network. */
|
||||
/** Non-expiring authority-signed grant binding a transport public key to a private network. */
|
||||
struct OPENDHT_PUBLIC Credential
|
||||
{
|
||||
static constexpr size_t PACKED_SIZE {1 + KEY_SIZE + 8 + SIGNATURE_SIZE};
|
||||
static constexpr size_t PACKED_SIZE {1 + KEY_SIZE + SIGNATURE_SIZE};
|
||||
|
||||
Key key {};
|
||||
SystemTime expiration {};
|
||||
Signature signature {};
|
||||
|
||||
Blob pack() const;
|
||||
@@ -64,8 +61,8 @@ struct OPENDHT_PUBLIC Credential
|
||||
static Credential unpack(const uint8_t* data, size_t size);
|
||||
static Credential unpack(const Blob& b) { return unpack(b.data(), b.size()); }
|
||||
|
||||
/** True if signed by one of the authorities and not expired at `now`. */
|
||||
bool verify(const std::vector<Key>& authorities, SystemTime now = std::chrono::system_clock::now()) const;
|
||||
/** True if signed by one of the trusted authorities. */
|
||||
bool verify(const std::vector<Key>& authorities) const;
|
||||
explicit operator bool() const { return key != Key {}; }
|
||||
};
|
||||
|
||||
@@ -77,7 +74,7 @@ struct OPENDHT_PUBLIC Authority
|
||||
|
||||
static Authority generate();
|
||||
static Authority fromSecret(const Key& secret);
|
||||
Credential issue(const Key& transportKey, SystemTime expiration) const;
|
||||
Credential issue(const Key& transportKey) const;
|
||||
};
|
||||
|
||||
/** Transport state of a remote node, attached to its Node. */
|
||||
@@ -91,12 +88,11 @@ struct Peer
|
||||
std::optional<Key> ephemeral {};
|
||||
/** Address proven by the Session or Data envelope carrying the ephemeral key */
|
||||
SockAddr validatedAddress {};
|
||||
/** Private networks: expiration of the verified peer credential */
|
||||
SystemTime authorizedUntil {};
|
||||
/** Private networks: the peer presented a grant from a trusted authority */
|
||||
bool authorized {false};
|
||||
/** Our session was lost: requests other than pings wait for a new ephemeral key */
|
||||
bool recovering {false};
|
||||
|
||||
bool isAuthorized(SystemTime now) const { return now < authorizedUntil; }
|
||||
void setEphemeral(const Key& k, const SockAddr& address = {})
|
||||
{
|
||||
ephemeral = k;
|
||||
|
||||
@@ -2060,9 +2060,6 @@ Dht::Dht(std::unique_ptr<net::DatagramSocket>&& sock,
|
||||
|
||||
if (logger_)
|
||||
logger_->debug("DHT node initialised with ID {:s}", myid);
|
||||
if (config.transport.isPrivate() and logger_
|
||||
and config.transport.credential.expiration < std::chrono::system_clock::now() + std::chrono::hours(24 * 7))
|
||||
logger_->warn("Transport credential expires in less than a week");
|
||||
}
|
||||
|
||||
bool
|
||||
|
||||
+11
-11
@@ -678,7 +678,7 @@ NetworkEngine::processMessage(const uint8_t* buf, size_t buflen, SockAddr f)
|
||||
return;
|
||||
}
|
||||
if (envelope and envelope->cached)
|
||||
envelope->cached->authorizedUntil = msg->peer->authorizedUntil;
|
||||
envelope->cached->authorized = msg->peer->authorized;
|
||||
if (envelope and envelope->pending)
|
||||
envelope->pending->epoch->store(from, envelope->pending->session);
|
||||
|
||||
@@ -1192,14 +1192,14 @@ NetworkEngine::session(const transport::Peer& peer, const SockAddr& addr)
|
||||
const bool validated = peer.validatedAddress and peer.validatedAddress == addr;
|
||||
if (validated)
|
||||
if (auto s = epoch_.find(addr, peer.key, peerEphemeral)) {
|
||||
s->authorizedUntil = std::max(s->authorizedUntil, peer.authorizedUntil);
|
||||
s->authorized = s->authorized or peer.authorized;
|
||||
return *s;
|
||||
}
|
||||
auto ephemeral = epoch_.ephemeral(addr);
|
||||
std::optional<Session> s;
|
||||
if (auto keys = transport::sessionKeys(config.transport.key, ephemeral, peer.key, peerEphemeral, config.network)) {
|
||||
s = Session {ephemeral.pub, peer.key, peerEphemeral, *keys};
|
||||
s->authorizedUntil = peer.authorizedUntil;
|
||||
s->authorized = peer.authorized;
|
||||
if (validated)
|
||||
epoch_.store(addr, *s);
|
||||
}
|
||||
@@ -1302,7 +1302,7 @@ NetworkEngine::attachPeer(Node& node, const Sp<transport::Peer>& peer)
|
||||
node.setTransport(peer);
|
||||
} else if (current != peer) {
|
||||
// Both keys match the node ID, so they are the same
|
||||
current->authorizedUntil = std::max(current->authorizedUntil, peer->authorizedUntil);
|
||||
current->authorized = current->authorized or peer->authorized;
|
||||
if (peer->ephemeral) {
|
||||
current->setEphemeral(*peer->ephemeral, peer->validatedAddress);
|
||||
}
|
||||
@@ -1312,13 +1312,13 @@ NetworkEngine::attachPeer(Node& node, const Sp<transport::Peer>& peer)
|
||||
bool
|
||||
NetworkEngine::authorize(transport::Peer& peer, const uint8_t* data, size_t size) const
|
||||
{
|
||||
if (peer.isAuthorized(std::chrono::system_clock::now()))
|
||||
if (peer.authorized)
|
||||
return true;
|
||||
try {
|
||||
auto credential = transport::Credential::unpack(data, size);
|
||||
if (credential.key != peer.key or not credential.verify(config.transport.authorities))
|
||||
return false;
|
||||
peer.authorizedUntil = credential.expiration;
|
||||
peer.authorized = true;
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
@@ -1377,28 +1377,28 @@ NetworkEngine::openEnvelope(const uint8_t* buf, size_t buflen, const SockAddr& f
|
||||
|
||||
auto peer = std::make_shared<transport::Peer>(header->sender);
|
||||
if (cached)
|
||||
peer->authorizedUntil = cached->authorizedUntil;
|
||||
peer->authorized = cached->authorized;
|
||||
if (auto node = cache.getNode(transport::nodeId(header->sender), from.getFamily()))
|
||||
if (const auto& current = node->getTransport())
|
||||
peer->authorizedUntil = std::max(peer->authorizedUntil, current->authorizedUntil);
|
||||
peer->authorized = peer->authorized or current->authorized;
|
||||
if (header->credential) {
|
||||
if (payload->size() < CREDENTIAL_SIZE)
|
||||
return std::nullopt;
|
||||
if (isPrivate and not peer->isAuthorized(std::chrono::system_clock::now())) {
|
||||
if (isPrivate and not peer->authorized) {
|
||||
if (header->type == EnvelopeType::Session and not pending and not limitCrypto(header->sender, from))
|
||||
return std::nullopt;
|
||||
if (not authorize(*peer, payload->data(), CREDENTIAL_SIZE))
|
||||
return std::nullopt;
|
||||
}
|
||||
payload->erase(payload->begin(), payload->begin() + CREDENTIAL_SIZE);
|
||||
} else if (isPrivate and not peer->isAuthorized(std::chrono::system_clock::now())) {
|
||||
} else if (isPrivate and not peer->authorized) {
|
||||
return std::nullopt;
|
||||
}
|
||||
// Session and Data envelopes were opened with our key for the source address: the sender receives there
|
||||
const bool session = header->type != EnvelopeType::Initial;
|
||||
peer->setEphemeral(header->ephemeral, session ? from : SockAddr {});
|
||||
if (pending)
|
||||
pending->session.authorizedUntil = peer->authorizedUntil;
|
||||
pending->session.authorized = peer->authorized;
|
||||
return OpenedEnvelope {std::move(peer), std::move(*payload), false, session, std::move(pending), cached};
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -152,8 +152,8 @@ Node::toString() const
|
||||
auto authorization = '-';
|
||||
if (transport_) {
|
||||
state = transport_->ephemeral && transport_->validatedAddress == addr ? 'A' : 'K';
|
||||
if (transport_->authorizedUntil != transport::SystemTime {})
|
||||
authorization = transport_->isAuthorized(std::chrono::system_clock::now()) ? '+' : '!';
|
||||
if (transport_->authorized)
|
||||
authorization = '+';
|
||||
}
|
||||
return fmt::format("{} {:<21} [{}{}]", id, addr.toString(), state, authorization);
|
||||
}
|
||||
|
||||
+13
-42
@@ -26,10 +26,10 @@ 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 CREDENTIAL_CONTEXT = "opendht-credential-v2"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};
|
||||
constexpr uint8_t CREDENTIAL_VERSION {2};
|
||||
|
||||
void
|
||||
randomBytes(uint8_t* data, size_t size, gnutls_rnd_level_t level)
|
||||
@@ -82,42 +82,18 @@ chachaDigest(struct chacha_poly1305_ctx* ctx, uint8_t* 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};
|
||||
constexpr size_t SIGNED_SIZE {CREDENTIAL_CONTEXT.size() + 1 + KEY_SIZE};
|
||||
|
||||
std::array<uint8_t, SIGNED_SIZE>
|
||||
signedData(const Key& key, uint64_t expiration)
|
||||
signedData(const Key& key)
|
||||
{
|
||||
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);
|
||||
auto position = std::copy(CREDENTIAL_CONTEXT.begin(), CREDENTIAL_CONTEXT.end(), data.begin());
|
||||
*position++ = CREDENTIAL_VERSION;
|
||||
std::copy(key.begin(), key.end(), position);
|
||||
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)
|
||||
{
|
||||
@@ -242,8 +218,7 @@ 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);
|
||||
std::copy(signature.begin(), signature.end(), b.begin() + 1 + KEY_SIZE);
|
||||
return b;
|
||||
}
|
||||
|
||||
@@ -254,17 +229,14 @@ Credential::unpack(const uint8_t* data, size_t size)
|
||||
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());
|
||||
std::copy_n(data + 1 + KEY_SIZE, SIGNATURE_SIZE, c.signature.begin());
|
||||
return c;
|
||||
}
|
||||
|
||||
bool
|
||||
Credential::verify(const std::vector<Key>& authorities, SystemTime now) const
|
||||
Credential::verify(const std::vector<Key>& authorities) const
|
||||
{
|
||||
if (now >= expiration)
|
||||
return false;
|
||||
auto data = signedData(key, toSeconds(expiration));
|
||||
auto data = signedData(key);
|
||||
return std::any_of(authorities.begin(), authorities.end(), [&](const Key& authority) {
|
||||
return ed25519_sha512_verify(authority.data(), data.size(), data.data(), signature.data()) == 1;
|
||||
});
|
||||
@@ -290,12 +262,11 @@ Authority::fromSecret(const Key& secret)
|
||||
}
|
||||
|
||||
Credential
|
||||
Authority::issue(const Key& transportKey, SystemTime expiration) const
|
||||
Authority::issue(const Key& transportKey) const
|
||||
{
|
||||
Credential c;
|
||||
c.key = transportKey;
|
||||
c.expiration = SystemTime(std::chrono::seconds(toSeconds(expiration)));
|
||||
auto data = signedData(c.key, toSeconds(c.expiration));
|
||||
auto data = signedData(c.key);
|
||||
ed25519_sha512_sign(pub.data(), secret.data(), data.size(), data.data(), c.signature.data());
|
||||
return c;
|
||||
}
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ print_usage()
|
||||
"[--no-encryption] [--transport-key file] [--network-authority key] [--network-credential file]"
|
||||
<< std::endl
|
||||
<< " dhtnode --gen-network-authority prefix" << std::endl
|
||||
<< " dhtnode --issue-credential file --authority-key file --transport-key file [--validity days]"
|
||||
<< " dhtnode --issue-credential file --authority-key file --transport-key file"
|
||||
<< std::endl
|
||||
<< std::endl;
|
||||
print_info();
|
||||
|
||||
@@ -213,7 +213,6 @@ struct dht_params
|
||||
std::string gen_authority {};
|
||||
std::string issue_credential {};
|
||||
std::string authority_key {};
|
||||
unsigned validity_days {365};
|
||||
};
|
||||
|
||||
/* Runs private network management commands. Returns true if one was run. */
|
||||
@@ -239,11 +238,10 @@ runNetworkAuthorityCommands(const dht_params& params)
|
||||
throw std::runtime_error("--issue-credential requires --authority-key and --transport-key");
|
||||
auto authority = dht::transport::Authority::fromSecret(loadTransportKey(params.authority_key));
|
||||
auto key = loadOrCreateTransportKey(params.transport_key);
|
||||
auto expiration = std::chrono::system_clock::now() + std::chrono::hours(24) * params.validity_days;
|
||||
auto credential = authority.issue(key.pub, expiration).pack();
|
||||
auto credential = authority.issue(key.pub).pack();
|
||||
saveFile(params.issue_credential, credential.data(), credential.size(), false);
|
||||
std::cout << "Credential for node " << dht::transport::nodeId(key.pub) << " saved to "
|
||||
<< params.issue_credential << ", valid for " << params.validity_days << " days" << std::endl;
|
||||
<< params.issue_credential << ", does not expire" << std::endl;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -365,7 +363,6 @@ static const constexpr struct option long_options[] = {
|
||||
{"gen-network-authority", required_argument, nullptr, 'G'},
|
||||
{"issue-credential", required_argument, nullptr, 'Y'},
|
||||
{"authority-key", required_argument, nullptr, 'X'},
|
||||
{"validity", required_argument, nullptr, 'W'},
|
||||
{nullptr, 0, nullptr, 0 }
|
||||
};
|
||||
|
||||
@@ -509,9 +506,6 @@ parseArgs(int argc, char** argv)
|
||||
case 'X':
|
||||
params.authority_key = optarg;
|
||||
break;
|
||||
case 'W':
|
||||
params.validity_days = std::stoul(optarg);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user