<1ms
Kyber Keygen
<1ms
Dilithium Sign
17/17
PQ Tests Passing
0ms
Join Latency Impact

Why Video Conferencing Is Uniquely Vulnerable

The cryptographic threat model for video is different from web browsing. When you load a webpage over TLS, the session is ephemeral — the data itself has a short shelf life. A leaked quarterly report matters for a few hours. But video conferences are where executive decisions get made, where M&A discussions happen, where classified briefings occur. The intelligence value of those recordings does not decay.

This is the harvest-now-decrypt-later (HNDL) attack. An adversary with passive access to encrypted traffic — a compromised backbone router, a mirror port at a peering exchange, a national surveillance program — records your encrypted video sessions today. The ciphertext sits in cold storage. In 10 or 15 years, when a sufficiently large quantum computer runs Shor's algorithm against the ECDH key exchange that protected those sessions, every recorded meeting decrypts at once.

For most web traffic, HNDL is a theoretical concern. For board meetings, legal strategy sessions, and government briefings conducted over video, it is an operational reality. NSA's CNSA 2.0 timeline calls for post-quantum key exchange in all classified systems by 2030. We decided not to wait.

The uncomfortable truth: Every video platform using classical-only ECDH key exchange is producing traffic that a quantum computer can retroactively decrypt. The TLS record layer is AES-256-GCM (quantum-resistant), but the key exchange that negotiated those AES keys is ECDH P-256 or X25519 (quantum-vulnerable). The weakest link determines the security.

The Architecture: Hybrid Key Exchange

We did not replace classical cryptography. We layered post-quantum on top of it. The approach is called a hybrid key exchange: both X25519 (classical ECDH) and ML-KEM-768 (FIPS 203, the algorithm formerly known as Kyber) run in parallel. The session key is derived from both shared secrets. If Kyber turns out to have a flaw, X25519 still protects the session. If a quantum computer breaks X25519, Kyber still protects the session. You need to break both simultaneously to recover the key.

Here is the protocol as implemented in our TURN server crate (v100-turn/src/pqc/hybrid.rs):

Hybrid Key Exchange Protocol
Alice (Initiator)                              Bob (Responder)

  Generate X25519 keypair                          Generate X25519 keypair
  Generate ML-KEM-768 keypair                      Generate ML-KEM-768 keypair
       |                                                |
       |  ---- public bundle (32B X25519 + 1184B Kyber) ---->
       |  <---- public bundle (32B X25519 + 1184B Kyber) ----
       |                                                |
  X25519 DH(alice_sk, bob_pk)                     |
  Kyber encapsulate(bob_kyber_pk)                 |
       |                                                |
       |  ---- ephemeral X25519 pk + Kyber CT (1088B) ------>
       |                                                |
       |                                  X25519 DH(bob_sk, alice_ephemeral_pk)
       |                                  Kyber decapsulate(CT, bob_kyber_sk)
       |                                                |
  session_key = SHA3-256(                        session_key = SHA3-256(
    "V100-HYBRID-KX-V1"                          "V100-HYBRID-KX-V1"
    || x25519_shared                               || x25519_shared
    || kyber_shared                                || kyber_shared
  )                                              )

  Both sides derive the identical 32-byte session key.
  Domain separation tag prevents cross-context key reuse.

The domain separation tag "V100-HYBRID-KX-V1" is critical. Without it, a shared secret derived for one purpose (video key exchange) could be confused with a shared secret derived for another purpose (TURN credential wrapping). The tag is prepended to the SHA3-256 input, ensuring that identical raw key material produces different session keys in different contexts.

The Combiner Function

This is the actual code from v100-turn/src/pqc/hybrid.rs that derives the session key:

v100-turn/src/pqc/hybrid.rs
/// Derive a combined shared secret from X25519 DH + ML-KEM. /// /// Output: SHA3-256("V100-HYBRID-KX-V1" || classical_shared || pq_shared) /// Domain separation tag prevents cross-context key reuse. /// Using a hash combiner ensures that the output is uniformly random /// as long as at least one input has sufficient entropy. pub fn combine_secrets(classical_shared: &[u8], pq_shared: &[u8]) -> Vec<u8> { let mut hasher = Sha3_256::new(); hasher.update(b"V100-HYBRID-KX-V1"); // Domain separation hasher.update(classical_shared); hasher.update(pq_shared); hasher.finalize().to_vec() }

We chose SHA3-256 (Keccak) over HKDF-SHA256 for the combiner because SHA3's sponge construction provides a clean domain separation model. The concatenated input is absorbed entirely before any output is squeezed, which means the domain tag, classical secret, and post-quantum secret are all committed before the key is derived. SHA3-256 is also quantum-resistant — Grover's algorithm only halves its security to 128-bit equivalent, which remains well above the security margin.

ML-KEM-768: The Key Encapsulation Layer

Our Kyber implementation uses pqcrypto-mlkem 0.1, which wraps the PQClean reference implementation of ML-KEM-768 (FIPS 203). This is NIST security Level 3 — equivalent to AES-192 classical security. We considered ML-KEM-1024 (Level 5), but the key and ciphertext sizes are substantially larger, and Level 3 already exceeds the security margin of the X25519 side of the hybrid exchange.

Parameter ML-KEM-768 Notes
Public Key 1,184 bytes Broadcast via signaling channel
Secret Key 2,400 bytes Never leaves the participant's session
Ciphertext 1,088 bytes Sent from initiator to responder
Shared Secret 32 bytes Input to SHA3-256 combiner
NIST Level 3 Equivalent to AES-192
FIPS Standard FIPS 203 Finalized August 2024

The signaling overhead is modest. A classical-only WebRTC offer/answer exchange is roughly 2-4KB of SDP. Our hybrid key bundle adds 1,216 bytes (32B X25519 + 1,184B Kyber public key) per participant, and the key exchange response adds 1,120 bytes (32B ephemeral X25519 + 1,088B Kyber ciphertext). For a 10-participant meeting, the total PQ signaling overhead is under 25KB — transmitted once at join time, not per-frame.

v100-turn/src/pqc/kyber.rs
/// Encapsulate a shared secret using the recipient's public key. /// /// The caller sends the ciphertext to the recipient, who decapsulates /// with their secret key to derive the same 32-byte shared secret. pub fn encapsulate(public_key: &[u8]) -> Result<Encapsulated, String> { let pk = mlkem768::PublicKey::from_bytes(public_key) .map_err(|_| format!( "Invalid public key length: expected {}, got {}", KYBER_PUBLICKEYBYTES, public_key.len() ))?; let (shared_secret, ciphertext) = mlkem768::encapsulate(&pk); Ok(Encapsulated { ciphertext: ciphertext.as_bytes().to_vec(), shared_secret: shared_secret.as_bytes().to_vec(), }) }

TURN Credential Wrapping: Protecting the Relay Layer

Here is a threat most people miss. WebRTC uses TURN (Traversal Using Relays around NAT) servers to relay media when direct peer-to-peer connections fail. To authenticate to a TURN server, the client presents an HMAC-SHA1 credential. That credential is typically delivered over TLS — which brings us back to the ECDH key exchange problem.

If an adversary records the TLS session where the client received its TURN credential, and later breaks the TLS key exchange with a quantum computer, they recover the TURN credential. With that credential, they can authenticate to the relay server and potentially intercept relayed media streams.

V100 wraps TURN credentials with an additional layer: Kyber encapsulation + AES-256-GCM. The server generates the HMAC-SHA1 TURN credential, then encrypts it with a fresh Kyber encapsulation before transmitting it. Even if the outer TLS layer is retroactively broken, the credential remains protected by the lattice-based KEM.

v100-turn/src/pqc/kyber.rs
/// Wrap a credential using Kyber KEM + AES-256-GCM. /// /// 1. Encapsulate with the recipient's Kyber public key to get a shared secret. /// 2. Encrypt the credential with AES-256-GCM using the KEM shared secret as key. /// 3. Return the WrappedCredential so the recipient can unwrap. /// /// This replaces the old XOR-based wrapping with proper AEAD encryption, /// supporting credentials of any length. pub fn wrap_credential(credential: &[u8], recipient_pk: &[u8]) -> Result<WrappedCredential, String> { let pk = mlkem768::PublicKey::from_bytes(recipient_pk) .map_err(|_| "Invalid Kyber public key".to_string())?; let (shared_secret, ciphertext) = mlkem768::encapsulate(&pk); // AES-256-GCM encrypt the credential with KEM shared secret let cipher = Aes256Gcm::new_from_slice(shared_secret.as_bytes()) .map_err(|e| format!("AES key error: {}", e))?; let mut nonce_bytes = [0u8; 12]; rand::thread_rng().fill_bytes(&mut nonce_bytes); let nonce = Nonce::from_slice(&nonce_bytes); let encrypted = cipher .encrypt(nonce, credential) .map_err(|e| format!("AES-GCM encrypt failed: {}", e))?; Ok(WrappedCredential { kyber_ct: ciphertext.as_bytes().to_vec(), encrypted_credential: encrypted, nonce: nonce_bytes.to_vec(), }) }

Note that this replaced an earlier XOR-based wrapping scheme. XOR wrapping only works when the shared secret and the credential are the same length (32 bytes). AES-256-GCM supports arbitrary-length credentials and provides authenticated encryption — the recipient can verify that the ciphertext has not been tampered with in transit.

ML-DSA-65: Signing Meeting Artifacts

Key exchange protects data in transit. But video conferencing also produces artifacts that need integrity guarantees at rest: recording files, transcripts, AI-generated meeting summaries. If an adversary modifies a recording manifest — changing the participant list, altering timestamps, splicing in fabricated segments — you need a signature that proves the manifest has not been tampered with.

V100 signs all meeting artifacts with ML-DSA-65 (FIPS 204, the algorithm formerly known as Dilithium). Here is the signing stack:

Artifact What's Signed Algorithm
Recording Manifests SHA3-256 hash of the manifest JSON ML-DSA-65
Transcripts SHA3-256 hash of the transcript content ML-DSA-65
Meeting Summaries SHA3-256 hash of the AI-generated summary ML-DSA-65
API Key Envelopes SHA3-256 hash of the key material ML-DSA-65 / H33-3-Key

ML-DSA-65 signatures are 3,309 bytes with 1,952 byte public keys. These are larger than Ed25519 (64-byte signatures, 32-byte public keys), but for meeting artifacts that are typically kilobytes or megabytes, the overhead is negligible. The signatures are stored alongside the artifacts and can be verified independently by any party with the public key.

v100-turn/src/pqc/dilithium.rs
/// A Dilithium signing keypair for meeting artifact integrity. pub struct SigningKeypair { pub public_key: Vec<u8>, secret_key: Vec<u8>, } impl SigningKeypair { /// Generate a fresh ML-DSA-65 signing keypair. pub fn generate() -> Self { let (pk, sk) = mldsa65::keypair(); Self { public_key: pk.as_bytes().to_vec(), secret_key: sk.as_bytes().to_vec(), } } /// Sign a message (meeting artifact) and return the detached signature. pub fn sign(&self, message: &[u8]) -> Vec<u8> { let sk = mldsa65::SecretKey::from_bytes(&self.secret_key) .expect("SigningKeypair holds a valid secret key"); let sig = mldsa65::detached_sign(message, &sk); sig.as_bytes().to_vec() } }

What We Didn't Change (and Why)

Post-quantum migration is not about replacing everything. It is about identifying which primitives are quantum-vulnerable and which are not. We deliberately left the following unchanged:

The rule of thumb: If the algorithm uses a shared secret or symmetric key, it is not quantum-vulnerable (Grover only halves the security level). If the algorithm relies on the hardness of factoring, discrete logarithms, or elliptic curve discrete logarithms, it is quantum-vulnerable (Shor breaks it entirely). We replaced the second category and left the first alone.

Performance: Zero Measurable Impact

Post-quantum cryptography has a reputation for being slow. That reputation is outdated. On modern hardware, ML-KEM-768 keygen, encapsulation, and decapsulation all complete in sub-millisecond time. ML-DSA-65 signing and verification are similarly fast.

The entire hybrid key exchange — X25519 DH + Kyber encapsulate/decapsulate + SHA3-256 combine — adds zero measurable latency to the meeting join flow. The key exchange happens during the WebSocket signaling phase, which is already dominated by network round-trip time (typically 50-200ms). Sub-millisecond cryptographic operations are invisible in that context.

1,216B
Public Bundle Size
1,120B
Exchange Payload
3,309B
Signature Size
1,952B
Signing Public Key

The bandwidth overhead deserves scrutiny. In a 10-person meeting, the total post-quantum signaling overhead is approximately 23KB (key bundles + exchange payloads). A single 720p video frame at H.264 baseline is roughly 30-100KB. The entire PQ key exchange for the entire meeting consumes less bandwidth than a single video frame.

The Crate Stack

We unified on the pqcrypto family of crates across both the TURN server and the API gateway, eliminating an earlier split where the TURN server used pqc_kyber and the gateway used pqcrypto-mlkem. The unified stack:

v100-turn/Cargo.toml (PQ dependencies)
pqcrypto-mlkem = "0.1" # ML-KEM-768 (FIPS 203) pqcrypto-mldsa = "0.1" # ML-DSA-65 (FIPS 204) pqcrypto-traits = "0.3" # Trait unification layer x25519-dalek = { version = "2", features = ["static_secrets"] } aes-gcm = "0.10" # AEAD for credential wrapping sha3 = "0.10" # SHA3-256 combiner + content hashing

The pqcrypto-mlkem and pqcrypto-mldsa crates wrap the PQClean C reference implementations with safe Rust bindings. The C code is compiled from source during the build, not dynamically linked, which means the exact algorithm implementation is pinned to the crate version. No supply chain ambiguity about which Kyber variant is running.

What We Haven't Shipped Yet

Honest status: The hybrid key exchange is complete and tested. Frame-level encryption with SFrame is next. Today, the post-quantum session key protects the signaling and credential exchange. The media frames themselves still rely on DTLS-SRTP with classical key exchange. Shipping SFrame (RFC 9605) with the PQ-derived session key is the next engineering milestone.

The gap is real and we are not going to pretend otherwise. Here is the current state:

The Test Suite

All 17 post-quantum tests pass in CI on every commit. The tests cover the critical properties that a PQ implementation must guarantee:

v100-turn/src/pqc/ test suite (excerpt)
// Hybrid key exchange produces identical secrets on both sides fn test_hybrid_key_exchange_roundtrip() { let alice = HybridKeypair::generate().unwrap(); let bob = HybridKeypair::generate().unwrap(); let encap = alice.initiate(&bob.classical_public, &bob.pq_public).unwrap(); let bob_secret = bob.respond(&encap.classical_public, &encap.pq_ciphertext).unwrap(); assert_eq!(encap.shared_secret, bob_secret); // Both derive the same key assert_eq!(encap.shared_secret.len(), 32); } // Each session produces different keys (ML-KEM randomized encapsulation) fn test_different_sessions_different_secrets() { let alice = HybridKeypair::generate().unwrap(); let bob = HybridKeypair::generate().unwrap(); let encap1 = alice.initiate(&bob.classical_public, &bob.pq_public).unwrap(); let encap2 = alice.initiate(&bob.classical_public, &bob.pq_public).unwrap(); assert_ne!(encap1.shared_secret, encap2.shared_secret); // Randomized } // TURN credential wrap/unwrap roundtrip with AES-256-GCM fn test_credential_wrap_unwrap() { let kp = generate_keypair().unwrap(); let credential = b"turn-password-abc123"; let wrapped = wrap_credential(credential, &kp.public_key).unwrap(); let recovered = unwrap_credential(&wrapped, &kp.secret_key).unwrap(); assert_eq!(recovered, credential); } // Dilithium detects tampered artifacts fn test_dilithium_tamper_detection() { let kp = SigningKeypair::generate(); let sig = kp.sign(b"Original transcript"); assert!(!verify(b"Tampered transcript", &sig, &kp.public_key).unwrap()); }

Open Source Considerations

We are considering open-sourcing our TURN server implementation, including the post-quantum credential wrapping layer. The PQ key exchange protocol is not a trade secret — the security of a KEM does not depend on the algorithm being secret (that would be security through obscurity). Publishing the implementation allows independent audit, which strengthens confidence in the cryptographic choices.

The TURN server is a standalone Rust crate (v100-turn) with clean module boundaries: src/pqc/kyber.rs, src/pqc/dilithium.rs, src/pqc/hybrid.rs. The PQ modules have no dependency on V100's proprietary signaling or billing infrastructure. They could be extracted and used by any Rust-based WebRTC stack.

If you are building a video platform and want to discuss PQ integration, or if you are a cryptographer who wants to review our combiner construction, reach out to engineering@v100.ai. We would rather have our implementation scrutinized than silently wrong.

Reproduce our claims. The 17 PQ tests run in our CI pipeline on every commit. The key exchange roundtrip, credential wrapping, Dilithium signing, and tamper detection are all exercised automatically. If we break post-quantum, CI breaks first.

Conclusion

Post-quantum encrypted video conferencing is not a future-tense statement. The hybrid key exchange is implemented, tested, and running. ML-KEM-768 protects session key negotiation. ML-DSA-65 signs every recording, transcript, and summary. TURN credentials are wrapped with Kyber + AES-256-GCM. The crate stack is unified. The tests pass.

The honest gap is frame-level encryption. SFrame integration is next. When it ships, every video frame in a V100 meeting will be encrypted with a key that was negotiated using post-quantum key exchange. No other video platform has published this architecture, because no other video platform has built it.

Quantum computers capable of breaking ECDH may be 10 years away or 20. The recordings of today's board meetings, legal briefings, and classified discussions will still exist then. We decided the right time to deploy post-quantum protection is before the threat materializes, not after.