In Reticulum, your identity is a pair of cryptographic keys. Your address is derived from those keys. This chapter explains how identities work and how addresses are computed.
In traditional networks: - Your identity is a username in someone else’s database - Your address is assigned by someone else (DHCP, ISP) - Trust comes from institutions (certificate authorities)
In Reticulum: - Your identity is a key pair you generate yourself - Your address is mathematically derived from your public keys - Trust comes from cryptographic proofs
A Reticulum identity consists of two key pairs:
Identity = {
X25519 key pair: For key exchange (encryption)
Ed25519 key pair: For signatures (authentication)
}
Each key pair has: - Private key: 32 bytes, kept secret - Public key: 32 bytes, shared freely
The public identity (what others see) is:
Public Identity = X25519_public (32 bytes) || Ed25519_public (32 bytes)
= 64 bytes total
Key insight: you create your own identity.
void create_identity(uint8_t priv_x25519[32], uint8_t pub_x25519[32],
uint8_t priv_ed25519[32], uint8_t pub_ed25519[32]) {
// Generate X25519 key pair
randombytes_buf(priv_x25519, 32);
crypto_scalarmult_base(pub_x25519, priv_x25519);
// Generate Ed25519 key pair
randombytes_buf(priv_ed25519, 32);
crypto_sign_seed_keypair(pub_ed25519, priv_ed25519_full, priv_ed25519);
}No registration. No permission. No authority. You generate random bytes, and you have an identity.
Identities can be: - Ephemeral: Generated for a single session, then discarded - Persistent: Saved to disk and reused across sessions
For persistent identities, store the private keys securely:
// Save identity (PROTECT THIS FILE!)
void save_identity(const char *path,
const uint8_t priv_x25519[32],
const uint8_t priv_ed25519[32]) {
FILE *f = fopen(path, "wb");
fwrite(priv_x25519, 1, 32, f);
fwrite(priv_ed25519, 1, 32, f);
fclose(f);
chmod(path, 0600); // Owner read/write only
}Warning: Never share identity files between nodes. If two nodes use the same identity (e.g., by copying the identity file), both can announce and receive packets for that destination. This causes: - Path instability: Announces from different network locations overwrite each other - Message delivery issues: Packets may be routed to either node unpredictably - No conflict detection: Reticulum does not warn about duplicate identities (only about hash collisions with different keys)
Each node should generate its own unique identity.
Network addresses need to be: - Globally unique (no collisions) - Compact (bandwidth is precious) - Self-certifying (prove ownership)
IP addresses solve uniqueness through central allocation. Reticulum uses hash-based addressing instead.
The address is derived from the public identity:
full_hash = SHA256(X25519_public || Ed25519_public)
address_hash = full_hash[0:16] // First 128 bits
In code:
void compute_address_hash(const uint8_t pub_x25519[32],
const uint8_t pub_ed25519[32],
uint8_t address[16]) {
uint8_t full_hash[32];
SHA256_CTX ctx;
SHA256_Init(&ctx);
SHA256_Update(&ctx, pub_x25519, 32);
SHA256_Update(&ctx, pub_ed25519, 32);
SHA256_Final(full_hash, &ctx);
memcpy(address, full_hash, 16); // Truncate to 128 bits
}Full SHA-256 is 256 bits, but Reticulum truncates to 128 bits. Why?
For comparison: - IPv4: 32 bits (4 billion addresses, exhausted) - IPv6: 128 bits (same as Reticulum) - Bitcoin addresses: 160 bits
Addresses are typically shown as hex strings:
void address_to_hex(const uint8_t address[16], char hex[33]) {
for (int i = 0; i < 16; i++) {
sprintf(&hex[i*2], "%02x", address[i]);
}
hex[32] = '\0';
}
// Example: "a1b2c3d4e5f6789012345678abcdef01"Identities support several cryptographic operations.
Use Ed25519 to prove authorship:
// Sign data with your identity
void identity_sign(const uint8_t priv_ed25519[32],
const uint8_t *data, size_t len,
uint8_t signature[64]) {
// Need full 64-byte private key for signing
uint8_t full_key[64], pub[32];
crypto_sign_seed_keypair(pub, full_key, priv_ed25519);
crypto_sign_detached(signature, NULL, data, len, full_key);
}
// Verify signature against public identity
bool identity_verify(const uint8_t pub_ed25519[32],
const uint8_t *data, size_t len,
const uint8_t signature[64]) {
return crypto_sign_verify_detached(signature, data, len, pub_ed25519) == 0;
}Use X25519 to establish shared secrets:
// Compute shared secret with another identity
void identity_key_exchange(const uint8_t my_priv_x25519[32],
const uint8_t peer_pub_x25519[32],
uint8_t shared_secret[32]) {
crypto_scalarmult(shared_secret, my_priv_x25519, peer_pub_x25519);
}To encrypt data for a specific identity:
size_t encrypt_to_identity(const uint8_t recipient_pub_x25519[32],
const uint8_t *plaintext, size_t plain_len,
uint8_t *output, size_t output_max) {
// 1. Generate ephemeral key pair
uint8_t eph_priv[32], eph_pub[32];
crypto_box_keypair(eph_pub, eph_priv);
// 2. Compute shared secret
uint8_t shared[32];
crypto_scalarmult(shared, eph_priv, recipient_pub_x25519);
// 3. Derive encryption key
uint8_t derived[32];
hkdf_sha256(derived, 32, shared, 32, NULL, 0, NULL, 0);
// 4. Copy ephemeral public key to output
memcpy(output, eph_pub, 32);
// 5. Encrypt with derived key (Fernet format)
size_t cipher_len = fernet_encrypt(derived, plaintext, plain_len,
output + 32, output_max - 32);
// 6. Clean up secrets
sodium_memzero(eph_priv, 32);
sodium_memzero(shared, 32);
sodium_memzero(derived, 32);
return 32 + cipher_len; // ephemeral_pub + ciphertext
}The recipient decrypts by: 1. Extracting the ephemeral public key 2. Computing the shared secret with their private key 3. Deriving the same encryption key 4. Decrypting the Fernet token
In Reticulum, you don’t communicate with identities directly. You communicate with destinations. A destination’s address includes more than just the identity hash.
Destinations include: - Application name: String identifying the application - Aspect: Additional qualifier (like a “port”) - Identity: The cryptographic identity
The name is first “expanded” by concatenating the app name and aspects with dots:
expanded_name = "app_name.aspect1.aspect2..."
Then the destination hash is computed:
name_hash = SHA256(expanded_name_utf8)[0:10] // First 10 bytes only!
address_hash = SHA256(name_hash || identity_hash)[0:16]
Note: The name hash is only 10 bytes (80 bits), not 16 bytes. This is a Reticulum-specific truncation different from the standard 16-byte address truncation.
// Application: "myapp", Aspect: "receiver", Identity hash: 0xabcd...
void compute_destination_hash(const char *app_name,
const char *aspect,
const uint8_t identity_hash[16],
uint8_t dest_hash[16]) {
// Build expanded name: "myapp.receiver"
char expanded_name[256];
snprintf(expanded_name, sizeof(expanded_name), "%s.%s", app_name, aspect);
// Hash expanded name, take first 10 bytes
uint8_t full_hash[32];
SHA256((uint8_t*)expanded_name, strlen(expanded_name), full_hash);
uint8_t name_hash[10];
memcpy(name_hash, full_hash, 10); // Only 10 bytes!
// Combine: name_hash (10 bytes) || identity_hash (16 bytes) = 26 bytes
uint8_t combined[26];
memcpy(combined, name_hash, 10);
memcpy(combined + 10, identity_hash, 16);
// Final hash, take first 16 bytes
SHA256(combined, 26, full_hash);
memcpy(dest_hash, full_hash, 16);
}This design allows: - Multiple destinations per identity: Same identity can host different services - Service discovery: Destination hash encodes what the service is - Namespace separation: Different apps won’t collide
Reticulum uses both full (256-bit) and truncated (128-bit) hashes:
| Use Case | Hash Size | Why |
|---|---|---|
| Address (destination hash) | 128 bits | Bandwidth efficiency |
| Link ID | 128 bits | Bandwidth efficiency |
| Announce hash | 256 bits | Full security |
| Internal operations | 256 bits | No bandwidth constraint |
The truncated hash is always the first 128 bits (16 bytes) of the full SHA-256 output.
When you receive an announce or proof, you learn a remote identity’s public keys. Reticulum caches these for future use:
typedef struct {
uint8_t address_hash[16]; // Lookup key
uint8_t x25519_public[32]; // For encryption
uint8_t ed25519_public[32]; // For verification
time_t last_seen; // For expiry
} IdentityCache;
// Cache an identity
void cache_identity(IdentityCache *cache, size_t *count,
const uint8_t addr[16],
const uint8_t x25519[32],
const uint8_t ed25519[32]) {
// Check if already cached
for (size_t i = 0; i < *count; i++) {
if (memcmp(cache[i].address_hash, addr, 16) == 0) {
cache[i].last_seen = time(NULL);
return;
}
}
// Add new entry
memcpy(cache[*count].address_hash, addr, 16);
memcpy(cache[*count].x25519_public, x25519, 32);
memcpy(cache[*count].ed25519_public, ed25519, 32);
cache[*count].last_seen = time(NULL);
(*count)++;
}
// Lookup identity by address
bool lookup_identity(IdentityCache *cache, size_t count,
const uint8_t addr[16],
uint8_t x25519_out[32],
uint8_t ed25519_out[32]) {
for (size_t i = 0; i < count; i++) {
if (memcmp(cache[i].address_hash, addr, 16) == 0) {
memcpy(x25519_out, cache[i].x25519_public, 32);
memcpy(ed25519_out, cache[i].ed25519_public, 32);
return true;
}
}
return false;
}This cache is populated by: - Announces: Broadcast identity information - Link proofs: Include responder’s public key - Explicit recall: Request identity from network
Identities and addresses are often displayed as hex strings:
a1b2c3d4e5f6789012345678abcdef01
1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef (X25519)
fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321 (Ed25519)
Never share this!
[32 bytes X25519 private][32 bytes Ed25519 private]
bool hex_to_bytes(const char *hex, uint8_t *out, size_t out_len) {
if (strlen(hex) != out_len * 2) return false;
for (size_t i = 0; i < out_len; i++) {
char byte[3] = {hex[i*2], hex[i*2+1], '\0'};
char *end;
out[i] = strtoul(byte, &end, 16);
if (*end != '\0') return false;
}
return true;
}| Concept | Size | Description |
|---|---|---|
| Identity | 64 bytes (public) | X25519 pubkey + Ed25519 pubkey |
| Private Identity | 64 bytes | X25519 privkey + Ed25519 privkey |
| Identity Hash | 16 bytes | SHA256(public identity)[0:16] |
| Destination Hash | 16 bytes | SHA256(name + aspects + identity)[0:16] |
Key operations: - Sign: Ed25519(private_key, data) → 64-byte signature - Verify: Ed25519_verify(public_key, data, signature) → bool - Key Exchange: X25519(my_private, peer_public) → 32-byte shared secret - Encrypt: Ephemeral ECDH + HKDF + Fernet
The next chapter explains how data is structured into packets for transmission.