Post-Quantum Secure OpenID Connect over KEMTLS
QuantumShield replaces the classical TLS handshake in an OpenID Connect authentication flow with KEMTLS — a signature-less key exchange protocol built on ML-KEM-768 (NIST FIPS 203). The result is a full OIDC provider where every credential, authorization code, and token travels over a post-quantum encrypted channel, and the server proves its identity implicitly through KEM decapsulation rather than a certificate signature.
This project implements the protocol described in Wiggers (2020), IACR ePrint 2020/534 and applies it end-to-end: from TCP socket transport, through an HTTP adapter layer, to a browser-compatible hybrid handshake and a live interactive dashboard.
- How It Works
- Architecture
- Algorithms
- Performance
- Project Structure
- Getting Started
- Running the Demo
- API Reference
- Browser Compatibility
- Tests
- Benchmarks
- References
Standard TLS authenticates the server with a certificate signature (RSA or ECDSA). KEMTLS eliminates that signature entirely. Instead, the server's identity is its long-term ML-KEM-768 public key. If the server can decapsulate the client's ciphertext and produce the correct Finished MAC, it has proven possession of the private key — no signature required.
Handshake message sequence (Wiggers 2020, §3, Fig. 1):
Client Server
| |
|<------ ServerHello (KEM public key) ------|
| |
|------- ClientKEMCiphertext (encap) ------>|
| |
| [both derive channel key] |
| |
|<------- Finished (HMAC-SHA3-256 MAC) -----|
| |
| [OIDC flow over AES-256-GCM channel] |
Once the channel is established, all OIDC traffic (authorization request, authorization code, token exchange, userinfo) is encrypted with AES-256-GCM keyed from the KEM shared secret via HKDF-SHA256.
OIDC ID Tokens are signed with ML-DSA-65 (NIST FIPS 204) for independent third-party verification — signatures appear only at the application layer, never in the transport handshake.
┌─────────────────────────────────────────────────────────────────┐
│ Client Side │
│ │
│ Browser / CLI / Test Client │
│ ┌──────────────────┐ ┌─────────────────────────────────┐ │
│ │ login.js │ │ kemtls_client_tcp.py │ │
│ │ (hybrid KEMTLS) │ │ KEMTLSTCPSession │ │
│ │ ECDH P-256 wrap │ │ kemtls_http_adapter.py │ │
│ └────────┬─────────┘ └──────────────┬────────────────── ┘ │
└───────────┼──────────────────────────── ┼───────────────────────┘
│ HTTPS / TCP │ Raw TCP :9001
┌───────────┼─────────────────────────────┼───────────────────────┐
│ │ Server Side │ │
│ ┌────────▼────────────────────────┐ ┌▼────────────────────┐ │
│ │ web_demo/server.py (Flask) │ │ kemtls_server_tcp.py│ │
│ │ :9000 │ │ :9999 / :9001 │ │
│ │ /kemtls/browser-handshake │ │ KEMTLSTCPServer │ │
│ │ /kemtls/send │ │ │ │
│ │ /oidc/authorize │◄──┤ OIDC Bridge │ │
│ │ /oidc/token │ │ │ │
│ │ /oidc/userinfo │ └─────────────────────┘ │
│ └────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ kemtls/handshake.py · kemtls/channel.py │ │
│ │ KEMTLSHandshake SecureChannel │ │
│ │ ML-KEM-768 keygen/encap/decap AES-256-GCM encrypt │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ web_demo/pq_crypto_real.py │ │
│ │ RealKEMTLS · PQTokenService │ │
│ │ liboqs ML-KEM-768 + ML-DSA-65 (NIST FIPS 203/204) │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
There are two transport paths:
| Path | Description | When to use |
|---|---|---|
| Browser (Hybrid) | Browser derives the KEMTLS session key via a two-round proxy handshake. P-256 ECDH delivers the ML-KEM-768-derived key to the browser; all subsequent OIDC traffic is encrypted with that key. | Web UI, any browser |
| TCP Native | Full socket-level KEMTLS. Client and server exchange raw framed messages; no HTTP wrapping. | CLI, programmatic clients, kemtls_http_adapter.py |
| Layer | Algorithm | Standard | Security Level |
|---|---|---|---|
| Key Encapsulation | ML-KEM-768 (Kyber768) | NIST FIPS 203 | Level 3 |
| Token Signing | ML-DSA-65 (Dilithium3) | NIST FIPS 204 | Level 3 |
| Symmetric Encryption | AES-256-GCM | NIST FIPS 197 | 256-bit |
| Key Derivation | HKDF-SHA256 | RFC 5869 | — |
| Transcript Binding | SHA3-256 | NIST FIPS 202 | 256-bit |
All post-quantum operations are performed by liboqs (Open Quantum Safe C library) via liboqs-python. There is no simulation or mock crypto anywhere in the codebase.
Why SHA-256 inside HKDF and SHA3-256 for transcripts? HKDF operates purely on symmetric material (the KEM shared secret); NIST's post-quantum requirements apply to public-key primitives. HKDF-SHA256 is used here to match the TLS 1.3 key schedule (RFC 8446 §7.1) and the KEMTLS paper's recommendation. SHA3-256 is used for handshake transcript binding to maintain domain separation from the KDF, consistent with Wiggers §3.
Measured on x86-64 hardware using metrics/benchmark.py with real liboqs operations over 1,000 iterations.
Primitive latencies:
| Operation | Mean (ms) | Std (ms) | N |
|---|---|---|---|
| ML-KEM-768 Keygen | 0.081 | 0.016 | 100 |
| ML-KEM-768 Encapsulation | 0.059 | 0.009 | 100 |
| ML-KEM-768 Decapsulation | 0.069 | 0.013 | 100 |
| ML-DSA-65 Signing | 0.506 | 0.355 | 100 |
| ML-DSA-65 Verification | 0.152 | 0.050 | 100 |
| Full KEMTLS Handshake | 1.08 | 0.461 | 1000 |
| OIDC Token Issuance | 0.58 | 0.287 | 1000 |
| OIDC Token Verification | 0.17 | 0.054 | 1000 |
Protocol comparison:
| Metric | Classical TLS (RSA-2048) | PQ-TLS (reference) | KEMTLS (this project) |
|---|---|---|---|
| Handshake latency | ~0.92 ms | ~1.38 ms | ~1.08 ms |
| vs. PQ-TLS | — | baseline | 21.6% faster |
| Handshake message size | ~1.4 KB | ~10.8 KB | ~7.5 KB |
| Post-quantum secure | No | Yes | Yes |
KEMTLS is 21.6% faster than standard PQ-TLS and 30% smaller on the wire because it eliminates the ML-DSA-65 signature (~3.3 KB) from the handshake entirely.
QuantumShield/
│
├── kemtls/ # Core protocol library
│ ├── handshake.py # KEMTLSHandshake — signature-less key exchange
│ └── channel.py # SecureChannel — AES-256-GCM over KEM shared secret
│
├── web_demo/ # Interactive web application
│ ├── server.py # Flask server: OIDC provider + KEMTLS endpoints (~2300 lines)
│ ├── pq_crypto_real.py # RealKEMTLS + PQTokenService — liboqs wrapper
│ ├── tls_comparison_demo.py # Classical TLS demo for side-by-side comparison
│ ├── static/
│ │ ├── login.js # Browser hybrid KEMTLS handshake + OIDC flow
│ │ ├── dashboard.js # Live test runner + WebSocket updates
│ │ ├── comparison.js # Protocol benchmark visualisation
│ │ └── particles.js # Background canvas animation
│ └── templates/
│ ├── login.html # KEMTLS login page
│ ├── dashboard.html # Security test dashboard
│ ├── comparison.html # KEMTLS vs TLS side-by-side
│ └── tls_login.html # Classical TLS login (control)
│
├── kemtls_server_tcp.py # Hardened raw TCP KEMTLS server (port 9999)
├── kemtls_client_tcp.py # Raw TCP KEMTLS client
├── kemtls_http_adapter.py # requests-compatible KEMTLS session wrapper
│
├── pq_crypto/
│ └── pq_jwt.py # ML-DSA-65 JWT issuance + verification
│
├── metrics/
│ └── benchmark.py # Cryptographic benchmark suite
│
├── benchmark/
│ └── benchmark_compare.py # KEMTLS vs TLS comparison benchmark
│
├── scripts/
│ └── demo_flow.py # End-to-end automated demonstration
│
├── tests/
│ ├── test_kemtls_protocol.py # Protocol unit tests (real crypto)
│ ├── test_kemtls_oidc_e2e.py # OIDC end-to-end tests
│ ├── test_kemtls_oidc_bridge_e2e.py # TCP bridge integration tests
│ ├── test_auth.py # Authentication tests
│ └── web_demo/ # Web demo API + security tests
│
├── tls_simulation/ # Classical TLS simulation (control group)
├── dashboard/ # State + event log files (runtime)
│
├── Dockerfile # Multi-stage build with liboqs from source
├── render.yaml # One-click Render.com deployment
└── web_demo/requirements.txt # Python dependencies
- Python 3.10+
- CMake 3.15+ and a C compiler (for building liboqs)
- Git
Or just use Docker — it handles the liboqs build automatically.
git clone https://github.com/<your-username>/QuantumShield.git
cd QuantumShield
docker build -t quantumshield .
docker run -p 9000:9000 quantumshieldOpen http://localhost:9000.
Note:
crypto.subtle(used in the browser handshake) requires a secure context. Access the app viahttp://localhost— not via an IP address or a plain HTTP hostname. On Render, HTTPS is provided automatically.
1. Install liboqs
git clone -b 0.10.0 https://github.com/open-quantum-safe/liboqs.git
cd liboqs && mkdir build && cd build
cmake -DOQS_USE_OPENSSL=OFF -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=/usr/local ..
make -j$(nproc) && sudo make install
sudo ldconfig
cd ../..2. Install Python dependencies
python -m venv .venv && source .venv/bin/activate
pip install -r web_demo/requirements.txt
pip install gunicorn gevent gevent-websocket3. Start the server
# Development
python web_demo/server.py
# Production (matches Docker CMD)
gunicorn -k gevent -w 1 --timeout 300 --bind 0.0.0.0:9000 --chdir web_demo "server:app"Open http://localhost:9000. Default credentials: admin / quantum123.
Web interface
Navigate to http://localhost:9000. The landing page offers two paths:
- KEMTLS Login — post-quantum handshake, step-by-step visualisation, ML-DSA-65 signed ID Token display
- Compare — side-by-side latency chart of KEMTLS vs classical TLS
Programmatic (HTTP adapter)
from kemtls_http_adapter import KEMTLSSession
session = KEMTLSSession("http://localhost:9000")
session.establish() # ML-KEM-768 handshake
result = session.authorize("admin", "quantum123")
print(result["authorization"]["code"]) # OIDC authorization code
tokens = session.exchange_token(result["authorization"]["code"])
print(tokens["id_token"]) # ML-DSA-65 signed JWT
session.close()Raw TCP (native KEMTLS)
from kemtls_http_adapter import KEMTLSTCPSession
session = KEMTLSTCPSession(host="127.0.0.1", port=9001)
result = session.authorize("admin", "quantum123")
tokens = session.exchange_token(result["authorization"]["code"])
session.close()End-to-end demo script
# Start the server first, then in a second terminal:
python scripts/demo_flow.pyThis runs the full pipeline: KEMTLS handshake test, OIDC flow, benchmark comparison, and a summary report.
Benchmark suite
python metrics/benchmark.pyAll OIDC endpoints conform to OpenID Connect Core 1.0. The KEMTLS-specific endpoints handle channel establishment and encrypted message relay.
| Method | Path | Description |
|---|---|---|
GET |
/.well-known/openid-configuration |
Discovery document |
GET |
/oidc/jwks |
JSON Web Key Set (ML-DSA-65 public key) |
POST |
/oidc/authorize |
Authorization endpoint (requires active KEMTLS channel) |
POST |
/oidc/token |
Token endpoint — returns ML-DSA-65 signed ID Token |
GET |
/oidc/userinfo |
Userinfo endpoint |
POST |
/oidc/logout |
Session logout |
| Method | Path | Description |
|---|---|---|
POST |
/kemtls/handshake |
Full KEMTLS handshake (native clients) |
POST |
/kemtls/send |
Send encrypted OIDC payload over established channel |
POST |
/kemtls/browser-handshake |
Hybrid browser handshake (Round 1 — P-256 key delivery) |
POST |
/kemtls/browser-encap |
Browser encapsulation relay (Round 2) |
{
"issuer": "http://localhost:9000",
"authorization_endpoint": "http://localhost:9000/oidc/authorize",
"token_endpoint": "http://localhost:9000/oidc/token",
"jwks_uri": "http://localhost:9000/oidc/jwks",
"id_token_signing_alg_values_supported": ["ML-DSA-65"],
"kem_algorithms_supported": ["ML-KEM-768"]
}Browsers cannot load native shared libraries, so ML-KEM-768 cannot run client-side. QuantumShield solves this with a two-round proxy handshake:
-
Round 1 — Browser generates an ephemeral P-256 keypair and sends the public key to the server. The server generates a real ML-KEM-768 keypair, derives the session key via HKDF-SHA256, wraps it under the browser's P-256 key using ECDH + AES-256-GCM, and returns the wrapped key.
-
Round 2 — Browser decrypts the wrapped key using its P-256 private key via
crypto.subtle. The ML-KEM-768-derived session key is now shared. All subsequent OIDC payloads are encrypted with that key.
P-256 is used solely as a key-delivery envelope, never for OIDC data. The channel key is ML-KEM-768-derived throughout.
Requirement: crypto.subtle is only available in secure contexts (https:// or localhost). The app will fail with a Cannot read properties of undefined (reading 'generateKey') error if accessed over plain HTTP on a non-localhost hostname.
# Unit + integration tests
pytest tests/ -v
# KEMTLS protocol tests (real crypto, no server needed)
pytest tests/test_kemtls_protocol.py -v
# End-to-end OIDC flow (server must be running)
pytest tests/test_kemtls_oidc_e2e.py -v
# TCP bridge tests
pytest tests/test_kemtls_oidc_bridge_e2e.py -vFull methodology, raw numbers, and interpretation are in BENCHMARKS.md.
To reproduce:
python metrics/benchmark.py # primitive + handshake latencies
python benchmark/benchmark_compare.py # KEMTLS vs classical TLS comparisonThe repository includes a render.yaml for one-click deployment on Render. Push to GitHub, connect the repo on Render, and it builds the Docker image and deploys automatically with HTTPS.
services:
- type: web
name: quantumshield
env: docker
plan: free
healthCheckPath: /- P. Schwabe, D. Stebila, T. Wiggers — Post-Quantum TLS without Handshake Signatures, CCS 2021. IACR ePrint 2020/534
- F. Schardong et al. — Post-Quantum OpenID Connect, IEEE/ACM S&P 2023.
- NIST FIPS 203 — Module-Lattice-Based Key-Encapsulation Mechanism Standard (ML-KEM)
- NIST FIPS 204 — Module-Lattice-Based Digital Signature Standard (ML-DSA)
- Open Quantum Safe — liboqs
- RFC 5869 — HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
- RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3