Handshake protocol reference
July 31, 2026 · View on GitHub
This page documents how the three handshake variants in poorman_handshake
derive a shared secret, and where each fits in the HiveMind mesh. For the
quickstart and API surface, see the README.
Where this sits in HiveMind
A HiveMind satellite and the hivemind-core
hub establish an encrypted channel before exchanging any bus traffic. The
shared secret produced by a handshake here is the input to that channel's
authenticated symmetric encryption. The access key handed out by
hivemind-core add-client is the pre-shared password consumed by the
password path below.
Symmetric path: PasswordHandShake
Both parties already share a password and derive a symmetric key without ever sending the password over the wire. This is Usenet hSub (hashed-subject) addressing repurposed as a key-confirmation step.
It is not a PAKE, despite the shape. A checkable image of the password (
SHA256(IV ‖ password), IV public) travels on the wire, so a passive observer can mount an offline dictionary attack against a weak password. Use a high-entropy access key here, and readsecurity.mdbefore relying on this path. It is a worked example of why the distinction matters.
- Each party generates a random 64-bit IV (
generate_iv). - Each derives a hsub (hashed subject) binding the IV to the password
(
create_hsub) and sends it as the handshake message. - On receipt, each party extracts the peer's IV (
iv_from_hsub) and verifies the hsub against the shared password (match_hsub). A mismatch means the peer does not hold the password, so the handshake is rejected. - The two IVs are XORed into a common salt, and the final key is derived with PBKDF2-HMAC-SHA256 over the password and that salt.
Because the salt mixes both IVs, neither side alone fixes the key, and the password itself never travels.
from poorman_handshake import PasswordHandShake
from secrets import compare_digest
a = PasswordHandShake("shared access key")
b = PasswordHandShake("shared access key")
ha, hb = a.generate_handshake(), b.generate_handshake()
assert a.receive_and_verify(hb) and b.receive_and_verify(ha)
assert compare_digest(a.secret, b.secret)
Asymmetric path: HandShake
Mutual RSA key agreement where both parties contribute randomness, so a single compromised party cannot dictate the key.
- Each party owns an RSA keypair (generated, or loaded from a PEM file via the
pathconstructor argument for a stable identity). - Public keys are exchanged out of band and loaded with
load_public. - Each party picks a random secret, signs it (RSA-PSS), encrypts it to the peer's public key (RSA-OAEP), and sends the concatenation as the handshake.
receive_and_verifychecks the signature against the peer's public key, decrypts the peer's secret, and XORs it with the locally chosen secret.
The final secret is the XOR of both contributions, so it is identical on both
ends and depends on input from each.
from poorman_handshake import HandShake
from secrets import compare_digest
a, b = HandShake(), HandShake()
a.load_public(b.pubkey); b.load_public(a.pubkey)
ha, hb = a.generate_handshake(), b.generate_handshake()
a.receive_and_verify(hb); b.receive_and_verify(ha)
assert compare_digest(a.secret, b.secret)
One-way: HalfHandShake
Subclass of HandShake for asymmetric trust: the sender chooses the secret
and the receiver authenticates the sender. Only the sender's contribution
is used (no XOR), so only the receiver needs the sender's public key in
advance.
from poorman_handshake import HalfHandShake
from secrets import compare_digest
sender, receiver = HalfHandShake(), HalfHandShake()
receiver.load_public(sender.pubkey) # sender pubkey known to receiver
shake = sender.generate_handshake(receiver.pubkey)
receiver.receive_and_verify(shake)
assert compare_digest(receiver.secret, sender.secret)
Identity and trust models
HandShake(path=...) loads or creates a persistent private key, giving a node a
stable public identity across restarts. With stable keys you can layer a trust
model on top of the raw exchange:
- TOFU (trust on first use): pin a peer's public key the first time it is
seen and reject changes after that. See
examples/tofu_handshake.py. - Pre-distributed keys: ship known public keys out of band and refuse
unknown peers. See
examples/static_handshake.py.
The examples/*_mitm.py scripts demonstrate why out-of-band public-key
validation matters: without it, an attacker who can relay messages can sit
between two parties. The signature step prevents tampering, but only if each
side already knows the other's authentic public key.
Low-level RSA helpers
poorman_handshake.asymmetric.utils exposes the primitives the handshake is
built from, usable directly for ad-hoc encryption or signing:
encrypt_RSA/decrypt_RSA: RSA-OAEP for short payloads.hybrid_encrypt_RSA/hybrid_decrypt_RSA: RSA-wrapped AES-GCM for arbitrary-length payloads.sign_RSA/verify_RSA: RSA-PSS signatures.load_RSA_key/export_RSA_key/create_RSA_key: PEM key management.
Security caveats
This library is the bootstrap primitive, not a full secure-transport stack.
The derived secret must be used with authenticated encryption (AES-GCM,
ChaCha20-Poly1305), and public-key distribution must be validated out of band.
Both paths have real limits that shape when they are safe to use: the password
path is offline-guessable and only safe with a high-entropy key, and the RSA
path provides no forward secrecy. These are analyzed in depth, with the
threat model, the exact attacks, and why they are a cautionary tale about
composing your own crypto, in security.md. Read it before
using either handshake outside its original context.