from cryptography.hazmat.primitives import hashesfrom cryptography.hazmat.primitives.asymmetric import ecfrom cryptography.hazmat.primitives.kdf.hkdf import HKDFfrom cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key def load_pair(private_key_file: str, peer_public_key_file: str): with open(private_key_file, "rb") as f: private_key = load_pem_private_key(f.read(), password=None) with open(peer_public_key_file, "rb") as f: peer_public_key = load_pem_public_key(f.read()) return private_key, peer_public_key def derive_key(shared_secret: bytes) -> bytes: # An HKDF object derives once; create a new one for every key hkdf = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=b"AES-256-GCM key v1") return hkdf.derive(shared_secret) # X25519: exchange(peer_public_key)private_key, peer = load_pair("alice-x25519.pem", "bob-x25519.pub.pem")shared = private_key.exchange(peer)print("X25519 shared secret:", shared.hex())print("X25519 derived key:", derive_key(shared).hex()) # NIST curves: exchange(ec.ECDH(), peer_public_key)private_key, peer = load_pair("alice-p256.pem", "bob-p256.pub.pem")shared = private_key.exchange(ec.ECDH(), peer)print("P-256 shared secret:", shared.hex())print("P-256 derived key:", derive_key(shared).hex()) from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKeyfrom cryptography.hazmat.primitives.serialization import Encoding, PublicFormat # In a live exchange each side makes a fresh key pair and sends only its public keyalice = X25519PrivateKey.generate()bob = X25519PrivateKey.generate() # On the wire: 32 raw bytes per X25519 public keyfrom_alice = alice.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)from_bob = bob.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) alice_secret = alice.exchange(X25519PublicKey.from_public_bytes(from_bob))bob_secret = bob.exchange(X25519PublicKey.from_public_bytes(from_alice))print("both sides agree:", alice_secret == bob_secret) How it works
load_pem_private_keyreturns anX25519PrivateKeyor anEllipticCurvePrivateKeydepending on the PEM;load_pem_public_keydoes the same for the peer's key.- X25519 is
private_key.exchange(peer_public_key). NIST curves need the algorithm object:private_key.exchange(ec.ECDH(), peer_public_key). HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=…)withderive(secret)produces the key;salt=Nonemeans the RFC 5869 default, a string of zero bytes.- For a live exchange, X25519 public keys travel as 32 raw bytes (
Encoding.Raw,PublicFormat.Raw) and are read back withfrom_public_bytes.
Gotchas
- The two signatures are not interchangeable:
ec_key.exchange(peer)raisesTypeError: ECPrivateKey.exchange() missing 1 required positional argument: 'peer_public_key', andx25519_key.exchange(ec.ECDH(), peer)raisesTypeError: X25519PrivateKey.exchange() takes 1 positional arguments but 2 were given. - An
HKDFinstance derives once. A secondderivecall raisesAlreadyFinalized: Context was already finalized.— create a new instance per key, asderive_keydoes. - Keys on different curves raise
ValueError: Error computing shared key., and so does a malicious all-zero X25519 peer key. Treat that exception as a rejected peer, not as a transient error. - The raw shared secret is not a key: use HKDF with a purpose-specific
inforather than slicing or hashing the secret ad hoc, so both sides derive the same key the same way.