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
- Key Storage: ML-DSA private keys are large (~4KB). Use secure storage.
- Memory Safety: Zeroize key material after use.
- Side Channels: Use constant-time implementations.
- Hybrid Security: Security relies on at least one algorithm being secure.
Performance
| Operation | Time (ms) | Size (bytes) |
|---|---|---|
| ML-KEM keygen | 0.1 | 1184 pub, 2400 priv |
| ML-KEM encap | 0.15 | 1088 ct, 32 ss |
| ML-KEM decap | 0.15 | 32 ss |
| ML-DSA keygen | 0.5 | 1952 pub, 4032 priv |
| ML-DSA sign | 1.5 | 3293 sig |
| ML-DSA verify | 0.3 | - |
| Full handshake | ~5 | ~4KB total |
References
Last updated on