The node loads a persisted ed25519 keypair (~/.thala/node-key, 0600) and uses it only to derive a PeerId. Nothing in the codebase ever signs or verifies anything.
Peer connections are raw TcpStream + length-prefixed postcard, and the identity in ConnectionReq is whatever the dialer claims it is:
pub struct ConnectionReq {
pub peer_id: PeerId, // self-asserted, never verified
pub listen_addr: SocketAddr,
pub message: Option<String>,
pub capabilities: Capabilities,
}
Consequences:
- Impersonation. Any TCP client can claim any
PeerId - including that of a trusted coordinator - and be recorded in known_peers and gossiped onward to the whole mesh.
- Forged claims and results.
TaskClaim.worker_id and TaskResult.worker_id are unverifiable, so results can be attributed to a node that never ran the task.
- Capability lies. A peer can advertise arbitrary
Capabilities to win placement.
- No confidentiality or integrity. Cleartext framing, so an on-path attacker can read and rewrite task payloads.
This is a prerequisite for any authorization layer: a capability system can only decide what a proven identity may do, so without key proof there is nothing to root authorization at.
Locations
Fix
A. Challenge–response over the existing TCP framing. Add a ConnectionChallenge/ConnectionProof step: responder sends a random nonce, dialer returns sign(node_key, nonce || peer_id || listen_addr), responder verifies against the public key inlined in the claimed PeerId. Cheaper to land; does not give confidentiality, so pair it with a transport-level story later.
B. Adopt litep2p properly (future). litep2p is already a dependency but only for PeerId and crypto::ed25519. Running connections over its Noise-authenticated transport gets key proof, encryption, and forward secrecy for free, and removes the hand-rolled framing.
Either way: reject the connection on proof failure, and only then insert into known_peers.
Notes
For ed25519, the protobuf-encoded public key is ≤ MAX_INLINE_KEY_LENGTH (42 bytes), so litep2p uses Code::Identity and the public key is inlined in the PeerId - a claimed key can be checked against a PeerId with PeerId::is_public_key(&public_key) with no extra wire field.
Acceptance
A peer that cannot produce a signature over the responder's nonce for its claimed PeerId is refused and never enters known_peers; an integration test asserts a spoofed-PeerId dialer is rejected.
The node loads a persisted ed25519 keypair (
~/.thala/node-key,0600) and uses it only to derive aPeerId. Nothing in the codebase ever signs or verifies anything.Peer connections are raw
TcpStream+ length-prefixedpostcard, and the identity inConnectionReqis whatever the dialer claims it is:Consequences:
PeerId- including that of a trusted coordinator - and be recorded inknown_peersand gossiped onward to the whole mesh.TaskClaim.worker_idandTaskResult.worker_idare unverifiable, so results can be attributed to a node that never ran the task.Capabilitiesto win placement.This is a prerequisite for any authorization layer: a capability system can only decide what a proven identity may do, so without key proof there is nothing to root authorization at.
Locations
src/node.rs-handle_peer_connection/handle_peer_message, ~330–400src/message.rs-ConnectionReq/ConnectionRespsrc/identity.rs- keypair is derived-from but never used to signFix
A. Challenge–response over the existing TCP framing. Add a
ConnectionChallenge/ConnectionProofstep: responder sends a random nonce, dialer returnssign(node_key, nonce || peer_id || listen_addr), responder verifies against the public key inlined in the claimedPeerId. Cheaper to land; does not give confidentiality, so pair it with a transport-level story later.B. Adopt litep2p properly (future). litep2p is already a dependency but only for
PeerIdandcrypto::ed25519. Running connections over its Noise-authenticated transport gets key proof, encryption, and forward secrecy for free, and removes the hand-rolled framing.Either way: reject the connection on proof failure, and only then insert into
known_peers.Notes
For ed25519, the protobuf-encoded public key is ≤
MAX_INLINE_KEY_LENGTH(42 bytes), so litep2p usesCode::Identityand the public key is inlined in thePeerId- a claimed key can be checked against aPeerIdwithPeerId::is_public_key(&public_key)with no extra wire field.Acceptance
A peer that cannot produce a signature over the responder's nonce for its claimed
PeerIdis refused and never entersknown_peers; an integration test asserts a spoofed-PeerIddialer is rejected.