Open source · Author & maintainer (solo) · Sep 2026 — Present
pqinv — post-quantum readiness scanner for identity infrastructure
A Go CLI that inventories the cryptography an identity platform actually uses — OIDC/JWKS signing keys, TLS handshakes, certificate chains, source code — classifies every finding against the NIST post-quantum migration timeline, and gates CI on the result. Outputs a readiness grade, stable JSON, or a CycloneDX 1.6 CBOM.

The project
Two deadlines drive the post-quantum migration: under NIST IR 8547, classical public-key cryptography is deprecated after 2030 and disallowed after 2035. The urgent problem arrives earlier. TLS traffic and encrypted tokens recorded today can be decrypted retroactively once a cryptographically relevant quantum computer exists, so quantum-vulnerable key exchange is a today-problem, while quantum-vulnerable signatures, which cannot be forged retroactively, are a plan-now problem. Identity infrastructure sits at the sharp end: every ID token an IdP issues is signed with RS256 or ES256, and no standardized post-quantum JOSE algorithm exists yet.
You cannot manage what you have not inventoried, and the existing PQ checkers answer one yes/no question about a TLS endpoint. pqinv aims at the layer above. Everything is a pipeline: independent probes collect evidence as flat, source-agnostic crypto assets; a data-driven classifier turns assets into verdicts with rationale and migration advice; a correlation pass adjusts urgency across findings; a scorer reduces verdicts to a 0–100 grade; and reporters render a terminal report, diffable JSON, or a CycloneDX CBOM. Probes know nothing about classification, and the classifier knows nothing about where an asset came from.
I built it because of my day job. Working on a self-hosted Keycloak identity platform with a hybrid post-quantum credential-signing path, I kept wanting a single command I could point at a realm to learn which keys, algorithms, and handshakes were quantum-vulnerable and how urgently. pqinv v0.1.0 shipped in September 2026 under the MIT licence, with an OIDC probe that no other open-source PQ scanner has.
What I shipped
- Designed the pipeline architecture — probes → classify → correlate → score → report — with a zero-dependency domain model at the centre, so a Kubernetes operator in v2 can import every internal package unchanged.
- Built the TLS probe: enumerates supported key-exchange groups (including hybrid X25519MLKEM768) and protocol versions TLS 1.0–1.3 by issuing deliberately constrained ClientHellos, and emits one asset per certificate in the presented chain.
- Built the OIDC probe: fetches the discovery document and JWKS for any issuer (Keycloak, Auth0, Entra, Okta) and classifies both the published keys, with real key sizes, and the advertised signing algorithms, including the dangerous alg: none.
- Built the source-code probe: walks a repository for crypto imports, JOSE algorithm literals, and committed private keys, reporting findings at file:line, with deduplication and sensible skips for binaries and documentation.
- Wrote the classifier as data, not code: an embedded rules.yaml matched first-match-wins on algorithm globs, usage, and key size, strictly validated at load so a mistyped rule fails loudly rather than silently never matching.
- Implemented usage-based urgency and cross-finding correlation, so a classical group offered alongside a hybrid PQ group is reported as a fallback rather than an exposure.
- Designed the scoring model — per-urgency deductions capped per usage category, mapped to A–F grades — and calibrated it against real scans of cloudflare.com, github.com, and a stock Keycloak realm.
- Shipped three reporters over one schema: a terminal view grouped by urgency, timestamp-free JSON that is byte-identical across identical scans, and a CycloneDX 1.6 Cryptography Bill of Materials.
- Added the CI gate (--fail-on) and multi-target fan-out with bounded concurrency, where an unreachable target always fails, because "couldn't check" must never read as "all clear".
- Wrote the test suite around real local TLS servers pinned to specific groups, an httptest fake issuer shaped like Keycloak, table tests over every classification rule, and a regression test for a CBOM library bug.
How it works
The pipeline: probes collect evidence, the classifier judges it, reporters render it
Challenges worth talking about
Urgency is a function of usage, not algorithm
A flat list of "RSA is quantum-vulnerable" findings is useless; everything is vulnerable. The model that makes pqinv actionable is that a vulnerable key exchange is urgent today, because recorded ciphertext is harvestable, while a vulnerable signature cannot be forged retroactively and is a plan-now item. I encoded that in the domain model, adding a Protocol usage mid-build so "still accepts TLS 1.0" is a classifiable finding rather than a footnote.
Enumerating TLS groups with a client that negotiates one
Go's crypto/tls picks a single key-exchange group per connection, so you cannot ask a server what it supports. The probe offers one group at a time in narrow ClientHellos and records which handshakes succeed. Pinning TLS ≤1.2 to ECDHE-only cipher suites was essential: otherwise a server could choose plain-RSA key transport and the handshake would succeed without using the offered group, a false positive.
A score that survives real-world scans
The original flat weights collapsed on real targets: a Keycloak realm advertising eleven classical JOSE algorithms restated one fact eleven times and scored as if it had eleven wounds. I introduced per-usage caps so key exchange, where harvest-now-decrypt-later bites, carries the most weight, while classical certificate signatures, unavoidable in 2026, cannot drag a well-configured system to an F. Calibration: cloudflare.com C 52, github.com D 31, a stock Keycloak realm C 60.
Deterministic output for a trustworthy CI gate
A gate that flaps is a gate that gets disabled. Assets are sorted before classification, results keep config order regardless of completion order, and the JSON and CBOM carry no timestamps or serial numbers, so identical scans produce identical bytes and reports diff cleanly in CI. A failed target becomes a report entry, never a crashed scan, and always trips the gate.
Working around a CBOM library that dropped the crypto typing
The CycloneDX Go library's version converter silently lost the cryptographic-asset component type, which is the whole point of a CBOM. I bypassed the converter with a plain encode and a manual 1.6 stamp, and pinned the behaviour with a regression test so a future upgrade cannot reintroduce it.
Stack
Language & libraries
- Go 1.24 (X25519MLKEM768 in crypto/tls)
- cobra
- lipgloss
- lestrrat-go/jwx v2
- cyclonedx-go
Probes
- TLS handshake enumeration + x509 chain
- OIDC discovery + JWKS
- Source-code pattern scan
Output
- Terminal report by urgency
- Stable, diffable JSON
- CycloneDX 1.6 CBOM
- --fail-on CI gate
Standards
- FIPS 203 (ML-KEM)
- FIPS 204 (ML-DSA)
- NIST IR 8547
- RFC 8446 / RFC 8996
Outcomes
- Released pqinv v0.1.0 (MIT) with go install and prebuilt binaries — the only open-source PQ scanner that covers the identity layer: OIDC/JWKS signing keys, certificate chains, and source code, not just TLS.
- Turned PQ readiness into a number teams can track: real September 2026 scans graded cloudflare.com C 52 (hybrid ML-KEM live, but TLS 1.0/1.1 still accepted), github.com D 31 (no PQ key exchange at all), and a stock Keycloak realm C 60 (every token signature quantum-vulnerable, RSA-OAEP encryption key urgent).
- Made the migration reviewable: every finding carries a rationale, a migration step, and standards references, and the CycloneDX 1.6 CBOM output slots into existing SBOM tooling.
- Shipped a CI gate that fails the build on findings at or above a chosen urgency, so PQ regressions are caught the same way test failures are.
- Set up v2 without a rewrite: a Kubernetes operator that watches Ingress and TLS Secrets and exports per-target readiness as Prometheus metrics needs only a second thin command, because nothing under internal/ prints or exits.