ZAP Protocol
Advanced Topics

Post-Quantum Cryptography

ML-KEM, ML-DSA, and hybrid key exchange in ZAP

Post-Quantum Cryptography

ZAP implements NIST-approved post-quantum algorithms to protect against future quantum computer attacks.

Why Post-Quantum?

Current encryption (RSA, ECDSA, ECDH) will be broken by large-scale quantum computers. "Harvest now, decrypt later" attacks mean sensitive data encrypted today could be exposed in the future.

ZAP uses:

  • ML-KEM-768 (NIST FIPS 203) for key encapsulation
  • ML-DSA-65 (NIST FIPS 204) for digital signatures
  • Hybrid handshake combining X25519 + ML-KEM for transitional security

Key Types

ML-KEM (Kyber)

Key encapsulation mechanism for secure key exchange:

struct MLKEMPublicKey {
  # ML-KEM-768 public key (1184 bytes)
  data @0 :Data;
}

struct MLKEMCiphertext {
  # ML-KEM-768 ciphertext (1088 bytes)
  data @0 :Data;
}

ML-DSA (Dilithium)

Digital signature scheme for authentication:

struct MLDSAPublicKey {
  # ML-DSA-65 public key (1952 bytes)
  data @0 :Data;
}

struct MLDSASignature {
  # ML-DSA-65 detached signature (3293 bytes)
  data @0 :Data;
}

Hybrid Handshake

ZAP uses a hybrid handshake combining classical and post-quantum algorithms:

Handshake Flow

Client                                     Server
  |                                           |
  |  PQHandshakeInit                          |
  |  - X25519 ephemeral public key (32B)      |
  |  - ML-KEM-768 public key (1184B)          |
  |  - Nonce (32B)                            |
  |  - Version                                |
  |  - Optional: identity key + signature     |
  |------------------------------------------>|
  |                                           |
  |  PQHandshakeResponse                      |
  |  - X25519 ephemeral public key (32B)      |
  |  - ML-KEM ciphertext (1088B)              |
  |  - Client nonce echo                      |
  |  - Server nonce (32B)                     |
  |  - Optional: identity key + signature     |
  |<------------------------------------------|
  |                                           |
  |  [Both derive shared secret]              |
  |  ss = HKDF(X25519_DH || ML-KEM_decap)     |
  |                                           |
  |  PQChannelMessage (encrypted RPC)         |
  |<==========================================>|

Init Message

struct PQHandshakeInit {
  # Client's X25519 ephemeral public key (32 bytes)
  x25519PublicKey @0 :Data;

  # Client's ML-KEM-768 public key for key encapsulation
  mlkemPublicKey @1 :MLKEMPublicKey;

  # Optional: Client's ML-DSA identity public key for authentication
  identityKey @2 :MLDSAPublicKey;

  # Optional: signature over (x25519PublicKey || mlkemPublicKey)
  identitySignature @3 :MLDSASignature;

  # Random nonce to prevent replay attacks
  nonce @4 :Data;

  # Protocol version
  version @5 :UInt16;
}

Response Message

struct PQHandshakeResponse {
  # Server's X25519 ephemeral public key (32 bytes)
  x25519PublicKey @0 :Data;

  # ML-KEM ciphertext containing encapsulated shared secret
  mlkemCiphertext @1 :MLKEMCiphertext;

  # Optional: Server's ML-DSA identity public key
  identityKey @2 :MLDSAPublicKey;

  # Optional: signature over (clientNonce || x25519PublicKey || mlkemCiphertext)
  identitySignature @3 :MLDSASignature;

  # Echo client nonce to bind response to request
  clientNonce @4 :Data;

  # Server nonce for session binding
  serverNonce @5 :Data;
}

Key Derivation

The shared secret combines both classical and post-quantum sources:

X25519_DH = X25519(client_sk, server_pk)
ML_KEM_SS = ML-KEM.Decapsulate(client_sk, ciphertext)

shared_secret = HKDF-SHA256(
  IKM = X25519_DH || ML_KEM_SS,
  salt = client_nonce || server_nonce,
  info = "ZAP-PQ-1"
)

This ensures security even if one algorithm is broken.

Channel Encryption

After handshake, all messages are encrypted:

struct PQChannelMessage {
  # Sequence number for replay protection
  sequence @0 :UInt64;

  # AEAD-encrypted payload (ChaCha20-Poly1305 or AES-256-GCM)
  ciphertext @1 :Data;

  # AEAD authentication tag
  tag @2 :Data;

  # Associated data (plaintext, authenticated but not encrypted)
  associatedData @3 :Data;
}

Key Rotation

For long-lived sessions, keys can be rotated:

struct PQKeyRotation {
  # New ML-KEM public key for next epoch
  newMlkemPublicKey @0 :MLKEMPublicKey;

  # ML-KEM ciphertext for the new key
  mlkemCiphertext @1 :MLKEMCiphertext;

  # Signature over (epoch || newMlkemPublicKey) with identity key
  signature @2 :MLDSASignature;

  # Epoch number (monotonically increasing)
  epoch @3 :UInt64;
}

Usage Examples

Rust

use zap_protocol::pq::{Identity, connect_pq};

// Generate identity
let identity = Identity::generate()?;
identity.save("./identity.key")?;

// Connect with PQ-TLS
let client = connect_pq("zap+pq://localhost:9000", identity).await?;

Go

import "github.com/zap-protocol/zap-go/pq"

// Generate identity
identity, _ := pq.GenerateIdentity()
identity.Save("./identity.key")

// Connect with PQ-TLS
client, _ := zap.Connect(ctx, "zap+pq://localhost:9000",
    zap.WithPQIdentity(identity))

Python

from zap_protocol.pq import Identity, connect_pq

# Generate identity
identity = Identity.generate()
identity.save("./identity.key")

# Connect with PQ-TLS
client = await connect_pq("zap+pq://localhost:9000", identity)

Security Considerations

  1. Key Storage: ML-DSA private keys are large (~4KB). Use secure storage.
  2. Memory Safety: Zeroize key material after use.
  3. Side Channels: Use constant-time implementations.
  4. Hybrid Security: Security relies on at least one algorithm being secure.

Performance

OperationTime (ms)Size (bytes)
ML-KEM keygen0.11184 pub, 2400 priv
ML-KEM encap0.151088 ct, 32 ss
ML-KEM decap0.1532 ss
ML-DSA keygen0.51952 pub, 4032 priv
ML-DSA sign1.53293 sig
ML-DSA verify0.3-
Full handshake~5~4KB total

References

Last updated on

On this page