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 ⚠️
- Shared Secret Not Uniform: Output must be passed through KDF before use
- No Authentication: X25519 alone doesn’t authenticate parties - vulnerable to MITM
- Static Key Risks: Long-term keys enable offline attacks if compromised
- Low Order Points: Implementation must handle properly (built-in for X25519)
- Quantum Vulnerable: Not resistant to quantum computers - consider hybrid approaches
API Functions
generate_keypair() -> (PrivateKey[32], PublicKey[32])
Security Contract:
- Preconditions:
- System CSPRNG must be properly seeded
- At least 256 bits of entropy available
- Postconditions:
- Private key is 32 random bytes with bits clamped
- Public key is derived from private key
- Keys are suitable for X25519 operations
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:
- Use system CSPRNG only
- Never derive from weak entropy sources
- Clear private keys from memory after use
- Consider key rotation schedule
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:
- Preconditions:
private_keymust be valid X25519 private keypublic_keymust be 32 bytes (validation built-in)- Keys should be from trusted source
- Postconditions:
- Returns 32-byte shared secret
- Same secret computed by both parties
- Output is not uniform - must use KDF
- Contributory behavior guaranteed
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:
- Always pass output through KDF
- Never use shared secret directly as key
- Include context in KDF to prevent cross-protocol attacks
- Authenticate public keys out-of-band
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:
- Preconditions:
private_keymust be 32 bytes- Will be clamped if not already
- Postconditions:
- Returns corresponding public key
- Deterministic derivation
- Safe to share publicly
Security Notes:
- One-way function (cannot recover private key)
- Public key can be recomputed anytime
- No timing leaks in derivation
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
- Pre-generate ephemeral keys
- Use batch scalar multiplication
- Cache static computations
- Consider Ed25519 to X25519 conversion
Platform-Specific Security Notes
Python
- Use
cryptographylibrary’s X25519 implementation - PyNaCl provides good bindings
- Avoid pure Python implementations
Rust
- Use
x25519-dalekfor constant-time guarantees curve25519-dalekfor low-level operations- Features for backend selection
TypeScript/JavaScript
- Use
@noble/curvesfor pure JS sodium-nativefor Node.js performance- WebCrypto API support limited
Swift
- CryptoKit provides native implementation
- Curve25519 support since iOS 13
- Hardware acceleration available
Kotlin
- Bouncy Castle for JVM
- Native implementations for multiplatform
- Consider libsodium bindings
Security Auditing
Verification Checklist
- All private keys from secure RNG
- Private keys cleared after use
- Shared secrets passed through KDF
- Public keys authenticated out-of-band
- Ephemeral keys truly ephemeral
- Protocol includes authentication
- Forward secrecy implemented
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:
- Algorithm-specific attack vectors
- Implementation vulnerabilities
- Side-channel considerations
- Quantum resistance analysis (where applicable)
- Deployment recommendations
For complete security analysis and risk assessment, see the dedicated threat model documentation.
References
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