X25519 is a Diffie-Hellman key exchange protocol that offers significantly better performance and security than older methods, and it’s the default choice for TLS 1.3.

Let’s see it in action. Imagine two parties, Alice and Bob, want to establish a shared secret key over an insecure channel.

# Alice's side
import os
from cryptography.hazmat.primitives.asymmetric import x25519
from cryptography.hazmat.primitives import serialization

# Generate Alice's private and public keys
alice_private_key = x25519.X25519PrivateKey.generate()
alice_public_key = alice_private_key.public_key()

# Serialize Alice's public key to send to Bob
alice_public_bytes = alice_public_key.public_bytes(
    encoding=serialization.Encoding.Raw,
    format=serialization.PublicFormat.Raw
)

print(f"Alice's public key (hex): {alice_public_bytes.hex()}")

# --- Alice sends alice_public_bytes to Bob ---

# Bob's side (receiving alice_public_bytes)
bob_private_key = x25519.X25519PrivateKey.generate()
bob_public_key = bob_private_key.public_key()

# Serialize Bob's public key to send to Alice
bob_public_bytes = bob_public_key.public_key().public_bytes(
    encoding=serialization.Encoding.Raw,
    format=serialization.PublicFormat.Raw
)

print(f"Bob's public key (hex): {bob_public_bytes.hex()}")

# --- Bob sends bob_public_bytes to Alice ---

# Alice receives bob_public_bytes and computes the shared secret
bob_public_key_deserialized = x25519.X25519PublicKey.from_public_bytes(bob_public_bytes)
shared_secret_alice = alice_private_key.exchange(bob_public_key_deserialized)

print(f"Alice's computed shared secret (hex): {shared_secret_alice.hex()}")

# Bob receives alice_public_bytes and computes the shared secret
alice_public_key_deserialized = x25519.X25519PublicKey.from_public_bytes(alice_public_bytes)
shared_secret_bob = bob_private_key.exchange(alice_public_key_deserialized)

print(f"Bob's computed shared secret (hex): {shared_secret_bob.hex()}")

# Verify they are the same
assert shared_secret_alice == shared_secret_bob
print("Shared secrets match!")

The problem X25519 solves is the secure negotiation of a symmetric encryption key between two parties who have never met before, only communicating over an untrusted network. Traditional Diffie-Hellman (DH) is vulnerable to "man-in-the-middle" attacks if the ephemeral keys aren’t authenticated. TLS 1.3 mandates Perfect Forward Secrecy (PFS), meaning that even if a server’s long-term private key is compromised, past communication sessions cannot be decrypted. X25519, when used with ephemeral keys, provides this PFS inherently because each session uses a new, temporary key pair.

Internally, X25519 is an elliptic curve Diffie-Hellman (ECDH) function. It operates on a specific elliptic curve called Curve25519, which is a Montgomery curve. The "25519" refers to parameters derived from the number 2^255 - 19. The core operation is scalar multiplication: given a base point $G$ on the curve and a private scalar $d$, the public key is $Q = d \cdot G$. Alice and Bob each generate a private scalar ($d_A$, $d_B$) and derive their public keys ($Q_A = d_A \cdot G$, $Q_B = d_B \cdot G$). When Alice receives Bob’s public key $Q_B$, she computes her shared secret as $S_A = d_A \cdot Q_B = d_A \cdot (d_B \cdot G) = (d_A \cdot d_B) \cdot G$. Bob, upon receiving $Q_A$, computes $S_B = d_B \cdot Q_A = d_B \cdot (d_A \cdot G) = (d_B \cdot d_A) \cdot G$. Since scalar multiplication is associative and commutative, $S_A = S_B$. The security relies on the difficulty of the Elliptic Curve Discrete Logarithm Problem (ECDLP): given $G$ and $Q$, it’s computationally infeasible to find the scalar $d$.

The levers you control are primarily in how you generate and manage these keys. In TLS 1.3, this is mostly handled by the library or server software. However, understanding the underlying protocol allows you to configure cipher suites correctly, ensuring that X25519 is prioritized. For example, on an Nginx server, you might specify your preferred cipher suites in the ssl_ciphers directive, prioritizing those that use X25519. A typical configuration might look like:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:X25519-;
ssl_prefer_server_ciphers on;

Note that in TLS 1.3, the cipher suite names are different (e.g., TLS_AES_256_GCM_SHA384) and implicitly include key exchange mechanisms. The X25519- at the end is a hint to the server to prefer X25519-based key exchanges when available.

A common misconception is that X25519 itself provides authentication. It’s a key exchange mechanism, not an authentication protocol. It ensures that Alice and Bob can agree on a secret key, but it doesn’t inherently prove who Alice and Bob are. This is why X25519 is almost always used in conjunction with other mechanisms, such as digital signatures (like ECDSA or RSA) during the TLS handshake, to authenticate the parties involved and prevent man-in-the-middle attacks. The TLS 1.3 handshake, by design, binds the ephemeral X25519 key exchange to the server’s certificate signature.

The next step in securing communication after establishing a shared secret is deriving session keys for encryption and integrity.

Want structured learning?

Take the full Cryptography course →