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\hosts on Windows or /etc/hosts on Linux/macOS): 192.168.56.10 pqc-lab.local

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 apache2 results in an error, and Apache won’t start.
  • Cause: Typically caused by a syntax error in /etc/apache2/mods-enabled/ssl.conf or an unrecognized command passed to SSLOpenSSLConfCmd.
  • Sanity Diagnosis: Run sudo apache2ctl configtest to isolate the exact line causing the failure.

X25519MLKEM768 Missing from openssl list -kem-algorithms

  • Symptom: The grep command returns nothing, or oqsprovider fails to load.
  • Cause: The oqs-provider was installed to a directory path that OpenSSL 3 cannot locate by default, or openssl.cnf has syntax errors in the provider activation section.

Browser Still Negotiates Classical X25519

  • Symptom: Wireshark and browser DevTools show standard X25519 even after configuring SSLOpenSSLConfCmd 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.

Discover more from CYBERDOM

Subscribe now to keep reading and get access to the full archive.

Continue reading