PeerNet.Identity (PeerNet v0.1.0)

Copy Markdown View Source

Cryptographic identity for a PeerNet node — an X25519 keypair that uniquely identifies one peer to all the others.

Each PeerNet node has exactly one identity. The public key is the peer's permanent address: anyone wanting to talk to this node names it by its public key, never by an IP/port pair. The private key is the static Diffie-Hellman key the Noise XX handshake uses to authenticate this node to peers and to derive forward-secret session keys.

Why X25519 (not Ed25519)

PeerNet's handshake is Noise_XX_25519_ChaChaPoly_SHA256 — a standard Noise pattern. Noise's DH primitive on 25519 is X25519, not Ed25519. Identity needs to do DH operations during the handshake, so the static key has to be an X25519 key.

We could carry both an Ed25519 (for signing) and an X25519 (for DH) key per identity, but the handshake authenticates the static X25519 key cryptographically by binding it into the transcript hash — no separate signing layer is needed.

Apps that need long-term-key signatures for app-level data can layer that on top with their own keypair.

Persistence

load_or_create/1 writes a keyfile under the supplied data directory so the node's identity survives restarts. The keyfile is a fixed-format binary that can only be parsed by this module.

Treat the keyfile as a secret. Anyone holding it can impersonate the node to any peer that has paired with it. PeerNet does not encrypt the keyfile at rest; that is delegated to the host (filesystem permissions, full-disk encryption, etc.).

Cryptographic primitives

  • Algorithm: X25519 (:ecdh / :x25519 via :crypto)
  • Public key size: 32 bytes
  • Private key size: 32 bytes
  • Fingerprint: first 8 bytes of SHA-256(public), hex-encoded — for UI display only, never for security decisions.

Examples

iex> id = PeerNet.Identity.generate()
iex> byte_size(id.public)
32
iex> byte_size(id.private)
32

Summary

Types

t()

An X25519 keypair. public is the permanent peer address.

Functions

Compute the X25519 shared secret between this identity's private key and peer_public. Used by the Noise handshake.

Short, human-readable fingerprint of a public key. Use in logs and UI for identifying a peer at a glance.

Generate a fresh X25519 keypair.

Load an existing identity from data_dir, or create one and persist it.

Types

t()

@type t() :: %PeerNet.Identity{private: binary(), public: binary()}

An X25519 keypair. public is the permanent peer address.

Functions

dh(identity, peer_public)

@spec dh(t(), binary()) :: binary()

Compute the X25519 shared secret between this identity's private key and peer_public. Used by the Noise handshake.

Examples

iex> a = PeerNet.Identity.generate()
iex> b = PeerNet.Identity.generate()
iex> PeerNet.Identity.dh(a, b.public) == PeerNet.Identity.dh(b, a.public)
true

fingerprint(public_key)

@spec fingerprint(binary()) :: String.t()

Short, human-readable fingerprint of a public key. Use in logs and UI for identifying a peer at a glance.

Not for security decisions — fingerprints are short and could collide under adversarial conditions. Always compare full public keys for trust decisions.

Examples

iex> fp = PeerNet.Identity.fingerprint(<<0::256>>)
iex> String.length(fp)
16

generate()

@spec generate() :: t()

Generate a fresh X25519 keypair.

Pure function — no I/O. Use load_or_create/1 for the normal node- lifecycle case where the identity should persist across restarts.

load_or_create(data_dir)

@spec load_or_create(Path.t()) ::
  {:ok, t(), :created | :loaded} | {:error, :invalid_keyfile | File.posix()}

Load an existing identity from data_dir, or create one and persist it.

Returns {:ok, identity, :created | :loaded, ...} where the third element tells the caller whether this is a fresh node (UI may want to show a "first-run" pairing screen).

Errors

  • {:error, :invalid_keyfile} — the keyfile exists but is malformed.
  • {:error, posix_reason} — filesystem permission or I/O error.