# 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. 1. a TLS 1.3 handshake with the usual classical groups: the baseline (protocol, cipher, group, the certificate and its chain); 2. a TLS 1.3 handshake offering **only** `X25519MLKEM768` (then `SecP256r1MLKEM768`): does the server know the hybrid key exchange? 3. 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); 4. a TLS 1.2-only handshake: is a version without post-quantum key exchange still accepted? 5. 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/` (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-client` header 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-For` is 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.