Notebook 02 — Hybrid X3DH key agreement#
We run the hybrid handshake between two fresh identities and verify both sides derive the same 96 bytes of keying material.
from pqmsg.identity import generate_identity
from pqmsg.session import initiate_session, accept_session, Handshake
Step 1 — generate identities#
alice = generate_identity("alice")
bob = generate_identity("bob")
print("alice X25519 pub (first 8B):", alice.x25519_public[:8].hex())
print("bob X25519 pub (first 8B):", bob.x25519_public[:8].hex())
print("alice ML-KEM pub size :", len(alice.ml_kem_public))
print("bob ML-KEM pub size :", len(bob.ml_kem_public))
alice X25519 pub (first 8B): 39f84640ab7fca1d
bob X25519 pub (first 8B): 9143a34e5bedee34
alice ML-KEM pub size : 1184
bob ML-KEM pub size : 1184
Step 2 — Alice initiates#
initiate_session runs X25519 DH, ML-KEM encapsulation, and the HKDF derivation.
sess_a, handshake = initiate_session(
ours=alice, peer_name=bob.name,
peer_x25519_pub=bob.x25519_public,
peer_ml_kem_pub=bob.ml_kem_public,
)
print("root_key (first 8B): ", sess_a.root_key[:8].hex())
print("chain_key_send (first 8B): ", sess_a.chain_key_send[:8].hex())
print("chain_key_recv (first 8B): ", sess_a.chain_key_recv[:8].hex())
print("ephemeral_pk size: ", len(handshake.ephemeral_pk))
print("kem_ciphertext size: ", len(handshake.kem_ciphertext))
root_key (first 8B): f3b43285b4c13f23
chain_key_send (first 8B): 4b3747c1e1f1df16
chain_key_recv (first 8B): 4d3a73d912de561c
ephemeral_pk size: 32
kem_ciphertext size: 1088
Step 3 — Bob accepts#
accept_session uses Bob’s private keys and the handshake fields Alice sent.
sess_b = accept_session(
ours=bob, peer_name=alice.name,
peer_x25519_pub=alice.x25519_public,
handshake=handshake,
)
print("root_key match: ", sess_a.root_key == sess_b.root_key)
print("chain_a2b mirrored: ", sess_a.chain_key_send == sess_b.chain_key_recv)
print("chain_b2a mirrored: ", sess_a.chain_key_recv == sess_b.chain_key_send)
root_key match: True
chain_a2b mirrored: True
chain_b2a mirrored: True
Why both lines must succeed#
The two chain keys are directional: one for Alice→Bob, one for Bob→Alice. After this handshake, each party can encrypt and the other can decrypt, independently. The next notebook cranks the symmetric ratchet ten steps to show forward secrecy — and where it stops.