| .. | ||
| adrese-private.mjs | ||
| proba-adrese-private.mjs | ||
| readiness-service.mjs | ||
| README.md | ||
Post-quantum readiness scanner
readiness-service.mjs measures, for one public hostname, whether its TLS edge is ready for post-quantum key exchange. It does not
estimate: it opens real connections and reports what the server did.
- a TLS 1.3 handshake with the usual classical groups: the baseline (protocol, cipher, group, the certificate and its chain);
- a TLS 1.3 handshake offering only
X25519MLKEM768(thenSecP256r1MLKEM768): does the server know the hybrid key exchange? - a TLS 1.3 handshake offering the hybrid group first, with classical fallbacks: does it prefer it? (OpenSSL does not name the hybrid group in the ephemeral key information, so the preference is inferred; the method control is that the classical baseline comes back with a named group, and without it no claim about preference is made);
- a TLS 1.2-only handshake: is a version without post-quantum key exchange still accepted?
- an HTTPS
HEAD /for HSTS.
From these it reports the harvest-now-decrypt-later exposure (no post-quantum key exchange: traffic recorded today can be read once a
cryptographically relevant quantum computer exists), classical certificate authentication (reported, not penalized: no public CA
issues post-quantum certificates yet), expiry, TLS 1.2, HSTS, a score and concrete recommendations. Every finding has a stable id;
the control plane's planner classifies exactly these ids, and its test derives them from this file.
It needs Node.js 24 with OpenSSL 3.5 or later (the ML-KEM groups) and refuses to start otherwise, instead of reporting "no post-quantum" about everyone.
As a service
PORT=8797 AERE_READINESS_FROM="your label" AERE_READINESS_DIR=/var/lib/aere/readiness node readiness-service.mjs
curl -s -H 'content-type: application/json' -d '{"domain":"example.com"}' http://127.0.0.1:8797/scan
It listens on 127.0.0.1 only; put your own proxy in front. Endpoints: POST /scan {"domain": ..., "fresh": true?}, GET /scan/<domain>
(the cached report), GET /health. Reports are cached for six hours; a failed scan is not cached.
- No connection to anything that is not proven public. A hostname that resolves, even partly, to a loopback, private, link-local,
CGNAT, documentation, multicast or reserved address (IPv4 or IPv6, including mapped and NAT64 forms) gets no connection, and the
answer does not name its addresses. The connections then go to the address that was checked, so the name is not resolved a second
time between the check and the connection (
adrese-private.mjs). - Rate limit per client: at most 12 scans per minute. The client is the value of the
x-aere-clientheader set by the front that calls the service (it listens on 127.0.0.1, so only something on the same host can set it), otherwise the connecting address.X-Forwarded-Foris never read: its first element is chosen by the client, and a limit keyed on it could be bypassed by changing it (fixed on 2026-09-29). - Bounded work: at most 4 scans at a time and 32 waiting; past that the answer is
503 busy, not an unbounded wait.
scaneaza(domain) and scaneazaAdresa(domain, address, { port }) can also be imported as functions; the second one has no
private-address guard (it is for callers that checked the target themselves, such as local tests on 127.0.0.1).
Tests
node proba-adrese-private.mjs # the private-address rules, and a local listener that must never be touched by a scan
The control plane's tests (../control-plane/) also run this scanner against real TLS servers on 127.0.0.1. The rate limit and the
queue bound are tested by the service that runs it in production (proba-client-real.mjs in the Aere Network repository, not
included here); here they are documented, not measured. No third party has reviewed it.