PeerNet.Handshake (PeerNet v0.1.0)

Copy Markdown View Source

Mutual-authentication handshake for PeerNet — implementation of the Noise protocol's XX pattern with the suite Noise_XX_25519_ChaChaPoly_SHA256.

Why Noise XX

The XX pattern is the canonical "two-party mutual auth without pre-shared knowledge of public keys" Noise pattern. It gives us:

  • Forward secrecy — every session uses fresh ephemeral X25519 keys. Compromising the long-term static key does not reveal past sessions.
  • Mutual authentication — both parties' static keys are cryptographically bound into the transcript hash; a peer cannot impersonate another even if they know the other's public key.
  • Identity hiding — static keys travel encrypted under the ephemeral DH; a passive observer cannot link a session to a long-term identity from the wire alone.
  • Replay resistance — every session has a fresh transcript hash.

Wire shape

Three messages over the framed wire (PeerNet.Frame):

M1 (initiator  responder)
  e

M2 (responder  initiator)
  e, ee, s, es     # peer ephemeral, then encrypted static + payload

M3 (initiator  responder)
  s, se            # encrypted static + payload

Each "encrypted" piece is wrapped with ChaCha20-Poly1305 using the key derived from successive MixKey operations on the running chain. The full Noise message-handling sequence is implemented in step/2.

After the handshake

Once both sides reach :authenticated, the state contains two PeerNet.Channel.CipherState instances:

  • tx_state — used to encrypt outbound application messages
  • rx_state — used to decrypt inbound application messages

Initiator's tx is the responder's rx and vice versa, so messages in each direction use independent keys and nonce counters.

Trust check

The peer's static public key is checked against the trust set at the point it's revealed during the handshake. A peer not in the trust set causes the handshake to abort with {:error, :untrusted_peer, role}. This happens after Noise has cryptographically verified the peer actually knows the corresponding private key, so a forged static key is impossible.

Examples

iex> a = PeerNet.Identity.generate()
iex> state = PeerNet.Handshake.init(:initiator, a, MapSet.new())
iex> state.role
:initiator
iex> state.phase
:send_m1

Summary

Types

t()

Handshake state for one side of the exchange.

Functions

Initialise a handshake state for one side.

Drive the state machine one step.

Types

phase()

@type phase() ::
  :send_m1
  | :wait_m1
  | :send_m2
  | :wait_m2
  | :send_m3
  | :wait_m3
  | :authenticated

t()

@type t() :: %PeerNet.Handshake{
  ck: binary(),
  eph_priv: binary() | nil,
  eph_pub: binary() | nil,
  h: binary(),
  identity: PeerNet.Identity.t(),
  inbox: binary(),
  k: binary() | nil,
  n: non_neg_integer(),
  peer_eph: binary() | nil,
  peer_pubkey: binary() | nil,
  phase: phase(),
  role: :initiator | :responder,
  rx: PeerNet.Channel.CipherState.t() | nil,
  trust: MapSet.t(binary()),
  tx: PeerNet.Channel.CipherState.t() | nil
}

Handshake state for one side of the exchange.

Functions

init(role, identity, trust)

@spec init(:initiator | :responder, PeerNet.Identity.t(), MapSet.t(binary())) :: t()

Initialise a handshake state for one side.

  • role:initiator (the dialer) or :responder (the acceptor).
  • identity — this node's PeerNet.Identity (X25519 keypair).
  • trustMapSet of peer public keys this node will accept.

step(state, inbound \\ <<>>)

@spec step(t(), binary()) ::
  {:ok, t(), binary()} | {:error, atom(), :initiator | :responder}

Drive the state machine one step.

Pass inbound_bytes (default empty) — bytes received from the peer since the last call. Returns:

  • {:ok, new_state, outbound_bytes} — possibly empty bytes to send to the peer, plus updated state.
  • {:error, reason, role} — handshake failed; close the connection.

Reasons:

  • :untrusted_peer — peer's static key not in the trust list.
  • :bad_decrypt — AEAD authentication failed (wire tampering).
  • :malformed — wire bytes don't match the expected XX shape.
  • :stalled — state machine asked to step in an unexpected phase.