This project implements a research protocol for measuring post-quantum session setup across different networking stacks. It is not a TLS implementation. The point is to keep the cryptographic protocol stable while changing the transport path underneath it.
The default suite is:
KYBER768+DILITHIUM3+SHAKE256+AESGCM
The C++ handshake benchmark can also sweep security levels:
| Level | KEM | Signature |
|---|---|---|
| Level1 | Kyber512 | Dilithium2 |
| Level3 | Kyber768 | Dilithium3 |
| Level5 | Kyber1024 | Dilithium5 |
The session setup is a four-message mutually authenticated exchange:
Client Server
| |
| CLIENT_HELLO: nonce, KEM pk, cert, sig |
|----------------------------------------->|
| | verify client signature
| SERVER_HELLO: nonce, KEM pk, cert, sig |
|<-----------------------------------------|
| verify server signature |
| CLIENT_KEM: ct to server KEM pk, sig |
|----------------------------------------->|
| | verify, decapsulate
| SERVER_KEM: ct to client KEM pk, sig |
|<-----------------------------------------|
| verify, decapsulate |
Both sides contribute ephemeral KEM material. Each signature covers the current message plus transcript context, so later messages are bound to the earlier exchange.
Messages are serialized canonically before signing and before key derivation. The protocol uses SHAKE256 domain separators for each signed context:
| Context | Purpose |
|---|---|
C-hello |
Client initial authentication |
S-hello |
Server authentication bound to client hello |
C-kem |
Client KEM ciphertext bound to accumulated transcript |
S-kem |
Server KEM ciphertext bound to accumulated transcript |
handshake |
Full transcript hash for key derivation |
The two KEM shared secrets and the full transcript feed a SHAKE256 extract-and-expand derivation:
transcript_hash = SHAKE256("handshake||" || full_transcript, 64)
prk = SHAKE256("EXTRACT||" || ss_client || ss_server || transcript_hash, 64)
client_write_key = SHAKE256("HKDF-v1||client write key||" || prk, 32)
server_write_key = SHAKE256("HKDF-v1||server write key||" || prk, 32)
client_base_iv = SHAKE256("HKDF-v1||client base iv||" || prk, 12)
server_base_iv = SHAKE256("HKDF-v1||server base iv||" || prk, 12)
After the handshake, the data path uses AES-256-GCM. The nonce follows the TLS-style construction:
nonce = base_iv XOR (0x00000000 || uint64_sequence_number)
AAD binds direction and sequence number:
v1||KYBER+DILITHIUM||direction||uint64_sequence_number
The receiver tracks the highest accepted sequence number and rejects stale encrypted messages.
TCP-style transports use a 4-byte big-endian length prefix followed by canonical JSON payloads. DPDK UDP uses application-level fragmentation/reassembly for PQC messages that exceed MTU.