210 lines
19 KiB
Markdown
210 lines
19 KiB
Markdown
# AERE Quantum Security Control Plane
|
|
|
|
From "we found the problem" to "this is exactly what changes, in which order, what we can migrate automatically, how the rest is
|
|
fixed on your server, and how that is proven". Node.js 24 with OpenSSL 3.5, no dependencies. It composes tools that exist beside it:
|
|
the cryptographic inventory (`crypto-inventory/`), the TLS readiness scanner (`readiness/`), the PQ Gateway, KMS and PKI
|
|
(`pq-gateway/`, `pq-kms/`, `pq-pki/`), the verification layer (`verify-layer/`) and the AIP-23 envelope builder (`proof-kinds/`).
|
|
|
|
Code comments, internal names, test names and file names are in Romanian; command lines, JSON fields, messages and this
|
|
documentation are in English (the data format was translated on 2026-09-29, version 2 of each format).
|
|
|
|
## 1. Plan: `control-plane.mjs`, `plan-migrare.mjs`
|
|
|
|
node control-plane.mjs --code <dir> [--scan scan.json] [--json]
|
|
|
|
Runs the inventory on a code directory and, optionally, takes a scan of a hostname (the JSON of the readiness scanner), and writes a
|
|
prioritized plan. Every quantum-vulnerable asset becomes an action:
|
|
|
|
Since 2026-10-18 (first run on a code base that is not ours, a Rust messenger): a key agreement that the
|
|
application calls itself (X25519 in a ratchet, a PQXDH handshake, a Tor `ntor` handshake, `createECDH` in
|
|
Node) is **not** routed to the PQ Gateway, because a TLS edge cannot see it; its action is `manual`, in
|
|
code, with the hybrid target written out (X25519 + ML-KEM-768 in the same handshake and key schedule). A
|
|
signature or public-key encryption that the application performs itself keeps its PKI/KMS action (the
|
|
product issues the hybrid key, for real), and `how` says that the call site must then be changed to use it.
|
|
Only a finding with no call context (no `api`, no language) keeps the old routing: its context is not
|
|
known, so it is not guessed. The inventory's TLS configuration findings still go to the gateway.
|
|
|
|
{ ref, asset, problem, current, target, method: "auto-aere" | "manual" | "blocked", product: "gateway" | "kms" | "pki" | null,
|
|
how, cn?, primitive?, urgency: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW", blocker, scope: "production" | "test", location, source }
|
|
|
|
Two rules added on 2026-10-18 after the run on the libsignal tree. A finding in **test code** (`tests/`, `benches/`, `fuzz/`,
|
|
`examples/`, `*Test.java`, `*.test.ts`, ...; the path rule lives in `scopulCaii`) stays in the plan but with `scope: "test"`,
|
|
urgency `LOW` and a `how` that says so, and `summary.byScope` counts it apart: on libsignal 76 of 197 actions (75 of them HIGH)
|
|
were in such files and stood in front of the production call sites. A **self-signed certificate** that is a pinned trust anchor
|
|
(valid for more than five years, or whose CN is not a host name: the AMD, Google and AWS attestation roots in libsignal) is
|
|
`blocked` with the reason written, not "auto through the PQ PKI": the PKI cannot replace a root the verifier pins, only its owner
|
|
can publish a post-quantum one. A short self-signed service certificate for a host name is still reissued by the PKI for the same CN.
|
|
|
|
Harvest-now-decrypt-later applies to key exchange and encrypted data, **not to signatures** (a signature is public; it is not
|
|
harvested), so a classical key exchange on a public endpoint is the only CRITICAL case, and a classical signature is a future
|
|
forgery risk. The plan is honest about blockers: a public WebPKI certificate cannot be reissued post-quantum today (no public CA
|
|
issues one), so it is `blocked` with the reason written, not `auto`. The scan findings are classified by a closed table on the ids
|
|
the scanner actually emits (the test derives them from the scanner's source and fails on an id without a classification).
|
|
|
|
## 2. Execution: `executa-migrare.mjs`
|
|
|
|
node executa-migrare.mjs --plan plan.json --out <dir> [--consent ref1,ref2|all] [--execute]
|
|
[--gw-cert c.pem --gw-key k.pem --gw-upstream http://h:p [--gw-mode hybrid-only|hybrid-preferred] [--gw-listen 127.0.0.1:8443] [--gw-keep]]
|
|
[--kms-url http://127.0.0.1:8420 --kms-token-file f] [--pki-dir ca --pki-ca issuing --pki-roots ca/root.crt] (AERE_PKI_PASSPHRASE)
|
|
|
|
Nothing runs without consent **per action**, and the default is a dry run (`--execute` starts the work). Each `auto-aere` action is
|
|
done through the matching product and judged by a **measurement afterwards**, not by an exit code:
|
|
|
|
| product | what it does | the proof afterwards | rollback |
|
|
|---|---|---|---|
|
|
| `gateway` | starts the PQ Gateway in front of the service and writes its configuration for your service manager | a client offering only `X25519MLKEM768` completes a TLS 1.3 handshake; a second, classical-only client says whether classical clients are still accepted (`hybrid-preferred`) or refused (`hybrid-only`) | the gateway is stopped |
|
|
| `kms` | creates the hybrid key (X25519 + ML-KEM-768) or rotates it if it exists; the name comes only from the `ref` (`mig-...`) | `latest_version` and the fingerprint read back from the KMS | older versions stay decryptable; nothing is deleted |
|
|
| `pki` | issues an ML-DSA-65 certificate from your PQ CA for a `cn` checked to be a host name (no wildcard) | `verifyChain` up to the root | files written in part are removed |
|
|
|
|
Every action is a record in a hash chain `sha256(seq | prev | record)` in `execution.json` (`records[]`, each `{ seq, prev, record,
|
|
hash }`; verdicts `OK`, `FAILED`, `DRY-RUN`, `SKIPPED-no-consent`). The console and anyone else can check it again; a changed or
|
|
removed record is caught. It does not change your DNS, ports or firewall (moving traffic to the gateway stays with you), does not
|
|
issue public WebPKI certificates, and does not delete keys.
|
|
|
|
## 3. Remediation: `remediere.mjs`
|
|
|
|
node remediere.mjs recipe --plan plan.json --profile nginx|apache|haproxy|node|go [--json]
|
|
node remediere.mjs verify --plan plan.json --scan after.json [--applied-at 2026-09-28T10:00:00Z] [--out dir]
|
|
|
|
The actions no AERE product can do (the `manual` ones: TLS 1.3 missing, TLS 1.2 accepted, the hybrid group not preferred, HSTS, the
|
|
certificate) live in your server's configuration, which we do not touch. The **recipe** gives, for your server software, the exact
|
|
fragment, where it goes, the measurable precondition (the OpenSSL or Go version and the command that shows it) and the configuration
|
|
check to run before reloading; fragments are generated from one structured form (groups, minimum version, HSTS), and certificate
|
|
problems get operational steps. The **proof**: after you apply it, a new scan judges each action `RESOLVED` (the defect is gone AND the
|
|
required property is measured), `UNRESOLVED`, or `UNMEASURED` (the scan failed, is of another host, or is not from after the time you
|
|
applied the change: `--applied-at`, otherwise the plan's `generatedAt`; with neither, everything is `UNMEASURED`). A domain in the
|
|
plan that is not a host name gets no recipe, so no command to copy is built with it. What is measured by us is written on every
|
|
recipe (`measured`): since 2026-10-18 all five profiles end to end, on real servers started from exactly the recipe's fragments
|
|
(nginx 1.31.7 with OpenSSL 3.5.8/3.5.9, Apache 2.4.69 with OpenSSL 3.5.9 and HAProxy 3.4.7 with OpenSSL 3.5.9, each in a container;
|
|
Go 1.26.5; Node earlier): a client offering only the hybrid group is accepted, a TLS 1.2 client is refused, HSTS is sent
|
|
(`dovezi/retete-2026-10-18/`); not measured: a production server with its full configuration, real browsers as clients.
|
|
|
|
## 4. Compliance report: `raport-conformitate.mjs`
|
|
|
|
import { complianceReport, verifyReport } from './raport-conformitate.mjs';
|
|
const { report, envelope, evidenceHash } = complianceReport({ plan, organization, date: '2026-09-28', risk: 'high', cnsa: true, cnsaCategories });
|
|
|
|
For every action: which regimes apply (NIST IR 8547 initial public draft; the EU coordinated roadmap of 2025-06-23; optionally
|
|
CNSA 2.0), each deadline and the days left on the report's date, and whether the asset is exposed today (key exchange: yes;
|
|
signature: no; a key of unknown use: not known). Each regime carries its source, the source's date and the date it was read; CNSA 2.0
|
|
is read from secondary sources, and says so. An action the report cannot classify is counted, and then the report cannot say that
|
|
there is no vulnerable algorithm. The report is bound to an AIP-23 `compliance` envelope whose `evidenceHash` is the digest of the
|
|
canonical report. It is not a certification and not a legal opinion, and it covers only what was inventoried and scanned.
|
|
|
|
## 5. Console: `consola.mjs`, `consola-web/`
|
|
|
|
node consola.mjs --bundle bundle.json [--plan plan.json] [--execution execution.json] [--json]
|
|
|
|
One state of a deployment: the audit chain of the verification layer recomputed from its entries, the integrity of every AIP-23
|
|
envelope recomputed, the plan folded in, and the execution chain checked again. The console does not trust what a bundle says about
|
|
itself (`chainOk: true` over a modified log is reported as a suspect self-report). `consola-web/index.html` renders that output; it
|
|
computes nothing in the browser, and a bundle pasted there directly is shown as `UNCHECKED`.
|
|
|
|
## 6. Organization judgement: `organizatie.mjs`
|
|
|
|
node organizatie.mjs --pack pack.json [--context context.json] [--require shield,identity,...] [--at <RFC 3339>] [--online] [--json]
|
|
|
|
Exit code 0 = VALID, 1 = INVALID, 2 = NOT_MEASURED (or the command could not run), 3 = PARTIAL.
|
|
|
|
An organization brings an **evidence pack**; every artifact in it is judged again by the published verifier of its kind, and the
|
|
result is one verdict per domain and a summary that is never stronger than the weakest domain that was asked for. Nothing the pack
|
|
says about itself is believed.
|
|
|
|
**The pack** (`aere-organization-evidence-pack`):
|
|
|
|
{ "kind": "aere-organization-evidence-pack", "organization": "...", "createdAt": "...",
|
|
"artifacts": [ { "id": "...", "artifact": "<kind>", "input": { ... } }, ... ] }
|
|
|
|
`id` is letters, digits and `. _ : / -`, at most 128 characters, unique in the pack (a repeated id is judged once, its copies are
|
|
`NOT_MEASURED`). `artifact` is one of 25 kinds in eight domains:
|
|
|
|
| domain | kinds |
|
|
|---|---|
|
|
| `shield` | `shield-console` (a sidecar bundle, the plan and the execution chain, judged by `consola.mjs`; an execution given with a plan must be the executor's run of that plan) |
|
|
| `identity` | `identity-presentation`, `sdjwt-presentation`, `sdjwt-vc-presentation` |
|
|
| `compliance` | `compliance-presentation`, `travel-rule-binding`, `travel-rule-message`, `travel-rule-receipt`, `pq-compliance-report` (a report of section 4 and its envelope) |
|
|
| `ai` | `ai-trust-bundle`, `agent-ledger` |
|
|
| `payments` | `payment-risk`, `payment-receipt`, `market-timeline` |
|
|
| `devices` | `device-bundle`, `device-operation-log`, `device-reputation` |
|
|
| `audit` | `audit-log`, `audit-inclusion`, `audit-consistency` |
|
|
| `proofs` | `envelope` (AIP-23), `dataset-record`, `dataset-answer`, `dataset-consistency`, `dataset-sample` |
|
|
|
|
Cloud is not a domain of its own: what a Cloud product returns is judged in the domain of the artifact. The 23 kinds of the
|
|
conformance suite are judged through the suite's adapter (`../conformance/implementare-aere.mjs` beside a public copy of the control
|
|
plane), called as a program, so a verifier that does not start is seen as such; the two kinds of the control plane itself are judged
|
|
here (`shield-console` by the console's own evaluation, plus the binding of the execution to the plan given with it;
|
|
`pq-compliance-report` by `verifyReport`, by making the report again from its own findings with the same generator, which must give
|
|
exactly the report and the envelope's statement, and by the AIP-23 verifier for its envelope). The compliance envelope is not
|
|
signed, so a report bound to it shows only that the report is self-consistent; whether its findings are the real plan's is not
|
|
judged. A kind no verifier here judges is listed as unjudged.
|
|
|
|
**The context** (`aere-organization-context`) belongs to the party that relies on the pack, not to the organization:
|
|
|
|
{ "kind": "aere-organization-context", "now": "<RFC 3339>", "require": ["identity", ...],
|
|
"byKind": { "<kind>": { ... } }, "byArtifact": { "<id>": { ... } } }
|
|
|
|
Every input of every verifier is classified (the test derives the keys from the adapter's source, from the source of the module the
|
|
adapter hands a whole input to, and from the 791 vectors of the suite, and fails on a key without a class): the **artifact** comes
|
|
only from the pack; a **stapled** input (a status list, a revocation, a firmware policy, signed by a third party) is taken from the
|
|
context and from the pack, joined, the context's lists first (a verifier reads only the first entries of a long list, so lists brought
|
|
by the pack cannot push the relying party's own revocation out of what is read); a **verifier** input (the trusted issuers or
|
|
manufacturers, the audience, the nonce, the expected values, the relying party's own readings: a market's arbiter and arbitration
|
|
timeout, the facts a reputation is computed from) comes from the context, and if only the pack brings it, it is used but the artifact
|
|
cannot be VALID (`PARTIAL`, because the organization chose its own trust); a **verifier-only** input (the chain to read, a
|
|
beneficiary's private KEM keys, the time limit) is never taken from the pack; the **clock** comes from the context for that artifact
|
|
or kind, then `--at`, then the context's global `now`, then this machine, never from the pack. For a market timeline and a reputation,
|
|
whose module reads the whole input, a key the module does not read by name stays with the artifact (so a member too many is still
|
|
refused).
|
|
|
|
**The verdicts.** Each artifact gets the verifier's verdict (`VALID`, `INVALID`, `PARTIAL`), or `NOT_MEASURED` when the verifier
|
|
did not run or gave no verdict of the suite's form: a verdict whose own levels do not give it (by the suite's rule in `verdict.mjs`),
|
|
or a verdict for another kind than the one asked in its place (the order of the adapter's answers cannot then be trusted, so none of
|
|
them is). When a batch does not complete (too few answers, or the time limit), each artifact is judged alone, so one artifact cannot
|
|
take the verdicts of the others with it. A domain is as weak as its weakest artifact; a domain with no artifact is
|
|
`NOT_PRESENTED`, and a required domain that is not presented makes the summary `NOT_MEASURED`, never `VALID`. The required domains
|
|
are `--require`, then `context.require`, then all eight. Unjudged artifacts cap a `VALID` summary at `PARTIAL` (the pack claims more
|
|
than was measured). A verdict written in the pack (`verdict`, `summary.verdict`, an artifact's `verdict` or `expected.verdict`) is
|
|
ignored, and named as suspect when it differs from the measurement.
|
|
|
|
**The network.** Anchoring and finality on a chain are read only with `--online`, and only for the kinds whose verifier reads the
|
|
chain (an AIP-23 envelope, a payment receipt, a market timeline, an AI trust bundle, the report's envelope). Without it, each of those
|
|
that claims a notarization is listed in `network.needed` and its levels say `NOT_MEASURED`. A device bundle or an operation log
|
|
judges its notarization on the relying party's own readings (`anchors`, `readings` in the context), never on the chain read here,
|
|
with `--online` or not: when they are missing it is listed in `network.readings`. `network.unmeasured` lists what `--online` could
|
|
not read, and `network.finality` names the chain each finality was read on, a testnet said as such (the summary says it too). The
|
|
console's Shield evaluation does not read a chain even with `--online`: a notarization claimed inside a sidecar bundle has to be
|
|
presented as its own `envelope` artifact.
|
|
|
|
**What it does not judge.** The truth of what an artifact asserts beyond what its verifier checks (a market timeline, a payment
|
|
receipt without the relying party's transaction readings, the findings of a compliance report), the honesty of the host that produced
|
|
a sidecar bundle (section 5), and any kind outside the table. `consola-web/index.html` shows the output (`--json`) domain by domain,
|
|
each artifact with its verifier and the evidence it rests on; a pack pasted there directly is shown as `UNCHECKED`. The view judges
|
|
nothing, but it never shows a verdict stronger than the output's own rows give (a domain no stronger than its weakest artifact, the
|
|
summary no stronger than its weakest required domain): an edited output that contradicts itself is shown at the weaker reading and
|
|
named. Everything that comes from the pack is shown as text.
|
|
|
|
## Tests
|
|
|
|
bash probele-b1.sh # all seven below, each VERDE / ROSU / STRICAT (in the development repository only)
|
|
node proba-plan-migrare.mjs # 44: real certificates and keys (openssl) through the real inventory; the scanner's ids derived from its source
|
|
node proba-control-plane.mjs # 10: the command line end to end, an all-post-quantum tree gives an empty plan
|
|
node proba-executa-migrare.mjs # 30: on real products started locally (KMS, PQ CA, gateway), with the attacks of the review
|
|
node proba-remediere.mjs # 33: real TLS servers on 127.0.0.1 scanned by the readiness scanner, before and after the recipe
|
|
node control-negativ-remediere.mjs # 7 of 7 guards broken in a copy, each turns the test red
|
|
node proba-raport-conformitate.mjs # 30: on the real outputs of the inventory and the scanner; the envelope through the AIP-23 verifier
|
|
node control-negativ-raport-conformitate.mjs # 3 plantings, each turns the named check red
|
|
node proba-consola.mjs # 8: a good bundle and four negative controls
|
|
node proba-consola-web.mjs # 26: the viewer in a real Chromium, phone (390 px) and desktop (1440 px); needs playwright-core and a Chromium
|
|
node control-negativ-consola-web.mjs # 8 defects planted in a copy of the page, each turns its named check red
|
|
node proba-organizatie.mjs # 74 (75 with --online): artifacts made by the real producers (the suite's vectors, checked against INDEX.json; a Shield state,
|
|
# a plan and a report made now), a good pack and one broken artifact per kind, and the attacks of the
|
|
# adversarial review of 2026-10-21; --online adds a row on testnet 28001
|
|
node control-negativ-organizatie.mjs # 45 of 45 guards removed in a copy, each turns its named row red
|
|
node proba-organizatie-web.mjs # 48: the organization view in a real Chromium, 390 px and 1440 px, edited outputs and text from the pack included
|
|
node control-negativ-organizatie-web.mjs # 20 defects planted in a copy of the page, each turns its named check red
|
|
|
|
The report test runs the envelope through the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools>`).
|
|
The counts above were measured on 2026-10-21 in the development repository and from a fresh clone of the public one (until
|
|
that day the planner and command-line lines said 30 and 9, counts from before rows were added to those two probes).
|
|
No third party has reviewed any of this.
|