check-pqc
v0.2.8
Published
Check if a host is post-quantum (PQC) ready — TLS 1.3 + ML-KEM hybrid key exchange. CLI wrapper over checkpqc.app.
Maintainers
Readme
Is your TLS post-quantum ready? A command-line tool that tells you whether any host on the public internet (or your own intranet) negotiates a quantum-resistant key exchange — and what to do about it if it doesn't.
npx check-pqc google.com ● HYBRID ENABLED — Server negotiated a hybrid PQC + classical group. Recommended state.
Target: google.com:443
Verdict: HYBRID_ENABLED
Hybrid attempt ✓ TLSv1.3 · X25519MLKEM768 · TLS_AES_256_GCM_SHA384 (62ms)
Classical attempt ✓ TLSv1.3 · X25519 · TLS_AES_256_GCM_SHA384 (58ms)
Full report: https://checkpqc.app/?host=google.comWhy should I care?
Right now, attackers are recording encrypted TLS traffic so they can decrypt it later when sufficiently large quantum computers exist. This is called "Harvest Now, Decrypt Later" (HNDL).
The defense is a hybrid TLS 1.3 key exchange that combines classical
ECDH (X25519) with ML-KEM-768 (formerly Kyber-768, NIST FIPS 203).
Even if a quantum computer breaks the classical half a decade from now, the
ML-KEM half still protects the session.
check-pqc tells you, in one command, whether a server is using that
hybrid handshake — so you can verify your own services and audit your
vendors.
Quick start (60 seconds)
You need one of these:
- Node.js 18 or newer (check with
node --version), OR - Docker (any modern Docker Desktop or daemon)
Then:
# Option A: no install, run once
npx check-pqc google.com
# Option B: install permanently (short alias or org-branded — same package)
npm install -g check-pqc
# or
npm install -g @aegyrix/check-pqc
check-pqc google.com
# Option C: Docker (no Node needed)
docker run --rm ghcr.io/aegyrix/check-pqc:latest google.comThat's it. If the output says HYBRID ENABLED or PQC ENABLED, the host
is post-quantum-ready. Anything else means it isn't — see Fixing a host
that fails below.
Getting started
You just installed check-pqc. Here's the 3-minute tour of what to do
next.
1. Confirm your local setup
check-pqc --version # tool itself
check-pqc --check-offline # local OpenSSL + ML-KEM capability--check-offline tells you whether your machine has an OpenSSL build
new enough to do PQC handshakes locally (used by --offline mode).
If it says PQC-capable, you're set. If not, online probes still
work — see Airgap / SCIF mode for fixes.
2. Audit a host you care about
check-pqc <your-domain> # e.g. checkpqc.app, your company's site
check-pqc <your-domain> --json # for scripts / dashboardsRead the verdict in the output table below. The exit code tells your
shell whether the host is OK (0) or needs work (non-zero).
3. Decide what to look for
| If you see... | What it means | Your move |
|---|---|---|
| PQC_ENABLED / HYBRID_ENABLED | Server picked a PQ key exchange. Recommended. | ✅ Nothing — re-check after stack upgrades. |
| AVAILABLE_NOT_ACTIVE | Server can speak PQC but didn't pick it. | Bump PQC groups to the front of ssl_conf_command Groups. |
| CLIENT_ONLY | Your client supports PQC; the server doesn't. | Server upgrade — see Fix. |
| SERVER_ONLY | Server supports PQC but classical-only clients can't connect. | Add classical fallback (X25519:secp384r1) to your group list. |
| NOT_READY | Neither side speaks PQC. | Upgrade OpenSSL to 3.5+ and reconfigure. |
| UNKNOWN | Network error / timeout / handshake aborted. | Re-run; check the target is reachable on port 443. |
4. Wire it into something useful
Most people install check-pqc and then forget about it. To get value,
pick one of these:
- CI gate on every deploy — see CI/CD example.
- Nightly cron that emails you on regression — see Scheduled audit.
- Library import in a custom dashboard — see Library import.
- Slack/Teams hook — pipe
--jsonthroughjqand post to a webhook.
If you only do one thing, do the CI gate. PQC posture is the kind of thing that silently regresses on a TLS library upgrade or an LB swap; a CI check catches it the same hour it happens.
5. Bookmark these
| Resource | URL |
|---|---|
| Web checker | checkpqc.app |
| Status badge | https://checkpqc.app/badge/<host> |
| API docs | api.checkpqc.app/docs |
| Source / issues | github.com/aegyrix/checkpqc.app |
| Security report | [email protected] (PGP key on /.well-known/security.txt) |
1. Install
check-pqc runs on macOS, Linux, and Windows. CI smoke-tests every
release on all three. Pick the install path that fits your environment.
macOS
# Easiest — uses Apple's bundled npm if you've ever installed Node
npm install -g check-pqc
# If you don't have Node yet:
brew install node
npm install -g check-pqc
# Verify
check-pqc --version # → check-pqc v0.2.6Linux (any distro)
# Debian / Ubuntu
sudo apt-get update && sudo apt-get install -y nodejs npm
sudo npm install -g check-pqc
# Fedora / RHEL / Rocky
sudo dnf install -y nodejs npm
sudo npm install -g check-pqc
# Alpine
apk add --no-cache nodejs npm
npm install -g check-pqc
# Arch
sudo pacman -S nodejs npm
sudo npm install -g check-pqc
# Verify
check-pqc --versionWindows
# PowerShell — install Node first if you don't have it
winget install OpenJS.NodeJS.LTS
# Open a NEW PowerShell window so PATH refreshes, then:
npm install -g check-pqc
# Verify
check-pqc --versionYou can also use WSL2 (Linux instructions above) or Docker Desktop (see below). All three paths work identically.
Docker (any OS, no Node required)
The image is multi-architecture (linux/amd64 + linux/arm64) and
ships with OpenSSL 3.5+ built-in so airgap mode "just works" without
installing anything else.
# Pull the latest image
docker pull ghcr.io/aegyrix/check-pqc:latest
# Run it
docker run --rm ghcr.io/aegyrix/check-pqc:latest google.com
# Pin to a specific version
docker run --rm ghcr.io/aegyrix/check-pqc:0.2.6 google.com
# Use the airgap-optimized image (same image, different tag)
docker run --rm ghcr.io/aegyrix/check-pqc:offline google.comFrom a tarball (airgapped / vendored)
When you can't reach npmjs.com directly:
# On a machine WITH internet:
npm pack check-pqc # → check-pqc-0.2.6.tgz
# transfer the .tgz to the airgapped machine, then:
npm install -g ./check-pqc-0.2.6.tgzVerifying the curl-pipe installer (optional)
If you'd rather not pipe curl | sh, every published installer has a
matching .sha256 sidecar at the same URL. The two files are written
together by the same deploy script, so a tampered installer will not
match its sidecar.
# macOS / Linux
curl -fsSL https://checkpqc.com/install.sh -o install.sh
curl -fsSL https://checkpqc.com/install.sh.sha256 -o install.sh.sha256
shasum -a 256 -c install.sh.sha256 # → install.sh: OK
sh install.sh# Windows
$ErrorActionPreference = 'Stop'
Invoke-WebRequest https://checkpqc.com/install.ps1 -OutFile install.ps1
Invoke-WebRequest https://checkpqc.com/install.ps1.sha256 -OutFile install.ps1.sha256
$expected = (Get-Content install.ps1.sha256).Split(' ')[0]
$actual = (Get-FileHash -Algorithm SHA256 install.ps1).Hash.ToLower()
if ($expected -ne $actual) { throw 'install.ps1 sha mismatch' }
.\install.ps1This protects against an attacker who can serve a different install.sh
than its sidecar (e.g. cache poisoning). It does not protect against
a full origin compromise that rewrites both files in lock-step — for
that, pin to a specific package version: npm install -g [email protected].
2. Check (run a probe)
The basic command is:
check-pqc <hostname>check-pqc connects to the target and performs two independent TLS 1.3
handshakes:
- A hybrid attempt that offers
X25519MLKEM768first. - A classical attempt that only offers classical ECDH groups.
The combination of the two outcomes determines the verdict.
Examples
# Default port 443
check-pqc example.com
# Custom port
check-pqc mail.example.com:993
check-pqc example.com --port 8443
# Machine-readable JSON
check-pqc example.com --json
# No ANSI colors (e.g. when piping to a file)
check-pqc example.com --no-color > report.txt
# Show the local PQC capability and exit
check-pqc --check-offline
# Run the probe with no network call (airgap / SCIFs)
check-pqc internal.corp --offlineReading the output
| Verdict | What it means | Exit code |
|---|---|---|
| PQC_ENABLED | Pure post-quantum group negotiated. Best possible state. | 0 |
| HYBRID_ENABLED | Hybrid PQC + classical group negotiated. Recommended. | 0 |
| AVAILABLE_NOT_ACTIVE | Server can speak PQC but did not select it for you. Check group ordering. | 2 |
| CLIENT_ONLY | Your client supports PQC; the server does not. Server upgrade needed. | 2 |
| SERVER_ONLY | Server supports PQC; a classical-only client could not negotiate. | 2 |
| NOT_READY | Neither side can negotiate PQC. | 1 |
| UNKNOWN | Network error, timeout, or inconclusive result. | 3 |
The exit code makes it easy to gate CI/CD pipelines:
check-pqc api.example.com || exit 13. Test (verify your install works)
After installing, run these to make sure everything is wired up:
# 1. Print version
check-pqc --version
# Expected: check-pqc v0.2.6
# 2. Check local PQC engine
check-pqc --check-offline
# Expected on a PQC-capable host:
# ● PQC-capable
# openssl: /opt/homebrew/opt/openssl@3/bin/openssl
# hybrid group: X25519MLKEM768
# Exit code: 0 (capable) or 3 (not capable — still usable, see below)
# 3. Probe a known-good public PQC host
check-pqc google.com
# Expected: HYBRID_ENABLED, exit code 0
# 4. Probe a known-not-yet-PQC host (most banks, most legacy hosts)
check-pqc www.irs.gov
# Expected: NOT_READY or AVAILABLE_NOT_ACTIVE, non-zero exit code
# 5. JSON parses cleanly
check-pqc google.com --json | jq .verdict
# Expected: "HYBRID_ENABLED"If all five pass, your install is good.
Continuous Integration example
# .github/workflows/pqc-audit.yml
- name: Audit our public TLS endpoints
run: |
npx check-pqc api.example.com
npx check-pqc www.example.com
npx check-pqc auth.example.com4. Fix (apply a remedy)
If check-pqc reports a non-PQC_ENABLED / HYBRID_ENABLED verdict,
here's what to do based on the verdict.
Fixing a host that fails
NOT_READY or CLIENT_ONLY
The server doesn't speak PQC. Upgrade or reconfigure it:
nginx (Linux)
You need OpenSSL 3.5+ (native ML-KEM) or OpenSSL 3.2+ with the
oqsprovider loaded.
# /etc/nginx/sites-available/your-site.conf
ssl_protocols TLSv1.3 TLSv1.2;
ssl_conf_command Groups X25519MLKEM768:SecP256r1MLKEM768:X25519:secp384r1:prime256v1;
ssl_ecdh_curve X25519:secp384r1:prime256v1;
ssl_prefer_server_ciphers off;Reload, then re-run check-pqc.
Apache 2.4.62+
SSLOpenSSLConfCmd Groups X25519MLKEM768:SecP256r1MLKEM768:X25519:secp384r1:prime256v1
SSLProtocol -all +TLSv1.2 +TLSv1.3Caddy 2.8+
{
servers {
protocols h1 h2 h3
}
}
example.com {
tls {
curves x25519mlkem768 x25519 secp384r1
}
}Cloudflare / Fastly / AWS CloudFront / Azure Front Door
Most large CDNs already enable PQC by default in 2026. If you front your
origin with one of these and check-pqc still reports NOT_READY, check
your custom TLS profile settings — some require explicit opt-in.
AVAILABLE_NOT_ACTIVE
The server can speak PQC but didn't pick it. Almost always a group
ordering issue: list the hybrid groups first in your TLS config (see
nginx example above — X25519MLKEM768 before X25519).
SERVER_ONLY
The server supports PQC; only your client is classical-only. This verdict isn't a problem with the host — it's telling you your local openssl can't negotiate hybrid. Install OpenSSL 3.5+ (see Fixing the local PQC engine).
UNKNOWN
Network or DNS issue. Try:
# Force IPv4
check-pqc example.com --json | jq .
# Try a different port
check-pqc example.com:8443
# Sanity-check connectivity
curl -v https://example.com 2>&1 | head -20Fixing the local PQC engine
check-pqc --offline and --check-offline need a system OpenSSL 3.5+
that exposes the X25519MLKEM768 group. Here's how to get one on each
platform:
| Platform | Command | Result |
|---|---|---|
| macOS | brew install openssl@3 | OpenSSL 3.5+ in /opt/homebrew/opt/openssl@3/bin/openssl |
| Linux — Alpine edge | apk add openssl | OpenSSL 3.5.x ✓ |
| Linux — Debian/Ubuntu | distro openssl is 3.0.x (too old). Use Docker (ghcr.io/aegyrix/check-pqc:offline) or compile OpenSSL 3.5 from source | |
| Linux — Fedora 41+ | sudo dnf install openssl | 3.5+ ✓ |
| Linux — RHEL 9 / Rocky 9 | enable EPEL + install oqs-provider, OR use Docker | |
| Linux — Arch / openSUSE Tumbleweed | sudo pacman -S openssl / sudo zypper in openssl-3 | rolling, 3.5+ ✓ |
| Windows | winget install ShiningLight.OpenSSL.Light or scoop install openssl | 3.5+ ✓ |
| Anything else | docker run --rm ghcr.io/aegyrix/check-pqc:offline ... | Bundled OpenSSL 3.5.6 ✓ |
After installing, verify:
openssl version # → OpenSSL 3.5.x or newer
openssl list -tls-groups | grep -i mlkem # should print X25519MLKEM768
check-pqc --check-offline # → ● PQC-capableIf your distro's openssl is stuck at 3.0.x and you can't upgrade, the Docker image is your easy out — it ships OpenSSL 3.5.6 inside and runs identically on every OS.
Library import (Node / TypeScript)
check-pqc is also a TypeScript library if you want to embed the probe
in your own tool:
import { offlineProbe, detectOpensslCapability } from 'check-pqc';
const cap = detectOpensslCapability();
if (!cap.available) {
throw new Error('Host has no PQC-capable OpenSSL — install 3.5+');
}
const result = await offlineProbe('internal.corp', 443);
if (result.verdict !== 'HYBRID_ENABLED' && result.verdict !== 'PQC_ENABLED') {
console.error('Not PQC-ready:', result.verdict);
process.exit(1);
}Or import only the offline subpath to keep the bundle tiny:
import { offlineProbe } from 'check-pqc/offline';Airgap / SCIF mode
check-pqc --offline runs the entire twin-probe locally, with zero
outbound API calls. It uses Node's built-in tls module for the
classical attempt and shells out to your system OpenSSL for the hybrid
attempt.
check-pqc --check-offline # confirm local engine status
check-pqc --offline internal.corp:443 # probe a host with no API call
check-pqc --offline target --json # machine-readableThree ways to get the tool into an airgapped environment
| Method | Command | Best for |
|---|---|---|
| npm tarball | npm pack check-pqc online → transfer the .tgz → npm install -g ./check-pqc-0.2.6.tgz | Any OS with Node 18+ |
| Vendored | clone the repo + pnpm pack → ship the .tgz | Strict supply-chain controls |
| Docker | docker pull ghcr.io/aegyrix/check-pqc:offline && docker save -o cli.tar ghcr.io/aegyrix/check-pqc:offline → transfer | Hosts with Docker but no Node |
Then on the airgapped host:
docker load -i cli.tar
docker run --rm ghcr.io/aegyrix/check-pqc:offline internal.corp --offlineAll command-line options
check-pqc <hostname[:port]> [options]
Options
--port, -p <n> Port (default: 443, or :PORT in hostname)
--json Output raw JSON
--no-color Disable ANSI colors
--offline Probe locally with no API call (airgap / SCIFs)
--check-offline Print local PQC capability and exit (no probe)
--api <url> Override the online API endpoint
--help, -h Show help
--version, -v Show versionOperate (run it day-to-day)
Scheduled audit (cron + Task Scheduler)
Linux / macOS — cron
/etc/cron.d/checkpqc-audit (root) or crontab -e (user):
# Probe a list of hosts every morning at 06:30 local time.
# On regression (non-zero exit), mail the user via cron's MAILTO.
[email protected]
30 6 * * * checkpqc /usr/local/bin/check-pqc api.example.com --json | tee -a /var/log/checkpqc/api.log | jq -e '.verdict | test("^(PQC|HYBRID)_ENABLED$")' >/dev/null || echo "API regressed"
35 6 * * * checkpqc /usr/local/bin/check-pqc www.example.com --json | tee -a /var/log/checkpqc/www.log | jq -e '.verdict | test("^(PQC|HYBRID)_ENABLED$")' >/dev/null || echo "WWW regressed"Or — simpler, no jq — just rely on the exit code:
30 6 * * * /usr/local/bin/check-pqc api.example.com >/dev/null || \
echo "PQC regression on api.example.com" | \
mail -s "[checkpqc] regression" [email protected]Windows — Task Scheduler (PowerShell)
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument '-NoProfile -Command "& check-pqc api.example.com; if ($LASTEXITCODE -ne 0) { Send-MailMessage -To [email protected] -Subject ''[checkpqc] regression'' -SmtpServer smtp.example.com }"'
$trigger = New-ScheduledTaskTrigger -Daily -At 6:30am
Register-ScheduledTask -TaskName "CheckPQC daily audit" -Action $action -Trigger $trigger -User SYSTEMSending results to a Slack / Teams webhook
RESULT=$(check-pqc api.example.com --json)
VERDICT=$(echo "$RESULT" | jq -r .verdict)
if [[ "$VERDICT" != "PQC_ENABLED" && "$VERDICT" != "HYBRID_ENABLED" ]]; then
curl -X POST -H 'Content-Type: application/json' \
-d "{\"text\":\":warning: PQC regression on api.example.com — verdict: $VERDICT\"}" \
"$SLACK_WEBHOOK_URL"
fiUpdating the CLI
# npm install
npm update -g check-pqc # or @aegyrix/check-pqc
# Pin to a specific version (recommended for CI)
npm install -g [email protected]
# Docker
docker pull ghcr.io/aegyrix/check-pqc:latest
# PowerShell module
Update-Module CheckPQC
# winget
winget upgrade aegyrix.check-pqcTelemetry / data sent
The CLI sends hostname + port only, and only in online mode. No
request bodies, no headers, no credentials. --offline makes zero
outbound calls. The API drops requester IPs after 7 days. Full details:
checkpqc.com/privacy.
Logs and where to find them
check-pqc itself does not write a log file — output goes to stdout.
If you want a persistent record, redirect:
check-pqc api.example.com --json >> /var/log/checkpqc/api.logThe API keeps a 7-day rotating audit log on the server (operator
side, not visible to clients). If you self-host the API, see
/var/log/checkpqc/contact.log and the systemd journal:
journalctl -u checkpqc-api -fTroubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| command not found: check-pqc | npm prefix not on $PATH | export PATH="$(npm config get prefix)/bin:$PATH" in your shell rc |
| UNKNOWN — handshake aborted | Target firewalled, blocking your IP, or down | Try from another network; retry; check target on port 443 |
| --check-offline says "not capable" | Local OpenSSL too old | brew upgrade openssl@3 (macOS) or use docker run ghcr.io/aegyrix/check-pqc:offline |
| All hosts return UNKNOWN | Outbound TLS to api.checkpqc.app blocked | Use --offline mode (requires OpenSSL 3.5+) or run via Docker |
| EACCES during npm install -g | npm prefix not user-writable | sudo npm install -g, or use a Node version manager (nvm/fnm/asdf) |
| Slow / hanging probe | DNS resolution slow | The CLI has a 15s overall timeout; increase upstream DNS-cache TTL |
For anything not in the table, file an issue: github.com/aegyrix/checkpqc.app/issues.
Uninstall
The CLI installs only one global npm package (or one container image,
or one PowerShell module). Removal is one command. No dotfiles, no
launch agents, no daemons, no registry edits — check-pqc is fully
ephemeral; every probe is a fresh subprocess.
npm install
# Whichever name you installed under
npm uninstall -g check-pqc
npm uninstall -g @aegyrix/check-pqc
# Verify
command -v check-pqc # should print nothingHomebrew (if you used brew install)
brew uninstall check-pqcDocker
# Just remove the image — there is no persistent container
docker rmi ghcr.io/aegyrix/check-pqc:latest
docker rmi ghcr.io/aegyrix/check-pqc:offline
docker rmi ghcr.io/aegyrix/check-pqc:0.2.6Windows — winget
winget uninstall aegyrix.check-pqcWindows / cross-platform — PowerShell module
Uninstall-Module CheckPQC -AllVersionsWhat gets left behind?
Nothing the CLI created itself. It doesn't write to ~/.checkpqc,
~/.config, or anywhere else. The only artifacts are whatever output
you redirected (e.g. > report.txt, >> /var/log/checkpqc/audit.log),
plus any cron entries or scheduled tasks you wrote.
If you want a paranoid sweep:
# macOS / Linux
which check-pqc # confirm gone
ls -la "$(npm config get prefix)/bin/check-pqc" 2>/dev/null # confirm gone
crontab -l 2>/dev/null | grep -i check-pqc # check for cron entries# Windows
Get-Command check-pqc -ErrorAction SilentlyContinue
Get-ScheduledTask | Where-Object { $_.Actions.Execute -match 'check-pqc' }Privacy
- The CLI sends only the hostname and port to
api.checkpqc.appin online mode — never request bodies, never headers. - Request IPs are dropped from logs after a sliding 7-day window.
--offlinemode makes zero outbound API calls.- See checkpqc.com/privacy.
License
MIT — © Aegyrix LLC
