Hands-On Post Quantum TLS Lab
The threat of quantum computing is no longer a general idea. It already shapes how we design serious security systems. Standards bodies such as NIST are working to finalize post-quantum cryptography to counter the “Harvest Now, Decrypt Later” approach, in which attackers capture encrypted traffic today and wait until powerful quantum computers can break classical public-key systems.
You do not need quantum hardware to build and test a stack ready for post-quantum cryptography. With basic virtualization, Apache, OpenSSL 3, and Wireshark, you can create a local lab and explore post-quantum cryptography on your own terms, rather than trusting polished marketing language or automated claims.
In this guide, we build a practical post-quantum lab from the ground up. First, you capture a normal TLS 1.3 handshake. Next, you upgrade the server to use a hybrid post-quantum key agreement (X25519MLKEM768). Finally, you confirm that the connection is truly protected by post-quantum cryptography by examining browser developer tools and packet captures, rather than relying on vendor or tool statements.
All bash commands in this post are available at the Post-Quantum Cryptography Lab.
Environment Setup & Requirements
Before diving into the installation, ensure you have the necessary environment components in place. This lab is designed to run locally using a hypervisor and your primary workstation.
- Server Environment (Virtual Machine):
- OS: Ubuntu (recommended 22.04 LTS or newer) running in a VM (e.g., VirtualBox, VMware, or KVM).
- Networking: Configured with a static or predictable local IP address (e.g.,
192.168.56.10). - Software Stack: Apache HTTP Server and OpenSSL 3.x pre-installed.
- Client Environment (Your Host Machine):
- Operating System: Windows, macOS, or Linux.
- Browser: A modern web browser with post-quantum key exchange support enabled (Google Chrome, Microsoft Edge, or Mozilla Firefox).
- Packet Analyzer: Wireshark installed to capture and inspect TLS handshake packets.
- Network / Host Resolution (Optional but Recommended):
- On your client machine, add the following entry to your hosts file (
C:\Windows\System32\drivers\etc\hostson Windows or/etc/hostson Linux/macOS): 192.168.56.10 pqc-lab.local
- On your client machine, add the following entry to your hosts file (
Lab Topology
Our setup is straightforward:
- Server: An Ubuntu virtual machine running Apache and OpenSSL 3 (
192.168.56.10). - Client: Your host machine equipped with a modern browser and Wireshark.
Phase 1: Establishing the Classical Baseline (No PQC)
Before we introduce post-quantum algorithms, we need a standard, classical TLS 1.3 baseline to compare against.
Install Apache, OpenSSL, and Configure HTTPS
SSH into your Ubuntu server and ensure you are running OpenSSL 3.x:
sudo apt update
sudo apt install apache2 openssl
openssl version # Verify you see 3.x
Enable the SSL module and generate a self-signed certificate for our lab domain:
sudo a2enmod ssl
sudo mkdir -p /etc/apache2/ssl
sudo openssl req -x509 -nodes -newkey rsa:2048 \
-keyout /etc/apache2/ssl/test.key \
-out /etc/apache2/ssl/test.crt \
-days 365
Tip: Save one Wireshark capture file and one browser security tab screenshot for the clean TLS 1.3 run, so you can compare them directly with your later post-quantum handshake.
Next, configure the default SSL virtual host (/etc/apache2/sites-available/default-ssl.conf) to restrict protocols to TLS 1.2 and 1.3, specifying standard modern cipher suites:
<VirtualHost _default_:443>
ServerName pqc-lab.local
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/test.crt
SSLCertificateKeyFile /etc/apache2/ssl/test.key
SSLProtocol -ALL +TLSv1.3 +TLSv1.2
SSLCipherSuite TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256
</VirtualHost>
Enable the site and reload Apache:
sudo a2ensite default-ssl
sudo systemctl reload apache2
Inspecting the Classical Connection
Open your browser and navigate to https://pqc-lab.local (accepting the self-signed certificate warning). Open DevTools (F12) and head to the Security tab. You’ll notice:
- TLS Version:
TLS 1.3 - Key Exchange:
X25519(classical Elliptic Curve Diffie-Hellman)
If you capture this in Wireshark using the display filter tls.handshake.type == 1 || tls.handshake.type == 2you’ll see the Client Hello offering various groups and the Server Hello choosing standard X25519 with a small key share payload (~36 bytes).
Sanity Check: Phase 1 Configuration Test
Run the Apache syntax check and verify that local loopback HTTPS responds properly to a classical connection query:
sudo apache2ctl configtest
curl -kiv https://localhost
Expected output: You should see a successful TLS 1.3 handshake negotiate a classical cipher suite and return standard Apache HTML output (ignoring certificate warnings with -k).
Headless Packet Analysis with tcpdump & tshark
If you’re running headless on your server or prefer extracting packet telemetry straight from the terminal instead of clicking through the Wireshark GUI, you can capture traffic with tcpdump and parse out your TLS handshake fields and ciphersuites into clean CSV output using tshark.
Capture Live TLS Traffic to a PCAP File
Run tcpdump on your active network interface (e.g., en0 or eth0) targeting port 443:
sudo tcpdump -i en0 -w capture.pcap port 443
While tcpdump is running, browse to [https://pqc-lab.local](https://pqc-lab.local). Once loaded, hit Ctrl+C to stop the capture.
Extract Client Hello Handshake Telemetry (Supported Groups)
tshark -r capture.pcap \
-Y "tls.handshake.type == 1" \
-T fields \
-e frame.number \
-e ip.src \
-e ip.dst \
-e tls.handshake.extensions_supported_group \
-E header=y -E separator=, -E quote=d -E occurrence=f
Extract Server Hello Handshake Telemetry & Ciphersuites
tshark -r capture.pcap \
-Y "tls.handshake.type == 2" \
-T fields \
-e frame.number \
-e ip.src \
-e ip.dst \
-e tls.handshake.ciphersuite \
-E header=y -E separator=, -E quote=d -E occurrence=f
Sample Output (Server Hello Cipher Suite Inspection):
Phase 2: Enable Hybrid PQC (X25519MLKEM768)
Supercharging OpenSSL with the Open Quantum Safe (OQS) Provider. To support post-quantum cryptography, we’ll extend OpenSSL using the open-source oqs-providerwhich implements NIST-standardized algorithms like ML-KEM.
Install Build Dependencies & Build liboqs
sudo apt update sudo apt install cmake libssl-dev git build-essential ninja-build
cd ~
rm -rf liboqs
git clone https://github.com/open-quantum-safe/liboqs.git
cd liboqs
mkdir build
cd build
cmake -GNinja -DCMAKE_INSTALL_PREFIX=/usr/local ..
ninja
sudo ninja install
Verify that the CMake config file is properly placed:
sudo find /usr /usr/local -name "liboqsConfig.cmake" 2>/dev/null
Build and Install oqs-provider
Clone and build the provider referencing your installed liboqs path:
cd ~/build/oqs-provider
ls CMakeLists.txt
cmake -B _build -G "Unix Makefiles" \
-Dliboqs_DIR=/usr/local/lib/cmake/liboqs
cd /home/eadmin/build/oqs-provider
cmake --build _build
sudo cmake --install _build
Configure the Oqs-Provider Block in OpenSSL
(Appending the provider directives safely to the end of your OpenSSL configuration file)
sudo bash -c 'cat << '\''EOF'\'' >> /etc/ssl/openssl.cnf
[provider_sect]
default = default_sect
oqsprovider = oqsprovider_sect
[default_sect]
activate = 1
[oqsprovider_sect]
activate = 1
module = /usr/local/lib64/ossl-modules/oqsprovider.so
EOF
'
Verify Registered Post-Quantum KEM Algorithms
(Querying the provider to list available hybrid and PQC key exchange mechanisms)
openssl list -kem-algorithms -provider oqsprovider
Test the TLS 1.3 Handshake using the PQC Hybrid Group
(Connecting via s_client, forcing the X25519MLKEM768 hybrid group with the provider loaded)
openssl s_client -connect 127.0.0.1:443 -tls1_3 -provider oqsprovider -provider default -groups X25519MLKEM768 -brief
Extract the Key Exchange Group/Temp Key Details
openssl s_client -connect 127.0.0.1:443 -tls1_3 -provider oqsprovider -provider default -groups X25519MLKEM768 2>/dev/null | grep -E "Server Temp Key|Group"
Configure Apache for Hybrid Curves
Edit the Apache SSL module config file:
sudo vi /etc/apache2/mods-enabled/ssl.conf
Add or update the curve preference directive so that X25519MLKEM768 takes priority:
SSLOpenSSLConfCmd Curves X25519MLKEM768:X25519:prime256v1
Restart Apache:
sudo systemctl restart apache2
Sanity Check: OpenSSL Provider & Algorithm Verification
Run the following commands to confirm that OpenSSL successfully recognizes the OQS provider and the target hybrid algorithm:
openssl list -providers
openssl list -kem-algorithms | grep -i x25519mlkem768
Expected output: You should see oqsprovider listed under active providers, and X25519MLKEM768 @ oqsprovider confirmed in the KEM algorithms list.
Phase 3: Configuring Apache for Hybrid Key Agreement
Now that OpenSSL understands post-quantum algorithms, we need to instruct Apache to prefer the hybrid group. Edit your Apache SSL configuration file (/etc/apache2/mods-enabled/ssl.conf) and add the following directive:
Configure key exchange and key encapsulation mechanisms
SSLOpenSSLConfCmd Curves X25519MLKEM768:X25519:prime256v1
By placing X25519MLKEM768 first, we ensure the server strongly prefers the hybrid post-quantum handshake whenever an interacting client supports it. Restart Apache to apply changes:
sudo systemctl restart apache2
Sanity Check: Server-Side TLS Handshake Verification
Test the active cipher and key exchange group directly from the server terminal using openssl s_client:
openssl s_client -connect 127.0.0.1:443 -tls1_3
openssl s_client -connect 127.0.0.1:443 -tls1_3 -brief 2>/dev/null | grep "Server Temp Key"
openssl list -providers
openssl list -kem-algorithms | grep -i mlkem
openssl list -kem-algorithms | grep -i X25519MLKEM768
Expected output: Scroll through the verbose handshake output to ensure the negotiated group or server-selected parameters reference X25519MLKEM768.
Phase 4: Verifying the Post-Quantum Upgrade
Let’s test our configuration to see the hybrid cryptographic exchange in action. 4.1: The Browser Check.
Head back to your browser, navigate to https://pqc-lab.localand check the Security tab in DevTools:
- Key Exchange:
X25519MLKEM768(or similar hybrid indicator) - Cipher: Still
AES_128_GCM(symmetric encryption remains unchanged)
The Wireshark Proof
Fire up Wireshark again with the filter tls.handshake.type == 1 || tls.handshake.type == 2:
- Client Hello: Notice group
4588 (0x11ec)(X25519MLKEM768) in the supported groups list. - Key Share: Alongside the small classical X25519 share, you will see a much chunkier payload roughly 1.1 to 1.2 KB representing the ML-KEM-768 ciphertext/public key data.
- Server Hello: The server explicitly selects the group
4588(X25519MLKEM768), completing our hybrid post-quantum handshake.
Sanity Check: Automated Traffic Inspection
If you have tshark installed on the server or client, run a quick packet summary test while making a request:
curl -k https://pqc-lab.local
Looking for PQC Lengths
Before diving in, you can inspect the raw packet stream using tcpdump itself. Because Post-Quantum key encapsulation algorithms (like ML-KEM-768) pack public keys around 1.1 KB, you will see a massive packet size jump in the TLS 1.3 Client Hello and Server Hello compared to classical 36-byte curves.
Run this to print the packets in real-time or from your capture:
sudo tcpdump -qns 0 -X -r capture.pcap port 443
What you’re looking for: Look at the length (e.g., length 1240 or length 1380 bytes) on the initial TLS handshake packets. In classical TLS 1.3 (Phase 1), a standard Client Hello fits comfortably under ~500 bytes. With hybrid PQC enabled, the packet payload visibly expands into multiple TCP segments or a single large frame exceeding 1 KB.
Parse the Fresh Capture with Advanced tshark Filters
Now, run these updated tshark filters against your fresh capture.pcap to extract the exact hybrid group negotiation:
tshark -r capture.pcap \
-Y "tls.handshake.type == 2" \
-T fields \
-e frame.number \
-e ip.src \
-e ip.dst \
-e tls.handshake.extensions_key_share_group \
-E header=y -E separator=, -E quote=d
Common Pitfalls and Fixes
Implementing custom OpenSSL providers and experimental hybrid groups in Apache can sometimes yield frustrating errors. Use this troubleshooting matrix to quickly diagnose common issues:
Apache Fails to Restart (Job for apache2.service failed)
- Symptom: Running
sudo systemctl restart apache2results in an error, and Apache won’t start. - Cause: Typically caused by a syntax error in
/etc/apache2/mods-enabled/ssl.confor an unrecognized command passed toSSLOpenSSLConfCmd. - Sanity Diagnosis: Run
sudo apache2ctl configtestto isolate the exact line causing the failure.
X25519MLKEM768 Missing from openssl list -kem-algorithms
- Symptom: The grep command returns nothing, or
oqsproviderfails to load. - Cause: The
oqs-providerwas installed to a directory path that OpenSSL 3 cannot locate by default, oropenssl.cnfhas syntax errors in the provider activation section.
Browser Still Negotiates Classical X25519
- Symptom: Wireshark and browser DevTools show standard
X25519even after configuringSSLOpenSSLConfCmd Curves X25519MLKEM768:... - Sanity Fix: Check the Apache error log for clues:
sudo tail -n 50 /var/log/apache2/error.log
Alternate SSL Conf Configuration
If your site configuration is throwing errors or you want a known-good baseline config file for your virtual host, compare yours against this working version /etc/apache2/sites-available/default-ssl.conf:
# /etc/apache2/sites-available/default-ssl.conf
<IfModule mod_ssl.c>
<VirtualHost _default_:443>
ServerAdmin webmaster@localhost
ServerName pqc-lab.local
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/test.crt
SSLCertificateKeyFile /etc/apache2/ssl/test.key
# Only modern protocols
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
# Simple, safe cipher suites (no PQC here)
SSLCipherSuite HIGH:!aNULL:!MD5
SSLHonorCipherOrder on
<FilesMatch "\.(?:cgi|shtml|phtml|php)$">
SSLOptions +StdEnvVars
</FilesMatch>
<Directory /usr/lib/cgi-bin>
SSLOptions +StdEnvVars
</Directory>
</VirtualHost>
</IfModule>
What was built?
In this walkthrough, you moved from abstract worries about quantum attacks to a working, local lab that you control. You started with a normal TLS 1.3 site, captured and inspected the handshake, and then upgraded your stack to use a hybrid key agreement that combines classical X25519 with ML‑KEM from the post-quantum family.
You did not need any quantum hardware, cloud magic, or vendor black boxes. You relied on tools you already know or can easily install: virtualization, Apache, OpenSSL 3, your browser, and Wireshark. You saw where the hybrid KEM appears in the handshake, how to confirm the negotiation in DevTools and packet captures, and how to tell the difference between marketing claims and real cryptographic changes on the wire.
At this point, you have more than a demo. You have a repeatable environment where you can test future KEMs, experiment with different TLS configurations, and run your own checks long before “post-quantum” becomes a checkbox in compliance templates.






