Post-Quantum Cryptography (PQC) is no longer a distant concern — it is happening now. Google Chrome, Cloudflare, and major cloud providers have already started deploying hybrid post-quantum key exchange in TLS connections. But how do you actually verify, from code, that your Node.js HTTPS requests are using quantum-safe cryptography? That is exactly what pqc-tracer solves.
What Is Post-Quantum Cryptography (PQC)?
Classical public-key cryptography — RSA, ECDH, and elliptic curve algorithms like X25519 — relies on mathematical problems (factoring large numbers, discrete logarithm) that are computationally hard for classical computers. A sufficiently powerful quantum computer running Shor's algorithm can solve these problems efficiently, breaking the confidentiality of any TLS session secured by these algorithms.
Post-Quantum Cryptography refers to algorithms believed to be resistant even against quantum computers. NIST standardized several PQC algorithms in 2024, most notably:
- ML-KEM (Module Lattice Key Encapsulation Mechanism, also known as CRYSTALS-Kyber) — used for key exchange in TLS.
- ML-DSA (CRYSTALS-Dilithium) — used for digital signatures.
In practice, TLS deployments today use hybrid key exchange, combining a classical algorithm with a PQC algorithm. For example, X25519MLKEM768 combines classical X25519 with ML-KEM-768. This ensures security even if one of the two algorithms is broken.
Why Do We Need a Tool to Check PQC Usage?
TLS handshake details are not exposed by default in high-level HTTP client APIs. When you do fetch('https://example.com') or use Node's https.get(...), you get a response — but you have no visibility into which key exchange group was negotiated during the TLS handshake.
A tool like pqc-tracer fills this gap. It:
- Makes an outgoing HTTPS request using Node's native TLS stack.
- Intercepts the established TLS socket and, using native FFI calls into
libssl.so.3, reads the negotiated key exchange group directly from the OpenSSLSSL*object. - Returns both the TLS trace (group name + cipher suite) and the full HTTP response.
This lets you answer the question: "Is this HTTPS connection actually quantum-safe?"
This is particularly valuable for:
- Security auditors checking whether production services have enabled PQC.
- Developers verifying their Node.js version supports PQC before deploying to production.
- Researchers monitoring the rollout of PQC across the internet.
How Does pqc-tracer Work?
The library uses koffi — a fast FFI library for Node.js — to call two OpenSSL C functions directly from native libssl.so.3:
-
SSL_ctrl(ssl, 134, 0, NULL)— queries the negotiated key exchange group ID (NID). Constant134isSSL_CTRL_GET_NEGOTIATED_GROUPfrom OpenSSL'sssl.h. -
SSL_group_to_name(ssl, groupId)— converts the numeric group ID to a human-readable name like"X25519MLKEM768"or"X25519".
The native SSL* pointer is accessed from Node's internal TLS socket through stderr-level fd capture of OpenSSL's SSL_trace output, which surfaces the group name via OpenSSL's own diagnostic output.
Linux only. This approach depends on Node's use of OpenSSL as its TLS backend and
libc.so.6for fd-level redirection. macOS and Windows use different native TLS libraries and are not supported.
Installation
npm install pqc-tracer
Requirements:
- Node.js >= 20
- Linux
Basic Usage
import { executeRequest } from 'pqc-tracer';
const request = {
hostname: 'www.example.com',
port: 443,
path: '/',
method: 'GET',
};
executeRequest(request).then(({ tlsTrace, response }) => {
console.log(`Negotiated Group: ${tlsTrace.group}`);
console.log(`Cipher Suite: ${tlsTrace.cipherSuite}`);
console.log(`HTTP Status: ${response.statusCode}`);
}).catch(console.error);
What You Get
The TlsTrace interface exposes:
interface TlsTrace {
group: string; // e.g. "X25519MLKEM768"
cipherSuite: string; // e.g. "TLS_AES_256_GCM_SHA384"
}
If group is X25519MLKEM768, your connection is using a hybrid PQC key exchange — you are quantum-safe. If it is X25519 or P-256, your connection uses only classical key exchange.
Full Response Type
interface HttpResponse {
statusCode: number | undefined;
statusMessage: string | undefined;
headers: IncomingHttpHeaders;
httpVersion: string;
body: string;
}
interface RequestResult {
tlsTrace: TlsTrace;
response: HttpResponse;
}
Low-Level API
If you want to integrate PQC tracing into your own request logic:
import { startStderrCapture, stopStderrCapture, getTlsTrace } from 'pqc-tracer';
// Before your TLS handshake:
startStderrCapture(); // redirects fd 2 to a temp file
// ... perform your HTTPS request ...
// After the handshake completes:
const traceOutput = stopStderrCapture(); // restores stderr, returns captured output
const tlsTrace = getTlsTrace(socket, traceOutput);
console.log(tlsTrace.group);
Does the Node.js Version Matter for PQC?
Absolutely — and this is the most important practical insight from this project.
Node.js bundles its own version of OpenSSL. PQC support in TLS (specifically ML-KEM via X25519MLKEM768) was added in OpenSSL 3.5.0. Whether your HTTPS connections are quantum-safe depends entirely on which OpenSSL version your Node.js build ships with.
The NodePqcReader repository contains a Docker-based test that runs the same HTTPS request to www.google.com under Node 20, 22, and 24 to compare results.
The Dockerfile
FROM ubuntu:24.04
ENV NVM_DIR=/root/.nvm
ENV NODE_VERSION_BUILD=22
ENV NODE_VERSION_RUN_1=20
ENV NODE_VERSION_RUN_2=22
ENV NODE_VERSION_RUN_3=24
RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*
# Install nvm and the required Node versions
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash && \
. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION_BUILD && \
nvm install $NODE_VERSION_RUN_1 && \
nvm install $NODE_VERSION_RUN_3
WORKDIR /app
# 1. Build pqc-tracer first (its dist/ is gitignored, so we must build it here)
COPY pqc-tracer ./pqc-tracer
WORKDIR /app/pqc-tracer
RUN . "$NVM_DIR/nvm.sh" && nvm use $NODE_VERSION_BUILD && npm install && npm run build
# 2. Build the main project
WORKDIR /app
COPY package.json package-lock.json ./
COPY src ./src
COPY tsconfig.json ./
RUN . "$NVM_DIR/nvm.sh" && nvm use $NODE_VERSION_BUILD && npm install && npm run build
COPY run.sh ./
RUN chmod +x run.sh
CMD ["./run.sh"]
The Run Script (run.sh)
#!/bin/bash
set -e
. "$NVM_DIR/nvm.sh"
echo "=== Running with Node $NODE_VERSION_RUN_1 ==="
nvm use $NODE_VERSION_RUN_1
echo "OpenSSL Version"
node -p "process.versions.openssl"
node dist/index.js
echo "=== Running with Node $NODE_VERSION_RUN_2 ==="
nvm use $NODE_VERSION_RUN_2
echo "OpenSSL Version"
node -p "process.versions.openssl"
node dist/index.js
echo "=== Running with Node $NODE_VERSION_RUN_3 ==="
nvm use $NODE_VERSION_RUN_3
echo "OpenSSL Version"
node -p "process.versions.openssl"
node dist/index.js
This script uses nvm to switch between Node versions and prints the bundled OpenSSL version before running the tracer against www.google.com.
The Main Script (src/index.ts)
import { executeRequest } from 'pqc-tracer';
const request = {
hostname: 'www.google.com',
port: 443,
path: '/',
method: 'GET',
headers: {
'User-Agent': 'NodePqcReader/1.0',
},
};
executeRequest(request).then(({ tlsTrace, response }) => {
console.log(`Negotiated Group: ${tlsTrace.group}`);
console.log(`Cipher Suite: ${tlsTrace.cipherSuite}`);
console.log(`HTTP Status: ${response.statusCode}`);
console.log(`Response Preview: ${response.body.slice(0, 10)}`);
}).catch(console.error);
Container Output
This is the actual output from a GitHub Actions run of the container:
=== Running with Node 20 ===
Now using node v20.20.0 (npm v10.8.2)
OpenSSL Version
3.0.17
Negotiated Group: X25519
Cipher Suite: TLS_AES_256_GCM_SHA384
HTTP Status: 200
Response Preview: <!doctype
=== Running with Node 22 ===
Now using node v22.22.0 (npm v10.9.4)
OpenSSL Version
3.5.4
Negotiated Group: X25519MLKEM768
Cipher Suite: TLS_AES_256_GCM_SHA384
HTTP Status: 200
Response Preview: <!doctype
=== Running with Node 24 ===
Now using node v24.13.1 (npm v11.8.0)
OpenSSL Version
3.5.5
Negotiated Group: X25519MLKEM768
Cipher Suite: TLS_AES_256_GCM_SHA384
HTTP Status: 200
Response Preview: <!doctype
What Does This Output Tell Us?
| Node Version | Exact Version | Bundled OpenSSL | Negotiated Group | Quantum-Safe? |
|---|---|---|---|---|
| 20 | v20.20.0 | 3.0.17 | X25519 | ❌ No |
| 22 | v22.22.0 | 3.5.4 | X25519MLKEM768 | ✅ Yes |
| 24 | v24.13.1 | 3.5.5 | X25519MLKEM768 | ✅ Yes |
The results are striking:
-
Node 20 bundles OpenSSL 3.0.17, which does not support ML-KEM. Connections to Google (which offers
X25519MLKEM768) fall back to classicalX25519. -
Node 22 bundles OpenSSL 3.5.4 — and that is already enough for full PQC support. The connection to Google negotiates
X25519MLKEM768, a hybrid PQC key exchange. -
Node 24 bundles OpenSSL 3.5.5, also negotiating
X25519MLKEM768.
The cipher suite (TLS_AES_256_GCM_SHA384) remains the same across all versions — this is the symmetric encryption used after key exchange and is already quantum-resistant at 256-bit strength.
The takeaway: Node 20 is the odd one out. Both Node 22 and Node 24 ship with OpenSSL 3.5 and support PQC out of the box. If you are still on Node 20, you are missing quantum-safe key exchange entirely.
Why Node.js Has an Advantage Over Python and .NET
Here is where Node.js shines compared to other languages: Node bundles its own OpenSSL.
In most Linux distributions, Python and .NET use the system's shared OpenSSL (/usr/lib/x86_64-linux-gnu/libssl.so.3 or similar). This means:
- On Ubuntu 24.04, the system OpenSSL is 3.0.x — no PQC support.
- To get PQC in Python, you would need to compile OpenSSL 3.5 from source, set
LD_LIBRARY_PATH, and possibly recompile Python itself or use a customsslmodule. - .NET on Linux also links against the system OpenSSL by default. Getting ML-KEM support requires either a custom OpenSSL build or waiting for the distribution to ship 3.5.
Node.js sidesteps all of this. When you install Node 24 via nvm, it ships with its own OpenSSL 3.5 statically or dynamically linked — no system-level changes required. PQC just works, immediately, without any manual setup.
This makes Node.js currently the most accessible runtime for experimenting with and deploying PQC in outgoing HTTPS requests on Linux.
Summary
| Aspect | Detail |
|---|---|
| Package | pqc-tracer |
| GitHub | ConnectingApps/NodePqcReader |
| Minimum Node for PQC | Node 22 (bundles OpenSSL 3.5+) |
| PQC group to look for | X25519MLKEM768 |
| Platform | Linux only |
| Key advantage vs Python/.NET | Node bundles its own OpenSSL — no system-level setup needed |
If you want to audit whether your Node.js services are making quantum-safe connections, pqc-tracer gives you a simple, programmatic way to find out. Install it, run it against your endpoints, and check whether you're seeing X25519MLKEM768 — or still stuck in the classical world.
Top comments (0)