pqcota

v0.9.0 — Discovery · Inventory · Provisioning run end to end on Linux, and the demo applies generated artifacts to real nodes and rolls them back. Actions carry which snapshot state they came from, and the generator resolves that evidence against the real history and records it. Windows nodes run through the same discovery path (CNG and JCA) — generating their migration artifacts is not there yet → roadmap
A PQC migration management platform (OSS, Apache-2.0). It handles the PQC migration of legacy crypto runtimes (OpenSSL · Java JCE/JCA) across three stages: Discovery → Inventory → Provisioning.
Name — pqcota (pronounced P-cota) = PQC (post-quantum cryptography) + Orchestra (-ota). This software is a player in the orchestra, not the maestro. The maestro who decides what to migrate and when is the user wielding the tool; pqcota plays its own part (observe · normalize · generate) precisely.
platform structure diagram
demo video (2 min 55 s) — from observation through applying and rolling back the generated artifacts: ML-KEM goes 0 → 14 → 0 on a real node.
Read on → roadmap · contributing · compatibility policy
What it looks like
Observations are tied to nodes and apps, and the group actually negotiated on the wire is shown alongside.
──────── ① discovered assets (per node) ────────
pay-app
• JCA provider chain: SUN,SunRsaSign,…,BC [EVIDENCE_STRENGTH_CONFIRMED]
↑ this BC appears nowhere in java.security (grep: 0 hits).
The app registered it at runtime — invisible to static scans.
• OpenSSL libcrypto.so.3 3.5.5 (OpenSSL) [EVIDENCE_STRENGTH_CONFIRMED]
pay-db
• OpenSSL libcrypto.so.1.1 1.1.1f (OpenSSL) [EVIDENCE_STRENGTH_CONFIRMED]
──────── ② observed edges + quantum-resistance grade ────────
🟢 web-gw → pay-app TLS X25519MLKEM768 [fips-standard]
🟢 web-gw → pay-app SSH sntrup761x25519-sha512@openssh.com [experimental]
🔴 web-gw → pay-db TLS x25519
🔴 web-gw → pay-db SSH curve25519-sha256
grade totals: 🟢 PQC 2 · 🔴 classical 2 · ⚪ unknown 0
The same observation is also rendered as a topology.

🔴 is not a verdict of "vulnerable" — it is the observation that a classical algorithm was negotiated. What to change, and when, is the user's call. Full expected output lives in demo/expected-output.
What it does — three stages
| Stage |
What it does |
Output |
| ① Discovery |
Observes which cryptography is in use on running systems — loaded libraries, JVM provider chains, algorithms negotiated in the handshake |
per-node observations (canonical CBOM) |
| ② Inventory |
Ties each observation to the node and the apps it belongs to, and accumulates them — machine metadata, diffs between snapshots |
a central, append-only inventory |
| ③ Provisioning |
Generates the PQC migration artifacts from a finalized plan — config fragments, apply/rollback Ansible playbooks (L1/L2/L3), rollback basis |
playbooks + before records |
What it does not do — declaration (CMDB) reconciliation, review-and-sign-off governance and
fleet orchestration are not in these repositories. The contracts (contracts/)
hold their place, and no judgment engine is built — once the tool decides for you, the line that
"🔴 is an observation, not a verdict" collapses. What each stage promises, and where it stops, is written in its own README and in the contracts.
Try it — demo
With just Docker, run the whole scope at once — access prep → discovery → inventory →
provisioning (generate, apply, roll back), against nodes it stands up as containers. The demo builds all five repositories, so clone them side by side first (Build).
./demo/scripts/up.sh && ./demo/scripts/demo.sh # tear down: ./demo/scripts/down.sh
Pick where to start.
- The whole scope at once, with Docker → the two lines above. Setup, expected output, and how to point it at your own hosts are in demo/
- One command at a time, minimal setup → examples/. Go is all you need — no Postgres, no target nodes
- Working on the repo → CONTRIBUTING, and the stage READMEs above
Requirements
To build
- Go 1.26.4+
- buf (+
protoc-gen-go, protoc-gen-go-grpc) — only when you change a proto (in pqcota-common). The generated gen/ is committed, so a plain build does not need it
- JDK 11+ — optional, only to build the JVM attach sidecar; without it that step is skipped
To run
- Multiple nodes — Ansible on the controller, SSH access to the targets
- A single node — nothing to install; run the binary on that node directly (
pqcota-netcap needs CAP_NET_RAW) → pqcota-discovery cmd
Build
pqcota is five repositories: this one (demo, examples, release bundles, contributing guide) and four
modules. Clone them side by side, because go.mod reads the four from ../ until they are tagged:
git clone https://github.com/randyinthedev-hash/pqcota
git clone https://github.com/randyinthedev-hash/pqcota-common # contracts, generated code, shared logic
git clone https://github.com/randyinthedev-hash/pqcota-inventory # the inventory stage
git clone https://github.com/randyinthedev-hash/pqcota-discovery # collectors, their commands, the reference playbook
git clone https://github.com/randyinthedev-hash/pqcota-provisioning # the provisioning stage
cd pqcota
pqcota consists of one central controller node and the target nodes it reaches over
Ansible/SSH. You build on the controller — both the CLIs you run there and the collectors you
ship to the target nodes are produced here.
It builds straight from a clone. The Go code generated from the contracts is committed in
pqcota-common, so you need no code-generation tooling of your own; consumers can use the contract
types with go get alone. It is regenerated only when a proto changes (see the end of this section).
① The CLIs you run on the controller — ingest and query observations, generate playbooks.
D=github.com/randyinthedev-hash
go build -o bin/ $D/pqcota-common/cmd/... $D/pqcota-discovery/cmd/... $D/pqcota-inventory/cmd/... $D/pqcota-provisioning/cmd/...
② The collectors that go on the target nodes — built statically for the node's OS and arch.
Which collector runs on which OS is in
the command reference.
D=github.com/randyinthedev-hash/pqcota-discovery/cmd
# Linux nodes
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/linux-amd64/ $D/pqcota-nodescan $D/pqcota-netcap $D/pqcota-jvmscan
# Windows nodes
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dist/windows-amd64/ $D/pqcota-cngscan $D/pqcota-jvmscan
make build-jar # only if you have JVM nodes: attach sidecar → build/collector.jar
CGO_ENABLED=0 (static linking — distro/libc agnostic) is fixed; what you change is GOOS and
GOARCH. For the accepted values see the Go documentation.
Linux nodes need kernel 3.2 or newer. That is the floor the Go toolchain sets, and this repo asks
for nothing newer. CentOS 7 (3.10) and Debian 8 (3.16) are above it; RHEL 6 (2.6.32) is below. What
individual features additionally require is in the supported scope.
Privileges and environment variables for running the collectors on a node → pqcota-discovery cmd.
If you changed a proto, regenerate the contract code. This is the procedure for someone working
on the contracts, and it runs in pqcota-common. make tools installs the generator plugins
(protoc-gen-go, -grpc) and make generate does the conversion. Put the resulting gen/ in
the same commit as the proto you changed.
cd ../pqcota-common
make tools && make generate # contracts/proto → gen/
make tools puts the plugins in $(go env GOPATH)/bin. If that directory isn't on your PATH,
make generate fails with "plugin not found" — which looks like a failed install but really means
it just isn't visible. Both targets call that case out, but adding it to your shell profile
saves you from hitting it every time: export PATH="$PATH:$(go env GOPATH)/bin".
Contributing to the repos (tests, gates, contract changes) → CONTRIBUTING.
Stack
- Go — every collector and CLI;
CGO_ENABLED=0 static single binaries
- Java — only the JVM attach sidecar (that observation is possible only from inside the JVM)
- Protobuf/gRPC — the contracts that join the stages (
contracts/)
- Postgres — only when accumulating and querying many nodes over time; not used for single-node observation
Supported scope
Observation (Discovery)
| What is observed |
Target |
Why |
| OpenSSL assets · communication edges |
Linux (amd64·arm64) |
depends on /proc, ELF, AF_PACKET |
| JVM provider chains |
Java 8+ · Linux (full) · Windows (with a JDK) |
the path that attaches without a JDK is Linux-only. On Windows the machine needs a JDK to reach runtime registrations; without one only java.security is read |
| Windows CNG providers and algorithms |
Windows (amd64·arm64) |
it asks bcrypt.dll directly for the registered providers — no WMI, no PowerShell |
Inventory
| What |
Target |
Why |
| the ingest and query CLIs |
anywhere — Linux, macOS, Windows |
they touch only files and a database, no OS primitives |
| the store |
Postgres (append-only) |
only when you want history and changes. For a single look, pqcota-discover-view needs no store |
| accepted input |
a collector's CollectionResult · CycloneDX 1.6/1.7 CBOM · a declaration written by a person |
what a machine saw and what a person wrote down never share a lane |
Migration (Provisioning)
What is generated depends on the remediation kind in the plan.
| Runtime |
Situation |
What is generated |
| OpenSSL |
3.5+ (native PQC) |
a config fragment only — the legacy runtime is untouched |
|
3.0–3.4 |
provider module staging + a config fragment referencing it. You supply the module |
|
1.1.1 and older |
nothing — a fork replacement is required, so it is marked as a manual step |
| JCA (Java) |
JDK 24+ (native PQC) |
a java.security fragment only. It keeps a classical group alongside — released JDKs still do not negotiate the hybrid TLS group (measured up to JDK 25) |
|
JDK 8+ (provider injection) |
provider JAR staging + a java.security registration fragment. Staging the JAR alone does not load it, so activation is a separate step |
|
older (EOL) |
nothing — a JDK upgrade is required, so it is marked as a manual step |
Application goes through Ansible playbooks. The generated playbook assumes POSIX paths and modules (ansible.builtin.copy, /opt/pqcota), so the target nodes are Linux — Ansible itself also drives Windows, but this output does not yet.
Windows (CNG) is observed (pqcota-cngscan). Generating its migration artifacts needs the substrate generalization first, so it stays on the roadmap.
Status · version
Every release attaches per-architecture static binaries and SHA256SUMS to the
releases. The bundles are split by where they
go: the node bundles (pqcota-linux-*, pqcota-windows-*) are carried onto the hosts you observe, and
the controller bundle (pqcota-ctl-linux-*) sits on the one central machine. The latter carries every
ingest, query, approval and generation CLI. Verify what you download with sha256sum -c SHA256SUMS; signed
releases are on the roadmap.
Per-version goals and results are in the release notes.
License