Research preview

Key Exchange Guide

Wire VORTEX-256 into a real client–server protocol with serialization and session keys.

Skip to content

Protocol overview

Step 1 · Server key generation

Generate once at startup (or during provisioning):

python
from vortex_pqc import generate_keypair, PEMKind, write_pem_file

kp = generate_keypair()

# Persist for restart
write_pem_file("/etc/vortex/server.key", PEMKind.PRIVATE_KEY, kp.private_key)
write_pem_file("/etc/vortex/server.pub",  PEMKind.PUBLIC_KEY,  kp.public_key)

The server keeps kp.private_key (1248 B) secret. The public key (800 B) is sent to clients.

Step 2 · Client encapsulation

When a client connects, it receives the server's public key and encapsulates:

python
from vortex_pqc import encapsulate
import hashlib

server_pk = receive_public_key()          # 800 bytes from wire
result    = encapsulate(server_pk)

ciphertext    = result.data               # 768 bytes → send to server
shared_secret = result.shared_secret      # 32 bytes  → keep locally

# Derive an application session key — don't use raw shared_secret directly
session_key = hashlib.hkdf_sha256(
    shared_secret,
    salt=b"vortex-session-v1",
    info=b"tls-like-channel",
    length=32,
)

Step 3 · Server decapsulation

python
from vortex_pqc import decapsulate, read_pem_file, PEMKind
import hashlib

sk = read_pem_file("/etc/vortex/server.key", PEMKind.PRIVATE_KEY)
ct = receive_ciphertext()                 # 768 bytes from wire

shared_secret = decapsulate(ct, sk)

session_key = hashlib.hkdf_sha256(
    shared_secret,
    salt=b"vortex-session-v1",
    info=b"tls-like-channel",
    length=32,
)

Both parties now hold the same session_key.

Wire format

VORTEX does not define a transport protocol. A minimal framing:

┌──────────────────────────────────────────────┐
│ Magic: "VTX1" (4 bytes)                      │
│ Type:  0x01 = public key, 0x02 = ciphertext │
│ Length: uint32 big-endian                    │
│ Payload: raw bytes (800 or 768)             │
└──────────────────────────────────────────────┘

Example encoder:

python
import struct

def frame(msg_type: int, payload: bytes) -> bytes:
    return b"VTX1" + struct.pack(">BI", msg_type, len(payload)) + payload

def unframe(data: bytes) -> tuple[int, bytes]:
    assert data[:4] == b"VTX1"
    msg_type, length = struct.unpack(">BI", data[4:9])
    payload = data[9:9 + length]
    assert len(payload) == length
    return msg_type, payload

Complete minimal example

python
#!/usr/bin/env python3
"""Minimal VORTEX-256 key exchange demo."""

import hashlib
from vortex_pqc import generate_keypair, encapsulate, decapsulate

def hkdf(secret: bytes, info: bytes) -> bytes:
    return hashlib.hkdf_derive(
        "sha256", 32, secret, salt=b"vortex-demo", info=info
    )

# ── Server ──────────────────────────────────────────────
server = generate_keypair()

# ── Client receives pk, encapsulates ────────────────────
client_result = encapsulate(server.public_key)
client_key = hkdf(client_result.shared_secret, b"client")

# ── Client sends ciphertext; server decapsulates ────────
server_secret = decapsulate(client_result.data, server.private_key)
server_key = hkdf(server_secret, b"client")

assert client_key == server_key
print(f"Session key: {client_key.hex()}")

Best practices

PracticeWhy
Derive session keys with HKDFDomain separation; don't use raw KEM output as AES key
Validate payload lengths before crypto ops800 B for pk, 768 B for ct — reject anything else
Authenticate the server's public keyBind pk to identity (certificate, TOFU, config pin)
One encapsulation per sessionFresh randomness = forward secrecy for that session
Don't compare shared secrets with == in productionUse hmac.compare_digest if you must compare

Common patterns

Pattern: Long-lived server key, ephemeral sessions
Server generates key pair once (days/months)
Each client connection:
  Client encapsulates → fresh 32-byte secret per session
  Both derive session key with HKDF(salt=connection_id)

This is the standard KEM usage model.

Pattern: Mutual authentication (both sides encapsulate)
Server has keypair A, Client has keypair B
  ss1 = encapsulate(pk_A)   → client holds ss1
  ss2 = encapsulate(pk_B)   → server holds ss2
  session_key = HKDF(ss1 || ss2)

Requires both parties to have key pairs. Combine with identity binding.