Node.js
Is PQC enabled? — quick check
macOS / Linux / Windows
# 1) No-dependency check — identify this machine first.
uname -a 2>/dev/null || true
# 2) Dependency check — prompt before installing anything.
if ! command -v node >/dev/null 2>&1; then
echo 'Node.js was not found.'
printf 'Install or enable Node.js now? [y/N] '
read answer
case "$answer" in
[Yy]*) echo 'Install Node.js 22+ from your OS package manager or nodejs.org, then rerun this snippet.' ;;
*) echo 'Skipping Node.js-based check.'; exit 1 ;;
esac
fi
# 3) Capability — offline. Node 22+ links OpenSSL 3.5 (= native ML-KEM).
node -p "process.versions.openssl"
node -p "+process.versions.openssl.split('.').slice(0,2).join('.') >= 3.5"
# 4) Liveness — confirm Node can complete a TLS 1.3 handshake at all.
# (Node has NO public API for the negotiated TLS named group, so the
# authoritative 'is PQ active?' answer comes from the local openssl below.)
node -e "
const tls=require('tls');
const s=tls.connect({host:'example.com',port:443,servername:'example.com',
ecdhCurve:'X25519MLKEM768:X25519:secp256r1'},()=>{
console.log('proto=',s.getProtocol(),'cipher=',s.getCipher().name);
s.end();
});
s.on('error',e=>console.error('handshake failed:',e.code));"
# 5) Authoritative named-group readout — still local, just via openssl CLI.
# 1) No-dependency check — identify this machine first.
uname -a 2>/dev/null || true
# 2) Dependency check — prompt before installing anything.
if ! command -v openssl >/dev/null 2>&1; then
echo 'OpenSSL was not found. A local PQC proof needs OpenSSL 3.5+.'
printf 'Install OpenSSL now? [y/N] '
read answer
case "$answer" in
[Yy]*)
if command -v brew >/dev/null 2>&1; then brew install openssl@3
elif command -v apt-get >/dev/null 2>&1; then sudo apt-get update && sudo apt-get install -y openssl
elif command -v dnf >/dev/null 2>&1; then sudo dnf install -y openssl
elif command -v yum >/dev/null 2>&1; then sudo yum install -y openssl
else echo 'No supported package manager found. Install OpenSSL 3.5+ and retry.'; exit 1
fi ;;
*) echo 'Install OpenSSL 3.5+ and retry for a local PQC proof.'; exit 1 ;;
esac
fi
OPENSSL=openssl
if command -v brew >/dev/null 2>&1; then
BREW_OPENSSL="$(brew --prefix openssl@3 2>/dev/null)/bin/openssl"
[ -x "$BREW_OPENSSL" ] && OPENSSL="$BREW_OPENSSL"
fi
$OPENSSL version
if ! $OPENSSL list -tls-groups 2>/dev/null | grep -qiE 'X25519MLKEM768|MLKEM|Kyber'; then
echo 'This OpenSSL does not advertise ML-KEM groups. Upgrade to OpenSSL 3.5+ or load oqsprovider, then retry.'
exit 1
fi
$OPENSSL s_client -connect example.com:443 -tls1_3 -groups X25519MLKEM768 </dev/null 2>&1 | grep -E 'Negotiated TLS1\.3 group|Server Temp Key' Expected when PQC is ON
3.5.0
true
proto= TLSv1.3 cipher= TLS_AES_256_GCM_SHA384
Negotiated TLS1.3 group: X25519MLKEM768 What you'll see when PQC is OFF
3.0.13+quic
false
proto= TLSv1.3 cipher= TLS_AES_256_GCM_SHA384
# (no group line — OpenSSL < 3.5 cannot offer ML-KEM) Node's tls module exposes cipher and protocol but not the negotiated TLS named group (getEphemeralKeyInfo() returns all-undefined for ML-KEM as of Node 25). The local openssl probe is the canonical local answer.
Node's tls module is a thin wrapper over its bundled OpenSSL. The official
Node binaries pin a specific OpenSSL version, so PQC support is gated on what Node ships:
- Node 22 (LTS) — OpenSSL 3.0.x — no native PQC; needs a custom build.
- Node 23+ — tracks OpenSSL 3.4 / 3.5 over time. Check
process.versions.openssl. - Custom builds —
./configure --shared-openssl --shared-openssl-includes=/opt/openssl-3.5/include --shared-openssl-libpath=/opt/openssl-3.5/lib
Set the group preference
const tls = require('node:tls');
const ctx = tls.createSecureContext({
ecdhCurve: 'X25519MLKEM768:X25519:secp256r1',
minVersion: 'TLSv1.2',
});
const server = require('https').createServer({ ...keyAndCert, ...ctx.context }, app); Client
const https = require('node:https');
const agent = new https.Agent({
ecdhCurve: 'X25519MLKEM768:X25519',
minVersion: 'TLSv1.3',
});
https.get('https://example.com', { agent }, res => res.pipe(process.stdout)); Frameworks
Express, Fastify, Hono on Node — all use the same tls.createSecureContext
under the hood. Pass ecdhCurve through your HTTPS bootstrap.
Bun uses BoringSSL and inherits its PQC support; Deno
uses rustls (see Rustls).