X25519 Security-Focused API Documentation

Version: 1.0
Last Updated: 2025-07-05
**Security Classification:
PUBLIC
Author: Phantom (phantom@metamui.id)

Overview

X25519 is an Elliptic Curve Diffie-Hellman (ECDH) key exchange function using Curve25519. It enables two parties to establish a shared secret over an insecure channel. Designed by Daniel J. Bernstein, it provides 128-bit security level with built-in resistance to timing attacks and implementation errors.

Security Level: 128 bits (~256-bit keys)
Private Key Size: 32 bytes
Public Key Size: 32 bytes
Shared Secret Size: 32 bytes

Security Warnings ⚠️

  1. Shared Secret Not Uniform: Output must be passed through KDF before use
  2. No Authentication: X25519 alone doesn’t authenticate parties - vulnerable to MITM
  3. Static Key Risks: Long-term keys enable offline attacks if compromised
  4. Low Order Points: Implementation must handle properly (built-in for X25519)
  5. Quantum Vulnerable: Not resistant to quantum computers - consider hybrid approaches

API Functions

generate_keypair() -> (PrivateKey[32], PublicKey[32])

Security Contract:

Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Weak RNG | ⚠️ | Depends on system entropy | | Timing Attack | ✅ | Constant-time scalar multiplication | | Small Subgroup | ✅ | Curve25519 design prevents | | Side Channel | ✅ | No secret-dependent branches | | Key Malleability | ✅ | Clamping ensures canonical form |

Security Requirements:

Secure Usage Example:

# SECURE: Ephemeral key exchange
def ephemeral_key_exchange():
    # Generate ephemeral keypair
    private_key, public_key = x25519.generate_keypair()
    
    try:
        # Send public key to peer
        send_to_peer(public_key)
        
        # Receive peer's public key
        peer_public_key = receive_from_peer()
        
        # Compute shared secret
        shared_secret = x25519.compute_shared_secret(
            private_key, 
            peer_public_key
        )
        
        # Derive session keys using KDF
        session_keys = hkdf(
            secret=shared_secret,
            salt=b"MyProtocol-v1",
            info=b"session-keys",
            length=64
        )
        
        encryption_key = session_keys[:32]
        mac_key = session_keys[32:]
        
        return encryption_key, mac_key
        
    finally:
        # Always clear private key
        secure_zero(private_key)
        secure_zero(shared_secret)

# SECURE: Static-ephemeral pattern
class SecureChannel:
    def __init__(self):
        # Long-term identity key
        self.static_private, self.static_public = x25519.generate_keypair()
        
    def initiate_connection(self, peer_static_public: bytes) -> bytes:
        # Generate ephemeral key for this connection
        eph_private, eph_public = x25519.generate_keypair()
        
        # Compute two shared secrets
        secret1 = x25519.compute_shared_secret(
            self.static_private, 
            peer_static_public
        )
        secret2 = x25519.compute_shared_secret(
            eph_private, 
            peer_static_public
        )
        
        # Combine secrets with KDF
        master_secret = hkdf(
            secret=secret1 + secret2,
            salt=eph_public + peer_static_public,
            info=b"handshake",
            length=32
        )
        
        # Clear ephemeral private key
        secure_zero(eph_private)
        secure_zero(secret1)
        secure_zero(secret2)
        
        return eph_public, master_secret

Common Mistakes:

# INSECURE: Weak entropy
import random
random.seed(12345)
private_key = bytes([random.randint(0, 255) for _ in range(32)])  # WEAK!

# INSECURE: Not clearing keys
private_key, public_key = x25519.generate_keypair()
shared_secret = x25519.compute_shared_secret(private_key, peer_public)
# private_key remains in memory - can be extracted!

# INSECURE: Reusing ephemeral keys
eph_private, eph_public = x25519.generate_keypair()
for peer in peers:
    # Same ephemeral key for everyone - BAD!
    shared = x25519.compute_shared_secret(eph_private, peer.public_key)

compute_shared_secret(private_key: bytes[32], public_key: bytes[32]) -> bytes[32]

Security Contract:

Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | MITM | ❌ | Need additional authentication | | Timing Attack | ✅ | Constant-time implementation | | Invalid Curve | ✅ | Points validated/clamped | | Small Subgroup | ✅ | Returns predictable output | | Twist Attack | ✅ | Curve25519 has secure twist |

Security Requirements:

Secure Usage Example:

use x25519_dalek::{EphemeralSecret, PublicKey};
use hkdf::Hkdf;
use sha2::Sha256;

/// Secure key exchange with authentication
pub struct AuthenticatedKeyExchange {
    identity_key: StaticSecret,
}

impl AuthenticatedKeyExchange {
    pub fn perform_exchange(
        &self,
        peer_identity: &PublicKey,
        peer_ephemeral: &PublicKey,
        peer_signature: &Signature,
    ) -> Result<SessionKeys, Error> {
        // Verify peer's ephemeral key signature
        if !verify_signature(peer_identity, peer_ephemeral, peer_signature) {
            return Err(Error::AuthenticationFailed);
        }
        
        // Generate our ephemeral key
        let our_ephemeral = EphemeralSecret::new(&mut OsRng);
        let our_ephemeral_public = PublicKey::from(&our_ephemeral);
        
        // Compute shared secrets (3DH pattern)
        let dh1 = our_ephemeral.diffie_hellman(peer_identity);
        let dh2 = our_ephemeral.diffie_hellman(peer_ephemeral);
        let dh3 = self.identity_key.diffie_hellman(peer_ephemeral);
        
        // Combine with KDF
        let mut master_secret = Vec::new();
        master_secret.extend_from_slice(dh1.as_bytes());
        master_secret.extend_from_slice(dh2.as_bytes());
        master_secret.extend_from_slice(dh3.as_bytes());
        
        // Derive session keys
        let hkdf = Hkdf::<Sha256>::new(
            Some(b"MyProtocol-v1-3DH"),
            &master_secret
        );
        
        let mut session_keys = SessionKeys::default();
        hkdf.expand(b"client-write", &mut session_keys.client_write)?;
        hkdf.expand(b"server-write", &mut session_keys.server_write)?;
        
        // Clear secrets
        master_secret.zeroize();
        
        Ok(session_keys)
    }
}

// SECURE: Forward secrecy with key rotation
pub struct ForwardSecureChannel {
    chain_key: [u8; 32],
    generation: u64,
}

impl ForwardSecureChannel {
    pub fn advance_keys(&mut self) -> ([u8; 32], [u8; 32]) {
        // Derive next chain key and message key
        let (next_chain, message_key) = self.derive_keys();
        
        // Update state (forward secrecy)
        self.chain_key = next_chain;
        self.generation += 1;
        
        (next_chain, message_key)
    }
    
    fn derive_keys(&self) -> ([u8; 32], [u8; 32]) {
        let hkdf = Hkdf::<Sha256>::new(
            Some(&self.generation.to_le_bytes()),
            &self.chain_key
        );
        
        let mut next_chain = [0u8; 32];
        let mut message_key = [0u8; 32];
        
        hkdf.expand(b"chain", &mut next_chain).unwrap();
        hkdf.expand(b"message", &mut message_key).unwrap();
        
        (next_chain, message_key)
    }
}

derive_public_key(private_key: bytes[32]) -> bytes[32]

Security Contract:

Security Notes:

Secure Usage Example:

# SECURE: Key validation and recovery
def validate_keypair(private_key: bytes, public_key: bytes) -> bool:
    """Verify that public key matches private key"""
    expected_public = x25519.derive_public_key(private_key)
    return constant_time_compare(expected_public, public_key)

# SECURE: Hierarchical key derivation
def derive_subkeys(master_private: bytes, context: str) -> tuple:
    """Derive context-specific keypairs"""
    # Derive context-specific private key
    derived_private = hkdf(
        secret=master_private,
        salt=b"x25519-derivation",
        info=context.encode(),
        length=32
    )
    
    # Ensure proper clamping
    derived_private = clamp_private_key(derived_private)
    
    # Derive public key
    derived_public = x25519.derive_public_key(derived_private)
    
    return derived_private, derived_public

Security Best Practices

Key Management

class SecureKeyManager:
    def __init__(self):
        self.keys = {}
        self.key_lifetime = 86400  # 24 hours
        
    def get_or_generate_key(self, key_id: str) -> tuple:
        """Get existing key or generate new one with expiration"""
        
        if key_id in self.keys:
            key_data = self.keys[key_id]
            if time.time() < key_data['expires']:
                return key_data['private'], key_data['public']
            else:
                # Key expired, clear it
                secure_zero(key_data['private'])
                del self.keys[key_id]
        
        # Generate new key
        private_key, public_key = x25519.generate_keypair()
        
        self.keys[key_id] = {
            'private': private_key,
            'public': public_key,
            'created': time.time(),
            'expires': time.time() + self.key_lifetime
        }
        
        return private_key, public_key
    
    def cleanup_expired(self):
        """Securely remove expired keys"""
        current_time = time.time()
        expired = []
        
        for key_id, key_data in self.keys.items():
            if current_time >= key_data['expires']:
                secure_zero(key_data['private'])
                expired.append(key_id)
        
        for key_id in expired:
            del self.keys[key_id]

Protocol Patterns

/// Noise protocol pattern implementation
pub struct NoiseProtocol {
    static_key: Option<StaticSecret>,
    ephemeral_key: Option<EphemeralSecret>,
    remote_static: Option<PublicKey>,
    handshake_hash: Vec<u8>,
}

impl NoiseProtocol {
    /// Noise XX pattern - mutual authentication
    pub fn handshake_xx(&mut self) -> Result<HandshakeResult, Error> {
        match self.state {
            // -> e
            State::Initial => {
                let ephemeral = EphemeralSecret::new(&mut OsRng);
                let ephemeral_public = PublicKey::from(&ephemeral);
                self.ephemeral_key = Some(ephemeral);
                
                self.mix_hash(&ephemeral_public);
                Ok(HandshakeResult::SendData(ephemeral_public.to_bytes()))
            }
            
            // <- e, ee, s, es
            State::WaitingResponse => {
                let their_ephemeral = self.read_public_key()?;
                let their_static = self.read_encrypted_public_key()?;
                
                // ee
                let secret1 = self.ephemeral_key.unwrap()
                    .diffie_hellman(&their_ephemeral);
                self.mix_key(secret1.as_bytes());
                
                // es
                let secret2 = self.ephemeral_key.unwrap()
                    .diffie_hellman(&their_static);
                self.mix_key(secret2.as_bytes());
                
                self.remote_static = Some(their_static);
                Ok(HandshakeResult::Continue)
            }
            
            // -> s, se
            State::SendingStatic => {
                let encrypted_static = self.encrypt_static()?;
                
                // se
                let secret3 = self.static_key.unwrap()
                    .diffie_hellman(&self.remote_static.unwrap());
                self.mix_key(secret3.as_bytes());
                
                Ok(HandshakeResult::Complete(encrypted_static))
            }
        }
    }
}

KDF Integration

def secure_key_derivation(shared_secret: bytes, context: dict) -> dict:
    """Derive multiple keys from X25519 shared secret"""
    
    # Build context string
    context_data = json.dumps(context, sort_keys=True).encode()
    
    # Use HKDF to derive keys
    hkdf = HKDF(
        algorithm=hashes.SHA256(),
        length=96,  # Total key material needed
        salt=b"X25519-KDF-v1",
        info=context_data
    )
    
    key_material = hkdf.derive(shared_secret)
    
    # Split into specific keys
    keys = {
        'encryption': key_material[0:32],    # ChaCha20 key
        'authentication': key_material[32:64], # Poly1305 key
        'channel_binding': key_material[64:96] # Extra key
    }
    
    # Clear intermediate material
    secure_zero(key_material)
    
    return keys

Common Integration Patterns

TLS-Style Handshake

class TLSStyleHandshake:
    """Simplified TLS 1.3-style key exchange"""
    
    def client_hello(self) -> tuple:
        # Generate ephemeral key
        self.client_ephemeral_private, client_ephemeral_public = \
            x25519.generate_keypair()
        
        # Send supported parameters
        return {
            'version': '1.3',
            'key_share': client_ephemeral_public,
            'supported_groups': ['x25519', 'x448'],
            'random': secrets.token_bytes(32)
        }
    
    def server_hello(self, client_hello: dict) -> tuple:
        # Generate server ephemeral
        server_ephemeral_private, server_ephemeral_public = \
            x25519.generate_keypair()
        
        # Compute shared secret
        shared_secret = x25519.compute_shared_secret(
            server_ephemeral_private,
            client_hello['key_share']
        )
        
        # Derive handshake keys
        handshake_secret = hkdf_extract(
            salt=b"",
            input_key_material=shared_secret
        )
        
        client_handshake_key = hkdf_expand(
            handshake_secret,
            info=b"client handshake key",
            length=32
        )
        
        server_handshake_key = hkdf_expand(
            handshake_secret,
            info=b"server handshake key",
            length=32
        )
        
        # Clear secrets
        secure_zero(server_ephemeral_private)
        secure_zero(shared_secret)
        
        return {
            'version': '1.3',
            'key_share': server_ephemeral_public,
            'random': secrets.token_bytes(32)
        }, client_handshake_key, server_handshake_key

Signal Protocol Pattern

/// Double Ratchet with X25519
pub struct DoubleRatchet {
    root_key: [u8; 32],
    chain_key: [u8; 32],
    our_ratchet_key: Option<StaticSecret>,
    their_ratchet_key: Option<PublicKey>,
}

impl DoubleRatchet {
    pub fn ratchet_encrypt(&mut self, plaintext: &[u8]) -> RatchetMessage {
        // Check if we need to perform DH ratchet
        if self.their_ratchet_key.is_some() && self.our_ratchet_key.is_none() {
            self.dh_ratchet();
        }
        
        // Symmetric ratchet
        let message_key = self.symmetric_ratchet();
        
        // Encrypt with message key
        let ciphertext = encrypt(message_key, plaintext);
        
        RatchetMessage {
            ratchet_key: self.our_ratchet_key.as_ref()
                .map(|k| PublicKey::from(k)),
            ciphertext,
        }
    }
    
    fn dh_ratchet(&mut self) {
        // Generate new ratchet key
        let new_ratchet = StaticSecret::new(&mut OsRng);
        
        // Perform DH
        let shared_secret = new_ratchet
            .diffie_hellman(&self.their_ratchet_key.unwrap());
        
        // Update root key
        let (new_root, new_chain) = kdf_chain(
            &self.root_key,
            shared_secret.as_bytes()
        );
        
        self.root_key = new_root;
        self.chain_key = new_chain;
        self.our_ratchet_key = Some(new_ratchet);
    }
}

Performance Considerations

Operation Time Notes
Key Generation ~90 μs Including public key derivation
Shared Secret ~90 μs Constant time
Public Derivation ~90 μs From private key
Batch Operations ~70 μs/op When processing many

Optimization Strategies

Platform-Specific Security Notes

Python

Rust

TypeScript/JavaScript

Swift

Kotlin

Security Auditing

Verification Checklist

Common Vulnerabilities

# AUDIT: Look for these patterns

# ❌ Using shared secret directly
key = x25519.compute_shared_secret(priv, pub)
encrypted = aes_encrypt(key, data)  # NO KDF!

# ❌ No authentication
shared = x25519.compute_shared_secret(priv, untrusted_pub)

# ❌ Static keys without rotation
if not hasattr(self, 'key'):
    self.key = x25519.generate_keypair()  # Never rotates

# ❌ Weak randomness
private_key = sha256(password)[:32]  # NOT RANDOM!

Post-Quantum Considerations

class HybridKeyExchange:
    """Combine X25519 with post-quantum KEM"""
    
    def hybrid_exchange(self, peer_x25519_pub: bytes, 
                       peer_kyber_pub: bytes) -> bytes:
        # Classical X25519
        x25519_private, x25519_public = x25519.generate_keypair()
        classic_shared = x25519.compute_shared_secret(
            x25519_private, peer_x25519_pub
        )
        
        # Post-quantum KEM (e.g., Kyber)
        pq_ciphertext, pq_shared = kyber.encapsulate(peer_kyber_pub)
        
        # Combine both secrets
        combined_secret = hkdf(
            secret=classic_shared + pq_shared,
            salt=b"hybrid-kex-v1",
            info=x25519_public + pq_ciphertext,
            length=32
        )
        
        # Clear components
        secure_zero(x25519_private)
        secure_zero(classic_shared)
        secure_zero(pq_shared)
        
        return x25519_public, pq_ciphertext, combined_secret

Security Analysis

Threat Model: X25519 Threat Model

The comprehensive threat analysis covers:

For complete security analysis and risk assessment, see the dedicated threat model documentation.

References

  1. RFC 7748 - Elliptic Curves for Security
  2. Curve25519 Paper
  3. Noise Protocol Framework

Support

Security Issues: security@metamui.id
Documentation Updates: phantom@metamui.id
Vulnerability Disclosure: See SECURITY.md


Document Version: 1.0
Review Cycle: Quarterly
Next Review: 2025-04-05
Classification: PUBLIC