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/:x25519via: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
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
Functions
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
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
@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.
@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.