Device-Centric Identity System
Instead of traditional accounts (like alice@server.com
with a password), each device generates its own cryptographic identity that serves as both identification and authentication.
Traditional Authentication Model
Section titled “Traditional Authentication Model”- User Account:
alice@server.com
- Password: hunter2
- Server stores:
- Username
- Password hash
- User data
The trust model essentially boils down to “Server vouches that alice@server.com
is legitimate”.
Cryptid’s Device-Centric Model
Section titled “Cryptid’s Device-Centric Model”- Device Identity is a cryptographic keypair + random device ID
- No passwords. Authentication happens entirely through digital signatures.
- The server stores nothing permanent about the device.
Here, the trust model becomes “I can verify this message cryptographically”.
Technical Implementation
Section titled “Technical Implementation”struct DeviceIdentity { // Core cryptographic identity (permanent) // Main public key for stable identity device_id: [u8; 32], // Main keypair that's going to be used for everything keypair: Ed25519KeyPair,
// Delivery addresses (ephemeral, rotatable) delivery_addresses: Vec<DeliveryAddress>
// Unix timestamp of creation created_timestamp: u64,
// User-chosen, non-unique label display_name: Option<String>, // Also user-chosen profile_picture: Option<ProfilePicture>}
struct ProfilePicture { // e.g., "image/png" mime_type: String, // Raw image bytes or hash/link if large data: Vec<u8>, // Timestamp uploaded_at: u64, // Signed for authenticity signature: Ed25519Signature,}
struct DeliveryAddress { // Address identifier (e.g., "a1b2c3d4e5...") prefix: String, // Server domain (e.g., "chat.example.com") server: String, // Timestamp of when the address was generated created_at: u64, // Can be deactivated without deletion active: bool,}
impl DeliveryAddress { fn full_address(&self) -> String { format!("{}@{}", self.prefix, self.server) }}
impl DeviceIdentity { /// Generate a new device identity without any external dependencies fn generate() -> Self { Self { // 256 bits of entropy device_id: generate_cryptographically_secure_random(32), // Modern elliptic curve cryptography keypair: Ed25519KeyPair::generate(), created_timestamp: current_unix_timestamp(), // User can set this later, e.g., "Alice's Phone" display_name: None, // This can be set later as well profile_picture: None, // Initially create empty delivery addresses delivery_addresses: Vec::new(), } }
/// Sign arbitrary data with this device's private key fn sign(&self, data: &[u8]) -> Ed25519Signature { self.keypair.sign(data) }
/// Create MLS credential for group operations fn create_mls_credential(&self) -> MLSCredential { MLSCredential::basic(self.device_id, self.keypair.public_key()) }
/// Create a shareable identity bundle for contact exchange fn create_share_bundle( &self, initial_delivery_address: String, keypackage_server: String,) -> Result<ShareableIdentityBundle> { // Check if the provided address belongs to this device if !self.active_addresses().contains(&initial_delivery_address) { return Err("Cannot create share bundle: provided address is not an active delivery address for this device."); }
Ok(ShareableIdentityBundle { device_id: self.device_id, public_key: self.keypair.public_key(), created_at: self.created_timestamp, initial_delivery_address, keypackage_server, display_name: self.display_name.clone(), profile_picture: self.profile_picture.clone(), proof_of_ownership: self.keypair.sign(&self.device_id), }) }
/// Create a new delivery address for your device fn create_delivery_address(&mut self, server: &str) -> String { // Generate random 16-byte identifier let random: [u8; 16] = generate_random(); let prefix = hex::encode(random);
let addr = DeliveryAddress { prefix: prefix.clone(), server: server.to_string(), created_at: current_unix_timestamp(), active: true, };
self.delivery_addresses.push(addr);
// Return full address format!("{}@{}", prefix, server) }
/// Deactivate a delivery address fn burn_address(&mut self, full_address: &str) { if let Some(addr) = self.delivery_addresses.iter_mut() .find(|a| a.full_address() == full_address) { addr.active = false; } }
/// Get all active delivery addresses fn active_addresses(&self) -> Vec<String> { self.delivery_addresses.iter() .filter(|a| a.active) .map(|a| a.full_address()) .collect() }
/// Get all active addresses for a specific server fn active_addresses_for_server(&self, server: &str) -> Vec<String> { self.delivery_addresses.iter() .filter(|a| a.active && a.server == server) .map(|a| a.full_address()) .collect() }
/// Generate MLS KeyPackages for others to add this device to groups. /// Implementation uses OpenMLS KeyPackage::builder() API. fn generate_key_packages(&mut self, count: usize) -> Vec<KeyPackageBundle> { // Implementation note: Uses MLS RFC 9420 KeyPackage format // with cipher suite MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 // // See: reference/contact-exchange-and-trust for usage details
(0..count).map(|_| { // OpenMLS implementation KeyPackage::builder() .build(ciphersuite, provider, &self.keypair, self.create_mls_credential())
// Returns KeyPackageBundle with: // - key_package: Public part (uploaded to server) // - private_key: Secret part (stored locally for welcome decryption) }).collect() }
}
Why Ed25519?
Section titled “Why Ed25519?”Ed25519 is a modern elliptic curve signature scheme that provides:
- High security: 128-bit security level (equivalent to 3072-bit RSA).
- Performance: Fast signature generation and verification.
- Simplicity: No parameter choices, no pitfalls.
- Deterministic signatures: Same message + key = same signature (important for MLS).
- Small keys: 32-byte public keys, 64-byte signatures.
RFC 8032 Compliance Requirement
Section titled “RFC 8032 Compliance Requirement”Cryptid requires Ed25519 as defined in RFC 8032. This is also known as Ed25519-IETF.
- MLS security proofs require SUF-CMA (Strong Unforgeability under Chosen Message Attack)
- RFC 8032 Ed25519 provides SUF-CMA security
- The original Ed25519 paper from 2011 only provides EUF-CMA (Existential Unforgeability), which is insufficient for us.
Device ID Properties
Section titled “Device ID Properties”The 32-byte device ID serves multiple purposes:
- Unique identification: 2^256 possible values make collisions astronomically unlikely.
- Delivery address derivation: First 16 bytes become a routing address.
- MLS group member identification: Used as member ID in MLS protocol.
- Cryptographic binding: Signed by device’s private key as proof of ownership.
Address Derivation Example
Section titled “Address Derivation Example”device_id = [0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0x17, 0x28, 0x39, 0x4a, 0x5b, 0x6c, 0x7d, 0x8e, 0x9f, 0x10, ...];address_prefix = hex::encode(&device_id[..16]) = "a1b2c3d4e5f61728394a5b6c7d8e9f10";delivery_address = "a1b2c3d4e5f61728394a5b6c7d8e9f10@chat.example.com"
Shareable Identity Bundle
Section titled “Shareable Identity Bundle”Used for contact exchange (QR codes, invite links).
struct ShareableIdentityBundle { device_id: [u8; 32], public_key: Ed25519PublicKey, created_at: u64,
// Initial delivery address for first contact initial_delivery_address: String, // Server where MLS keypackages are uploaded keypackage_server: String,
display_name: Option<String>, profile_picture: Option<ProfilePicture>,
// Self-signed proof that we own the private key for this device_id proof_of_ownership: Ed25519Signature,}
Why include an initial delivery address?
Section titled “Why include an initial delivery address?”In order to contact you, the recipient needs:
- Your
device_id
(permanent identity for trust/blocking) - An
initial_delivery_address
(where to send the first message)
Without the delivery address, the recipient wouldn’t know:
- Which server you’re on
- How to reach you for initial contact
Address Freshness
Section titled “Address Freshness”The initial_delivery_address
may become stale if you rotate addresses.
Recipients should:
- Use it for initial contact
- Update automatically via MLS group address rotation messages
- Handle 404 errors by having user share new address out-of-band
Privacy Considerations
Section titled “Privacy Considerations”Including a delivery address in the share bundle means anyone who scans your QR code learns one of your addresses.
For public sharing scenarios (posting QR code online), consider:
- Creating a dedicated “public contact” address
- Rotating it frequently
- Never reusing it for private communications
Creating Share Bundles with Specific Addresses
Section titled “Creating Share Bundles with Specific Addresses”Users control which delivery address appears in share bundle.
impl DeviceIdentity { fn create_share_bundle( &self, initial_delivery_address: String, keypackage_server: String,) -> Result<ShareableIdentityBundle> { // Check if the provided address belongs to this device if !self.active_addresses().contains(&initial_delivery_address) { return Err("Cannot create share bundle: provided address is not an active delivery address for this device."); }
Ok(ShareableIdentityBundle { device_id: self.device_id, public_key: self.keypair.public_key(), created_at: self.created_timestamp, initial_delivery_address, keypackage_server, display_name: self.display_name.clone(), profile_picture: self.profile_picture.clone(), proof_of_ownership: self.keypair.sign(&self.device_id), }) }
Device Identity vs. Delivery Addresses
Section titled “Device Identity vs. Delivery Addresses”Cryptid separates identity from routing:
Device Identity (device_id)
Section titled “Device Identity (device_id)”- Permanent cryptographic identity (main Ed25519 public key)
- Used in MLS groups for member authentication
- Used by contacts to identify you
- Revealed to message recipients (MLS protocol requirement)
- Never changes unless you create an entirely new device identity
Delivery Address
Section titled “Delivery Address”- Ephemeral routing endpoints (e.g.,
a1b2c3d4...@chat.example.com
) - Used by servers for message delivery
- Can be created, rotated, and burned (deleted) freely
- Hidden from message recipients (only routing metadata visible to servers)
- Not used for blocking (recipients block by device_id, not address)
Key Properties
Section titled “Key Properties”- One Keypair: Device has only ONE master keypair. All delivery addresses are signed using this keypair.
- Multiple Addresses: Devices can maintain multiple active delivery addresses simultaneously.
- Server-Specific: Each delivery address belongs to a specific server.
- Multi-Homing: Devices can have addresses on multiple servers (e.g., primary + backup).
Why Separate These?
Section titled “Why Separate These?”- Spam Prevention: Burn addresses that receive spam and create new ones.
- Privacy: Rotate addresses to prevent long-term tracking by network observers.
- Flexibility: Different addresses for different contexts (work, general, public, etc).
- Stability: Contacts know your device_id and will be notified of address changes IF they share a chat with you.
Address Derivation
Section titled “Address Derivation”Delivery address prefixes are randomly generated (16 bytes):
- They have no relationship to device_id
- They are not derived from the main keypair
- Pure randomness for maximum privacy
This ensures that address rotation cannot be linked through cryptographic analysis.
MLS KeyPackages
Section titled “MLS KeyPackages”Devices generate KeyPackages (one-time-use cryptographic material) for MLS group additions. KeyPackages are uploaded to a designated server and fetched when adding the device to groups.
For complete details on KeyPackage management, contact exchange, and trust establishment, see Contact Exchange and Trust
Comparison with Traditional Systems
Section titled “Comparison with Traditional Systems”Aspect | Traditional Authentication Systems | Cryptid’s Device-Centric System |
---|---|---|
Account Creation | Server registration required | Local key generation only |
Identity Proof | Server password verification | Cryptographic signature |
Trust Anchor | ”Server says alice@server is legitimate" | "Alice’s signature proves ownership” |
Multi-Device | Shared account across devices | Each device is an independent entity |
Server Compromise | All user accounts affected | Individual devices unaffected |
Privacy | Username/email required | No PII needed |