Skip to main content

Ed25519 Identity

How nodes generate, store, and use cryptographic identities.

Key Type​

TealeNet uses Ed25519 (Curve25519.Signing in Apple CryptoKit) for node identity. Every node has exactly one Ed25519 keypair.

  • Private key: 32 bytes (raw scalar)
  • Public key: 32 bytes (compressed point)
  • Node ID: Hex-encoded public key (64 hex characters)

The node ID and public key are the same value -- the hex-encoded 32-byte Ed25519 signing public key.

Key Generation​

Generate a new Ed25519 keypair:

import CryptoKit

let privateKey = Curve25519.Signing.PrivateKey()
let publicKey = privateKey.publicKey
let nodeID = publicKey.rawRepresentation.map { String(format: "%02x", $0) }.joined()
// nodeID is 64 hex chars, e.g. "a1b2c3d4e5f6..."

In other languages, use any Ed25519 implementation (libsodium, ring, tweetnacl, etc.).

Key Persistence​

The raw 32-byte private key is stored in a file with 0600 permissions (owner read/write only).

PlatformPath
macOS / iOS~/Library/Application Support/Teale/wan-identity.key
Linux~/.local/share/teale/wan-identity.key
Windows%APPDATA%\Teale\wan-identity.key
AndroidApp-private directory

The file contains exactly 32 bytes -- the raw private key with no encoding or wrapper.

Registration Signature​

When registering with the relay, the node proves ownership of its Ed25519 keypair by signing the node ID string.

signature = Ed25519.sign(UTF8_bytes(nodeID))

Important: The input to the signature function is the hex-encoded public key string (64 ASCII characters) encoded as UTF-8 bytes, not the raw 32-byte public key. This means the signature is over 64 bytes of hex characters, not 32 bytes of key material.

Example in pseudocode:

private_key = load_key("wan-identity.key") // 32 bytes
public_key = derive_public_key(private_key) // 32 bytes
node_id = hex_encode(public_key) // "a1b2c3..." (64 chars)
signature = ed25519_sign(private_key, node_id) // sign the string
hex_signature = hex_encode(signature) // 128 hex chars

Offer/Answer Signature​

When sending offer or answer messages for connection signaling, the signature covers a concatenation of the session participants and session ID.

signature = Ed25519.sign(UTF8_bytes("{fromNodeID}:{toNodeID}:{sessionID}"))

Example input string:

a1b2c3...:d4e5f6...:550e8400-e29b-41d4-a716-446655440000

Key Agreement Derivation​

For the Noise protocol handshake, a Curve25519 key agreement keypair is derived from the same seed as the Ed25519 signing keypair. In Apple CryptoKit, the same 32-byte private key seed can initialize both:

  • Curve25519.Signing.PrivateKey(rawRepresentation: seed) -- for signing
  • Curve25519.KeyAgreement.PrivateKey(rawRepresentation: seed) -- for Noise DH

The key agreement public key is advertised as wgPublicKey in registration and discovery messages. Nodes that set wgPublicKey to a non-null value indicate support for Noise encryption.

Security Considerations​

  1. Key file permissions: Always set 0600 on the key file. The raw private key provides full control of the node identity.
  2. No key derivation: The private key is stored as raw bytes, not derived from a password or mnemonic. Back it up accordingly.
  3. Single identity: Each node has one identity. Running multiple nodes requires separate key files.
  4. Key rotation: Not currently supported at the protocol level. Changing keys means appearing as a new node to the network.